From ccd6e8ef4da9bdd14d730274a1d9342218d6899d Mon Sep 17 00:00:00 2001 From: bruce Date: Thu, 11 Jun 2026 00:09:54 +0800 Subject: [PATCH] =?UTF-8?q?chore(master):=20=E6=B8=85=E7=90=86V2=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E5=90=8E=E7=9A=84=E6=97=A7=E7=89=88=E9=81=97=E7=95=99?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 18 - AGENTS.md | 172 ------ Dockerfile | 15 - agent_core/__init__.py | 1 - agent_core/llm_provider.py | 201 ------ agent_core/orchestrator.py | 153 ----- agent_core/rag/__init__.py | 1 - agent_core/rag/chroma_store.py | 104 ---- agent_core/rag/ingest.py | 171 ----- agent_core/rag/retriever.py | 105 ---- agent_core/results.py | 22 - agent_core/schemas/__init__.py | 1 - agent_core/schemas/outputs.py | 7 - agent_core/structured_output.py | 142 ----- agent_core/tool_registry.py | 70 --- agent_core/tools/__init__.py | 1 - agent_core/tools/builtin_tools.py | 124 ---- apps/__init__.py | 1 - apps/audit/__init__.py | 1 - apps/audit/admin.py | 19 - apps/audit/apps.py | 7 - apps/audit/migrations/0001_initial.py | 46 -- .../migrations/0002_demobusinessrecord.py | 35 -- apps/audit/migrations/__init__.py | 0 apps/audit/models.py | 68 -- apps/audit/services.py | 57 -- apps/audit/urls.py | 12 - apps/audit/views.py | 26 - apps/chat/__init__.py | 1 - apps/chat/apps.py | 7 - apps/chat/forms.py | 40 -- apps/chat/urls.py | 11 - apps/chat/views.py | 60 -- apps/documents/__init__.py | 1 - apps/documents/admin.py | 11 - apps/documents/apps.py | 7 - apps/documents/forms.py | 37 -- apps/documents/migrations/0001_initial.py | 42 -- apps/documents/migrations/__init__.py | 0 apps/documents/models.py | 38 -- apps/documents/services.py | 127 ---- apps/documents/urls.py | 13 - apps/documents/views.py | 43 -- apps/scenarios/__init__.py | 1 - apps/scenarios/apps.py | 7 - apps/scenarios/services.py | 110 ---- apps/scenarios/urls.py | 10 - apps/scenarios/views.py | 16 - configs/document_review.yaml | 22 - configs/knowledge_qa.yaml | 22 - configs/quality_analysis.yaml | 23 - configs/risk_audit.yaml | 24 - configs/ticket_assistant.yaml | 23 - docker-compose.yml | 10 - .../plans/2026-05-29-V1-Django基线实现计划.md | 343 ----------- docs/设计文档/0.设计文档索引.md | 71 --- docs/设计文档/1.智能体总体设计.md | 211 ------- docs/设计文档/2.功能流程设计.md | 169 ----- docs/设计文档/3.数据库设计.md | 144 ----- docs/设计文档/4.页面与路由设计.md | 179 ------ docs/设计文档/5.部署设计.md | 111 ---- docs/设计文档/模块设计/1.配置模块详细设计.md | 112 ---- docs/设计文档/模块设计/2.场景模块详细设计.md | 134 ---- docs/设计文档/模块设计/3.文档模块详细设计.md | 127 ---- docs/设计文档/模块设计/4.对话模块详细设计.md | 118 ---- docs/设计文档/模块设计/5.审计模块详细设计.md | 121 ---- .../模块设计/6.智能核心模块详细设计.md | 259 -------- docs/需求分析/1.V1总需求文档.md | 583 ------------------ docs/需求分析/2.模块需求索引.md | 85 --- docs/需求分析/3.配置模块需求.md | 115 ---- docs/需求分析/4.场景模块需求.md | 143 ----- docs/需求分析/5.文档模块需求.md | 132 ---- docs/需求分析/6.对话模块需求.md | 129 ---- docs/需求分析/7.审计模块需求.md | 143 ----- docs/需求分析/8.智能核心模块需求.md | 225 ------- templates/audit/log_detail.html | 61 -- templates/audit/log_list.html | 50 -- templates/chat/index.html | 169 ----- templates/documents/document_list.html | 54 -- templates/documents/upload.html | 38 -- templates/scenarios/index.html | 53 -- tests/test_agent_core.py | 250 -------- tests/test_audit.py | 119 ---- tests/test_chat.py | 107 ---- tests/test_documents.py | 147 ----- tests/test_llm_provider.py | 126 ---- tests/test_project_configuration.py | 21 - tests/test_scenarios.py | 129 ---- tests/test_tool_registry.py | 54 -- 89 files changed, 7318 deletions(-) delete mode 100644 .env.example delete mode 100644 AGENTS.md delete mode 100644 Dockerfile delete mode 100644 agent_core/__init__.py delete mode 100644 agent_core/llm_provider.py delete mode 100644 agent_core/orchestrator.py delete mode 100644 agent_core/rag/__init__.py delete mode 100644 agent_core/rag/chroma_store.py delete mode 100644 agent_core/rag/ingest.py delete mode 100644 agent_core/rag/retriever.py delete mode 100644 agent_core/results.py delete mode 100644 agent_core/schemas/__init__.py delete mode 100644 agent_core/schemas/outputs.py delete mode 100644 agent_core/structured_output.py delete mode 100644 agent_core/tool_registry.py delete mode 100644 agent_core/tools/__init__.py delete mode 100644 agent_core/tools/builtin_tools.py delete mode 100644 apps/__init__.py delete mode 100644 apps/audit/__init__.py delete mode 100644 apps/audit/admin.py delete mode 100644 apps/audit/apps.py delete mode 100644 apps/audit/migrations/0001_initial.py delete mode 100644 apps/audit/migrations/0002_demobusinessrecord.py delete mode 100644 apps/audit/migrations/__init__.py delete mode 100644 apps/audit/models.py delete mode 100644 apps/audit/services.py delete mode 100644 apps/audit/urls.py delete mode 100644 apps/audit/views.py delete mode 100644 apps/chat/__init__.py delete mode 100644 apps/chat/apps.py delete mode 100644 apps/chat/forms.py delete mode 100644 apps/chat/urls.py delete mode 100644 apps/chat/views.py delete mode 100644 apps/documents/__init__.py delete mode 100644 apps/documents/admin.py delete mode 100644 apps/documents/apps.py delete mode 100644 apps/documents/forms.py delete mode 100644 apps/documents/migrations/0001_initial.py delete mode 100644 apps/documents/migrations/__init__.py delete mode 100644 apps/documents/models.py delete mode 100644 apps/documents/services.py delete mode 100644 apps/documents/urls.py delete mode 100644 apps/documents/views.py delete mode 100644 apps/scenarios/__init__.py delete mode 100644 apps/scenarios/apps.py delete mode 100644 apps/scenarios/services.py delete mode 100644 apps/scenarios/urls.py delete mode 100644 apps/scenarios/views.py delete mode 100644 configs/document_review.yaml delete mode 100644 configs/knowledge_qa.yaml delete mode 100644 configs/quality_analysis.yaml delete mode 100644 configs/risk_audit.yaml delete mode 100644 configs/ticket_assistant.yaml delete mode 100644 docker-compose.yml delete mode 100644 docs/superpowers/plans/2026-05-29-V1-Django基线实现计划.md delete mode 100644 docs/设计文档/0.设计文档索引.md delete mode 100644 docs/设计文档/1.智能体总体设计.md delete mode 100644 docs/设计文档/2.功能流程设计.md delete mode 100644 docs/设计文档/3.数据库设计.md delete mode 100644 docs/设计文档/4.页面与路由设计.md delete mode 100644 docs/设计文档/5.部署设计.md delete mode 100644 docs/设计文档/模块设计/1.配置模块详细设计.md delete mode 100644 docs/设计文档/模块设计/2.场景模块详细设计.md delete mode 100644 docs/设计文档/模块设计/3.文档模块详细设计.md delete mode 100644 docs/设计文档/模块设计/4.对话模块详细设计.md delete mode 100644 docs/设计文档/模块设计/5.审计模块详细设计.md delete mode 100644 docs/设计文档/模块设计/6.智能核心模块详细设计.md delete mode 100644 docs/需求分析/1.V1总需求文档.md delete mode 100644 docs/需求分析/2.模块需求索引.md delete mode 100644 docs/需求分析/3.配置模块需求.md delete mode 100644 docs/需求分析/4.场景模块需求.md delete mode 100644 docs/需求分析/5.文档模块需求.md delete mode 100644 docs/需求分析/6.对话模块需求.md delete mode 100644 docs/需求分析/7.审计模块需求.md delete mode 100644 docs/需求分析/8.智能核心模块需求.md delete mode 100644 templates/audit/log_detail.html delete mode 100644 templates/audit/log_list.html delete mode 100644 templates/chat/index.html delete mode 100644 templates/documents/document_list.html delete mode 100644 templates/documents/upload.html delete mode 100644 templates/scenarios/index.html delete mode 100644 tests/test_agent_core.py delete mode 100644 tests/test_audit.py delete mode 100644 tests/test_chat.py delete mode 100644 tests/test_documents.py delete mode 100644 tests/test_llm_provider.py delete mode 100644 tests/test_project_configuration.py delete mode 100644 tests/test_scenarios.py delete mode 100644 tests/test_tool_registry.py diff --git a/.env.example b/.env.example deleted file mode 100644 index c918251..0000000 --- a/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -DJANGO_SECRET_KEY=replace-with-a-local-secret-key -DJANGO_DEBUG=true -DJANGO_ALLOWED_HOSTS=* - -# OpenAI-compatible LLM API -LLM_API_KEY=your_llm_api_key -LLM_BASE_URL=https://api.openai.com/v1 -LLM_MODEL=gpt-4.1-mini - -# Embedding model for RAG -# Leave EMBEDDING_API_KEY empty to reuse LLM_API_KEY if desired. -EMBEDDING_API_KEY= -EMBEDDING_BASE_URL= -EMBEDDING_MODEL=text-embedding-3-small - -SCENARIO_CONFIG_DIR=configs -UPLOAD_ROOT=data/uploads -CHROMA_PATH=data/chroma diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 9733d63..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,172 +0,0 @@ -# AGENTS.md - -本文档约定本项目后续由人或编码 Agent 协作开发时需要遵守的边界、风格和实现顺序。 - -## 项目定位 - -Universal Agent Demo Framework 是一个用于复试展示的通用 AI Agent Demo 框架。 - -优先目标: - -- 快速适配未知复试题。 -- 保证本地可运行。 -- 保证代码结构清楚,方便讲解。 -- 避免为了平台完整性牺牲改题速度。 - -## 架构原则 - -采用: - -```text -Django 单体 + 独立 Agent Core + Docker Compose -``` - -核心边界: - -- Django 负责页面、数据库、文件上传、审计日志和后台管理。 -- Agent Core 负责 RAG、Prompt、工具调用、模型适配和结构化输出。 -- Django View 不直接写大模型调用、向量检索和工具执行细节。 -- Agent Core 不依赖 Django View。 - -## 模块边界 - -### config - -负责 Django 项目配置、URL 总入口、环境变量、静态资源、上传路径和部署配置。 - -### apps.scenarios - -负责场景列表、场景配置读取、场景元信息展示。 - -### apps.documents - -负责文件上传、文件记录、文件状态和触发 RAG 入库。 - -### apps.chat - -负责对话页面、用户输入表单、调用 Agent Core 和展示结果。 - -### apps.audit - -负责审计日志模型、日志写入服务、日志列表和详情页。 - -### agent_core - -负责 Agent 编排、RAG、工具注册、LLM Provider、结构化输出和 Adapter 扩展。 - -## 开发顺序 - -建议按以下顺序推进: - -1. 创建 Django 项目骨架。 -2. 完成 Config 模块。 -3. 完成 Scenarios 模块,先展示 5 个场景。 -4. 完成 Agent Core 最小闭环,先返回模拟结果。 -5. 完成 Chat 页面,打通对话链路。 -6. 完成 Audit 模块,记录每次对话。 -7. 完成 Documents 模块,支持上传文件。 -8. 完成 RAG 入库和检索。 -9. 完成内置工具系统。 -10. 补 Docker Compose 一键启动。 - -当前仓库状态说明: - -- Django 单体骨架已完成。 -- 5 个预置场景 YAML 已接通首页和对话页。 -- Agent Core 已具备 Prompt 编排、结构化解析、工具注册和 RAG fallback / Chroma 双路径。 -- Chat、Documents、Audit 页面已经可以形成完整演示闭环。 -- 全量测试已覆盖主要模块行为,并默认隔离真实 LLM 网络调用。 - -## 编码约定 - -- Python 代码优先保持简单、直观、可讲解。 -- 不为了抽象而抽象。 -- View 只做请求处理和页面渲染,复杂逻辑放到 `services.py` 或 `agent_core`。 -- 配置化优先,业务场景不要写死在代码中。 -- 工具函数必须通过 Tool Registry 注册。 -- 模型调用必须通过 LLM Provider,不允许散落在业务代码中。 -- 审计日志要记录成功和失败两种情况。 -- 不在日志中保存 API Key、密钥或敏感环境变量。 -- 新增或重构模块时,优先补清晰的中文注释,说明职责边界、输入输出和设计取舍。 -- 页面模板优先直接表达业务信息,不在模板中堆积复杂逻辑判断。 -- 测试优先覆盖服务层和核心编排逻辑,再由页面测试补齐关键展示行为。 - -## 文档约定 - -需求文档放在: - -```text -docs/ -``` - -需求分析文档放在: - -```text -docs/需求分析/ -``` - -设计文档放在: - -```text -docs/设计文档/ -``` - -场景配置放在: - -```text -configs/ -``` - -重要设计变更需要同步更新: - -- `README.md` -- `docs/需求分析/1.V1总需求文档.md` -- 相关模块需求文档 -- `AGENTS.md` 中的协作边界与当前实现状态 - -推荐同步文档的场景: - -- 新增用户可见页面或流程。 -- 调整环境变量、生效方式或部署命令。 -- 修改 Agent Core 的输入输出合约。 -- 新增工具、审计字段或场景配置字段。 - -## 测试与验证约定 - -每个阶段至少验证: - -- Django 可以启动。 -- 首页可以访问。 -- 场景列表可显示。 -- 对话流程可执行。 -- 出错时页面有清晰提示。 -- 审计日志能记录。 -- Docker Compose 可以启动。 - -当前默认验证命令: - -```bash -pytest -python manage.py check -docker compose config -``` - -补充约定: - -- 若本地 `.env` 存在真实模型密钥,测试仍应保持可离线执行。 -- 每完成一项功能或一轮重构后,应先跑相关测试,再跑全量测试或核心回归测试。 -- 完成改动后,按逻辑分组使用 Conventional Commit 风格提交到本地。 - -## 不优先做的事项 - -第一版不要优先做: - -- React / Vue 前端。 -- 多租户。 -- 复杂 RBAC。 -- 完整工作流引擎。 -- 深度 Dify 集成。 -- 微服务拆分。 -- 分布式任务队列。 - -这些内容可以作为后续增强,不应影响 V1 快速成型。 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ac87143..0000000 --- a/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM python:3.13-slim - -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PYTHONUNBUFFERED=1 - -WORKDIR /app - -COPY requirements.txt /app/ -RUN pip install --no-cache-dir -r requirements.txt - -COPY . /app/ - -EXPOSE 8000 - -CMD ["sh", "-c", "python manage.py migrate && python manage.py runserver 0.0.0.0:8000"] diff --git a/agent_core/__init__.py b/agent_core/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/agent_core/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agent_core/llm_provider.py b/agent_core/llm_provider.py deleted file mode 100644 index 089177c..0000000 --- a/agent_core/llm_provider.py +++ /dev/null @@ -1,201 +0,0 @@ -from dataclasses import dataclass -import json -import os -from urllib.error import URLError -from urllib.request import Request, urlopen - - -class LLMConfigurationError(ValueError): - """LLM 调用缺少关键配置时抛出的业务异常。""" - - -class EmbeddingConfigurationError(ValueError): - """Embedding 调用缺少关键配置时抛出的业务异常。""" - - -@dataclass -class LLMResponse: - """ - 统一的模型响应对象。 - - Agent Core 的 Orchestrator 只依赖这一个结构,而不直接感知底层供应商差异。 - """ - content: str = "" - model_name: str = "" - success: bool = True - error: Exception | None = None - - -class MockLLMProvider: - """ - 本地和测试默认使用的 Mock Provider。 - - 设计目标不是拟真对话,而是提供一个稳定、可断言、可结构化解析的响应, - 让前后端在未接入真实模型时也能完整演示链路。 - """ - - def __init__(self, model_name: str = "mock-model"): - self.model_name = model_name or "mock-model" - - def generate(self, messages: list[dict], response_format: dict | None = None) -> LLMResponse: - user_content = _find_last_user_message(messages) - return LLMResponse( - content=json.dumps( - { - "answer": f"模拟回答:{user_content}", - "confidence": "medium", - "references": [], - }, - ensure_ascii=False, - ), - model_name=self.model_name, - success=True, - ) - - -class OpenAICompatibleProvider: - """调用 OpenAI Chat Completions 兼容接口的 Provider。""" - - def __init__(self, api_key: str, base_url: str, model_name: str): - self.api_key = api_key - self.base_url = base_url - self.model_name = model_name - - def generate(self, messages: list[dict], response_format: dict | None = None) -> LLMResponse: - if not self.api_key: - return LLMResponse( - model_name=self.model_name, - success=False, - error=LLMConfigurationError("LLM_API_KEY 未配置,无法调用 OpenAI 兼容模型接口"), - ) - payload = { - "model": self.model_name, - "messages": messages, - } - if response_format: - payload["response_format"] = response_format - try: - data = _post_json( - base_url=self.base_url, - endpoint="chat/completions", - api_key=self.api_key, - payload=payload, - ) - choice = data.get("choices", [{}])[0] - content = choice.get("message", {}).get("content", "") - return LLMResponse( - content=content, - model_name=data.get("model", self.model_name), - success=True, - ) - except Exception as exc: - return LLMResponse(model_name=self.model_name, success=False, error=exc) - - -class OpenAICompatibleEmbeddingProvider: - """调用 OpenAI Embeddings 兼容接口的 Provider。""" - - def __init__(self, api_key: str, base_url: str, model_name: str): - self.api_key = api_key - self.base_url = base_url - self.model_name = model_name - - def embed_texts(self, texts: list[str]) -> list[list[float]]: - if not self.api_key: - raise EmbeddingConfigurationError("EMBEDDING_API_KEY 未配置,无法调用 OpenAI 兼容 Embedding 接口") - data = _post_json( - base_url=self.base_url, - endpoint="embeddings", - api_key=self.api_key, - payload={"model": self.model_name, "input": texts}, - ) - return [item.get("embedding", []) for item in data.get("data", [])] - - -def create_llm_provider(config: dict | None = None): - """ - 根据配置创建 LLM Provider。 - - 默认策略: - - 明确指定 `LLM_PROVIDER=mock` 时使用 Mock - - 未指定但存在 `LLM_API_KEY` 时默认走 OpenAI 兼容接口 - - 否则回退到 Mock,保证页面仍可闭环 - """ - config = config or {} - provider_name = _resolve_provider_name(config) - model_name = config.get("LLM_MODEL", "mock-model") - if provider_name == "mock": - return MockLLMProvider(model_name=model_name) - return OpenAICompatibleProvider( - api_key=config.get("LLM_API_KEY", ""), - base_url=config.get("LLM_BASE_URL", "https://api.openai.com/v1"), - model_name=model_name, - ) - - -def create_embedding_provider(config: dict | None = None): - """ - 创建 Embedding Provider。 - - 当未单独配置 Embedding Key 或 Base URL 时,会自动复用 LLM 配置, - 以减少复试演示时的环境变量负担。 - """ - config = config or {} - return OpenAICompatibleEmbeddingProvider( - api_key=config.get("EMBEDDING_API_KEY", config.get("LLM_API_KEY", "")), - base_url=config.get("EMBEDDING_BASE_URL", config.get("LLM_BASE_URL", "https://api.openai.com/v1")), - model_name=config.get("EMBEDDING_MODEL", "text-embedding-3-small"), - ) - - -def get_runtime_llm_config(overrides: dict | None = None) -> dict: - """ - 从环境变量读取运行时配置。 - - Agent Core 通过这一层读取模型配置,避免直接依赖 Django settings, - 这样本模块在独立脚本、测试和 Django 环境中都可复用。 - """ - config = { - "LLM_PROVIDER": os.environ.get("LLM_PROVIDER", ""), - "LLM_API_KEY": os.environ.get("LLM_API_KEY", ""), - "LLM_BASE_URL": os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1"), - "LLM_MODEL": os.environ.get("LLM_MODEL", "mock-model"), - } - if overrides: - config.update(overrides) - return config - - -def _resolve_provider_name(config: dict) -> str: - """统一推导当前应启用的 Provider 名称。""" - provider_name = config.get("LLM_PROVIDER") - if provider_name: - return provider_name - return "openai_compatible" if config.get("LLM_API_KEY") else "mock" - - -def _find_last_user_message(messages: list[dict]) -> str: - """从消息列表中提取最后一条用户输入,用于 Mock Provider 回显。""" - for message in reversed(messages): - if message.get("role") == "user": - return message.get("content", "") - return "" - - -def _post_json(base_url: str, endpoint: str, api_key: str, payload: dict) -> dict: - """向 OpenAI 兼容接口发送 JSON POST 请求并解析响应。""" - url = f"{base_url.rstrip('/')}/{endpoint}" - request = Request( - url, - data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), - headers={ - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - }, - method="POST", - ) - try: - with urlopen(request, timeout=60) as response: - return json.loads(response.read().decode("utf-8")) - except URLError as exc: - raise RuntimeError(f"OpenAI 兼容接口调用失败:{exc}") from exc diff --git a/agent_core/orchestrator.py b/agent_core/orchestrator.py deleted file mode 100644 index 287cdb8..0000000 --- a/agent_core/orchestrator.py +++ /dev/null @@ -1,153 +0,0 @@ -import json -import time - -from .llm_provider import create_llm_provider, get_runtime_llm_config -from .results import AgentResult -from .structured_output import ( - build_response_schema_hint, - extract_answer_from_structured_output, - parse_structured_output, -) -from .tool_registry import run_declared_tools -from .rag.retriever import retrieve - - -def run_agent(scenario_config: dict, user_input: str, options: dict | None = None) -> AgentResult: - """ - 执行当前场景的最小 Agent 闭环。 - - 处理顺序保持和设计文档一致: - 1. 读取场景配置 - 2. 执行 RAG 检索 - 3. 执行声明式工具 - 4. 构造 Prompt 并调用 LLM - 5. 解析结构化结果 - 6. 统一返回 AgentResult - """ - started_at = time.perf_counter() - options = options or {} - output_type = scenario_config.get("output", {}).get("type", "general_answer") - - references = _collect_references(scenario_config=scenario_config, user_input=user_input, options=options) - tool_calls = run_declared_tools(scenario_config.get("tools", []), user_input) - messages = build_messages( - scenario_config=scenario_config, - user_input=user_input, - references=references, - tool_calls=tool_calls, - ) - - provider = options.get("llm_provider") or create_llm_provider( - get_runtime_llm_config(options.get("llm_config")) - ) - llm_response = provider.generate( - messages, - response_format=build_response_schema_hint(output_type), - ) - latency_ms = int((time.perf_counter() - started_at) * 1000) - - if not llm_response.success: - return AgentResult( - answer="模型调用失败,请检查配置或稍后重试。", - structured_output={}, - references=references, - tool_calls=tool_calls, - raw_output="", - model_name=llm_response.model_name or "unknown-model", - latency_ms=latency_ms, - status="failed", - error=str(llm_response.error or "未知模型错误"), - ) - - structured_output, _ = parse_structured_output(llm_response.content, output_type) - answer = extract_answer_from_structured_output(structured_output, llm_response.content) - return AgentResult( - answer=answer, - structured_output=structured_output, - references=references, - tool_calls=tool_calls, - raw_output=llm_response.content, - model_name=llm_response.model_name or "unknown-model", - latency_ms=latency_ms, - status="success", - ) - - -def build_messages( - scenario_config: dict, - user_input: str, - references: list[dict], - tool_calls: list[dict], -) -> list[dict]: - """将场景配置、检索结果和工具结果整合为最小可解释 Prompt。""" - agent_config = scenario_config.get("agent", {}) - system_message = "\n".join( - [ - f"你当前扮演的角色:{agent_config.get('role', '通用业务助手')}", - f"当前任务目标:{agent_config.get('goal', '根据输入生成结构化结果')}", - "执行要求:", - _format_instructions(agent_config.get("instructions", [])), - f"输出类型:{scenario_config.get('output', {}).get('type', 'general_answer')}", - "请优先输出 JSON 对象,字段必须贴近约定输出结构。", - ] - ) - context_message = "\n".join( - [ - f"当前场景:{scenario_config.get('name', '未命名场景')}", - _format_references(references), - _format_tool_calls(tool_calls), - ] - ) - return [ - {"role": "system", "content": system_message}, - {"role": "assistant", "content": context_message}, - {"role": "user", "content": user_input}, - ] - - -def _collect_references(scenario_config: dict, user_input: str, options: dict) -> list[dict]: - """按场景配置执行检索,并保持无 RAG 场景也能正常返回空列表。""" - rag_config = scenario_config.get("rag", {}) - if not rag_config.get("enabled"): - return [] - return retrieve( - scenario_id=scenario_config.get("id", ""), - query=user_input, - collection=rag_config.get("collection", scenario_config.get("id", "")), - top_k=rag_config.get("top_k", 5), - document_ids=options.get("document_ids"), - store_path=options.get("rag_store_path"), - ) - - -def _format_instructions(instructions: list[str]) -> str: - if not instructions: - return "1. 结合知识库和工具结果回答。\n2. 信息不足时明确说明。" - return "\n".join(f"{index}. {item}" for index, item in enumerate(instructions, start=1)) - - -def _format_references(references: list[dict]) -> str: - if not references: - return "知识库引用:当前没有检索到可用片段。" - lines = ["知识库引用:"] - for index, reference in enumerate(references, start=1): - lines.append( - f"{index}. 来源={reference.get('source', '未知来源')} 内容={reference.get('content', '')}" - ) - return "\n".join(lines) - - -def _format_tool_calls(tool_calls: list[dict]) -> str: - if not tool_calls: - return "工具结果:当前场景未声明工具或无需调用工具。" - lines = ["工具结果:"] - for index, tool_call in enumerate(tool_calls, start=1): - if tool_call.get("success"): - lines.append( - f"{index}. 工具={tool_call.get('tool_name')} 结果={json.dumps(tool_call.get('result', {}), ensure_ascii=False)}" - ) - else: - lines.append( - f"{index}. 工具={tool_call.get('tool_name')} 失败={tool_call.get('error', '未知错误')}" - ) - return "\n".join(lines) diff --git a/agent_core/rag/__init__.py b/agent_core/rag/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/agent_core/rag/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agent_core/rag/chroma_store.py b/agent_core/rag/chroma_store.py deleted file mode 100644 index ec61414..0000000 --- a/agent_core/rag/chroma_store.py +++ /dev/null @@ -1,104 +0,0 @@ -from pathlib import Path - -from django.conf import settings - -from agent_core.llm_provider import create_embedding_provider - - -def _client(path: str | Path | None = None): - """按给定路径初始化 Chroma 持久化客户端。""" - import chromadb - - resolved_path = str(path or settings.CHROMA_PATH) - return chromadb.PersistentClient(path=resolved_path) - - -def _embedding_provider(): - """从 Django settings 构造 Embedding Provider,避免在业务层散落配置读取。""" - return create_embedding_provider( - { - "EMBEDDING_API_KEY": settings.EMBEDDING_API_KEY, - "EMBEDDING_BASE_URL": settings.EMBEDDING_BASE_URL, - "EMBEDDING_MODEL": settings.EMBEDDING_MODEL, - } - ) - - -def upsert_chunks( - collection: str, - chunks: list[dict], - store_path: str | Path | None = None, -) -> None: - """ - 将 chunk 写入 Chroma。 - - 同一 document_id 重新入库前会先删除旧记录,保证一次文档只有一份有效向量数据。 - """ - client = _client(store_path) - chroma_collection = client.get_or_create_collection(collection) - document_ids = {chunk["document_id"] for chunk in chunks if chunk.get("document_id") is not None} - for document_id in document_ids: - chroma_collection.delete(where={"document_id": document_id}) - texts = [chunk["content"] for chunk in chunks] - embeddings = _embedding_provider().embed_texts(texts) - chroma_collection.upsert( - ids=[chunk["chunk_id"] for chunk in chunks], - documents=texts, - embeddings=embeddings, - metadatas=[ - { - "scenario_id": chunk["scenario_id"], - "document_id": chunk["document_id"], - "source": chunk["source"], - "chunk_id": chunk["chunk_id"], - "created_at": chunk["created_at"], - } - for chunk in chunks - ], - ) - - -def query_chunks( - scenario_id: str, - query: str, - collection: str, - top_k: int = 5, - document_ids: list[int] | None = None, - store_path: str | Path | None = None, -) -> list[dict]: - """执行向量检索,并把 Chroma 原始结果转换为统一引用结构。""" - client = _client(store_path) - chroma_collection = client.get_or_create_collection(collection) - where: dict = {"scenario_id": scenario_id} - if document_ids: - where = { - "$and": [ - {"scenario_id": scenario_id}, - {"document_id": {"$in": document_ids}}, - ] - } - embedding = _embedding_provider().embed_texts([query])[0] - result = chroma_collection.query( - query_embeddings=[embedding], - n_results=top_k, - where=where, - include=["documents", "metadatas", "distances"], - ) - chunks = [] - documents = result.get("documents", [[]])[0] - metadatas = result.get("metadatas", [[]])[0] - distances = result.get("distances", [[]])[0] - for content, metadata, distance in zip(documents, metadatas, distances): - chunks.append( - { - "scenario_id": metadata.get("scenario_id"), - "document_id": metadata.get("document_id"), - "collection": collection, - "source": metadata.get("source"), - "chunk_id": metadata.get("chunk_id"), - "content": content, - "created_at": metadata.get("created_at"), - "score": round(1 / (1 + float(distance)), 4), - } - ) - return chunks diff --git a/agent_core/rag/ingest.py b/agent_core/rag/ingest.py deleted file mode 100644 index 2a4ad32..0000000 --- a/agent_core/rag/ingest.py +++ /dev/null @@ -1,171 +0,0 @@ -import importlib.util -import json -import re -from dataclasses import dataclass -from datetime import datetime, timezone -from pathlib import Path - -from django.conf import settings - -from .chroma_store import upsert_chunks - - -@dataclass -class IngestResult: - """RAG 入库统一返回结构,供 Documents 模块稳定消费。""" - success: bool - chunks_count: int = 0 - error: str = "" - - -def ingest_document( - scenario_id: str, - source_file: str, - text: str, - collection: str, - document_id: int | None = None, - store_path: str | Path | None = None, -) -> IngestResult: - """ - 将单个文档文本切分后写入知识库。 - - 运行策略: - - 如果显式传入 `store_path`,说明当前是测试或降级模式,走本地 JSON 存储。 - - 如果未传入且环境可用 chromadb,则走真实 Chroma 持久化。 - """ - if not text.strip(): - return IngestResult(success=False, error="文档内容为空") - if _should_use_chroma(store_path): - return _ingest_chroma_document( - document_id=document_id, - scenario_id=scenario_id, - source_file=source_file, - text=text, - collection=collection, - ) - resolved_store_path = Path(store_path) if store_path else _default_store_path() - chunks = _build_chunks( - scenario_id=scenario_id, - source_file=source_file, - text=text, - collection=collection, - document_id=document_id, - chunk_id_prefix=source_file, - ) - persisted_chunks = _filter_out_same_document_chunks( - _load_store(resolved_store_path), - scenario_id=scenario_id, - collection=collection, - document_id=document_id, - ) - _save_store(resolved_store_path, [*persisted_chunks, *chunks]) - return IngestResult(success=True, chunks_count=len(chunks)) - - -def _should_use_chroma(store_path: str | Path | None) -> bool: - """只在未指定测试存储路径且安装 chromadb 时启用真实向量库。""" - return store_path is None and importlib.util.find_spec("chromadb") is not None - - -def _default_store_path() -> Path: - return Path(settings.CHROMA_PATH) / "rag_store.json" - - -def _load_store(store_path: Path) -> list[dict]: - if not store_path.exists(): - return [] - with store_path.open("r", encoding="utf-8") as file: - return json.load(file) - - -def _save_store(store_path: Path, chunks: list[dict]) -> None: - store_path.parent.mkdir(parents=True, exist_ok=True) - with store_path.open("w", encoding="utf-8") as file: - json.dump(chunks, file, ensure_ascii=False, indent=2) - - -def _split_text(text: str, chunk_size: int = 800, overlap: int = 120) -> list[str]: - """ - 使用固定窗口 + overlap 切分文本。 - - 该策略简单但稳定,便于解释: - - chunk_size 控制每个片段最大长度 - - overlap 保证相邻片段共享上下文,降低边界信息丢失 - """ - normalized = re.sub(r"\s+", " ", text).strip() - if not normalized: - return [] - chunks = [] - start = 0 - while start < len(normalized): - end = start + chunk_size - chunks.append(normalized[start:end]) - if end >= len(normalized): - break - start = max(end - overlap, start + 1) - return chunks - - -def _build_chunks( - scenario_id: str, - source_file: str, - text: str, - collection: str, - document_id: int | None, - chunk_id_prefix: str, -) -> list[dict]: - """把原始文本切分并封装为统一 chunk 结构。""" - created_at = datetime.now(timezone.utc).isoformat() - return [ - { - "scenario_id": scenario_id, - "document_id": document_id, - "collection": collection, - "source": source_file, - "chunk_id": f"{scenario_id}:{chunk_id_prefix}:{index}", - "content": chunk_text, - "created_at": created_at, - } - for index, chunk_text in enumerate(_split_text(text), start=1) - ] - - -def _filter_out_same_document_chunks( - chunks: list[dict], - scenario_id: str, - collection: str, - document_id: int | None, -) -> list[dict]: - """重新入库同一 document_id 时,先删除旧 chunk,避免重复检索。""" - return [ - chunk - for chunk in chunks - if not ( - chunk.get("document_id") == document_id - and chunk.get("scenario_id") == scenario_id - and chunk.get("collection") == collection - ) - ] - - -def _ingest_chroma_document( - document_id: int | None, - scenario_id: str, - source_file: str, - text: str, - collection: str, -) -> IngestResult: - """真实 Chroma 模式的入库分支。""" - chunks = _build_chunks( - scenario_id=scenario_id, - source_file=source_file, - text=text, - collection=collection, - document_id=document_id, - chunk_id_prefix=str(document_id or source_file), - ) - try: - upsert_chunks(collection=collection, chunks=chunks) - except Exception as exc: - return IngestResult(success=False, error=str(exc)) - return IngestResult(success=True, chunks_count=len(chunks)) diff --git a/agent_core/rag/retriever.py b/agent_core/rag/retriever.py deleted file mode 100644 index f17def7..0000000 --- a/agent_core/rag/retriever.py +++ /dev/null @@ -1,105 +0,0 @@ -import importlib.util -import json -import re -from pathlib import Path - -from django.conf import settings - -from .chroma_store import query_chunks - - -def retrieve( - scenario_id: str, - query: str, - collection: str, - top_k: int = 5, - document_ids: list[int] | None = None, - store_path: str | Path | None = None, -) -> list[dict]: - """ - 统一对外提供检索入口。 - - 与 ingest_document 保持一致: - - 真实运行优先走 Chroma - - 测试或降级模式走本地 JSON + 轻量文本打分 - """ - if _should_use_chroma(store_path): - return query_chunks( - scenario_id=scenario_id, - query=query, - collection=collection, - top_k=top_k, - document_ids=document_ids, - ) - resolved_store_path = Path(store_path) if store_path else _default_store_path() - query_tokens = _tokens(query) - allowed_document_ids = set(document_ids or []) - scored_chunks = [] - for chunk in _load_store(resolved_store_path): - if not _matches_scope( - chunk=chunk, - scenario_id=scenario_id, - collection=collection, - allowed_document_ids=allowed_document_ids, - ): - continue - score = _score(query_tokens, chunk.get("content", "")) - if score <= 0: - continue - scored_chunks.append({**chunk, "score": score}) - return sorted(scored_chunks, key=lambda item: item["score"], reverse=True)[:top_k] - - -def _should_use_chroma(store_path: str | Path | None) -> bool: - return store_path is None and importlib.util.find_spec("chromadb") is not None - - -def _default_store_path() -> Path: - return Path(settings.CHROMA_PATH) / "rag_store.json" - - -def _load_store(store_path: Path) -> list[dict]: - if not store_path.exists(): - return [] - with store_path.open("r", encoding="utf-8") as file: - return json.load(file) - - -def _matches_scope( - chunk: dict, - scenario_id: str, - collection: str, - allowed_document_ids: set[int], -) -> bool: - """先按场景、collection 和可选文档范围过滤,再进行相关性打分。""" - if chunk.get("scenario_id") != scenario_id: - return False - if chunk.get("collection") != collection: - return False - if allowed_document_ids and chunk.get("document_id") not in allowed_document_ids: - return False - return True - - -def _tokens(text: str) -> set[str]: - """ - 兼容中英文的轻量分词策略。 - - 该分词仅用于 fallback 模式,不替代真实向量检索: - - 英文/数字按词提取 - - 中文按连续词片段和单字同时保留,提升短查询命中率 - """ - lowered = text.lower() - ascii_tokens = set(re.findall(r"[a-z0-9_]+", lowered)) - cjk_tokens = set(re.findall(r"[\u4e00-\u9fff]{2,}", lowered)) - chars = {char for char in lowered if "\u4e00" <= char <= "\u9fff"} - return ascii_tokens | cjk_tokens | chars - - -def _score(query_tokens: set[str], content: str) -> float: - """使用交集占比计算一个便于排序的简化相关性分数。""" - content_tokens = _tokens(content) - if not query_tokens or not content_tokens: - return 0.0 - overlap = query_tokens & content_tokens - return round(len(overlap) / len(query_tokens), 4) diff --git a/agent_core/results.py b/agent_core/results.py deleted file mode 100644 index 84f0ba8..0000000 --- a/agent_core/results.py +++ /dev/null @@ -1,22 +0,0 @@ -from dataclasses import dataclass, field - - -@dataclass -class AgentResult: - """ - Agent Core 对 Django 层暴露的统一结果对象。 - - 任何底层编排实现都必须返回这一结构,确保: - - Chat 页面有稳定字段可展示 - - Audit 模块有稳定字段可落库 - - 未来替换编排引擎时不影响 Django 业务层 - """ - answer: str = "" - structured_output: dict = field(default_factory=dict) - references: list = field(default_factory=list) - tool_calls: list = field(default_factory=list) - raw_output: str = "" - model_name: str = "mock-model" - latency_ms: int = 0 - status: str = "success" - error: str = "" diff --git a/agent_core/schemas/__init__.py b/agent_core/schemas/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/agent_core/schemas/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agent_core/schemas/outputs.py b/agent_core/schemas/outputs.py deleted file mode 100644 index c3a7be7..0000000 --- a/agent_core/schemas/outputs.py +++ /dev/null @@ -1,7 +0,0 @@ -SUPPORTED_OUTPUT_TYPES = { - "general_answer", - "document_review_report", - "ticket_response", - "quality_report", - "risk_audit_report", -} diff --git a/agent_core/structured_output.py b/agent_core/structured_output.py deleted file mode 100644 index 98a16c3..0000000 --- a/agent_core/structured_output.py +++ /dev/null @@ -1,142 +0,0 @@ -import json - -from .schemas.outputs import SUPPORTED_OUTPUT_TYPES - - -# 按输出类型声明页面和审计日志真正需要消费的结构化字段。 -# 这里不追求复杂 schema 框架,优先保证字段稳定、可读、易讲解。 -OUTPUT_FIELD_TEMPLATES = { - "general_answer": { - "answer": "", - "confidence": "medium", - "references": [], - }, - "document_review_report": { - "summary": "", - "issues": [], - "risk_level": "medium", - "suggestions": [], - "missing_items": [], - "references": [], - }, - "ticket_response": { - "reply": "", - "category": "general", - "priority": "medium", - "suggested_action": "", - "need_human_review": False, - }, - "quality_report": { - "summary": "", - "possible_causes": [], - "evidence": [], - "risk_level": "medium", - "suggested_actions": [], - "references": [], - }, - "risk_audit_report": { - "summary": "", - "risk_points": [], - "risk_level": "medium", - "suggestions": [], - "references": [], - }, -} - - -def build_response_schema_hint(output_type: str) -> dict: - """返回给 LLM 的结构化提示,帮助模型尽量输出稳定 JSON。""" - normalized_output_type = normalize_output_type(output_type) - return { - "output_type": normalized_output_type, - "fields": list(OUTPUT_FIELD_TEMPLATES[normalized_output_type].keys()), - } - - -def normalize_output_type(output_type: str) -> str: - """对外部配置做轻量归一化,避免拼写差异导致解析分支混乱。""" - if output_type in SUPPORTED_OUTPUT_TYPES: - return output_type - return "general_answer" - - -def parse_structured_output(raw_content: str, output_type: str) -> tuple[dict, str]: - """ - 优先将模型输出解析为 JSON。 - - 返回值: - - structured_output: 页面和审计日志可直接消费的标准结构 - - parse_mode: `json` 表示成功解析,`fallback` 表示降级处理 - """ - normalized_output_type = normalize_output_type(output_type) - parsed = _try_parse_json_object(raw_content) - if parsed is None: - return build_fallback_structured_output( - output_type=normalized_output_type, - raw_content=raw_content, - ), "fallback" - - template = { - "output_type": normalized_output_type, - "parse_mode": "json", - } - template.update(OUTPUT_FIELD_TEMPLATES[normalized_output_type]) - template.update(parsed) - return template, "json" - - -def build_fallback_structured_output(output_type: str, raw_content: str) -> dict: - """当模型没有输出合法 JSON 时,仍然构造一个稳定的展示结构。""" - normalized_output_type = normalize_output_type(output_type) - structured_output = { - "output_type": normalized_output_type, - "parse_mode": "fallback", - } - structured_output.update(OUTPUT_FIELD_TEMPLATES[normalized_output_type]) - - if normalized_output_type == "general_answer": - structured_output["answer"] = raw_content - return structured_output - if normalized_output_type == "document_review_report": - structured_output["summary"] = raw_content - return structured_output - if normalized_output_type == "ticket_response": - structured_output["reply"] = raw_content - return structured_output - if normalized_output_type == "quality_report": - structured_output["summary"] = raw_content - return structured_output - - structured_output["summary"] = raw_content - return structured_output - - -def extract_answer_from_structured_output(structured_output: dict, raw_content: str) -> str: - """从结构化结果里提取页面主回答,保证不同输出类型有统一入口。""" - for field_name in ("answer", "reply", "summary"): - value = structured_output.get(field_name) - if isinstance(value, str) and value.strip(): - return value.strip() - return raw_content.strip() - - -def _try_parse_json_object(raw_content: str) -> dict | None: - """支持纯 JSON 或被 Markdown 代码块包裹的 JSON。""" - content = raw_content.strip() - if not content: - return None - candidates = [content] - if content.startswith("```"): - stripped = content.strip("`").strip() - if stripped.lower().startswith("json"): - stripped = stripped[4:].strip() - candidates.append(stripped) - - for candidate in candidates: - try: - parsed = json.loads(candidate) - except json.JSONDecodeError: - continue - if isinstance(parsed, dict): - return parsed - return None diff --git a/agent_core/tool_registry.py b/agent_core/tool_registry.py deleted file mode 100644 index 89fe15a..0000000 --- a/agent_core/tool_registry.py +++ /dev/null @@ -1,70 +0,0 @@ -from collections.abc import Callable - -from .tools.builtin_tools import BUILTIN_TOOLS - - -class ToolRegistry: - """ - 统一管理工具注册、查询和执行。 - - 设计目标: - - 让 Orchestrator 只关心“声明了哪些工具”,不关心工具如何存放。 - - 固化统一的工具调用结果结构,便于页面展示和审计日志保存。 - - 后续新增业务工具时,只需要注册函数,不必改调用协议。 - """ - - def __init__(self, initial_tools: dict[str, Callable] | None = None): - self._tools: dict[str, Callable] = dict(initial_tools or {}) - - def register(self, tool_name: str, tool_func: Callable) -> None: - """注册一个可通过名称调用的工具函数。""" - self._tools[tool_name] = tool_func - - def get(self, tool_name: str) -> Callable | None: - """按名称返回工具函数;未注册时返回 None。""" - return self._tools.get(tool_name) - - def run(self, tool_name: str, **kwargs) -> dict: - """ - 执行单个工具,并返回统一结果结构。 - - 统一返回值是审计日志、页面展示和后续 Agent 编排共享的协议。 - 即使工具不存在或执行失败,也返回可消费的失败结果,而不是抛异常。 - """ - tool = self.get(tool_name) - if tool is None: - return { - "tool_name": tool_name, - "success": False, - "arguments": kwargs, - "result": {}, - "error": "工具未注册", - } - try: - return { - "tool_name": tool_name, - "success": True, - "arguments": kwargs, - "result": tool(**kwargs), - "error": "", - } - except Exception as exc: - return { - "tool_name": tool_name, - "success": False, - "arguments": kwargs, - "result": {}, - "error": str(exc), - } - - -# 默认注册表承载项目内置工具,便于当前 V1 直接复用。 -DEFAULT_TOOL_REGISTRY = ToolRegistry(BUILTIN_TOOLS) - - -def run_declared_tools(tool_names: list[str], user_input: str) -> list[dict]: - """按场景声明顺序执行工具,保证结果顺序与配置顺序一致。""" - return [ - DEFAULT_TOOL_REGISTRY.run(tool_name, user_input=user_input) - for tool_name in tool_names - ] diff --git a/agent_core/tools/__init__.py b/agent_core/tools/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/agent_core/tools/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/agent_core/tools/builtin_tools.py b/agent_core/tools/builtin_tools.py deleted file mode 100644 index 038bab3..0000000 --- a/agent_core/tools/builtin_tools.py +++ /dev/null @@ -1,124 +0,0 @@ -import re - - -def calculate_rate(user_input: str) -> dict: - """ - 从自然语言中提取两个数值并计算比例。 - - V1 目标不是构建复杂公式引擎,而是提供一个可演示的“业务工具”示例: - 只要输入中出现两个数字,就将其解释为“已完成值 / 总数”。 - """ - numbers = [float(item) for item in re.findall(r"\d+(?:\.\d+)?", user_input)] - if len(numbers) < 2: - return { - "success": False, - "rate": 0.0, - "numerator": 0.0, - "denominator": 0.0, - "note": "未能从输入中提取两个数字,无法计算比例。", - } - numerator, denominator = numbers[0], numbers[1] - if denominator == 0: - return { - "success": False, - "rate": 0.0, - "numerator": numerator, - "denominator": denominator, - "note": "分母为 0,无法计算比例。", - } - return { - "success": True, - "numerator": numerator, - "denominator": denominator, - "rate": round(numerator / denominator, 4), - "note": "已按输入中的前两个数字完成比例计算。", - } - - -def query_demo_records(user_input: str) -> dict: - """ - 查询示例业务记录。 - - 该工具依赖 Audit 模块中的 DemoBusinessRecord 演示表,用于证明 - “场景 + 结构化数据 + 工具调用”可以组成更可信的业务 Agent。 - """ - try: - from apps.audit.models import DemoBusinessRecord - except Exception as exc: - return {"records": [], "error": str(exc)} - - queryset = DemoBusinessRecord.objects.all() - tokens = {token.strip().lower() for token in user_input.split() if token.strip()} - scenario_ids = set(queryset.values_list("scenario_id", flat=True)) - record_types = set(queryset.values_list("record_type", flat=True)) - matched_scenario_ids = scenario_ids & tokens - matched_record_types = record_types & tokens - if matched_scenario_ids: - queryset = queryset.filter(scenario_id__in=matched_scenario_ids) - if matched_record_types: - queryset = queryset.filter(record_type__in=matched_record_types) - records = [ - { - "id": record.id, - "scenario_id": record.scenario_id, - "record_type": record.record_type, - "title": record.title, - "payload": record.payload, - } - for record in queryset[:20] - ] - return {"records": records} - - -def check_required_fields(user_input: str) -> dict: - """ - 检查输入中声明的必填项是否全部出现。 - - 约定格式示例: - “请检查必填项:合同编号、供应商、金额。当前只提供了合同编号和金额。” - """ - required_match = re.search(r"必填项[::](.+?)(?:。|\.)", user_input) - provided_match = re.search(r"(?:当前|已|仅)?提供了(.+?)(?:。|\.)", user_input) - required_fields = _split_cn_items(required_match.group(1) if required_match else "") - provided_fields = set(_split_cn_items(provided_match.group(1) if provided_match else "")) - missing_fields = [field for field in required_fields if field not in provided_fields] - return { - "required_fields": required_fields, - "provided_fields": list(provided_fields), - "missing_fields": missing_fields, - "note": "已根据输入中的“必填项/提供了”描述完成检查。", - } - - -def generate_action_items(user_input: str) -> dict: - """ - 生成最小可执行行动项。 - - 该工具主要用于演示“模型回答之外,还可以得到结构化待办建议”。 - """ - return { - "items": [ - "先确认问题背景和适用场景。", - f"围绕当前问题继续核实:{user_input}", - "根据知识库和审计结果安排下一步处理动作。", - ] - } - - -def _split_cn_items(raw_text: str) -> list[str]: - """将中文顿号、逗号和连接词分隔的字段串切分为列表。""" - normalized = ( - raw_text.replace("和", "、") - .replace("以及", "、") - .replace(",", "、") - .replace(",", "、") - ) - return [item.strip(" 。.") for item in normalized.split("、") if item.strip(" 。.")] - - -BUILTIN_TOOLS = { - "calculate_rate": calculate_rate, - "query_demo_records": query_demo_records, - "check_required_fields": check_required_fields, - "generate_action_items": generate_action_items, -} diff --git a/apps/__init__.py b/apps/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/apps/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/audit/__init__.py b/apps/audit/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/apps/audit/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/audit/admin.py b/apps/audit/admin.py deleted file mode 100644 index 41feca0..0000000 --- a/apps/audit/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin - -from .models import AgentAuditLog, DemoBusinessRecord - - -@admin.register(AgentAuditLog) -class AgentAuditLogAdmin(admin.ModelAdmin): - """便于在 Django Admin 中快速查看一次 Agent 执行的关键信息。""" - list_display = ("id", "scenario_name", "status", "model_name", "latency_ms", "created_at") - list_filter = ("status", "scenario_id") - search_fields = ("scenario_id", "scenario_name", "user_input", "final_answer") - - -@admin.register(DemoBusinessRecord) -class DemoBusinessRecordAdmin(admin.ModelAdmin): - """管理工具查询依赖的示例业务记录。""" - list_display = ("id", "title", "scenario_id", "record_type", "created_at") - list_filter = ("scenario_id", "record_type") - search_fields = ("title", "scenario_id", "record_type") diff --git a/apps/audit/apps.py b/apps/audit/apps.py deleted file mode 100644 index 73eef28..0000000 --- a/apps/audit/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class AuditConfig(AppConfig): - """Audit 模块应用配置。""" - default_auto_field = "django.db.models.BigAutoField" - name = "apps.audit" diff --git a/apps/audit/migrations/0001_initial.py b/apps/audit/migrations/0001_initial.py deleted file mode 100644 index ab1aa1d..0000000 --- a/apps/audit/migrations/0001_initial.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 5.2.14 on 2026-05-29 13:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="AgentAuditLog", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("scenario_id", models.CharField(db_index=True, max_length=100)), - ("scenario_name", models.CharField(blank=True, max_length=200)), - ("user_input", models.TextField()), - ("retrieved_chunks", models.JSONField(blank=True, default=list)), - ("tool_calls", models.JSONField(blank=True, default=list)), - ("structured_output", models.JSONField(blank=True, default=dict)), - ("final_answer", models.TextField(blank=True)), - ("raw_output", models.TextField(blank=True)), - ("model_name", models.CharField(blank=True, max_length=100)), - ("latency_ms", models.PositiveIntegerField(default=0)), - ( - "status", - models.CharField(db_index=True, default="success", max_length=20), - ), - ("error_message", models.TextField(blank=True)), - ("created_at", models.DateTimeField(auto_now_add=True, db_index=True)), - ], - options={ - "ordering": ["-created_at"], - }, - ), - ] diff --git a/apps/audit/migrations/0002_demobusinessrecord.py b/apps/audit/migrations/0002_demobusinessrecord.py deleted file mode 100644 index 42d0387..0000000 --- a/apps/audit/migrations/0002_demobusinessrecord.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated for V1 demo business records. - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("audit", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="DemoBusinessRecord", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("scenario_id", models.CharField(db_index=True, max_length=100)), - ("record_type", models.CharField(db_index=True, max_length=100)), - ("title", models.CharField(max_length=255)), - ("payload", models.JSONField(blank=True, default=dict)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ], - options={ - "ordering": ["-created_at"], - }, - ), - ] diff --git a/apps/audit/migrations/__init__.py b/apps/audit/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/audit/models.py b/apps/audit/models.py deleted file mode 100644 index f34322d..0000000 --- a/apps/audit/models.py +++ /dev/null @@ -1,68 +0,0 @@ -from django.db import models - - -class AgentAuditLog(models.Model): - """ - 保存一次 Agent 执行的完整审计快照。 - - 该模型是“系统可解释性”的核心: - - 对话页负责触发执行 - - Agent Core 负责生成结果 - - Audit 模型负责长期保存输入、引用、工具调用和模型输出 - """ - # 审计状态需要同时服务数据库检索和前端展示。 - STATUS_SUCCESS = "success" - STATUS_FAILED = "failed" - - scenario_id = models.CharField(max_length=100, db_index=True) - scenario_name = models.CharField(max_length=200, blank=True) - user_input = models.TextField() - retrieved_chunks = models.JSONField(default=list, blank=True) - tool_calls = models.JSONField(default=list, blank=True) - structured_output = models.JSONField(default=dict, blank=True) - final_answer = models.TextField(blank=True) - raw_output = models.TextField(blank=True) - model_name = models.CharField(max_length=100, blank=True) - latency_ms = models.PositiveIntegerField(default=0) - status = models.CharField(max_length=20, default=STATUS_SUCCESS, db_index=True) - error_message = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True, db_index=True) - - class Meta: - ordering = ["-created_at"] - - def __str__(self) -> str: - return f"{self.scenario_name or self.scenario_id} #{self.pk}" - - def get_status_display_text(self) -> str: - """返回更适合页面展示的中文状态。""" - return { - self.STATUS_SUCCESS: "执行成功", - self.STATUS_FAILED: "执行失败", - }.get(self.status, self.status) - - def get_user_input_summary(self, max_length: int = 28) -> str: - """在列表页展示用户输入摘要,避免长文本撑破表格。""" - if len(self.user_input) <= max_length: - return self.user_input - return f"{self.user_input[:max_length]}..." - - -class DemoBusinessRecord(models.Model): - """ - 演示用业务记录表。 - - 该表不直接参与页面主流程,而是供内置工具 `query_demo_records` - 查询,证明 Agent 除知识库外也可以结合结构化业务数据。 - """ - scenario_id = models.CharField(max_length=100, db_index=True) - record_type = models.CharField(max_length=100, db_index=True) - title = models.CharField(max_length=255) - payload = models.JSONField(default=dict, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - - class Meta: - ordering = ["-created_at"] - - def __str__(self) -> str: - return self.title diff --git a/apps/audit/services.py b/apps/audit/services.py deleted file mode 100644 index d56d5cc..0000000 --- a/apps/audit/services.py +++ /dev/null @@ -1,57 +0,0 @@ -from agent_core.results import AgentResult - -from .models import AgentAuditLog - - -def create_audit_log( - scenario_id: str, - scenario_name: str, - user_input: str, - agent_result: AgentResult, -) -> AgentAuditLog: - """ - 将一次 Agent 执行结果落库为审计日志。 - - 设计原则: - - 成功与失败都必须记录,方便复盘整条执行链路 - - 敏感信息在写库前先脱敏,避免误存 API Key - - 对前端和 Django Model 统一输出稳定字段 - """ - return AgentAuditLog.objects.create( - scenario_id=scenario_id, - scenario_name=scenario_name, - user_input=user_input, - retrieved_chunks=agent_result.references, - tool_calls=agent_result.tool_calls, - structured_output=agent_result.structured_output, - final_answer=agent_result.answer, - raw_output=agent_result.raw_output, - model_name=agent_result.model_name, - latency_ms=max(agent_result.latency_ms, 0), - status=agent_result.status, - error_message=mask_sensitive_text(agent_result.error), - ) - - -def mask_sensitive_text(value: str) -> str: - """ - 对错误文本中的敏感配置进行脱敏。 - - 当前至少处理: - - `LLM_API_KEY=...` - - `EMBEDDING_API_KEY=...` - """ - masked = value - for marker in ("LLM_API_KEY=", "EMBEDDING_API_KEY="): - masked = _mask_token_after_marker(masked, marker) - return masked - - -def _mask_token_after_marker(value: str, marker: str) -> str: - """将 marker 后紧跟的 token 替换为脱敏占位符。""" - if marker not in value: - return value - prefix, _, suffix = value.partition(marker) - secret, separator, rest = suffix.partition(" ") - masked_secret = "sk-***" if secret.startswith("sk-") else "***" - return f"{prefix}{marker}{masked_secret}{separator}{rest}" diff --git a/apps/audit/urls.py b/apps/audit/urls.py deleted file mode 100644 index 367a972..0000000 --- a/apps/audit/urls.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.urls import path - -from . import views - - -app_name = "audit" - -# V1 的审计功能由列表页和详情页组成,暂不拆分页或复杂筛选接口。 -urlpatterns = [ - path("", views.log_list, name="list"), - path("/", views.log_detail, name="detail"), -] diff --git a/apps/audit/views.py b/apps/audit/views.py deleted file mode 100644 index 37824dc..0000000 --- a/apps/audit/views.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.shortcuts import get_object_or_404, render - -from .models import AgentAuditLog - - -def log_list(request): - # 列表页支持按场景筛选,方便演示时快速定位同一类场景的执行记录。 - scenario_id = (request.GET.get("scenario_id") or "").strip() - logs = AgentAuditLog.objects.all() - if scenario_id: - logs = logs.filter(scenario_id=scenario_id) - return render( - request, - "audit/log_list.html", - { - "logs": logs, - "selected_scenario_id": scenario_id, - }, - ) - - -def log_detail(request, log_id: int): - # 详情页只负责按主键加载审计快照并渲染; - # 所有脱敏和字段映射都应在服务层完成。 - audit_log = get_object_or_404(AgentAuditLog, pk=log_id) - return render(request, "audit/log_detail.html", {"log": audit_log}) diff --git a/apps/chat/__init__.py b/apps/chat/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/apps/chat/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/chat/apps.py b/apps/chat/apps.py deleted file mode 100644 index 0e8e60e..0000000 --- a/apps/chat/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ChatConfig(AppConfig): - """Chat 模块应用配置。""" - default_auto_field = "django.db.models.BigAutoField" - name = "apps.chat" diff --git a/apps/chat/forms.py b/apps/chat/forms.py deleted file mode 100644 index 5f5d286..0000000 --- a/apps/chat/forms.py +++ /dev/null @@ -1,40 +0,0 @@ -from django import forms - - -class ChatForm(forms.Form): - # 该表单只负责收集用户问题和可选文档范围, - # 不承载任何 Agent 业务逻辑,便于在 View 层保持轻量。 - message = forms.CharField( - label="问题", - max_length=4000, - error_messages={ - "required": "请输入要咨询的问题。", - "max_length": "问题过长,请控制在 4000 字以内。", - }, - widget=forms.Textarea( - attrs={ - "rows": 8, - "placeholder": "例如:请结合已上传 SOP,分析当前异常的原因、风险等级和建议动作。", - } - ), - ) - document_ids = forms.MultipleChoiceField( - label="文档范围", - required=False, - choices=(), - widget=forms.CheckboxSelectMultiple, - error_messages={"invalid_choice": "请选择当前场景下已入库的文档。"}, - ) - - def __init__(self, *args, documents=None, **kwargs): - super().__init__(*args, **kwargs) - documents = documents or [] - # 仅允许选择当前场景且已完成入库的文档, - # 避免前端把无效文件范围传入 Agent Core。 - self.fields["document_ids"].choices = [ - (str(document.id), document.original_name) for document in documents - ] - - def clean_document_ids(self): - # View 与 Agent Core 都使用整型文档 ID,统一在表单层完成转换。 - return [int(document_id) for document_id in self.cleaned_data.get("document_ids", [])] diff --git a/apps/chat/urls.py b/apps/chat/urls.py deleted file mode 100644 index 4ddfec7..0000000 --- a/apps/chat/urls.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.urls import path - -from . import views - - -app_name = "chat" - -# 当前 V1 仅保留一个场景对话入口,场景详情合并在对话页中展示。 -urlpatterns = [ - path("/", views.index, name="index"), -] diff --git a/apps/chat/views.py b/apps/chat/views.py deleted file mode 100644 index c7af5c2..0000000 --- a/apps/chat/views.py +++ /dev/null @@ -1,60 +0,0 @@ -from django.shortcuts import render - -from agent_core.orchestrator import run_agent -from agent_core.results import AgentResult -from apps.audit.services import create_audit_log -from apps.documents.models import UploadedDocument -from apps.scenarios.services import ScenarioNotFound, get_scenario - -from .forms import ChatForm - - -def index(request, scenario_id: str): - # View 只负责请求编排、表单校验和模板渲染。 - # 具体 Agent 执行、审计写入和文档筛选规则分别交给独立模块处理。 - try: - scenario = get_scenario(scenario_id) - except ScenarioNotFound: - return render( - request, - "chat/index.html", - { - "scenario": None, - "form": ChatForm(), - "error": "场景不存在,请返回首页检查配置。", - }, - status=404, - ) - - result = None - audit_log = None - documents = UploadedDocument.objects.filter( - scenario_id=scenario["id"], - status=UploadedDocument.STATUS_INDEXED, - ) - form = ChatForm(request.POST or None, documents=documents) - if request.method == "POST" and form.is_valid(): - message = form.cleaned_data["message"] - try: - # 只把必要的运行选项传给 Agent Core,避免在 View 中散落模型细节。 - result = run_agent( - scenario, - message, - options={"document_ids": form.cleaned_data["document_ids"]}, - ) - except Exception as exc: - result = AgentResult(status="failed", error=str(exc), answer="") - audit_log = create_audit_log(scenario["id"], scenario["name"], message, result) - - return render( - request, - "chat/index.html", - { - "scenario": scenario, - "form": form, - "documents": documents, - "document_count": documents.count(), - "result": result, - "audit_log": audit_log, - }, - ) diff --git a/apps/documents/__init__.py b/apps/documents/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/apps/documents/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/documents/admin.py b/apps/documents/admin.py deleted file mode 100644 index 872bd43..0000000 --- a/apps/documents/admin.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.contrib import admin - -from .models import UploadedDocument - - -@admin.register(UploadedDocument) -class UploadedDocumentAdmin(admin.ModelAdmin): - """管理上传文档及其入库状态,便于后台排查问题。""" - list_display = ("id", "original_name", "scenario_id", "file_type", "status", "created_at") - list_filter = ("status", "scenario_id", "file_type") - search_fields = ("original_name", "scenario_id") diff --git a/apps/documents/apps.py b/apps/documents/apps.py deleted file mode 100644 index 601f604..0000000 --- a/apps/documents/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class DocumentsConfig(AppConfig): - """Documents 模块应用配置。""" - default_auto_field = "django.db.models.BigAutoField" - name = "apps.documents" diff --git a/apps/documents/forms.py b/apps/documents/forms.py deleted file mode 100644 index 92f7f33..0000000 --- a/apps/documents/forms.py +++ /dev/null @@ -1,37 +0,0 @@ -from pathlib import Path - -from django import forms - -from apps.scenarios.services import ScenarioNotFound, get_scenario -from apps.scenarios.services import list_scenarios - -SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"} - - -class DocumentUploadForm(forms.Form): - # 使用 ChoiceField 让表单自己维护场景选项, - # 这样模板、校验和后续扩展都能围绕一个入口完成。 - scenario_id = forms.ChoiceField(label="场景", choices=()) - file = forms.FileField(label="文件") - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["scenario_id"].choices = [ - (scenario["id"], scenario["name"]) - for scenario in list_scenarios() - ] - - def clean_scenario_id(self): - scenario_id = self.cleaned_data["scenario_id"] - try: - get_scenario(scenario_id) - except ScenarioNotFound as exc: - raise forms.ValidationError("场景不存在") from exc - return scenario_id - - def clean_file(self): - uploaded_file = self.cleaned_data["file"] - extension = Path(uploaded_file.name).suffix.lower() - if extension not in SUPPORTED_EXTENSIONS: - raise forms.ValidationError("仅支持 .txt、.md、.pdf 和 .docx 文件") - return uploaded_file diff --git a/apps/documents/migrations/0001_initial.py b/apps/documents/migrations/0001_initial.py deleted file mode 100644 index c738384..0000000 --- a/apps/documents/migrations/0001_initial.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 5.2.14 on 2026-05-29 13:41 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="UploadedDocument", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("scenario_id", models.CharField(db_index=True, max_length=100)), - ("original_name", models.CharField(max_length=255)), - ("file", models.FileField(upload_to="documents/%Y%m%d/")), - ("file_type", models.CharField(max_length=20)), - ("size", models.PositiveIntegerField(default=0)), - ( - "status", - models.CharField(db_index=True, default="uploaded", max_length=20), - ), - ("error_message", models.TextField(blank=True)), - ("created_at", models.DateTimeField(auto_now_add=True)), - ("updated_at", models.DateTimeField(auto_now=True)), - ], - options={ - "ordering": ["-created_at"], - }, - ), - ] diff --git a/apps/documents/migrations/__init__.py b/apps/documents/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apps/documents/models.py b/apps/documents/models.py deleted file mode 100644 index e7fdcf5..0000000 --- a/apps/documents/models.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.db import models - - -class UploadedDocument(models.Model): - """ - 保存用户上传文档的元数据和入库状态。 - - 设计上只记录“文件属于哪个场景、当前是否已入库、失败原因是什么”, - 不把 RAG 细节耦合进模型层。 - """ - # 文档状态用于驱动前端提示和后续可操作项。 - STATUS_UPLOADED = "uploaded" - STATUS_INDEXED = "indexed" - STATUS_FAILED = "failed" - - scenario_id = models.CharField(max_length=100, db_index=True) - original_name = models.CharField(max_length=255) - file = models.FileField(upload_to="documents/%Y%m%d/") - file_type = models.CharField(max_length=20) - size = models.PositiveIntegerField(default=0) - status = models.CharField(max_length=20, default=STATUS_UPLOADED, db_index=True) - error_message = models.TextField(blank=True) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - ordering = ["-created_at"] - - def __str__(self) -> str: - return self.original_name - - def get_status_display_text(self) -> str: - """为模板提供更适合演示的中文状态文案。""" - return { - self.STATUS_UPLOADED: "已上传,待入库", - self.STATUS_INDEXED: "已入库,可检索", - self.STATUS_FAILED: "入库失败", - }.get(self.status, self.status) diff --git a/apps/documents/services.py b/apps/documents/services.py deleted file mode 100644 index a3b8e73..0000000 --- a/apps/documents/services.py +++ /dev/null @@ -1,127 +0,0 @@ -from pathlib import Path -import re -import xml.etree.ElementTree as ET -from zipfile import BadZipFile, ZipFile - -from agent_core.rag.ingest import ingest_document - -from .models import UploadedDocument - - -def create_uploaded_document(scenario_id: str, uploaded_file) -> UploadedDocument: - """ - 保存上传文件的元数据记录。 - - Documents 模块只记录文件与场景关系、原始名称、类型和大小, - 真正的入库动作由用户后续主动触发,避免上传阶段就耦合 RAG 流程。 - """ - extension = _detect_extension(uploaded_file.name) - return UploadedDocument.objects.create( - scenario_id=scenario_id, - original_name=uploaded_file.name, - file=uploaded_file, - file_type=extension, - size=uploaded_file.size, - status=UploadedDocument.STATUS_UPLOADED, - ) - - -def extract_text(document: UploadedDocument) -> str: - """ - 根据文档类型选择合适的文本抽取策略。 - - V1 的目标是“可演示且稳定”,因此: - - `.txt` / `.md` 直接按文本读取 - - `.pdf` 优先走 pypdf,失败时回退为二进制容错读取 - - `.docx` 优先解析 Word XML,失败时回退为二进制容错读取 - """ - path = Path(document.file.path) - extension = f".{document.file_type.lower().lstrip('.')}" - if extension == ".pdf": - return _extract_pdf_text(path) - if extension == ".docx": - return _extract_docx_text(path) - return _read_text_file(path) - - -def index_document(document: UploadedDocument) -> UploadedDocument: - """ - 触发单个文档入库,并把成功/失败状态回写到 UploadedDocument。 - - 这里故意不抛业务异常给 View: - View 层只需要知道“最终状态是什么”,而错误信息统一落到模型字段中, - 便于页面重试和演示。 - """ - try: - text = extract_text(document) - ingest_result = ingest_document( - document_id=document.id, - scenario_id=document.scenario_id, - source_file=document.original_name, - text=text, - collection=document.scenario_id, - ) - _apply_ingest_result(document, ingest_result.success, ingest_result.error) - except Exception as exc: - _apply_ingest_result(document, success=False, error=str(exc)) - document.save(update_fields=["status", "error_message", "updated_at"]) - return document - - -def _apply_ingest_result(document: UploadedDocument, success: bool, error: str = "") -> None: - """把入库结果映射为 UploadedDocument 的稳定状态字段。""" - if success: - document.status = UploadedDocument.STATUS_INDEXED - document.error_message = "" - return - document.status = UploadedDocument.STATUS_FAILED - document.error_message = error - - -def _detect_extension(file_name: str) -> str: - """统一将扩展名转成小写且去掉前导点,便于模型字段存储。""" - return Path(file_name).suffix.lower().lstrip(".") - - -def _read_text_file(path: Path) -> str: - """优先按 UTF-8 读取;失败时回退到系统默认编码。""" - try: - return path.read_text(encoding="utf-8") - except UnicodeDecodeError: - return path.read_text() - - -def _extract_pdf_text(path: Path) -> str: - """优先使用 pypdf 抽取 PDF 文本,失败时回退到容错方案。""" - try: - import pypdf - - reader = pypdf.PdfReader(str(path)) - return "\n".join(page.extract_text() or "" for page in reader.pages) - except Exception: - return _read_binary_text_fallback(path) - - -def _extract_docx_text(path: Path) -> str: - """提取 Word XML 中的可见文字内容,不追求保留样式。""" - try: - with ZipFile(path) as archive: - document_xml = archive.read("word/document.xml") - root = ET.fromstring(document_xml) - namespace = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} - texts = [node.text for node in root.findall(".//w:t", namespace) if node.text] - return "\n".join(texts) - except (BadZipFile, KeyError, ET.ParseError): - return _read_binary_text_fallback(path) - - -def _read_binary_text_fallback(path: Path) -> str: - """ - 当结构化抽取失败时,退回到“尽可能保留纯文本”的保底方案。 - - 该方案不保证版式,但足以支撑 V1 入库和演示。 - """ - data = path.read_bytes() - text = data.decode("utf-8", errors="ignore") - text = re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]+", " ", text) - return text.strip() diff --git a/apps/documents/urls.py b/apps/documents/urls.py deleted file mode 100644 index 4c4c7d4..0000000 --- a/apps/documents/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.urls import path - -from . import views - - -app_name = "documents" - -# 文档模块对外暴露三个基础动作:列表、上传、手动入库。 -urlpatterns = [ - path("", views.document_list, name="list"), - path("upload/", views.upload, name="upload"), - path("/index/", views.index, name="index"), -] diff --git a/apps/documents/views.py b/apps/documents/views.py deleted file mode 100644 index ee71adf..0000000 --- a/apps/documents/views.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.contrib import messages -from django.shortcuts import get_object_or_404, redirect, render -from django.views.decorators.http import require_POST - -from apps.scenarios.services import list_scenarios - -from .forms import DocumentUploadForm -from .models import UploadedDocument -from .services import create_uploaded_document, index_document - - -def document_list(request): - # 列表页只负责展示文档元数据和可执行操作,不处理入库细节。 - documents = UploadedDocument.objects.all() - return render(request, "documents/document_list.html", {"documents": documents}) - - -def upload(request): - # 上传成功后仅保存文件和元数据,是否入库由用户显式触发。 - if request.method == "POST": - form = DocumentUploadForm(request.POST, request.FILES) - if form.is_valid(): - create_uploaded_document(form.cleaned_data["scenario_id"], form.cleaned_data["file"]) - messages.success(request, "文件已上传,可继续执行入库。") - return redirect("documents:list") - else: - form = DocumentUploadForm() - return render( - request, - "documents/upload.html", - {"form": form, "scenarios": list_scenarios()}, - ) - - -@require_POST -def index(request, document_id: int): - document = get_object_or_404(UploadedDocument, pk=document_id) - document = index_document(document) - if document.status == UploadedDocument.STATUS_INDEXED: - messages.success(request, "文档入库成功,当前文档已可参与检索。") - else: - messages.error(request, "文档入库失败,请检查错误原因后重试。") - return redirect("documents:list") diff --git a/apps/scenarios/__init__.py b/apps/scenarios/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/apps/scenarios/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/scenarios/apps.py b/apps/scenarios/apps.py deleted file mode 100644 index fb9758c..0000000 --- a/apps/scenarios/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig - - -class ScenariosConfig(AppConfig): - """Scenarios 模块应用配置。""" - default_auto_field = "django.db.models.BigAutoField" - name = "apps.scenarios" diff --git a/apps/scenarios/services.py b/apps/scenarios/services.py deleted file mode 100644 index 1fdbf83..0000000 --- a/apps/scenarios/services.py +++ /dev/null @@ -1,110 +0,0 @@ -from pathlib import Path - -import yaml -from django.conf import settings - - -REQUIRED_FIELDS = [ - ("id",), - ("name",), - ("description",), - ("agent", "role"), - ("agent", "goal"), - ("agent", "instructions"), - ("rag", "enabled"), - ("tools",), - ("output", "type"), - ("audit", "enabled"), -] - - -class ScenarioNotFound(KeyError): - pass - - -class ScenarioValidationError(ValueError): - pass - - -def _get_nested(config: dict, path: tuple[str, ...]): - value = config - for key in path: - if not isinstance(value, dict) or key not in value: - raise ScenarioValidationError("缺失必填字段: " + ".".join(path)) - value = value[key] - return value - - -def validate_scenario(config: dict) -> dict: - # 仅校验真正影响运行闭环的必填字段; - # 页面展示字段允许缺失,并在归一化阶段补默认值。 - for field_path in REQUIRED_FIELDS: - _get_nested(config, field_path) - return normalize_scenario(config) - - -def normalize_scenario(config: dict) -> dict: - """补齐页面和其它模块常用的派生字段,避免模板层重复判断。""" - normalized = dict(config) - normalized["applicable_questions"] = list(config.get("applicable_questions") or []) - normalized["rag"] = dict(config.get("rag", {})) - normalized["rag"]["enabled"] = bool(normalized["rag"].get("enabled")) - normalized["tools"] = list(config.get("tools") or []) - normalized["tool_count"] = len(normalized["tools"]) - normalized["is_enabled"] = True - return normalized - - -def _scenario_files() -> list[Path]: - config_dir = Path(settings.SCENARIO_CONFIG_DIR) - if not config_dir.exists(): - return [] - return sorted([*config_dir.glob("*.yaml"), *config_dir.glob("*.yml")]) - - -def _read_yaml_file(path: Path) -> dict: - with path.open("r", encoding="utf-8") as file: - return yaml.safe_load(file) or {} - - -def _collect_scenario_load_result() -> tuple[list[dict], list[dict]]: - """ - 统一读取配置目录中的所有场景文件。 - - 返回值: - - scenarios: 校验通过的场景列表 - - issues: 非法 YAML / 缺字段等错误摘要,供首页展示 - """ - scenarios = [] - issues = [] - for path in _scenario_files(): - try: - config = _read_yaml_file(path) - scenarios.append(validate_scenario(config)) - except (yaml.YAMLError, ScenarioValidationError) as exc: - issues.append( - { - "file_name": path.name, - "message": str(exc), - } - ) - return scenarios, issues - - -def list_scenarios() -> list[dict]: - # 首页每次读取最新 YAML,便于复试现场快速改题。 - scenarios, _issues = _collect_scenario_load_result() - return scenarios - - -def list_scenario_issues() -> list[dict]: - """返回配置异常摘要,便于页面明确提示而不是直接 500。""" - _scenarios, issues = _collect_scenario_load_result() - return issues - - -def get_scenario(scenario_id: str) -> dict: - for scenario in list_scenarios(): - if scenario["id"] == scenario_id: - return scenario - raise ScenarioNotFound(f"场景不存在: {scenario_id}") diff --git a/apps/scenarios/urls.py b/apps/scenarios/urls.py deleted file mode 100644 index 6284dee..0000000 --- a/apps/scenarios/urls.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.urls import path - -from . import views - - -app_name = "scenarios" - -urlpatterns = [ - path("", views.index, name="index"), -] diff --git a/apps/scenarios/views.py b/apps/scenarios/views.py deleted file mode 100644 index 5d1d25b..0000000 --- a/apps/scenarios/views.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.shortcuts import render - -from .services import list_scenario_issues, list_scenarios - - -def index(request): - # 首页只消费服务层给出的场景摘要和错误摘要, - # 不自行读取 YAML,更不在 View 里做字段拼装。 - return render( - request, - "scenarios/index.html", - { - "scenarios": list_scenarios(), - "scenario_issues": list_scenario_issues(), - }, - ) diff --git a/configs/document_review.yaml b/configs/document_review.yaml deleted file mode 100644 index 9d2c593..0000000 --- a/configs/document_review.yaml +++ /dev/null @@ -1,22 +0,0 @@ -id: document_review -name: 文档审核助手 -description: 检查合同、制度或 SOP 中的风险点和缺失项 -applicable_questions: - - 合同审核 - - 制度审核 -agent: - role: 文档审核专家 - goal: 根据审核规则和知识库内容输出结构化审核意见 - instructions: - - 不确定的问题必须标记为需人工复核 - - 输出必须包含风险等级和修改建议 -rag: - enabled: true - collection: document_review - top_k: 5 -tools: - - check_required_fields -output: - type: document_review_report -audit: - enabled: true diff --git a/configs/knowledge_qa.yaml b/configs/knowledge_qa.yaml deleted file mode 100644 index 525c75c..0000000 --- a/configs/knowledge_qa.yaml +++ /dev/null @@ -1,22 +0,0 @@ -id: knowledge_qa -name: 知识库问答助手 -description: 用于 SOP、制度、客服知识库和内部文档问答 -applicable_questions: - - SOP 问答 - - 制度问答 -agent: - role: 知识库问答专家 - goal: 基于知识库内容回答用户问题 - instructions: - - 回答必须优先基于检索内容 - - 不确定时说明缺失信息 -rag: - enabled: true - collection: knowledge_qa - top_k: 5 -tools: - - generate_action_items -output: - type: general_answer -audit: - enabled: true diff --git a/configs/quality_analysis.yaml b/configs/quality_analysis.yaml deleted file mode 100644 index cb61b3d..0000000 --- a/configs/quality_analysis.yaml +++ /dev/null @@ -1,23 +0,0 @@ -id: quality_analysis -name: 质量异常分析助手 -description: 用于分析生产质量异常、检索 SOP、生成处理建议 -applicable_questions: - - 质量异常分析 - - 缺陷原因定位 -agent: - role: 质量管理专家 - goal: 根据用户问题、知识库和工具结果,输出可执行的质量分析报告 - instructions: - - 回答必须基于知识库或工具结果 - - 涉及质量风险时给出风险等级 -rag: - enabled: true - collection: quality_analysis - top_k: 5 -tools: - - query_demo_records - - calculate_rate -output: - type: quality_report -audit: - enabled: true diff --git a/configs/risk_audit.yaml b/configs/risk_audit.yaml deleted file mode 100644 index f0d3b64..0000000 --- a/configs/risk_audit.yaml +++ /dev/null @@ -1,24 +0,0 @@ -id: risk_audit -name: 风险审核助手 -description: 用于财务、采购、报销和合同风险审核 -applicable_questions: - - 财务审核 - - 采购审核 - - 合同风险分析 -agent: - role: 风险审核专家 - goal: 识别业务材料中的风险点并给出审核建议 - instructions: - - 风险点必须说明依据 - - 缺失材料要单独列出 -rag: - enabled: true - collection: risk_audit - top_k: 5 -tools: - - check_required_fields - - calculate_rate -output: - type: risk_audit_report -audit: - enabled: true diff --git a/configs/ticket_assistant.yaml b/configs/ticket_assistant.yaml deleted file mode 100644 index f6e7e91..0000000 --- a/configs/ticket_assistant.yaml +++ /dev/null @@ -1,23 +0,0 @@ -id: ticket_assistant -name: 工单处理助手 -description: 用于客服、售后和运维工单的分类与回复建议 -applicable_questions: - - 客服工单 - - 售后工单 -agent: - role: 工单处理专家 - goal: 判断工单类别、优先级并生成处理建议 - instructions: - - 需要人工处理时明确标记 - - 回复建议要简洁可执行 -rag: - enabled: true - collection: ticket_assistant - top_k: 5 -tools: - - query_demo_records - - generate_action_items -output: - type: ticket_response -audit: - enabled: true diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 6ddb3b7..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,10 +0,0 @@ -services: - web: - build: . - env_file: - - .env - ports: - - "8000:8000" - volumes: - - ./data:/app/data - - ./configs:/app/configs diff --git a/docs/superpowers/plans/2026-05-29-V1-Django基线实现计划.md b/docs/superpowers/plans/2026-05-29-V1-Django基线实现计划.md deleted file mode 100644 index ad8e26d..0000000 --- a/docs/superpowers/plans/2026-05-29-V1-Django基线实现计划.md +++ /dev/null @@ -1,343 +0,0 @@ -# V1 Django Baseline Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Build the smallest runnable Django baseline that satisfies the current Chinese requirements and design documents. - -**Architecture:** Use a Django monolith with four apps (`scenarios`, `documents`, `chat`, `audit`) plus an independent `agent_core` package. The first implementation returns deterministic mock Agent results so the UI, audit, documents and module boundaries can be verified before real RAG/LLM integration. - -**Tech Stack:** Python 3.13, Django 5.x, PyYAML, pytest, pytest-django, SQLite, Docker Compose. - ---- - -## File Structure - -- Create `requirements.txt`: runtime and test dependencies. -- Create `manage.py`, `config/settings.py`, `config/urls.py`, `config/wsgi.py`, `config/asgi.py`: Django project shell. -- Create `apps/scenarios/`: YAML scenario loading, homepage, tests. -- Create `apps/documents/`: upload model, upload/list/index views, text extraction, tests. -- Create `apps/chat/`: message form, chat view, Agent Core call, audit write, tests. -- Create `apps/audit/`: audit model, service, list/detail views, tests. -- Create `agent_core/`: dataclasses, orchestrator, mock RAG ingest/retrieve, tool registry, structured output parser. -- Create `configs/*.yaml`: five required scenarios. -- Create `templates/`: minimal Django Templates for pages. -- Create `Dockerfile`, `docker-compose.yml`, `.env.example`: one-command startup. - -## Task 1: Dependencies and Django Project Shell - -**Files:** -- Create: `requirements.txt` -- Create: `manage.py` -- Create: `config/__init__.py` -- Create: `config/settings.py` -- Create: `config/urls.py` -- Create: `config/wsgi.py` -- Create: `config/asgi.py` -- Test: `pytest.ini` - -- [ ] **Step 1: Write failing configuration test** - -Create `tests/test_project_configuration.py`: - -```python -from django.conf import settings -from django.urls import reverse - - -def test_core_settings_expose_documented_paths(): - assert settings.SCENARIO_CONFIG_DIR.name == "configs" - assert settings.CHROMA_PATH.name == "chroma" - assert settings.MEDIA_ROOT.name == "uploads" - - -def test_home_url_is_registered(client): - response = client.get(reverse("scenarios:index")) - assert response.status_code == 200 -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pytest tests/test_project_configuration.py -q` -Expected: FAIL because Django project and apps do not exist. - -- [ ] **Step 3: Implement minimal project shell** - -Add dependencies and project files with settings for installed apps, templates, SQLite, media paths, and URL includes. - -- [ ] **Step 4: Run test to verify progress** - -Run: `pytest tests/test_project_configuration.py -q` -Expected: either PASS or fail only because `apps.scenarios` is not implemented yet. - -## Task 2: Scenarios Module and Five YAML Configs - -**Files:** -- Create: `apps/scenarios/services.py` -- Create: `apps/scenarios/views.py` -- Create: `apps/scenarios/urls.py` -- Create: `apps/scenarios/apps.py` -- Create: `configs/knowledge_qa.yaml` -- Create: `configs/document_review.yaml` -- Create: `configs/ticket_assistant.yaml` -- Create: `configs/quality_analysis.yaml` -- Create: `configs/risk_audit.yaml` -- Create: `templates/scenarios/index.html` -- Test: `tests/test_scenarios.py` - -- [ ] **Step 1: Write failing scenario tests** - -```python -from apps.scenarios.services import get_scenario, list_scenarios - - -def test_list_scenarios_loads_five_configs(): - scenarios = list_scenarios() - assert [scenario["id"] for scenario in scenarios] == [ - "knowledge_qa", - "document_review", - "ticket_assistant", - "quality_analysis", - "risk_audit", - ] - - -def test_get_scenario_returns_full_agent_config(): - scenario = get_scenario("quality_analysis") - assert scenario["agent"]["role"] - assert scenario["rag"]["enabled"] is True - assert scenario["output"]["type"] == "quality_report" -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pytest tests/test_scenarios.py -q` -Expected: FAIL because services/configs are missing. - -- [ ] **Step 3: Implement scenario loader and homepage** - -Use `yaml.safe_load()`, validate required fields, and render scenario cards on `/`. - -- [ ] **Step 4: Run tests** - -Run: `pytest tests/test_scenarios.py tests/test_project_configuration.py -q` -Expected: PASS. - -## Task 3: Agent Core Mock Orchestrator - -**Files:** -- Create: `agent_core/results.py` -- Create: `agent_core/orchestrator.py` -- Create: `agent_core/structured_output.py` -- Create: `agent_core/tool_registry.py` -- Create: `agent_core/tools/builtin_tools.py` -- Create: `agent_core/rag/ingest.py` -- Create: `agent_core/rag/retriever.py` -- Test: `tests/test_agent_core.py` - -- [ ] **Step 1: Write failing Agent Core tests** - -```python -from agent_core.orchestrator import run_agent - - -def test_run_agent_returns_structured_mock_result(): - scenario = { - "id": "knowledge_qa", - "name": "知识库问答助手", - "rag": {"enabled": True, "collection": "knowledge_qa", "top_k": 3}, - "tools": ["generate_action_items"], - "output": {"type": "general_answer"}, - } - result = run_agent(scenario, "如何处理异常?") - assert result.status == "success" - assert result.answer - assert result.structured_output["output_type"] == "general_answer" - assert result.tool_calls[0]["tool_name"] == "generate_action_items" -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pytest tests/test_agent_core.py -q` -Expected: FAIL because `agent_core` is missing. - -- [ ] **Step 3: Implement deterministic mock AgentResult** - -Return stable answer, references, tool calls, model name `mock-model`, and latency. - -- [ ] **Step 4: Run tests** - -Run: `pytest tests/test_agent_core.py -q` -Expected: PASS. - -## Task 4: Audit Module - -**Files:** -- Create: `apps/audit/models.py` -- Create: `apps/audit/services.py` -- Create: `apps/audit/views.py` -- Create: `apps/audit/urls.py` -- Create: `apps/audit/admin.py` -- Create: `templates/audit/log_list.html` -- Create: `templates/audit/log_detail.html` -- Test: `tests/test_audit.py` - -- [ ] **Step 1: Write failing audit tests** - -```python -from apps.audit.models import AgentAuditLog -from apps.audit.services import create_audit_log -from agent_core.results import AgentResult - - -def test_create_audit_log_records_success_result(db): - result = AgentResult(answer="回答", structured_output={"x": 1}, status="success") - log = create_audit_log("knowledge_qa", "知识库问答助手", "问题", result) - assert AgentAuditLog.objects.count() == 1 - assert log.final_answer == "回答" - assert log.structured_output == {"x": 1} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pytest tests/test_audit.py -q` -Expected: FAIL because audit app is missing. - -- [ ] **Step 3: Implement model, service, admin, views** - -Use JSONField defaults and avoid storing sensitive environment values. - -- [ ] **Step 4: Run migrations and tests** - -Run: `python manage.py makemigrations audit && pytest tests/test_audit.py -q` -Expected: PASS. - -## Task 5: Documents Module - -**Files:** -- Create: `apps/documents/models.py` -- Create: `apps/documents/services.py` -- Create: `apps/documents/forms.py` -- Create: `apps/documents/views.py` -- Create: `apps/documents/urls.py` -- Create: `apps/documents/admin.py` -- Create: `templates/documents/document_list.html` -- Create: `templates/documents/upload.html` -- Test: `tests/test_documents.py` - -- [ ] **Step 1: Write failing document tests** - -```python -from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse - -from apps.documents.models import UploadedDocument - - -def test_upload_txt_document_creates_uploaded_record(client, db): - file = SimpleUploadedFile("rules.txt", "hello".encode("utf-8"), content_type="text/plain") - response = client.post(reverse("documents:upload"), {"scenario_id": "knowledge_qa", "file": file}) - assert response.status_code == 302 - document = UploadedDocument.objects.get() - assert document.status == "uploaded" - assert document.file_type == "txt" -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pytest tests/test_documents.py -q` -Expected: FAIL because documents app is missing. - -- [ ] **Step 3: Implement upload/list/index flow** - -Support `.txt` and `.md`; index action calls `agent_core.rag.ingest.ingest_document()` and updates status. - -- [ ] **Step 4: Run migrations and tests** - -Run: `python manage.py makemigrations documents && pytest tests/test_documents.py -q` -Expected: PASS. - -## Task 6: Chat Module - -**Files:** -- Create: `apps/chat/forms.py` -- Create: `apps/chat/views.py` -- Create: `apps/chat/urls.py` -- Create: `apps/chat/apps.py` -- Create: `templates/chat/index.html` -- Test: `tests/test_chat.py` - -- [ ] **Step 1: Write failing chat tests** - -```python -from django.urls import reverse - -from apps.audit.models import AgentAuditLog - - -def test_chat_post_returns_agent_result_and_audit_log(client, db): - response = client.post(reverse("chat:index", args=["knowledge_qa"]), {"message": "如何处理异常?"}) - assert response.status_code == 200 - assert "mock-model" in response.content.decode("utf-8") - assert AgentAuditLog.objects.count() == 1 -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pytest tests/test_chat.py -q` -Expected: FAIL because chat app is missing. - -- [ ] **Step 3: Implement chat form and view** - -Validate message, call `get_scenario()`, `run_agent()`, then `create_audit_log()`. - -- [ ] **Step 4: Run tests** - -Run: `pytest tests/test_chat.py tests/test_audit.py tests/test_agent_core.py -q` -Expected: PASS. - -## Task 7: Docker and Documentation Alignment - -**Files:** -- Create: `.env.example` -- Create: `Dockerfile` -- Create: `docker-compose.yml` -- Modify: `README.md` -- Test: all tests and Django checks. - -- [ ] **Step 1: Add deployment files** - -Use a single web service, install `requirements.txt`, run migrations, and serve `0.0.0.0:8000`. - -- [ ] **Step 2: Verify Django and tests** - -Run: - -```bash -python manage.py check -pytest -q -``` - -Expected: all checks pass. - -- [ ] **Step 3: Verify docs path references** - -Run: - -```powershell -$patterns = @('docs/需求分析', 'docs/设计文档', 'V1总需求文档', '智能体总体设计') -Get-ChildItem -Recurse -File | - Where-Object { - $_.FullName -notlike '*\.git\*' -and - $_.FullName -notlike '*\.idea\*' -and - $_.FullName -notlike '*docs\superpowers\plans\2026-05-29-v1-django-baseline.md' - } | - Select-String -Pattern $patterns -``` - -Expected: no matches. - -## Self-Review - -- Spec coverage: The plan covers Chinese docs, five scenario configs, Django startup, homepage, chat, audit, documents, Agent Core, and Docker baseline. -- Placeholder scan: No implementation step relies on an undefined placeholder; mock LLM/RAG is intentionally scoped as the first runnable baseline. -- Type consistency: Tests use `AgentResult`, `run_agent`, `list_scenarios`, `get_scenario`, `UploadedDocument`, and `AgentAuditLog` consistently. diff --git a/docs/设计文档/0.设计文档索引.md b/docs/设计文档/0.设计文档索引.md deleted file mode 100644 index 6629cf1..0000000 --- a/docs/设计文档/0.设计文档索引.md +++ /dev/null @@ -1,71 +0,0 @@ -# 模块详细设计文档索引 - -## 1. 设计文档说明 - -本目录存放 Universal Agent Demo Framework V1 的设计文档。需求文档回答“要做什么”,设计文档回答“怎么实现、边界在哪里、如何验证”。 - -文档命名统一使用中文编号,便于复试讲解和按顺序阅读。 - -## 2. 模块设计文档列表 - -| 顺序 | 文档 | 说明 | -|---|---|---| -| 0 | `0.设计文档索引.md` | 当前索引 | -| 1 | `1.智能体总体设计.md` | 智能核心总体链路、配置、输出和 Adapter | -| 2 | `2.功能流程设计.md` | 复试准备、演示、上传、入库、对话和审计流程 | -| 3 | `3.数据库设计.md` | Django 数据模型、字段、索引和初始化策略 | -| 4 | `4.页面与路由设计.md` | 页面结构、URL、跳转和异常状态 | -| 5 | `5.部署设计.md` | 本地、Docker、环境变量和持久化 | - -模块详细设计位于 `模块设计/`: - -| 模块 | 文档 | -|---|---| -| 配置 | `模块设计/1.配置模块详细设计.md` | -| 场景 | `模块设计/2.场景模块详细设计.md` | -| 文档 | `模块设计/3.文档模块详细设计.md` | -| 对话 | `模块设计/4.对话模块详细设计.md` | -| 审计 | `模块设计/5.审计模块详细设计.md` | -| 智能核心 | `模块设计/6.智能核心模块详细设计.md` | - -## 3. 模块依赖关系 - -```text -config - |-- apps.scenarios - |-- apps.documents - |-- apps.chat - |-- apps.audit - -apps.scenarios - |-- reads configs/*.yaml - -apps.documents - |-- depends on apps.scenarios - |-- calls agent_core.rag.ingest - -apps.chat - |-- depends on apps.scenarios - |-- calls agent_core.orchestrator - |-- calls apps.audit.services - -apps.audit - |-- stores AgentResult snapshots - -agent_core - |-- consumes scenario config - |-- uses RAG, tools, LLM provider and structured output parser -``` - -## 4. 推荐阅读顺序 - -1. `docs/需求分析/1.V1总需求文档.md` -2. `docs/需求分析/2.模块需求索引.md` -3. `docs/设计文档/1.智能体总体设计.md` -4. `docs/设计文档/2.功能流程设计.md` -5. `docs/设计文档/3.数据库设计.md` -6. `docs/设计文档/4.页面与路由设计.md` -7. `docs/设计文档/5.部署设计.md` -8. `docs/设计文档/模块设计/*.md` - -后续编码时,每个模块应先对照对应需求文档和详细设计,再实现模型、服务、视图和测试。 diff --git a/docs/设计文档/1.智能体总体设计.md b/docs/设计文档/1.智能体总体设计.md deleted file mode 100644 index bb9ef1f..0000000 --- a/docs/设计文档/1.智能体总体设计.md +++ /dev/null @@ -1,211 +0,0 @@ -# 智能体总体设计文档 - -## 1. 设计目标 - -Agent 设计的核心目标是支持未知复试题的快速适配。 - -系统不针对单一业务写死,而是通过场景配置、知识库、工具和输出模板组合出不同业务 Agent。 - -```text -业务 Agent = 场景配置 + 知识库 + 工具集 + 输出模板 + 审计日志 + 模型适配器 -``` - -## 2. Agent 类型 - -V1 预置 5 类 Agent 场景: - -| Agent ID | 名称 | 适用场景 | -|---|---|---| -| `knowledge_qa` | 知识库问答助手 | SOP、制度、客服知识库 | -| `document_review` | 文档审核助手 | 合同、制度、SOP、材料审核 | -| `ticket_assistant` | 工单处理助手 | 客服、售后、运维工单 | -| `quality_analysis` | 质量异常分析助手 | 生产、质检、缺陷分析 | -| `risk_audit` | 风险审核助手 | 财务、采购、报销、合同风险 | - -## 3. Agent 执行链路 - -```text -用户输入 - ↓ -加载场景配置 - ↓ -判断是否启用 RAG - ↓ -检索知识库片段 - ↓ -加载可用工具 - ↓ -构造 Prompt - ↓ -调用大模型 - ↓ -解析工具调用和结构化输出 - ↓ -生成 AgentResult - ↓ -写入审计日志 - ↓ -页面展示 -``` - -## 4. 场景配置结构 - -场景配置使用 YAML,V1 以配置文件作为场景唯一事实来源,后台管理不作为场景配置入口。 - -```yaml -id: quality_analysis -name: 质量异常分析助手 -description: 用于分析生产质量异常、检索 SOP、生成处理建议 - -agent: - role: 质量管理专家 - goal: 根据用户问题、知识库和工具结果,输出可执行的质量分析报告 - system_prompt: "" - instructions: - - 回答必须基于知识库或工具结果 - - 不确定时必须说明缺失信息 - - 涉及质量风险时给出风险等级 - -rag: - enabled: true - collection: quality_docs - top_k: 5 - -tools: - - query_demo_records - - calculate_rate - -output: - type: quality_report - -audit: - enabled: true - log_retrieval: true - log_tool_calls: true -``` - -## 5. Prompt 组成 - -Prompt 建议由以下部分组成: - -```text -系统角色 -任务目标 -行为约束 -输出格式要求 -知识库检索内容 -工具调用结果 -用户问题 -``` - -V1 不追求复杂 Prompt 框架,优先保证可读、可改、可解释。 - -## 6. RAG 策略 - -RAG 在 V1 中负责给 Agent 提供题目材料和业务知识。 - -入库流程: - -```text -上传文件 - ↓ -抽取文本 - ↓ -文本切分 - ↓ -生成 embedding - ↓ -写入 Chroma -``` - -检索流程: - -```text -用户问题 - ↓ -按 scenario_id 和可选 document_ids 过滤 - ↓ -向量检索 top_k - ↓ -返回片段内容、来源和分数 -``` - -## 7. 工具调用策略 - -工具用于补足大模型不能直接可靠完成的业务动作。 - -V1 内置工具: - -| 工具 | 用途 | -|---|---| -| `calculate_rate` | 计算比例、缺陷率、通过率 | -| `query_demo_records` | 查询模拟业务数据 | -| `check_required_fields` | 检查必填项 | -| `generate_action_items` | 生成行动项 | - -工具返回格式: - -```json -{ - "tool_name": "calculate_rate", - "success": true, - "arguments": {}, - "result": {}, - "error": "" -} -``` - -## 8. 结构化输出 - -V1 支持以下输出类型: - -- `general_answer` -- `document_review_report` -- `ticket_response` -- `quality_report` -- `risk_audit_report` - -结构化输出优先使用 JSON。 - -解析失败时: - -- 保留模型原始输出。 -- 返回解析错误。 -- 页面展示原始回答。 -- 审计日志记录失败原因。 - -## 9. AgentResult - -Agent Core 统一返回: - -```json -{ - "answer": "", - "structured_output": {}, - "references": [], - "tool_calls": [], - "raw_output": "", - "model_name": "", - "latency_ms": 0, - "status": "success", - "error": "" -} -``` - -## 10. Adapter 策略 - -V1 默认使用自研轻量 Orchestrator,通过 OpenAI 兼容接口接入 LLM 与 Embedding,可自主选择 OpenAI、硅基流动等兼容服务。 - -后续可以扩展: - -- OpenAI Agents SDK Adapter。 -- Dify API Adapter。 -- LangGraph Adapter。 - -所有 Adapter 应保持统一接口: - -```text -run_agent(scenario_config, user_input, options=None) -> AgentResult -``` - -这样可以保证 Django 业务层不受底层 Agent 编排实现影响。 diff --git a/docs/设计文档/2.功能流程设计.md b/docs/设计文档/2.功能流程设计.md deleted file mode 100644 index 88b2fc7..0000000 --- a/docs/设计文档/2.功能流程设计.md +++ /dev/null @@ -1,169 +0,0 @@ -# V1 功能设计文档 - -## 1. 功能设计目标 - -V1 的功能设计目标是让复试展示者在本地快速完成一个可讲解、可演示、可改题的 Agent Demo。系统不追求复杂平台能力,而是优先保证以下闭环稳定: - -- 场景配置可选择。 -- 文档可上传并入库。 -- 用户可在场景下发起对话。 -- Agent 可返回结构化结果、引用来源和工具调用记录。 -- 每次成功或失败的对话都有审计记录。 -- 本地和 Docker 均可启动。 - -## 2. 用户角色 - -V1 仅设计一个用户角色:Demo 操作者。 - -该角色负责启动系统、选择场景、上传材料、触发入库、发起对话、查看输出和审计日志。系统不在 V1 中区分管理员、审核员、普通用户等权限角色。 - -## 3. 核心业务流程 - -```text -启动系统 - ↓ -查看 5 个预置场景 - ↓ -选择场景 - ↓ -上传题目材料 - ↓ -触发知识库入库 - ↓ -发起 Agent 对话 - ↓ -查看结构化输出、引用和工具调用 - ↓ -查看审计日志 -``` - -任一环节失败时,页面应给出明确提示,并尽量保留用户已完成的上下文。 - -## 4. 场景选择流程 - -1. 首页调用 `apps.scenarios.services.list_scenarios()`。 -2. 服务从 `configs/` 读取 YAML 场景配置。 -3. 校验必填字段、工具名称和输出类型。 -4. 页面展示场景名称、描述、适用题型、启用状态。 -5. 用户点击进入 `/chat//`。 - -异常处理: - -- 配置目录不存在:展示空状态和配置目录提示。 -- 单个配置非法:不阻断其他配置,页面展示该配置错误。 -- 场景不存在:跳转或渲染错误页,提示检查场景 ID。 - -## 5. 文件上传流程 - -1. 用户进入 `/documents/upload/`。 -2. 页面加载可用场景下拉框。 -3. 用户选择场景并上传 `.txt`、`.md`、`.pdf` 或 `.docx` 文件。 -4. Documents 模块校验文件类型和大小。 -5. 保存文件到 `UPLOAD_ROOT//`。 -6. 写入 `UploadedDocument` 记录,状态为 `uploaded`。 -7. 返回文件列表页并展示上传结果。 - -V1 文件上传默认手动入库,避免上传大文件时页面阻塞过久。 - -## 6. 文档入库流程 - -1. 用户在文件列表点击“入库”。 -2. Documents 模块读取文件并抽取文本。 -3. 调用 `agent_core.rag.ingest.ingest_document()`。 -4. Agent Core 按固定长度切分文本。 -5. 写入本地 Chroma collection。 -6. 入库成功:更新状态为 `indexed`。 -7. 入库失败:更新状态为 `failed`,保存错误信息。 - -文本为空、文件丢失、向量库不可写都应进入失败状态,不能让页面报 500。 - -## 7. Agent 对话流程 - -```text -用户提交问题 - ↓ -Chat 表单校验 - ↓ -Scenarios 加载场景配置 - ↓ -Agent Core 执行 run_agent() - ↓ -RAG 按场景和可选文档范围检索知识片段 - ↓ -工具系统执行可用工具 - ↓ -LLM Provider 生成结果 - ↓ -结构化输出解析 - ↓ -Audit 写入日志 - ↓ -Chat 页面展示结果 -``` - -Chat 模块只负责请求处理和页面展示,不直接写 RAG、工具和模型调用细节。 - -## 8. RAG 检索流程 - -1. Orchestrator 读取场景配置中的 `rag.enabled`、`collection`、`top_k`。 -2. 若启用 RAG,则调用 `agent_core.rag.retriever.retrieve()`。 -3. 检索必须按 `scenario_id` 过滤,避免跨场景污染。 -4. 如果用户在对话页选择了文档,则同时按 `document_ids` 过滤;未选择时使用当前场景全部已入库文档。 -5. 返回片段内容、来源文件、chunk ID、分数。 -6. 片段进入 Prompt,同时随 AgentResult 返回给页面和审计日志。 - -检索失败时,AgentResult 应记录错误或警告;若业务允许,可继续使用非 RAG 上下文回答。 - -## 9. 工具调用流程 - -1. 场景配置声明可用工具名称。 -2. Orchestrator 从 Tool Registry 查询工具。 -3. 对不可用工具记录失败,不中断整个流程。 -4. 内置工具按统一参数和返回结构执行。 -5. 工具结果进入 Prompt 或结构化输出上下文。 -6. 所有工具调用写入 AgentResult 和审计日志。 - -V1 先采用“配置声明 + Orchestrator 决策”的轻量策略,不实现复杂多轮工具调用协议。 - -## 10. 审计日志流程 - -1. Chat 模块在 Agent Core 返回后调用 `apps.audit.services.create_audit_log()`。 -2. 成功结果记录输入、输出、引用、工具调用、模型名和耗时。 -3. 失败结果也记录场景、输入、错误信息和已产生的中间结果。 -4. 日志中不得保存 `LLM_API_KEY`、环境变量完整内容或上传文件绝对敏感路径。 -5. 审计列表展示摘要,详情页展示完整 JSON 片段。 - -## 11. 复试改题流程 - -1. 判断题目最接近的模板。 -2. 复制 `configs/` 中相近 YAML。 -3. 修改场景名称、角色、目标、指令和输出类型。 -4. 上传题目文档并入库。 -5. 如题目需要计算或查询,新增一个内置工具并在场景中声明。 -6. 用 2 到 3 个问题验证输出和审计链路。 -7. 演示时重点展示配置、知识库、工具调用、结构化结果和审计日志。 - -## 12. 异常处理流程 - -| 异常 | 处理方式 | -|---|---| -| 场景配置缺失 | 页面展示错误,保留返回首页入口 | -| 场景字段非法 | 标记非法配置,不影响其他场景 | -| 上传文件类型不支持 | 表单错误提示 | -| 文件读取失败 | 文档状态改为 `failed` | -| RAG 入库失败 | 记录错误信息并允许重试 | -| LLM 配置缺失 | AgentResult 返回失败,审计日志记录失败 | -| 工具调用失败 | 记录工具失败,流程尽量继续 | -| 结构化解析失败 | 展示原始输出并记录解析错误 | - -## 13. V1 功能验收标准 - -- 首页可以展示 5 个预置场景。 -- 场景配置来自 YAML 文件。 -- 可以上传 `.txt`、`.md`、`.pdf` 和 `.docx` 文件。 -- 文件可触发入库,并显示 `uploaded`、`indexed`、`failed` 状态。 -- 可以进入任一场景对话页并提交问题。 -- AgentResult 至少包含回答、结构化输出、引用、工具调用、耗时和状态。 -- 成功和失败对话都能生成审计日志。 -- 审计详情可以解释一次 Agent 输出的输入、依据和过程。 -- 本地启动和 Docker 启动路径清晰可执行。 diff --git a/docs/设计文档/3.数据库设计.md b/docs/设计文档/3.数据库设计.md deleted file mode 100644 index cc061ac..0000000 --- a/docs/设计文档/3.数据库设计.md +++ /dev/null @@ -1,144 +0,0 @@ -# V1 数据库设计文档 - -## 1. 数据库设计目标 - -V1 数据库设计优先服务本地演示、讲解清晰和快速改题。数据模型只覆盖文件、对话、审计和简单示例业务数据,不引入复杂权限、多租户或工作流状态机。 - -## 2. 数据库选型 - -默认使用 SQLite,数据库文件位于 `data/db.sqlite3`。SQLite 适合复试现场单机运行,便于 Docker 挂载和备份。 - -后续如需多人协作或更正式部署,可通过 Django settings 切换到 PostgreSQL,但 V1 不强制实现。 - -## 3. 表结构总览 - -| 表 | Django Model | 模块 | 说明 | -|---|---|---|---| -| uploaded_document | `UploadedDocument` | Documents | 上传文件元数据和入库状态 | -| agent_audit_log | `AgentAuditLog` | Audit | Agent 执行审计快照 | -| demo_business_record | `DemoBusinessRecord` | Agent Core / Tools | 内置工具可查询的模拟业务数据 | -| chat_session | `ChatSession` | Chat | 可选,对话会话 | -| chat_message | `ChatMessage` | Chat | 可选,对话消息 | - -## 4. UploadedDocument 表设计 - -| 字段 | 类型 | 约束 | 说明 | -|---|---|---|---| -| id | BigAutoField | PK | 主键 | -| scenario_id | CharField(100) | indexed | 关联场景 ID | -| original_name | CharField(255) | required | 原始文件名 | -| file | FileField | required | 文件相对路径 | -| file_type | CharField(20) | required | `txt`、`md`、`pdf`、`docx` 等 | -| size | PositiveIntegerField | default 0 | 字节数 | -| status | CharField(20) | indexed | `uploaded`、`indexed`、`failed` | -| error_message | TextField | blank | 入库失败原因 | -| created_at | DateTimeField | auto_now_add | 上传时间 | -| updated_at | DateTimeField | auto_now | 更新时间 | - -状态流转: - -```text -uploaded -> indexed -uploaded -> failed -failed -> indexed -failed -> failed -``` - -重新入库时应按文档维度覆盖或清理旧 chunk,避免同一文件重复出现在向量检索结果中。文档选择范围由 Chat 表单本次提交的 `document_ids` 传入 Agent Core,V1 不需要为该选择单独建表。 - -## 5. AgentAuditLog 表设计 - -| 字段 | 类型 | 约束 | 说明 | -|---|---|---|---| -| id | BigAutoField | PK | 主键 | -| scenario_id | CharField(100) | indexed | 场景 ID | -| scenario_name | CharField(200) | blank | 场景名称快照 | -| user_input | TextField | required | 用户输入 | -| retrieved_chunks | JSONField | default list | RAG 引用片段 | -| tool_calls | JSONField | default list | 工具调用记录 | -| structured_output | JSONField | default dict | 结构化输出 | -| final_answer | TextField | blank | 最终回答 | -| raw_output | TextField | blank | 模型原始输出 | -| model_name | CharField(100) | blank | 模型名称 | -| latency_ms | PositiveIntegerField | default 0 | 执行耗时 | -| status | CharField(20) | indexed | `success`、`failed` | -| error_message | TextField | blank | 错误信息 | -| created_at | DateTimeField | auto_now_add, indexed | 创建时间 | - -审计日志保存的是执行快照,不依赖场景配置后续是否被修改。 - -## 6. DemoBusinessRecord 表设计 - -| 字段 | 类型 | 约束 | 说明 | -|---|---|---|---| -| id | BigAutoField | PK | 主键 | -| scenario_id | CharField(100) | indexed | 适用场景 | -| record_type | CharField(100) | indexed | 记录类型,如 defect、ticket、invoice | -| title | CharField(255) | required | 标题 | -| payload | JSONField | default dict | 模拟业务数据 | -| created_at | DateTimeField | auto_now_add | 创建时间 | - -该表为 V1 必需表,用于 `query_demo_records` 工具,避免工具只能返回硬编码数据。Django Admin 可以管理该表的数据,场景 YAML 仍不在 Admin 中编辑。 - -## 7. ChatSession 表设计 - -V1 可先不实现会话持久化。如果实现,字段建议如下: - -| 字段 | 类型 | 说明 | -|---|---|---| -| id | BigAutoField | 主键 | -| scenario_id | CharField(100) | 场景 ID | -| title | CharField(255) | 会话标题 | -| created_at | DateTimeField | 创建时间 | -| updated_at | DateTimeField | 更新时间 | - -## 8. ChatMessage 表设计 - -V1 可通过审计日志满足演示追踪,不强制实现消息表。如果实现,字段建议如下: - -| 字段 | 类型 | 说明 | -|---|---|---| -| id | BigAutoField | 主键 | -| session | ForeignKey(ChatSession) | 所属会话 | -| role | CharField(20) | `user`、`assistant`、`system` | -| content | TextField | 消息内容 | -| audit_log | ForeignKey(AgentAuditLog, null=True) | 关联审计 | -| created_at | DateTimeField | 创建时间 | - -## 9. 表关系设计 - -```text -Scenario YAML - |-- scenario_id - |-- UploadedDocument.scenario_id - |-- AgentAuditLog.scenario_id - |-- DemoBusinessRecord.scenario_id - |-- ChatSession.scenario_id - -ChatSession 1 -- N ChatMessage -ChatMessage 0/1 -- 1 AgentAuditLog -``` - -场景配置 V1 存在 YAML 中,不建 `Scenario` 数据表。这样更方便复试现场复制和修改配置文件。 - -## 10. 索引设计 - -- `UploadedDocument(scenario_id, status)`:用于按场景查看文件和入库状态。 -- `AgentAuditLog(scenario_id, created_at)`:用于按场景查看最近日志。 -- `AgentAuditLog(status, created_at)`:用于排查失败日志。 -- `DemoBusinessRecord(scenario_id, record_type)`:用于工具查询模拟数据。 - -## 11. 数据初始化策略 - -- 场景初始化:读取 `configs/*.yaml`,不写数据库。 -- 示例业务数据:可提供 Django management command 初始化 `DemoBusinessRecord`。 -- 超级用户:本地演示可手动创建,Docker 可通过说明引导创建。 -- 上传文件和 Chroma 数据:存放在 `data/` 下,通过 Docker volume 持久化。 - -## 12. 后续扩展方向 - -- 增加 `Scenario` 表,实现后台编辑场景。 -- 增加 `ToolCallLog` 独立表,用于复杂工具审计。 -- 使用 PostgreSQL JSONB 优化 JSON 查询。 -- 增加用户和权限模型。 -- 增加文档 chunk 元数据表,便于从数据库追踪向量库内容。 diff --git a/docs/设计文档/4.页面与路由设计.md b/docs/设计文档/4.页面与路由设计.md deleted file mode 100644 index d8f5bdb..0000000 --- a/docs/设计文档/4.页面与路由设计.md +++ /dev/null @@ -1,179 +0,0 @@ -# V1 页面与路由设计文档 - -## 1. 页面设计目标 - -V1 页面使用 Django Templates,优先保证清晰、稳定、可讲解。页面应围绕复试演示的主路径组织:选择场景、上传文档、入库、对话、查看审计。 - -## 2. 页面列表 - -| 页面 | 路径 | 模块 | 说明 | -|---|---|---|---| -| 首页/场景列表 | `/` | Scenarios | 展示 5 个预置场景 | -| Agent 对话页 | `/chat//` | Chat | 提交问题并展示结果 | -| 文件列表页 | `/documents/` | Documents | 查看上传文件和入库状态 | -| 文件上传页 | `/documents/upload/` | Documents | 上传题目材料 | -| 文档入库动作 | `/documents//index/` | Documents | POST 触发入库 | -| 审计日志列表 | `/audit/` | Audit | 查看对话记录 | -| 审计日志详情 | `/audit//` | Audit | 查看单次执行详情 | -| Django Admin | `/admin/` | Config | 后台管理 | - -## 3. 路由总览 - -```text -config.urls - |-- "" -> apps.scenarios.urls - |-- "chat/" -> apps.chat.urls - |-- "documents/" -> apps.documents.urls - |-- "audit/" -> apps.audit.urls - |-- "admin/" -> django.contrib.admin -``` - -各模块只暴露自己的 URL,避免把业务路由集中写在 `config.urls` 中。 - -## 4. 首页与场景列表页 - -路径:`/` - -展示内容: - -- 系统名称和简短定位。 -- 5 个场景卡片或列表。 -- 场景名称、描述、适用题型、启用状态。 -- “进入对话”按钮。 -- 文件管理和审计日志入口。 - -错误状态: - -- 没有可用场景:展示配置目录提示。 -- 配置读取失败:展示失败原因和文件名。 - -## 5. Agent 对话页 - -路径:`/chat//` - -页面区域: - -- 场景摘要:名称、角色、目标、RAG 状态、工具列表。 -- 文档范围:当前场景下状态为 `indexed` 的文档多选框;未选择时默认使用全部已入库文档。 -- 输入区:一个 textarea 和提交按钮。 -- 结果区:自然语言回答和结构化输出。 -- 引用区:source、chunk_id、score、content。 -- 工具区:tool_name、success、arguments、result、error。 -- 审计入口:当前对话生成日志后展示详情链接。 - -POST 成功后仍渲染同一页面,保留用户问题和 AgentResult。 - -## 6. 文件上传页 - -路径:`/documents/upload/` - -页面元素: - -- 场景选择下拉框。 -- 文件选择控件。 -- 支持类型提示。 -- 上传按钮。 -- 错误或成功提示。 - -表单接受 `.txt`、`.md`、`.pdf`、`.docx`。PDF 仅要求纯文本抽取,DOCX 仅要求段落和普通文本抽取。 - -## 7. 文件列表页 - -路径:`/documents/` - -展示字段: - -- 原始文件名。 -- 所属场景。 -- 文件类型。 -- 文件大小。 -- 入库状态。 -- 上传时间。 -- 入库按钮。 -- 失败原因。 - -状态为 `indexed` 时可以显示“重新入库”,重新入库需要覆盖或清理该文档旧 chunk。 - -## 8. 审计日志列表页 - -路径:`/audit/` - -展示字段: - -- 日志 ID。 -- 场景名称。 -- 用户输入摘要。 -- 状态。 -- 模型名称。 -- 执行耗时。 -- 创建时间。 -- 详情入口。 - -默认按 `created_at desc` 排序。 - -## 9. 审计日志详情页 - -路径:`/audit//` - -展示内容: - -- 场景信息。 -- 用户输入。 -- 最终回答。 -- 结构化输出 JSON。 -- RAG 引用列表。 -- 工具调用列表。 -- 模型名称和耗时。 -- 错误信息。 - -JSON 内容可以先用 `
` 展示,优先保证可读。
-
-## 10. Django Admin 页面
-
-Admin 注册:
-
-- `UploadedDocument`
-- `AgentAuditLog`
-- `DemoBusinessRecord`
-
-V1 不要求在 Admin 中编辑 YAML 场景,场景仍以配置文件为准。
-
-## 11. 页面跳转关系
-
-```text
-首页
-  |-- 进入对话页
-  |-- 文件列表页
-  |-- 审计日志列表页
-
-文件列表页
-  |-- 文件上传页
-  |-- 触发入库后回到文件列表页
-
-对话页
-  |-- 提交后留在当前对话页
-  |-- 查看当前审计详情
-
-审计列表页
-  |-- 审计详情页
-```
-
-## 12. 页面异常状态
-
-| 页面 | 异常 | 展示方式 |
-|---|---|---|
-| 首页 | 场景配置为空 | 空状态和配置目录说明 |
-| 对话页 | 场景不存在 | 明确提示并提供返回首页 |
-| 对话页 | Agent 执行失败 | 展示错误、保留输入、写入失败审计 |
-| 上传页 | 文件类型错误 | 表单错误 |
-| 文件列表 | 入库失败 | 状态为 failed 并显示原因 |
-| 审计详情 | 日志不存在 | 404 或友好错误页 |
-
-## 13. V1 页面验收标准
-
-- 主要页面可通过浏览器访问。
-- 页面之间跳转路径完整。
-- POST 表单使用 CSRF 保护。
-- 所有用户可见错误都有中文提示。
-- Agent 对话结果可以同时看到回答、引用、工具和审计入口。
-- 页面不依赖 React/Vue。
diff --git a/docs/设计文档/5.部署设计.md b/docs/设计文档/5.部署设计.md
deleted file mode 100644
index 06327f0..0000000
--- a/docs/设计文档/5.部署设计.md
+++ /dev/null
@@ -1,111 +0,0 @@
-# V1 部署设计文档
-
-## 1. 部署设计目标
-
-V1 部署目标是降低复试现场环境风险。系统应支持本地 Python 方式启动,也支持 Docker Compose 一键启动。默认不依赖外部数据库、Redis 或任务队列。
-
-## 2. 本地运行方式
-
-建议命令:
-
-```bash
-python -m venv .venv
-.venv\Scripts\activate
-pip install -r requirements.txt
-python manage.py migrate
-python manage.py runserver
-```
-
-本地运行使用 SQLite、`data/uploads` 和 `data/chroma`。
-
-当前本地方式会在启动时自动读取根目录 `.env`,因此 `runserver`、`pytest` 和日常脚本可以共享同一套配置。
-
-## 3. Docker 运行方式
-
-建议命令:
-
-```bash
-docker compose up --build
-```
-
-V1 Docker Compose 只需要一个 Django Web 服务。Chroma 使用本地持久化目录,不额外启动独立服务。
-
-## 4. 环境变量设计
-
-| 变量 | 默认值 | 说明 |
-|---|---|---|
-| `DJANGO_SECRET_KEY` | `dev-secret-key` | 开发密钥 |
-| `DJANGO_DEBUG` | `true` | 是否开启调试 |
-| `DJANGO_ALLOWED_HOSTS` | `*` | 允许主机 |
-| `LLM_API_KEY` | 空 | 大模型 API Key |
-| `LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI 兼容接口地址,可接入 OpenAI、硅基流动等兼容服务 |
-| `LLM_MODEL` | `gpt-4.1-mini` | 默认模型 |
-| `EMBEDDING_API_KEY` | 空 | Embedding API Key;为空时可复用 `LLM_API_KEY` |
-| `EMBEDDING_BASE_URL` | 空 | Embedding OpenAI 兼容接口地址;为空时可复用 `LLM_BASE_URL` |
-| `EMBEDDING_MODEL` | `text-embedding-3-small` | 默认 Embedding 模型 |
-| `SCENARIO_CONFIG_DIR` | `configs` | 场景配置目录 |
-| `UPLOAD_ROOT` | `data/uploads` | 上传目录 |
-| `CHROMA_PATH` | `data/chroma` | 向量库目录 |
-
-`.env.example` 应提供这些变量的样例,不写真实密钥。
-
-当前实现说明:
-
-- 本地 Python 方式启动时,会先加载根目录 `.env`,再读取进程环境中的覆盖值。
-- Docker Compose 方式可通过 `env_file` 向容器注入环境变量;当前仓库默认读取 `.env`。
-- 因此本地运行和容器运行可以默认共用一份 `.env`,但演示前仍应确认密钥和模型参数是否正确。
-
-## 5. 目录挂载设计
-
-Docker 需要持久化以下目录:
-
-```text
-./data/db.sqlite3
-./data/uploads
-./data/chroma
-./configs
-```
-
-`configs` 挂载后可以在不重建镜像的情况下修改场景配置。
-
-## 6. SQLite 数据持久化
-
-SQLite 文件放在 `data/db.sqlite3`。Docker 中应将 `data/` 作为 volume 挂载,避免容器重建后数据丢失。
-
-## 7. Chroma 数据持久化
-
-Chroma 数据放在 `data/chroma`。RAG 入库后,重启容器不应丢失向量数据。
-
-## 8. 上传文件持久化
-
-上传文件放在 `data/uploads//`。数据库只保存相对路径或 Django FileField 路径。
-
-## 9. 启动命令设计
-
-Docker 容器启动时建议执行:
-
-```bash
-python manage.py migrate
-python manage.py runserver 0.0.0.0:8000
-```
-
-V1 可以先用开发服务器满足演示。后续正式部署可切换到 Gunicorn。
-
-## 10. 常见部署问题
-
-| 问题 | 处理 |
-|---|---|
-| 端口 8000 被占用 | 修改 compose 端口映射 |
-| API Key 缺失 | 页面提示 LLM 或 Embedding 配置缺失 |
-| Chroma 目录无权限 | 检查 `data/chroma` 挂载权限 |
-| 上传目录不存在 | settings 或启动脚本创建目录 |
-| 场景配置读取失败 | 检查 `configs/*.yaml` 格式 |
-| Docker 构建慢 | 提前构建镜像或使用本地 Python 方式演示 |
-
-## 11. 后续部署扩展
-
-- 使用 Gunicorn + WhiteNoise。
-- 增加 PostgreSQL 服务。
-- 增加 Redis 和 Celery 做异步入库。
-- 增加 Nginx 反向代理。
-- 增加健康检查接口。
diff --git a/docs/设计文档/模块设计/1.配置模块详细设计.md b/docs/设计文档/模块设计/1.配置模块详细设计.md
deleted file mode 100644
index 2be15f2..0000000
--- a/docs/设计文档/模块设计/1.配置模块详细设计.md
+++ /dev/null
@@ -1,112 +0,0 @@
-# 配置模块详细设计
-
-## 1. 模块目标
-
-Config 模块负责 Django 项目的启动配置和总装配。它不承载业务逻辑,只为其他模块提供稳定运行环境。
-
-目标:
-
-- 项目本地和 Docker 均可启动。
-- 环境变量可覆盖关键配置。
-- App、模板、静态资源、上传文件和数据库路径统一配置。
-- URL 总入口清晰,模块路由各自维护。
-
-## 2. 职责边界
-
-负责:
-
-- `settings.py`、`urls.py`、`wsgi.py`、`asgi.py`。
-- 环境变量读取和默认值。
-- SQLite、静态文件、媒体文件、Chroma、场景配置目录。
-- Django Admin 和模块 URL 装配。
-
-不负责:
-
-- 不读取场景 YAML 业务内容。
-- 不调用 Agent Core。
-- 不处理上传文件文本抽取。
-- 不写审计日志。
-
-## 3. 配置项设计
-
-| 配置 | Django setting | 默认值 |
-|---|---|---|
-| `DJANGO_SECRET_KEY` | `SECRET_KEY` | `dev-secret-key` |
-| `DJANGO_DEBUG` | `DEBUG` | `true` |
-| `DJANGO_ALLOWED_HOSTS` | `ALLOWED_HOSTS` | `["*"]` |
-| `UPLOAD_ROOT` | `MEDIA_ROOT` | `BASE_DIR / "data" / "uploads"` |
-| `SCENARIO_CONFIG_DIR` | `SCENARIO_CONFIG_DIR` | `BASE_DIR / "configs"` |
-| `CHROMA_PATH` | `CHROMA_PATH` | `BASE_DIR / "data" / "chroma"` |
-| `LLM_API_KEY` | `LLM_API_KEY` | 空 |
-| `LLM_BASE_URL` | `LLM_BASE_URL` | `https://api.openai.com/v1` |
-| `LLM_MODEL` | `LLM_MODEL` | `gpt-4.1-mini` |
-| `EMBEDDING_API_KEY` | `EMBEDDING_API_KEY` | 空,默认可复用 `LLM_API_KEY` |
-| `EMBEDDING_BASE_URL` | `EMBEDDING_BASE_URL` | 空,默认可复用 `LLM_BASE_URL` |
-| `EMBEDDING_MODEL` | `EMBEDDING_MODEL` | `text-embedding-3-small` |
-
-## 4. 目录路径设计
-
-启动前或初始化时应确保:
-
-```text
-data/
-  uploads/
-  chroma/
-configs/
-static/
-templates/
-```
-
-V1 可以在 `settings.py` 中定义路径,在 management command 或启动脚本中创建目录。生产代码不应在每次请求中反复创建目录。
-
-## 5. URL 总路由设计
-
-`config.urls`:
-
-```python
-urlpatterns = [
-    path("admin/", admin.site.urls),
-    path("", include("apps.scenarios.urls")),
-    path("chat/", include("apps.chat.urls")),
-    path("documents/", include("apps.documents.urls")),
-    path("audit/", include("apps.audit.urls")),
-]
-```
-
-开发模式下追加 `static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)`,用于访问上传文件。
-
-## 6. 静态资源与上传文件设计
-
-- `STATIC_URL = "static/"`
-- `STATICFILES_DIRS = [BASE_DIR / "static"]`
-- `MEDIA_URL = "media/"`
-- `MEDIA_ROOT = UPLOAD_ROOT`
-
-上传文件路径由 Documents 模块按场景组织,Config 只提供根目录。
-
-## 7. 环境变量读取设计
-
-V1 可使用标准库 `os.environ.get()`,不强制引入复杂配置库。
-
-布尔值规则:
-
-```text
-"1", "true", "yes", "on" -> True
-其他 -> False
-```
-
-`DJANGO_ALLOWED_HOSTS` 使用逗号分隔,空值时默认 `["*"]`。
-
-当前实现约束:
-
-- 本地直接运行 Django 命令时,会先尝试解析根目录 `.env` 文件,再读取进程环境中的覆盖值。
-- Docker Compose 方式可以通过 `env_file` 传入同一批变量;当前仓库默认读取 `.env`。
-- `.env.example` 只保留占位符示例,不保存真实 API Key。
-
-## 8. 验收标准
-
-- `python manage.py check` 通过。
-- `python manage.py migrate` 可执行。
-- `/`、`/admin/` 路由可访问。
-- `MEDIA_ROOT`、`CHROMA_PATH`、`SCENARIO_CONFIG_DIR` 在 settings 中可被其他模块引用。
-- LLM 与 Embedding 配置只从 settings 或环境变量读取,不散落在业务代码中。
diff --git a/docs/设计文档/模块设计/2.场景模块详细设计.md b/docs/设计文档/模块设计/2.场景模块详细设计.md
deleted file mode 100644
index a8c8bee..0000000
--- a/docs/设计文档/模块设计/2.场景模块详细设计.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# 场景模块详细设计
-
-## 1. 模块目标
-
-Scenarios 模块是业务 Agent 的入口,负责读取和展示场景配置,并向 Chat、Documents、Agent Core 提供场景上下文。
-
-## 2. 职责边界
-
-负责:
-
-- 从 `configs/*.yaml` 读取场景。
-- 校验场景必填字段。
-- 展示场景列表和场景摘要。
-- 提供 `list_scenarios()`、`get_scenario()` 等服务。
-
-不负责:
-
-- 不执行 Agent。
-- 不做 RAG 检索。
-- 不调用工具和大模型。
-- 不保存审计日志。
-
-## 3. 场景配置结构
-
-必填结构:
-
-```yaml
-id: knowledge_qa
-name: 知识库问答助手
-description: 用于 SOP、制度和内部知识库问答
-applicable_questions:
-  - SOP 问答
-  - 制度问答
-
-agent:
-  role: 知识库问答专家
-  goal: 基于知识库回答用户问题
-  system_prompt: ""
-  instructions:
-    - 回答必须基于检索内容
-
-rag:
-  enabled: true
-  collection: knowledge_qa
-  top_k: 5
-
-tools:
-  - generate_action_items
-
-output:
-  type: general_answer
-
-audit:
-  enabled: true
-```
-
-`agent.system_prompt` 为可选字段。配置了非空值时,Agent Core 优先使用该字段作为系统提示词;为空或缺失时,由 `role`、`goal` 和 `instructions` 组合生成系统提示词。
-
-`applicable_questions` 作为页面展示字段,若缺失可显示为空列表。
-
-## 4. 场景加载流程
-
-1. 读取 `settings.SCENARIO_CONFIG_DIR`。
-2. 遍历 `.yaml` 和 `.yml` 文件。
-3. 使用 YAML parser 转为 dict。
-4. 调用 `validate_scenario()`。
-5. 转换为 `ScenarioConfig` dataclass 或普通 dict。
-6. 按文件名或配置顺序返回。
-
-为了便于复试修改,V1 不需要强缓存;若加缓存,应提供清理方式或在 DEBUG 下禁用缓存。
-
-## 5. 场景校验规则
-
-必填字段:
-
-- `id`
-- `name`
-- `description`
-- `agent.role`
-- `agent.goal`
-- `agent.instructions`
-- `rag.enabled`
-- `tools`
-- `output.type`
-- `audit.enabled`
-
-校验失败时返回包含文件名、字段路径、错误原因的结果。列表页可以跳过非法场景并展示错误摘要。
-
-## 6. 页面设计
-
-首页路径:`/`
-
-展示:
-
-- 场景名称。
-- 场景描述。
-- 适用题型。
-- RAG 是否启用。
-- 工具数量。
-- 进入对话按钮。
-
-可选详情页:`/scenarios//`。V1 可以把详情合并到 Chat 页面。
-
-## 7. 服务函数设计
-
-```python
-def list_scenarios() -> list[ScenarioConfig]:
-    """读取配置目录中的合法场景,非法场景以错误摘要返回给页面。"""
-
-def get_scenario(scenario_id: str) -> ScenarioConfig:
-    """按场景 ID 返回完整配置,找不到时抛出 ScenarioNotFound。"""
-
-def validate_scenario(config: dict) -> ValidationResult:
-    """校验必填字段、字段类型、工具名称和输出类型。"""
-```
-
-`get_scenario()` 找不到时抛出业务异常,例如 `ScenarioNotFound`,由 View 转成中文错误提示。
-
-## 8. 异常处理
-
-| 异常 | 处理 |
-|---|---|
-| 配置目录不存在 | 返回空列表和错误提示 |
-| YAML 语法错误 | 标记该文件无效 |
-| ID 重复 | 保留第一个,报告重复错误 |
-| 必填字段缺失 | 标记该场景无效 |
-| 工具不存在 | 场景仍可展示,但 Chat 执行时记录工具错误 |
-
-## 9. 验收标准
-
-- 首页至少展示 5 个场景。
-- 场景配置来自 `configs/` 文件。
-- 非法配置有明确错误,不导致首页 500。
-- Chat 可通过 `scenario_id` 获取完整配置。
diff --git a/docs/设计文档/模块设计/3.文档模块详细设计.md b/docs/设计文档/模块设计/3.文档模块详细设计.md
deleted file mode 100644
index 4f34e05..0000000
--- a/docs/设计文档/模块设计/3.文档模块详细设计.md
+++ /dev/null
@@ -1,127 +0,0 @@
-# 文档模块详细设计
-
-## 1. 模块目标
-
-Documents 模块让用户把复试题材料快速变成 Agent 可检索的知识库。V1 必须支持 `.txt`、`.md`、`.pdf` 和 `.docx`,保证常见复试材料可以进入 RAG。
-
-## 2. 职责边界
-
-负责:
-
-- 文件上传表单和页面。
-- 文件保存与元数据记录。
-- 读取文本内容。
-- 调用 Agent Core RAG 入库。
-- 更新入库状态。
-
-不负责:
-
-- 不实现向量检索算法。
-- 不生成模型回答。
-- 不直接写审计日志。
-
-## 3. 数据模型设计
-
-模型:`UploadedDocument`
-
-字段见 `docs/设计文档/3.数据库设计.md`。
-
-常量:
-
-```python
-STATUS_UPLOADED = "uploaded"
-STATUS_INDEXED = "indexed"
-STATUS_FAILED = "failed"
-SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"}
-```
-
-文件保存路径建议:
-
-```text
-uploads///_
-```
-
-## 4. 文件上传流程
-
-1. GET `/documents/upload/` 渲染上传表单。
-2. POST 校验 `scenario_id` 和文件。
-3. 调用 Scenarios 服务确认场景存在。
-4. 校验扩展名和文件大小。
-5. 保存文件。
-6. 创建 `UploadedDocument(status="uploaded")`。
-7. 跳转文件列表页并展示成功提示。
-
-## 5. 文本抽取流程
-
-抽取函数:
-
-```python
-def extract_text(document: UploadedDocument) -> str:
-    """按文件类型抽取可入库纯文本,失败时抛出可展示的业务异常。"""
-```
-
-规则:
-
-- `.txt`:优先 UTF-8,失败时尝试系统默认编码。
-- `.md`:UTF-8 读取,保留标题、列表和正文。
-- `.pdf`:抽取纯文本,不要求 OCR、表格还原和复杂版式理解。
-- `.docx`:抽取段落、标题和普通表格文本,不要求完整保留 Word 样式。
-- 空文本视为失败。
-- 文件不存在视为失败。
-
-XLSX 暂不作为 V1 必须项,可作为后续结构化业务数据导入能力。
-
-## 6. RAG 入库触发流程
-
-POST `/documents//index/`
-
-1. 获取 `UploadedDocument`。
-2. 调用 `extract_text()`。
-3. 调用 `agent_core.rag.ingest.ingest_document()`,传入 `document_id`、`scenario_id`、文件名和抽取文本。
-4. 成功后更新 `status="indexed"`,清空 `error_message`。
-5. 失败后更新 `status="failed"`,写入 `error_message`。
-6. 重定向回文件列表页。
-
-入库动作必须使用 POST,避免 GET 触发写操作。
-
-已入库或失败文档允许重新入库。重新入库前需要按 `document_id` 清理或覆盖旧 chunk,避免重复检索。
-
-## 7. 页面设计
-
-文件列表页展示:
-
-- 文件名。
-- 场景 ID。
-- 文件类型。
-- 文件大小。
-- 状态。
-- 上传时间。
-- 入库按钮。
-- 错误信息。
-
-上传页展示:
-
-- 场景下拉框。
-- 文件控件。
-- 支持类型提示。
-- 表单错误。
-
-## 8. 异常处理
-
-| 异常 | 处理 |
-|---|---|
-| 场景不存在 | 表单错误 |
-| 文件为空 | 表单错误 |
-| 扩展名不支持 | 表单错误 |
-| 文件保存失败 | 页面提示失败 |
-| 文本为空 | 状态 failed |
-| RAG 入库失败 | 状态 failed 并保存原因 |
-
-## 9. 验收标准
-
-- 可以上传 `.txt`、`.md`、`.pdf` 和 `.docx`。
-- 文件列表可看到记录。
-- 文件可按场景关联。
-- 入库成功状态变为 `indexed`。
-- 入库失败状态变为 `failed` 且可查看原因。
-- 入库失败或已入库文档可重新入库。
diff --git a/docs/设计文档/模块设计/4.对话模块详细设计.md b/docs/设计文档/模块设计/4.对话模块详细设计.md
deleted file mode 100644
index a92680b..0000000
--- a/docs/设计文档/模块设计/4.对话模块详细设计.md
+++ /dev/null
@@ -1,118 +0,0 @@
-# 对话模块详细设计
-
-## 1. 模块目标
-
-Chat 模块负责复试演示中的主交互:用户选择场景后提交问题,系统展示 Agent 输出、引用、工具调用和审计入口。
-
-## 2. 职责边界
-
-负责:
-
-- 对话页 GET/POST。
-- 用户输入表单校验。
-- 获取场景配置。
-- 调用 Agent Core。
-- 调用 Audit 服务写日志。
-- 渲染 AgentResult。
-
-不负责:
-
-- 不直接读取 YAML。
-- 不直接调用 LLM。
-- 不直接执行 RAG 和工具。
-- 不实现复杂多轮会话状态。
-
-## 3. 页面设计
-
-路径:`/chat//`
-
-GET:
-
-- 加载场景配置。
-- 展示场景摘要。
-- 加载当前场景下状态为 `indexed` 的文档列表。
-- 展示空表单。
-
-POST:
-
-- 校验输入。
-- 执行 Agent。
-- 写审计。
-- 展示结果和审计链接。
-
-## 4. 表单设计
-
-字段:
-
-| 字段 | 类型 | 规则 |
-|---|---|---|
-| `message` | textarea | 必填,最大 4000 字 |
-| `document_ids` | 多选 | 可选,只能选择当前场景下已入库文档 |
-
-错误提示:
-
-- 空输入:`请输入要咨询的问题。`
-- 超长输入:`问题过长,请控制在 4000 字以内。`
-- 文档不属于当前场景或未入库:`请选择当前场景下已入库的文档。`
-
-## 5. Agent Core 调用流程
-
-```python
-scenario = get_scenario(scenario_id)
-result = run_agent(
-    scenario_config=scenario,
-    user_input=form.cleaned_data["message"],
-    options={"document_ids": form.cleaned_data.get("document_ids", [])}
-)
-```
-
-Chat 只依赖 Agent Core 的统一返回对象,不关心内部是否使用 RAG、工具或真实模型。
-
-未选择文档时,`document_ids` 传空列表或不传,由 Agent Core 默认使用当前场景全部已入库文档。
-
-## 6. 结果展示设计
-
-优先级:
-
-1. 如果 `structured_output` 不为空,展示结构化 JSON 或字段化结果。
-2. 展示 `answer`。
-3. 展示 `references`。
-4. 展示 `tool_calls`。
-5. 展示 `latency_ms`、`model_name`、`status`。
-6. 如果有 `error`,展示中文错误提示。
-
-结构化解析失败时,页面仍展示 `raw_output` 或 `answer`。
-
-## 7. 审计日志写入流程
-
-Agent Core 返回后调用:
-
-```python
-audit_log = create_audit_log(
-    scenario_id=scenario.id,
-    scenario_name=scenario.name,
-    user_input=message,
-    agent_result=result,
-)
-```
-
-如果 Agent Core 抛异常,Chat 应构造失败结果并继续写失败审计。
-
-## 8. 异常处理
-
-| 异常 | 处理 |
-|---|---|
-| 场景不存在 | 显示错误并返回首页入口 |
-| 表单无效 | 留在页面并显示表单错误 |
-| Agent Core 抛异常 | 构造 failed AgentResult,写审计 |
-| 审计写入失败 | 页面提示审计失败,但展示 Agent 输出 |
-| LLM 配置缺失 | 展示模型配置缺失 |
-
-## 9. 验收标准
-
-- 从首页可进入对话页。
-- 可提交问题并渲染 AgentResult。
-- 可选择本次对话使用的文档范围;未选择时默认使用当前场景全部已入库文档。
-- 失败时有中文提示。
-- 成功和失败都尽量写入审计。
-- View 中没有 RAG、工具、LLM 的细节实现。
diff --git a/docs/设计文档/模块设计/5.审计模块详细设计.md b/docs/设计文档/模块设计/5.审计模块详细设计.md
deleted file mode 100644
index 886abea..0000000
--- a/docs/设计文档/模块设计/5.审计模块详细设计.md
+++ /dev/null
@@ -1,121 +0,0 @@
-# 审计模块详细设计
-
-## 1. 模块目标
-
-Audit 模块记录 Agent 执行过程,使演示者能够解释一次输出的来源、工具调用和模型结果。它是系统从“普通问答页面”变成“可追踪业务 Agent”的关键。
-
-## 2. 职责边界
-
-负责:
-
-- `AgentAuditLog` 模型。
-- 审计写入服务。
-- 审计列表页。
-- 审计详情页。
-- 敏感信息过滤。
-
-不负责:
-
-- 不执行 Agent。
-- 不执行 RAG。
-- 不执行工具。
-- 不调用模型。
-
-## 3. 数据模型设计
-
-模型:`AgentAuditLog`
-
-字段见 `docs/设计文档/3.数据库设计.md`。
-
-JSON 字段默认值必须使用函数,例如 `default=list`、`default=dict`,避免多实例共享同一对象。
-
-## 4. 日志写入流程
-
-服务函数:
-
-```python
-def create_audit_log(
-    scenario_id: str,
-    scenario_name: str,
-    user_input: str,
-    agent_result: AgentResult,
-) -> AgentAuditLog:
-    """将 AgentResult 映射为 AgentAuditLog,并在保存前做敏感信息脱敏。"""
-```
-
-写入映射:
-
-- `agent_result.references` -> `retrieved_chunks`
-- `agent_result.tool_calls` -> `tool_calls`
-- `agent_result.structured_output` -> `structured_output`
-- `agent_result.answer` -> `final_answer`
-- `agent_result.raw_output` -> `raw_output`
-- `agent_result.model_name` -> `model_name`
-- `agent_result.latency_ms` -> `latency_ms`
-- `agent_result.status` -> `status`
-- `agent_result.error` -> `error_message`
-
-## 5. 日志列表页设计
-
-路径:`/audit/`
-
-查询:
-
-- 默认按创建时间倒序。
-- V1 可不做分页,若日志较多再加 Django Paginator。
-
-展示:
-
-- ID。
-- 场景名称。
-- 用户输入前 80 字。
-- 状态。
-- 模型名。
-- 耗时。
-- 创建时间。
-- 详情链接。
-
-## 6. 日志详情页设计
-
-路径:`/audit//`
-
-展示:
-
-- 基础信息。
-- 用户输入。
-- 最终回答。
-- 结构化输出。
-- RAG 检索片段。
-- 工具调用。
-- 原始输出。
-- 错误信息。
-
-JSON 可用格式化后的 `
` 展示。
-
-## 7. 敏感信息处理
-
-不得保存:
-
-- `LLM_API_KEY`
-- 完整环境变量 dump
-- 用户机器上的敏感绝对路径
-- Docker secret 或 token
-
-如错误信息来自异常对象,应在保存前做简单脱敏,至少替换 API Key 值。
-
-## 8. 异常处理
-
-| 异常 | 处理 |
-|---|---|
-| AgentResult 字段缺失 | 使用默认空值 |
-| JSON 不可序列化 | 转为字符串或空对象 |
-| 日志不存在 | 返回 404 |
-| 写入失败 | 抛给 Chat,由 Chat 展示审计失败提示 |
-
-## 9. 验收标准
-
-- 每次对话成功后有审计日志。
-- Agent 失败也有失败日志。
-- 列表页可查看日志摘要。
-- 详情页可查看输入、输出、引用和工具调用。
-- 日志不包含 API Key。
diff --git a/docs/设计文档/模块设计/6.智能核心模块详细设计.md b/docs/设计文档/模块设计/6.智能核心模块详细设计.md
deleted file mode 100644
index 4854373..0000000
--- a/docs/设计文档/模块设计/6.智能核心模块详细设计.md
+++ /dev/null
@@ -1,259 +0,0 @@
-# 智能核心模块详细设计
-
-## 1. 模块目标
-
-Agent Core 提供独立于 Django View 的智能编排能力。它消费场景配置,执行 RAG、工具、模型调用和结构化解析,最终返回统一 AgentResult。
-
-## 2. 职责边界
-
-负责:
-
-- Agent 编排。
-- 场景配置对象消费。
-- RAG 入库和检索。
-- 工具注册与执行。
-- LLM Provider 与 Embedding Provider。
-- 结构化输出解析。
-- AgentResult 定义。
-
-不负责:
-
-- 不渲染页面。
-- 不处理 Django 表单。
-- 不保存 Django Model。
-- 不管理登录权限。
-
-## 3. 子模块划分
-
-```text
-agent_core/
-  orchestrator.py
-  scenario_loader.py
-  llm_provider.py
-  tool_registry.py
-  structured_output.py
-  rag/
-    ingest.py
-    retriever.py
-  tools/
-    builtin_tools.py
-  schemas/
-    outputs.py
-```
-
-`scenario_loader.py` 可作为非 Django 环境下加载配置的工具;Django 场景展示仍由 `apps.scenarios` 负责。
-
-## 4. Orchestrator 设计
-
-入口:
-
-```python
-def run_agent(scenario_config, user_input: str, options: dict | None = None) -> AgentResult:
-    """执行一次 Agent 编排,options 可包含 document_ids 等运行期约束。"""
-```
-
-流程:
-
-1. 记录开始时间。
-2. 根据 `rag.enabled`、`scenario_id` 和可选 `document_ids` 检索引用。
-3. 根据 `tools` 执行或准备工具结果。
-4. 构造 messages。
-5. 调用 LLM Provider。
-6. 解析结构化输出。
-7. 计算耗时。
-8. 返回 `AgentResult(status="success")`。
-9. 捕获可恢复异常并返回 `status="failed"`。
-
-V1 在缺少 LLM 或 Embedding 配置时必须返回清晰失败结果。测试代码可以使用 mock provider,但 V1 验收链路必须通过真实 OpenAI 兼容 LLM、Embedding 和 Chroma。
-
-## 5. Scenario Loader 设计
-
-Agent Core 的 Scenario Loader 用于脚本、测试或后续独立服务场景。它不依赖 Django View,可以复用 Scenarios 模块的字段规范。
-
-接口:
-
-```python
-load_scenario(path: str) -> dict
-load_scenarios(directory: str) -> list[dict]
-```
-
-## 6. RAG 设计
-
-入库接口:
-
-```python
-def ingest_document(
-    document_id: int,
-    scenario_id: str,
-    source_file: str,
-    text: str,
-    collection: str,
-) -> IngestResult:
-    """切分文档、生成 embedding,并写入 Chroma。重新入库时覆盖同一 document_id 的旧 chunk。"""
-```
-
-检索接口:
-
-```python
-def retrieve(
-    scenario_id: str,
-    query: str,
-    collection: str,
-    top_k: int = 5,
-    document_ids: list[int] | None = None,
-) -> list[ReferenceChunk]:
-    """按场景和可选文档范围执行向量检索,返回可审计引用片段。"""
-```
-
-切分策略:
-
-- 默认 chunk size 800 到 1000 字。
-- overlap 100 到 150 字。
-- metadata 包含 `scenario_id`、`document_id`、`source_file`、`chunk_id`。
-
-RAG 入库和检索必须使用 Embedding Provider 与 Chroma。单元测试桩或开发阶段临时验证方案不属于 V1 验收设计。
-
-## 7. Tool Registry 设计
-
-工具注册:
-
-```python
-registry.register("calculate_rate", calculate_rate)
-registry.get("calculate_rate")
-registry.run("calculate_rate", **kwargs)
-```
-
-工具结果统一:
-
-```json
-{
-  "tool_name": "calculate_rate",
-  "success": true,
-  "arguments": {},
-  "result": {},
-  "error": ""
-}
-```
-
-内置工具:
-
-- `calculate_rate`
-- `query_demo_records`
-- `check_required_fields`
-- `generate_action_items`
-
-工具函数不得直接读取 API Key 或执行无审计的外部副作用。
-
-## 8. LLM Provider 设计
-
-接口:
-
-```python
-class LLMProvider:
-    def generate(self, messages: list[dict], response_format: dict | None = None) -> LLMResponse:
-        """调用 OpenAI 兼容 Chat Completions 接口并返回统一响应对象。"""
-```
-
-配置来源:
-
-- `LLM_API_KEY`
-- `LLM_BASE_URL`
-- `LLM_MODEL`
-
-Provider 对外隐藏供应商差异,Orchestrator 只处理 `LLMResponse.content`、`LLMResponse.model_name` 和错误信息。供应商可自主选择 OpenAI、硅基流动等 OpenAI 兼容服务。
-
-Embedding Provider 接口:
-
-```python
-class EmbeddingProvider:
-    def embed_texts(self, texts: list[str]) -> list[list[float]]:
-        """调用 OpenAI 兼容 Embeddings 接口,返回与输入文本一一对应的向量。"""
-```
-
-配置来源:
-
-- `EMBEDDING_API_KEY`
-- `EMBEDDING_BASE_URL`
-- `EMBEDDING_MODEL`
-
-当 `EMBEDDING_API_KEY` 或 `EMBEDDING_BASE_URL` 为空时,可以复用 `LLM_API_KEY` 和 `LLM_BASE_URL`。
-
-## 9. Structured Output 设计
-
-接口:
-
-```python
-def parse_structured_output(raw_output: str, output_type: str) -> ParseResult:
-    """优先解析 JSON,并根据输出类型返回结构化结果或解析错误。"""
-```
-
-策略:
-
-- 优先解析 JSON。
-- 根据 `output_type` 做字段补齐或轻校验。
-- 失败时返回 `success=False`,保留 `raw_output`。
-- 不因结构化解析失败导致整个 Agent 流程崩溃。
-
-## 10. AgentResult 设计
-
-建议 dataclass:
-
-```python
-@dataclass
-class AgentResult:
-    answer: str
-    structured_output: dict
-    references: list
-    tool_calls: list
-    raw_output: str
-    model_name: str
-    latency_ms: int
-    status: str
-    error: str = ""
-```
-
-所有字段必须有默认值或构造时明确传入,保证 Audit 模块写入稳定。
-
-## 11. Adapter 扩展设计
-
-统一接口:
-
-```python
-class AgentEngine:
-    def run_agent(self, scenario_config, user_input: str, options: dict | None = None) -> AgentResult:
-        """保持与顶层 run_agent 函数一致的输入输出合约。"""
-```
-
-V1 实现:
-
-- `LightweightOrchestrator`
-
-后续扩展:
-
-- `DifyAdapter`
-- `OpenAIAgentsAdapter`
-- `LangGraphAdapter`
-
-Adapter 只能替换编排实现,不能改变 Django 层依赖的 AgentResult 合约。
-
-## 12. 异常处理
-
-| 异常 | 处理 |
-|---|---|
-| RAG 检索失败 | 记录错误,允许继续或返回 failed |
-| 工具不存在 | 记录失败工具调用 |
-| 工具执行异常 | 捕获并返回失败工具结果 |
-| LLM 配置缺失 | 返回 failed AgentResult |
-| LLM 调用失败 | 返回 failed AgentResult |
-| JSON 解析失败 | 返回 success 但带解析错误,展示 raw output |
-
-## 13. 验收标准
-
-- Chat 可以调用 `run_agent()`。
-- 返回对象字段稳定完整。
-- RAG 按 `scenario_id` 隔离。
-- RAG 支持按 `document_ids` 限定本次对话的文档范围。
-- 工具调用结果格式统一。
-- LLM 与 Embedding 配置从环境变量读取。
-- 结构化解析失败不导致页面崩溃。
-- Agent Core 不依赖 Django View。
diff --git a/docs/需求分析/1.V1总需求文档.md b/docs/需求分析/1.V1总需求文档.md
deleted file mode 100644
index 39c77d3..0000000
--- a/docs/需求分析/1.V1总需求文档.md
+++ /dev/null
@@ -1,583 +0,0 @@
-# Universal Agent Demo Framework V1 需求文档
-
-## 1. 项目背景
-
-本项目用于复试展示。复试题目暂时未知,但大概率围绕企业生产、质量、客服、财务、SOP、文档审核、工单处理等场景。
-
-项目目标不是提前猜中某一个具体业务题,而是先搭建一个通用 AI Agent Demo 底座。拿到复试题目后,可以通过修改场景配置、上传知识库、补充少量业务工具,快速生成一个可演示的业务 Agent 系统。
-
-核心理念:
-
-```text
-业务 Agent = 场景配置 + 知识库 + 工具集 + 输出模板 + 审计日志 + 模型适配器
-```
-
-## 2. 项目目标
-
-V1 版本目标是实现一个可运行、可演示、可快速改题的基础平台。
-
-系统需要支持:
-
-- 通过配置快速创建不同业务 Agent。
-- 支持上传文档并构建 RAG 知识库。
-- 支持根据场景调用内置业务工具。
-- 支持结构化输出,方便展示报告、风险点、建议动作等结果。
-- 支持审计日志,记录用户输入、检索内容、工具调用和模型输出。
-- 支持 Docker 一键启动,降低复试现场环境风险。
-- 支持快速替换大模型 API。
-
-## 3. 非目标
-
-V1 不追求完整企业级平台能力,以下内容暂不作为第一版重点:
-
-- 复杂权限系统。
-- 多租户管理。
-- 完整工作流引擎。
-- 复杂多 Agent 协作。
-- 前后端分离架构。
-- 深度集成 Dify。
-- 生产级高并发优化。
-- 完整在线文档协同编辑。
-
-## 4. 技术方案
-
-### 4.1 总体架构
-
-V1 使用 Django 单体应用承载企业系统外壳,Agent Core 作为独立 Python 模块承载智能编排能力。
-
-```text
-Django Monolith
-  |
-  |-- Web UI
-  |   |-- 场景选择
-  |   |-- Agent 对话
-  |   |-- 文件上传
-  |   |-- 结构化结果展示
-  |   |-- 审计日志查看
-  |
-  |-- Django Admin
-  |   |-- 上传文件管理
-  |   |-- 审计日志管理
-  |   |-- 示例业务数据管理
-  |
-  |-- Agent Core
-  |   |-- 场景配置加载
-  |   |-- RAG 检索
-  |   |-- 工具注册与调用
-  |   |-- 大模型适配
-  |   |-- 结构化输出解析
-  |
-  |-- Storage
-      |-- SQLite
-      |-- Chroma
-      |-- Uploaded Files
-```
-
-### 4.2 技术栈
-
-| 模块 | 技术 | 说明 |
-|---|---|---|
-| Web 框架 | Django | 负责页面、模型、后台、文件上传和业务管理 |
-| 页面渲染 | Django Templates + Bootstrap | 降低前端复杂度,快速完成 Demo |
-| 数据库 | SQLite | V1 默认数据库,适合本地演示 |
-| 向量库 | Chroma | 本地 RAG 知识库 |
-| Agent Core | 自研轻量 Orchestrator | 保证可控、易讲解、易改题 |
-| LLM 接入 | OpenAI API 兼容接口 | 方便切换 OpenAI、硅基流动等兼容服务、国产模型或本地代理 |
-| Embedding 接入 | OpenAI API 兼容接口 | 用于文档向量化,供应商可自主选择 |
-| 部署 | Docker + Docker Compose | 支持一键启动 |
-
-## 5. 用户角色
-
-V1 只设计一个主要用户角色:
-
-### Demo 操作者
-
-通常是复试时的展示者,负责选择场景、上传材料、输入问题、查看 Agent 输出和审计记录。
-
-暂不区分管理员、业务人员、审核人员等复杂角色。
-
-## 6. 核心使用流程
-
-### 6.1 复试前准备流程
-
-1. 启动系统。
-2. 选择或复制一个已有场景模板。
-3. 根据题目修改场景配置。
-4. 上传题目相关文档。
-5. 如有必要,补充一个业务工具函数。
-6. 运行一次测试对话。
-7. 使用审计日志确认 RAG、工具调用和输出链路正常。
-
-### 6.2 复试演示流程
-
-1. 打开系统首页。
-2. 展示系统支持多个业务场景。
-3. 选择当前题目对应的 Agent。
-4. 上传或选择知识库文档。
-5. 输入业务问题。
-6. 展示 Agent 的结构化输出。
-7. 展示引用来源、工具调用和审计日志。
-8. 说明同一平台可通过配置切换到其他业务场景。
-
-## 7. 场景模板
-
-V1 预置 5 类通用场景模板,用于覆盖大多数复试题型。
-
-| 模板 ID | 模板名称 | 适用题型 |
-|---|---|---|
-| knowledge_qa | 知识库问答助手 | SOP、制度、客服知识库、内部文档问答 |
-| document_review | 文档审核助手 | 合同审核、制度审核、SOP 审核、材料合规检查 |
-| ticket_assistant | 工单处理助手 | 客服工单、售后工单、运维工单 |
-| quality_analysis | 质量异常分析助手 | 生产质量、缺陷分析、原因定位 |
-| risk_audit | 风险审核助手 | 财务审核、采购审核、报销审核、合同风险 |
-
-## 8. 场景配置需求
-
-场景应通过 YAML 或 JSON 文件定义,避免把业务逻辑写死在代码中。
-
-配置内容包括:
-
-- 场景 ID。
-- 场景名称。
-- 场景描述。
-- Agent 角色。
-- Agent 任务目标。
-- 系统提示词 可选。
-- 是否启用 RAG。
-- RAG 检索参数。
-- 可用工具列表。
-- 输出模板类型。
-- 审计策略。
-
-示例:
-
-```yaml
-id: quality_analysis
-name: 质量异常分析助手
-description: 用于分析生产质量异常、检索 SOP、生成处理建议
-
-agent:
-  role: 质量管理专家
-  goal: 根据用户问题、知识库和工具结果,输出可执行的质量分析报告
-  system_prompt: 你是质量管理专家,需要基于知识库和工具结果输出结构化质量分析报告
-  instructions:
-    - 回答必须基于知识库或工具结果
-    - 不确定时必须说明缺失信息
-    - 涉及质量风险时给出风险等级
-
-rag:
-  enabled: true
-  collection: quality_docs
-  top_k: 5
-
-tools:
-  - query_demo_records
-  - calculate_rate
-
-output:
-  type: quality_report
-
-audit:
-  enabled: true
-  log_retrieval: true
-  log_tool_calls: true
-```
-
-## 9. 功能需求
-
-### 9.1 首页
-
-首页需要展示系统定位和可用场景列表。
-
-页面能力:
-
-- 查看所有 Agent 场景。
-- 进入某个场景的对话页。
-- 查看最近审计日志入口。
-- 查看文件上传入口。
-
-### 9.2 场景选择
-
-系统需要支持从预置模板中选择业务场景。
-
-V1 从 YAML 配置文件读取场景。后台管理只负责上传文件、审计日志和示例业务数据管理,不作为场景配置入口。
-
-最低要求:
-
-- 展示场景名称。
-- 展示场景描述。
-- 展示场景适用题型。
-- 点击后进入对应 Agent 对话页。
-
-### 9.3 Agent 对话
-
-Agent 对话页是核心演示页面。
-
-页面需要包含:
-
-- 当前场景名称。
-- 用户输入框。
-- 文件上下文选择,可多选当前场景已入库文档;不选时默认使用当前场景全部已入库文档。
-- Agent 输出区域。
-- 结构化结果展示区域。
-- 引用片段展示区域。
-- 工具调用展示区域。
-
-Agent 执行流程:
-
-1. 接收用户问题。
-2. 加载当前场景配置。
-3. 如果启用 RAG,则检索相关知识片段。
-4. 根据场景判断是否调用工具。
-5. 调用大模型生成结果。
-6. 解析为结构化输出。
-7. 写入审计日志。
-8. 返回页面展示。
-
-### 9.4 文件上传
-
-系统需要支持上传题目材料和知识库文档。
-
-V1 支持的文件类型:
-
-- TXT
-- Markdown
-- PDF
-- DOCX
-- XLSX 可作为后续增强
-
-文件上传后需要保存:
-
-- 原始文件名。
-- 文件路径。
-- 文件类型。
-- 上传时间。
-- 关联场景。
-- 是否已入库。
-
-### 9.5 RAG 知识库
-
-系统需要支持将上传文档写入向量库,并在 Agent 对话时检索。
-
-V1 RAG 流程:
-
-1. 读取上传文件文本。
-2. 按固定长度切分文本。
-3. 生成 embedding。
-4. 写入 Chroma collection。
-5. 对话时根据用户问题检索 top_k 片段。
-6. 将片段作为上下文传给 Agent。
-7. 在结果中展示引用来源。
-
-### 9.6 工具调用
-
-系统需要提供一个工具注册机制。
-
-V1 内置工具建议包括:
-
-| 工具名 | 用途 |
-|---|---|
-| calculate_rate | 计算比例、缺陷率、通过率等指标 |
-| query_demo_records | 查询模拟业务数据 |
-| check_required_fields | 检查文档或表单必填项 |
-| generate_action_items | 根据问题生成行动项 |
-
-工具调用需要记录到审计日志中。
-
-### 9.7 结构化输出
-
-不同场景需要不同输出模板。
-
-V1 至少支持以下输出类型:
-
-#### 通用问答输出
-
-- answer
-- references
-- confidence
-
-#### 文档审核输出
-
-- summary
-- issues
-- risk_level
-- suggestions
-- missing_items
-- references
-
-#### 工单处理输出
-
-- reply
-- category
-- priority
-- suggested_action
-- need_human_review
-
-#### 质量分析输出
-
-- summary
-- possible_causes
-- evidence
-- risk_level
-- suggested_actions
-- references
-
-### 9.8 审计日志
-
-系统需要记录每次 Agent 执行过程。
-
-审计字段:
-
-- 日志 ID。
-- 场景 ID。
-- 用户输入。
-- 检索片段。
-- 工具调用记录。
-- 模型名称。
-- 结构化输出。
-- 原始输出。
-- 执行耗时。
-- 创建时间。
-
-审计日志页面需要支持:
-
-- 查看日志列表。
-- 查看单条日志详情。
-- 展示检索内容。
-- 展示工具调用。
-- 展示最终输出。
-
-### 9.9 模型适配
-
-系统需要通过统一接口调用大模型,避免模型 API 写死。
-
-V1 模型适配器需要支持:
-
-- 从环境变量读取 API Key。
-- 从环境变量读取 Base URL。
-- 从环境变量读取 Model Name。
-- 支持 OpenAI API 兼容格式,可接入 OpenAI、硅基流动等兼容供应商。
-- 支持独立配置 Embedding 模型,用于 RAG 入库和检索。
-
-环境变量示例:
-
-```env
-DJANGO_SECRET_KEY=replace-with-a-local-secret-key
-DJANGO_DEBUG=true
-DJANGO_ALLOWED_HOSTS=*
-
-LLM_API_KEY=your_api_key
-LLM_BASE_URL=https://api.openai.com/v1
-LLM_MODEL=gpt-4.1-mini
-EMBEDDING_API_KEY=
-EMBEDDING_BASE_URL=
-EMBEDDING_MODEL=text-embedding-3-small
-SCENARIO_CONFIG_DIR=configs
-UPLOAD_ROOT=data/uploads
-CHROMA_PATH=data/chroma
-```
-
-补充说明:
-
-- `EMBEDDING_API_KEY` 为空时可复用 `LLM_API_KEY`。
-- `EMBEDDING_BASE_URL` 为空时可复用 `LLM_BASE_URL`。
-- `.env.example` 仅作为模板,不允许放真实密钥。
-- 当前 V1 代码会在 settings 初始化时自动读取根目录 `.env`,本地运行与 `pytest` 可复用同一套配置;当前 Docker Compose 配置也通过 `env_file` 读取 `.env`。
-
-## 10. Dify 集成策略
-
-V1 不把 Dify 作为核心依赖。
-
-原因:
-
-- 复试现场需要最大程度保证可控。
-- 自研 Agent Core 更方便解释架构设计。
-- 题目未知时,直接依赖外部平台会增加部署和调试风险。
-- Django + Agent Core 已能覆盖第一版演示需求。
-
-系统预留 Agent Engine Adapter 概念,后续可接入 Dify、OpenAI Agents SDK 或其他企业 AI 平台。
-
-V1 默认引擎:
-
-```text
-Lightweight Orchestrator
-```
-
-后续可扩展:
-
-```text
-Dify API Adapter
-OpenAI Agents SDK Adapter
-LangGraph Adapter
-```
-
-## 11. Docker 部署需求
-
-系统需要支持 Docker Compose 一键启动。
-
-基础命令:
-
-```bash
-docker compose up --build
-```
-
-V1 容器内容:
-
-- Django Web 服务。
-- SQLite 数据文件挂载。
-- Chroma 数据目录挂载。
-- 上传文件目录挂载。
-
-V1 暂不强制引入 PostgreSQL。如果后续需要更正式的部署效果,可以在 Docker Compose 中增加 PostgreSQL 服务。
-
-## 12. 推荐项目结构
-
-```text
-universal-agent-demo/
-  manage.py
-  requirements.txt
-  Dockerfile
-  docker-compose.yml
-  .env.example
-  README.md
-
-  config/
-    settings.py
-    urls.py
-    wsgi.py
-    asgi.py
-
-  apps/
-    scenarios/
-      models.py
-      admin.py
-      services.py
-
-    chat/
-      views.py
-      urls.py
-      forms.py
-
-    documents/
-      models.py
-      views.py
-      services.py
-
-    audit/
-      models.py
-      admin.py
-      services.py
-
-  agent_core/
-    orchestrator.py
-    scenario_loader.py
-    llm_provider.py
-    tool_registry.py
-    structured_output.py
-
-    rag/
-      ingest.py
-      retriever.py
-
-    tools/
-      builtin_tools.py
-
-    schemas/
-      outputs.py
-
-  configs/
-    knowledge_qa.yaml
-    document_review.yaml
-    ticket_assistant.yaml
-    quality_analysis.yaml
-    risk_audit.yaml
-
-  data/
-    uploads/
-    chroma/
-    db.sqlite3
-
-  templates/
-    base.html
-    home.html
-    chat/index.html
-    documents/upload.html
-    audit/logs.html
-
-  static/
-    css/
-    js/
-```
-
-## 13. 模块需求文档
-
-V1 按 6 个核心模块拆分,具体模块需求见:
-
-| 模块 | 文档 |
-|---|---|
-| 配置 | `docs/需求分析/3.配置模块需求.md` |
-| 场景 | `docs/需求分析/4.场景模块需求.md` |
-| 文档 | `docs/需求分析/5.文档模块需求.md` |
-| 对话 | `docs/需求分析/6.对话模块需求.md` |
-| 审计 | `docs/需求分析/7.审计模块需求.md` |
-| 智能核心 | `docs/需求分析/8.智能核心模块需求.md` |
-
-模块总览见:
-
-```text
-docs/需求分析/2.模块需求索引.md
-```
-
-## 14. V1 验收标准
-
-V1 完成后,需要满足以下验收标准:
-
-- 可以通过 Docker Compose 启动系统。
-- 首页可以看到至少 5 个预置场景。
-- 可以进入某个场景进行 Agent 对话。
-- 可以上传 TXT、Markdown、PDF 或 DOCX 文件。
-- 可以将上传文件写入本地知识库。
-- Agent 回答时可以使用知识库检索结果。
-- 至少支持 2 个内置工具调用。
-- Agent 输出可以以结构化方式展示。
-- 每次对话都会生成审计日志。
-- 审计日志中可以查看用户问题、检索内容、工具调用和最终输出。
-- 可以通过环境变量切换大模型 API 地址和模型名。
-
-## 15. 复试改题策略
-
-拿到题目后,优先按以下步骤适配:
-
-1. 判断题目属于哪类模板。
-2. 复制最接近的 YAML 场景配置。
-3. 修改 Agent 角色、任务目标和输出模板。
-4. 上传题目给出的文档或样例数据。
-5. 如果题目需要业务计算,则新增一个工具函数。
-6. 用 2 到 3 个测试问题验证效果。
-7. 演示时重点展示配置、知识库、工具调用、结构化输出和审计日志。
-
-题型映射:
-
-| 题目类型 | 优先模板 |
-|---|---|
-| SOP 问答 | knowledge_qa |
-| 制度问答 | knowledge_qa |
-| 文档审核 | document_review |
-| 客服处理 | ticket_assistant |
-| 质量异常分析 | quality_analysis |
-| 财务审核 | risk_audit |
-| 采购审核 | risk_audit |
-| 合同风险分析 | document_review 或 risk_audit |
-
-## 16. 后续迭代方向
-
-V1 完成后,可以根据时间增加以下能力:
-
-- 支持 Excel 数据分析工具。
-- 支持后台页面编辑场景配置,并同步生成或更新 YAML。
-- 支持流式输出。
-- 支持 OpenAI Agents SDK Adapter。
-- 支持 Dify API Adapter。
-- 支持 PostgreSQL 部署模式。
-- 支持简单登录认证。
-- 支持演示数据一键初始化。
diff --git a/docs/需求分析/2.模块需求索引.md b/docs/需求分析/2.模块需求索引.md
deleted file mode 100644
index ee50f30..0000000
--- a/docs/需求分析/2.模块需求索引.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# 模块需求文档索引
-
-本文档用于汇总 Universal Agent Demo Framework V1 的模块拆分和需求文档位置。
-
-## 1. 模块拆分原则
-
-V1 按 6 个核心模块拆分:
-
-```text
-config
-apps.scenarios
-apps.documents
-apps.chat
-apps.audit
-agent_core
-```
-
-拆分原则:
-
-- Django Apps 负责业务外壳。
-- Agent Core 负责 AI 能力。
-- RAG、工具调用、模型适配不直接写进 View。
-- 第一版不做复杂权限、多租户和完整工作流。
-- 模块数量保持克制,方便复试前快速改题。
-
-## 2. 模块文档列表
-
-| 模块 | 文档 | 说明 |
-|---|---|---|
-| 配置 | `3.配置模块需求.md` | Django 项目配置、环境变量、部署配置 |
-| 场景 | `4.场景模块需求.md` | 场景模板、场景配置、场景列表 |
-| 文档 | `5.文档模块需求.md` | 文件上传、文件管理、RAG 入库入口 |
-| 对话 | `6.对话模块需求.md` | 对话页面、Agent 调用、结果展示 |
-| 审计 | `7.审计模块需求.md` | 审计日志、检索记录、工具调用记录 |
-| 智能核心 | `8.智能核心模块需求.md` | RAG、工具、模型调用、结构化输出、编排 |
-
-## 3. 模块依赖关系
-
-```text
-apps.chat
-  |-- depends on apps.scenarios
-  |-- depends on apps.audit
-  |-- calls agent_core
-
-apps.documents
-  |-- depends on apps.scenarios
-  |-- calls agent_core.rag.ingest
-
-apps.audit
-  |-- stores result from apps.chat / agent_core
-
-agent_core
-  |-- reads scenario config object
-  |-- uses Chroma
-  |-- uses LLM Provider
-  |-- uses Tool Registry
-```
-
-## 4. 推荐开发顺序
-
-建议按以下顺序开发:
-
-1. Config 模块:保证项目可启动。
-2. Scenarios 模块:展示 5 个预置场景。
-3. 智能核心最小闭环:输入问题,通过 OpenAI 兼容模型接口返回结构化结果。
-4. Chat 模块:页面调用 Agent Core。
-5. Audit 模块:记录每次对话。
-6. Documents 模块:上传文档。
-7. Agent Core RAG:文档入库和检索。
-8. Agent Core 工具系统:增加内置工具。
-9. Docker:一键启动。
-
-## 5. V1 完成标准
-
-模块文档全部完成后,V1 的实现应满足:
-
-- 系统可以启动。
-- 首页可以看到 5 个场景。
-- 可以进入场景对话。
-- 可以上传文档。
-- 可以触发 RAG 入库。
-- Agent 可以返回结构化输出。
-- 工具调用和引用来源可以展示。
-- 每次对话都有审计日志。
-- Docker Compose 可以一键启动。
diff --git a/docs/需求分析/3.配置模块需求.md b/docs/需求分析/3.配置模块需求.md
deleted file mode 100644
index bb4a671..0000000
--- a/docs/需求分析/3.配置模块需求.md
+++ /dev/null
@@ -1,115 +0,0 @@
-# 配置模块需求文档
-
-## 1. 模块定位
-
-Config 模块是 Django 项目的基础配置模块,负责系统启动、路由装配、环境变量读取、静态资源、文件存储、数据库、日志和第三方组件配置。
-
-该模块不承载业务逻辑,只负责让系统稳定启动,并为其他模块提供统一运行环境。
-
-## 2. 模块目标
-
-- 支持本地开发和 Docker 部署两种运行方式。
-- 支持通过环境变量切换模型 API、Embedding API、调试模式和文件路径。
-- 统一注册 Django Apps、模板目录、静态资源目录和上传目录。
-- 提供系统级 URL 路由入口。
-- 为后续扩展 PostgreSQL、Redis、Celery 等组件预留配置空间。
-
-## 3. 职责边界
-
-### 3.1 负责
-
-- Django `settings.py` 配置。
-- Django `urls.py` 总路由配置。
-- WSGI / ASGI 启动配置。
-- 环境变量读取。
-- SQLite 默认数据库配置。
-- 静态文件和上传文件配置。
-- Chroma 本地持久化目录配置。
-- LLM 与 Embedding 相关环境变量配置。
-
-### 3.2 不负责
-
-- 不处理具体 Agent 业务逻辑。
-- 不解析场景 YAML。
-- 不处理文件入库。
-- 不直接调用大模型。
-- 不保存审计日志。
-
-## 4. 配置项需求
-
-系统至少需要支持以下环境变量:
-
-| 配置项 | 默认值 | 说明 |
-|---|---|---|
-| `DJANGO_SECRET_KEY` | `dev-secret-key` | Django 密钥 |
-| `DJANGO_DEBUG` | `true` | 是否开启调试模式 |
-| `DJANGO_ALLOWED_HOSTS` | `*` | 允许访问的主机 |
-| `DATABASE_URL` | 空 | 预留配置,V1 默认 SQLite,不要求解析该配置 |
-| `LLM_API_KEY` | 空 | 大模型 API Key |
-| `LLM_BASE_URL` | `https://api.openai.com/v1` | OpenAI 兼容接口地址,可接入 OpenAI、硅基流动等兼容服务 |
-| `LLM_MODEL` | `gpt-4.1-mini` | 默认模型名称 |
-| `EMBEDDING_API_KEY` | 空 | Embedding API Key;为空时可复用 `LLM_API_KEY` |
-| `EMBEDDING_BASE_URL` | 空 | Embedding OpenAI 兼容接口地址;为空时可复用 `LLM_BASE_URL` |
-| `EMBEDDING_MODEL` | `text-embedding-3-small` | 默认 Embedding 模型名称 |
-| `CHROMA_PATH` | `data/chroma` | Chroma 持久化目录 |
-| `UPLOAD_ROOT` | `data/uploads` | 上传文件目录 |
-| `SCENARIO_CONFIG_DIR` | `configs` | 场景配置目录 |
-
-补充要求:
-
-- `.env.example` 仅作为模板文件,不得写入真实密钥。
-- 本地直接执行 `python manage.py runserver` 时,应自动读取根目录 `.env`。
-- Docker 运行时可通过 `env_file` 或容器环境变量注入同一组配置;当前仓库默认由 Compose 读取 `.env`。
-
-## 5. 目录需求
-
-系统启动时需要保证以下目录存在:
-
-```text
-data/
-  uploads/
-  chroma/
-  db.sqlite3
-
-configs/
-```
-
-如果目录不存在,V1 可以在初始化脚本或启动流程中创建。
-
-## 6. 路由需求
-
-总路由需要聚合以下模块路由:
-
-| 路径 | 模块 | 用途 |
-|---|---|---|
-| `/` | `apps.scenarios` | 首页和场景列表 |
-| `/chat/` | `apps.chat` | Agent 对话 |
-| `/documents/` | `apps.documents` | 文件上传和文档管理 |
-| `/audit/` | `apps.audit` | 审计日志查看 |
-| `/admin/` | Django Admin | 后台管理 |
-
-## 7. 启动需求
-
-本地启动:
-
-```bash
-python manage.py migrate
-python manage.py runserver
-```
-
-说明:上述命令执行前,应先准备好根目录 `.env`;当前 V1 代码会在启动时自动加载该文件。
-
-Docker 启动:
-
-```bash
-docker compose up --build
-```
-
-## 8. 验收标准
-
-- 项目可以通过 `python manage.py runserver` 启动。
-- 项目可以通过 `docker compose up --build` 启动。
-- `/admin/` 可以访问。
-- 首页 `/` 可以访问。
-- 环境变量可以覆盖默认 LLM 与 Embedding 配置。
-- 上传目录和 Chroma 目录有明确配置。
diff --git a/docs/需求分析/4.场景模块需求.md b/docs/需求分析/4.场景模块需求.md
deleted file mode 100644
index 390645f..0000000
--- a/docs/需求分析/4.场景模块需求.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# 场景模块需求文档
-
-## 1. 模块定位
-
-Scenarios 模块负责管理业务 Agent 场景,是整个平台快速适配未知复试题的核心入口。
-
-场景定义需要尽量配置化,避免把具体业务逻辑写死在 Django View 或 Agent Core 中。
-
-## 2. 模块目标
-
-- 读取预置场景配置。
-- 展示可用业务 Agent 列表。
-- 提供场景详情。
-- 为 Chat 模块提供当前场景的完整配置。
-- 以 YAML 配置文件作为 V1 场景唯一事实来源。
-
-## 3. 职责边界
-
-### 3.1 负责
-
-- 场景模板定义。
-- 场景配置文件读取。
-- 场景元信息展示。
-- 场景启用/禁用状态。
-- 场景与文档、审计日志的关联关系。
-
-### 3.2 不负责
-
-- 不执行 Agent 对话。
-- 不直接处理 RAG 检索。
-- 不直接调用工具。
-- 不直接调用大模型。
-- 不解析结构化输出。
-
-## 4. 场景模板需求
-
-V1 预置 5 类场景模板:
-
-| 模板 ID | 模板名称 | 适用题型 |
-|---|---|---|
-| `knowledge_qa` | 知识库问答助手 | SOP、制度、客服知识库、内部文档问答 |
-| `document_review` | 文档审核助手 | 合同审核、制度审核、材料合规检查 |
-| `ticket_assistant` | 工单处理助手 | 客服工单、售后工单、运维工单 |
-| `quality_analysis` | 质量异常分析助手 | 生产质量、缺陷分析、原因定位 |
-| `risk_audit` | 风险审核助手 | 财务审核、采购审核、报销审核、合同风险 |
-
-## 5. 场景配置字段
-
-场景配置文件使用 YAML。V1 的后台管理只管理上传文件、审计日志和示例业务数据等外围数据,不作为场景配置入口。
-
-必填字段:
-
-| 字段 | 类型 | 说明 |
-|---|---|---|
-| `id` | string | 场景唯一标识 |
-| `name` | string | 场景名称 |
-| `description` | string | 场景说明 |
-| `agent.role` | string | Agent 角色 |
-| `agent.goal` | string | Agent 目标 |
-| `agent.instructions` | list[string] | Agent 指令 |
-| `agent.system_prompt` | string | 可选字段;配置后优先作为系统提示词 |
-| `rag.enabled` | boolean | 是否启用 RAG |
-| `tools` | list[string] | 可用工具列表 |
-| `output.type` | string | 输出模板类型 |
-| `audit.enabled` | boolean | 是否记录审计 |
-
-示例:
-
-```yaml
-id: document_review
-name: 文档审核助手
-description: 检查合同、制度或 SOP 中的风险点和缺失项
-
-agent:
-  role: 文档审核专家
-  goal: 根据审核规则和知识库内容输出结构化审核意见
-  system_prompt: ""
-  instructions:
-    - 只基于用户提供文档和知识库进行判断
-    - 不确定的问题必须标记为需人工复核
-    - 输出必须包含风险等级和修改建议
-
-rag:
-  enabled: true
-  collection: document_review
-  top_k: 5
-
-tools:
-  - check_required_fields
-
-output:
-  type: document_review_report
-
-audit:
-  enabled: true
-```
-
-## 6. 页面需求
-
-### 6.1 场景列表页
-
-路径:`/`
-
-展示内容:
-
-- 场景名称。
-- 场景描述。
-- 适用题型。
-- 是否启用。
-- 进入对话按钮。
-
-### 6.2 场景详情页 可选
-
-路径:`/scenarios//`
-
-展示内容:
-
-- Agent 角色。
-- Agent 目标。
-- RAG 是否启用。
-- 可用工具列表。
-- 输出模板类型。
-
-V1 可以不做独立详情页,在对话页展示当前场景摘要即可。
-
-## 7. 服务接口需求
-
-Scenarios 模块至少需要提供以下服务函数:
-
-```text
-list_scenarios() -> list[ScenarioConfig]
-get_scenario(scenario_id: str) -> ScenarioConfig
-validate_scenario(config: dict) -> ValidationResult
-```
-
-## 8. 验收标准
-
-- 首页可以展示 5 个预置场景。
-- 点击场景可以进入对应对话页。
-- 场景配置来自配置文件,而不是硬编码在 View 中。
-- 后台管理不作为 V1 场景配置编辑入口。
-- 缺失必填字段时能给出明确错误。
-- Chat 模块可以根据 `scenario_id` 获取完整场景配置。
diff --git a/docs/需求分析/5.文档模块需求.md b/docs/需求分析/5.文档模块需求.md
deleted file mode 100644
index 8ff736e..0000000
--- a/docs/需求分析/5.文档模块需求.md
+++ /dev/null
@@ -1,132 +0,0 @@
-# 文档模块需求文档
-
-## 1. 模块定位
-
-Documents 模块负责文件上传、文件管理、文本抽取和知识库入库入口。
-
-该模块是复试题快速适配的关键模块。拿到题目材料后,用户需要能快速上传文档,并让 Agent 在对话中使用这些文档。
-
-## 2. 模块目标
-
-- 支持上传题目材料和知识库文件。
-- 保存文件元数据。
-- 支持按场景关联文件。
-- 提供文档入库入口。
-- 为 Agent Core 的 RAG 模块提供文件内容。
-
-## 3. 职责边界
-
-### 3.1 负责
-
-- 文件上传页面。
-- 文件保存。
-- 文件元数据记录。
-- 文件与场景关联。
-- 文本抽取入口。
-- 触发 RAG 入库。
-
-### 3.2 不负责
-
-- 不负责具体向量检索算法。
-- 不负责 embedding 生成细节。
-- 不负责 Agent 对话编排。
-- 不负责模型回答。
-
-## 4. 支持文件类型
-
-V1 必须支持:
-
-| 类型 | 扩展名 | 说明 |
-|---|---|---|
-| 文本文档 | `.txt` | 第一优先级,最稳定 |
-| Markdown | `.md` | 适合准备知识库和规则 |
-| PDF | `.pdf` | 复试常见材料格式,V1 抽取纯文本 |
-| Word | `.docx` | 复试常见材料格式,V1 抽取段落文本 |
-
-后续增强:
-
-| 类型 | 扩展名 | 说明 |
-|---|---|---|
-| Excel | `.xlsx` | 后续可作为业务数据源或结构化表格导入 |
-
-## 5. 数据模型需求
-
-建议模型:`UploadedDocument`
-
-字段:
-
-| 字段 | 类型 | 说明 |
-|---|---|---|
-| `id` | int | 主键 |
-| `scenario_id` | string | 关联场景 ID |
-| `original_name` | string | 原始文件名 |
-| `file` | FileField | Django FileField 相对路径,不保存用户本机绝对路径 |
-| `file_type` | string | 文件类型 |
-| `size` | int | 文件大小 |
-| `status` | string | `uploaded` / `indexed` / `failed` |
-| `error_message` | text | 入库失败原因 |
-| `created_at` | datetime | 上传时间 |
-| `updated_at` | datetime | 更新时间 |
-
-## 6. 页面需求
-
-### 6.1 文件上传页
-
-路径:`/documents/upload/`
-
-页面元素:
-
-- 场景选择下拉框。
-- 文件选择按钮。
-- 上传按钮。
-- 支持类型提示。
-- 上传结果提示。
-
-### 6.2 文件列表页
-
-路径:`/documents/`
-
-展示内容:
-
-- 文件名。
-- 所属场景。
-- 文件类型。
-- 文件大小。
-- 入库状态。
-- 上传时间。
-- 入库按钮。
-
-## 7. RAG 入库流程
-
-用户上传文件后,可以手动触发入库。
-
-流程:
-
-1. 用户上传文件。
-2. 系统保存文件和元数据。
-3. 用户点击入库按钮。
-4. Documents 模块读取文件文本。
-5. 调用 `agent_core.rag.ingest`。
-6. 入库成功后更新状态为 `indexed`。
-7. 入库失败后更新状态为 `failed` 并保存错误信息。
-
-## 8. 文本抽取需求
-
-V1 文本抽取策略:
-
-- `.txt`:按 UTF-8 读取,失败时尝试系统默认编码。
-- `.md`:按 UTF-8 读取,保留标题和正文。
-- `.pdf`:抽取纯文本,不要求 OCR、表格还原和复杂版式理解。
-- `.docx`:抽取段落、标题和普通表格文本,不要求完整保留 Word 样式。
-
-入库失败后的文档允许重新触发入库。重新入库前需要清理或覆盖同一 `document_id` 对应的旧 chunk,避免重复检索。
-
-## 9. 验收标准
-
-- 可以上传 `.txt`、`.md`、`.pdf`、`.docx` 文件。
-- 上传后可以在文件列表看到记录。
-- 文件可以关联到指定场景。
-- 可以触发文件入库。
-- 入库成功后状态变为 `indexed`。
-- 入库失败时页面能显示失败原因。
-- 入库失败的文档可以重新入库。
diff --git a/docs/需求分析/6.对话模块需求.md b/docs/需求分析/6.对话模块需求.md
deleted file mode 100644
index f3b9638..0000000
--- a/docs/需求分析/6.对话模块需求.md
+++ /dev/null
@@ -1,129 +0,0 @@
-# 对话模块需求文档
-
-## 1. 模块定位
-
-Chat 模块负责 Agent 对话页面和用户交互,是复试演示时最核心的入口。
-
-该模块接收用户问题,加载场景配置,调用 Agent Core 执行智能编排,并将结构化结果、引用来源、工具调用和审计信息展示给用户。
-
-## 2. 模块目标
-
-- 提供按场景进入的 Agent 对话页。
-- 支持用户输入业务问题。
-- 调用 Agent Core 执行完整 Agent 流程。
-- 展示结构化输出。
-- 展示 RAG 引用片段。
-- 展示工具调用记录。
-- 触发审计日志写入。
-
-## 3. 职责边界
-
-### 3.1 负责
-
-- 对话页面渲染。
-- 表单接收和校验。
-- 当前场景上下文传递。
-- 调用 Agent Core。
-- 展示 Agent 返回结果。
-
-### 3.2 不负责
-
-- 不直接读取 YAML 场景文件。
-- 不直接执行 RAG 检索。
-- 不直接执行工具函数。
-- 不直接调用大模型 API。
-- 不直接写复杂审计细节。
-
-## 4. 页面需求
-
-### 4.1 Agent 对话页
-
-路径:`/chat//`
-
-页面区域:
-
-- 当前场景摘要。
-- 当前场景下已入库文档多选框。
-- 用户问题输入框。
-- 提交按钮。
-- Agent 结构化输出区域。
-- 引用来源区域。
-- 工具调用区域。
-- 执行耗时区域。
-- 审计日志详情入口。
-
-## 5. 表单需求
-
-用户输入表单字段:
-
-| 字段 | 类型 | 必填 | 说明 |
-|---|---|---|---|
-| `message` | textarea | 是 | 用户业务问题 |
-| `document_ids` | list[int] | 否 | 本次对话指定使用的已入库文档 |
-
-校验规则:
-
-- 输入不能为空。
-- 输入长度建议不超过 4000 字。
-- 如果场景不存在,需要返回明确错误。
-- `document_ids` 只能包含当前场景下状态为 `indexed` 的文档。
-- 未选择文档时,默认使用当前场景下全部已入库文档作为 RAG 范围。
-
-## 6. Agent 执行流程
-
-Chat 模块调用 Agent Core 的流程:
-
-```text
-用户提交问题
-  ↓
-校验 scenario_id 和 message
-  ↓
-获取场景配置
-  ↓
-调用 `run_agent(scenario_config, user_input, options=None)`
-  ↓
-获取 AgentResult
-  ↓
-调用 Audit 模块记录日志
-  ↓
-渲染结果页面
-```
-
-## 7. AgentResult 展示需求
-
-Agent Core 返回结果建议包含:
-
-| 字段 | 说明 |
-|---|---|
-| `answer` | 自然语言回答 |
-| `structured_output` | 结构化结果 |
-| `references` | RAG 引用来源 |
-| `tool_calls` | 工具调用记录 |
-| `raw_output` | 模型原始输出 |
-| `latency_ms` | 执行耗时 |
-| `error` | 错误信息 |
-
-页面需要优先展示结构化结果。如果结构化解析失败,则展示自然语言回答和错误提示。
-
-## 8. 错误处理需求
-
-需要处理以下错误:
-
-| 错误 | 页面行为 |
-|---|---|
-| 场景不存在 | 显示场景不存在 |
-| 用户输入为空 | 显示表单错误 |
-| LLM API Key 缺失 | 显示模型配置缺失 |
-| RAG 检索失败 | 显示检索失败,但允许模型基于已有信息回答 |
-| 工具调用失败 | 显示工具失败信息,并继续生成结果 |
-| 结构化解析失败 | 展示原始回答,并提示结构化解析失败 |
-
-## 9. 验收标准
-
-- 可以从场景列表进入对话页。
-- 可以提交问题并获得 Agent 输出。
-- 页面能展示结构化结果。
-- 页面能展示引用来源。
-- 页面能展示工具调用记录。
-- 执行失败时有可理解的错误提示。
-- 每次对话都会产生审计日志。
diff --git a/docs/需求分析/7.审计模块需求.md b/docs/需求分析/7.审计模块需求.md
deleted file mode 100644
index eebf8cd..0000000
--- a/docs/需求分析/7.审计模块需求.md
+++ /dev/null
@@ -1,143 +0,0 @@
-# Audit 模块需求文档
-
-## 1. 模块定位
-
-Audit 模块负责记录和展示 Agent 执行过程,是项目体现企业级能力的重要模块。
-
-复试演示时,审计日志用于证明系统不是黑盒问答,而是可以追踪输入、检索、工具调用、模型输出和执行耗时。
-
-## 2. 模块目标
-
-- 记录每次 Agent 对话。
-- 记录 RAG 检索片段。
-- 记录工具调用详情。
-- 记录模型输出和结构化结果。
-- 提供审计日志列表和详情页。
-- 支持按场景查看日志。
-
-## 3. 职责边界
-
-### 3.1 负责
-
-- 审计日志数据模型。
-- 日志写入服务。
-- 日志列表页面。
-- 日志详情页面。
-- 工具调用记录展示。
-- RAG 引用记录展示。
-
-### 3.2 不负责
-
-- 不执行 Agent。
-- 不执行工具调用。
-- 不执行 RAG 检索。
-- 不参与模型生成。
-- 不做复杂权限控制。
-
-## 4. 数据模型需求
-
-建议模型:`AgentAuditLog`
-
-字段:
-
-| 字段 | 类型 | 说明 |
-|---|---|---|
-| `id` | int | 主键 |
-| `scenario_id` | string | 场景 ID |
-| `scenario_name` | string | 场景名称 |
-| `user_input` | text | 用户输入 |
-| `retrieved_chunks` | JSON | 检索片段 |
-| `tool_calls` | JSON | 工具调用记录 |
-| `structured_output` | JSON | 结构化输出 |
-| `final_answer` | text | 最终回答 |
-| `raw_output` | text | 模型原始输出 |
-| `model_name` | string | 模型名称 |
-| `latency_ms` | int | 执行耗时 |
-| `status` | string | `success` / `failed` |
-| `error_message` | text | 错误信息 |
-| `created_at` | datetime | 创建时间 |
-
-## 5. 日志写入需求
-
-Audit 模块需要提供服务函数:
-
-```text
-create_audit_log(
-  scenario_id,
-  scenario_name,
-  user_input,
-  agent_result
-) -> AgentAuditLog
-```
-
-写入规则:
-
-- Agent 成功时,记录完整结果。
-- Agent 失败时,也要记录用户输入、场景和错误信息。
-- RAG 片段和工具调用使用 JSON 保存。
-- 不记录 API Key 等敏感配置。
-
-## 6. 页面需求
-
-### 6.1 审计日志列表页
-
-路径:`/audit/`
-
-展示字段:
-
-- 日志 ID。
-- 场景名称。
-- 用户输入摘要。
-- 状态。
-- 模型名称。
-- 执行耗时。
-- 创建时间。
-- 详情入口。
-
-### 6.2 审计日志详情页
-
-路径:`/audit//`
-
-展示内容:
-
-- 用户输入。
-- 最终回答。
-- 结构化输出。
-- RAG 检索片段。
-- 工具调用记录。
-- 模型名称。
-- 执行耗时。
-- 错误信息。
-
-## 7. 检索片段展示需求
-
-每个引用片段建议包含:
-
-| 字段 | 说明 |
-|---|---|
-| `source` | 来源文件名 |
-| `chunk_id` | 片段 ID |
-| `content` | 片段内容 |
-| `score` | 相似度分数 |
-
-## 8. 工具调用展示需求
-
-每次工具调用建议包含:
-
-| 字段 | 说明 |
-|---|---|
-| `tool_name` | 工具名称 |
-| `arguments` | 调用参数 |
-| `result` | 工具结果 |
-| `success` | 是否成功 |
-| `error` | 错误信息 |
-
-## 9. 验收标准
-
-- 每次对话成功后都会生成审计日志。
-- Agent 执行失败时也会生成失败日志。
-- 审计列表可以查看所有日志。
-- 审计详情可以查看用户输入、检索片段、工具调用和最终输出。
-- 日志中不保存 API Key。
-- 可以根据日志解释一次 Agent 输出的依据。
-
diff --git a/docs/需求分析/8.智能核心模块需求.md b/docs/需求分析/8.智能核心模块需求.md
deleted file mode 100644
index bb70b08..0000000
--- a/docs/需求分析/8.智能核心模块需求.md
+++ /dev/null
@@ -1,225 +0,0 @@
-# 智能核心模块需求文档
-
-## 1. 模块定位
-
-Agent Core 是系统的智能能力核心,负责根据场景配置完成 RAG 检索、工具调用、大模型调用和结构化输出。
-
-该模块应保持独立于 Django View,方便后续迁移为独立服务,或接入 OpenAI Agents SDK、Dify 等外部编排引擎。
-
-## 2. 模块目标
-
-- 提供统一 Agent 执行入口。
-- 根据场景配置组织 Prompt。
-- 支持 RAG 检索。
-- 支持工具注册与调用。
-- 支持 OpenAI API 兼容的 LLM 与 Embedding 调用,可自主接入 OpenAI、硅基流动等兼容服务。
-- 支持结构化输出解析。
-- 返回可审计的 AgentResult。
-
-## 3. 职责边界
-
-### 3.1 负责
-
-- Agent 编排。
-- 场景配置对象消费。
-- RAG 入库和检索核心逻辑。
-- 工具注册和工具执行。
-- LLM Provider 适配。
-- 输出结构化解析。
-- 生成 AgentResult。
-
-### 3.2 不负责
-
-- 不渲染页面。
-- 不直接处理 Django 表单。
-- 不直接保存 Django Model。
-- 不管理用户登录。
-- 不负责 Docker 部署。
-
-## 4. 子模块划分
-
-```text
-agent_core/
-  orchestrator.py
-  scenario_loader.py
-  llm_provider.py
-  tool_registry.py
-  structured_output.py
-
-  rag/
-    ingest.py
-    retriever.py
-
-  tools/
-    builtin_tools.py
-
-  schemas/
-    outputs.py
-```
-
-## 5. Orchestrator 需求
-
-`orchestrator.py` 提供统一入口:
-
-```text
-run_agent(
-  scenario_config,
-  user_input,
-  options=None
-) -> AgentResult
-```
-
-执行流程:
-
-1. 读取场景配置。
-2. 根据配置判断是否启用 RAG。
-3. 按 `scenario_id` 和可选 `document_ids` 检索相关知识片段。
-4. 根据配置加载可用工具。
-5. 构造系统提示词。
-6. 调用大模型。
-7. 执行必要的工具调用。
-8. 解析结构化输出。
-9. 返回 AgentResult。
-
-V1 可以使用轻量 Orchestrator,不强制引入完整 Agent SDK。
-
-## 6. RAG 需求
-
-### 6.1 入库
-
-`rag/ingest.py` 负责:
-
-- 接收文档文本。
-- 文本切分。
-- 通过 OpenAI 兼容 Embedding Provider 生成 embedding。
-- 写入 Chroma。
-- 保存 metadata。
-
-metadata 至少包含:
-
-- `scenario_id`
-- `document_id`
-- `source_file`
-- `chunk_id`
-- `created_at`
-
-### 6.2 检索
-
-`rag/retriever.py` 负责:
-
-- 根据用户问题检索相关片段。
-- 支持按 `scenario_id` 过滤。
-- 支持按本次对话选择的 `document_ids` 过滤;未选择时使用当前场景全部已入库文档。
-- 返回 top_k 结果。
-- 返回内容、来源和分数。
-
-## 7. 工具系统需求
-
-`tool_registry.py` 负责工具注册、查找和执行。
-
-V1 内置工具:
-
-| 工具名 | 说明 |
-|---|---|
-| `calculate_rate` | 计算通过率、缺陷率、占比等 |
-| `query_demo_records` | 查询模拟业务数据 |
-| `check_required_fields` | 检查必填项是否缺失 |
-| `generate_action_items` | 生成行动项清单 |
-
-工具执行结果需要统一格式:
-
-```json
-{
-  "tool_name": "calculate_rate",
-  "success": true,
-  "arguments": {},
-  "result": {},
-  "error": ""
-}
-```
-
-## 8. LLM Provider 需求
-
-`llm_provider.py` 负责模型调用。
-
-V1 需要支持 OpenAI API 兼容 LLM 接口:
-
-- `LLM_API_KEY`
-- `LLM_BASE_URL`
-- `LLM_MODEL`
-
-接口需要隐藏不同模型供应商差异,对 Orchestrator 暴露统一方法:
-
-```text
-generate(messages, response_format=None) -> LLMResponse
-```
-
-Embedding 也通过 OpenAI 兼容接口接入:
-
-- `EMBEDDING_API_KEY`
-- `EMBEDDING_BASE_URL`
-- `EMBEDDING_MODEL`
-
-当 Embedding 专用 Key 或 Base URL 为空时,可以复用 LLM 的 Key 和 Base URL。RAG 入库和检索必须通过真实 embedding 与 Chroma 完成,模拟 embedding 或简单文本匹配只能作为开发阶段临时桩,不计入 V1 验收。
-
-## 9. 结构化输出需求
-
-`structured_output.py` 负责将模型输出转换为业务结构。
-
-V1 输出类型:
-
-- `general_answer`
-- `document_review_report`
-- `ticket_response`
-- `quality_report`
-- `risk_audit_report`
-
-解析策略:
-
-- 优先要求模型直接返回 JSON。
-- JSON 解析成功则展示结构化结果。
-- JSON 解析失败则保留原始输出,并返回解析错误。
-
-## 10. AgentResult 需求
-
-Agent Core 最终返回统一结果对象。
-
-字段:
-
-| 字段 | 说明 |
-|---|---|
-| `answer` | 最终自然语言回答 |
-| `structured_output` | 结构化输出 |
-| `references` | RAG 引用片段 |
-| `tool_calls` | 工具调用记录 |
-| `raw_output` | 模型原始输出 |
-| `model_name` | 模型名称 |
-| `latency_ms` | 执行耗时 |
-| `status` | `success` / `failed` |
-| `error` | 错误信息 |
-
-## 11. Adapter 扩展需求
-
-V1 默认使用 `LightweightOrchestrator`。
-
-后续可扩展:
-
-- OpenAI Agents SDK Adapter。
-- Dify API Adapter。
-- LangGraph Adapter。
-
-Adapter 需要保持同样输入输出:
-
-```text
-run_agent(scenario_config, user_input, options=None) -> AgentResult
-```
-
-## 12. 验收标准
-
-- Chat 模块可以调用 Agent Core 获得统一 AgentResult。
-- RAG 可以按场景检索知识片段。
-- RAG 可以按本次对话选择的文档范围检索知识片段。
-- 工具调用结果可以记录并返回。
-- LLM 与 Embedding 配置可以通过环境变量切换。
-- 结构化输出解析失败时不会导致整个流程崩溃。
-- Agent Core 不依赖 Django View。
diff --git a/templates/audit/log_detail.html b/templates/audit/log_detail.html
deleted file mode 100644
index fb79ca6..0000000
--- a/templates/audit/log_detail.html
+++ /dev/null
@@ -1,61 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}审计日志详情{% endblock %}
-
-{% block content %}
-  
-
-  
-
-

