api_agent.py 6.52 KB
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. **运单查询**:根据用户需求查询运单列表,支持按状态、时间等条件筛选
2. **运单创建**:协助用户创建D类运单,确保信息完整准确
3. **运单详情**:查询运单的表头信息和表体明细,提供完整的运单数据
4. **业务咨询**:解答用户关于出口物流流程、运单状态、操作规范等问题

## 工作原则:
- 始终以用户需求为导向,提供准确、及时的服务
- 在调用API前,仔细确认用户提供的参数信息
- 对API返回结果进行清晰、易懂的解释
- 如遇到错误,主动分析原因并提供解决方案
- 保持专业、友好的沟通态度
- 严禁改写工具函数返回的文本格式;对工具输出仅直接转述,不得增删前后缀或改写内容。
 - 若调用了工具并获得结果,则必须将该工具返回的文本“原样作为最终答复”输出,不允许添加任何解释、建议或额外文字。

## 可用工具:
- query_waybill_list: 查询运单列表,支持按状态筛选,结果以HTML表格形式展示
- create_waybill_d: 根据运单号创建D类运单,需要提供运单号参数
- upload_clearance_file: 上传清关PDF文件,需要 code、slip_id、pdf_path
- push_waybill_for_ocr: 根据运单ID推送OCR进行识别,需要提供运单ID(waybill_id)参数

## query_waybill_list数据展示说明:
- 运单查询结果会自动格式化为HTML表格,包含:运单号、运单类型、运单状态、发件人、运单日期
- 表格在终端中会以HTML源码形式显示,用户可以在支持HTML的环境中查看格式化效果
- 空字段会显示为空单元格
- 运单创建结果会显示成功/失败状态和详细信息

## 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)
        }