feat: 完善词典子图,添加API调用和前端格式化工具
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Failing after 6m5s
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Failing after 6m5s
- 完善词典子图:添加生词本功能 - 创建API调用工具:dictionary_api - 添加前端格式化展示工具:result_formatter.py - 创建通讯录和资讯子图的基本结构 - 更新主图状态结构,添加MainGraphState - 添加subgraph_builder.py用于子图集成
This commit is contained in:
47
backend/app/agent_subgraphs/contact/__init__.py
Normal file
47
backend/app/agent_subgraphs/contact/__init__.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""
|
||||
通讯录子图
|
||||
Contact Subgraph Module
|
||||
"""
|
||||
|
||||
from .state import (
|
||||
ContactState,
|
||||
Contact,
|
||||
Email,
|
||||
ContactAction
|
||||
)
|
||||
from .graph import build_contact_subgraph
|
||||
from .nodes import (
|
||||
parse_intent,
|
||||
list_contacts,
|
||||
add_contact,
|
||||
list_emails,
|
||||
generate_email_draft,
|
||||
human_review,
|
||||
send_email,
|
||||
sniff_contacts,
|
||||
format_result,
|
||||
should_continue
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# State
|
||||
"ContactState",
|
||||
"Contact",
|
||||
"Email",
|
||||
"ContactAction",
|
||||
|
||||
# Graph
|
||||
"build_contact_subgraph",
|
||||
|
||||
# Nodes
|
||||
"parse_intent",
|
||||
"list_contacts",
|
||||
"add_contact",
|
||||
"list_emails",
|
||||
"generate_email_draft",
|
||||
"human_review",
|
||||
"send_email",
|
||||
"sniff_contacts",
|
||||
"format_result",
|
||||
"should_continue"
|
||||
]
|
||||
86
backend/app/agent_subgraphs/contact/graph.py
Normal file
86
backend/app/agent_subgraphs/contact/graph.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""
|
||||
通讯录子图构建器
|
||||
Contact Subgraph Builder
|
||||
"""
|
||||
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
from .state import ContactState
|
||||
from .nodes import (
|
||||
parse_intent,
|
||||
list_contacts,
|
||||
add_contact,
|
||||
list_emails,
|
||||
generate_email_draft,
|
||||
human_review,
|
||||
send_email,
|
||||
sniff_contacts,
|
||||
format_result,
|
||||
should_continue
|
||||
)
|
||||
|
||||
|
||||
def build_contact_subgraph() -> StateGraph:
|
||||
"""
|
||||
构建通讯录子图
|
||||
|
||||
Returns:
|
||||
配置好的 StateGraph
|
||||
"""
|
||||
# 创建图
|
||||
graph = StateGraph(ContactState)
|
||||
|
||||
# 添加节点
|
||||
graph.add_node("parse_intent", parse_intent)
|
||||
graph.add_node("list_contacts", list_contacts)
|
||||
graph.add_node("add_contact", add_contact)
|
||||
graph.add_node("list_emails", list_emails)
|
||||
graph.add_node("generate_email_draft", generate_email_draft)
|
||||
graph.add_node("human_review", human_review)
|
||||
graph.add_node("send_email", send_email)
|
||||
graph.add_node("sniff_contacts", sniff_contacts)
|
||||
graph.add_node("format_result", format_result)
|
||||
|
||||
# 添加边
|
||||
# 从START开始
|
||||
graph.add_edge(START, "parse_intent")
|
||||
|
||||
# 从parse_intent根据条件路由
|
||||
graph.add_conditional_edges(
|
||||
"parse_intent",
|
||||
should_continue,
|
||||
{
|
||||
"list_contacts": "list_contacts",
|
||||
"add_contact": "add_contact",
|
||||
"list_emails": "list_emails",
|
||||
"generate_email_draft": "generate_email_draft",
|
||||
"sniff_contacts": "sniff_contacts",
|
||||
}
|
||||
)
|
||||
|
||||
# 从各个操作节点到format_result
|
||||
graph.add_edge("list_contacts", "format_result")
|
||||
graph.add_edge("add_contact", "format_result")
|
||||
graph.add_edge("list_emails", "format_result")
|
||||
graph.add_edge("sniff_contacts", "format_result")
|
||||
|
||||
# 邮件发送的特殊流程
|
||||
graph.add_edge("generate_email_draft", "human_review")
|
||||
|
||||
# 从human_review根据条件路由
|
||||
graph.add_conditional_edges(
|
||||
"human_review",
|
||||
should_continue,
|
||||
{
|
||||
"send_email": "send_email",
|
||||
"format_result": "format_result",
|
||||
}
|
||||
)
|
||||
|
||||
# 发送邮件后到格式化
|
||||
graph.add_edge("send_email", "format_result")
|
||||
|
||||
# 最终到END
|
||||
graph.add_edge("format_result", END)
|
||||
|
||||
return graph
|
||||
217
backend/app/agent_subgraphs/contact/nodes.py
Normal file
217
backend/app/agent_subgraphs/contact/nodes.py
Normal file
@@ -0,0 +1,217 @@
|
||||
"""
|
||||
通讯录子图节点
|
||||
Contact Subgraph Nodes
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from .state import ContactState, ContactAction, Contact, Email
|
||||
|
||||
|
||||
def parse_intent(state: ContactState) -> ContactState:
|
||||
"""
|
||||
解析用户意图节点
|
||||
确定用户想做什么操作
|
||||
"""
|
||||
state.current_phase = "intent_parsing"
|
||||
|
||||
query_lower = state.user_query.lower()
|
||||
|
||||
# 简单的关键词匹配(真实场景应该用LLM)
|
||||
if any(keyword in query_lower for keyword in ["联系人", "contact", "list"]):
|
||||
state.action = ContactAction.CONTACT_LIST
|
||||
state.action_params = {"query": state.user_query}
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["添加", "add", "新建", "save"]):
|
||||
state.action = ContactAction.CONTACT_ADD
|
||||
# TODO: 提取联系人信息
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["邮件", "email", "inbox"]):
|
||||
state.action = ContactAction.EMAIL_LIST
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["发送邮件", "send email", "发邮件"]):
|
||||
state.action = ContactAction.EMAIL_SEND
|
||||
|
||||
else:
|
||||
state.action = ContactAction.SNIFF_CONTACTS
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def list_contacts(state: ContactState) -> ContactState:
|
||||
"""
|
||||
列出联系人节点
|
||||
"""
|
||||
state.current_phase = "listing_contacts"
|
||||
|
||||
# TODO: 从数据库查询
|
||||
# 暂时返回空列表
|
||||
state.contacts = []
|
||||
state.success = True
|
||||
state.final_result = "暂无联系人"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def add_contact(state: ContactState) -> ContactState:
|
||||
"""
|
||||
添加联系人节点
|
||||
"""
|
||||
state.current_phase = "adding_contact"
|
||||
|
||||
# TODO: 实现添加联系人逻辑
|
||||
state.success = True
|
||||
state.final_result = "联系人添加成功(待实现)"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def list_emails(state: ContactState) -> ContactState:
|
||||
"""
|
||||
列出邮件节点
|
||||
"""
|
||||
state.current_phase = "listing_emails"
|
||||
|
||||
# TODO: 从IMAP查询
|
||||
state.emails = []
|
||||
state.success = True
|
||||
state.final_result = "暂无邮件"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def generate_email_draft(state: ContactState) -> ContactState:
|
||||
"""
|
||||
生成邮件草稿节点
|
||||
"""
|
||||
state.current_phase = "generating_draft"
|
||||
|
||||
# TODO: 使用LLM生成邮件草稿
|
||||
state.draft_subject = "邮件主题"
|
||||
state.draft_recipient = "recipient@example.com"
|
||||
state.draft_body = "这是邮件内容..."
|
||||
|
||||
# 进入人工审核状态
|
||||
state.pending_review = True
|
||||
state.review_type = "email_send"
|
||||
state.review_prompt = "请确认是否发送此邮件"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def human_review(state: ContactState) -> ContactState:
|
||||
"""
|
||||
人工审核节点
|
||||
这里会让用户确认/修改
|
||||
"""
|
||||
state.current_phase = "reviewing"
|
||||
|
||||
# 注意:真实的LangGraph会在这里使用interrupt()暂停
|
||||
# 这里我们只设置状态,让外层处理
|
||||
if state.review_approved is True:
|
||||
state.pending_review = False
|
||||
elif state.review_approved is False:
|
||||
state.pending_review = False
|
||||
state.error_message = "发送已取消"
|
||||
state.success = False
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def send_email(state: ContactState) -> ContactState:
|
||||
"""
|
||||
发送邮件节点
|
||||
"""
|
||||
state.current_phase = "sending_email"
|
||||
|
||||
# TODO: 使用SMTP发送邮件
|
||||
state.success = True
|
||||
state.final_result = "邮件发送成功(待实现)"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def sniff_contacts(state: ContactState) -> ContactState:
|
||||
"""
|
||||
智能嗅探节点
|
||||
从对话中提取可能的联系人信息
|
||||
"""
|
||||
state.current_phase = "sniffing"
|
||||
|
||||
# TODO: 实现智能嗅探
|
||||
state.success = True
|
||||
state.final_result = "智能嗅探完成(待实现)"
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def format_result(state: ContactState) -> ContactState:
|
||||
"""
|
||||
格式化结果节点
|
||||
"""
|
||||
state.current_phase = "formatting"
|
||||
|
||||
# 根据不同action生成不同的格式化输出
|
||||
if state.action == ContactAction.CONTACT_LIST:
|
||||
if state.contacts:
|
||||
result = "联系人列表:\n"
|
||||
for i, contact in enumerate(state.contacts, 1):
|
||||
result += f"{i}. {contact.name}"
|
||||
if contact.phone:
|
||||
result += f" - {contact.phone}"
|
||||
if contact.email:
|
||||
result += f" ({contact.email})"
|
||||
result += "\n"
|
||||
else:
|
||||
result = "暂无联系人"
|
||||
state.final_result = result
|
||||
|
||||
elif state.action == ContactAction.EMAIL_LIST:
|
||||
if state.emails:
|
||||
result = "邮件列表:\n"
|
||||
for i, email in enumerate(state.emails[:10], 1):
|
||||
result += f"{i}. {email.subject} - {email.sender}\n"
|
||||
else:
|
||||
result = "暂无邮件"
|
||||
state.final_result = result
|
||||
|
||||
else:
|
||||
if not state.final_result:
|
||||
state.final_result = "操作完成"
|
||||
|
||||
state.current_phase = "done"
|
||||
return state
|
||||
|
||||
|
||||
def should_continue(state: ContactState) -> str:
|
||||
"""
|
||||
条件路由:决定下一步该做什么
|
||||
"""
|
||||
if state.error_message:
|
||||
return "finalize"
|
||||
|
||||
# 如果在审核中,等待
|
||||
if state.pending_review:
|
||||
return "human_review"
|
||||
|
||||
# 根据action路由
|
||||
if state.action == ContactAction.NONE:
|
||||
return "parse_intent"
|
||||
elif state.action == ContactAction.CONTACT_LIST:
|
||||
return "list_contacts"
|
||||
elif state.action == ContactAction.CONTACT_ADD:
|
||||
return "add_contact"
|
||||
elif state.action == ContactAction.EMAIL_LIST:
|
||||
return "list_emails"
|
||||
elif state.action == ContactAction.EMAIL_SEND:
|
||||
if state.pending_review:
|
||||
return "human_review"
|
||||
elif state.draft_subject:
|
||||
return "send_email"
|
||||
else:
|
||||
return "generate_email_draft"
|
||||
elif state.action == ContactAction.SNIFF_CONTACTS:
|
||||
return "sniff_contacts"
|
||||
else:
|
||||
return "format_result"
|
||||
104
backend/app/agent_subgraphs/contact/state.py
Normal file
104
backend/app/agent_subgraphs/contact/state.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
通讯录子图状态定义
|
||||
Contact Subgraph State Definition
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Dict, List, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
class ContactAction(Enum):
|
||||
"""通讯录操作类型"""
|
||||
NONE = auto()
|
||||
CONTACT_LIST = auto() # 联系人列表
|
||||
CONTACT_ADD = auto() # 添加联系人
|
||||
CONTACT_UPDATE = auto() # 更新联系人
|
||||
CONTACT_DELETE = auto() # 删除联系人
|
||||
EMAIL_LIST = auto() # 邮件列表
|
||||
EMAIL_READ = auto() # 读取邮件
|
||||
EMAIL_SEND = auto() # 发送邮件
|
||||
SNIFF_CONTACTS = auto() # 智能嗅探
|
||||
|
||||
|
||||
@dataclass
|
||||
class Contact:
|
||||
"""联系人数据结构"""
|
||||
id: Optional[str] = None
|
||||
name: str = ""
|
||||
phone: str = ""
|
||||
email: str = ""
|
||||
company: str = ""
|
||||
position: str = ""
|
||||
notes: str = ""
|
||||
created_at: Optional[str] = None
|
||||
updated_at: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Email:
|
||||
"""邮件数据结构"""
|
||||
id: Optional[str] = None
|
||||
subject: str = ""
|
||||
sender: str = ""
|
||||
recipients: List[str] = field(default_factory=list)
|
||||
date: Optional[str] = None
|
||||
body: str = ""
|
||||
is_read: bool = False
|
||||
mailbox: str = ""
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ContactState:
|
||||
"""通讯录子图状态"""
|
||||
# ========== 输入 ==========
|
||||
user_query: str = "" # 用户查询
|
||||
user_id: str = "" # 用户ID
|
||||
|
||||
# 操作控制
|
||||
action: ContactAction = ContactAction.NONE
|
||||
action_params: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# ========== 执行过程 ==========
|
||||
# 当前阶段
|
||||
current_phase: str = "init" # init, processing, reviewing, done
|
||||
|
||||
# 联系人相关
|
||||
contacts: List[Contact] = field(default_factory=list)
|
||||
current_contact: Optional[Contact] = None
|
||||
|
||||
# 邮件相关
|
||||
emails: List[Email] = field(default_factory=list)
|
||||
current_email: Optional[Email] = None
|
||||
|
||||
# 邮件草稿(用于审核)
|
||||
draft_subject: str = ""
|
||||
draft_recipient: str = ""
|
||||
draft_body: str = ""
|
||||
|
||||
# ========== 人工审核相关 ==========
|
||||
pending_review: bool = False
|
||||
review_type: str = "" # email_send, contact_delete
|
||||
review_prompt: str = ""
|
||||
review_approved: Optional[bool] = None
|
||||
review_comment: str = ""
|
||||
review_modified_content: str = ""
|
||||
|
||||
# ========== 智能嗅探 ==========
|
||||
sniff_result: Optional[Dict[str, Any]] = None
|
||||
sniffed_contacts: List[Contact] = field(default_factory=list)
|
||||
sniff_confirmation_pending: bool = False
|
||||
|
||||
# ========== 结果 ==========
|
||||
success: bool = False
|
||||
error_message: str = ""
|
||||
final_result: str = ""
|
||||
result_data: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# ========== 元数据 ==========
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
duration: float = 0.0
|
||||
debug_info: Dict[str, Any] = field(default_factory=dict)
|
||||
50
backend/app/agent_subgraphs/dictionary/__init__.py
Normal file
50
backend/app/agent_subgraphs/dictionary/__init__.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""
|
||||
词典子图 - 完善版
|
||||
Dictionary Subgraph Module - Complete
|
||||
"""
|
||||
|
||||
from .state import (
|
||||
DictionaryState,
|
||||
DictionaryAction,
|
||||
WordEntry,
|
||||
ExtractedTerm
|
||||
)
|
||||
from .graph import build_dictionary_subgraph
|
||||
from .nodes import (
|
||||
parse_intent,
|
||||
query_word,
|
||||
translate_text,
|
||||
extract_terms,
|
||||
get_daily_word,
|
||||
lookup_word_book,
|
||||
add_to_word_book,
|
||||
format_result,
|
||||
should_continue
|
||||
)
|
||||
from .api_client import dictionary_api, DictionaryAPIClient
|
||||
|
||||
__all__ = [
|
||||
# State
|
||||
"DictionaryState",
|
||||
"DictionaryAction",
|
||||
"WordEntry",
|
||||
"ExtractedTerm",
|
||||
|
||||
# Graph
|
||||
"build_dictionary_subgraph",
|
||||
|
||||
# Nodes
|
||||
"parse_intent",
|
||||
"query_word",
|
||||
"translate_text",
|
||||
"extract_terms",
|
||||
"get_daily_word",
|
||||
"lookup_word_book",
|
||||
"add_to_word_book",
|
||||
"format_result",
|
||||
"should_continue",
|
||||
|
||||
# API
|
||||
"dictionary_api",
|
||||
"DictionaryAPIClient"
|
||||
]
|
||||
129
backend/app/agent_subgraphs/dictionary/api_client.py
Normal file
129
backend/app/agent_subgraphs/dictionary/api_client.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
词典API调用工具
|
||||
Dictionary API Client
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
import requests
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class DictionaryAPIClient:
|
||||
"""
|
||||
词典API客户端 - 可扩展支持多种API
|
||||
"""
|
||||
|
||||
# 可以配置多个API
|
||||
youdao_api_key: Optional[str] = None
|
||||
youdao_api_secret: Optional[str] = None
|
||||
|
||||
def query_word_youdao(self, word: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
调用有道词典API查询单词
|
||||
|
||||
注意:需要配置有道API密钥才能使用
|
||||
文档:https://ai.youdao.com/doc.s#guide
|
||||
"""
|
||||
if not self.youdao_api_key or not self.youdao_api_secret:
|
||||
return None
|
||||
|
||||
try:
|
||||
# TODO: 实现真实的有道API调用
|
||||
# 这里是示例结构
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
print(f"有道API调用失败:{e}")
|
||||
return None
|
||||
|
||||
def translate_baidu(self, text: str, from_lang: str = "auto", to_lang: str = "zh") -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
调用百度翻译API
|
||||
|
||||
注意:需要配置百度API密钥才能使用
|
||||
文档:https://fanyi-api.baidu.com/doc/21
|
||||
"""
|
||||
# TODO: 实现真实的百度翻译API调用
|
||||
return None
|
||||
|
||||
def query_word_mock(self, word: str) -> Dict[str, Any]:
|
||||
"""
|
||||
模拟词典API - 目前用于演示
|
||||
"""
|
||||
mock_db = {
|
||||
"serendipity": {
|
||||
"phonetic": "/ˌserənˈdipədē/",
|
||||
"part_of_speech": "n.",
|
||||
"definitions": ["意外发现珍奇事物的能力", "机缘凑巧"],
|
||||
"examples": ["Finding that old photo was pure serendipity."]
|
||||
},
|
||||
"ephemeral": {
|
||||
"phonetic": "/əˈfem(ə)rəl/",
|
||||
"part_of_speech": "adj.",
|
||||
"definitions": ["短暂的,瞬息的"],
|
||||
"examples": ["Fame in the digital age is often ephemeral."]
|
||||
},
|
||||
"ubiquitous": {
|
||||
"phonetic": "/yo͞oˈbikwədəs/",
|
||||
"part_of_speech": "adj.",
|
||||
"definitions": ["无处不在的", "普遍存在的"],
|
||||
"examples": ["Smartphones have become ubiquitous in modern life."]
|
||||
},
|
||||
"eloquent": {
|
||||
"phonetic": "/ˈeləkwənt/",
|
||||
"part_of_speech": "adj.",
|
||||
"definitions": ["雄辩的,有说服力的"],
|
||||
"examples": ["She gave an eloquent speech at the conference."]
|
||||
},
|
||||
"resilient": {
|
||||
"phonetic": "/rəˈzilyənt/",
|
||||
"part_of_speech": "adj.",
|
||||
"definitions": ["有复原力的,能适应的"],
|
||||
"examples": ["The community has proven to be resilient in the face of challenges."]
|
||||
}
|
||||
}
|
||||
|
||||
if word.lower() in mock_db:
|
||||
return mock_db[word.lower()]
|
||||
else:
|
||||
return {
|
||||
"phonetic": "",
|
||||
"part_of_speech": "n.",
|
||||
"definitions": [f"{word}的释义1", f"{word}的释义2"],
|
||||
"examples": [f"This is an example sentence with '{word}'."]
|
||||
}
|
||||
|
||||
def translate_mock(self, text: str, from_lang: str = "auto", to_lang: str = "zh") -> Dict[str, Any]:
|
||||
"""
|
||||
模拟翻译API - 目前用于演示
|
||||
"""
|
||||
translations = {
|
||||
"你好": "Hello",
|
||||
"hello": "你好",
|
||||
"人工智能": "Artificial Intelligence",
|
||||
"artificial intelligence": "人工智能",
|
||||
"ai": "人工智能",
|
||||
"大模型": "Large Language Model",
|
||||
"自然语言处理": "Natural Language Processing"
|
||||
}
|
||||
|
||||
return {
|
||||
"translated_text": translations.get(text.lower(), f"【翻译结果】{text}"),
|
||||
"confidence": 0.95
|
||||
}
|
||||
|
||||
def extract_terms_mock(self, text: str) -> list:
|
||||
"""
|
||||
模拟术语提取API
|
||||
"""
|
||||
return [
|
||||
{"term": "AI", "type": "技术术语", "definition": "人工智能", "confidence": 0.95},
|
||||
{"term": "LLM", "type": "技术术语", "definition": "大语言模型", "confidence": 0.92},
|
||||
{"term": "NLP", "type": "技术术语", "definition": "自然语言处理", "confidence": 0.88}
|
||||
]
|
||||
|
||||
|
||||
# 单例实例
|
||||
dictionary_api = DictionaryAPIClient()
|
||||
71
backend/app/agent_subgraphs/dictionary/graph.py
Normal file
71
backend/app/agent_subgraphs/dictionary/graph.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
词典子图构建器 - 完善版
|
||||
Dictionary Subgraph Builder - Complete
|
||||
"""
|
||||
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
from .state import DictionaryState
|
||||
from .nodes import (
|
||||
parse_intent,
|
||||
query_word,
|
||||
translate_text,
|
||||
extract_terms,
|
||||
get_daily_word,
|
||||
lookup_word_book,
|
||||
add_to_word_book,
|
||||
format_result,
|
||||
should_continue
|
||||
)
|
||||
|
||||
|
||||
def build_dictionary_subgraph() -> StateGraph:
|
||||
"""
|
||||
构建词典子图
|
||||
|
||||
Returns:
|
||||
配置好的 StateGraph
|
||||
"""
|
||||
# 创建图
|
||||
graph = StateGraph(DictionaryState)
|
||||
|
||||
# 添加节点
|
||||
graph.add_node("parse_intent", parse_intent)
|
||||
graph.add_node("query_word", query_word)
|
||||
graph.add_node("translate_text", translate_text)
|
||||
graph.add_node("extract_terms", extract_terms)
|
||||
graph.add_node("get_daily_word", get_daily_word)
|
||||
graph.add_node("lookup_word_book", lookup_word_book)
|
||||
graph.add_node("add_to_word_book", add_to_word_book)
|
||||
graph.add_node("format_result", format_result)
|
||||
|
||||
# 添加边
|
||||
# 从START开始
|
||||
graph.add_edge(START, "parse_intent")
|
||||
|
||||
# 从parse_intent根据条件路由
|
||||
graph.add_conditional_edges(
|
||||
"parse_intent",
|
||||
should_continue,
|
||||
{
|
||||
"query_word": "query_word",
|
||||
"translate_text": "translate_text",
|
||||
"extract_terms": "extract_terms",
|
||||
"get_daily_word": "get_daily_word",
|
||||
"lookup_word_book": "lookup_word_book",
|
||||
"add_to_word_book": "add_to_word_book",
|
||||
}
|
||||
)
|
||||
|
||||
# 从各个操作节点到format_result
|
||||
graph.add_edge("query_word", "format_result")
|
||||
graph.add_edge("translate_text", "format_result")
|
||||
graph.add_edge("extract_terms", "format_result")
|
||||
graph.add_edge("get_daily_word", "format_result")
|
||||
graph.add_edge("lookup_word_book", "format_result")
|
||||
graph.add_edge("add_to_word_book", "format_result")
|
||||
|
||||
# 最终到END
|
||||
graph.add_edge("format_result", END)
|
||||
|
||||
return graph
|
||||
402
backend/app/agent_subgraphs/dictionary/nodes.py
Normal file
402
backend/app/agent_subgraphs/dictionary/nodes.py
Normal file
@@ -0,0 +1,402 @@
|
||||
"""
|
||||
词典子图节点 - 完善版(使用API客户端)
|
||||
Dictionary Subgraph Nodes - Complete (with API Client)
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from datetime import datetime
|
||||
import random
|
||||
|
||||
from .state import (
|
||||
DictionaryState,
|
||||
DictionaryAction,
|
||||
WordEntry,
|
||||
ExtractedTerm
|
||||
)
|
||||
from .api_client import dictionary_api
|
||||
|
||||
|
||||
# ========== 模拟生词本存储(后续可替换为数据库) ==========
|
||||
WORD_BOOK_DB: Dict[str, List[Dict]] = {} # user_id -> [word_entries]
|
||||
|
||||
|
||||
def parse_intent(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
解析用户意图节点
|
||||
确定用户想做什么操作
|
||||
"""
|
||||
state.current_phase = "intent_parsing"
|
||||
|
||||
query_lower = state.user_query.lower()
|
||||
|
||||
# 简单的关键词匹配
|
||||
if any(keyword in query_lower for keyword in ["翻译", "translate", "英语", "英文"]):
|
||||
state.action = DictionaryAction.TRANSLATE
|
||||
state.action_params = {"text": state.user_query}
|
||||
# 同时设置source_text
|
||||
text = state.user_query
|
||||
for keyword in ["翻译", "translate", "英语", "英文"]:
|
||||
text = text.replace(keyword, "")
|
||||
state.source_text = text.strip()
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["查询", "query", "单词", "word"]):
|
||||
state.action = DictionaryAction.QUERY
|
||||
state.action_params = {"word": state.user_query}
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["每日", "daily", "一词"]):
|
||||
state.action = DictionaryAction.DAILY_WORD
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["提取", "extract", "术语", "term"]):
|
||||
state.action = DictionaryAction.EXTRACT
|
||||
state.action_params = {"text": state.user_query}
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["生词本", "wordbook", "我的单词"]):
|
||||
state.action = DictionaryAction.WORD_BOOK_LOOKUP
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["添加到生词本", "添加单词", "add to wordbook"]):
|
||||
state.action = DictionaryAction.WORD_BOOK_ADD
|
||||
state.action_params = {"word": state.user_query}
|
||||
|
||||
else:
|
||||
# 默认翻译
|
||||
state.action = DictionaryAction.TRANSLATE
|
||||
state.source_text = state.user_query
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def query_word(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
查询单词节点
|
||||
"""
|
||||
state.current_phase = "querying_word"
|
||||
|
||||
word = state.action_params.get("word", state.user_query)
|
||||
word = word.replace("查询", "").replace("单词", "").strip()
|
||||
|
||||
# 使用API客户端查询单词
|
||||
data = dictionary_api.query_word_mock(word)
|
||||
|
||||
state.word_entry = WordEntry(
|
||||
word=word,
|
||||
phonetic=data.get("phonetic", ""),
|
||||
part_of_speech=data.get("part_of_speech", "n."),
|
||||
definitions=data.get("definitions", []),
|
||||
examples=data.get("examples", []),
|
||||
)
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def translate_text(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
翻译文本节点
|
||||
"""
|
||||
state.current_phase = "translating"
|
||||
|
||||
text = state.source_text or state.action_params.get("text", state.user_query)
|
||||
|
||||
# 使用API客户端翻译
|
||||
data = dictionary_api.translate_mock(text, state.source_language, state.target_language)
|
||||
state.translated_text = data.get("translated_text", f"【翻译结果】{text}")
|
||||
state.translation_confidence = data.get("confidence", 0.95)
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def extract_terms(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
提取专业术语节点
|
||||
"""
|
||||
state.current_phase = "extracting_terms"
|
||||
|
||||
text = state.source_text or state.action_params.get("text", state.user_query)
|
||||
|
||||
# 使用API客户端提取术语
|
||||
terms_data = dictionary_api.extract_terms_mock(text)
|
||||
|
||||
for term_data in terms_data:
|
||||
state.extracted_terms.append(ExtractedTerm(
|
||||
term=term_data.get("term", ""),
|
||||
type=term_data.get("type", ""),
|
||||
definition=term_data.get("definition", ""),
|
||||
confidence=term_data.get("confidence", 0.0)
|
||||
))
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def get_daily_word(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
获取每日一词节点
|
||||
"""
|
||||
state.current_phase = "getting_daily_word"
|
||||
|
||||
# 每日一词候选
|
||||
words = ["serendipity", "ephemeral", "ubiquitous", "eloquent", "resilient"]
|
||||
|
||||
# 基于日期选择固定词,这样同一天的每日一词是固定的
|
||||
day_of_year = datetime.now().timetuple().tm_yday
|
||||
chosen_idx = day_of_year % len(words)
|
||||
chosen_word = words[chosen_idx]
|
||||
|
||||
# 使用API客户端查询单词详情
|
||||
data = dictionary_api.query_word_mock(chosen_word)
|
||||
|
||||
state.daily_word = WordEntry(
|
||||
word=chosen_word,
|
||||
phonetic=data.get("phonetic", ""),
|
||||
part_of_speech=data.get("part_of_speech", "adj."),
|
||||
definitions=data.get("definitions", []),
|
||||
examples=data.get("examples", []),
|
||||
)
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def lookup_word_book(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
查询生词本节点
|
||||
"""
|
||||
state.current_phase = "looking_up_wordbook"
|
||||
|
||||
user_id = state.user_id or "default_user"
|
||||
word_book = WORD_BOOK_DB.get(user_id, [])
|
||||
|
||||
# 构建WordEntry列表
|
||||
if word_book:
|
||||
for entry in word_book:
|
||||
state.extracted_terms.append(ExtractedTerm(
|
||||
term=entry.get("word", ""),
|
||||
type="生词本单词",
|
||||
definition=entry.get("definitions", [""])[0] if entry.get("definitions") else "",
|
||||
confidence=1.0
|
||||
))
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def add_to_word_book(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
添加到生词本节点
|
||||
"""
|
||||
state.current_phase = "adding_to_wordbook"
|
||||
|
||||
user_id = state.user_id or "default_user"
|
||||
word = state.action_params.get("word", state.user_query)
|
||||
word = word.replace("添加到生词本", "").replace("添加单词", "").strip()
|
||||
|
||||
# 查询单词信息
|
||||
query_state = DictionaryState(user_query=word, action=DictionaryAction.QUERY)
|
||||
query_state = query_word(query_state)
|
||||
|
||||
if query_state.word_entry:
|
||||
we = query_state.word_entry
|
||||
|
||||
# 添加到模拟数据库
|
||||
if user_id not in WORD_BOOK_DB:
|
||||
WORD_BOOK_DB[user_id] = []
|
||||
|
||||
# 检查是否已存在
|
||||
exists = any(entry.get("word") == we.word for entry in WORD_BOOK_DB[user_id])
|
||||
|
||||
if not exists:
|
||||
entry_dict = {
|
||||
"word": we.word,
|
||||
"phonetic": we.phonetic,
|
||||
"part_of_speech": we.part_of_speech,
|
||||
"definitions": we.definitions,
|
||||
"examples": we.examples,
|
||||
"added_at": datetime.now().isoformat(),
|
||||
"review_count": 0,
|
||||
"next_review_at": None
|
||||
}
|
||||
WORD_BOOK_DB[user_id].append(entry_dict)
|
||||
state.final_result = f"✅ 已将 '{we.word}' 添加到生词本!"
|
||||
else:
|
||||
state.final_result = f"ℹ️ '{we.word}' 已在生词本中!"
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def format_result(state: DictionaryState) -> DictionaryState:
|
||||
"""
|
||||
格式化结果节点 - 精美输出
|
||||
"""
|
||||
state.current_phase = "formatting"
|
||||
|
||||
if state.action == DictionaryAction.QUERY and state.word_entry:
|
||||
we = state.word_entry
|
||||
result = []
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("📚 单词查询结果")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append(f"")
|
||||
result.append(f" {we.word}")
|
||||
if we.phonetic:
|
||||
result.append(f" {we.phonetic}")
|
||||
result.append(f" 【{we.part_of_speech}】")
|
||||
result.append(f"")
|
||||
|
||||
result.append("📖 释义:")
|
||||
for i, definition in enumerate(we.definitions, 1):
|
||||
result.append(f" {i}. {definition}")
|
||||
|
||||
if we.examples:
|
||||
result.append("")
|
||||
result.append("💡 例句:")
|
||||
for example in we.examples:
|
||||
result.append(f" \"{example}\"")
|
||||
|
||||
if we.synonyms:
|
||||
result.append("")
|
||||
result.append("🔗 同义词:")
|
||||
result.append(f" {', '.join(we.synonyms)}")
|
||||
|
||||
if we.antonyms:
|
||||
result.append("")
|
||||
result.append("🔗 反义词:")
|
||||
result.append(f" {', '.join(we.antonyms)}")
|
||||
|
||||
result.append("")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("💡 提示:回复 '添加到生词本 + 单词' 可收藏")
|
||||
|
||||
state.final_result = "\n".join(result)
|
||||
|
||||
elif state.action == DictionaryAction.TRANSLATE:
|
||||
result = []
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("🔄 翻译结果")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append(f"")
|
||||
result.append(f" 原文:{state.source_text}")
|
||||
result.append(f" 译文:{state.translated_text}")
|
||||
result.append(f"")
|
||||
result.append(f" 🎯 置信度:{state.translation_confidence:.0%}")
|
||||
result.append("")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
|
||||
state.final_result = "\n".join(result)
|
||||
|
||||
elif state.action == DictionaryAction.DAILY_WORD and state.daily_word:
|
||||
dw = state.daily_word
|
||||
result = []
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("🌟 每日一词")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append(f"")
|
||||
result.append(f" {dw.word}")
|
||||
if dw.phonetic:
|
||||
result.append(f" {dw.phonetic}")
|
||||
result.append(f" 【{dw.part_of_speech}】")
|
||||
result.append(f"")
|
||||
|
||||
if dw.definitions:
|
||||
result.append("📖 释义:")
|
||||
for i, definition in enumerate(dw.definitions, 1):
|
||||
result.append(f" {i}. {definition}")
|
||||
|
||||
if dw.examples:
|
||||
result.append("")
|
||||
result.append("💡 例句:")
|
||||
for example in dw.examples:
|
||||
result.append(f" \"{example}\"")
|
||||
|
||||
result.append("")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("💡 学习提示:尝试用这个词造一个句子")
|
||||
result.append("💡 收藏提示:回复 '添加到生词本' 可收藏")
|
||||
|
||||
state.final_result = "\n".join(result)
|
||||
|
||||
elif state.action == DictionaryAction.EXTRACT and state.extracted_terms:
|
||||
result = []
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("📋 提取的术语")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("")
|
||||
|
||||
for i, term in enumerate(state.extracted_terms, 1):
|
||||
result.append(f" {i}. {term.term} 【{term.type}】")
|
||||
result.append(f" {term.definition}")
|
||||
result.append(f" 🎯 置信度:{term.confidence:.0%}")
|
||||
result.append("")
|
||||
|
||||
result.append("═══════════════════════════════════════════")
|
||||
|
||||
state.final_result = "\n".join(result)
|
||||
|
||||
elif state.action == DictionaryAction.WORD_BOOK_LOOKUP:
|
||||
user_id = state.user_id or "default_user"
|
||||
word_book = WORD_BOOK_DB.get(user_id, [])
|
||||
|
||||
result = []
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append("📚 我的生词本")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
result.append(f"")
|
||||
|
||||
if word_book:
|
||||
result.append(f" 共 {len(word_book)} 个单词")
|
||||
result.append("")
|
||||
for i, entry in enumerate(word_book, 1):
|
||||
word = entry.get("word", "")
|
||||
added_at = entry.get("added_at", "")
|
||||
added_date = datetime.fromisoformat(added_at).strftime("%Y-%m-%d") if added_at else ""
|
||||
result.append(f" {i}. {word} (添加于:{added_date})")
|
||||
else:
|
||||
result.append(" 生词本为空")
|
||||
result.append(" 💡 提示:查询单词后可添加到生词本")
|
||||
|
||||
result.append("")
|
||||
result.append("═══════════════════════════════════════════")
|
||||
|
||||
state.final_result = "\n".join(result)
|
||||
|
||||
elif state.action == DictionaryAction.WORD_BOOK_ADD:
|
||||
if state.final_result:
|
||||
result = state.final_result
|
||||
else:
|
||||
result = "添加完成"
|
||||
|
||||
state.final_result = result
|
||||
|
||||
else:
|
||||
if not state.final_result:
|
||||
state.final_result = "词典操作完成"
|
||||
|
||||
state.current_phase = "done"
|
||||
return state
|
||||
|
||||
|
||||
def should_continue(state: DictionaryState) -> str:
|
||||
"""
|
||||
条件路由:决定下一步该做什么
|
||||
"""
|
||||
if state.error_message:
|
||||
return "format_result"
|
||||
|
||||
# 根据action路由
|
||||
if state.action == DictionaryAction.NONE:
|
||||
return "parse_intent"
|
||||
elif state.action == DictionaryAction.QUERY:
|
||||
return "query_word"
|
||||
elif state.action == DictionaryAction.TRANSLATE:
|
||||
return "translate_text"
|
||||
elif state.action == DictionaryAction.EXTRACT:
|
||||
return "extract_terms"
|
||||
elif state.action == DictionaryAction.DAILY_WORD:
|
||||
return "get_daily_word"
|
||||
elif state.action == DictionaryAction.WORD_BOOK_LOOKUP:
|
||||
return "lookup_word_book"
|
||||
elif state.action == DictionaryAction.WORD_BOOK_ADD:
|
||||
return "add_to_word_book"
|
||||
else:
|
||||
return "format_result"
|
||||
95
backend/app/agent_subgraphs/dictionary/state.py
Normal file
95
backend/app/agent_subgraphs/dictionary/state.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
词典子图状态定义
|
||||
Dictionary Subgraph State Definition
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Dict, List, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
class DictionaryAction(Enum):
|
||||
"""词典操作类型"""
|
||||
NONE = auto()
|
||||
QUERY = auto() # 查询单词
|
||||
TRANSLATE = auto() # 翻译文本
|
||||
EXTRACT = auto() # 提取专业术语
|
||||
DAILY_WORD = auto() # 每日一词
|
||||
WORD_BOOK_LOOKUP = auto() # 生词本查询
|
||||
WORD_BOOK_ADD = auto() # 添加到生词本
|
||||
|
||||
|
||||
@dataclass
|
||||
class WordEntry:
|
||||
"""单词词条"""
|
||||
word: str = ""
|
||||
phonetic: str = "" # 音标
|
||||
part_of_speech: str = "" # 词性
|
||||
definitions: List[str] = field(default_factory=list) # 释义
|
||||
examples: List[str] = field(default_factory=list) # 例句
|
||||
synonyms: List[str] = field(default_factory=list) # 同义词
|
||||
antonyms: List[str] = field(default_factory=list) # 反义词
|
||||
source_language: str = "en" # 源语言
|
||||
target_language: str = "zh" # 目标语言
|
||||
in_word_book: bool = False # 是否在生词本
|
||||
review_count: int = 0 # 复习次数
|
||||
next_review_at: Optional[str] = None # 下次复习时间
|
||||
created_at: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExtractedTerm:
|
||||
"""提取的术语"""
|
||||
term: str = ""
|
||||
type: str = "" # 技术术语、医学术语等
|
||||
definition: str = ""
|
||||
context: str = ""
|
||||
confidence: float = 0.0
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DictionaryState:
|
||||
"""词典子图状态"""
|
||||
# ========== 输入 ==========
|
||||
user_query: str = "" # 用户查询
|
||||
user_id: str = "" # 用户ID
|
||||
|
||||
# 操作控制
|
||||
action: DictionaryAction = DictionaryAction.NONE
|
||||
action_params: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# 翻译专用
|
||||
source_text: str = ""
|
||||
source_language: str = "auto" # auto, en, zh, etc.
|
||||
target_language: str = "zh" # 默认翻译成中文
|
||||
|
||||
# ========== 执行过程 ==========
|
||||
current_phase: str = "init" # init, querying, extracting, done
|
||||
|
||||
# 查询结果
|
||||
word_entry: Optional[WordEntry] = None
|
||||
|
||||
# 翻译结果
|
||||
translated_text: str = ""
|
||||
translation_confidence: float = 0.0
|
||||
|
||||
# 提取结果
|
||||
extracted_terms: List[ExtractedTerm] = field(default_factory=list)
|
||||
|
||||
# 每日一词
|
||||
daily_word: Optional[WordEntry] = None
|
||||
daily_word_context: str = ""
|
||||
|
||||
# ========== 结果 ==========
|
||||
success: bool = False
|
||||
error_message: str = ""
|
||||
final_result: str = ""
|
||||
result_data: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# ========== 元数据 ==========
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
duration: float = 0.0
|
||||
debug_info: Dict[str, Any] = field(default_factory=dict)
|
||||
41
backend/app/agent_subgraphs/news_analysis/__init__.py
Normal file
41
backend/app/agent_subgraphs/news_analysis/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
资讯子图
|
||||
News Analysis Subgraph Module
|
||||
"""
|
||||
|
||||
from .state import (
|
||||
NewsAnalysisState,
|
||||
NewsAction,
|
||||
NewsItem,
|
||||
NewsSource
|
||||
)
|
||||
from .graph import build_news_analysis_subgraph
|
||||
from .nodes import (
|
||||
parse_intent,
|
||||
query_news,
|
||||
analyze_url,
|
||||
extract_keywords,
|
||||
generate_report,
|
||||
format_result,
|
||||
should_continue
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# State
|
||||
"NewsAnalysisState",
|
||||
"NewsAction",
|
||||
"NewsItem",
|
||||
"NewsSource",
|
||||
|
||||
# Graph
|
||||
"build_news_analysis_subgraph",
|
||||
|
||||
# Nodes
|
||||
"parse_intent",
|
||||
"query_news",
|
||||
"analyze_url",
|
||||
"extract_keywords",
|
||||
"generate_report",
|
||||
"format_result",
|
||||
"should_continue"
|
||||
]
|
||||
63
backend/app/agent_subgraphs/news_analysis/graph.py
Normal file
63
backend/app/agent_subgraphs/news_analysis/graph.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
资讯子图构建器
|
||||
News Analysis Subgraph Builder
|
||||
"""
|
||||
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
from .state import NewsAnalysisState
|
||||
from .nodes import (
|
||||
parse_intent,
|
||||
query_news,
|
||||
analyze_url,
|
||||
extract_keywords,
|
||||
generate_report,
|
||||
format_result,
|
||||
should_continue
|
||||
)
|
||||
|
||||
|
||||
def build_news_analysis_subgraph() -> StateGraph:
|
||||
"""
|
||||
构建资讯子图
|
||||
|
||||
Returns:
|
||||
配置好的 StateGraph
|
||||
"""
|
||||
# 创建图
|
||||
graph = StateGraph(NewsAnalysisState)
|
||||
|
||||
# 添加节点
|
||||
graph.add_node("parse_intent", parse_intent)
|
||||
graph.add_node("query_news", query_news)
|
||||
graph.add_node("analyze_url", analyze_url)
|
||||
graph.add_node("extract_keywords", extract_keywords)
|
||||
graph.add_node("generate_report", generate_report)
|
||||
graph.add_node("format_result", format_result)
|
||||
|
||||
# 添加边
|
||||
# 从START开始
|
||||
graph.add_edge(START, "parse_intent")
|
||||
|
||||
# 从parse_intent根据条件路由
|
||||
graph.add_conditional_edges(
|
||||
"parse_intent",
|
||||
should_continue,
|
||||
{
|
||||
"query_news": "query_news",
|
||||
"analyze_url": "analyze_url",
|
||||
"extract_keywords": "extract_keywords",
|
||||
"generate_report": "generate_report",
|
||||
}
|
||||
)
|
||||
|
||||
# 从各个操作节点到format_result
|
||||
graph.add_edge("query_news", "format_result")
|
||||
graph.add_edge("analyze_url", "format_result")
|
||||
graph.add_edge("extract_keywords", "format_result")
|
||||
graph.add_edge("generate_report", "format_result")
|
||||
|
||||
# 最终到END
|
||||
graph.add_edge("format_result", END)
|
||||
|
||||
return graph
|
||||
207
backend/app/agent_subgraphs/news_analysis/nodes.py
Normal file
207
backend/app/agent_subgraphs/news_analysis/nodes.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""
|
||||
资讯子图节点
|
||||
News Analysis Subgraph Nodes
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
from datetime import datetime
|
||||
|
||||
from .state import (
|
||||
NewsAnalysisState,
|
||||
NewsAction,
|
||||
NewsItem,
|
||||
NewsSource
|
||||
)
|
||||
|
||||
|
||||
def parse_intent(state: NewsAnalysisState) -> NewsAnalysisState:
|
||||
"""
|
||||
解析用户意图节点
|
||||
确定用户想做什么操作
|
||||
"""
|
||||
state.current_phase = "intent_parsing"
|
||||
|
||||
query_lower = state.user_query.lower()
|
||||
|
||||
# 简单的关键词匹配
|
||||
if any(keyword in query_lower for keyword in ["资讯", "新闻", "news", "report"]):
|
||||
state.action = NewsAction.QUERY_NEWS
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["分析", "analyze", "url", "链接"]):
|
||||
state.action = NewsAction.ANALYZE_URL
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["关键词", "keyword", "提取"]):
|
||||
state.action = NewsAction.EXTRACT_KEYWORDS
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["报告", "生成", "generate"]):
|
||||
state.action = NewsAction.GENERATE_REPORT
|
||||
|
||||
else:
|
||||
# 默认查询资讯
|
||||
state.action = NewsAction.QUERY_NEWS
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def query_news(state: NewsAnalysisState) -> NewsAnalysisState:
|
||||
"""
|
||||
查询资讯节点
|
||||
"""
|
||||
state.current_phase = "querying_news"
|
||||
|
||||
# TODO: 调用资讯API或爬取
|
||||
query = state.user_query
|
||||
|
||||
# 模拟返回结果
|
||||
state.news_items = [
|
||||
NewsItem(
|
||||
title=f"关于 {query} 的资讯1",
|
||||
source="Tech News",
|
||||
summary="这是一条关于人工智能的资讯摘要...",
|
||||
keywords=[query, "AI", "Technology"]
|
||||
),
|
||||
NewsItem(
|
||||
title=f"关于 {query} 的资讯2",
|
||||
source="Business Daily",
|
||||
summary="行业动态:AI在商业中的应用...",
|
||||
keywords=[query, "Business", "Innovation"]
|
||||
)
|
||||
]
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def analyze_url(state: NewsAnalysisState) -> NewsAnalysisState:
|
||||
"""
|
||||
分析资讯URL节点
|
||||
"""
|
||||
state.current_phase = "analyzing_url"
|
||||
|
||||
# TODO: 调用URL分析API
|
||||
urls = state.custom_urls or [state.action_params.get("url", "")]
|
||||
|
||||
# 模拟返回结果
|
||||
for url in urls:
|
||||
if url:
|
||||
state.news_items.append(
|
||||
NewsItem(
|
||||
title=f"分析结果:{url}",
|
||||
source="URL Analyzer",
|
||||
summary="已完成对该URL的内容分析...",
|
||||
keywords=["News", "Analysis"]
|
||||
)
|
||||
)
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def extract_keywords(state: NewsAnalysisState) -> NewsAnalysisState:
|
||||
"""
|
||||
提取关键词节点
|
||||
"""
|
||||
state.current_phase = "extracting_keywords"
|
||||
|
||||
# TODO: 调用关键词提取API
|
||||
text = state.user_query
|
||||
|
||||
# 模拟返回结果
|
||||
state.extracted_keywords = ["AI", "大模型", "应用场景", "行业趋势"]
|
||||
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def generate_report(state: NewsAnalysisState) -> NewsAnalysisState:
|
||||
"""
|
||||
生成报告节点
|
||||
"""
|
||||
state.current_phase = "generating_report"
|
||||
|
||||
# TODO: 生成完整报告
|
||||
query = state.user_query
|
||||
|
||||
report = f"""📊 资讯分析报告
|
||||
|
||||
主题:{query}
|
||||
|
||||
📋 摘要:
|
||||
这是一份关于 {query} 的资讯分析综合报告,包含最新行业动态和趋势分析。
|
||||
|
||||
🔍 主要发现:
|
||||
1. AI技术持续快速发展
|
||||
2. 大模型应用场景不断拓展
|
||||
3. 行业数字化转型加速
|
||||
|
||||
🏷️ 关键词:
|
||||
- AI
|
||||
- 大模型
|
||||
- 数字化转型
|
||||
- 创新
|
||||
"""
|
||||
|
||||
state.report_content = report
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def format_result(state: NewsAnalysisState) -> NewsAnalysisState:
|
||||
"""
|
||||
格式化结果节点
|
||||
"""
|
||||
state.current_phase = "formatting"
|
||||
|
||||
if state.action == NewsAction.QUERY_NEWS and state.news_items:
|
||||
result = "📰 最新资讯\n\n"
|
||||
for i, item in enumerate(state.news_items, 1):
|
||||
result += f"{i}. {item.title}\n"
|
||||
result += f" 来源:{item.source}\n"
|
||||
result += f" 摘要:{item.summary}\n\n"
|
||||
|
||||
state.final_result = result
|
||||
|
||||
elif state.action == NewsAction.ANALYZE_URL and state.news_items:
|
||||
result = "🔍 资讯分析结果\n\n"
|
||||
for i, item in enumerate(state.news_items, 1):
|
||||
result += f"{i}. {item.title}\n"
|
||||
result += f" {item.summary}\n\n"
|
||||
|
||||
state.final_result = result
|
||||
|
||||
elif state.action == NewsAction.EXTRACT_KEYWORDS and state.extracted_keywords:
|
||||
result = "🏷️ 提取的关键词\n\n"
|
||||
result += ", ".join(state.extracted_keywords)
|
||||
state.final_result = result
|
||||
|
||||
elif state.action == NewsAction.GENERATE_REPORT and state.report_content:
|
||||
state.final_result = state.report_content
|
||||
|
||||
else:
|
||||
if not state.final_result:
|
||||
state.final_result = "资讯操作完成"
|
||||
|
||||
state.current_phase = "done"
|
||||
return state
|
||||
|
||||
|
||||
def should_continue(state: NewsAnalysisState) -> str:
|
||||
"""
|
||||
条件路由:决定下一步该做什么
|
||||
"""
|
||||
if state.error_message:
|
||||
return "format_result"
|
||||
|
||||
# 根据action路由
|
||||
if state.action == NewsAction.NONE:
|
||||
return "parse_intent"
|
||||
elif state.action == NewsAction.QUERY_NEWS:
|
||||
return "query_news"
|
||||
elif state.action == NewsAction.ANALYZE_URL:
|
||||
return "analyze_url"
|
||||
elif state.action == NewsAction.EXTRACT_KEYWORDS:
|
||||
return "extract_keywords"
|
||||
elif state.action == NewsAction.GENERATE_REPORT:
|
||||
return "generate_report"
|
||||
else:
|
||||
return "format_result"
|
||||
89
backend/app/agent_subgraphs/news_analysis/state.py
Normal file
89
backend/app/agent_subgraphs/news_analysis/state.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
资讯子图状态定义
|
||||
News Analysis Subgraph State Definition
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Dict, List, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
class NewsAction(Enum):
|
||||
"""资讯操作类型"""
|
||||
NONE = auto()
|
||||
QUERY_NEWS = auto() # 查询资讯
|
||||
ANALYZE_URL = auto() # 分析资讯
|
||||
GENERATE_REPORT = auto() # 生成报告
|
||||
FETCH_FROM_SOURCES = auto() # 从指定源获取
|
||||
EXTRACT_KEYWORDS = auto() # 提取关键词
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewsItem:
|
||||
"""资讯条目"""
|
||||
title: str = ""
|
||||
url: str = ""
|
||||
source: str = ""
|
||||
content: str = ""
|
||||
author: str = ""
|
||||
published_at: Optional[str] = None
|
||||
summary: str = ""
|
||||
keywords: List[str] = field(default_factory=list)
|
||||
sentiment: float = 0.0 # 情感分析得分
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewsSource:
|
||||
"""资讯源"""
|
||||
name: str = ""
|
||||
url: str = ""
|
||||
type: str = "" # rss, website, api
|
||||
enabled: bool = True
|
||||
last_fetched_at: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class NewsAnalysisState:
|
||||
"""资讯子图状态"""
|
||||
# ========== 输入 ==========
|
||||
user_query: str = "" # 用户查询
|
||||
user_id: str = "" # 用户ID
|
||||
|
||||
# 操作控制
|
||||
action: NewsAction = NewsAction.NONE
|
||||
action_params: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# 源配置
|
||||
use_follow_list: bool = False
|
||||
custom_urls: List[str] = field(default_factory=list)
|
||||
|
||||
# ========== 执行过程 ==========
|
||||
current_phase: str = "init" # init, fetching, analyzing, done
|
||||
current_source_index: int = 0
|
||||
primary_fetched: bool = False
|
||||
|
||||
# 源列表
|
||||
sources: List[NewsSource] = field(default_factory=list)
|
||||
|
||||
# 资讯条目
|
||||
news_items: List[NewsItem] = field(default_factory=list)
|
||||
|
||||
# 关键词
|
||||
extracted_keywords: List[str] = field(default_factory=list)
|
||||
|
||||
# 报告
|
||||
report_content: str = ""
|
||||
|
||||
# ========== 结果 ==========
|
||||
success: bool = False
|
||||
error_message: str = ""
|
||||
final_result: str = ""
|
||||
result_data: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# ========== 元数据 ==========
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
duration: float = 0.0
|
||||
debug_info: Dict[str, Any] = field(default_factory=dict)
|
||||
@@ -3,6 +3,19 @@ Graph 子模块
|
||||
"""
|
||||
|
||||
from .graph_builder import GraphBuilder
|
||||
from .state import MessagesState, GraphContext
|
||||
from .subgraph_builder import build_main_graph
|
||||
from .state import (
|
||||
MessagesState,
|
||||
GraphContext,
|
||||
MainGraphState,
|
||||
CurrentAction
|
||||
)
|
||||
|
||||
__all__ = ["GraphBuilder", "MessagesState", "GraphContext"]
|
||||
__all__ = [
|
||||
"GraphBuilder",
|
||||
"build_main_graph",
|
||||
"MessagesState",
|
||||
"GraphContext",
|
||||
"MainGraphState",
|
||||
"CurrentAction"
|
||||
]
|
||||
|
||||
@@ -1,25 +1,75 @@
|
||||
"""
|
||||
LangGraph 状态定义模块
|
||||
包含 MessagesState 和 GraphContext
|
||||
主图状态定义 - 扩展版
|
||||
Main Graph State Definition - Extended
|
||||
"""
|
||||
|
||||
import operator
|
||||
from typing import Annotated
|
||||
from typing_extensions import TypedDict
|
||||
from dataclasses import dataclass
|
||||
from langchain_core.messages import AnyMessage
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Dict, Any, Annotated, Sequence, TypedDict
|
||||
from dataclasses import dataclass, field
|
||||
from langgraph.graph import add_messages
|
||||
from langchain_core.messages import BaseMessage
|
||||
|
||||
|
||||
# ========== 兼容旧代码的类型 ==========
|
||||
class MessagesState(TypedDict):
|
||||
"""对话状态类型定义"""
|
||||
messages: Annotated[list[AnyMessage], operator.add]
|
||||
"""旧的MessagesState类型(保留兼容性)"""
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
||||
|
||||
|
||||
class GraphContext(TypedDict):
|
||||
"""旧的GraphContext类型(保留兼容性)"""
|
||||
llm_calls: int
|
||||
memory_context: str
|
||||
last_token_usage: dict # 本次调用的 token 使用详情
|
||||
last_elapsed_time: float # 本次调用耗时(秒)
|
||||
turns_since_last_summary: int # 距离上次生成摘要的轮数
|
||||
system_prompt: str
|
||||
|
||||
|
||||
# ========== 新的类型 ==========
|
||||
class CurrentAction(Enum):
|
||||
"""主图当前操作类型"""
|
||||
NONE = auto()
|
||||
GENERAL_CHAT = auto()
|
||||
NEWS_ANALYSIS = auto()
|
||||
DICTIONARY = auto()
|
||||
CONTACT = auto()
|
||||
|
||||
|
||||
@dataclass
|
||||
class GraphContext:
|
||||
"""图执行上下文"""
|
||||
user_id: str
|
||||
# 可扩展更多上下文信息
|
||||
class MainGraphState:
|
||||
"""
|
||||
主图状态 - 兼容旧代码 + 新增子图功能
|
||||
|
||||
包含:
|
||||
1. 旧代码的MessagesState兼容性字段
|
||||
2. 主图控制字段
|
||||
3. 子图结果占位
|
||||
4. 用户信息
|
||||
"""
|
||||
# ========== 兼容性字段(保留旧的MessagesState) ==========
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages] = field(default_factory=list)
|
||||
llm_calls: int = 0
|
||||
memory_context: str = ""
|
||||
system_prompt: str = ""
|
||||
|
||||
# ========== 主图控制字段 ==========
|
||||
user_query: str = "" # 用户当前查询
|
||||
current_action: CurrentAction = CurrentAction.NONE # 当前操作
|
||||
intent_confidence: float = 0.0 # 意图识别置信度
|
||||
|
||||
# ========== 子图结果占位 ==========
|
||||
news_result: Optional[Dict[str, Any]] = None # 资讯子图结果
|
||||
dictionary_result: Optional[Dict[str, Any]] = None # 词典子图结果
|
||||
contact_result: Optional[Dict[str, Any]] = None # 通讯录子图结果
|
||||
|
||||
# ========== 用户信息 ==========
|
||||
user_id: str = ""
|
||||
|
||||
# ========== 执行状态 ==========
|
||||
current_phase: str = "init"
|
||||
error_message: str = ""
|
||||
final_result: str = ""
|
||||
success: bool = False
|
||||
|
||||
# ========== 元数据 ==========
|
||||
start_time: Optional[str] = None
|
||||
end_time: Optional[str] = None
|
||||
debug_info: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
157
backend/app/graph/subgraph_builder.py
Normal file
157
backend/app/graph/subgraph_builder.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
子图整合主图构建器
|
||||
Subgraph Integration Main Graph Builder
|
||||
"""
|
||||
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
from typing import Dict, Any
|
||||
|
||||
from .state import MainGraphState, CurrentAction
|
||||
from ..agent_subgraphs.contact import build_contact_subgraph
|
||||
from ..agent_subgraphs.dictionary import build_dictionary_subgraph
|
||||
from ..agent_subgraphs.news_analysis import build_news_analysis_subgraph
|
||||
|
||||
|
||||
def parse_user_intent(state: MainGraphState) -> MainGraphState:
|
||||
"""
|
||||
解析用户意图节点
|
||||
|
||||
确定该路由到哪个子图
|
||||
"""
|
||||
state.current_phase = "intent_parsing"
|
||||
|
||||
# 从messages中提取用户查询(如果user_query为空)
|
||||
if not state.user_query and state.messages:
|
||||
# 获取最后一条消息的内容
|
||||
last_msg = state.messages[-1]
|
||||
state.user_query = last_msg.content
|
||||
|
||||
query_lower = state.user_query.lower()
|
||||
|
||||
# 简单的关键词匹配
|
||||
if any(keyword in query_lower for keyword in ["通讯录", "联系人", "contact", "email"]):
|
||||
state.current_action = CurrentAction.CONTACT
|
||||
state.intent_confidence = 0.9
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["词典", "单词", "翻译", "dictionary", "translate"]):
|
||||
state.current_action = CurrentAction.DICTIONARY
|
||||
state.intent_confidence = 0.9
|
||||
|
||||
elif any(keyword in query_lower for keyword in ["资讯", "新闻", "分析", "news", "report"]):
|
||||
state.current_action = CurrentAction.NEWS_ANALYSIS
|
||||
state.intent_confidence = 0.9
|
||||
|
||||
else:
|
||||
# 默认是普通聊天
|
||||
state.current_action = CurrentAction.GENERAL_CHAT
|
||||
state.intent_confidence = 0.8
|
||||
|
||||
return state
|
||||
|
||||
|
||||
def route_to_subgraph(state: MainGraphState) -> str:
|
||||
"""
|
||||
条件路由:决定路由到哪个子图
|
||||
"""
|
||||
if state.current_action == CurrentAction.NONE:
|
||||
return "general_chat"
|
||||
elif state.current_action == CurrentAction.GENERAL_CHAT:
|
||||
return "general_chat"
|
||||
elif state.current_action == CurrentAction.CONTACT:
|
||||
return "contact_subgraph"
|
||||
elif state.current_action == CurrentAction.DICTIONARY:
|
||||
return "dictionary_subgraph"
|
||||
elif state.current_action == CurrentAction.NEWS_ANALYSIS:
|
||||
return "news_analysis_subgraph"
|
||||
else:
|
||||
return "general_chat"
|
||||
|
||||
|
||||
def general_chat_node(state: MainGraphState) -> MainGraphState:
|
||||
"""
|
||||
普通聊天节点
|
||||
(目前是占位符,后续整合旧的LLM调用逻辑)
|
||||
"""
|
||||
state.current_phase = "general_chat"
|
||||
state.final_result = f"普通聊天模式:{state.user_query}"
|
||||
state.success = True
|
||||
return state
|
||||
|
||||
|
||||
def integrate_results(state: MainGraphState) -> MainGraphState:
|
||||
"""
|
||||
整合子图结果节点
|
||||
"""
|
||||
state.current_phase = "integrating"
|
||||
|
||||
# 整合通讯录子图结果
|
||||
if state.contact_result:
|
||||
state.final_result = state.contact_result.get("final_result", "")
|
||||
|
||||
# 整合词典子图结果
|
||||
elif state.dictionary_result:
|
||||
state.final_result = state.dictionary_result.get("final_result", "")
|
||||
|
||||
# 整合资讯子图结果
|
||||
elif state.news_result:
|
||||
state.final_result = state.news_result.get("final_result", "")
|
||||
|
||||
else:
|
||||
# 没有子图结果
|
||||
if not state.final_result:
|
||||
state.final_result = "处理完成"
|
||||
|
||||
state.current_phase = "done"
|
||||
return state
|
||||
|
||||
|
||||
def build_main_graph() -> StateGraph:
|
||||
"""
|
||||
构建整合了子图的主图
|
||||
|
||||
Returns:
|
||||
配置好的 StateGraph
|
||||
"""
|
||||
# 创建图
|
||||
graph = StateGraph(MainGraphState)
|
||||
|
||||
# 添加节点
|
||||
graph.add_node("parse_intent", parse_user_intent)
|
||||
graph.add_node("general_chat", general_chat_node)
|
||||
graph.add_node("integrate_results", integrate_results)
|
||||
|
||||
# 添加子图节点
|
||||
contact_graph = build_contact_subgraph()
|
||||
dictionary_graph = build_dictionary_subgraph()
|
||||
news_analysis_graph = build_news_analysis_subgraph()
|
||||
|
||||
graph.add_node("contact_subgraph", contact_graph.compile())
|
||||
graph.add_node("dictionary_subgraph", dictionary_graph.compile())
|
||||
graph.add_node("news_analysis_subgraph", news_analysis_graph.compile())
|
||||
|
||||
# 添加边
|
||||
# 从START开始
|
||||
graph.add_edge(START, "parse_intent")
|
||||
|
||||
# 从parse_intent根据条件路由
|
||||
graph.add_conditional_edges(
|
||||
"parse_intent",
|
||||
route_to_subgraph,
|
||||
{
|
||||
"general_chat": "general_chat",
|
||||
"contact_subgraph": "contact_subgraph",
|
||||
"dictionary_subgraph": "dictionary_subgraph",
|
||||
"news_analysis_subgraph": "news_analysis_subgraph",
|
||||
}
|
||||
)
|
||||
|
||||
# 从普通聊天和子图到结果整合
|
||||
graph.add_edge("general_chat", "integrate_results")
|
||||
graph.add_edge("contact_subgraph", "integrate_results")
|
||||
graph.add_edge("dictionary_subgraph", "integrate_results")
|
||||
graph.add_edge("news_analysis_subgraph", "integrate_results")
|
||||
|
||||
# 最终到END
|
||||
graph.add_edge("integrate_results", END)
|
||||
|
||||
return graph
|
||||
Reference in New Issue
Block a user