From a14744f18b786d3966d67378a110e3b071c0321d Mon Sep 17 00:00:00 2001 From: root <953994191@qq.com> Date: Sat, 25 Apr 2026 18:29:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E8=AF=8D=E5=85=B8?= =?UTF-8?q?=E5=AD=90=E5=9B=BE=EF=BC=8C=E6=B7=BB=E5=8A=A0API=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E5=92=8C=E5=89=8D=E7=AB=AF=E6=A0=BC=E5=BC=8F=E5=8C=96?= =?UTF-8?q?=E5=B7=A5=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 完善词典子图:添加生词本功能 - 创建API调用工具:dictionary_api - 添加前端格式化展示工具:result_formatter.py - 创建通讯录和资讯子图的基本结构 - 更新主图状态结构,添加MainGraphState - 添加subgraph_builder.py用于子图集成 --- .../app/agent_subgraphs/contact/__init__.py | 47 ++ backend/app/agent_subgraphs/contact/graph.py | 86 ++++ backend/app/agent_subgraphs/contact/nodes.py | 217 ++++++++++ backend/app/agent_subgraphs/contact/state.py | 104 +++++ .../agent_subgraphs/dictionary/__init__.py | 50 +++ .../agent_subgraphs/dictionary/api_client.py | 129 ++++++ .../app/agent_subgraphs/dictionary/graph.py | 71 ++++ .../app/agent_subgraphs/dictionary/nodes.py | 402 ++++++++++++++++++ .../app/agent_subgraphs/dictionary/state.py | 95 +++++ .../agent_subgraphs/news_analysis/__init__.py | 41 ++ .../agent_subgraphs/news_analysis/graph.py | 63 +++ .../agent_subgraphs/news_analysis/nodes.py | 207 +++++++++ .../agent_subgraphs/news_analysis/state.py | 89 ++++ backend/app/graph/__init__.py | 17 +- backend/app/graph/state.py | 82 +++- backend/app/graph/subgraph_builder.py | 157 +++++++ frontend/src/components/result_formatter.py | 218 ++++++++++ 17 files changed, 2057 insertions(+), 18 deletions(-) create mode 100644 backend/app/agent_subgraphs/contact/__init__.py create mode 100644 backend/app/agent_subgraphs/contact/graph.py create mode 100644 backend/app/agent_subgraphs/contact/nodes.py create mode 100644 backend/app/agent_subgraphs/contact/state.py create mode 100644 backend/app/agent_subgraphs/dictionary/__init__.py create mode 100644 backend/app/agent_subgraphs/dictionary/api_client.py create mode 100644 backend/app/agent_subgraphs/dictionary/graph.py create mode 100644 backend/app/agent_subgraphs/dictionary/nodes.py create mode 100644 backend/app/agent_subgraphs/dictionary/state.py create mode 100644 backend/app/agent_subgraphs/news_analysis/__init__.py create mode 100644 backend/app/agent_subgraphs/news_analysis/graph.py create mode 100644 backend/app/agent_subgraphs/news_analysis/nodes.py create mode 100644 backend/app/agent_subgraphs/news_analysis/state.py create mode 100644 backend/app/graph/subgraph_builder.py create mode 100644 frontend/src/components/result_formatter.py diff --git a/backend/app/agent_subgraphs/contact/__init__.py b/backend/app/agent_subgraphs/contact/__init__.py new file mode 100644 index 0000000..4cb5e87 --- /dev/null +++ b/backend/app/agent_subgraphs/contact/__init__.py @@ -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" +] diff --git a/backend/app/agent_subgraphs/contact/graph.py b/backend/app/agent_subgraphs/contact/graph.py new file mode 100644 index 0000000..80266b3 --- /dev/null +++ b/backend/app/agent_subgraphs/contact/graph.py @@ -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 diff --git a/backend/app/agent_subgraphs/contact/nodes.py b/backend/app/agent_subgraphs/contact/nodes.py new file mode 100644 index 0000000..497f73f --- /dev/null +++ b/backend/app/agent_subgraphs/contact/nodes.py @@ -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" diff --git a/backend/app/agent_subgraphs/contact/state.py b/backend/app/agent_subgraphs/contact/state.py new file mode 100644 index 0000000..9f4afe6 --- /dev/null +++ b/backend/app/agent_subgraphs/contact/state.py @@ -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) diff --git a/backend/app/agent_subgraphs/dictionary/__init__.py b/backend/app/agent_subgraphs/dictionary/__init__.py new file mode 100644 index 0000000..600f1b0 --- /dev/null +++ b/backend/app/agent_subgraphs/dictionary/__init__.py @@ -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" +] diff --git a/backend/app/agent_subgraphs/dictionary/api_client.py b/backend/app/agent_subgraphs/dictionary/api_client.py new file mode 100644 index 0000000..b2da115 --- /dev/null +++ b/backend/app/agent_subgraphs/dictionary/api_client.py @@ -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() diff --git a/backend/app/agent_subgraphs/dictionary/graph.py b/backend/app/agent_subgraphs/dictionary/graph.py new file mode 100644 index 0000000..6f3ce31 --- /dev/null +++ b/backend/app/agent_subgraphs/dictionary/graph.py @@ -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 diff --git a/backend/app/agent_subgraphs/dictionary/nodes.py b/backend/app/agent_subgraphs/dictionary/nodes.py new file mode 100644 index 0000000..301b7fb --- /dev/null +++ b/backend/app/agent_subgraphs/dictionary/nodes.py @@ -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" diff --git a/backend/app/agent_subgraphs/dictionary/state.py b/backend/app/agent_subgraphs/dictionary/state.py new file mode 100644 index 0000000..114d74d --- /dev/null +++ b/backend/app/agent_subgraphs/dictionary/state.py @@ -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) diff --git a/backend/app/agent_subgraphs/news_analysis/__init__.py b/backend/app/agent_subgraphs/news_analysis/__init__.py new file mode 100644 index 0000000..70438d7 --- /dev/null +++ b/backend/app/agent_subgraphs/news_analysis/__init__.py @@ -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" +] diff --git a/backend/app/agent_subgraphs/news_analysis/graph.py b/backend/app/agent_subgraphs/news_analysis/graph.py new file mode 100644 index 0000000..9dfbf90 --- /dev/null +++ b/backend/app/agent_subgraphs/news_analysis/graph.py @@ -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 diff --git a/backend/app/agent_subgraphs/news_analysis/nodes.py b/backend/app/agent_subgraphs/news_analysis/nodes.py new file mode 100644 index 0000000..6b62ecf --- /dev/null +++ b/backend/app/agent_subgraphs/news_analysis/nodes.py @@ -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" diff --git a/backend/app/agent_subgraphs/news_analysis/state.py b/backend/app/agent_subgraphs/news_analysis/state.py new file mode 100644 index 0000000..5820506 --- /dev/null +++ b/backend/app/agent_subgraphs/news_analysis/state.py @@ -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) diff --git a/backend/app/graph/__init__.py b/backend/app/graph/__init__.py index a4c7afc..eab1487 100644 --- a/backend/app/graph/__init__.py +++ b/backend/app/graph/__init__.py @@ -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" +] diff --git a/backend/app/graph/state.py b/backend/app/graph/state.py index 2fd214e..db9dc0c 100644 --- a/backend/app/graph/state.py +++ b/backend/app/graph/state.py @@ -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) diff --git a/backend/app/graph/subgraph_builder.py b/backend/app/graph/subgraph_builder.py new file mode 100644 index 0000000..808a778 --- /dev/null +++ b/backend/app/graph/subgraph_builder.py @@ -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 diff --git a/frontend/src/components/result_formatter.py b/frontend/src/components/result_formatter.py new file mode 100644 index 0000000..a9affe6 --- /dev/null +++ b/frontend/src/components/result_formatter.py @@ -0,0 +1,218 @@ +""" +前端格式化展示工具 +Frontend Formatter and Display Utilities +""" + +from typing import Dict, Any, Optional, List +from dataclasses import dataclass + + +@dataclass +class FormattedResult: + """格式化后的结果结构""" + # 类型:"text", "card", "list", "table" + type: str + + # 内容 + title: str = "" + content: str = "" + + # 卡片列表 + items: List[Dict[str, Any]] = None + + # 元数据 + metadata: Dict[str, Any] = None + + def __post_init__(self): + if self.items is None: + self.items = [] + if self.metadata is None: + self.metadata = {} + + +class DictionaryResultFormatter: + """ + 词典子图结果格式化器 + """ + + @staticmethod + def format_word_entry(word: str, phonetic: str, part_of_speech: str, + definitions: List[str], examples: List[str]) -> FormattedResult: + """ + 格式化单词查询结果 + """ + result = FormattedResult(type="card", title=f"📚 {word}") + + content = [] + if phonetic: + content.append(f"🔊 {phonetic}") + content.append(f"🏷️ {part_of_speech}") + content.append("") + content.append("📖 释义:") + for i, definition in enumerate(definitions, 1): + content.append(f" {i}. {definition}") + + if examples: + content.append("") + content.append("💡 例句:") + for example in examples: + content.append(f" \"{example}\"") + + result.content = "\n".join(content) + + # 添加快捷按钮建议 + result.metadata["suggestions"] = [ + "添加到生词本", + "查看更多例句", + "发音练习" + ] + + return result + + @staticmethod + def format_translation(source_text: str, translated_text: str, confidence: float) -> FormattedResult: + """ + 格式化翻译结果 + """ + result = FormattedResult(type="card", title="🔄 翻译结果") + + content = [] + content.append(f"原文:{source_text}") + content.append(f"译文:{translated_text}") + content.append("") + content.append(f"🎯 置信度:{confidence:.0%}") + + result.content = "\n".join(content) + + # 添加快捷按钮建议 + result.metadata["suggestions"] = [ + "复制译文", + "重新翻译", + "发音练习" + ] + + return result + + @staticmethod + def format_daily_word(word: str, phonetic: str, part_of_speech: str, + definitions: List[str], examples: List[str]) -> FormattedResult: + """ + 格式化每日一词结果 + """ + result = FormattedResult(type="card", title="🌟 每日一词") + + content = [] + content.append(f"📚 {word}") + if phonetic: + content.append(f"🔊 {phonetic}") + content.append(f"🏷️ {part_of_speech}") + content.append("") + + if definitions: + content.append("📖 释义:") + for i, definition in enumerate(definitions, 1): + content.append(f" {i}. {definition}") + + if examples: + content.append("") + content.append("💡 例句:") + for example in examples: + content.append(f" \"{example}\"") + + result.content = "\n".join(content) + + # 添加快捷按钮建议 + result.metadata["suggestions"] = [ + "添加到生词本", + "用这个词造句", + "查看更多单词" + ] + + return result + + @staticmethod + def format_extracted_terms(terms: List[Dict[str, Any]]) -> FormattedResult: + """ + 格式化提取的术语结果 + """ + result = FormattedResult(type="list", title="📋 提取的术语") + result.items = terms + + content = [] + for i, term in enumerate(terms, 1): + content.append(f"{i}. {term.get('term', '')}") + content.append(f" 【{term.get('type', '')}】") + content.append(f" {term.get('definition', '')}") + if term.get('confidence'): + content.append(f" 🎯 {term.get('confidence', 0.0):.0%}") + content.append("") + + result.content = "\n".join(content) + + return result + + @staticmethod + def format_word_book(words: List[Dict[str, Any]]) -> FormattedResult: + """ + 格式化生词本结果 + """ + result = FormattedResult(type="list", title="📚 我的生词本") + result.items = words + + if words: + content = [] + content.append(f"共 {len(words)} 个单词") + content.append("") + for i, word in enumerate(words, 1): + word_text = word.get('word', '') + added_at = word.get('added_at', '') + if added_at: + content.append(f"{i}. {word_text} (添加于:{added_at})") + else: + content.append(f"{i}. {word_text}") + + result.content = "\n".join(content) + else: + result.content = "生词本为空\n\n💡 提示:查询单词后可添加到生词本" + + # 添加快捷按钮建议 + result.metadata["suggestions"] = [ + "复习单词", + "清空生词本", + "导出单词" + ] + + return result + + +class ContactResultFormatter: + """ + 通讯录子图结果格式化器 + """ + # TODO: 实现通讯录结果格式化 + pass + + +class NewsResultFormatter: + """ + 资讯子图结果格式化器 + """ + # TODO: 实现资讯结果格式化 + pass + + +# 快捷工具 +def format_dictionary_output(output: str) -> str: + """ + 快速格式化词典输出 + 把纯文本包装成更好的展示格式 + """ + return output # 暂时保持原样,后续可以加入更多格式处理 + + +# 导出快捷工具 +formatter = { + "dictionary": DictionaryResultFormatter(), + "contact": ContactResultFormatter(), + "news": NewsResultFormatter() +}