集成三个子图到主Agent架构 + 修复前后端字段不匹配问题
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Has been cancelled
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Has been cancelled
主要变更: 1. 创建 subgraph_tools.py - 将三个子图包装为 LangChain 工具 2. 更新 graph_tools.py - 删除旧工具,添加子图工具 3. 更新系统提示词 - 介绍三个子系统 + RAG 能力 4. 简化 backend.py - 删除独立子图 API 端点 5. 修复 service.py 字段名不匹配问题 - content -> token 6. 前端界面优化 - 移动子图测试到侧边栏、删除测试审核按钮 7. 添加 pyjwt 依赖到 requirements.txt 8. 更新 docker-compose.yml - 添加前端代码挂载
This commit is contained in:
@@ -9,14 +9,18 @@ def create_system_prompt(tools: list = None) -> ChatPromptTemplate:
|
||||
if tools:
|
||||
tool_descs = []
|
||||
for tool in tools:
|
||||
# 提取工具名称和描述的第一行
|
||||
name = getattr(tool, 'name', None) or getattr(tool, '__name__', 'unknown_tool')
|
||||
desc = (tool.description or "").split('\n')[0]
|
||||
tool_descs.append(f"- {name}: {desc}")
|
||||
tools_section = "\n".join(tool_descs)
|
||||
|
||||
system_template = (
|
||||
"你是一个个人生活助手和数据分析助手,请使用中文交流。\n\n"
|
||||
"你是一个智能助手,具有三个专业子系统和RAG检索能力,请使用中文交流。\n\n"
|
||||
"【核心功能】\n"
|
||||
"1. 📚 词典/翻译子系统 - 查询单词、翻译文本、提取术语、每日一词\n"
|
||||
"2. 📰 资讯分析子系统 - 查询新闻、分析URL、提取关键词、生成报告\n"
|
||||
"3. 📇 通讯录子系统 - 查询联系人、添加联系人、管理通讯录\n"
|
||||
"4. 🔍 RAG检索 - 从知识库中检索相关信息回答问题\n\n"
|
||||
"【用户背景信息】\n"
|
||||
"以下是对当前用户的已知信息和长期记忆,你必须优先采纳并在回答中体现:\n"
|
||||
"{memory_context}\n"
|
||||
@@ -34,4 +38,4 @@ def create_system_prompt(tools: list = None) -> ChatPromptTemplate:
|
||||
return ChatPromptTemplate.from_messages([
|
||||
("system", system_template),
|
||||
MessagesPlaceholder(variable_name="messages")
|
||||
])
|
||||
])
|
||||
|
||||
@@ -187,9 +187,9 @@ class AIAgentService:
|
||||
# 处理思考过程
|
||||
if reasoning_token:
|
||||
processed_event = {
|
||||
"type": "reasoning",
|
||||
"type": "llm_token",
|
||||
"node": node_name,
|
||||
"content": reasoning_token
|
||||
"reasoning_token": reasoning_token
|
||||
}
|
||||
# 处理工具调用
|
||||
elif hasattr(message_chunk, 'tool_calls') and message_chunk.tool_calls:
|
||||
@@ -215,7 +215,8 @@ class AIAgentService:
|
||||
processed_event = {
|
||||
"type": "llm_token",
|
||||
"node": node_name,
|
||||
"content": token_content
|
||||
"token": token_content, # ✅ 改为 token
|
||||
"reasoning_token": reasoning_token
|
||||
}
|
||||
|
||||
elif chunk_type == "updates":
|
||||
@@ -289,7 +290,7 @@ class AIAgentService:
|
||||
yield {
|
||||
"type": "llm_token",
|
||||
"node": "fast_path",
|
||||
"content": char
|
||||
"token": char # ✅ 改为 token
|
||||
}
|
||||
await asyncio.sleep(0.03)
|
||||
|
||||
@@ -303,7 +304,7 @@ class AIAgentService:
|
||||
yield {
|
||||
"type": "llm_token",
|
||||
"node": "fast_path",
|
||||
"content": char
|
||||
"token": char # ✅ 改为 token
|
||||
}
|
||||
await asyncio.sleep(0.03)
|
||||
|
||||
@@ -334,7 +335,7 @@ class AIAgentService:
|
||||
yield {
|
||||
"type": "llm_token",
|
||||
"node": "fast_path",
|
||||
"content": char
|
||||
"token": char # ✅ 改为 token
|
||||
}
|
||||
await asyncio.sleep(0.03)
|
||||
|
||||
@@ -348,7 +349,7 @@ class AIAgentService:
|
||||
yield {
|
||||
"type": "llm_token",
|
||||
"node": "fast_path",
|
||||
"content": char
|
||||
"token": char # ✅ 改为 token
|
||||
}
|
||||
await asyncio.sleep(0.03)
|
||||
|
||||
|
||||
@@ -17,24 +17,13 @@ from .formatter import (
|
||||
)
|
||||
|
||||
from .intent import (
|
||||
# 旧版 API(保持向后兼容)
|
||||
IntentType,
|
||||
Intent,
|
||||
Entity,
|
||||
IntentParser,
|
||||
RuleBasedIntentClassifier,
|
||||
RuleBasedEntityExtractor,
|
||||
IntentRegistry,
|
||||
create_default_intent_parser,
|
||||
# 新版 React 模式 API
|
||||
# React 模式 API
|
||||
ReasoningAction,
|
||||
RetrievalConfig,
|
||||
ReasoningResult,
|
||||
BaseIntentReasoner,
|
||||
RuleBasedReactReasoner,
|
||||
LLMReactReasoner,
|
||||
create_react_reasoner,
|
||||
ReactIntentReasoner,
|
||||
react_reason,
|
||||
react_reason_async,
|
||||
get_route_by_reasoning
|
||||
)
|
||||
|
||||
@@ -60,24 +49,13 @@ __all__ = [
|
||||
"TemplateManager",
|
||||
"OutputRenderer",
|
||||
"PresetTemplates",
|
||||
# intent - 旧版
|
||||
"IntentType",
|
||||
"Intent",
|
||||
"Entity",
|
||||
"IntentParser",
|
||||
"RuleBasedIntentClassifier",
|
||||
"RuleBasedEntityExtractor",
|
||||
"IntentRegistry",
|
||||
"create_default_intent_parser",
|
||||
# intent - 新版 React 模式
|
||||
# intent - React 模式
|
||||
"ReasoningAction",
|
||||
"RetrievalConfig",
|
||||
"ReasoningResult",
|
||||
"BaseIntentReasoner",
|
||||
"RuleBasedReactReasoner",
|
||||
"LLMReactReasoner",
|
||||
"create_react_reasoner",
|
||||
"ReactIntentReasoner",
|
||||
"react_reason",
|
||||
"react_reason_async",
|
||||
"get_route_by_reasoning",
|
||||
# human_review
|
||||
"ReviewStatus",
|
||||
|
||||
@@ -9,8 +9,6 @@ import random
|
||||
|
||||
# 公共工具
|
||||
from ..common import (
|
||||
IntentParser,
|
||||
RuleBasedIntentClassifier,
|
||||
MarkdownFormatter
|
||||
)
|
||||
|
||||
@@ -29,18 +27,9 @@ WORD_BOOK_DB: Dict[str, List[Dict]] = {} # user_id -> [word_entries]
|
||||
|
||||
def parse_intent(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
解析用户意图节点(使用公共工具)
|
||||
解析用户意图节点(使用规则匹配)
|
||||
确定用户想做什么操作
|
||||
"""
|
||||
# 使用公共意图解析器
|
||||
parser = IntentParser()
|
||||
|
||||
# 为词典子图添加特定规则
|
||||
if isinstance(parser.classifier, RuleBasedIntentClassifier):
|
||||
# 为了复用公共工具,我们在内部继续使用关键词匹配
|
||||
# 但可以通过扩展 IntentParser 来支持子图特定的意图
|
||||
pass
|
||||
|
||||
# 子图特定的意图解析
|
||||
query_lower = state.user_query.lower()
|
||||
|
||||
|
||||
@@ -350,235 +350,3 @@ if __name__ == "__main__":
|
||||
# 使用环境变量或默认端口 8079(避免与 llama.cpp 的 8081 端口冲突)
|
||||
port = int(BACKEND_PORT)
|
||||
uvicorn.run(app, host="0.0.0.0", port=port)
|
||||
|
||||
|
||||
# ==================== 子图专用 API 端点 ====================
|
||||
# 简化版本,直接调用各个子图,无需完整 agent_service
|
||||
# 注意:这些是独立测试用的简化端点,方便前端直接调用
|
||||
|
||||
@app.get("/subgraph/dictionary/{action}")
|
||||
async def dictionary_subgraph_api(
|
||||
action: str,
|
||||
query: str = "",
|
||||
user_id: str = "default"
|
||||
):
|
||||
"""词典子图简化 API"""
|
||||
from backend.app.agent_subgraphs.dictionary import (
|
||||
DictionaryState,
|
||||
DictionaryAction,
|
||||
parse_intent,
|
||||
format_result
|
||||
)
|
||||
from backend.app.agent_subgraphs.dictionary.nodes import (
|
||||
query_word, translate_text, extract_terms, get_daily_word
|
||||
)
|
||||
|
||||
# 创建初始状态
|
||||
state = DictionaryState(user_query=query, user_id=user_id)
|
||||
|
||||
# 处理 action
|
||||
try:
|
||||
if action == "query":
|
||||
state.action = DictionaryAction.QUERY
|
||||
state.action_params = {"word": query}
|
||||
state = query_word(state)
|
||||
elif action == "translate":
|
||||
state.action = DictionaryAction.TRANSLATE
|
||||
state.source_text = query
|
||||
state = translate_text(state)
|
||||
elif action == "daily":
|
||||
state.action = DictionaryAction.DAILY_WORD
|
||||
state = get_daily_word(state)
|
||||
elif action == "extract":
|
||||
state.action = DictionaryAction.EXTRACT
|
||||
state.action_params = {"text": query}
|
||||
state = extract_terms(state)
|
||||
else:
|
||||
# 自动解析意图
|
||||
state = parse_intent(state)
|
||||
# 根据解析后的 action 调用
|
||||
if state.action == DictionaryAction.QUERY:
|
||||
state = query_word(state)
|
||||
elif state.action == DictionaryAction.TRANSLATE:
|
||||
state = translate_text(state)
|
||||
elif state.action == DictionaryAction.DAILY_WORD:
|
||||
state = get_daily_word(state)
|
||||
elif state.action == DictionaryAction.EXTRACT:
|
||||
state = extract_terms(state)
|
||||
|
||||
# 格式化结果
|
||||
state = format_result(state)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"action": str(state.action),
|
||||
"result": state.final_result,
|
||||
"raw_data": {
|
||||
"word_entry": vars(state.word_entry) if state.word_entry else None,
|
||||
"translated_text": state.translated_text,
|
||||
"extracted_terms": [vars(t) for t in state.extracted_terms],
|
||||
"daily_word": vars(state.daily_word) if state.daily_word else None
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@app.get("/subgraph/news/{action}")
|
||||
async def news_subgraph_api(
|
||||
action: str,
|
||||
query: str = "",
|
||||
user_id: str = "default"
|
||||
):
|
||||
"""资讯子图简化 API"""
|
||||
from backend.app.agent_subgraphs.news_analysis import (
|
||||
NewsAnalysisState,
|
||||
NewsAction,
|
||||
parse_intent,
|
||||
format_result
|
||||
)
|
||||
from backend.app.agent_subgraphs.news_analysis.nodes import (
|
||||
query_news, analyze_url, extract_keywords, generate_report
|
||||
)
|
||||
|
||||
# 创建初始状态
|
||||
state = NewsAnalysisState(user_query=query, user_id=user_id)
|
||||
|
||||
# 处理 action
|
||||
try:
|
||||
if action == "query":
|
||||
state.action = NewsAction.QUERY_NEWS
|
||||
state = query_news(state)
|
||||
elif action == "analyze":
|
||||
state.action = NewsAction.ANALYZE_URL
|
||||
state.custom_urls = [query]
|
||||
state = analyze_url(state)
|
||||
elif action == "keywords":
|
||||
state.action = NewsAction.EXTRACT_KEYWORDS
|
||||
state = extract_keywords(state)
|
||||
elif action == "report":
|
||||
state.action = NewsAction.GENERATE_REPORT
|
||||
state = generate_report(state)
|
||||
else:
|
||||
# 自动解析意图
|
||||
state = parse_intent(state)
|
||||
# 根据解析后的 action 调用
|
||||
if state.action == NewsAction.QUERY_NEWS:
|
||||
state = query_news(state)
|
||||
elif state.action == NewsAction.ANALYZE_URL:
|
||||
state.custom_urls = [query]
|
||||
state = analyze_url(state)
|
||||
elif state.action == NewsAction.EXTRACT_KEYWORDS:
|
||||
state = extract_keywords(state)
|
||||
elif state.action == NewsAction.GENERATE_REPORT:
|
||||
state = generate_report(state)
|
||||
|
||||
# 格式化结果
|
||||
state = format_result(state)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"action": str(state.action),
|
||||
"result": state.final_result,
|
||||
"raw_data": {
|
||||
"news_items": [vars(item) for item in state.news_items],
|
||||
"extracted_keywords": state.extracted_keywords,
|
||||
"report_content": state.report_content
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@app.get("/subgraph/contact/{action}")
|
||||
async def contact_subgraph_api(
|
||||
action: str,
|
||||
query: str = "",
|
||||
user_id: str = "default"
|
||||
):
|
||||
"""通讯录子图简化 API"""
|
||||
from backend.app.agent_subgraphs.contact import (
|
||||
ContactState,
|
||||
ContactAction,
|
||||
parse_intent,
|
||||
format_result
|
||||
)
|
||||
from backend.app.agent_subgraphs.contact.nodes import (
|
||||
list_contacts, add_contact, list_emails, generate_email_draft, sniff_contacts
|
||||
)
|
||||
|
||||
# 创建初始状态
|
||||
state = ContactState(user_query=query, user_id=user_id)
|
||||
|
||||
# 处理 action
|
||||
try:
|
||||
if action == "list":
|
||||
state.action = ContactAction.CONTACT_LIST
|
||||
state = list_contacts(state)
|
||||
elif action == "add":
|
||||
state.action = ContactAction.CONTACT_ADD
|
||||
state = add_contact(state)
|
||||
elif action == "emails":
|
||||
state.action = ContactAction.EMAIL_LIST
|
||||
state = list_emails(state)
|
||||
elif action == "draft":
|
||||
state.action = ContactAction.EMAIL_SEND
|
||||
state = generate_email_draft(state)
|
||||
elif action == "sniff":
|
||||
state.action = ContactAction.SNIFF_CONTACTS
|
||||
state = sniff_contacts(state)
|
||||
else:
|
||||
# 自动解析意图
|
||||
state = parse_intent(state)
|
||||
# 根据解析后的 action 调用
|
||||
if state.action == ContactAction.CONTACT_LIST:
|
||||
state = list_contacts(state)
|
||||
elif state.action == ContactAction.CONTACT_ADD:
|
||||
state = add_contact(state)
|
||||
elif state.action == ContactAction.EMAIL_LIST:
|
||||
state = list_emails(state)
|
||||
elif state.action == ContactAction.EMAIL_SEND:
|
||||
state = generate_email_draft(state)
|
||||
elif state.action == ContactAction.SNIFF_CONTACTS:
|
||||
state = sniff_contacts(state)
|
||||
|
||||
# 格式化结果
|
||||
state = format_result(state)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"action": str(state.action),
|
||||
"result": state.final_result,
|
||||
"raw_data": {
|
||||
"contacts": [vars(c) for c in state.contacts],
|
||||
"emails": [vars(e) for e in state.emails],
|
||||
"current_contact": vars(state.current_contact) if state.current_contact else None,
|
||||
"draft": {
|
||||
"subject": state.draft_subject,
|
||||
"recipient": state.draft_recipient,
|
||||
"body": state.draft_body
|
||||
},
|
||||
"sniffed": [vars(c) for c in state.sniffed_contacts]
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
@app.get("/subgraph/help")
|
||||
async def subgraph_help_api():
|
||||
"""子图 API 使用帮助"""
|
||||
return {
|
||||
"dictionary": {
|
||||
"actions": ["query", "translate", "daily", "extract", "auto"],
|
||||
"endpoint": "/subgraph/dictionary/{action}"
|
||||
},
|
||||
"news": {
|
||||
"actions": ["query", "analyze", "keywords", "report", "auto"],
|
||||
"endpoint": "/subgraph/news/{action}"
|
||||
},
|
||||
"contact": {
|
||||
"actions": ["list", "add", "emails", "draft", "sniff", "auto"],
|
||||
"endpoint": "/subgraph/contact/{action}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,17 @@
|
||||
"""
|
||||
工具定义模块 - 纯函数工具,无依赖 AIAgent 类
|
||||
工具定义模块 - 子图工具 + RAG 工具
|
||||
Subgraph Tools + RAG Tools
|
||||
"""
|
||||
|
||||
# 标准库
|
||||
from pathlib import Path
|
||||
|
||||
# 第三方库
|
||||
import pandas as pd
|
||||
import pypdf
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from langchain_core.tools import tool
|
||||
|
||||
def _file_allow_check(filename: str) -> Path:
|
||||
"""检查用户文件名是否位于允许目录 './user_docs' 下,防止路径遍历攻击。"""
|
||||
allowed_dir = Path("./user_docs").resolve()
|
||||
allowed_dir.mkdir(exist_ok=True)
|
||||
|
||||
file_path = (allowed_dir / filename).resolve()
|
||||
if not str(file_path).startswith(str(allowed_dir)):
|
||||
raise ValueError("错误:非法文件路径。")
|
||||
|
||||
if not file_path.exists():
|
||||
raise FileNotFoundError(f"错误:文件 '{filename}' 不存在。")
|
||||
|
||||
return file_path
|
||||
|
||||
@tool
|
||||
def get_current_temperature(location: str) -> str:
|
||||
"""获取指定地点的当前温度。"""
|
||||
return f'当前{location}的温度为25℃'
|
||||
|
||||
@tool
|
||||
def read_local_file(filename: str) -> str:
|
||||
"""读取用户指定名称的本地文本文件内容并返回摘要。"""
|
||||
try:
|
||||
file_path = _file_allow_check(filename)
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
return f"文件 '{filename}' 的内容开头:\n{content[:1000]}..."
|
||||
except Exception as e:
|
||||
return f"读取文件时出错:{str(e)}"
|
||||
|
||||
@tool
|
||||
def read_pdf_summary(filename: str) -> str:
|
||||
"""读取PDF文件并返回内容文本摘要。"""
|
||||
try:
|
||||
file_path = _file_allow_check(filename)
|
||||
text = ""
|
||||
with open(file_path, 'rb') as f:
|
||||
reader = pypdf.PdfReader(f)
|
||||
for page in reader.pages[:3]:
|
||||
text += page.extract_text()
|
||||
return f"PDF文件 '{filename}' 的前几页内容:\n{text[:2000]}..."
|
||||
except Exception as e:
|
||||
return f"读取PDF出错:{e}"
|
||||
|
||||
@tool
|
||||
def read_excel_as_markdown(filename: str) -> str:
|
||||
"""读取Excel文件,并将其主要数据转换为Markdown表格格式。"""
|
||||
try:
|
||||
file_path = _file_allow_check(filename)
|
||||
df = pd.read_excel(file_path)
|
||||
markdown_table = df.head(10).to_markdown(index=False)
|
||||
return f"Excel文件 '{filename}' 的数据预览(前10行):\n{markdown_table}"
|
||||
except Exception as e:
|
||||
return f"读取Excel出错:{e}"
|
||||
|
||||
@tool
|
||||
def fetch_webpage_content(url: str) -> str:
|
||||
"""抓取给定URL的网页正文内容,并返回清晰的纯文本。"""
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, 'html.parser')
|
||||
for script in soup(["script", "style"]):
|
||||
script.decompose()
|
||||
text = soup.get_text()
|
||||
lines = (line.strip() for line in text.splitlines())
|
||||
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
||||
text = '\n'.join(chunk for chunk in chunks if chunk)
|
||||
return f"成功抓取网页 {url},正文内容开头:\n{text[:1500]}..."
|
||||
except Exception as e:
|
||||
return f"抓取网页时出错:{str(e)}"
|
||||
# 子图工具
|
||||
from .subgraph_tools import (
|
||||
SUBGRAPH_TOOLS,
|
||||
SUBGRAPH_TOOLS_BY_NAME,
|
||||
dictionary_tool,
|
||||
news_analysis_tool,
|
||||
contact_tool
|
||||
)
|
||||
|
||||
# 工具列表和映射(全局常量)
|
||||
AVAILABLE_TOOLS = [
|
||||
get_current_temperature,
|
||||
read_local_file,
|
||||
fetch_webpage_content,
|
||||
read_pdf_summary,
|
||||
read_excel_as_markdown
|
||||
]
|
||||
TOOLS_BY_NAME = {tool.name: tool for tool in AVAILABLE_TOOLS}
|
||||
AVAILABLE_TOOLS = SUBGRAPH_TOOLS.copy()
|
||||
TOOLS_BY_NAME = SUBGRAPH_TOOLS_BY_NAME.copy()
|
||||
|
||||
193
backend/app/graph/subgraph_tools.py
Normal file
193
backend/app/graph/subgraph_tools.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
子图工具模块 - 将三个子图包装成 LangChain 工具
|
||||
Subgraph Tools Module - Wrap three subgraphs as LangChain tools
|
||||
"""
|
||||
|
||||
from langchain_core.tools import tool
|
||||
from typing import Optional
|
||||
|
||||
|
||||
# ============== 词典子图工具 ==============
|
||||
@tool
|
||||
def dictionary_tool(query: str, action: Optional[str] = None) -> str:
|
||||
"""
|
||||
词典/翻译工具 - 查询单词、翻译文本、提取术语、获取每日一词
|
||||
|
||||
Args:
|
||||
query: 用户查询内容(单词、句子、文本等)
|
||||
action: 可选,指定操作类型("query" 查询单词,"translate" 翻译,
|
||||
"extract" 提取术语,"daily" 每日一词,不指定则自动识别)
|
||||
|
||||
Returns:
|
||||
格式化的结果文本
|
||||
"""
|
||||
try:
|
||||
from backend.app.agent_subgraphs.dictionary import (
|
||||
DictionaryState,
|
||||
DictionaryAction,
|
||||
parse_intent,
|
||||
format_result
|
||||
)
|
||||
from backend.app.agent_subgraphs.dictionary.nodes import (
|
||||
query_word, translate_text, extract_terms, get_daily_word
|
||||
)
|
||||
|
||||
# 创建初始状态
|
||||
state = DictionaryState(user_query=query, user_id="default")
|
||||
|
||||
# 处理 action
|
||||
if action == "query":
|
||||
state.action = DictionaryAction.QUERY
|
||||
state.action_params = {"word": query}
|
||||
state = query_word(state)
|
||||
elif action == "translate":
|
||||
state.action = DictionaryAction.TRANSLATE
|
||||
state.source_text = query
|
||||
state = translate_text(state)
|
||||
elif action == "daily":
|
||||
state.action = DictionaryAction.DAILY_WORD
|
||||
state = get_daily_word(state)
|
||||
elif action == "extract":
|
||||
state.action = DictionaryAction.EXTRACT
|
||||
state.action_params = {"text": query}
|
||||
state = extract_terms(state)
|
||||
else:
|
||||
# 自动解析意图
|
||||
state = parse_intent(state)
|
||||
if state.action == DictionaryAction.QUERY:
|
||||
state = query_word(state)
|
||||
elif state.action == DictionaryAction.TRANSLATE:
|
||||
state = translate_text(state)
|
||||
elif state.action == DictionaryAction.DAILY_WORD:
|
||||
state = get_daily_word(state)
|
||||
elif state.action == DictionaryAction.EXTRACT:
|
||||
state = extract_terms(state)
|
||||
|
||||
# 格式化结果
|
||||
state = format_result(state)
|
||||
|
||||
return state.final_result or "操作完成"
|
||||
|
||||
except Exception as e:
|
||||
return f"词典工具执行出错:{str(e)}"
|
||||
|
||||
|
||||
# ============== 资讯分析子图工具 ==============
|
||||
@tool
|
||||
def news_analysis_tool(query: str, action: Optional[str] = None) -> str:
|
||||
"""
|
||||
资讯分析工具 - 查询新闻、分析URL、提取关键词、生成报告
|
||||
|
||||
Args:
|
||||
query: 用户查询内容(关键词、URL、文本等)
|
||||
action: 可选,指定操作类型("query" 查询新闻,"analyze" 分析URL,
|
||||
"keywords" 提取关键词,"report" 生成报告,不指定则自动识别)
|
||||
|
||||
Returns:
|
||||
格式化的结果文本
|
||||
"""
|
||||
try:
|
||||
from backend.app.agent_subgraphs.news_analysis import (
|
||||
NewsAnalysisState,
|
||||
NewsAction,
|
||||
parse_intent,
|
||||
format_result
|
||||
)
|
||||
from backend.app.agent_subgraphs.news_analysis.nodes import (
|
||||
query_news, analyze_url, extract_keywords, generate_report
|
||||
)
|
||||
|
||||
# 创建初始状态
|
||||
state = NewsAnalysisState(user_query=query, user_id="default")
|
||||
|
||||
# 处理 action
|
||||
if action == "query":
|
||||
state.action = NewsAction.QUERY_NEWS
|
||||
state = query_news(state)
|
||||
elif action == "analyze":
|
||||
state.action = NewsAction.ANALYZE_URL
|
||||
state.custom_urls = [query]
|
||||
state = analyze_url(state)
|
||||
elif action == "keywords":
|
||||
state.action = NewsAction.EXTRACT_KEYWORDS
|
||||
state = extract_keywords(state)
|
||||
elif action == "report":
|
||||
state.action = NewsAction.GENERATE_REPORT
|
||||
state = generate_report(state)
|
||||
else:
|
||||
# 自动解析意图
|
||||
state = parse_intent(state)
|
||||
if state.action == NewsAction.QUERY_NEWS:
|
||||
state = query_news(state)
|
||||
elif state.action == NewsAction.ANALYZE_URL:
|
||||
state.custom_urls = [query]
|
||||
state = analyze_url(state)
|
||||
elif state.action == NewsAction.EXTRACT_KEYWORDS:
|
||||
state = extract_keywords(state)
|
||||
elif state.action == NewsAction.GENERATE_REPORT:
|
||||
state = generate_report(state)
|
||||
|
||||
# 格式化结果
|
||||
state = format_result(state)
|
||||
|
||||
return state.final_result or "操作完成"
|
||||
|
||||
except Exception as e:
|
||||
return f"资讯分析工具执行出错:{str(e)}"
|
||||
|
||||
|
||||
# ============== 通讯录子图工具 ==============
|
||||
@tool
|
||||
def contact_tool(query: str, action: Optional[str] = None) -> str:
|
||||
"""
|
||||
通讯录工具 - 查询联系人、添加联系人、管理通讯录
|
||||
|
||||
Args:
|
||||
query: 用户查询内容(姓名、电话、信息等)
|
||||
action: 可选,指定操作类型(不指定则自动识别)
|
||||
|
||||
Returns:
|
||||
格式化的结果文本
|
||||
"""
|
||||
try:
|
||||
from backend.app.agent_subgraphs.contact import (
|
||||
ContactState,
|
||||
ContactAction,
|
||||
parse_intent,
|
||||
format_result
|
||||
)
|
||||
from backend.app.agent_subgraphs.contact.nodes import (
|
||||
query_contact, add_contact, list_contacts
|
||||
)
|
||||
|
||||
# 创建初始状态
|
||||
state = ContactState(user_query=query, user_id="default")
|
||||
|
||||
# 自动解析意图
|
||||
state = parse_intent(state)
|
||||
|
||||
# 根据解析结果执行操作
|
||||
if state.action == ContactAction.QUERY:
|
||||
state = query_contact(state)
|
||||
elif state.action == ContactAction.ADD:
|
||||
state = add_contact(state)
|
||||
elif state.action == ContactAction.LIST:
|
||||
state = list_contacts(state)
|
||||
|
||||
# 格式化结果
|
||||
state = format_result(state)
|
||||
|
||||
return state.final_result or "操作完成"
|
||||
|
||||
except Exception as e:
|
||||
return f"通讯录工具执行出错:{str(e)}"
|
||||
|
||||
|
||||
# ============== 工具列表 ==============
|
||||
SUBGRAPH_TOOLS = [
|
||||
dictionary_tool,
|
||||
news_analysis_tool,
|
||||
contact_tool
|
||||
]
|
||||
|
||||
SUBGRAPH_TOOLS_BY_NAME = {tool.name: tool for tool in SUBGRAPH_TOOLS}
|
||||
@@ -38,7 +38,7 @@ from .retriever import (
|
||||
create_base_retriever,
|
||||
create_hybrid_retriever,
|
||||
)
|
||||
from .reranker import LLaMaCPPReranker
|
||||
from .rerank import DocumentReranker, create_document_reranker
|
||||
from .query_transform import MultiQueryGenerator
|
||||
from .fusion import reciprocal_rank_fusion
|
||||
from .pipeline import RAGPipeline
|
||||
@@ -51,7 +51,8 @@ __all__ = [
|
||||
"create_hybrid_retriever",
|
||||
|
||||
# 重排序器
|
||||
"LLaMaCPPReranker",
|
||||
"DocumentReranker",
|
||||
"create_document_reranker",
|
||||
|
||||
# 查询改写生成器
|
||||
"MultiQueryGenerator",
|
||||
|
||||
@@ -36,6 +36,7 @@ tenacity==9.1.4
|
||||
rich==15.0.0
|
||||
PyYAML==6.0.3
|
||||
numpy>=1.26.2
|
||||
pyjwt==2.10.1
|
||||
|
||||
# Document Processing
|
||||
unstructured==0.22.21
|
||||
|
||||
@@ -87,6 +87,8 @@ services:
|
||||
environment:
|
||||
# Docker 内部网络使用服务名 'backend' 解析后端服务
|
||||
- API_URL=http://backend:8079/chat
|
||||
volumes:
|
||||
- ../frontend/src:/app/src # 挂载源代码目录,修改立即生效
|
||||
ports:
|
||||
- "8501:8501"
|
||||
networks:
|
||||
|
||||
@@ -393,32 +393,6 @@ def _render_review_confirmation():
|
||||
except Exception as e:
|
||||
st.session_state.review_error = str(e)
|
||||
|
||||
# 测试按钮 - 用于演示审核功能
|
||||
with st.container():
|
||||
st.markdown("<div style='height: 20px;'></div>", unsafe_allow_html=True)
|
||||
col_test, col_info = st.columns([1, 3])
|
||||
with col_test:
|
||||
if st.button("🔧 测试审核", key="test_review_chat"):
|
||||
# 创建一个测试审核请求
|
||||
test_content = "这是一条待审核的测试内容。\n\n您可以选择:\n✅ 确定 - 批准此内容\n✏️ 修改 - 修改后批准\n❌ 拒绝 - 拒绝此内容"
|
||||
review_id = api_client.request_review(thread_id, user_id, test_content)
|
||||
if review_id:
|
||||
st.session_state.pending_review = {
|
||||
"review_id": review_id,
|
||||
"content_to_review": test_content,
|
||||
"created_at": "2024-01-01T12:00:00",
|
||||
"user_id": user_id
|
||||
}
|
||||
st.success("✅ 已创建测试审核")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("❌ 创建测试审核失败")
|
||||
with col_info:
|
||||
if st.session_state.get("review_error"):
|
||||
st.warning(f"⚠️ {st.session_state.review_error}")
|
||||
elif st.session_state.pending_review:
|
||||
st.info("📋 有待审核内容")
|
||||
|
||||
# 显示审核确认界面
|
||||
if st.session_state.pending_review:
|
||||
review = st.session_state.pending_review
|
||||
|
||||
@@ -21,6 +21,17 @@ def render_sidebar():
|
||||
# 底部放用户部分
|
||||
st.divider()
|
||||
_render_user_section()
|
||||
|
||||
# 子图测试面板:放在最底部的角落里
|
||||
st.divider()
|
||||
with st.expander("🔧 测试工具", expanded=False):
|
||||
from components.subgraph_panel import _render_dictionary_panel, _render_news_panel, _render_contact_panel
|
||||
st.caption("📚 词典子图")
|
||||
_render_dictionary_panel()
|
||||
st.caption("📰 资讯子图")
|
||||
_render_news_panel()
|
||||
st.caption("📇 通讯录子图")
|
||||
_render_contact_panel()
|
||||
|
||||
def _render_user_section():
|
||||
"""渲染用户登录区域"""
|
||||
|
||||
@@ -19,14 +19,12 @@ if __name__ == '__main__':
|
||||
from components.sidebar import render_sidebar
|
||||
from components.chat_area import render_chat_area
|
||||
from components.info_panel import render_info_panel
|
||||
from components.subgraph_panel import render_subgraph_panel
|
||||
else:
|
||||
from .config import config
|
||||
from .state import AppState
|
||||
from .components.sidebar import render_sidebar
|
||||
from .components.chat_area import render_chat_area
|
||||
from .components.info_panel import render_info_panel
|
||||
from .components.subgraph_panel import render_subgraph_panel
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -129,10 +127,6 @@ def main():
|
||||
|
||||
# 中间主区域:全宽的聊天区域
|
||||
render_chat_area()
|
||||
|
||||
# 底部:子图测试面板
|
||||
st.divider()
|
||||
render_subgraph_panel()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user