基础信息

-
    -
  • 场景:{{ log.scenario_name }}
  • -
  • 状态:{{ log.get_status_display_text }}
  • -
  • 模型:{{ log.model_name }}
  • -
  • 耗时:{{ log.latency_ms }} ms
  • -
-
- -
-

用户输入

-
{{ log.user_input|linebreaksbr }}
-
- -
-

最终回答

-
{{ log.final_answer|linebreaksbr }}
-
- -
-

结构化输出

-
{{ log.structured_output }}
-
- -
-

引用来源

-
{{ log.retrieved_chunks }}
-
- -
-

工具调用

-
{{ log.tool_calls }}
-
- -
-

原始输出

-
{{ log.raw_output }}
-
- - {% if log.error_message %} -
-

错误信息

-
{{ log.error_message }}
-
- {% endif %} -
-{% endblock %} diff --git a/templates/audit/log_list.html b/templates/audit/log_list.html deleted file mode 100644 index cca078b..0000000 --- a/templates/audit/log_list.html +++ /dev/null @@ -1,50 +0,0 @@ -{% extends "base.html" %} - -{% block title %}审计日志{% endblock %} - -{% block content %} - - -
- - - - - - - - - - - - - - - {% for log in logs %} - - - - - - - - - - - {% empty %} - - {% endfor %} - -
ID场景输入摘要状态模型耗时创建时间详情
{{ log.id }}{{ log.scenario_name }}{{ log.get_user_input_summary }}{{ log.get_status_display_text }}{{ log.model_name }}{{ log.latency_ms }} ms{{ log.created_at|date:"Y-m-d H:i" }}查看详情
暂无审计日志,先去执行一次对话吧。
-
-{% endblock %} diff --git a/templates/chat/index.html b/templates/chat/index.html deleted file mode 100644 index 663ba49..0000000 --- a/templates/chat/index.html +++ /dev/null @@ -1,169 +0,0 @@ -{% extends "base.html" %} - -{% block title %}{{ scenario.name|default:"Agent 对话" }}{% endblock %} - -{% block content %} - {% if error %} -
{{ error }}
- {% endif %} - - {% if scenario %} - - -
-
-
-

