refactor: 重构目录结构 - 简化层级
Some checks failed
构建并部署 AI Agent 服务 / deploy (push) Has been cancelled

This commit is contained in:
2026-04-29 12:52:41 +08:00
parent 223d1c9afd
commit ef5113bffb
54 changed files with 42 additions and 1819 deletions

View File

@@ -0,0 +1,391 @@
# 通讯录子图 (Contact Subgraph)
该子图负责处理通讯录管理、邮件读取与发送等功能,基于 LangGraph 状态机编排多阶段工作流,支持联系人 CRUD、IMAP 邮箱绑定、邮件审核发送等核心能力。子图设计遵循"安全优先、审核强制、隐私保护"原则,通过敏感信息加密和人工审核保障数据安全。
> **使用公共工具**意图理解、人工审核、格式化输出、检查点持久化、条件路由、LLM 调用、数据库工具、状态基类
---
## 🎯 核心架构
### 技术栈
| 层级 | 组件 | 说明 |
|:-----|:-----|:-----|
| **编排框架** | LangGraph StateGraph | 状态机驱动的子图工作流编排,支持中断恢复 |
| **LLM 服务** | 智谱 AI / DeepSeek API | 意图理解、邮件草稿生成、联系人信息提取(使用公共 LLM 工具) |
| **关系存储** | PostgreSQL | 联系人、邮箱配置持久化(使用公共数据库工具) |
| **邮件协议** | imaplib / smtplib | 邮件读取与发送 |
| **加密存储** | cryptography | 邮箱密码等敏感信息加密 |
### 子图分层架构
```
┌─────────────────────────────────────────────────────────────────┐
│ 主图 (Main Graph) │
└──────────────────────────────┬──────────────────────────────────┘
│ 状态映射 / 结果聚合
┌─────────────────────────────────────────────────────────────────┐
│ 通讯录子图接口层 │
│ - 状态转换:主状态 ↔ 子图状态(使用公共状态基类) │
│ - 错误传播与优雅降级 │
└──────────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 工作流编排层 │
│ - 节点调度与条件路由(使用公共路由工具) │
│ - 人工审核节点暂停/恢复管理(使用公共审核工具) │
│ - 状态持久化与检查点(使用公共检查点工具) │
└──────────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 节点层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │意图理解 │ │联系人CRUD│ │邮件读取 │ │草稿生成 │ │人工审核│ │
│ │(公共工具)│ │ │ │ │ │ │ │(公共) │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │邮件发送 │ │智能嗅探 │ │格式输出 │ │
│ │ │ │ │ │(公共) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────┬──────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 工具层 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │数据库工具│ │IMAP工具 │ │SMTP工具 │ │加密工具 │ │
│ │(公共) │ │ │ │ │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 数据流总览
通讯录子图根据意图类型分支执行,关键操作前强制人工审核。
```
用户请求
┌─────────────┐
│ 意图理解 │ ← 使用公共意图理解工具
└──────┬──────┘
├──────────┬──────────┬──────────┬──────────┐
▼ ▼ ▼ ▼ ▼
联系人CRUD 邮件读取 邮件发送 智能嗅探 列表查询
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
数据库操作 IMAP读取 草稿生成 信息提取 结果展示
│ │ │ │ │
│ │ ▼ │ │
│ │ 人工审核 │ │
│ │ (公共) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ SMTP发送 │ │
│ │ │ │ │
└──────────┴──────────┴──────────┴──────────┘
格式输出 ← 使用公共格式化工具
返回主图
```
---
## 📂 模块与文件结构
```
app/contact/
├── __init__.py
├── graph.py # 子图构建入口,定义状态图与路由
├── state.py # 子图状态定义(继承公共状态基类)
├── nodes/ # 节点实现
│ ├── __init__.py
│ ├── crud.py # 联系人CRUD节点
│ ├── email_read.py # 邮件读取节点
│ ├── draft.py # 邮件草稿生成节点
│ ├── email_send.py # 邮件发送节点
│ └── sniff.py # 智能嗅探节点
├── tools/ # 子图特有工具集
│ ├── imap.py # IMAP邮件读取工具
│ ├── smtp.py # SMTP邮件发送工具
│ └── crypto.py # 敏感信息加密工具
└── persistence/ # (使用公共检查点工具,无需单独实现)
```
> **注意**:以下模块使用公共工具,无需单独实现:
> - 意图理解节点 → 使用 `agent_subgraphs.common.intent`
> - 人工审核节点 → 使用 `agent_subgraphs.common.human_loop`
> - 格式输出节点 → 使用 `agent_subgraphs.common.format`
> - 检查点持久化 → 使用 `agent_subgraphs.common.checkpoint`
> - 条件路由 → 使用 `agent_subgraphs.common.routing`
> - LLM 调用 → 使用 `agent_subgraphs.common.llm`
> - 数据库操作 → 使用 `agent_subgraphs.common.db`
---
## 🎯 演进路线与核心机制
### Level 1基础联系人管理
**核心机制**:联系人 CRUD 操作 + 基础列表查询。
- 使用 LLM 解析用户请求,提取联系人信息(姓名、电话、邮箱等)。
- 数据库存储联系人信息,支持增删改查。
- 支持按姓名模糊查询和列表展示。
**适用场景**:保存联系人、查询电话、修改信息等基础操作。
**实现指引**:意图理解节点识别 CRUD 类型,路由到对应节点。
### Level 2邮件读取与基础发送
**核心机制**IMAP 邮箱绑定 + 邮件列表读取 + 简单发送。
- 支持配置多个 IMAP/SMTP 邮箱账户。
- 读取收件箱邮件列表,展示主题、发件人、时间。
- 生成邮件草稿,人工审核后发送。
**适用场景**:查收邮件、发送简单邮件。
**实现指引**:邮箱密码加密存储,发送前强制人工审核。
### Level 3智能嗅探与上下文感知
**核心机制**:对话中自动识别联系人信息,主动询问是否保存。
- 在任意对话中监听"人名+联系方式"模式。
- 识别到潜在联系人信息时,主动询问用户是否保存。
- 支持确认后自动创建联系人记录。
**适用场景**:日常对话中自然保存联系人。
**实现指引**:与主图协作,在对话节点后插入嗅探检查点。
### Level 4邮件智能处理
**核心机制**:邮件内容理解 + 智能回复建议 + 批量处理。
- 读取邮件全文,理解内容意图。
- 基于邮件内容生成智能回复草稿。
- 支持邮件分类、标记、归档操作。
**适用场景**:邮件管理、智能回复。
**实现指引**:利用 LLM 进行邮件内容理解和回复生成。
### Level 5通讯录智能助理
**核心机制**:联系人关系图谱 + 智能推荐 + 多模态交互。
- 构建联系人关系网络,识别社交圈子。
- 基于历史交互推荐联系人。
- 支持名片扫描、语音输入等多模态交互。
**适用场景**:深度联系人管理、智能社交助理。
---
## 🔧 核心组件详解
### 1. 意图理解节点
**职责**:接收用户原始请求,区分联系人 CRUD、邮件读取、邮件发送、智能嗅探等意图类型。
**输入**:用户自然语言请求。
**输出**
- `intent_type`意图类别枚举contact_crud / email_read / email_send / sniff / list
- `extracted_info`:提取的关键信息(联系人姓名、邮件主题等)。
**实现要点**
- 使用 LLM 进行少样本分类,输出结构化 JSON。
- 关键词匹配兜底(如"保存"、"添加" → contact_crud
### 2. 联系人 CRUD 节点
**职责**:执行联系人的增删改查操作,与数据库交互。
**输入**:意图类型、提取的联系人信息。
**输出**
- `operation_result`:操作结果(成功/失败)。
- `contact_data`:操作后的联系人数据。
**实现要点**
- 使用 SQLAlchemy 与 PostgreSQL 交互。
- 支持部分字段更新(如只修改电话)。
- 删除操作前二次确认。
### 3. 邮件读取节点
**职责**:通过 IMAP 协议连接邮箱,读取邮件列表或单封邮件详情。
**输入**:邮箱配置、读取指令(列表/详情)。
**输出**
- `email_list`:邮件列表(主题、发件人、时间)。
- `email_content`:单封邮件详情(如需要)。
**实现要点**
- 支持配置多个邮箱账户。
- 密码使用 cryptography 加密存储。
- 分页读取邮件列表,避免一次性加载过多。
### 4. 邮件草稿生成节点
**职责**:根据用户指令生成邮件草稿,包含收件人、主题、正文。
**输入**:用户发送邮件的指令、上下文。
**输出**
- `draft_recipient`:收件人邮箱。
- `draft_subject`:邮件主题。
- `draft_body`:邮件正文。
**实现要点**
- 使用 LLM 生成自然流畅的邮件内容。
- 从通讯录智能匹配收件人邮箱。
- 支持多语言邮件生成。
### 5. 人工审核节点
**职责**:在邮件发送、联系人删除等关键操作前挂起,等待用户确认。
**输入**:待审核内容(邮件草稿、删除确认等)。
**输出**
- `user_approved`:是否通过审核。
- `user_modification`:用户修改内容(如有)。
**实现要点**
- 使用 LangGraph `interrupt` 机制实现挂起。
- 支持用户修改草稿后再次审核。
- 超时自动取消操作。
### 6. 邮件发送节点
**职责**:审核通过后,通过 SMTP 协议发送邮件。
**输入**:审核通过的邮件草稿。
**输出**
- `send_result`:发送结果。
- `sent_time`:发送时间。
**实现要点**
- 使用 smtplib 发送邮件。
- 支持抄送、密送。
- 发送失败重试机制。
### 7. 智能嗅探节点
**职责**:在对话中检测联系人信息,主动询问是否保存。
**输入**:用户对话历史。
**输出**
- `detected_contact`:检测到的潜在联系人信息。
- `should_ask`:是否应该询问用户。
**实现要点**
- 使用 NER 识别实体(人名、电话、邮箱)。
- 与已有联系人去重。
- 询问用户确认后自动保存。
---
## 🔀 条件路由详解
### 入口路由:意图分支
- **位置**:意图理解节点之后。
- **条件**
- `intent_type == "contact_crud"` → 联系人 CRUD 节点。
- `intent_type == "email_read"` → 邮件读取节点。
- `intent_type == "email_send"` → 草稿生成节点。
- `intent_type == "sniff"` → 智能嗅探节点。
- `intent_type == "list"` → 列表查询节点。
### 审核路由
- **位置**:人工审核节点之后。
- **条件**
- `user_approved == True` → 执行操作(发送/删除等)。
- `user_approved == False``user_modification` 存在 → 返回草稿生成节点。
- `user_approved == False` 且无修改 → 取消操作。
### 嗅探路由
- **位置**:智能嗅探节点之后。
- **条件**
- `should_ask == True` → 询问用户确认。
- `should_ask == False` → 直接结束,不打扰用户。
---
## 📊 状态设计
### 状态结构概览
| 分组 | 字段 | 类型 | 说明 |
|:-----|:-----|:-----|:-----|
| **输入** | `user_input` | `str` | 用户原始请求 |
| **意图** | `intent_type` | `str` | 意图类别 |
| | `extracted_info` | `dict` | 提取的关键信息 |
| **联系人** | `contact_data` | `dict` | 联系人数据 |
| | `contact_list` | `list[dict]` | 联系人列表 |
| **邮件** | `email_config` | `dict` | 邮箱配置 |
| | `email_list` | `list[dict]` | 邮件列表 |
| | `draft_recipient` | `str` | 草稿收件人 |
| | `draft_subject` | `str` | 草稿主题 |
| | `draft_body` | `str` | 草稿正文 |
| **审核** | `pending_action` | `dict` | 待审核操作 |
| | `user_approved` | `bool` | 是否通过审核 |
| | `user_modification` | `dict` | 用户修改 |
| **控制流** | `current_phase` | `str` | 当前执行阶段 |
| | `next_node` | `str` | 下一节点名称 |
| | `interrupt_point` | `str` | 中断点标识 |
| **输出** | `final_result` | `str` | 最终结果 |
---
## 🔄 工作流程与中断恢复
### 邮件发送流程
| 步骤 | 节点 | 人工干预 |
|:-----|:-----|:---------|
| 1 | 意图理解 | 否 |
| 2 | 草稿生成 | 否 |
| 3 | 人工审核 | **是** |
| 4 | 邮件发送 | 否 |
| 5 | 格式输出 | 否 |
### 联系人保存流程
| 步骤 | 节点 | 人工干预 |
|:-----|:-----|:---------|
| 1 | 意图理解 | 否 |
| 2 | 联系人 CRUD | 否 |
| 3 | 格式输出 | 否 |
### 智能嗅探流程
| 步骤 | 节点 | 人工干预 |
|:-----|:-----|:---------|
| 1 | 智能嗅探 | 否 |
| 2 | 询问确认 | **是** |
| 3 | 联系人 CRUD | 否 |
### 中断恢复机制
子图支持在人工审核节点中断,恢复时从审核点继续执行。

View File

@@ -0,0 +1,52 @@
"""
通讯录子图 - 完善版
Contact Subgraph Module - Complete
"""
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
)
from .api_client import contact_api, ContactAPIClient
__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",
# API
"contact_api",
"ContactAPIClient"
]

View File

@@ -0,0 +1,286 @@
"""
通讯录子图 API 调用工具
支持模拟数据和真实数据库两种模式
"""
from typing import Dict, Any, Optional, List
from datetime import datetime
from dataclasses import dataclass
from .state import Contact, Email
# ========== 模拟数据(保留作为备选)==========
# 模拟数据库
MOCK_CONTACTS_DB = {}
MOCK_EMAILS_DB = []
@dataclass
class ContactAPIClient:
"""
通讯录 API 客户端 - 支持真实数据库和模拟模式
使用方式:
1. 真实数据库模式:传入 conn 参数
2. 模拟模式:不传入 conn或 conn 为 None
"""
def __init__(self, conn=None):
"""
初始化
Args:
conn: 数据库连接(来自 checkpointer.conn为 None 时使用模拟模式
"""
self.conn = conn
self._use_db = conn is not None
if self._use_db:
try:
from ...db.models import ContactRepository, ContactEntity
self._repo = ContactRepository(conn)
except Exception as e:
print(f"Repository 初始化失败,回退到模拟模式: {e}")
self._use_db = False
self._repo = None
# ========== 真实数据库方法 ==========
async def list_contacts_db(self, user_id: str = "default") -> List[Contact]:
"""真实数据库:获取联系人列表"""
if not self._repo:
return await self.list_contacts_mock(user_id)
entities = await self._repo.list_by_user(user_id)
return [
Contact(
id=e.id,
name=e.name,
phone=e.phone,
email=e.email,
company=e.company,
position=e.position,
created_at=e.created_at
)
for e in entities
]
async def add_contact_db(self, user_id: str, contact: Contact) -> bool:
"""真实数据库:添加联系人"""
if not self._repo:
return await self.save_contact_mock(user_id, contact)
from ...db.models import ContactEntity
entity = ContactEntity(
user_id=user_id,
name=contact.name,
phone=contact.phone,
email=contact.email,
company=contact.company,
position=contact.position,
created_at=contact.created_at or datetime.now().isoformat()
)
await self._repo.insert(entity)
return True
# ========== 模拟数据方法(保留)==========
def list_contacts_mock(self, user_id: str = "default") -> List[Contact]:
"""模拟查询联系人列表"""
if user_id not in MOCK_CONTACTS_DB:
# 初始化一些示例数据
MOCK_CONTACTS_DB[user_id] = [
Contact(
id="1",
name="张三",
phone="13800138000",
email="zhangsan@example.com",
company="科技公司",
position="工程师",
created_at=datetime.now().isoformat()
),
Contact(
id="2",
name="李四",
phone="13900139000",
email="lisi@example.com",
company="贸易公司",
position="经理",
created_at=datetime.now().isoformat()
),
Contact(
id="3",
name="王五",
phone="13700137000",
email="wangwu@example.com",
company="咨询公司",
position="顾问",
created_at=datetime.now().isoformat()
),
]
return MOCK_CONTACTS_DB[user_id]
def extract_contact_info_mock(self, query: str) -> Optional[Dict[str, Any]]:
"""模拟从查询中提取联系人信息"""
import re
# 提取邮箱
email_match = re.search(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', query)
# 提取手机号
phone_match = re.search(r'1[3-9]\d{9}', query)
# 提取姓名(简单匹配)
if any(keyword in query for keyword in ["添加", "add"]):
name = "未知"
clean_query = query
if email_match:
clean_query = clean_query.replace(email_match.group(), "")
if phone_match:
clean_query = clean_query.replace(phone_match.group(), "")
clean_query = clean_query.replace("添加", "").replace("add", "").replace("联系人", "").strip()
if clean_query:
name = clean_query
return {
"name": name,
"phone": phone_match.group() if phone_match else "",
"email": email_match.group() if email_match else "",
"created_at": datetime.now().isoformat()
}
return None
def save_contact_mock(self, user_id: str, contact: Contact) -> bool:
"""模拟保存联系人"""
if user_id not in MOCK_CONTACTS_DB:
MOCK_CONTACTS_DB[user_id] = []
if not contact.id:
contact.id = str(len(MOCK_CONTACTS_DB[user_id]) + 1)
MOCK_CONTACTS_DB[user_id].append(contact)
return True
def list_emails_mock(self) -> List[Email]:
"""模拟查询邮件列表"""
global MOCK_EMAILS_DB
if not MOCK_EMAILS_DB:
MOCK_EMAILS_DB = [
Email(
id="1",
subject="会议邀请AI 技术分享",
sender="admin@example.com",
recipients=["user@example.com"],
date=datetime.now().isoformat(),
body="你好,下周一将举办 AI 技术分享会,欢迎参加。"
),
Email(
id="2",
subject="项目进度更新",
sender="manager@example.com",
recipients=["user@example.com"],
date=datetime.now().isoformat(),
body="项目进度良好,继续保持。"
),
]
return MOCK_EMAILS_DB
def generate_email_draft_mock(self, query: str) -> Dict[str, str]:
"""模拟生成邮件草稿"""
return {
"subject": f"Re: {query}",
"recipient": "recipient@example.com",
"body": "你好,\n\n这是一封自动生成的邮件草稿。\n\n此致,\n你的助手"
}
def send_email_mock(self, recipient: str, subject: str, body: str) -> Dict[str, Any]:
"""模拟发送邮件"""
global MOCK_EMAILS_DB
MOCK_EMAILS_DB.append(
Email(
id=str(len(MOCK_EMAILS_DB) + 1),
subject=subject,
sender="me@example.com",
recipients=[recipient],
date=datetime.now().isoformat(),
body=body
)
)
return {
"success": True,
"message": "邮件发送成功"
}
def sniff_contacts_mock(self, query: str) -> Dict[str, Any]:
"""模拟智能嗅探联系人"""
import re
emails = re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', query)
phones = re.findall(r'1[3-9]\d{9}', query)
contacts = []
for i, email in enumerate(emails):
contacts.append({
"name": f"联系人{i+1}",
"email": email,
"phone": phones[i] if i < len(phones) else ""
})
return {
"contacts": contacts,
"count": len(contacts),
"suggestion": "是否添加这些联系人?"
}
# ========== 公共方法(自动选择模式)==========
async def list_contacts(self, user_id: str = "default") -> List[Contact]:
"""获取联系人列表(自动选择数据库或模拟模式)"""
if self._use_db:
return await self.list_contacts_db(user_id)
return self.list_contacts_mock(user_id)
async def add_contact(self, user_id: str, contact: Contact) -> bool:
"""添加联系人(自动选择数据库或模拟模式)"""
if self._use_db:
return await self.add_contact_db(user_id, contact)
return self.save_contact_mock(user_id, contact)
async def list_emails(self, user_id: str = "default") -> List[Email]:
"""查询邮件列表(目前用模拟)"""
return self.list_emails_mock()
async def generate_email_draft(self, query: str) -> Dict[str, str]:
"""生成邮件草稿(目前用模拟)"""
return self.generate_email_draft_mock(query)
async def send_email(self, user_id: str, recipient: str, subject: str, body: str) -> bool:
"""发送邮件(目前用模拟)"""
result = self.send_email_mock(recipient, subject, body)
return result.get("success", False)
async def sniff_contacts(self, query: str) -> List[Contact]:
"""智能嗅探联系人(目前用模拟)"""
result = self.sniff_contacts_mock(query)
contact_dicts = result.get("contacts", [])
return [
Contact(
id=str(i+1),
name=c.get("name", ""),
phone=c.get("phone", ""),
email=c.get("email", ""),
company="",
position="",
created_at=datetime.now().isoformat()
)
for i, c in enumerate(contact_dicts)
]
# 全局实例(模拟模式,保留向后兼容)
contact_api = ContactAPIClient()

View File

@@ -0,0 +1,109 @@
"""
通讯录子图构建器
Contact Subgraph Builder
支持 API 注入的工厂模式
"""
from app.main_graph.graph import StateGraph, START, END
from .state import ContactState
from .nodes import create_contact_nodes
def build_contact_subgraph(contact_api=None):
"""
构建通讯录子图(工厂模式)
Args:
contact_api: 可选的 ContactAPIClient 实例(支持真实数据库或模拟模式)
不传入则使用默认模拟 API向后兼容
Returns:
配置好的 StateGraph
"""
# 创建节点(传入 API
nodes = create_contact_nodes(contact_api) if contact_api else None
# 如果没有传入 API使用向后兼容的导入
if nodes is None:
from .nodes import (
parse_intent,
list_contacts,
add_contact,
list_emails,
generate_email_draft,
human_review,
send_email,
sniff_contacts,
format_result,
should_continue
)
else:
parse_intent = nodes["parse_intent"]
list_contacts = nodes["list_contacts"]
add_contact = nodes["add_contact"]
list_emails = nodes["list_emails"]
generate_email_draft = nodes["generate_email_draft"]
human_review = nodes["human_review"]
send_email = nodes["send_email"]
sniff_contacts = nodes["sniff_contacts"]
format_result = nodes["format_result"]
should_continue = nodes["should_continue"]
# 创建图
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

View File

@@ -0,0 +1,278 @@
"""
通讯录子图节点 - 使用公共工具版本
Contact Subgraph Nodes - Using Common Tools
支持 async 和 API 注入
"""
from typing import Dict, Any
from datetime import datetime
# 公共工具
from ..common import MarkdownFormatter
from .state import ContactState, ContactAction, Contact, Email
from .api_client import ContactAPIClient
# 模拟联系人数据库(临时存储,保留作为备选)
CONTACT_DB = {}
def create_contact_nodes(contact_api: ContactAPIClient):
"""
创建通讯录子图节点工厂函数
Args:
contact_api: 已初始化的 ContactAPIClient支持真实数据库或模拟模式
Returns:
节点函数字典
"""
async def parse_intent(state: ContactState) -> ContactState:
"""
解析用户意图节点
"""
query_lower = state.user_query.lower()
if any(keyword in query_lower for keyword in ["添加", "add", "新建", "save"]):
state.action = ContactAction.CONTACT_ADD
elif 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 ["邮件", "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
async def list_contacts(state: ContactState) -> ContactState:
"""
列出联系人节点
"""
state.current_phase = "executing"
# 使用 API 客户端async
contacts = await contact_api.list_contacts(state.user_id)
state.contacts = contacts
return state
async def add_contact(state: ContactState) -> ContactState:
"""
添加联系人节点
"""
state.current_phase = "executing"
# 使用 API 客户端(简化添加,实际项目应解析用户输入)
new_contact = Contact(
id=str(len(CONTACT_DB) + 1),
name="新联系人",
email="new@example.com",
phone="13800000000",
created_at=datetime.now().isoformat()
)
# 保存到数据库
await contact_api.add_contact(state.user_id, new_contact)
state.current_contact = new_contact
return state
async def list_emails(state: ContactState) -> ContactState:
"""
列出邮件节点
"""
state.current_phase = "executing"
# 使用 API 客户端async
emails = await contact_api.list_emails(state.user_id)
state.emails = emails
return state
async def generate_email_draft(state: ContactState) -> ContactState:
"""
生成邮件草稿节点
"""
state.current_phase = "executing"
# 使用 API 客户端async
draft = await contact_api.generate_email_draft(state.user_query)
state.draft_recipient = draft.get("recipient", "recipient@example.com")
state.draft_subject = draft.get("subject", "邮件主题")
state.draft_body = draft.get("body", "邮件正文")
return state
async def sniff_contacts(state: ContactState) -> ContactState:
"""
嗅探联系人节点
"""
state.current_phase = "executing"
# 使用 API 客户端async
contacts = await contact_api.sniff_contacts(state.user_query)
state.sniffed_contacts = contacts
return state
async def format_result(state: ContactState) -> ContactState:
"""
格式化结果节点(使用公共工具)
"""
state.current_phase = "formatting"
md = MarkdownFormatter()
output_lines = []
output_lines.append("┌───────────────────────────────────┐")
output_lines.append("│ 📇 通讯录助手 │")
output_lines.append("└───────────────────────────────────┘")
output_lines.append("")
if state.action == ContactAction.CONTACT_LIST and state.contacts:
output_lines.append(md.heading("📇 联系人列表", 2))
output_lines.append("")
contact_data = [
{"姓名": c.name, "邮箱": c.email, "电话": c.phone or "-"}
for c in state.contacts
]
output_lines.append(md.table(contact_data))
elif state.action == ContactAction.EMAIL_LIST and state.emails:
output_lines.append(md.heading("📬 邮件列表", 2))
output_lines.append("")
# 兼容两种 date 格式
email_data = []
for e in state.emails:
date_str = e.date
if hasattr(e, 'received_at') and e.received_at:
try:
date_str = e.received_at.strftime('%Y-%m-%d %H:%M')
except:
pass
email_data.append({
"发件人": e.sender,
"主题": e.subject,
"时间": date_str
})
output_lines.append(md.table(email_data))
elif state.action == ContactAction.EMAIL_SEND and state.draft_subject:
output_lines.append(md.heading("📝 邮件草稿", 2))
output_lines.append("")
output_lines.append(f"**收件人**: {state.draft_recipient}")
output_lines.append(f"**主题**: {state.draft_subject}")
output_lines.append("")
output_lines.append(md.quote(state.draft_body))
elif state.action == ContactAction.SNIFF_CONTACTS and state.sniffed_contacts:
output_lines.append(md.heading("🔍 发现的联系人", 2))
output_lines.append("")
contact_data = [
{"姓名": c.name, "邮箱": c.email}
for c in state.sniffed_contacts
]
output_lines.append(md.table(contact_data))
else:
output_lines.append(md.heading("✨ 操作完成", 2))
output_lines.append("您的请求已处理。")
# 页脚提示
output_lines.append("")
output_lines.append("---")
output_lines.append("💡 提示:您可以继续查询联系人、查看邮件,或者生成邮件草稿!")
state.final_result = "\n".join(output_lines)
state.success = True
state.current_phase = "completed"
return state
async def human_review(state: ContactState) -> ContactState:
"""
人工审核节点(用于邮件草稿)
"""
state.current_phase = "reviewing"
# 标记需要审核,等待用户决定
state.needs_approval = True
return state
async def send_email(state: ContactState) -> ContactState:
"""
发送邮件节点
"""
state.current_phase = "executing"
# 使用 API 客户端发送邮件async
success = await contact_api.send_email(
state.user_id,
state.draft_recipient,
state.draft_subject,
state.draft_body
)
state.success = success
return state
def should_continue(state: ContactState) -> str:
"""
条件路由函数:根据 action 和状态决定下一个节点
"""
# 如果是从 human_review 来的,根据审核状态决定
if state.current_phase == "reviewing":
if state.needs_approval:
# 这里会等待用户操作,实际运行时通过 checkpointer 或后端 API 处理
return "format_result"
else:
return "send_email"
# 普通路由
action = state.action
if action == ContactAction.CONTACT_LIST:
return "list_contacts"
elif action == ContactAction.CONTACT_ADD:
return "add_contact"
elif action == ContactAction.EMAIL_LIST:
return "list_emails"
elif action == ContactAction.EMAIL_SEND:
return "generate_email_draft"
elif action == ContactAction.SNIFF_CONTACTS:
return "sniff_contacts"
else:
return "format_result"
# 返回节点字典
return {
"parse_intent": parse_intent,
"list_contacts": list_contacts,
"add_contact": add_contact,
"list_emails": list_emails,
"generate_email_draft": generate_email_draft,
"sniff_contacts": sniff_contacts,
"format_result": format_result,
"human_review": human_review,
"send_email": send_email,
"should_continue": should_continue
}
# ========== 向后兼容的全局版本(使用模拟 API ==========
from .api_client import contact_api as _default_contact_api
# 创建默认节点(用模拟 API保持向后兼容
_default_nodes = create_contact_nodes(_default_contact_api)
# 导出默认节点
parse_intent = _default_nodes["parse_intent"]
list_contacts = _default_nodes["list_contacts"]
add_contact = _default_nodes["add_contact"]
list_emails = _default_nodes["list_emails"]
generate_email_draft = _default_nodes["generate_email_draft"]
sniff_contacts = _default_nodes["sniff_contacts"]
format_result = _default_nodes["format_result"]
human_review = _default_nodes["human_review"]
send_email = _default_nodes["send_email"]
should_continue = _default_nodes["should_continue"]

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