| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- """
- 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
- def test_resolve_tenant_prefers_busiest(fresh_db, tmp_path):
- """多租户老库:示例库应归属活跃智能体最多的租户(浏览器会话所在地),
- 而非最老租户或 config.json 租户。"""
- from agentpaas.db.models import gen_id, now_utc
- from agentpaas.engine.sample_kb import ensure_sample_kb, SAMPLE_KB_NAME
- db, tid_old = fresh_db
- # 第二个租户,拥有 2 个活跃智能体(模拟日常使用租户)
- tid_busy = gen_id("tn_")
- now = now_utc()
- db.execute("INSERT INTO tenants (id, name, plan, status, created_at) "
- "VALUES (?, 'busy', 'free', 'active', ?)", (tid_busy, now))
- for i in range(2):
- db.execute(
- "INSERT INTO agents (id, tenant_id, name, current_version, status, "
- "created_at, updated_at) VALUES (?, ?, ?, 1, 'active', ?, ?)",
- (gen_id("ag_"), tid_busy, f"a{i}", now, now))
- db.commit()
- ensure_sample_kb(str(tmp_path))
- kb = db.fetchone("SELECT tenant_id FROM knowledge_bases WHERE name = ?",
- (SAMPLE_KB_NAME,))
- assert kb["tenant_id"] == tid_busy, "应归属智能体最多的租户"
|