提问面板

-

可以直接提问,也可以勾选部分已入库文档作为当前上下文范围。

-
- {% csrf_token %} -
- {{ form.message.label_tag }} - {{ form.message }} - {% if form.message.errors %} -

{{ form.message.errors|join:" " }}

- {% endif %} -
-
- {{ form.document_ids.label_tag }} -

不勾选时默认检索当前场景全部已入库文档。

-
- {% for checkbox in form.document_ids %} - - {% empty %} -
当前场景还没有已入库文档,系统将仅依赖工具和模型能力生成结果。
- {% endfor %} -
- {% if form.document_ids.errors %} -

{{ form.document_ids.errors|join:" " }}

- {% endif %} -
-
- -
-
-
- -
-

执行说明

-
    -
  • - 1. 场景配置 - 系统会先读取当前 YAML 场景配置,确定角色、目标、工具和输出结构。 -
  • -
  • - 2. RAG 与工具 - 如果场景启用了知识库检索,系统会根据你的问题召回相关片段,并执行声明式工具。 -
  • -
  • - 3. 结构化结果 - Agent Core 会优先解析 JSON 输出,解析失败时回退为稳定的展示结构。 -
  • -
-
-
- -
-
-

回答总览

- {% if result %} -
    -
  • 模型:{{ result.model_name }}
  • -
  • 状态:{{ result.status }}
  • -
  • 耗时:{{ result.latency_ms }} ms
  • -
