集成三个子图到主Agent架构 + 修复前后端字段不匹配问题
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:
2026-04-27 15:23:50 +08:00
parent 26f872f975
commit 048f57a89f
13 changed files with 244 additions and 406 deletions

View File

@@ -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()

View 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}