feat: 向量记忆 RAG、工具市场、SSE 流式响应、前端集成与测试覆盖

- 新增 embedding_service(语义检索)、knowledge_service(RAG)、text_chunker、document_parser
- 新增 tool_registry(自定义工具注册表)并完善工具市场 API(CRUD + code/http 执行)
- 新增 agent_vector_memory / knowledge_base 模型及对应数据库表
- 实现 SSE 流式响应与 Agent 预算控制
- AgentChat.vue 集成 MainLayout 导航布局
- 完善测试体系:7 个新测试文件共 110 个测试覆盖
- 修复 conftest.py SQLite 内存数据库连接隔离问题

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
renjianbo
2026-05-01 22:30:46 +08:00
parent 036f533881
commit 7b9e0826de
35 changed files with 4353 additions and 365 deletions

View File

@@ -8,8 +8,10 @@ POST /api/v1/agent-chat/bare
from __future__ import annotations
import logging
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException
import json
from typing import Any, AsyncGenerator, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from app.core.database import get_db
@@ -23,6 +25,7 @@ from app.agent_runtime import (
AgentConfig,
AgentLLMConfig,
AgentToolConfig,
AgentBudgetConfig,
AgentStep,
AgentOrchestrator,
OrchestratorAgentConfig,
@@ -64,6 +67,14 @@ def _make_llm_logger(
return _log
async def _sse_stream(gen: AsyncGenerator[dict, None]) -> AsyncGenerator[str, None]:
"""将 run_stream 生成的 dict 事件格式化为 SSE 文本流。"""
async for event in gen:
event_type = event.get("type", "message")
data = {k: v for k, v in event.items() if k != "type"}
yield f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
class ChatRequest(BaseModel):
message: str
session_id: Optional[str] = None
@@ -205,6 +216,39 @@ async def chat_bare(
)
@router.post("/bare/stream")
async def chat_bare_stream(
req: ChatRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""无需 Agent 配置,使用默认设置直接对话(流式 SSE"""
config = AgentConfig(
name="bare_agent",
system_prompt="你是一个有用的AI助手。请使用可用工具来帮助用户完成任务。",
llm=AgentLLMConfig(
model=req.model or (
"gpt-4o-mini" if settings.OPENAI_API_KEY and settings.OPENAI_API_KEY != "your-openai-api-key"
else "deepseek-v4-flash"
),
temperature=req.temperature or 0.7,
max_iterations=req.max_iterations or 10,
),
user_id=current_user.id,
)
on_llm_call = _make_llm_logger(db, agent_id=None, user_id=current_user.id)
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
return StreamingResponse(
_sse_stream(runtime.run_stream(req.message)),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
@router.post("/{agent_id}", response_model=ChatResponse)
async def chat_with_agent(
agent_id: str,
@@ -225,9 +269,25 @@ async def chat_with_agent(
# 查找 agent 节点的配置(或第一个 llm 节点的配置)
agent_node_cfg = _find_agent_node_config(nodes)
# 构建 system prompt并自动注入智能体名称
system_prompt = agent_node_cfg.get("system_prompt") or agent.description or "你是一个有用的AI助手。"
if agent.name:
name_prefix = f"你的名字是{agent.name}"
if name_prefix not in system_prompt:
system_prompt = f"{name_prefix}\n\n{system_prompt}"
# 合并执行预算Agent.budget_config 覆盖默认值
budget = AgentBudgetConfig()
if agent.budget_config and isinstance(agent.budget_config, dict):
bc = agent.budget_config
if "max_llm_invocations" in bc and bc["max_llm_invocations"] is not None:
budget.max_llm_invocations = max(1, int(bc["max_llm_invocations"]))
if "max_tool_calls" in bc and bc["max_tool_calls"] is not None:
budget.max_tool_calls = max(1, int(bc["max_tool_calls"]))
config = AgentConfig(
name=agent.name,
system_prompt=agent_node_cfg.get("system_prompt") or agent.description or "你是一个有用的AI助手。",
system_prompt=system_prompt,
llm=AgentLLMConfig(
provider=agent_node_cfg.get("provider", "openai"),
model=req.model or agent_node_cfg.get("model", "gpt-4o-mini"),
@@ -238,6 +298,7 @@ async def chat_with_agent(
include_tools=agent_node_cfg.get("tools", []),
exclude_tools=agent_node_cfg.get("exclude_tools", []),
),
budget=budget,
user_id=current_user.id,
)
@@ -256,6 +317,68 @@ async def chat_with_agent(
)
@router.post("/{agent_id}/stream")
async def chat_with_agent_stream(
agent_id: str,
req: ChatRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
"""与指定的 Agent 对话(流式 SSE"""
agent = db.query(Agent).filter(Agent.id == agent_id).first()
if not agent:
raise HTTPException(status_code=404, detail="Agent 不存在")
if agent.user_id and agent.user_id != current_user.id and current_user.role != "admin":
raise HTTPException(status_code=403, detail="无权访问该 Agent")
wc = agent.workflow_config or {}
nodes = wc.get("nodes", [])
agent_node_cfg = _find_agent_node_config(nodes)
system_prompt = agent_node_cfg.get("system_prompt") or agent.description or "你是一个有用的AI助手。"
if agent.name:
name_prefix = f"你的名字是{agent.name}"
if name_prefix not in system_prompt:
system_prompt = f"{name_prefix}\n\n{system_prompt}"
budget = AgentBudgetConfig()
if agent.budget_config and isinstance(agent.budget_config, dict):
bc = agent.budget_config
if "max_llm_invocations" in bc and bc["max_llm_invocations"] is not None:
budget.max_llm_invocations = max(1, int(bc["max_llm_invocations"]))
if "max_tool_calls" in bc and bc["max_tool_calls"] is not None:
budget.max_tool_calls = max(1, int(bc["max_tool_calls"]))
config = AgentConfig(
name=agent.name,
system_prompt=system_prompt,
llm=AgentLLMConfig(
provider=agent_node_cfg.get("provider", "openai"),
model=req.model or agent_node_cfg.get("model", "gpt-4o-mini"),
temperature=req.temperature or float(agent_node_cfg.get("temperature", 0.7)),
max_iterations=req.max_iterations or int(agent_node_cfg.get("max_iterations", 10)),
),
tools=AgentToolConfig(
include_tools=agent_node_cfg.get("tools", []),
exclude_tools=agent_node_cfg.get("exclude_tools", []),
),
budget=budget,
user_id=current_user.id,
)
on_llm_call = _make_llm_logger(db, agent_id=agent_id, user_id=current_user.id)
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
return StreamingResponse(
_sse_stream(runtime.run_stream(req.message)),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
},
)
def _find_agent_node_config(nodes: list) -> Dict[str, Any]:
"""从工作流节点列表中查找第一个 agent 类型或 llm 类型的节点配置。"""
if not nodes: