zhouhui.jiang

update

...@@ -13,7 +13,7 @@ API_CONFIG = { ...@@ -13,7 +13,7 @@ API_CONFIG = {
13 "headers": { 13 "headers": {
14 "accept": "*/*", 14 "accept": "*/*",
15 "Content-Type": "application/json", 15 "Content-Type": "application/json",
16 - "Authorization": os.getenv("API_AUTHORIZATION", "Bearer 2.21e3deecca904c7697ab891b8276cf36"), 16 + "Authorization": os.getenv("API_AUTHORIZATION", "Bearer 2.bd593d8f36c4468fbf02ee95c473cbb6"),
17 "Ver": os.getenv("API_VER", "033BD94B1168D7E4F0D644C3C95E35BF.D73E33B659AD1D6B7D181D1DF8D05760"), 17 "Ver": os.getenv("API_VER", "033BD94B1168D7E4F0D644C3C95E35BF.D73E33B659AD1D6B7D181D1DF8D05760"),
18 "Referer": os.getenv("API_REFERER", "http://192.168.1.251/") 18 "Referer": os.getenv("API_REFERER", "http://192.168.1.251/")
19 } 19 }
......
...@@ -103,3 +103,79 @@ def upload_clearance_file( ...@@ -103,3 +103,79 @@ def upload_clearance_file(
103 return f"上传清关文件失败: {str(e)}" 103 return f"上传清关文件失败: {str(e)}"
104 104
105 105
106 +def upload_file_for_ocr(
107 + pdf_path: str,
108 + Authorization: Optional[str] = None,
109 +) -> str:
110 + """上传文件进行OCR识别
111 +
112 + Args:
113 + pdf_path: 本地 PDF 文件路径(必填)
114 + Authorization: 授权令牌,如果提供则使用该值,否则使用 API_CONFIG 中的默认值
115 +
116 + Returns:
117 + 文本:成功/失败信息
118 + """
119 + if not os.path.isfile(pdf_path):
120 + return f"参数错误: 文件不存在 - {pdf_path}"
121 +
122 + # 从文件路径提取文件名
123 + file_name = os.path.basename(pdf_path)
124 +
125 + # 构建URL(不包含查询参数)
126 + url = f"{API_CONFIG['base_url']}/API/ocrDemo/pushDemo"
127 +
128 + # 封装参数到 params_payload
129 + params_payload = {
130 + "fileName01": file_name,
131 + "fileNumber": 1
132 + }
133 +
134 + # form-data: params 是 JSON 字符串,fileUpload 是文件
135 + data = {
136 + "params": json.dumps(params_payload, ensure_ascii=False),
137 + }
138 +
139 + files = {
140 + "file": (file_name, open(pdf_path, "rb"), "application/pdf"),
141 + }
142 +
143 + # 构建 headers,优先使用传入的 Authorization
144 + headers = dict(API_CONFIG.get("headers", {}))
145 + if Authorization:
146 + headers['Authorization'] = Authorization
147 + # 移除 Content-Type(由 requests 根据 multipart 自动设置)
148 + headers.pop("Content-Type", None)
149 +
150 + try:
151 + resp = requests.post(url, headers=headers, data=data, files=files, timeout=60)
152 + # 确保文件句柄尽快关闭
153 + try:
154 + files["file"][1].close()
155 + except Exception:
156 + pass
157 +
158 + if resp.status_code == 200:
159 + # 解析响应JSON
160 + result = resp.json()
161 +
162 + # 提取指定字段,只返回 status、message、chmCode、chmId
163 + filtered_result = {}
164 + if "status" in result:
165 + filtered_result["status"] = result["status"]
166 + if "message" in result:
167 + filtered_result["message"] = result["message"]
168 + if "chmCode" in result:
169 + filtered_result["chmCode"] = result["chmCode"]
170 + if "chmId" in result:
171 + filtered_result["chmId"] = result["chmId"]
172 +
173 + # 返回过滤后的JSON字符串
174 + return json.dumps(filtered_result, ensure_ascii=False)
175 + return f"上传失败: HTTP {resp.status_code}, 错误信息: {resp.text}"
176 +
177 + except requests.exceptions.RequestException as e:
178 + return f"网络请求失败: {str(e)}"
179 + except Exception as e:
180 + return f"上传文件进行OCR识别失败: {str(e)}"
181 +
......
...@@ -133,6 +133,70 @@ def create_waybill_d(consignmentCode: str, Authorization: str = None) -> str: ...@@ -133,6 +133,70 @@ def create_waybill_d(consignmentCode: str, Authorization: str = None) -> str:
133 return f"❌ 创建D类运单失败: {str(e)}" 133 return f"❌ 创建D类运单失败: {str(e)}"
134 134
135 135
136 +def create_waybill_d_with_id(
137 + consignmentCode: str,
138 + consignmentId: str,
139 + loginName: str,
140 + userId: int,
141 + Authorization: str = None,
142 +) -> str:
143 + """根据运单ID及运单号创建D类运单
144 +
145 + Args:
146 + consignmentCode: 运单号(必填)
147 + consignmentId: 运单ID(必填)
148 + loginName: 登录名(必填)
149 + userId: 用户ID(必填)
150 + Authorization: 授权令牌,如果提供则使用该值,否则使用 API_CONFIG 中的默认值
151 +
152 + Returns:
153 + 创建结果,成功时返回序列化的JSON,失败时返回错误信息
154 + """
155 + try:
156 + url = f"{API_CONFIG['base_url']}/Exp/bus-customer/consignment/createChmConsignmentD/demo"
157 +
158 + # 构建请求数据
159 + data = {
160 + "consignmentCode": consignmentCode,
161 + "consignmentId": consignmentId,
162 + "isAIRecognition": 1, # 写死为1
163 + "loginName": loginName,
164 + "userId": userId
165 + }
166 +
167 + # 构建 headers,优先使用传入的 Authorization
168 + headers = dict(API_CONFIG['headers'])
169 + if Authorization:
170 + headers['Authorization'] = Authorization
171 +
172 + # 发送POST请求
173 + response = requests.post(
174 + url,
175 + headers=headers,
176 + json=data,
177 + timeout=30
178 + )
179 +
180 + # 检查响应状态
181 + if response.status_code == 200:
182 + result = response.json()
183 + # 提取运单ID
184 + data = result.get('data') or {}
185 + extra = data.get('extra') or {}
186 + waybill_id = extra.get('id')
187 + if waybill_id is not None:
188 + return waybill_id # 返回数字格式的运单ID
189 + return f"❌ 创建失败: 响应中未找到运单ID"
190 + return f"❌ 创建失败: HTTP {response.status_code}, 错误信息: {response.text}"
191 +
192 + except requests.exceptions.RequestException as e:
193 + return f"❌ 网络请求失败: {str(e)}"
194 + except json.JSONDecodeError as e:
195 + return f"❌ 响应解析失败: {str(e)}"
196 + except Exception as e:
197 + return f"❌ 创建D类运单失败: {str(e)}"
198 +
199 +
136 def query_waybill_info(consignmentCode: str, Authorization: str = None) -> str: 200 def query_waybill_info(consignmentCode: str, Authorization: str = None) -> str:
137 """根据运单编号查询运单信息 201 """根据运单编号查询运单信息
138 202
......
...@@ -12,8 +12,8 @@ from langchain_core.messages import AnyMessage ...@@ -12,8 +12,8 @@ from langchain_core.messages import AnyMessage
12 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 12 sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
13 13
14 # 导入 API 模块 14 # 导入 API 模块
15 -from API.waybill_api import query_waybill_list, create_waybill_d, push_waybill_for_ocr, query_waybill_info 15 +from API.waybill_api import query_waybill_list, create_waybill_d, push_waybill_for_ocr, query_waybill_info, create_waybill_d_with_id
16 -from API.paperless_api import upload_clearance_file 16 +from API.paperless_api import upload_clearance_file, upload_file_for_ocr
17 17
18 # 导入工具类 18 # 导入工具类
19 from langgraph_examples.utils.message_processor import MessageProcessor 19 from langgraph_examples.utils.message_processor import MessageProcessor
...@@ -91,20 +91,28 @@ def pre_model_inspect_attachments(state, **kwargs): ...@@ -91,20 +91,28 @@ def pre_model_inspect_attachments(state, **kwargs):
91 return {} 91 return {}
92 92
93 93
94 -def extract_token(state: Dict[str, Any]) -> str: 94 +def extract_token(state: Dict[str, Any]) -> Dict[str, Any]:
95 """ 95 """
96 - 从 state 中提取 token 96 + 从 state 中提取参数
97 - 获取最后一个类型为 HumanMessage 或 human 的消息中的 token 97 + 获取最后一个类型为 HumanMessage 或 human 的消息中的参数(token、consignmentCode、consignmentId、loginName、userId)
98 98
99 Args: 99 Args:
100 state: LangGraph 状态字典,包含 messages 数组 100 state: LangGraph 状态字典,包含 messages 数组
101 101
102 Returns: 102 Returns:
103 - token 字符串,如果未找到则返回空字符串 103 + 包含所有参数的字典,如果未找到则返回空字符串或None
104 """ 104 """
105 + result = {
106 + "token": "",
107 + "consignmentCode": None,
108 + "consignmentId": None,
109 + "loginName": None,
110 + "userId": None
111 + }
112 +
105 messages = state.get("messages", []) 113 messages = state.get("messages", [])
106 if not messages: 114 if not messages:
107 - return "" 115 + return result
108 116
109 # 找到所有 is_human 类型的消息 117 # 找到所有 is_human 类型的消息
110 human_messages = [] 118 human_messages = []
...@@ -122,32 +130,45 @@ def extract_token(state: Dict[str, Any]) -> str: ...@@ -122,32 +130,45 @@ def extract_token(state: Dict[str, Any]) -> str:
122 130
123 # 如果没有 human 消息,直接返回 131 # 如果没有 human 消息,直接返回
124 if not human_messages: 132 if not human_messages:
125 - return "" 133 + return result
126 134
127 # 直接取最后一个 human 消息(不需要循环判断) 135 # 直接取最后一个 human 消息(不需要循环判断)
128 last_human_msg = human_messages[-1] 136 last_human_msg = human_messages[-1]
129 137
130 - # 从 content 中提取 token 138 + # 从 content 中提取参数
131 if isinstance(last_human_msg, dict): 139 if isinstance(last_human_msg, dict):
132 content = last_human_msg.get("content") 140 content = last_human_msg.get("content")
133 else: 141 else:
134 content = getattr(last_human_msg, "content", None) 142 content = getattr(last_human_msg, "content", None)
135 143
136 if isinstance(content, list): 144 if isinstance(content, list):
137 - # content 是列表,遍历查找包含 token 的 part 145 + # content 是列表,遍历查找包含参数的 part
138 for part in content: 146 for part in content:
139 - if isinstance(part, dict) and "token" in part: 147 + if isinstance(part, dict):
140 - token = part.get("token") 148 + if "token" in part and part.get("token"):
141 - if token: 149 + result["token"] = part.get("token")
142 - return token 150 + if "consignmentCode" in part:
151 + result["consignmentCode"] = part.get("consignmentCode")
152 + if "consignmentId" in part:
153 + result["consignmentId"] = part.get("consignmentId")
154 + if "loginName" in part:
155 + result["loginName"] = part.get("loginName")
156 + if "userId" in part:
157 + result["userId"] = part.get("userId")
143 elif isinstance(content, dict): 158 elif isinstance(content, dict):
144 - # content 是字典,直接获取 token 159 + # content 是字典,直接获取参数
145 - if "token" in content: 160 + if "token" in content and content.get("token"):
146 - token = content.get("token") 161 + result["token"] = content.get("token")
147 - if token: 162 + if "consignmentCode" in content:
148 - return token 163 + result["consignmentCode"] = content.get("consignmentCode")
164 + if "consignmentId" in content:
165 + result["consignmentId"] = content.get("consignmentId")
166 + if "loginName" in content:
167 + result["loginName"] = content.get("loginName")
168 + if "userId" in content:
169 + result["userId"] = content.get("userId")
149 170
150 - return "" 171 + return result
151 172
152 def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List[AnyMessage]: 173 def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List[AnyMessage]:
153 """ 174 """
...@@ -166,14 +187,23 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List ...@@ -166,14 +187,23 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
166 # print(f"state keys: {list(state.keys()) if isinstance(state, dict) else 'not a dict'}") 187 # print(f"state keys: {list(state.keys()) if isinstance(state, dict) else 'not a dict'}")
167 188
168 # 从 state 中提取动态参数 189 # 从 state 中提取动态参数
169 - token = extract_token(state) 190 + params = extract_token(state)
191 + token = params.get("token", "")
192 + consignmentCode = params.get("consignmentCode")
193 + consignmentId = params.get("consignmentId")
194 + loginName = params.get("loginName")
195 + userId = params.get("userId")
170 196
171 # 如果从 state 中提取的 token 为空,则从 api_config.py 中获取 Authorization 作为备选 197 # 如果从 state 中提取的 token 为空,则从 api_config.py 中获取 Authorization 作为备选
172 if not token: 198 if not token:
173 from API.api_config import API_CONFIG 199 from API.api_config import API_CONFIG
174 token = API_CONFIG.get("headers", {}).get("Authorization", "") 200 token = API_CONFIG.get("headers", {}).get("Authorization", "")
175 - # 201 +
176 - # print(f"提取到的 token: {token[:30] if token else 'None'}...") 202 + # 格式化参数值用于提示词显示
203 + consignmentCode_str = str(consignmentCode) if consignmentCode is not None else "无"
204 + consignmentId_str = str(consignmentId) if consignmentId is not None else "无"
205 + loginName_str = str(loginName) if loginName is not None else "无"
206 + userId_str = str(userId) if userId is not None else "无"
177 207
178 # 创建系统提示词(使用 f-string 以便插入 token) 208 # 创建系统提示词(使用 f-string 以便插入 token)
179 system_msg = f"""你是一个专业的出口物流系统智能助手,专门帮助用户处理运单相关的业务操作。 209 system_msg = f"""你是一个专业的出口物流系统智能助手,专门帮助用户处理运单相关的业务操作。
...@@ -183,9 +213,16 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List ...@@ -183,9 +213,16 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
183 - **查询运单列表**:当用户查询运单但不提供运单号时,使用 query_waybill_list 查询运单列表,支持按状态、时间等条件筛选,结果以JSON形式展示,AI不用对返回数据JSON进行加工 213 - **查询运单列表**:当用户查询运单但不提供运单号时,使用 query_waybill_list 查询运单列表,支持按状态、时间等条件筛选,结果以JSON形式展示,AI不用对返回数据JSON进行加工
184 - **查询单个运单详情**:当用户提供运单号查询时,使用 query_waybill_info 查询该运单的详细信息(包括运单基本信息、公司信息、商品明细等),返回格式与 create_waybill_d 相同 214 - **查询单个运单详情**:当用户提供运单号查询时,使用 query_waybill_info 查询该运单的详细信息(包括运单基本信息、公司信息、商品明细等),返回格式与 create_waybill_d 相同
185 2. **运单创建**:协助用户创建D类运单,确保信息完整准确 215 2. **运单创建**:协助用户创建D类运单,确保信息完整准确
186 - - **重要说明**:当用户输入"创建运单"或"申报"时,都理解为"创建运单"操作,应调用 create_waybill_d 工具 216 + - **优先级规则**:当用户输入"创建运单"或"申报"时,首先检查提示词中是否已有 consignmentCode、consignmentId、loginName、userId 且都不为None
187 -3. **业务咨询**:解答用户关于出口物流流程、运单状态、操作规范等问题 217 + - **如果提示词中有这些参数且都不为None**:必须使用 create_waybill_d_with_id 工具创建运单,参数从提示词中获取
188 -4. **您当前访问工具Authorization的传参为 Authorization= {token} 218 + - **如果提示词中没有这些参数或为None**:使用 create_waybill_d 工具,运单编号需要从用户输入中获取
219 + - **create_waybill_d**:根据运单号创建D类运单,运单编号需要从用户输入中获取,不是从提示词中获取。仅在提示词中没有 consignmentCode、consignmentId、loginName、userId 或这些参数为None时使用
220 + - **create_waybill_d_with_id**:根据运单ID及运单号创建D类运单,参数从提示词中获取(consignmentCode、consignmentId、loginName、userId),不需要用户再次输入。当提示词中这些参数齐全且都不为None时,必须使用此工具
221 +3. **文件上传**:根据用户需求上传文件
222 + - **上传清关文件**:当用户需要上传清关文件并关联运单信息(code、slip_id)时,使用 upload_clearance_file 工具
223 + - **上传文件进行OCR识别**:当用户只有一个上传文件地址,需要直接进行OCR识别时,使用 upload_file_for_ocr 工具
224 +4. **业务咨询**:解答用户关于出口物流流程、运单状态、操作规范等问题
225 +5. **您当前访问工具Authorization的传参为 Authorization= {token}
189 226
190 ## 工作原则: 227 ## 工作原则:
191 - 上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口 228 - 上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口
...@@ -203,8 +240,17 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List ...@@ -203,8 +240,17 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
203 - query_waybill_info: 根据运单编号查询单个运单的详细信息(包括运单基本信息、公司信息、商品明细等),需要提供参数(运单编号、Authorization),返回格式与 create_waybill_d 相同 240 - query_waybill_info: 根据运单编号查询单个运单的详细信息(包括运单基本信息、公司信息、商品明细等),需要提供参数(运单编号、Authorization),返回格式与 create_waybill_d 相同
204 - **使用场景**:当用户提供运单号查询运单详情时使用此工具 241 - **使用场景**:当用户提供运单号查询运单详情时使用此工具
205 - create_waybill_d: 根据运单号创建D类运单,需要提供参数(运单号、Authorization) 242 - create_waybill_d: 根据运单号创建D类运单,需要提供参数(运单号、Authorization)
206 - - **注意**:当用户说"创建运单"或"申报"时,都应调用此工具 243 + - **使用场景**:当用户输入"创建运单"或"申报"时,仅在提示词中没有 consignmentCode、consignmentId、loginName、userId 或这些参数为None时使用此工具
244 + - **重要说明**:运单编号需要从用户输入中获取,不是从提示词中获取
245 + - **优先级**:如果提示词中已有 consignmentCode、consignmentId、loginName、userId 且都不为None,则不应使用此工具,应使用 create_waybill_d_with_id
246 +- create_waybill_d_with_id: 根据运单ID及运单号创建D类运单,需要提供参数(运单编号、运单ID、登录名、用户ID、Authorization)
247 + - **使用场景**:当用户输入"创建运单"或"申报"时,如果提示词中已经有了 consignmentCode、consignmentId、loginName、userId,并且这几个参数都不为None,那么必须调用此工具创建运单
248 + - **重要说明**:此工具的参数从提示词中获取,不需要用户再次输入。当提示词中这些参数齐全且都不为None时,必须优先使用此工具,而不是 create_waybill_d
249 + - **可用参数**:您当前访问工具 create_waybill_d_with_id 的传参为 consignmentCode={consignmentCode_str}、consignmentId={consignmentId_str}、loginName={loginName_str}、userId={userId_str},如果参数有值则使用,没有值则不使用
207 - upload_clearance_file: 上传清关PDF文件,需要 code、slip_id、pdf_path、Authorization,上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口 250 - upload_clearance_file: 上传清关PDF文件,需要 code、slip_id、pdf_path、Authorization,上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口
251 + - **使用场景**:当用户需要上传清关文件并关联运单信息(code、slip_id)时使用此工具
252 +- upload_file_for_ocr: 上传文件进行OCR识别,只需要提供文件路径(pdf_path、Authorization),上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,请直接调用工具接口
253 + - **使用场景**:当用户只有一个上传文件地址,需要直接进行OCR识别时,使用此工具
208 - push_waybill_for_ocr: 根据运单ID推送OCR进行识别,需要提供运单ID(waybill_id、Authorization)参数 254 - push_waybill_for_ocr: 根据运单ID推送OCR进行识别,需要提供运单ID(waybill_id、Authorization)参数
209 255
210 ## query_waybill_list数据展示说明: 256 ## query_waybill_list数据展示说明:
...@@ -219,6 +265,12 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List ...@@ -219,6 +265,12 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
219 ## create_waybill_d数据展示说明: 265 ## create_waybill_d数据展示说明:
220 - 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。 266 - 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
221 267
268 +## create_waybill_d_with_id数据展示说明:
269 +- 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
270 +
271 +## upload_file_for_ocr数据展示说明:
272 +- 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
273 +
222 请根据用户的具体需求,选择合适的工具并提供帮助。""" 274 请根据用户的具体需求,选择合适的工具并提供帮助。"""
223 275
224 # 返回系统消息 + 原始消息 276 # 返回系统消息 + 原始消息
...@@ -231,7 +283,7 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List ...@@ -231,7 +283,7 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
231 # 创建 ReAct 智能体 283 # 创建 ReAct 智能体
232 agent = create_react_agent( 284 agent = create_react_agent(
233 model=model, 285 model=model,
234 - tools=[query_waybill_list, query_waybill_info, create_waybill_d, upload_clearance_file, push_waybill_for_ocr], 286 + tools=[query_waybill_list, query_waybill_info, create_waybill_d, create_waybill_d_with_id, upload_clearance_file, upload_file_for_ocr, push_waybill_for_ocr],
235 pre_model_hook=pre_model_inspect_attachments, 287 pre_model_hook=pre_model_inspect_attachments,
236 prompt=_create_system_prompt, 288 prompt=_create_system_prompt,
237 ) 289 )
......