-
- 主回答 -
{{ result.answer|linebreaksbr }}
-
- {% if audit_log %} -

- 查看本次审计日志 -

- {% endif %} - {% else %} -
提交问题后,这里会展示 Agent 的主回答、模型信息和执行状态。
- {% endif %} -
- - {% if result %} -
-

结构化结果

- - - {% for key, value in result.structured_output.items %} - - - - - {% endfor %} - -
{{ key }} - {% if key == "answer" or key == "summary" or key == "reply" %} - {{ value|linebreaksbr }} - {% else %} -
{{ value }}
- {% endif %} -
-
- -
-

引用片段

- {% if result.references %} -
    - {% for reference in result.references %} -
  • - {{ reference.source }} -
    {{ reference.content|default:"无正文内容"|linebreaksbr }}
    -
  • - {% endfor %} -
- {% else %} -
当前回答没有引用知识库片段。
- {% endif %} -
- -
-

工具调用

- {% if result.tool_calls %} -
    - {% for tool_call in result.tool_calls %} -
  • - {{ tool_call.tool_name }} -

    执行状态:{{ tool_call.success }}

    - {% if tool_call.error %} -

    {{ tool_call.error }}

    - {% endif %} -
    {{ tool_call.result }}
    -
  • - {% endfor %} -
