api_agent.py
6.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from langgraph.prebuilt import create_react_agent
from langchain_openai import ChatOpenAI
import os
import sys
import json
from typing import Dict, Any
# 添加项目根目录到 Python 路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 导入 API 模块
from API.waybill_api import query_waybill_list, create_waybill_d, push_waybill_for_ocr
from API.paperless_api import upload_clearance_file
# 导入工具类
from langgraph_examples.utils.message_processor import MessageProcessor
# 设置 DEEPSEEK API 配置
os.environ["OPENAI_API_KEY"] = os.getenv("OPENAI_API_KEY", "sk-e59da2fbc73240ea8d5ef8fb12657e4b")
os.environ["OPENAI_BASE_URL"] = os.getenv("OPENAI_BASE_URL", "https://api.deepseek.com/v1")
# 创建 DEEPSEEK 聊天模型
model = ChatOpenAI(
model="deepseek-chat", # 使用 DEEPSEEK 模型
temperature=0 # 固定输出,避免改写工具返回
)
## 直接传递函数作为工具
def pre_model_inspect_attachments(state, **kwargs):
"""
LangGraph 预模型钩子:
- 输入/输出都是"状态(dict)",更新 'messages'
- 发现文件/二进制分段:保存到目录,再把该段替换为纯文本 URL
- 支持环境变量:
ATTACH_SAVE_DIR 保存目录,默认 uploads
"""
print("\n=== pre_model_hook: inspect attachments ===")
try:
messages = state.get("messages", [])
# 结构化打印:处理前消息
def _to_simple(msgs):
out = []
for m in msgs or []:
if isinstance(m, dict):
out.append({"role": m.get("role"), "content": m.get("content")})
else:
out.append({
"type": m.__class__.__name__,
"role": getattr(m, "role", None),
"content": getattr(m, "content", None),
})
return out
print("=== 处理前消息 ===")
print(json.dumps(_to_simple(messages), ensure_ascii=False, indent=2))
# 避免字符串与列表拼接导致异常,统一用结构化打印
# print("处理前消息:", messages)
# 使用工具类处理消息
processor = MessageProcessor()
filtered_messages, saved_files = processor.process_messages(messages)
# 直接修改 state 中的 messages 结构,确保后续序列化使用新内容
try:
state["messages"] = filtered_messages
except Exception:
pass
# 结构化打印:处理后消息
print("=== 处理后消息 ===")
print(json.dumps(_to_simple(filtered_messages), ensure_ascii=False, indent=2))
if saved_files:
print("=== saved files ===")
for f in saved_files:
print(f" {f}")
# 返回整个 state,避免上层忽略 messages 的替换
return state
except Exception as e:
print(f"[pre_model_hook error] {e}")
import traceback
traceback.print_exc()
return {}
# 创建 ReAct 智能体
agent = create_react_agent(
model=model,
tools=[query_waybill_list, create_waybill_d, upload_clearance_file, push_waybill_for_ocr],
pre_model_hook=pre_model_inspect_attachments,
prompt="""你是一个专业的出口物流系统智能助手,专门帮助用户处理运单相关的业务操作。
## 你的主要职责:
1. **运单查询**:根据用户需求查询运单列表,支持按状态、时间等条件筛选,结果以JSON形式展示,AI不用对返回数据JSON进行加工
2. **运单创建**:协助用户创建D类运单,确保信息完整准确
4. **业务咨询**:解答用户关于出口物流流程、运单状态、操作规范等问题
## 工作原则:
- 上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口
- 始终以用户需求为导向,提供准确、及时的服务
- 在调用API前,仔细确认用户提供的参数信息
- 对API返回结果进行清晰、易懂的解释
- 如遇到错误,主动分析原因并提供解决方案
- 保持专业、友好的沟通态度
- 严禁改写工具函数返回的文本格式;对工具输出仅直接转述,不得增删前后缀或改写内容。
- 若调用了工具并获得结果,则必须将该工具返回的文本“原样作为最终答复”输出,不允许添加任何解释、建议或额外文字。
## 可用工具:
- query_waybill_list: 查询运单列表,支持按状态筛选,结果以JSON形式展示,AI不用对返回数据JSON进行加工
- create_waybill_d: 根据运单号创建D类运单,需要提供运单号参数
- upload_clearance_file: 上传清关PDF文件,需要 code、slip_id、pdf_path,上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口
- push_waybill_for_ocr: 根据运单ID推送OCR进行识别,需要提供运单ID(waybill_id)参数
## query_waybill_list数据展示说明:
- 运单查询结果会自动格式化为JSON形式展示,包含:运单号、运单类型、运单状态、发件人、运单日期
- 空字段会显示为空单元格
- 运单创建结果会显示成功/失败状态和详细信息
## create_waybill_d数据展示说明:
- 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
请根据用户的具体需求,选择合适的工具并提供帮助。"""
)
# 如果直接运行此文件
if __name__ == "__main__":
# {"messages": [{"role": "user", "content": "查询状态为'等待录入'的运单列表"}]}
# {"messages": [{"role": "user", "content": "帮我创建运单,运单编号:2025102904"}]}
# 测试上传清关PDF文件(通过智能体调用 upload_clearance_file 工具)
test_message = (
"请调用工具 upload_clearance_file,并严格按以下参数执行:\n"
"- code: 202510281\n"
"- slip_id: 177950273\n"
"- pdf_path: C:\\Users\\24790\\Desktop\\出口AI资料\\test2-1.pdf\n"
"- file_index: 0\n"
"- uid: 1761635727889\n"
"只需执行工具并原样输出工具返回的文本,不要添加任何解释。"
)
result = agent.invoke({"messages": [{"role": "user", "content": test_message}]})
print(result)
# LangGraph 服务端点
def api_agent_endpoint(input_data: Dict[str, Any]) -> Dict[str, Any]:
"""API 智能体服务端点"""
try:
result = agent.invoke(input_data)
return {
"status": "success",
"data": result,
"error": None
}
except Exception as e:
return {
"status": "error",
"data": None,
"error": str(e)
}