Showing
39 changed files
with
1411 additions
and
0 deletions
.env
0 → 100644
No preview for this file type
.gitignore
0 → 100644
| 1 | +/venv |
.idea/.gitignore
0 → 100644
.idea/Test_LangGraph.iml
0 → 100644
| 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 |
.idea/ai_toolkit.xml
0 → 100644
.idea/encodings.xml
0 → 100644
| 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 |
.idea/inspectionProfiles/Project_Default.xml
0 → 100644
| 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 |
.idea/misc.xml
0 → 100644
| 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 |
.idea/modules.xml
0 → 100644
| 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 |
.langgraph_api/.langgraph_checkpoint.1.pckl
0 → 100644
No preview for this file type
.langgraph_api/.langgraph_checkpoint.2.pckl
0 → 100644
No preview for this file type
.langgraph_api/.langgraph_checkpoint.3.pckl
0 → 100644
No preview for this file type
.langgraph_api/.langgraph_ops.pckl
0 → 100644
No preview for this file type
.langgraph_api/.langgraph_retry_counter.pckl
0 → 100644
No preview for this file type
.langgraph_api/store.pckl
0 → 100644
No preview for this file type
.langgraph_api/store.vectors.pckl
0 → 100644
No preview for this file type
API/__init__.py
0 → 100644
| 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'] |
API/__pycache__/__init__.cpython-312.pyc
0 → 100644
No preview for this file type
API/__pycache__/api_config.cpython-312.pyc
0 → 100644
No preview for this file type
No preview for this file type
API/__pycache__/waybill_api.cpython-312.pyc
0 → 100644
No preview for this file type
API/api_config.py
0 → 100644
| 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 | +} |
API/paperless_api.py
0 → 100644
| 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 | + |
API/waybill_api.py
0 → 100644
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +""" | ||
| 3 | +运单查询 API 模块 | ||
| 4 | +包含运单查询相关的所有函数 | ||
| 5 | +""" | ||
| 6 | + | ||
| 7 | +import requests | ||
| 8 | +import json | ||
| 9 | +from .api_config import API_CONFIG | ||
| 10 | + | ||
| 11 | + | ||
| 12 | +def generate_waybill_table(data: list) -> str: | ||
| 13 | + """生成运单数据的 Markdown 表格 | ||
| 14 | + | ||
| 15 | + Args: | ||
| 16 | + data: 运单数据列表 | ||
| 17 | + | ||
| 18 | + Returns: | ||
| 19 | + Markdown 表格字符串 | ||
| 20 | + """ | ||
| 21 | + # 添加数据类型检查 | ||
| 22 | + if not data or not isinstance(data, list): | ||
| 23 | + return "暂无数据或数据格式错误" | ||
| 24 | + | ||
| 25 | + # 确保 data 是列表且不为空 | ||
| 26 | + if len(data) == 0: | ||
| 27 | + return "暂无数据" | ||
| 28 | + | ||
| 29 | + # 检查第一个元素是否为字典 | ||
| 30 | + if not isinstance(data[0], dict): | ||
| 31 | + return f"数据格式错误: 期望字典列表,实际类型: {type(data[0])}" | ||
| 32 | + | ||
| 33 | + # 生成 Markdown 表格 | ||
| 34 | + lines = [] | ||
| 35 | + lines.append("| 运单号 | 运单类型 | 运单状态 | 发件人 | 运单日期 |") | ||
| 36 | + lines.append("|--------|----------|----------|--------|----------|") | ||
| 37 | + | ||
| 38 | + for item in data: | ||
| 39 | + # 字段映射和空值处理,添加类型检查 | ||
| 40 | + consignment_code = item.get('consignmentCode', '') if isinstance(item, dict) else '' | ||
| 41 | + cons_type = item.get('consType', '') if isinstance(item, dict) else '' | ||
| 42 | + cons_status = item.get('consStatus', '') if isinstance(item, dict) else '' | ||
| 43 | + sender = item.get('sender', '') if isinstance(item, dict) else '' | ||
| 44 | + create_time = item.get('createTime', '') if isinstance(item, dict) else '' | ||
| 45 | + | ||
| 46 | + # 添加表格行 | ||
| 47 | + lines.append(f"| {consignment_code} | {cons_type} | {cons_status} | {sender} | {create_time} |") | ||
| 48 | + | ||
| 49 | + return "\n".join(lines) | ||
| 50 | + | ||
| 51 | + | ||
| 52 | +def create_waybill_d(consignmentCode: str) -> str: | ||
| 53 | + """根据运单号创建D类运单 | ||
| 54 | + | ||
| 55 | + Args: | ||
| 56 | + consignmentCode: 运单号(必填) | ||
| 57 | + | ||
| 58 | + Returns: | ||
| 59 | + 创建结果信息 | ||
| 60 | + """ | ||
| 61 | + try: | ||
| 62 | + url = f"{API_CONFIG['base_url']}/Exp/bus-customer/consignment/createChmConsignmentD" | ||
| 63 | + | ||
| 64 | + # 构建请求数据 | ||
| 65 | + data = { | ||
| 66 | + "consignmentCode": consignmentCode, | ||
| 67 | + "consignmentId": "", | ||
| 68 | + "isAIRecognition": 0 | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + # 发送POST请求 | ||
| 72 | + response = requests.post( | ||
| 73 | + url, | ||
| 74 | + headers=API_CONFIG['headers'], | ||
| 75 | + json=data, | ||
| 76 | + timeout=30 | ||
| 77 | + ) | ||
| 78 | + | ||
| 79 | + # 检查响应状态 | ||
| 80 | + if response.status_code != 200: | ||
| 81 | + return f"❌ 创建失败: HTTP {response.status_code}, 错误信息: {response.text}" | ||
| 82 | + | ||
| 83 | + result = response.json() | ||
| 84 | + | ||
| 85 | + # 添加调试信息 | ||
| 86 | + print(f"创建D类运单API响应: {result}") | ||
| 87 | + | ||
| 88 | + # 业务状态判定(保守容错) | ||
| 89 | + if result.get('success') is False: | ||
| 90 | + error_msg = result.get('message', '未知错误') | ||
| 91 | + return f"❌ 创建失败: {error_msg}" | ||
| 92 | + | ||
| 93 | + # 提取新建的运单ID与编号 | ||
| 94 | + data = result.get('data') or {} | ||
| 95 | + extra = data.get('extra') or {} | ||
| 96 | + waybill_id = extra.get('id') | ||
| 97 | + code = extra.get('consignmentCode') or consignmentCode or "" | ||
| 98 | + | ||
| 99 | + # 若拿不到ID,严格按目标格式返回(字段留空) | ||
| 100 | + if not waybill_id: | ||
| 101 | + lines = [ | ||
| 102 | + f"运单号:{code} 的D类运单已成功创建。", | ||
| 103 | + f"运单号:{code}", | ||
| 104 | + "\t公司信息", | ||
| 105 | + "\t发件人公司:", | ||
| 106 | + "\t海关编码:", | ||
| 107 | + "\t商品明细信息", | ||
| 108 | + "\t出口商品编码:", | ||
| 109 | + "\t中文品名:", | ||
| 110 | + ] | ||
| 111 | + return "\n".join(lines) | ||
| 112 | + | ||
| 113 | + # 直接请求表头详情(不引入额外文本,空值留空) | ||
| 114 | + head_url = f"{API_CONFIG['base_url']}/Exp/bus-customer/consignment/headInfo" | ||
| 115 | + operate_unit_name = "" | ||
| 116 | + operate_unit_code = "" | ||
| 117 | + try: | ||
| 118 | + head_resp = requests.post( | ||
| 119 | + head_url, | ||
| 120 | + headers=API_CONFIG["headers"], | ||
| 121 | + json={"id": waybill_id}, | ||
| 122 | + timeout=30, | ||
| 123 | + ) | ||
| 124 | + if head_resp.status_code == 200: | ||
| 125 | + head_json = head_resp.json() | ||
| 126 | + head_extra = (head_json.get("data") or {}).get("extra") or {} | ||
| 127 | + operate_unit_name = head_extra.get("operateUnitName") or "" | ||
| 128 | + operate_unit_code = head_extra.get("operateUnitCode") or "" | ||
| 129 | + except Exception: | ||
| 130 | + pass | ||
| 131 | + | ||
| 132 | + # 直接请求表体明细(取第一条,空值留空) | ||
| 133 | + detail_url = f"{API_CONFIG['base_url']}/Exp/bus-customer/consignment/detail/list" | ||
| 134 | + sku_code = "" | ||
| 135 | + sku_name = "" | ||
| 136 | + try: | ||
| 137 | + detail_payload = { | ||
| 138 | + "consType": "consType_D", | ||
| 139 | + "consignmentId": waybill_id, | ||
| 140 | + "pageIndex": 1, | ||
| 141 | + "pageSize": 5, | ||
| 142 | + } | ||
| 143 | + detail_resp = requests.post( | ||
| 144 | + detail_url, | ||
| 145 | + headers=API_CONFIG["headers"], | ||
| 146 | + json=detail_payload, | ||
| 147 | + timeout=30, | ||
| 148 | + ) | ||
| 149 | + if detail_resp.status_code == 200: | ||
| 150 | + detail_json = detail_resp.json() | ||
| 151 | + records = (detail_json.get("data") or {}).get("records") or [] | ||
| 152 | + if isinstance(records, list) and records: | ||
| 153 | + first = records[0] if isinstance(records[0], dict) else {} | ||
| 154 | + sku_code = first.get("skuCode") or "" | ||
| 155 | + sku_name = first.get("skuName") or "" | ||
| 156 | + except Exception: | ||
| 157 | + pass | ||
| 158 | + | ||
| 159 | + # 严格按目标格式输出(不添加解释性文字、提示或建议) | ||
| 160 | + lines = [ | ||
| 161 | + f"运单号:{code} 的D类运单已成功创建。", | ||
| 162 | + f"运单号:{code}", | ||
| 163 | + "\t公司信息", | ||
| 164 | + f"\t发件人公司:{operate_unit_name}", | ||
| 165 | + f"\t海关编码:{operate_unit_code}", | ||
| 166 | + "\t商品明细信息", | ||
| 167 | + f"\t出口商品编码:{sku_code}", | ||
| 168 | + f"\t中文品名:{sku_name}", | ||
| 169 | + ] | ||
| 170 | + return "\n".join(lines) | ||
| 171 | + | ||
| 172 | + except requests.exceptions.RequestException as e: | ||
| 173 | + return f"❌ 网络请求失败: {str(e)}" | ||
| 174 | + except json.JSONDecodeError as e: | ||
| 175 | + return f"❌ 响应解析失败: {str(e)}" | ||
| 176 | + except Exception as e: | ||
| 177 | + return f"❌ 创建D类运单失败: {str(e)}" | ||
| 178 | + | ||
| 179 | + | ||
| 180 | +def query_waybill_list(initialize: int = 1, consStatusName: str = "等待录入") -> str: | ||
| 181 | + """查询运单列表信息 | ||
| 182 | + | ||
| 183 | + Args: | ||
| 184 | + initialize: 初始化标志,默认为1 | ||
| 185 | + consStatusName: 运单状态名称,默认为"等待录入" | ||
| 186 | + | ||
| 187 | + Returns: | ||
| 188 | + 运单查询结果 | ||
| 189 | + """ | ||
| 190 | + try: | ||
| 191 | + url = f"{API_CONFIG['base_url']}{API_CONFIG['endpoint']}" | ||
| 192 | + | ||
| 193 | + # 构建请求数据 | ||
| 194 | + data = { | ||
| 195 | + "consStatusName": consStatusName, | ||
| 196 | + "consTypeId": None, | ||
| 197 | + "consignmentCode": [], | ||
| 198 | + "endTime": None, | ||
| 199 | + "sender": None, | ||
| 200 | + "startTime": None, | ||
| 201 | + "initialize": initialize, | ||
| 202 | + "pageIndex": 1, | ||
| 203 | + "pageSize": 5, | ||
| 204 | + "total": 0 | ||
| 205 | + } | ||
| 206 | + | ||
| 207 | + # 发送POST请求 | ||
| 208 | + response = requests.post( | ||
| 209 | + url, | ||
| 210 | + headers=API_CONFIG['headers'], | ||
| 211 | + json=data, | ||
| 212 | + timeout=30 | ||
| 213 | + ) | ||
| 214 | + | ||
| 215 | + # 检查响应状态 | ||
| 216 | + if response.status_code == 200: | ||
| 217 | + result = response.json() | ||
| 218 | + | ||
| 219 | + # 添加调试信息 | ||
| 220 | + print(f"API 响应结构: {type(result)}") | ||
| 221 | + print(f"响应内容: {result}") | ||
| 222 | + | ||
| 223 | + # 正确提取 records 数组 | ||
| 224 | + data = result.get('data', {}).get('records', []) | ||
| 225 | + | ||
| 226 | + # 添加数据类型检查 | ||
| 227 | + if not isinstance(data, list): | ||
| 228 | + return f"查询成功但数据格式错误: records 字段类型为 {type(data)},内容: {data}" | ||
| 229 | + | ||
| 230 | + if not data: | ||
| 231 | + return f"查询成功: 未找到状态为 '{consStatusName}' 的运单记录。" | ||
| 232 | + | ||
| 233 | + # 生成 HTML 表格 | ||
| 234 | + html_table = generate_waybill_table(data) | ||
| 235 | + return f"查询成功: 找到 {len(data)} 条运单记录。状态: {consStatusName}\n\n{html_table}" | ||
| 236 | + else: | ||
| 237 | + return f"查询失败: HTTP {response.status_code}, 错误信息: {response.text}" | ||
| 238 | + | ||
| 239 | + except requests.exceptions.RequestException as e: | ||
| 240 | + return f"网络请求失败: {str(e)}" | ||
| 241 | + except json.JSONDecodeError as e: | ||
| 242 | + return f"响应解析失败: {str(e)}" | ||
| 243 | + except Exception as e: | ||
| 244 | + return f"查询运单失败: {str(e)}" | ||
| 245 | + | ||
| 246 | + | ||
| 247 | +def get_waybill_head_info(waybill_id: int) -> str: | ||
| 248 | + """根据运单ID查询表头详情并返回指定字段 | ||
| 249 | + | ||
| 250 | + Args: | ||
| 251 | + waybill_id: 运单ID(必填) | ||
| 252 | + | ||
| 253 | + Returns: | ||
| 254 | + 格式化字符串: | ||
| 255 | + 您好!运单号:XXXX\n | ||
| 256 | + \t公司信息\n | ||
| 257 | + \t发件人公司:XXXXX\n | ||
| 258 | + \t海关编码:XXXX | ||
| 259 | + """ | ||
| 260 | + try: | ||
| 261 | + url = f"{API_CONFIG['base_url']}/Exp/bus-customer/consignment/headInfo" | ||
| 262 | + | ||
| 263 | + payload = {"id": waybill_id} | ||
| 264 | + | ||
| 265 | + response = requests.post( | ||
| 266 | + url, | ||
| 267 | + headers=API_CONFIG["headers"], | ||
| 268 | + json=payload, | ||
| 269 | + timeout=30, | ||
| 270 | + ) | ||
| 271 | + | ||
| 272 | + if response.status_code != 200: | ||
| 273 | + return f"查询失败: HTTP {response.status_code}, 错误信息: {response.text}" | ||
| 274 | + | ||
| 275 | + result = response.json() | ||
| 276 | + | ||
| 277 | + # 容错解析:期望字段位于 data.extra | ||
| 278 | + data = result.get("data") or {} | ||
| 279 | + extra = data.get("extra") or {} | ||
| 280 | + | ||
| 281 | + consignment_code = extra.get("consignmentCode") or "" | ||
| 282 | + operate_unit_name = extra.get("operateUnitName") or "" | ||
| 283 | + operate_unit_code = extra.get("operateUnitCode") or "" | ||
| 284 | + | ||
| 285 | + return ( | ||
| 286 | + f"您好!运单号:{consignment_code}\n" | ||
| 287 | + f"\t公司信息\n" | ||
| 288 | + f"\t发件人公司:{operate_unit_name}\n" | ||
| 289 | + f"\t海关编码:{operate_unit_code}" | ||
| 290 | + ) | ||
| 291 | + | ||
| 292 | + except requests.exceptions.RequestException as e: | ||
| 293 | + return f"网络请求失败: {str(e)}" | ||
| 294 | + except json.JSONDecodeError as e: | ||
| 295 | + return f"响应解析失败: {str(e)}" | ||
| 296 | + except Exception as e: | ||
| 297 | + return f"查询表头详情失败: {str(e)}" | ||
| 298 | + | ||
| 299 | + | ||
| 300 | +def get_waybill_detail_list( | ||
| 301 | + consignment_id: int, | ||
| 302 | + cons_type: str = "consType_D", | ||
| 303 | + page_index: int = 1, | ||
| 304 | + page_size: int = 5, | ||
| 305 | +) -> str: | ||
| 306 | + """根据运单ID查询表体明细列表,仅返回 skuCode 与 skuName。 | ||
| 307 | + | ||
| 308 | + Args: | ||
| 309 | + consignment_id: 运单ID(必填)。 | ||
| 310 | + cons_type: 运单类型,默认 "consType_D"。 | ||
| 311 | + page_index: 页码,默认 1。 | ||
| 312 | + page_size: 每页数量,默认 5。 | ||
| 313 | + | ||
| 314 | + Returns: | ||
| 315 | + 多行文本: | ||
| 316 | + \t商品明细信息\n | ||
| 317 | + \t出口商品编码:XXXX\n | ||
| 318 | + \t中文品名:XXXX | ||
| 319 | + 若数据为空,则两个字段均为空字符。 | ||
| 320 | + """ | ||
| 321 | + try: | ||
| 322 | + url = f"{API_CONFIG['base_url']}/Exp/bus-customer/consignment/detail/list" | ||
| 323 | + | ||
| 324 | + payload = { | ||
| 325 | + "consType": cons_type, | ||
| 326 | + "consignmentId": consignment_id, | ||
| 327 | + "pageIndex": page_index, | ||
| 328 | + "pageSize": page_size, | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + response = requests.post( | ||
| 332 | + url, | ||
| 333 | + headers=API_CONFIG["headers"], | ||
| 334 | + json=payload, | ||
| 335 | + timeout=30, | ||
| 336 | + ) | ||
| 337 | + | ||
| 338 | + if response.status_code != 200: | ||
| 339 | + return f"查询失败: HTTP {response.status_code}, 错误信息: {response.text}" | ||
| 340 | + | ||
| 341 | + result = response.json() | ||
| 342 | + | ||
| 343 | + data = result.get("data") or {} | ||
| 344 | + records = data.get("records") or [] | ||
| 345 | + | ||
| 346 | + # 若无记录,按需求返回空字段 | ||
| 347 | + if not isinstance(records, list) or len(records) == 0: | ||
| 348 | + return ( | ||
| 349 | + "\t商品明细信息\n" | ||
| 350 | + "\t出口商品编码:\n" | ||
| 351 | + "\t中文品名:" | ||
| 352 | + ) | ||
| 353 | + | ||
| 354 | + # 拼接所有记录(每条记录两行)。如需仅返回第一条,可切片 records[:1] | ||
| 355 | + lines = ["\t商品明细信息"] | ||
| 356 | + for item in records: | ||
| 357 | + sku_code = item.get("skuCode") if isinstance(item, dict) else "" | ||
| 358 | + sku_name = item.get("skuName") if isinstance(item, dict) else "" | ||
| 359 | + lines.append(f"\t出口商品编码:{sku_code or ''}") | ||
| 360 | + lines.append(f"\t中文品名:{sku_name or ''}") | ||
| 361 | + | ||
| 362 | + return "\n".join(lines) | ||
| 363 | + | ||
| 364 | + except requests.exceptions.RequestException as e: | ||
| 365 | + return f"网络请求失败: {str(e)}" | ||
| 366 | + except json.JSONDecodeError as e: | ||
| 367 | + return f"响应解析失败: {str(e)}" | ||
| 368 | + except Exception as e: | ||
| 369 | + return f"查询表体明细失败: {str(e)}" | ||
| 370 | + | ||
| 371 | + | ||
| 372 | +def push_waybill_for_ocr(waybill_id: int) -> str: | ||
| 373 | + """根据运单ID推送OCR进行识别 | ||
| 374 | + | ||
| 375 | + Args: | ||
| 376 | + waybill_id: 运单ID(必填) | ||
| 377 | + | ||
| 378 | + Returns: | ||
| 379 | + 推送结果信息 | ||
| 380 | + """ | ||
| 381 | + try: | ||
| 382 | + url = f"{API_CONFIG['base_url']}/Exp/bus-customer/dataworksNew/vue/push/file" | ||
| 383 | + | ||
| 384 | + # 构建请求数据 | ||
| 385 | + data = { | ||
| 386 | + "id": waybill_id | ||
| 387 | + } | ||
| 388 | + | ||
| 389 | + # 发送POST请求 | ||
| 390 | + response = requests.post( | ||
| 391 | + url, | ||
| 392 | + headers=API_CONFIG['headers'], | ||
| 393 | + json=data, | ||
| 394 | + timeout=30 | ||
| 395 | + ) | ||
| 396 | + | ||
| 397 | + # 检查响应状态 | ||
| 398 | + if response.status_code != 200: | ||
| 399 | + return f"❌ 推送失败: HTTP {response.status_code}, 错误信息: {response.text}" | ||
| 400 | + | ||
| 401 | + result = response.json() | ||
| 402 | + | ||
| 403 | + # 添加调试信息 | ||
| 404 | + print(f"推送OCR识别API响应: {result}") | ||
| 405 | + | ||
| 406 | + # 业务状态判定 | ||
| 407 | + if result.get('success') is False: | ||
| 408 | + error_msg = result.get('message', '未知错误') | ||
| 409 | + return f"❌ 推送失败: {error_msg}" | ||
| 410 | + | ||
| 411 | + # 成功情况 | ||
| 412 | + return f"✅ 运单ID {waybill_id} 已成功推送到OCR识别系统" | ||
| 413 | + | ||
| 414 | + except requests.exceptions.RequestException as e: | ||
| 415 | + return f"❌ 网络请求失败: {str(e)}" | ||
| 416 | + except json.JSONDecodeError as e: | ||
| 417 | + return f"❌ 响应解析失败: {str(e)}" | ||
| 418 | + except Exception as e: | ||
| 419 | + return f"❌ 推送OCR识别失败: {str(e)}" | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
README.md
0 → 100644
| 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 密钥才能使用此项目。 |
langgraph.json
0 → 100644
No preview for this file type
No preview for this file type
langgraph_examples/api_agent.py
0 → 100644
| 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 |
langgraph_examples/test_agent.py
0 → 100644
| 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 |
langgraph_examples/utils/__init__.py
0 → 100644
No preview for this file type
No preview for this file type
| 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) |
requirements.txt
0 → 100644
| 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 |
start_langgraph.py
0 → 100644
| 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() |
start_server_direct.py
0 → 100644
| 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 |
uploads/test2-1.pdf
0 → 100644
No preview for this file type
-
Please register or login to post a comment