- {% else %} -
当前场景没有声明工具,或本次执行无需调用工具。
- {% endif %} -
- - {% if result.error %} -
-

错误信息

-
{{ result.error }}
-
- {% endif %} - {% endif %} -
-
- {% endif %} -{% endblock %} diff --git a/templates/documents/document_list.html b/templates/documents/document_list.html deleted file mode 100644 index 3d9eca1..0000000 --- a/templates/documents/document_list.html +++ /dev/null @@ -1,54 +0,0 @@ -{% extends "base.html" %} - -{% block title %}文档中心{% endblock %} - -{% block content %} - - -
- - - - - - - - - - - - - {% for document in documents %} - - - - - - - - - {% empty %} - - {% endfor %} - -
文件名场景类型大小状态操作
{{ document.original_name }}{{ document.scenario_id }}{{ document.file_type }}{{ document.size }}{{ document.get_status_display_text }} - {% if document.status != "indexed" %} -
- {% csrf_token %} - -
- {% else %} - 已可用于检索 - {% endif %} - {% if document.error_message %} -
{{ document.error_message }}
- {% endif %} -

上传时间:{{ document.created_at|date:"Y-m-d H:i" }}

-
暂无文件,请先上传题目材料。
-
-{% endblock %} diff --git a/templates/documents/upload.html b/templates/documents/upload.html deleted file mode 100644 index 37883be..0000000 --- a/templates/documents/upload.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "base.html" %} - -{% block title %}上传文件{% endblock %} - -{% block content %} - - -
-
- {% csrf_token %} -
- {{ form.scenario_id.label_tag }} - {{ form.scenario_id }} - {% if form.scenario_id.errors %} -

