|
|
@@ -40,11 +40,19 @@ class CreateAgentRequest(BaseModel):
|
|
|
agent_dir: str = Field(default="", description="Agent template directory path")
|
|
|
agent_template: str = Field(default="", description="Agent template name (e.g. qaagent67wiki)")
|
|
|
instance_dir: str = Field(default="", description="Instance data directory for this agent")
|
|
|
+ kb_ids: list = Field(default_factory=list, description="Linked knowledge base IDs")
|
|
|
+ kb_search_mode: str = Field(default="bm25", description="bm25 | pageindex | fusion")
|
|
|
|
|
|
class UpdateAgentRequest(BaseModel):
|
|
|
config: Dict[str, Any]
|
|
|
changelog: str = ""
|
|
|
agent_dir: str = ""
|
|
|
+ kb_ids: Optional[list] = None
|
|
|
+ kb_search_mode: Optional[str] = None
|
|
|
+
|
|
|
+class UpdateKBsRequest(BaseModel):
|
|
|
+ kb_ids: list = Field(default_factory=list)
|
|
|
+ kb_search_mode: str = Field(default="bm25")
|
|
|
|
|
|
class RunRequest(BaseModel):
|
|
|
input: str = Field(..., max_length=102400) # 100KB max
|
|
|
@@ -110,11 +118,12 @@ async def create_agent(
|
|
|
now = now_utc()
|
|
|
db.execute(
|
|
|
"INSERT INTO agents (id, tenant_id, name, description, current_version, tags, environment, "
|
|
|
- "agent_dir, agent_template, instance_dir, status, created_at, updated_at) "
|
|
|
- "VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, 'active', ?, ?)",
|
|
|
+ "agent_dir, agent_template, instance_dir, kb_ids, kb_search_mode, status, created_at, updated_at) "
|
|
|
+ "VALUES (?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, 'active', ?, ?)",
|
|
|
(agent_id, tenant.tenant_id, req.name, req.description,
|
|
|
json.dumps(req.tags), req.environment,
|
|
|
- agent_dir, req.agent_template, instance_dir, now, now)
|
|
|
+ agent_dir, req.agent_template, instance_dir,
|
|
|
+ json.dumps(req.kb_ids), req.kb_search_mode, now, now)
|
|
|
)
|
|
|
db.execute(
|
|
|
"INSERT INTO agent_versions (agent_id, version, config, config_hash, changelog, created_by, created_at) "
|
|
|
@@ -149,6 +158,7 @@ async def list_agents(
|
|
|
agents = db.fetchall(sql, tuple(params))
|
|
|
for a in agents:
|
|
|
a["tags"] = json.loads(a.get("tags", "[]"))
|
|
|
+ a["kb_ids"] = json.loads(a.get("kb_ids") or "[]")
|
|
|
return {"agents": agents, "count": len(agents)}
|
|
|
|
|
|
|
|
|
@@ -171,6 +181,7 @@ async def get_agent(
|
|
|
(agent_id, agent["current_version"])
|
|
|
)
|
|
|
agent["tags"] = json.loads(agent.get("tags", "[]"))
|
|
|
+ agent["kb_ids"] = json.loads(agent.get("kb_ids") or "[]")
|
|
|
agent["config"] = json.loads(version["config"]) if version else {}
|
|
|
return agent
|
|
|
|
|
|
@@ -216,10 +227,16 @@ async def update_agent(
|
|
|
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
|
(agent_id, new_version, config_json, config_hash, req.changelog, tenant.user_id, now)
|
|
|
)
|
|
|
- db.execute(
|
|
|
- "UPDATE agents SET current_version = ?, agent_dir = ?, updated_at = ? WHERE id = ?",
|
|
|
- (new_version, req.agent_dir, now, agent_id)
|
|
|
- )
|
|
|
+ update_fields = "current_version = ?, agent_dir = ?, updated_at = ?"
|
|
|
+ update_vals = [new_version, req.agent_dir, now]
|
|
|
+ if req.kb_ids is not None:
|
|
|
+ update_fields += ", kb_ids = ?"
|
|
|
+ update_vals.append(json.dumps(req.kb_ids))
|
|
|
+ if req.kb_search_mode is not None:
|
|
|
+ update_fields += ", kb_search_mode = ?"
|
|
|
+ update_vals.append(req.kb_search_mode)
|
|
|
+ update_vals.append(agent_id)
|
|
|
+ db.execute(f"UPDATE agents SET {update_fields} WHERE id = ?", tuple(update_vals))
|
|
|
db.commit()
|
|
|
|
|
|
return {"agent_id": agent_id, "version": new_version, "updated_at": now}
|
|
|
@@ -337,11 +354,20 @@ async def run_agent(
|
|
|
instance_dir = agent.get("instance_dir", "") or ""
|
|
|
agent_dir = instance_dir or agent.get("agent_dir", "") or ""
|
|
|
|
|
|
+ # KB context injection: enrich input with top-K retrieved passages
|
|
|
+ kb_ids = json.loads(agent.get("kb_ids") or "[]")
|
|
|
+ kb_search_mode = agent.get("kb_search_mode") or "bm25"
|
|
|
+ enriched_input = req.input
|
|
|
+ if kb_ids:
|
|
|
+ kb_ctx = _build_kb_context(db, kb_ids, req.input, search_mode=kb_search_mode)
|
|
|
+ if kb_ctx:
|
|
|
+ enriched_input = f"{kb_ctx}\n\n[用户问题]\n{req.input}"
|
|
|
+
|
|
|
# Execute via lambdagent
|
|
|
t0 = time.time()
|
|
|
try:
|
|
|
result, trace_info = _execute_agent(
|
|
|
- config, req.input, agent_dir=agent_dir, run_id=run_id,
|
|
|
+ config, enriched_input, agent_dir=agent_dir, run_id=run_id,
|
|
|
)
|
|
|
duration_ms = int((time.time() - t0) * 1000)
|
|
|
workspace_path = trace_info.get("workspace_path", "")
|
|
|
@@ -431,6 +457,15 @@ async def run_agent_stream(
|
|
|
instance_dir = agent.get("instance_dir", "") or ""
|
|
|
agent_dir = instance_dir or agent.get("agent_dir", "") or ""
|
|
|
|
|
|
+ # KB context injection for streaming run
|
|
|
+ kb_ids = json.loads(agent.get("kb_ids") or "[]")
|
|
|
+ kb_search_mode = agent.get("kb_search_mode") or "bm25"
|
|
|
+ stream_input = req.input
|
|
|
+ if kb_ids:
|
|
|
+ kb_ctx = _build_kb_context(db, kb_ids, req.input, search_mode=kb_search_mode)
|
|
|
+ if kb_ctx:
|
|
|
+ stream_input = f"{kb_ctx}\n\n[用户问题]\n{req.input}"
|
|
|
+
|
|
|
# Create run record before streaming starts
|
|
|
run_id = gen_id("run_")
|
|
|
now = now_utc()
|
|
|
@@ -452,7 +487,7 @@ async def run_agent_stream(
|
|
|
def _run():
|
|
|
try:
|
|
|
result, trace_info = _execute_agent(
|
|
|
- config, req.input, on_step=event_queue.put,
|
|
|
+ config, stream_input, on_step=event_queue.put,
|
|
|
agent_dir=agent_dir, run_id=run_id,
|
|
|
)
|
|
|
duration_ms = int((time.time() - t0) * 1000)
|
|
|
@@ -695,6 +730,140 @@ async def delete_agent_memory(
|
|
|
return {"deleted": key}
|
|
|
|
|
|
|
|
|
+# ── Agent-KB Linking API ──
|
|
|
+
|
|
|
+@router.get("/{agent_id}/knowledge")
|
|
|
+async def get_agent_knowledge(
|
|
|
+ agent_id: str,
|
|
|
+ tenant: TenantContext = Depends(get_tenant),
|
|
|
+ db: Database = Depends(get_database),
|
|
|
+):
|
|
|
+ """List knowledge bases linked to this agent, with their index status."""
|
|
|
+ require_permission(tenant, "agents:read")
|
|
|
+ agent = db.fetchone(
|
|
|
+ "SELECT kb_ids, kb_search_mode FROM agents WHERE id = ? AND tenant_id = ?",
|
|
|
+ (agent_id, tenant.tenant_id)
|
|
|
+ )
|
|
|
+ if not agent:
|
|
|
+ api_error(404, "AGENT_NOT_FOUND", f"Agent {agent_id} not found")
|
|
|
+ kb_ids = json.loads(agent.get("kb_ids") or "[]")
|
|
|
+ kbs = []
|
|
|
+ for kb_id in kb_ids:
|
|
|
+ kb = db.fetchone("SELECT * FROM knowledge_bases WHERE id = ?", (kb_id,))
|
|
|
+ if kb:
|
|
|
+ kbs.append(dict(kb))
|
|
|
+ return {
|
|
|
+ "kb_ids": kb_ids,
|
|
|
+ "kb_search_mode": agent.get("kb_search_mode", "bm25"),
|
|
|
+ "knowledge_bases": kbs,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@router.put("/{agent_id}/knowledge")
|
|
|
+async def update_agent_knowledge(
|
|
|
+ agent_id: str,
|
|
|
+ req: UpdateKBsRequest,
|
|
|
+ tenant: TenantContext = Depends(get_tenant),
|
|
|
+ db: Database = Depends(get_database),
|
|
|
+):
|
|
|
+ """Replace the list of knowledge bases linked to this agent."""
|
|
|
+ require_permission(tenant, "agents:write")
|
|
|
+ if not db.fetchone(
|
|
|
+ "SELECT id FROM agents WHERE id = ? AND tenant_id = ?",
|
|
|
+ (agent_id, tenant.tenant_id)
|
|
|
+ ):
|
|
|
+ api_error(404, "AGENT_NOT_FOUND", f"Agent {agent_id} not found")
|
|
|
+ # Validate that all kb_ids exist (for this tenant)
|
|
|
+ for kb_id in req.kb_ids:
|
|
|
+ if not db.fetchone("SELECT id FROM knowledge_bases WHERE id = ? AND tenant_id = ?",
|
|
|
+ (kb_id, tenant.tenant_id)):
|
|
|
+ api_error(404, "KB_NOT_FOUND", f"Knowledge base {kb_id} not found")
|
|
|
+ db.execute(
|
|
|
+ "UPDATE agents SET kb_ids = ?, kb_search_mode = ?, updated_at = ? WHERE id = ?",
|
|
|
+ (json.dumps(req.kb_ids), req.kb_search_mode, now_utc(), agent_id)
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+ return {"kb_ids": req.kb_ids, "kb_search_mode": req.kb_search_mode}
|
|
|
+
|
|
|
+
|
|
|
+# ── KB Context Injection (RAG helper for agent execution) ──
|
|
|
+
|
|
|
+def _build_kb_context(
|
|
|
+ db: "Database",
|
|
|
+ kb_ids: list,
|
|
|
+ query: str,
|
|
|
+ search_mode: str = "bm25",
|
|
|
+ top_k: int = 5,
|
|
|
+) -> str:
|
|
|
+ """Search linked KBs and return a formatted context block.
|
|
|
+
|
|
|
+ Tries pageindex first (fast, in-process), then falls back to BM25
|
|
|
+ subprocess. Returns empty string when nothing useful is found.
|
|
|
+ """
|
|
|
+ if not kb_ids or not query.strip():
|
|
|
+ return ""
|
|
|
+
|
|
|
+ all_results: list = []
|
|
|
+ for kb_id in kb_ids:
|
|
|
+ kb = db.fetchone("SELECT * FROM knowledge_bases WHERE id = ?", (kb_id,))
|
|
|
+ if not kb:
|
|
|
+ continue
|
|
|
+ kb_root = kb.get("root_dir", "")
|
|
|
+ if not kb_root:
|
|
|
+ continue
|
|
|
+ kb_name = kb.get("name", kb_id)
|
|
|
+
|
|
|
+ results: list = []
|
|
|
+ try:
|
|
|
+ from pathlib import Path as _Path
|
|
|
+ from agentpaas.api.v1.knowledge import (
|
|
|
+ _page_index_search,
|
|
|
+ _find_scripts_dir,
|
|
|
+ _search_subprocess,
|
|
|
+ )
|
|
|
+ page_idx_exists = (_Path(kb_root) / "rag_page_index.json").exists()
|
|
|
+
|
|
|
+ if search_mode == "pageindex" and page_idx_exists:
|
|
|
+ results = _page_index_search(kb_root, query, top_k)
|
|
|
+ else:
|
|
|
+ # BM25 subprocess
|
|
|
+ try:
|
|
|
+ sd = _find_scripts_dir(_Path(kb_root))
|
|
|
+ if sd:
|
|
|
+ results = _search_subprocess(kb_root, sd, "bm25", query, top_k)
|
|
|
+ except Exception:
|
|
|
+ pass
|
|
|
+ # Fallback to pageindex
|
|
|
+ if not results and page_idx_exists:
|
|
|
+ results = _page_index_search(kb_root, query, top_k)
|
|
|
+ except Exception:
|
|
|
+ continue
|
|
|
+
|
|
|
+ for r in results:
|
|
|
+ r["_kb_name"] = kb_name
|
|
|
+ all_results.append(r)
|
|
|
+
|
|
|
+ if not all_results:
|
|
|
+ return ""
|
|
|
+
|
|
|
+ # Sort by score descending (results may come from multiple KBs)
|
|
|
+ all_results.sort(key=lambda x: -(x.get("score") or 0))
|
|
|
+
|
|
|
+ lines = ["[知识库检索结果]"]
|
|
|
+ for i, r in enumerate(all_results[:top_k], 1):
|
|
|
+ src = r.get("_kb_name", "")
|
|
|
+ doc = r.get("source", "") or r.get("doc_id", "")
|
|
|
+ if doc:
|
|
|
+ src += f" / {doc}"
|
|
|
+ pg = r.get("page")
|
|
|
+ if pg:
|
|
|
+ src += f" 第{pg}页"
|
|
|
+ text = (r.get("text") or r.get("chunk_text") or r.get("content") or "")[:600].strip()
|
|
|
+ lines.append(f"\n[来源{i}: {src}]\n{text}")
|
|
|
+
|
|
|
+ return "\n".join(lines)
|
|
|
+
|
|
|
+
|
|
|
# ── Persistent Memory API (Layer 1 Core + Layer 2 Recall) ──
|
|
|
|
|
|
def _get_agent_dir(agent_id: str, tenant_id: str, db: "Database") -> str:
|