|
@@ -0,0 +1,194 @@
|
|
|
|
|
+"""
|
|
|
|
|
+tests/test_sample_kb.py — 示例知识库种子 + 平台 KB 工具桥接(KB 调用链核查修复)。
|
|
|
|
|
+
|
|
|
|
|
+覆盖:
|
|
|
|
|
+1. ensure_sample_kb: 建库/登记/索引、幂等、无租户时延迟。
|
|
|
|
|
+2. _make_platform_kb_tools: KBSearch 查到挂接库内容;KBList 列出库;
|
|
|
|
|
+ 无挂接时返回 {}(不注入,保留 lambdagent 内置行为)。
|
|
|
|
|
+3. compiler 工具合并: subAgents 的 call_* 与宿主注入的自定义工具共存
|
|
|
|
|
+ (原来注入任何 tools 都会让 subAgents 整段跳过)。
|
|
|
|
|
+"""
|
|
|
|
|
+from __future__ import annotations
|
|
|
|
|
+
|
|
|
|
|
+import json
|
|
|
|
|
+import os
|
|
|
|
|
+
|
|
|
|
|
+import pytest
|
|
|
|
|
+
|
|
|
|
|
+os.environ.setdefault("AGENTPAAS_DATABASE_URL", "sqlite:///:memory:")
|
|
|
|
|
+os.environ.setdefault("AGENTPAAS_TESTING", "1")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@pytest.fixture()
|
|
|
|
|
+def fresh_db(monkeypatch):
|
|
|
|
|
+ """独立内存 DB + 一个活跃租户。"""
|
|
|
|
|
+ from agentpaas.db.models import Database, gen_id, now_utc
|
|
|
|
|
+ import agentpaas.db.session as _session_mod
|
|
|
|
|
+
|
|
|
|
|
+ prev = _session_mod._db
|
|
|
|
|
+ _session_mod._db = Database("sqlite:///:memory:")
|
|
|
|
|
+ db = _session_mod._db
|
|
|
|
|
+ tid = gen_id("tn_")
|
|
|
|
|
+ db.execute(
|
|
|
|
|
+ "INSERT INTO tenants (id, name, plan, status, created_at) "
|
|
|
|
|
+ "VALUES (?, 'test', 'free', 'active', ?)", (tid, now_utc()))
|
|
|
|
|
+ db.commit()
|
|
|
|
|
+ yield db, tid
|
|
|
|
|
+ _session_mod._db = prev
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_sample_kb_created_and_indexed(fresh_db, tmp_path):
|
|
|
|
|
+ from agentpaas.engine.sample_kb import ensure_sample_kb, SAMPLE_KB_NAME
|
|
|
|
|
+
|
|
|
|
|
+ db, tid = fresh_db
|
|
|
|
|
+ ensure_sample_kb(str(tmp_path))
|
|
|
|
|
+
|
|
|
|
|
+ kb = db.fetchone("SELECT * FROM knowledge_bases WHERE name = ?", (SAMPLE_KB_NAME,))
|
|
|
|
|
+ assert kb, "示例知识库应被创建"
|
|
|
|
|
+ assert kb["tenant_id"] == tid
|
|
|
|
|
+
|
|
|
|
|
+ # 文件已拷贝并登记
|
|
|
|
|
+ files = db.fetchall("SELECT * FROM kb_files WHERE kb_id = ?", (kb["id"],))
|
|
|
|
|
+ assert len(files) >= 4, files
|
|
|
|
|
+ names = {f["file_name"] for f in files}
|
|
|
|
|
+ assert "知识库使用指南.md" in names
|
|
|
|
|
+
|
|
|
|
|
+ # pageindex 已生成且可检索
|
|
|
|
|
+ idx = os.path.join(kb["root_dir"], "rag_page_index.json")
|
|
|
|
|
+ assert os.path.isfile(idx)
|
|
|
|
|
+ entries = json.loads(open(idx, encoding="utf-8").read())
|
|
|
|
|
+ assert len(entries) > 10 # 按 ## 小节切分
|
|
|
|
|
+
|
|
|
|
|
+ from agentpaas.api.v1.knowledge import _page_index_search
|
|
|
|
|
+ hits = _page_index_search(kb["root_dir"], "顺序表 插入 删除", top_k=3)
|
|
|
|
|
+ assert hits and "顺序" in (hits[0].get("text", "") + hits[0].get("summary", ""))
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_sample_kb_idempotent(fresh_db, tmp_path):
|
|
|
|
|
+ from agentpaas.engine.sample_kb import ensure_sample_kb, SAMPLE_KB_NAME
|
|
|
|
|
+
|
|
|
|
|
+ db, _ = fresh_db
|
|
|
|
|
+ ensure_sample_kb(str(tmp_path))
|
|
|
|
|
+ ensure_sample_kb(str(tmp_path)) # 再跑一次
|
|
|
|
|
+ rows = db.fetchall("SELECT id FROM knowledge_bases WHERE name = ?", (SAMPLE_KB_NAME,))
|
|
|
|
|
+ assert len(rows) == 1, "幂等:不应重复创建"
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_sample_kb_defers_without_tenant(tmp_path, monkeypatch):
|
|
|
|
|
+ from agentpaas.db.models import Database
|
|
|
|
|
+ import agentpaas.db.session as _session_mod
|
|
|
|
|
+ from agentpaas.engine.sample_kb import ensure_sample_kb, SAMPLE_KB_NAME
|
|
|
|
|
+
|
|
|
|
|
+ prev = _session_mod._db
|
|
|
|
|
+ _session_mod._db = Database("sqlite:///:memory:") # 无租户
|
|
|
|
|
+ try:
|
|
|
|
|
+ ensure_sample_kb(str(tmp_path))
|
|
|
|
|
+ kb = _session_mod._db.fetchone(
|
|
|
|
|
+ "SELECT id FROM knowledge_bases WHERE name = ?", (SAMPLE_KB_NAME,))
|
|
|
|
|
+ assert kb is None, "无租户时应延迟到下次启动"
|
|
|
|
|
+ finally:
|
|
|
|
|
+ _session_mod._db = prev
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ── 平台 KB 工具桥接 ────────────────────────────────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def test_platform_kb_tools_search_and_list(fresh_db, tmp_path):
|
|
|
|
|
+ from agentpaas.engine.sample_kb import ensure_sample_kb, SAMPLE_KB_NAME
|
|
|
|
|
+ from agentpaas.api.v1.agents import _make_platform_kb_tools
|
|
|
|
|
+
|
|
|
|
|
+ db, tid = fresh_db
|
|
|
|
|
+ ensure_sample_kb(str(tmp_path))
|
|
|
|
|
+ kb = db.fetchone("SELECT * FROM knowledge_bases WHERE name = ?", (SAMPLE_KB_NAME,))
|
|
|
|
|
+
|
|
|
|
|
+ tools = _make_platform_kb_tools(db, [kb["id"]], tid, search_mode="pageindex")
|
|
|
|
|
+ assert set(tools) == {"KBSearch", "KBList"}
|
|
|
|
|
+
|
|
|
|
|
+ # KBSearch: JSON 入参与裸字符串都支持
|
|
|
|
|
+ out = tools["KBSearch"]('{"query": "循环队列 队满"}')
|
|
|
|
|
+ assert "知识库检索结果" in out and "队" in out
|
|
|
|
|
+ out2 = tools["KBSearch"]("栈 后进先出")
|
|
|
|
|
+ assert "知识库检索结果" in out2
|
|
|
|
|
+
|
|
|
|
|
+ # 查不到 → NO_MATCH 提示而非空串
|
|
|
|
|
+ miss = tools["KBSearch"]("量子引力 弦论")
|
|
|
|
|
+ assert "NO_MATCH" in miss or "知识库检索结果" in miss
|
|
|
|
|
+
|
|
|
|
|
+ # KBList 列出挂接库与文件数
|
|
|
|
|
+ lst = tools["KBList"]("")
|
|
|
|
|
+ assert SAMPLE_KB_NAME in lst and "文件" in lst
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_platform_kb_tools_empty_when_no_kb(fresh_db):
|
|
|
|
|
+ from agentpaas.api.v1.agents import _make_platform_kb_tools
|
|
|
|
|
+ db, tid = fresh_db
|
|
|
|
|
+ assert _make_platform_kb_tools(db, [], tid) == {}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_platform_kb_tools_tenant_scoped(fresh_db, tmp_path):
|
|
|
|
|
+ """别的租户挂了这个 kb_id 也查不到(audit #30 语义在工具层同样成立)。"""
|
|
|
|
|
+ from agentpaas.engine.sample_kb import ensure_sample_kb, SAMPLE_KB_NAME
|
|
|
|
|
+ from agentpaas.api.v1.agents import _make_platform_kb_tools
|
|
|
|
|
+
|
|
|
|
|
+ db, tid = fresh_db
|
|
|
|
|
+ ensure_sample_kb(str(tmp_path))
|
|
|
|
|
+ kb = db.fetchone("SELECT * FROM knowledge_bases WHERE name = ?", (SAMPLE_KB_NAME,))
|
|
|
|
|
+
|
|
|
|
|
+ tools = _make_platform_kb_tools(db, [kb["id"]], "tn_other_tenant", "pageindex")
|
|
|
|
|
+ out = tools["KBSearch"]("顺序表")
|
|
|
|
|
+ assert "NO_MATCH" in out # 跨租户被静默跳过 → 查无结果
|
|
|
|
|
+ assert SAMPLE_KB_NAME not in tools["KBList"]("")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+# ── compiler 工具合并(subAgents 与注入工具共存)────────────────────────────
|
|
|
|
|
+
|
|
|
|
|
+def test_compiler_merges_injected_tools_with_subagents():
|
|
|
|
|
+ from lambdagent.fromconfig.compiler import build_agent
|
|
|
|
|
+
|
|
|
|
|
+ cfg = {
|
|
|
|
|
+ "name": "orch", "type": "react",
|
|
|
|
|
+ "model": {"name": "ollama/qwen2.5:7b"},
|
|
|
|
|
+ "systemPrompt": "test",
|
|
|
|
|
+ "react": {"maxSteps": 3},
|
|
|
|
|
+ "subAgents": {
|
|
|
|
|
+ "helper": {"inline": {"type": "simple",
|
|
|
|
|
+ "model": {"name": "ollama/qwen2.5:7b"},
|
|
|
|
|
+ "systemPrompt": "sub"},
|
|
|
|
|
+ "tool": "call_helper"},
|
|
|
|
|
+ },
|
|
|
|
|
+ "mcp": {"localTools": ["KBSearch", "terminate"]},
|
|
|
|
|
+ }
|
|
|
|
|
+ sentinel = {"value": None}
|
|
|
|
|
+
|
|
|
|
|
+ def fake_kb_search(x):
|
|
|
|
|
+ sentinel["value"] = x
|
|
|
|
|
+ return "[平台KB] hit"
|
|
|
|
|
+
|
|
|
|
|
+ term = build_agent(cfg, {"tools": {"KBSearch": fake_kb_search}})
|
|
|
|
|
+ assert term is not None
|
|
|
|
|
+ # 注入的 KBSearch 生效(而非 lambdagent 内置实现)
|
|
|
|
|
+ from lambdagent.fromconfig.compiler import _compile_tools # noqa: F401
|
|
|
|
|
+ # 通过重新编译工具表来断言合并语义
|
|
|
|
|
+ tools = {}
|
|
|
|
|
+ from lambdagent.fromconfig import compiler as _c
|
|
|
|
|
+ merged = {** _c._compile_sub_agents(cfg, cfg["subAgents"], {}),
|
|
|
|
|
+ **{"KBSearch": fake_kb_search}}
|
|
|
|
|
+ assert "call_helper" in merged and merged["KBSearch"] is fake_kb_search
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def test_page_index_search_chinese_query(fresh_db, tmp_path):
|
|
|
|
|
+ """中文检索回归:原 \\W+ 分词把整句中文当一个 token → 全 0 分乱序。
|
|
|
|
|
+ 现在 CJK bigram 切分,长中文问句应命中正确小节。"""
|
|
|
|
|
+ from agentpaas.engine.sample_kb import ensure_sample_kb, SAMPLE_KB_NAME
|
|
|
|
|
+ from agentpaas.api.v1.knowledge import _page_index_search
|
|
|
|
|
+
|
|
|
|
|
+ db, _ = fresh_db
|
|
|
|
|
+ ensure_sample_kb(str(tmp_path))
|
|
|
|
|
+ kb = db.fetchone("SELECT * FROM knowledge_bases WHERE name = ?", (SAMPLE_KB_NAME,))
|
|
|
|
|
+
|
|
|
|
|
+ hits = _page_index_search(kb["root_dir"], "循环队列怎么判断队满?讲义里怎么说的", 3)
|
|
|
|
|
+ assert hits and hits[0]["score"] > 0, "中文长句必须有非零得分"
|
|
|
|
|
+ assert "栈与队列" in hits[0]["source"], f"应命中第3章, got {hits[0]['source']}"
|
|
|
|
|
+
|
|
|
|
|
+ # 英文 query 不受影响
|
|
|
|
|
+ hits_en = _page_index_search(kb["root_dir"], "ADT stack push pop", 3)
|
|
|
|
|
+ assert hits_en and hits_en[0]["score"] > 0
|