{{ form.scenario_id.errors|join:" " }}

- {% endif %} -
-
- {{ form.file.label_tag }} - {{ form.file }} - {% if form.file.errors %} -

{{ form.file.errors|join:" " }}

- {% endif %} -
- {% if form.errors %} -
{{ form.errors }}
- {% endif %} -
- - 返回文件列表 -
-
-
-{% endblock %} diff --git a/templates/scenarios/index.html b/templates/scenarios/index.html deleted file mode 100644 index 8fbca6d..0000000 --- a/templates/scenarios/index.html +++ /dev/null @@ -1,53 +0,0 @@ -{% extends "base.html" %} - -{% block title %}场景首页{% endblock %} - -{% block content %} - - - {% if scenario_issues %} -
-

配置异常

-

以下 YAML 场景文件存在问题,系统已自动跳过,不会影响其它合法场景展示。

-
    - {% for issue in scenario_issues %} -
  • - {{ issue.file_name }} -
    {{ issue.message }}
    -
  • - {% endfor %} -
-
- {% endif %} - -
- {% for scenario in scenarios %} -
-

{{ scenario.name }}

-

{{ scenario.description }}

-
    -
  • 场景 ID:{{ scenario.id }}
  • -
  • 输出:{{ scenario.output.type }}
  • -
  • RAG:{% if scenario.rag.enabled %}已启用{% else %}未启用{% endif %}
  • -
  • 工具数:{{ scenario.tool_count }}
  • -
