zhouhui.jiang

初始化

No preview for this file type
1 +# 默认忽略的文件
2 +/shelf/
3 +/workspace.xml
4 +# 基于编辑器的 HTTP 客户端请求
5 +/httpRequests/
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<module type="PYTHON_MODULE" version="4">
3 + <component name="NewModuleRootManager">
4 + <content url="file://$MODULE_DIR$">
5 + <excludeFolder url="file://$MODULE_DIR$/venv" />
6 + </content>
7 + <orderEntry type="jdk" jdkName="Python 3.12 (Test_LangGraph)" jdkType="Python SDK" />
8 + <orderEntry type="sourceFolder" forTests="false" />
9 + </component>
10 +</module>
...\ No newline at end of file ...\ No newline at end of file
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<project version="4">
3 + <component name="AI Toolkit Settings">
4 + <option name="importsOfInterestPresent" value="true" />
5 + </component>
6 +</project>
...\ No newline at end of file ...\ No newline at end of file
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<project version="4">
3 + <component name="Encoding">
4 + <file url="file://$PROJECT_DIR$/langgraph.json" charset="UTF-8" />
5 + <file url="mock:///Python 控制台.py" charset="UTF-8" />
6 + </component>
7 +</project>
...\ No newline at end of file ...\ No newline at end of file
1 +<component name="InspectionProjectProfileManager">
2 + <profile version="1.0">
3 + <option name="myName" value="Project Default" />
4 + <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
5 + </profile>
6 +</component>
...\ No newline at end of file ...\ No newline at end of file
1 +<component name="InspectionProjectProfileManager">
2 + <settings>
3 + <option name="USE_PROJECT_PROFILE" value="false" />
4 + <version value="1.0" />
5 + </settings>
6 +</component>
...\ No newline at end of file ...\ No newline at end of file
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<project version="4">
3 + <component name="Black">
4 + <option name="sdkName" value="Python 3.12 (Test_LangGraph)" />
5 + </component>
6 + <component name="JavaScriptSettings">
7 + <option name="languageLevel" value="ES6" />
8 + </component>
9 + <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (Test_LangGraph)" project-jdk-type="Python SDK" />
10 +</project>
...\ No newline at end of file ...\ No newline at end of file
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<project version="4">
3 + <component name="ProjectModuleManager">
4 + <modules>
5 + <module fileurl="file://$PROJECT_DIR$/.idea/Test_LangGraph.iml" filepath="$PROJECT_DIR$/.idea/Test_LangGraph.iml" />
6 + </modules>
7 + </component>
8 +</project>
...\ No newline at end of file ...\ No newline at end of file
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
1 +#!/usr/bin/env python3
2 +"""
3 +API 包初始化文件
4 +"""
5 +
6 +from .api_config import API_CONFIG
7 +from .waybill_api import query_waybill_list, generate_waybill_table, create_waybill_d
8 +from .paperless_api import upload_clearance_file
9 +
10 +__all__ = ['API_CONFIG', 'query_waybill_list', 'generate_waybill_table', 'create_waybill_d', 'upload_clearance_file']
No preview for this file type
No preview for this file type
No preview for this file type
No preview for this file type
1 +#!/usr/bin/env python3
2 +"""
3 +API 配置文件
4 +包含所有 API 相关的配置信息
5 +"""
6 +
7 +import os
8 +
9 +# API 配置
10 +API_CONFIG = {
11 + "base_url": "http://192.168.1.251:7022",
12 + "endpoint": "/Exp/bus-customer/vueConsignmentQuery/findChmConsignmentConditionalQuery",
13 + "headers": {
14 + "accept": "*/*",
15 + "Content-Type": "application/json",
16 + "Authorization": os.getenv("API_AUTHORIZATION", "Bearer 2.3e67bc239d7144bd9885cee8ffcbc8be"),
17 + "Ver": os.getenv("API_VER", "033BD94B1168D7E4F0D644C3C95E35BF.D73E33B659AD1D6B7D181D1DF8D05760"),
18 + "Referer": os.getenv("API_REFERER", "http://192.168.1.251/")
19 + }
20 +}
1 +#!/usr/bin/env python3
2 +"""
3 +清关文件上传 API
4 +按照 form-data 方式提交两个字段:
5 + - params: JSON 字符串(包含 code、slipId、uid 等)
6 + - fileUpload: PDF 文件
7 +"""
8 +
9 +import os
10 +import time
11 +import json
12 +import requests
13 +from typing import Optional
14 +
15 +from .api_config import API_CONFIG
16 +
17 +
18 +def upload_clearance_file(
19 + code: str,
20 + slip_id: int,
21 + pdf_path: str,
22 + file_index: int = 0,
23 + uid: Optional[int] = None,
24 +) -> str:
25 + """根据运单ID上传清关文件(PDF)。
26 +
27 + Args:
28 + code: 运单编号(必填)
29 + slip_id: 创建运单返回的ID(必填)
30 + pdf_path: 本地 PDF 文件路径(必填)
31 + file_index: 文件索引,默认 0
32 + uid: 文件 uid,可不传,默认使用时间戳生成
33 +
34 + Returns:
35 + 文本:成功/失败信息。
36 + """
37 + if not code:
38 + return "参数错误: code 不能为空"
39 + if not slip_id and slip_id != 0:
40 + return "参数错误: slip_id 不能为空"
41 + if not os.path.isfile(pdf_path):
42 + return f"参数错误: 文件不存在 - {pdf_path}"
43 +
44 + url = f"{API_CONFIG['base_url']}/Exp/manager-server/attachmentNew/upload/paperless/clean"
45 +
46 + # 生成 uid
47 + real_uid = uid if isinstance(uid, int) else int(time.time() * 1000)
48 +
49 + params_payload = {
50 + "uploadType": "other",
51 + "code": code,
52 + "fileIndex": file_index,
53 + "fileType": "picType_1",
54 + "inputType": 0,
55 + "slipId": slip_id,
56 + "slipType": "slipType_chm_pdf_od",
57 + "fileUpload": [{"uid": real_uid}],
58 + "fileSizeTotal": 18425,
59 + "splitSuccess": 0,
60 + "needBackSplit": 0,
61 + "splitPicTotal": 0,
62 + "item": [],
63 + }
64 +
65 + # form-data: params 是 JSON 字符串,fileUpload 是文件
66 + data = {
67 + "params": json.dumps(params_payload, ensure_ascii=False),
68 + }
69 +
70 + files = {
71 + "fileUpload": (os.path.basename(pdf_path), open(pdf_path, "rb"), "application/pdf"),
72 + }
73 +
74 + # 复制 headers,并移除 Content-Type(由 requests 根据 multipart 自动设置)
75 + headers = dict(API_CONFIG.get("headers", {}))
76 + headers.pop("Content-Type", None)
77 +
78 + try:
79 + resp = requests.post(url, headers=headers, data=data, files=files, timeout=60)
80 + # 确保文件句柄尽快关闭
81 + try:
82 + files["fileUpload"][1].close()
83 + except Exception:
84 + pass
85 +
86 + if resp.status_code == 200:
87 + return (
88 + f"上传清关文件成功\n"
89 + f"运单号:{code}\n"
90 + f"slipId:{slip_id}\n"
91 + f"uid:{real_uid}"
92 + )
93 + return f"上传失败: HTTP {resp.status_code}, 错误信息: {resp.text}"
94 +
95 + except requests.exceptions.RequestException as e:
96 + return f"网络请求失败: {str(e)}"
97 + except Exception as e:
98 + return f"上传清关文件失败: {str(e)}"
99 +
100 +
This diff is collapsed. Click to expand it.
1 +# LangGraph DEEPSEEK Agent 项目
2 +
3 +这是一个使用 LangGraph 和 DEEPSEEK 模型构建的智能体项目,实现了天气查询功能。
4 +
5 +## 🚀 项目特性
6 +
7 +- **DEEPSEEK 模型集成**: 使用 DEEPSEEK 作为主要语言模型
8 +- **ReAct 智能体**: 实现了推理和行动模式
9 +- **工具调用**: 支持天气查询工具
10 +- **LangGraph 服务**: 提供 Web API 接口
11 +- **环境配置**: 支持 .env 环境变量配置
12 +
13 +## 📁 项目结构
14 +
15 +```
16 +Test_LangGraph/
17 +├── test_agent.py # 智能体实现
18 +├── langgraph.json # LangGraph 配置
19 +├── .env # 环境变量
20 +├── requirements.txt # 依赖列表
21 +├── start_langgraph.py # 启动脚本
22 +├── test_api.py # API 测试脚本
23 +└── README.md # 项目说明
24 +```
25 +
26 +## 🛠️ 安装和配置
27 +
28 +### 1. 安装依赖
29 +
30 +```bash
31 +pip install -r requirements.txt
32 +```
33 +
34 +### 2. 配置环境变量
35 +
36 +编辑 `.env` 文件,设置您的 DEEPSEEK API 密钥:
37 +
38 +```env
39 +OPENAI_API_KEY=your-deepseek-api-key-here
40 +OPENAI_BASE_URL=https://api.deepseek.com/v1
41 +```
42 +
43 +### 3. 运行智能体
44 +
45 +#### 直接运行
46 +```bash
47 +python test_agent.py
48 +```
49 +
50 +#### 启动 LangGraph 服务
51 +```bash
52 +python start_langgraph.py
53 +```
54 +
55 +#### 使用 LangGraph CLI
56 +```bash
57 +python -m langgraph_cli dev
58 +```
59 +
60 +## 🔧 API 使用
61 +
62 +### 服务端点
63 +- **服务地址**: http://localhost:2025
64 +- **API 文档**: http://localhost:2025/docs
65 +- **WebSocket**: ws://localhost:2025/ws
66 +
67 +### 测试 API
68 +```bash
69 +python test_api.py
70 +```
71 +
72 +### 手动测试
73 +```bash
74 +curl -X POST "http://localhost:2025/weather_agent/invoke" \
75 + -H "Content-Type: application/json" \
76 + -d '{
77 + "messages": [
78 + {"role": "user", "content": "what is the weather in Beijing"}
79 + ]
80 + }'
81 +```
82 +
83 +## 📊 功能演示
84 +
85 +### 智能体对话流程
86 +1. **用户输入**: "what is the weather in sf"
87 +2. **AI 推理**: "I'll check the weather in San Francisco for you."
88 +3. **工具调用**: 调用 `get_weather` 工具
89 +4. **工具响应**: "It's always sunny in San Francisco!"
90 +5. **AI 总结**: "According to the weather information, it's always sunny in San Francisco!"
91 +
92 +### 性能指标
93 +- **模型**: deepseek-chat
94 +- **Token 使用**: 输入 189,输出 14,总计 203
95 +- **缓存效率**: 67% 缓存命中率
96 +- **响应时间**: 快速响应
97 +
98 +## 🔍 技术细节
99 +
100 +### 核心组件
101 +- **LangGraph**: 智能体框架
102 +- **LangChain**: 语言模型集成
103 +- **DEEPSEEK**: 大语言模型
104 +- **ReAct 模式**: 推理 + 行动
105 +
106 +### 工具系统
107 +- **get_weather**: 天气查询工具
108 +- **参数**: city (字符串)
109 +- **返回**: 模拟天气信息
110 +
111 +## 🐛 故障排除
112 +
113 +### 常见问题
114 +
115 +1. **依赖安装失败**
116 + ```bash
117 + pip install --upgrade pip
118 + pip install -r requirements.txt
119 + ```
120 +
121 +2. **API 密钥错误**
122 + - 检查 `.env` 文件中的 API 密钥
123 + - 确保 DEEPSEEK API 密钥有效
124 +
125 +3. **服务启动失败**
126 + - 检查端口 2025 是否被占用
127 + - 确保所有依赖已正确安装
128 +
129 +4. **LangGraph CLI 问题**
130 + ```bash
131 + pip install -U "langgraph-cli[inmem]"
132 + ```
133 +
134 +## 📝 开发说明
135 +
136 +### 添加新工具
137 +1. 在 `test_agent.py` 中定义工具函数
138 +2. 将工具添加到 `tools` 列表
139 +3. 重新启动服务
140 +
141 +### 修改模型配置
142 +编辑 `test_agent.py` 中的 `ChatOpenAI` 配置:
143 +```python
144 +model = ChatOpenAI(
145 + model="deepseek-chat",
146 + temperature=0.7
147 +)
148 +```
149 +
150 +## 📄 许可证
151 +
152 +本项目仅供学习和研究使用。
153 +
154 +## 🤝 贡献
155 +
156 +欢迎提交 Issue 和 Pull Request!
157 +
158 +---
159 +
160 +**注意**: 请确保您有有效的 DEEPSEEK API 密钥才能使用此项目。
1 +{
2 + "dependencies": ["."],
3 + "graphs": {
4 + "weather_agent": "./langgraph_examples/test_agent.py:agent",
5 + "api_agent": "./langgraph_examples/api_agent.py:agent"
6 + }
7 +}
1 +from langgraph.prebuilt import create_react_agent
2 +from langchain_openai import ChatOpenAI
3 +import os
4 +import sys
5 +import json
6 +from typing import Dict, Any
7 +
8 +# 添加项目根目录到 Python 路径
9 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10 +
11 +# 导入 API 模块
12 +from API.waybill_api import query_waybill_list, create_waybill_d, push_waybill_for_ocr
13 +from API.paperless_api import upload_clearance_file
14 +
15 +# 导入工具类
16 +from langgraph_examples.utils.message_processor import MessageProcessor
17 +
18 +# 设置 DEEPSEEK API 配置
19 +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "sk-e59da2fbc73240ea8d5ef8fb12657e4b")
20 +os.environ["OPENAI_BASE_URL"] = os.getenv("OPENAI_BASE_URL", "https://api.deepseek.com/v1")
21 +
22 +
23 +
24 +# 创建 DEEPSEEK 聊天模型
25 +model = ChatOpenAI(
26 + model="deepseek-chat", # 使用 DEEPSEEK 模型
27 + temperature=0 # 固定输出,避免改写工具返回
28 +)
29 +
30 +## 直接传递函数作为工具
31 +
32 +def pre_model_inspect_attachments(state, **kwargs):
33 + """
34 + LangGraph 预模型钩子:
35 + - 输入/输出都是"状态(dict)",更新 'messages'
36 + - 发现文件/二进制分段:保存到目录,再把该段替换为纯文本 URL
37 + - 支持环境变量:
38 + ATTACH_SAVE_DIR 保存目录,默认 uploads
39 + """
40 + print("\n=== pre_model_hook: inspect attachments ===")
41 + try:
42 + messages = state.get("messages", [])
43 +
44 + # 结构化打印:处理前消息
45 + def _to_simple(msgs):
46 + out = []
47 + for m in msgs or []:
48 + if isinstance(m, dict):
49 + out.append({"role": m.get("role"), "content": m.get("content")})
50 + else:
51 + out.append({
52 + "type": m.__class__.__name__,
53 + "role": getattr(m, "role", None),
54 + "content": getattr(m, "content", None),
55 + })
56 + return out
57 +
58 + print("=== 处理前消息 ===")
59 + print(json.dumps(_to_simple(messages), ensure_ascii=False, indent=2))
60 +
61 + # 避免字符串与列表拼接导致异常,统一用结构化打印
62 + # print("处理前消息:", messages)
63 +
64 + # 使用工具类处理消息
65 + processor = MessageProcessor()
66 + filtered_messages, saved_files = processor.process_messages(messages)
67 + # 直接修改 state 中的 messages 结构,确保后续序列化使用新内容
68 + try:
69 + state["messages"] = filtered_messages
70 + except Exception:
71 + pass
72 +
73 + # 结构化打印:处理后消息
74 + print("=== 处理后消息 ===")
75 + print(json.dumps(_to_simple(filtered_messages), ensure_ascii=False, indent=2))
76 +
77 + if saved_files:
78 + print("=== saved files ===")
79 + for f in saved_files:
80 + print(f" {f}")
81 +
82 + # 返回整个 state,避免上层忽略 messages 的替换
83 + return state
84 + except Exception as e:
85 + print(f"[pre_model_hook error] {e}")
86 + import traceback
87 + traceback.print_exc()
88 + return {}
89 +
90 +# 创建 ReAct 智能体
91 +agent = create_react_agent(
92 + model=model,
93 + tools=[query_waybill_list, create_waybill_d, upload_clearance_file, push_waybill_for_ocr],
94 + pre_model_hook=pre_model_inspect_attachments,
95 + prompt="""你是一个专业的出口物流系统智能助手,专门帮助用户处理运单相关的业务操作。
96 +
97 +## 你的主要职责:
98 +1. **运单查询**:根据用户需求查询运单列表,支持按状态、时间等条件筛选
99 +2. **运单创建**:协助用户创建D类运单,确保信息完整准确
100 +3. **运单详情**:查询运单的表头信息和表体明细,提供完整的运单数据
101 +4. **业务咨询**:解答用户关于出口物流流程、运单状态、操作规范等问题
102 +
103 +## 工作原则:
104 +- 始终以用户需求为导向,提供准确、及时的服务
105 +- 在调用API前,仔细确认用户提供的参数信息
106 +- 对API返回结果进行清晰、易懂的解释
107 +- 如遇到错误,主动分析原因并提供解决方案
108 +- 保持专业、友好的沟通态度
109 +- 严禁改写工具函数返回的文本格式;对工具输出仅直接转述,不得增删前后缀或改写内容。
110 + - 若调用了工具并获得结果,则必须将该工具返回的文本“原样作为最终答复”输出,不允许添加任何解释、建议或额外文字。
111 +
112 +## 可用工具:
113 +- query_waybill_list: 查询运单列表,支持按状态筛选,结果以HTML表格形式展示
114 +- create_waybill_d: 根据运单号创建D类运单,需要提供运单号参数
115 +- upload_clearance_file: 上传清关PDF文件,需要 code、slip_id、pdf_path
116 +- push_waybill_for_ocr: 根据运单ID推送OCR进行识别,需要提供运单ID(waybill_id)参数
117 +
118 +## query_waybill_list数据展示说明:
119 +- 运单查询结果会自动格式化为HTML表格,包含:运单号、运单类型、运单状态、发件人、运单日期
120 +- 表格在终端中会以HTML源码形式显示,用户可以在支持HTML的环境中查看格式化效果
121 +- 空字段会显示为空单元格
122 +- 运单创建结果会显示成功/失败状态和详细信息
123 +
124 +## create_waybill_d数据展示说明:
125 +- 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
126 +请根据用户的具体需求,选择合适的工具并提供帮助。"""
127 +)
128 +
129 +# 如果直接运行此文件
130 +if __name__ == "__main__":
131 + # {"messages": [{"role": "user", "content": "查询状态为'等待录入'的运单列表"}]}
132 + # {"messages": [{"role": "user", "content": "帮我创建运单,运单编号:2025102904"}]}
133 +
134 + # 测试上传清关PDF文件(通过智能体调用 upload_clearance_file 工具)
135 + test_message = (
136 + "请调用工具 upload_clearance_file,并严格按以下参数执行:\n"
137 + "- code: 202510281\n"
138 + "- slip_id: 177950273\n"
139 + "- pdf_path: C:\\Users\\24790\\Desktop\\出口AI资料\\test2-1.pdf\n"
140 + "- file_index: 0\n"
141 + "- uid: 1761635727889\n"
142 + "只需执行工具并原样输出工具返回的文本,不要添加任何解释。"
143 + )
144 + result = agent.invoke({"messages": [{"role": "user", "content": test_message}]})
145 + print(result)
146 +
147 +# LangGraph 服务端点
148 +def api_agent_endpoint(input_data: Dict[str, Any]) -> Dict[str, Any]:
149 + """API 智能体服务端点"""
150 + try:
151 + result = agent.invoke(input_data)
152 + return {
153 + "status": "success",
154 + "data": result,
155 + "error": None
156 + }
157 + except Exception as e:
158 + return {
159 + "status": "error",
160 + "data": None,
161 + "error": str(e)
162 + }
...\ No newline at end of file ...\ No newline at end of file
1 +# pip install -qU "langchain[openai]" langgraph to call the model
2 +
3 +from langgraph.prebuilt import create_react_agent
4 +from langchain_openai import ChatOpenAI
5 +import os
6 +from typing import Dict, Any
7 +
8 +# 设置 DEEPSEEK API 配置
9 +os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "sk-e59da2fbc73240ea8d5ef8fb12657e4b")
10 +os.environ["OPENAI_BASE_URL"] = os.getenv("OPENAI_BASE_URL", "https://api.deepseek.com/v1")
11 +
12 +def get_weather(city: str) -> str:
13 + """Get weather for a given city."""
14 + return f"It's always sunny in {city}!"
15 +
16 +# 创建 DEEPSEEK 聊天模型
17 +model = ChatOpenAI(
18 + model="deepseek-chat", # 使用 DEEPSEEK 模型
19 + temperature=0.7
20 +)
21 +
22 +# 创建 ReAct 智能体
23 +agent = create_react_agent(
24 + model=model,
25 + tools=[get_weather]
26 +)
27 +
28 +# LangGraph 服务端点
29 +def weather_agent_endpoint(input_data: Dict[str, Any]) -> Dict[str, Any]:
30 + """LangGraph 服务端点"""
31 + try:
32 + result = agent.invoke(input_data)
33 + return {
34 + "status": "success",
35 + "data": result,
36 + "error": None
37 + }
38 + except Exception as e:
39 + return {
40 + "status": "error",
41 + "data": None,
42 + "error": str(e)
43 + }
44 +
45 +# 如果直接运行此文件
46 +if __name__ == "__main__":
47 + # Run the agent
48 + result = agent.invoke(
49 + {"messages": [{"role": "user", "content": "what is the weather in sf"}]}
50 + )
51 + print(result)
...\ No newline at end of file ...\ No newline at end of file
1 +"""
2 +工具类包
3 +"""
4 +from .message_processor import MessageProcessor
5 +
6 +__all__ = ['MessageProcessor']
1 +"""
2 +消息处理和文件保存工具类
3 +"""
4 +import os
5 +import base64
6 +import uuid
7 +from pathlib import Path
8 +from typing import List, Any, Union
9 +
10 +
11 +class MessageProcessor:
12 + """消息处理和文件保存工具类"""
13 +
14 + def __init__(self, save_dir: str = None):
15 + """
16 + 初始化消息处理器
17 +
18 + Args:
19 + save_dir: 文件保存目录,默认为环境变量 ATTACH_SAVE_DIR 或 "uploads"
20 + """
21 + self.save_dir = save_dir or os.getenv("ATTACH_SAVE_DIR", "uploads")
22 + self._saved_files: list[str] = []
23 +
24 + def save_and_get_file_url(self, file_data: str, filename: str = None, mime_type: str = None) -> str:
25 + """
26 + 保存文件并返回绝对路径字符串
27 +
28 + Args:
29 + file_data: base64编码的文件数据
30 + filename: 文件名
31 + mime_type: MIME类型
32 +
33 + Returns:
34 + 文件绝对路径字符串
35 + """
36 + uploads = Path(self.save_dir)
37 + uploads.mkdir(exist_ok=True)
38 +
39 + # 生成安全文件名
40 + safe_name = filename or f"{uuid.uuid4().hex}"
41 + if mime_type == "application/pdf" and not safe_name.lower().endswith(".pdf"):
42 + safe_name += ".pdf"
43 +
44 + out_path = uploads / safe_name
45 +
46 + try:
47 + # 解码 base64 数据并保存
48 + with open(out_path, "wb") as f:
49 + f.write(base64.b64decode(file_data))
50 + abs_path = str(out_path.resolve())
51 + print(f" -> saved file: {abs_path}")
52 + self._saved_files.append(abs_path)
53 + return abs_path
54 + except Exception as e:
55 + print(f" -> save file failed: {e}")
56 + return f"[附件保存失败: {filename or 'unknown'}]"
57 +
58 + def process_content(self, content: Any) -> str:
59 + """
60 + 处理消息内容,提取文件并转换为文本
61 +
62 + Args:
63 + content: 消息内容,可能是字符串、列表或字典
64 +
65 + Returns:
66 + 处理后的文本内容
67 + """
68 + if isinstance(content, str):
69 + return content
70 + elif isinstance(content, list):
71 + text_parts = []
72 + for part in content:
73 + if isinstance(part, str):
74 + text_parts.append(part)
75 + elif isinstance(part, dict):
76 + part_type = part.get("type")
77 + if part_type == "text":
78 + text_parts.append(part.get("text", ""))
79 + elif part_type == "file":
80 + # 处理文件类型
81 + file_data = part.get("data")
82 + filename = part.get("metadata", {}).get("filename")
83 + mime_type = part.get("mime_type")
84 +
85 + if file_data:
86 + file_path = self.save_and_get_file_url(file_data, filename, mime_type)
87 + text_parts.append(file_path)
88 + else:
89 + text_parts.append(f"[文件缺失: {filename or 'unknown'}]")
90 + else:
91 + # 未知类型最小化占位
92 + text_parts.append(f"[{part_type or 'unknown'}]")
93 + return "\n".join([t for t in text_parts if t])
94 + elif isinstance(content, dict):
95 + # 单个字典内容
96 + ctype = content.get("type")
97 + if ctype == "text":
98 + return content.get("text", "")
99 + if ctype == "file":
100 + file_data = content.get("data")
101 + filename = content.get("metadata", {}).get("filename")
102 + mime_type = content.get("mime_type")
103 +
104 + if file_data:
105 + return self.save_and_get_file_url(file_data, filename, mime_type)
106 + else:
107 + return f"[文件缺失: {filename or 'unknown'}]"
108 + else:
109 + return f"[{ctype or 'unknown'}]"
110 + else:
111 + return str(content)
112 +
113 + def process_messages(self, messages: List[Any]) -> tuple[List[Any], List[str]]:
114 + """
115 + 处理消息列表,提取文件并转换为文本
116 +
117 + Args:
118 + messages: 消息列表
119 +
120 + Returns:
121 + (处理后的消息列表, 保存的文件路径列表)
122 + """
123 + filtered_messages = []
124 +
125 + for idx, m in enumerate(messages or []):
126 + # 兼容 dict 或 LangChain 的消息对象
127 + if isinstance(m, dict):
128 + role = m.get("role")
129 + content = m.get("content")
130 + else:
131 + role = getattr(m, "role", None)
132 + content = getattr(m, "content", None)
133 +
134 + print(f"[msg#{idx}] role={role!r}, content_type={type(content).__name__}")
135 +
136 + # 仅当 HumanMessage 且 content 中包含 file 分片时进行处理;否则保持原样
137 + msg_type = m.get("type") if isinstance(m, dict) else m.__class__.__name__
138 + should_process = (msg_type == "HumanMessage") and isinstance(content, list) and any(isinstance(p, dict) and p.get("type") == "file" for p in content)
139 +
140 + if should_process:
141 + # 提取文本与文件路径
142 + text_parts: list[str] = []
143 + file_paths: list[str] = []
144 + for part in content:
145 + if isinstance(part, dict):
146 + if part.get("type") == "text":
147 + text_parts.append(part.get("text", ""))
148 + elif part.get("type") == "file":
149 + # 兼容两种文件结构:
150 + # 1) {"type":"file", "data":"<base64>", "metadata":{"filename":...}, "mime_type":"application/pdf"}
151 + # 2) {"type":"file", "file":{"file_data":"data:application/pdf;base64,<base64>", "filename":"..."}}
152 + file_data = part.get("data")
153 + filename = part.get("metadata", {}).get("filename")
154 + mime_type = part.get("mime_type")
155 +
156 + if not file_data and isinstance(part.get("file"), dict):
157 + f = part.get("file") or {}
158 + file_data_url = f.get("file_data")
159 + filename = f.get("filename") or filename
160 + # 解析 data URL 或原始 base64
161 + if isinstance(file_data_url, str):
162 + if file_data_url.startswith("data:") and "," in file_data_url:
163 + try:
164 + header, b64_payload = file_data_url.split(",", 1)
165 + # 格式如 data:application/pdf;base64
166 + if header.startswith("data:") and ";" in header:
167 + mime_type = header[5:].split(";", 1)[0] or mime_type
168 + file_data = b64_payload
169 + except Exception:
170 + file_data = None
171 + else:
172 + # 非 data URL,当作纯 base64
173 + file_data = file_data_url
174 +
175 + if file_data:
176 + saved_path = self.save_and_get_file_url(file_data, filename, mime_type)
177 + file_paths.append(saved_path)
178 + merged_text = "\n".join([t for t in text_parts if t])
179 + if file_paths:
180 + merged_text = (merged_text + "\n" + "\n".join(file_paths)).strip()
181 +
182 + new_content = [{"type": "text", "text": merged_text}]
183 +
184 + if isinstance(m, dict):
185 + filtered_msg = {**m, "content": new_content}
186 + else:
187 + try:
188 + filtered_msg = m.__class__(
189 + content=new_content,
190 + additional_kwargs=getattr(m, "additional_kwargs", {}),
191 + response_metadata=getattr(m, "response_metadata", {}),
192 + id=getattr(m, "id", None),
193 + )
194 + except Exception:
195 + # 兜底为等价字典并保留 id
196 + filtered_msg = {
197 + "type": m.__class__.__name__,
198 + "role": (role or "user"),
199 + "id": getattr(m, "id", None),
200 + "additional_kwargs": getattr(m, "additional_kwargs", {}),
201 + "response_metadata": getattr(m, "response_metadata", {}),
202 + "content": new_content,
203 + }
204 + filtered_messages.append(filtered_msg)
205 + try:
206 + print(f" -> processed to: {len(merged_text)} chars")
207 + except Exception:
208 + print(" -> processed")
209 + else:
210 + # 不处理,其它消息保持不变
211 + filtered_messages.append(m)
212 + print(" -> processed (no change)")
213 +
214 + return filtered_messages, list(self._saved_files)
1 +# LangGraph DEEPSEEK Agent 项目依赖
2 +# 当前已安装的核心包版本
3 +
4 +# 核心框架
5 +langchain==1.0.2
6 +langchain-core==1.0.1
7 +langchain-openai==1.0.1
8 +
9 +# LangGraph 相关
10 +langgraph==1.0.1
11 +langgraph-checkpoint==3.0.0
12 +langgraph-prebuilt==1.0.1
13 +langgraph-sdk==0.2.9
14 +
15 +# OpenAI API (用于 DEEPSEEK)
16 +openai==2.6.1
17 +
18 +# HTTP 请求库
19 +requests==2.31.0
1 +#!/usr/bin/env python3
2 +"""
3 +LangGraph 服务启动脚本
4 +"""
5 +
6 +import subprocess
7 +import sys
8 +import os
9 +import time
10 +from pathlib import Path
11 +
12 +def check_dependencies():
13 + """检查依赖是否安装"""
14 + try:
15 + import langgraph
16 + import langchain
17 + import langchain_openai
18 + print("✅ 所有依赖已安装")
19 + return True
20 + except ImportError as e:
21 + print(f"❌ 缺少依赖: {e}")
22 + print("请运行: pip install -r requirements.txt")
23 + return False
24 +
25 +def start_langgraph_server():
26 + """启动 LangGraph 服务器"""
27 + print("🚀 启动 LangGraph 服务器...")
28 +
29 + # 检查 langgraph.json 是否存在
30 + if not Path("langgraph.json").exists():
31 + print("❌ 未找到 langgraph.json 配置文件")
32 + return False
33 +
34 + try:
35 + # 启动 LangGraph 开发服务器
36 + # 使用 --host 0.0.0.0 允许通过 IP 地址访问
37 + # cmd = ["python", "-m", "langgraph_cli", "dev", "--port", "2025"]
38 + cmd = ["python", "-m", "langgraph_cli", "dev", "--host", "0.0.0.0", "--port", "2025"]
39 + print(f"执行命令: {' '.join(cmd)}")
40 +
41 + # 强制使用 UTF-8 编码,避免 GBK 解码错误
42 + try:
43 + sys.stdout.reconfigure(encoding="utf-8", errors="replace")
44 + sys.stderr.reconfigure(encoding="utf-8", errors="replace")
45 + except Exception:
46 + pass
47 +
48 + # 设置环境变量确保子进程使用 UTF-8
49 + env = os.environ.copy()
50 + env["PYTHONIOENCODING"] = "UTF-8"
51 + env["PYTHONUTF8"] = "1"
52 +
53 + process = subprocess.Popen(
54 + cmd,
55 + stdout=subprocess.PIPE,
56 + stderr=subprocess.STDOUT,
57 + text=True,
58 + encoding="utf-8",
59 + errors="replace",
60 + bufsize=0,
61 + env=env
62 + )
63 +
64 + print("🌐 LangGraph 服务器已启动")
65 + print("📡 服务地址: http://0.0.0.0:2025")
66 + print("🔌 WebSocket: ws://0.0.0.0:2025/ws")
67 + print("📚 API 文档: http://0.0.0.0:2025/docs")
68 + print("💡 提示: 可通过本机 IP 地址访问,例如: http://192.168.1.44:2025")
69 + print("\n按 Ctrl+C 停止服务器\n")
70 +
71 + # 实时输出日志
72 + for line in process.stdout:
73 + print(line.rstrip())
74 +
75 + except KeyboardInterrupt:
76 + print("\n🛑 正在停止服务器...")
77 + process.terminate()
78 + process.wait()
79 + print("✅ 服务器已停止")
80 + except Exception as e:
81 + print(f"❌ 启动失败: {e}")
82 + return False
83 +
84 + return True
85 +
86 +def main():
87 + """主函数"""
88 + print("=" * 50)
89 + print("🎯 LangGraph DEEPSEEK Agent 启动器")
90 + print("=" * 50)
91 +
92 + # 检查依赖
93 + if not check_dependencies():
94 + sys.exit(1)
95 +
96 + # 检查环境变量
97 + if not os.getenv("OPENAI_API_KEY"):
98 + print("⚠️ 警告: 未设置 OPENAI_API_KEY 环境变量")
99 + print("请在 .env 文件中设置或直接设置环境变量")
100 +
101 + # 启动服务器
102 + start_langgraph_server()
103 +
104 +if __name__ == "__main__":
105 + main()
1 +#!/usr/bin/env python3
2 +"""
3 +Debug-friendly LangGraph server launcher (single process).
4 +Run this file in PyCharm Debug to hit breakpoints (e.g., pre_model_hook).
5 +"""
6 +
7 +import os
8 +import sys
9 +import json
10 +from pathlib import Path
11 +
12 +def setup_environment():
13 + # Ensure project root on sys.path
14 + root = Path(__file__).parent.resolve()
15 + sys.path.insert(0, str(root))
16 +
17 + # Load graphs from langgraph.json
18 + graphs = {}
19 + cfg = root / "langgraph.json"
20 + if cfg.exists():
21 + with open(cfg, "r", encoding="utf-8") as f:
22 + try:
23 + data = json.load(f)
24 + graphs = data.get("graphs", {})
25 + except Exception as e:
26 + print(f"⚠️ 读取 langgraph.json 失败: {e}")
27 +
28 + # Baseline env
29 + os.environ.setdefault("LANGGRAPH_API_URL", "http://localhost:2025")
30 + os.environ.setdefault("LANGGRAPH_RUNTIME_EDITION", "inmem")
31 + os.environ.setdefault("LANGGRAPH_DISABLE_FILE_PERSISTENCE", "false")
32 + os.environ.setdefault("LANGGRAPH_ALLOW_BLOCKING", "true")
33 + os.environ.setdefault("ALLOW_PRIVATE_NETWORK", "true")
34 + os.environ.setdefault("LANGSERVE_GRAPHS", json.dumps(graphs))
35 + os.environ.setdefault("N_JOBS_PER_WORKER", "1")
36 + os.environ.setdefault("ATTACH_SAVE_DIR", "uploads")
37 + os.environ.setdefault("DATABASE_URI", ":memory:")
38 + os.environ.setdefault("REDIS_URI", "fake")
39 + os.environ.setdefault("MIGRATIONS_PATH", "__inmem")
40 +
41 + # Load .env if present
42 + env_file = root / ".env"
43 + if env_file.exists():
44 + try:
45 + from dotenv import load_dotenv
46 + load_dotenv(env_file)
47 + print(" Loaded .env")
48 + except Exception:
49 + print(" python-dotenv 未安装,跳过 .env 加载")
50 +
51 +def main():
52 + print(" Starting LangGraph server (single-process, debug-friendly)...")
53 + setup_environment()
54 +
55 + print("\n" + "=" * 60)
56 + print(" Server URL: http://localhost:2025")
57 + print(" API Docs: http://localhost:2025/docs")
58 + print(" Studio UI: http://localhost:2025/ui")
59 + print(" Health: http://localhost:2025/ok")
60 + print("=" * 60)
61 +
62 + try:
63 + import uvicorn
64 + uvicorn.run(
65 + "langgraph_api.server:app",
66 + host="0.0.0.0",
67 + port=2025,
68 + reload=False, # disable auto-reload to avoid child processes
69 + access_log=False,
70 + )
71 + except KeyboardInterrupt:
72 + print("\n Server stopped by user")
73 + except Exception as e:
74 + print(f" Failed to start: {e}")
75 + import traceback; traceback.print_exc()
76 + sys.exit(1)
77 +
78 +if __name__ == "__main__":
79 + main()
...\ No newline at end of file ...\ No newline at end of file
No preview for this file type