-

适用题型: - {% if scenario.applicable_questions %} - {{ scenario.applicable_questions|join:"、" }} - {% else %} - 暂未配置 - {% endif %} -

-

- 进入对话 -

-
- {% empty %} -
暂无可用场景,请检查 `configs/` 目录和 YAML 配置内容。
- {% endfor %} -
-{% endblock %} diff --git a/tests/test_agent_core.py b/tests/test_agent_core.py deleted file mode 100644 index f66fb90..0000000 --- a/tests/test_agent_core.py +++ /dev/null @@ -1,250 +0,0 @@ -from agent_core.orchestrator import build_messages, run_agent -from agent_core.rag.ingest import _split_text, ingest_document -from agent_core.rag.retriever import retrieve - - -def test_run_agent_returns_structured_result_from_llm_output(): - scenario = { - "id": "knowledge_qa", - "name": "知识库问答助手", - "agent": { - "role": "知识库助手", - "goal": "基于资料回答问题", - "instructions": ["仅根据证据回答"], - }, - "rag": {"enabled": True, "collection": "knowledge_qa", "top_k": 3}, - "tools": ["generate_action_items"], - "output": {"type": "general_answer"}, - } - provider_response = """ - { - "answer": "请先隔离异常现场,再通知负责人。", - "confidence": "high", - "references": [ - {"source": "sop.md", "excerpt": "异常处理 SOP:先隔离现场"} - ] - } - """ - - class FakeProvider: - def generate(self, messages, response_format=None): - from agent_core.llm_provider import LLMResponse - - return LLMResponse( - content=provider_response, - model_name="demo-model", - success=True, - ) - - result = run_agent( - scenario, - "如何处理异常?", - options={"llm_provider": FakeProvider()}, - ) - - assert result.status == "success" - assert result.answer == "请先隔离异常现场,再通知负责人。" - assert result.structured_output["output_type"] == "general_answer" - assert result.structured_output["confidence"] == "high" - assert isinstance(result.references, list) - assert result.tool_calls[0]["tool_name"] == "generate_action_items" - assert result.model_name == "demo-model" - - -def test_run_agent_falls_back_when_llm_returns_non_json(): - scenario = { - "id": "document_review", - "name": "文档审核助手", - "agent": { - "role": "审核助手", - "goal": "总结审核意见", - "instructions": ["输出重点问题"], - }, - "rag": {"enabled": False}, - "tools": [], - "output": {"type": "document_review_report"}, - } - - class FakeProvider: - def generate(self, messages, response_format=None): - from agent_core.llm_provider import LLMResponse - - return LLMResponse( - content="这是非 JSON 的普通回答", - model_name="demo-model", - success=True, - ) - - result = run_agent( - scenario, - "请检查合同风险", - options={"llm_provider": FakeProvider()}, - ) - - assert result.status == "success" - assert result.answer == "这是非 JSON 的普通回答" - assert result.structured_output["output_type"] == "document_review_report" - assert result.structured_output["summary"] == "这是非 JSON 的普通回答" - assert result.structured_output["parse_mode"] == "fallback" - - -def test_build_messages_contains_role_goal_references_and_tool_results(): - scenario = { - "name": "质量异常分析助手", - "agent": { - "role": "质量管理专家", - "goal": "生成结构化质量分析报告", - "instructions": ["必须引用知识库", "缺失信息要说明"], - }, - "output": {"type": "quality_report"}, - } - - messages = build_messages( - scenario_config=scenario, - user_input="分析 A 线异常", - references=[{"source": "sop.md", "content": "先隔离现场"}], - tool_calls=[ - { - "tool_name": "query_demo_records", - "success": True, - "result": {"records": [{"title": "A线缺陷"}]}, - "error": "", - } - ], - ) - - assert messages[0]["role"] == "system" - assert "质量管理专家" in messages[0]["content"] - assert "生成结构化质量分析报告" in messages[0]["content"] - assert "quality_report" in messages[0]["content"] - assert "先隔离现场" in messages[1]["content"] - assert "A线缺陷" in messages[1]["content"] - assert "分析 A 线异常" in messages[2]["content"] - - -def test_rag_ingest_and_retrieve_filters_by_scenario_and_query(tmp_path): - store_path = tmp_path / "rag_store.json" - text = "设备点检需要先断电挂牌。质量异常需要记录批次、工位和缺陷现象。" - - result = ingest_document( - scenario_id="quality_analysis", - source_file="quality.md", - text=text, - collection="quality_analysis", - store_path=store_path, - ) - ingest_document( - scenario_id="risk_audit", - source_file="risk.md", - text="报销审核需要检查发票、金额和审批链。", - collection="risk_audit", - store_path=store_path, - ) - - chunks = retrieve( - scenario_id="quality_analysis", - query="质量异常批次", - collection="quality_analysis", - top_k=3, - store_path=store_path, - ) - - assert result.success is True - assert result.chunks_count >= 1 - assert chunks - assert chunks[0]["source"] == "quality.md" - assert "质量异常" in chunks[0]["content"] - assert all(chunk["scenario_id"] == "quality_analysis" for chunk in chunks) - - -def test_rag_reingest_replaces_same_document_and_retrieve_filters_document_ids(tmp_path): - store_path = tmp_path / "rag_store.json" - - ingest_document( - document_id=1, - scenario_id="knowledge_qa", - source_file="old.md", - text="旧制度要求人工登记。", - collection="knowledge_qa", - store_path=store_path, - ) - ingest_document( - document_id=1, - scenario_id="knowledge_qa", - source_file="new.md", - text="新制度要求系统自动登记。", - collection="knowledge_qa", - store_path=store_path, - ) - ingest_document( - document_id=2, - scenario_id="knowledge_qa", - source_file="other.md", - text="系统自动登记后需要生成审计记录。", - collection="knowledge_qa", - store_path=store_path, - ) - - chunks = retrieve( - scenario_id="knowledge_qa", - query="系统自动登记", - collection="knowledge_qa", - top_k=5, - document_ids=[1], - store_path=store_path, - ) - - assert chunks - assert {chunk["document_id"] for chunk in chunks} == {1} - assert all(chunk["source"] == "new.md" for chunk in chunks) - assert all("旧制度" not in chunk["content"] for chunk in chunks) - - -def test_run_agent_uses_retrieved_document_chunks(tmp_path): - store_path = tmp_path / "rag_store.json" - ingest_document( - scenario_id="knowledge_qa", - source_file="sop.md", - text="异常处理 SOP:先隔离现场,再通知负责人。", - collection="knowledge_qa", - store_path=store_path, - ) - scenario = { - "id": "knowledge_qa", - "name": "知识库问答助手", - "rag": {"enabled": True, "collection": "knowledge_qa", "top_k": 3}, - "tools": [], - "output": {"type": "general_answer"}, - } - - result = run_agent(scenario, "异常处理怎么做?", options={"rag_store_path": store_path}) - - assert result.references[0]["source"] == "sop.md" - assert "隔离现场" in result.references[0]["content"] - - -def test_rag_split_text_keeps_overlap_and_non_empty_chunks(): - chunks = _split_text("A" * 20, chunk_size=8, overlap=3) - - assert chunks == ["AAAAAAAA", "AAAAAAAA", "AAAAAAAA", "AAAAA"] - - -def test_retrieve_returns_empty_when_query_has_no_overlap(tmp_path): - store_path = tmp_path / "rag_store.json" - ingest_document( - scenario_id="knowledge_qa", - source_file="rules.md", - text="这里描述的是报销流程和审批链。", - collection="knowledge_qa", - store_path=store_path, - ) - - chunks = retrieve( - scenario_id="knowledge_qa", - query="设备点检", - collection="knowledge_qa", - top_k=3, - store_path=store_path, - ) - - assert chunks == [] diff --git a/tests/test_audit.py b/tests/test_audit.py deleted file mode 100644 index fc83ee1..0000000 --- a/tests/test_audit.py +++ /dev/null @@ -1,119 +0,0 @@ -from django.urls import reverse - -from agent_core.results import AgentResult -from apps.audit.models import AgentAuditLog, DemoBusinessRecord -from apps.audit.services import create_audit_log -from agent_core.tools.builtin_tools import query_demo_records - - -def test_create_audit_log_records_success_result(db): - result = AgentResult(answer="回答", structured_output={"x": 1}, status="success") - - log = create_audit_log("knowledge_qa", "知识库问答助手", "问题", result) - - assert AgentAuditLog.objects.count() == 1 - assert log.final_answer == "回答" - assert log.structured_output == {"x": 1} - assert log.status == "success" - - -def test_audit_list_page_shows_log(client, db): - result = AgentResult(answer="回答", status="success") - create_audit_log("knowledge_qa", "知识库问答助手", "问题", result) - - response = client.get(reverse("audit:list")) - - assert response.status_code == 200 - assert "知识库问答助手" in response.content.decode("utf-8") - - -def test_audit_list_can_filter_by_scenario(client, db): - create_audit_log( - "knowledge_qa", - "知识库问答助手", - "制度问题", - AgentResult(answer="回答一", status="success"), - ) - create_audit_log( - "quality_analysis", - "质量异常分析助手", - "质量问题", - AgentResult(answer="回答二", status="success"), - ) - - response = client.get(reverse("audit:list"), {"scenario_id": "knowledge_qa"}) - - content = response.content.decode("utf-8") - assert response.status_code == 200 - assert "知识库问答助手" in content - assert "质量异常分析助手" not in content - - -def test_audit_list_page_shows_user_input_summary(client, db): - create_audit_log( - "knowledge_qa", - "知识库问答助手", - "这是一个比较长的用户输入,用于确认列表页会展示输入摘要。", - AgentResult(answer="回答", status="success"), - ) - - response = client.get(reverse("audit:list")) - - assert "这是一个比较长的用户输入" in response.content.decode("utf-8") - - -def test_audit_detail_page_shows_raw_output(client, db): - result = AgentResult( - answer="结构化回答", - raw_output='{"answer":"结构化回答","confidence":"high"}', - status="success", - ) - log = create_audit_log("knowledge_qa", "知识库问答助手", "问题", result) - - response = client.get(reverse("audit:detail", args=[log.id])) - - content = response.content.decode("utf-8") - assert response.status_code == 200 - assert "原始输出" in content - assert "confidence" in content - assert "high" in content - - -def test_create_audit_log_masks_api_keys_from_error_message(db): - result = AgentResult( - answer="", - status="failed", - error="LLM_API_KEY=sk-secret-value 调用失败", - ) - - log = create_audit_log("knowledge_qa", "知识库问答助手", "问题", result) - - assert "sk-secret-value" not in log.error_message - assert "sk-***" in log.error_message - - -def test_create_audit_log_masks_embedding_api_keys_from_error_message(db): - result = AgentResult( - answer="", - status="failed", - error="EMBEDDING_API_KEY=embed-secret 调用失败", - ) - - log = create_audit_log("knowledge_qa", "知识库问答助手", "问题", result) - - assert "embed-secret" not in log.error_message - assert "EMBEDDING_API_KEY=***" in log.error_message - - -def test_query_demo_records_reads_demo_business_record_table(db): - DemoBusinessRecord.objects.create( - scenario_id="quality_analysis", - record_type="defect", - title="A线缺陷", - payload={"rate": 0.12}, - ) - - result = query_demo_records(user_input="quality_analysis defect") - - assert result["records"][0]["title"] == "A线缺陷" - assert result["records"][0]["payload"] == {"rate": 0.12} diff --git a/tests/test_chat.py b/tests/test_chat.py deleted file mode 100644 index 7bae4cc..0000000 --- a/tests/test_chat.py +++ /dev/null @@ -1,107 +0,0 @@ -from django.urls import reverse - -from agent_core.results import AgentResult -from apps.audit.models import AgentAuditLog -from apps.documents.models import UploadedDocument - - -def test_chat_post_returns_agent_result_and_audit_log(client, db): - response = client.post( - reverse("chat:index", args=["knowledge_qa"]), - {"message": "如何处理异常?"}, - ) - - assert response.status_code == 200 - content = response.content.decode("utf-8") - assert "mock-model" in content - assert "模拟回答" in content - assert AgentAuditLog.objects.count() == 1 - - -def test_chat_rejects_empty_message(client, db): - response = client.post(reverse("chat:index", args=["knowledge_qa"]), {"message": ""}) - - assert response.status_code == 200 - assert AgentAuditLog.objects.count() == 0 - assert "请输入要咨询的问题" in response.content.decode("utf-8") - - -def test_chat_passes_selected_document_ids_to_agent_core(client, db, monkeypatch): - selected = UploadedDocument.objects.create( - scenario_id="knowledge_qa", - original_name="selected.md", - file_type="md", - size=1, - status=UploadedDocument.STATUS_INDEXED, - ) - other = UploadedDocument.objects.create( - scenario_id="knowledge_qa", - original_name="other.md", - file_type="md", - size=1, - status=UploadedDocument.STATUS_INDEXED, - ) - captured = {} - - def fake_run_agent(scenario_config, user_input, options=None): - captured["options"] = options or {} - from agent_core.results import AgentResult - - return AgentResult(answer="ok", status="success") - - monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent) - - response = client.post( - reverse("chat:index", args=["knowledge_qa"]), - {"message": "只查选中文档", "document_ids": [str(selected.id)]}, - ) - - assert response.status_code == 200 - assert captured["options"]["document_ids"] == [selected.id] - assert other.id not in captured["options"]["document_ids"] - - -def test_chat_renders_structured_output_references_and_tool_calls(client, db, monkeypatch): - def fake_run_agent(scenario_config, user_input, options=None): - return AgentResult( - answer="建议先隔离现场。", - structured_output={ - "output_type": "quality_report", - "summary": "发现异常批次需要立即处置。", - "risk_level": "high", - "suggested_actions": ["隔离现场", "通知负责人"], - }, - references=[ - { - "source": "sop.md", - "content": "异常处理 SOP:先隔离现场,再通知负责人。", - } - ], - tool_calls=[ - { - "tool_name": "query_demo_records", - "success": True, - "result": {"records": [{"title": "A线缺陷"}]}, - "error": "", - } - ], - model_name="mock-model", - status="success", - ) - - monkeypatch.setattr("apps.chat.views.run_agent", fake_run_agent) - - response = client.post( - reverse("chat:index", args=["quality_analysis"]), - {"message": "分析 A 线异常"}, - ) - - content = response.content.decode("utf-8") - assert response.status_code == 200 - assert "结构化结果" in content - assert "发现异常批次需要立即处置" in content - assert "引用片段" in content - assert "sop.md" in content - assert "工具调用" in content - assert "query_demo_records" in content - assert "查看本次审计日志" in content diff --git a/tests/test_documents.py b/tests/test_documents.py deleted file mode 100644 index 5af8267..0000000 --- a/tests/test_documents.py +++ /dev/null @@ -1,147 +0,0 @@ -from django.core.files.uploadedfile import SimpleUploadedFile -from django.urls import reverse - -from apps.documents.forms import DocumentUploadForm -from apps.documents.models import UploadedDocument -from apps.documents.services import extract_text, index_document - - -def test_upload_txt_document_creates_uploaded_record(client, db): - file = SimpleUploadedFile("rules.txt", "hello".encode("utf-8"), content_type="text/plain") - - response = client.post( - reverse("documents:upload"), - {"scenario_id": "knowledge_qa", "file": file}, - ) - - assert response.status_code == 302 - document = UploadedDocument.objects.get() - assert document.status == "uploaded" - assert document.file_type == "txt" - assert document.scenario_id == "knowledge_qa" - - -def test_upload_redirect_shows_success_message(client, db): - file = SimpleUploadedFile("notice.txt", "hello".encode("utf-8"), content_type="text/plain") - - response = client.post( - reverse("documents:upload"), - {"scenario_id": "knowledge_qa", "file": file}, - follow=True, - ) - - assert response.status_code == 200 - assert "文件已上传,可继续执行入库" in response.content.decode("utf-8") - - -def test_upload_accepts_pdf_and_docx_documents(client, db): - for filename, payload in [ - ("policy.pdf", b"%PDF-1.4\nplain policy text"), - ("contract.docx", b"fake-docx-body"), - ]: - file = SimpleUploadedFile(filename, payload) - - response = client.post( - reverse("documents:upload"), - {"scenario_id": "knowledge_qa", "file": file}, - ) - - assert response.status_code == 302 - - assert set(UploadedDocument.objects.values_list("file_type", flat=True)) == {"pdf", "docx"} - - -def test_index_document_updates_status_to_indexed(client, db): - document = UploadedDocument.objects.create( - scenario_id="knowledge_qa", - original_name="rules.md", - file="knowledge_qa/rules.md", - file_type="md", - size=5, - status="uploaded", - ) - document.file.save("rules.md", SimpleUploadedFile("rules.md", b"# rule").file) - - response = client.post(reverse("documents:index", args=[document.id])) - - assert response.status_code == 302 - document.refresh_from_db() - assert document.status == "indexed" - assert document.error_message == "" - - -def test_extract_text_supports_pdf_and_docx_plain_text_fallback(db): - pdf_document = UploadedDocument.objects.create( - scenario_id="knowledge_qa", - original_name="policy.pdf", - file_type="pdf", - size=10, - status="uploaded", - ) - pdf_document.file.save("policy.pdf", SimpleUploadedFile("policy.pdf", b"%PDF-1.4\nSafety policy")) - - docx_document = UploadedDocument.objects.create( - scenario_id="knowledge_qa", - original_name="contract.docx", - file_type="docx", - size=10, - status="uploaded", - ) - docx_document.file.save( - "contract.docx", - SimpleUploadedFile("contract.docx", b"Contract clause review"), - ) - - assert "Safety policy" in extract_text(pdf_document) - assert "Contract clause review" in extract_text(docx_document) - - -def test_document_upload_form_builds_scenario_choices(): - form = DocumentUploadForm() - - choice_values = [value for value, _label in form.fields["scenario_id"].choices] - - assert "knowledge_qa" in choice_values - assert "quality_analysis" in choice_values - - -def test_index_failure_message_is_visible_on_document_list(client, db, monkeypatch): - document = UploadedDocument.objects.create( - scenario_id="knowledge_qa", - original_name="broken.md", - file_type="md", - size=5, - status="uploaded", - ) - - def fake_index_document(target_document): - target_document.status = UploadedDocument.STATUS_FAILED - target_document.error_message = "模拟入库失败" - target_document.save(update_fields=["status", "error_message", "updated_at"]) - return target_document - - monkeypatch.setattr("apps.documents.views.index_document", fake_index_document) - - response = client.post(reverse("documents:index", args=[document.id]), follow=True) - - content = response.content.decode("utf-8") - assert response.status_code == 200 - assert "文档入库失败,请检查错误原因后重试" in content - assert "模拟入库失败" in content - - -def test_index_document_marks_failed_when_extracted_text_is_empty(db, monkeypatch): - document = UploadedDocument.objects.create( - scenario_id="knowledge_qa", - original_name="empty.md", - file_type="md", - size=0, - status="uploaded", - ) - - monkeypatch.setattr("apps.documents.services.extract_text", lambda target: " ") - - updated_document = index_document(document) - - assert updated_document.status == UploadedDocument.STATUS_FAILED - assert "文档内容为空" in updated_document.error_message diff --git a/tests/test_llm_provider.py b/tests/test_llm_provider.py deleted file mode 100644 index a3f69fd..0000000 --- a/tests/test_llm_provider.py +++ /dev/null @@ -1,126 +0,0 @@ -from agent_core.llm_provider import ( - EmbeddingConfigurationError, - LLMConfigurationError, - create_embedding_provider, - create_llm_provider, - get_runtime_llm_config, -) - - -def test_create_llm_provider_requires_api_key_for_openai_compatible(): - provider = create_llm_provider( - { - "LLM_API_KEY": "", - "LLM_BASE_URL": "https://api.openai.com/v1", - "LLM_MODEL": "gpt-4.1-mini", - "LLM_PROVIDER": "openai_compatible", - } - ) - - response = provider.generate([{"role": "user", "content": "hello"}]) - - assert response.success is False - assert isinstance(response.error, LLMConfigurationError) - assert "LLM_API_KEY" in str(response.error) - - -def test_mock_provider_returns_deterministic_content(): - provider = create_llm_provider({"LLM_PROVIDER": "mock", "LLM_MODEL": "demo-model"}) - - response = provider.generate([{"role": "user", "content": "hello"}]) - - assert response.success is True - assert response.model_name == "demo-model" - assert "hello" in response.content - - -def test_openai_compatible_provider_posts_chat_completion(monkeypatch): - captured = {} - - class FakeResponse: - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, traceback): - return False - - def read(self): - return b'{"choices":[{"message":{"content":"ok"}}],"model":"demo-model"}' - - def fake_urlopen(request, timeout): - captured["url"] = request.full_url - captured["headers"] = dict(request.header_items()) - captured["body"] = request.data.decode("utf-8") - return FakeResponse() - - monkeypatch.setattr("agent_core.llm_provider.urlopen", fake_urlopen) - provider = create_llm_provider( - { - "LLM_PROVIDER": "openai_compatible", - "LLM_API_KEY": "sk-test", - "LLM_BASE_URL": "https://api.siliconflow.cn/v1", - "LLM_MODEL": "demo-model", - } - ) - - response = provider.generate([{"role": "user", "content": "hello"}]) - - assert response.success is True - assert response.content == "ok" - assert captured["url"] == "https://api.siliconflow.cn/v1/chat/completions" - assert '"model": "demo-model"' in captured["body"] - assert captured["headers"]["Authorization"] == "Bearer sk-test" - - -def test_embedding_provider_uses_openai_compatible_embeddings(monkeypatch): - class FakeResponse: - def __enter__(self): - return self - - def __exit__(self, exc_type, exc, traceback): - return False - - def read(self): - return b'{"data":[{"embedding":[0.1,0.2]},{"embedding":[0.3,0.4]}]}' - - monkeypatch.setattr("agent_core.llm_provider.urlopen", lambda request, timeout: FakeResponse()) - provider = create_embedding_provider( - { - "EMBEDDING_API_KEY": "sk-test", - "EMBEDDING_BASE_URL": "https://api.siliconflow.cn/v1", - "EMBEDDING_MODEL": "demo-embedding", - } - ) - - assert provider.embed_texts(["a", "b"]) == [[0.1, 0.2], [0.3, 0.4]] - - -def test_embedding_provider_requires_api_key(): - provider = create_embedding_provider( - { - "EMBEDDING_API_KEY": "", - "EMBEDDING_BASE_URL": "https://api.siliconflow.cn/v1", - "EMBEDDING_MODEL": "demo-embedding", - } - ) - - try: - provider.embed_texts(["a"]) - except EmbeddingConfigurationError as exc: - assert "EMBEDDING_API_KEY" in str(exc) - else: - raise AssertionError("expected EmbeddingConfigurationError") - - -def test_get_runtime_llm_config_uses_environment_and_overrides(monkeypatch): - monkeypatch.setenv("LLM_PROVIDER", "mock") - monkeypatch.setenv("LLM_API_KEY", "sk-env") - monkeypatch.setenv("LLM_BASE_URL", "https://env.example/v1") - monkeypatch.setenv("LLM_MODEL", "env-model") - - config = get_runtime_llm_config({"LLM_MODEL": "override-model"}) - - assert config["LLM_PROVIDER"] == "mock" - assert config["LLM_API_KEY"] == "sk-env" - assert config["LLM_BASE_URL"] == "https://env.example/v1" - assert config["LLM_MODEL"] == "override-model" diff --git a/tests/test_project_configuration.py b/tests/test_project_configuration.py deleted file mode 100644 index 9fe8208..0000000 --- a/tests/test_project_configuration.py +++ /dev/null @@ -1,21 +0,0 @@ -import os - -from django.conf import settings -from django.urls import reverse - - -def test_core_settings_expose_documented_paths(): - assert settings.SCENARIO_CONFIG_DIR.name == "configs" - assert settings.CHROMA_PATH.name == "chroma" - assert settings.MEDIA_ROOT.name == "uploads" - assert settings.EMBEDDING_MODEL == os.environ.get( - "EMBEDDING_MODEL", - "text-embedding-3-small", - ) - assert settings.EMBEDDING_BASE_URL == settings.LLM_BASE_URL - assert settings.EMBEDDING_API_KEY == settings.LLM_API_KEY - - -def test_home_url_is_registered(client): - response = client.get(reverse("scenarios:index")) - assert response.status_code == 200 diff --git a/tests/test_scenarios.py b/tests/test_scenarios.py deleted file mode 100644 index 1438fb9..0000000 --- a/tests/test_scenarios.py +++ /dev/null @@ -1,129 +0,0 @@ -import pytest - -from apps.scenarios.services import ( - ScenarioNotFound, - get_scenario, - list_scenario_issues, - list_scenarios, -) - - -def test_list_scenarios_loads_five_configs(): - scenarios = list_scenarios() - assert [scenario["id"] for scenario in scenarios] == [ - "document_review", - "knowledge_qa", - "quality_analysis", - "risk_audit", - "ticket_assistant", - ] - - -def test_get_scenario_returns_full_agent_config(): - scenario = get_scenario("quality_analysis") - assert scenario["agent"]["role"] == "质量管理专家" - assert scenario["rag"]["enabled"] is True - assert scenario["output"]["type"] == "quality_report" - assert "质量异常分析" in scenario["applicable_questions"][0] - - -def test_get_scenario_raises_clear_error_for_missing_id(): - with pytest.raises(ScenarioNotFound, match="场景不存在"): - get_scenario("missing") - - -def test_home_page_shows_applicable_questions(client): - response = client.get("/") - - content = response.content.decode("utf-8") - assert response.status_code == 200 - assert "适用题型" in content - assert "SOP 问答" in content - - -def test_list_scenarios_skips_invalid_config_and_collects_issues(settings, tmp_path): - valid_file = tmp_path / "valid.yaml" - invalid_file = tmp_path / "invalid.yaml" - valid_file.write_text( - """ -id: demo_valid -name: 有效场景 -description: 用于测试 -agent: - role: 测试助手 - goal: 正常返回 - instructions: - - 输出结果 -rag: - enabled: false -tools: [] -output: - type: general_answer -audit: - enabled: true -""".strip(), - encoding="utf-8", - ) - invalid_file.write_text( - """ -id: broken -name: 非法场景 -description: 缺少 agent.goal -agent: - role: 测试助手 - instructions: - - 输出结果 -rag: - enabled: true -tools: [] -output: - type: general_answer -audit: - enabled: true -""".strip(), - encoding="utf-8", - ) - settings.SCENARIO_CONFIG_DIR = tmp_path - - scenarios = list_scenarios() - issues = list_scenario_issues() - - assert [scenario["id"] for scenario in scenarios] == ["demo_valid"] - assert len(issues) == 1 - assert issues[0]["file_name"] == "invalid.yaml" - assert "agent.goal" in issues[0]["message"] - - -def test_home_page_shows_invalid_scenario_issues_instead_of_500(client, settings, tmp_path): - valid_file = tmp_path / "valid.yaml" - invalid_file = tmp_path / "invalid.yaml" - valid_file.write_text( - """ -id: demo_valid -name: 有效场景 -description: 用于测试 -agent: - role: 测试助手 - goal: 正常返回 - instructions: - - 输出结果 -rag: - enabled: false -tools: [] -output: - type: general_answer -audit: - enabled: true -""".strip(), - encoding="utf-8", - ) - invalid_file.write_text("id: broken\nname: 缺失结构", encoding="utf-8") - settings.SCENARIO_CONFIG_DIR = tmp_path - - response = client.get("/") - - content = response.content.decode("utf-8") - assert response.status_code == 200 - assert "有效场景" in content - assert "配置异常" in content - assert "invalid.yaml" in content diff --git a/tests/test_tool_registry.py b/tests/test_tool_registry.py deleted file mode 100644 index 7acd436..0000000 --- a/tests/test_tool_registry.py +++ /dev/null @@ -1,54 +0,0 @@ -from agent_core.tool_registry import ToolRegistry, run_declared_tools -from agent_core.tools.builtin_tools import calculate_rate, check_required_fields - - -def test_tool_registry_register_get_and_run(): - registry = ToolRegistry() - - def hello_tool(user_input: str) -> dict: - return {"echo": user_input} - - registry.register("hello", hello_tool) - - assert registry.get("hello") is hello_tool - assert registry.run("hello", user_input="demo") == { - "tool_name": "hello", - "success": True, - "arguments": {"user_input": "demo"}, - "result": {"echo": "demo"}, - "error": "", - } - - -def test_tool_registry_returns_failed_result_for_missing_tool(): - registry = ToolRegistry() - - result = registry.run("missing", user_input="demo") - - assert result["tool_name"] == "missing" - assert result["success"] is False - assert result["error"] == "工具未注册" - - -def test_run_declared_tools_executes_multiple_tools_in_order(): - results = run_declared_tools(["generate_action_items", "missing_tool"], "请生成行动项") - - assert [item["tool_name"] for item in results] == ["generate_action_items", "missing_tool"] - assert results[0]["success"] is True - assert results[1]["success"] is False - - -def test_calculate_rate_extracts_fraction_like_numbers(): - result = calculate_rate("产线合格率,已完成 18 件,总数 24 件") - - assert result["success"] is True - assert result["numerator"] == 18.0 - assert result["denominator"] == 24.0 - assert result["rate"] == 0.75 - - -def test_check_required_fields_reports_missing_fields(): - result = check_required_fields("请检查必填项:合同编号、供应商、金额。当前只提供了合同编号和金额。") - - assert "供应商" in result["missing_fields"] - assert "合同编号" not in result["missing_fields"]