zhouhui.jiang

update

......@@ -13,7 +13,7 @@ API_CONFIG = {
"headers": {
"accept": "*/*",
"Content-Type": "application/json",
"Authorization": os.getenv("API_AUTHORIZATION", "Bearer 2.21e3deecca904c7697ab891b8276cf36"),
"Authorization": os.getenv("API_AUTHORIZATION", "Bearer 2.bd593d8f36c4468fbf02ee95c473cbb6"),
"Ver": os.getenv("API_VER", "033BD94B1168D7E4F0D644C3C95E35BF.D73E33B659AD1D6B7D181D1DF8D05760"),
"Referer": os.getenv("API_REFERER", "http://192.168.1.251/")
}
......
......@@ -103,3 +103,79 @@ def upload_clearance_file(
return f"上传清关文件失败: {str(e)}"
def upload_file_for_ocr(
pdf_path: str,
Authorization: Optional[str] = None,
) -> str:
"""上传文件进行OCR识别
Args:
pdf_path: 本地 PDF 文件路径(必填)
Authorization: 授权令牌,如果提供则使用该值,否则使用 API_CONFIG 中的默认值
Returns:
文本:成功/失败信息
"""
if not os.path.isfile(pdf_path):
return f"参数错误: 文件不存在 - {pdf_path}"
# 从文件路径提取文件名
file_name = os.path.basename(pdf_path)
# 构建URL(不包含查询参数)
url = f"{API_CONFIG['base_url']}/API/ocrDemo/pushDemo"
# 封装参数到 params_payload
params_payload = {
"fileName01": file_name,
"fileNumber": 1
}
# form-data: params 是 JSON 字符串,fileUpload 是文件
data = {
"params": json.dumps(params_payload, ensure_ascii=False),
}
files = {
"file": (file_name, open(pdf_path, "rb"), "application/pdf"),
}
# 构建 headers,优先使用传入的 Authorization
headers = dict(API_CONFIG.get("headers", {}))
if Authorization:
headers['Authorization'] = Authorization
# 移除 Content-Type(由 requests 根据 multipart 自动设置)
headers.pop("Content-Type", None)
try:
resp = requests.post(url, headers=headers, data=data, files=files, timeout=60)
# 确保文件句柄尽快关闭
try:
files["file"][1].close()
except Exception:
pass
if resp.status_code == 200:
# 解析响应JSON
result = resp.json()
# 提取指定字段,只返回 status、message、chmCode、chmId
filtered_result = {}
if "status" in result:
filtered_result["status"] = result["status"]
if "message" in result:
filtered_result["message"] = result["message"]
if "chmCode" in result:
filtered_result["chmCode"] = result["chmCode"]
if "chmId" in result:
filtered_result["chmId"] = result["chmId"]
# 返回过滤后的JSON字符串
return json.dumps(filtered_result, ensure_ascii=False)
return f"上传失败: HTTP {resp.status_code}, 错误信息: {resp.text}"
except requests.exceptions.RequestException as e:
return f"网络请求失败: {str(e)}"
except Exception as e:
return f"上传文件进行OCR识别失败: {str(e)}"
......
......@@ -133,6 +133,70 @@ def create_waybill_d(consignmentCode: str, Authorization: str = None) -> str:
return f"❌ 创建D类运单失败: {str(e)}"
def create_waybill_d_with_id(
consignmentCode: str,
consignmentId: str,
loginName: str,
userId: int,
Authorization: str = None,
) -> str:
"""根据运单ID及运单号创建D类运单
Args:
consignmentCode: 运单号(必填)
consignmentId: 运单ID(必填)
loginName: 登录名(必填)
userId: 用户ID(必填)
Authorization: 授权令牌,如果提供则使用该值,否则使用 API_CONFIG 中的默认值
Returns:
创建结果,成功时返回序列化的JSON,失败时返回错误信息
"""
try:
url = f"{API_CONFIG['base_url']}/Exp/bus-customer/consignment/createChmConsignmentD/demo"
# 构建请求数据
data = {
"consignmentCode": consignmentCode,
"consignmentId": consignmentId,
"isAIRecognition": 1, # 写死为1
"loginName": loginName,
"userId": userId
}
# 构建 headers,优先使用传入的 Authorization
headers = dict(API_CONFIG['headers'])
if Authorization:
headers['Authorization'] = Authorization
# 发送POST请求
response = requests.post(
url,
headers=headers,
json=data,
timeout=30
)
# 检查响应状态
if response.status_code == 200:
result = response.json()
# 提取运单ID
data = result.get('data') or {}
extra = data.get('extra') or {}
waybill_id = extra.get('id')
if waybill_id is not None:
return waybill_id # 返回数字格式的运单ID
return f"❌ 创建失败: 响应中未找到运单ID"
return f"❌ 创建失败: HTTP {response.status_code}, 错误信息: {response.text}"
except requests.exceptions.RequestException as e:
return f"❌ 网络请求失败: {str(e)}"
except json.JSONDecodeError as e:
return f"❌ 响应解析失败: {str(e)}"
except Exception as e:
return f"❌ 创建D类运单失败: {str(e)}"
def query_waybill_info(consignmentCode: str, Authorization: str = None) -> str:
"""根据运单编号查询运单信息
......
......@@ -12,8 +12,8 @@ from langchain_core.messages import AnyMessage
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, query_waybill_info
from API.paperless_api import upload_clearance_file
from API.waybill_api import query_waybill_list, create_waybill_d, push_waybill_for_ocr, query_waybill_info, create_waybill_d_with_id
from API.paperless_api import upload_clearance_file, upload_file_for_ocr
# 导入工具类
from langgraph_examples.utils.message_processor import MessageProcessor
......@@ -91,20 +91,28 @@ def pre_model_inspect_attachments(state, **kwargs):
return {}
def extract_token(state: Dict[str, Any]) -> str:
def extract_token(state: Dict[str, Any]) -> Dict[str, Any]:
"""
从 state 中提取 token
获取最后一个类型为 HumanMessage 或 human 的消息中的 token
从 state 中提取参数
获取最后一个类型为 HumanMessage 或 human 的消息中的参数(token、consignmentCode、consignmentId、loginName、userId)
Args:
state: LangGraph 状态字典,包含 messages 数组
Returns:
token 字符串,如果未找到则返回空字符串
包含所有参数的字典,如果未找到则返回空字符串或None
"""
result = {
"token": "",
"consignmentCode": None,
"consignmentId": None,
"loginName": None,
"userId": None
}
messages = state.get("messages", [])
if not messages:
return ""
return result
# 找到所有 is_human 类型的消息
human_messages = []
......@@ -122,32 +130,45 @@ def extract_token(state: Dict[str, Any]) -> str:
# 如果没有 human 消息,直接返回
if not human_messages:
return ""
return result
# 直接取最后一个 human 消息(不需要循环判断)
last_human_msg = human_messages[-1]
# 从 content 中提取 token
# 从 content 中提取参数
if isinstance(last_human_msg, dict):
content = last_human_msg.get("content")
else:
content = getattr(last_human_msg, "content", None)
if isinstance(content, list):
# content 是列表,遍历查找包含 token 的 part
# content 是列表,遍历查找包含参数的 part
for part in content:
if isinstance(part, dict) and "token" in part:
token = part.get("token")
if token:
return token
if isinstance(part, dict):
if "token" in part and part.get("token"):
result["token"] = part.get("token")
if "consignmentCode" in part:
result["consignmentCode"] = part.get("consignmentCode")
if "consignmentId" in part:
result["consignmentId"] = part.get("consignmentId")
if "loginName" in part:
result["loginName"] = part.get("loginName")
if "userId" in part:
result["userId"] = part.get("userId")
elif isinstance(content, dict):
# content 是字典,直接获取 token
if "token" in content:
token = content.get("token")
if token:
return token
# content 是字典,直接获取参数
if "token" in content and content.get("token"):
result["token"] = content.get("token")
if "consignmentCode" in content:
result["consignmentCode"] = content.get("consignmentCode")
if "consignmentId" in content:
result["consignmentId"] = content.get("consignmentId")
if "loginName" in content:
result["loginName"] = content.get("loginName")
if "userId" in content:
result["userId"] = content.get("userId")
return ""
return result
def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List[AnyMessage]:
"""
......@@ -166,14 +187,23 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
# print(f"state keys: {list(state.keys()) if isinstance(state, dict) else 'not a dict'}")
# 从 state 中提取动态参数
token = extract_token(state)
params = extract_token(state)
token = params.get("token", "")
consignmentCode = params.get("consignmentCode")
consignmentId = params.get("consignmentId")
loginName = params.get("loginName")
userId = params.get("userId")
# 如果从 state 中提取的 token 为空,则从 api_config.py 中获取 Authorization 作为备选
if not token:
from API.api_config import API_CONFIG
token = API_CONFIG.get("headers", {}).get("Authorization", "")
#
# print(f"提取到的 token: {token[:30] if token else 'None'}...")
# 格式化参数值用于提示词显示
consignmentCode_str = str(consignmentCode) if consignmentCode is not None else "无"
consignmentId_str = str(consignmentId) if consignmentId is not None else "无"
loginName_str = str(loginName) if loginName is not None else "无"
userId_str = str(userId) if userId is not None else "无"
# 创建系统提示词(使用 f-string 以便插入 token)
system_msg = f"""你是一个专业的出口物流系统智能助手,专门帮助用户处理运单相关的业务操作。
......@@ -183,9 +213,16 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
- **查询运单列表**:当用户查询运单但不提供运单号时,使用 query_waybill_list 查询运单列表,支持按状态、时间等条件筛选,结果以JSON形式展示,AI不用对返回数据JSON进行加工
- **查询单个运单详情**:当用户提供运单号查询时,使用 query_waybill_info 查询该运单的详细信息(包括运单基本信息、公司信息、商品明细等),返回格式与 create_waybill_d 相同
2. **运单创建**:协助用户创建D类运单,确保信息完整准确
- **重要说明**:当用户输入"创建运单"或"申报"时,都理解为"创建运单"操作,应调用 create_waybill_d 工具
3. **业务咨询**:解答用户关于出口物流流程、运单状态、操作规范等问题
4. **您当前访问工具Authorization的传参为 Authorization= {token}
- **优先级规则**:当用户输入"创建运单"或"申报"时,首先检查提示词中是否已有 consignmentCode、consignmentId、loginName、userId 且都不为None
- **如果提示词中有这些参数且都不为None**:必须使用 create_waybill_d_with_id 工具创建运单,参数从提示词中获取
- **如果提示词中没有这些参数或为None**:使用 create_waybill_d 工具,运单编号需要从用户输入中获取
- **create_waybill_d**:根据运单号创建D类运单,运单编号需要从用户输入中获取,不是从提示词中获取。仅在提示词中没有 consignmentCode、consignmentId、loginName、userId 或这些参数为None时使用
- **create_waybill_d_with_id**:根据运单ID及运单号创建D类运单,参数从提示词中获取(consignmentCode、consignmentId、loginName、userId),不需要用户再次输入。当提示词中这些参数齐全且都不为None时,必须使用此工具
3. **文件上传**:根据用户需求上传文件
- **上传清关文件**:当用户需要上传清关文件并关联运单信息(code、slip_id)时,使用 upload_clearance_file 工具
- **上传文件进行OCR识别**:当用户只有一个上传文件地址,需要直接进行OCR识别时,使用 upload_file_for_ocr 工具
4. **业务咨询**:解答用户关于出口物流流程、运单状态、操作规范等问题
5. **您当前访问工具Authorization的传参为 Authorization= {token}
## 工作原则:
- 上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口
......@@ -203,8 +240,17 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
- query_waybill_info: 根据运单编号查询单个运单的详细信息(包括运单基本信息、公司信息、商品明细等),需要提供参数(运单编号、Authorization),返回格式与 create_waybill_d 相同
- **使用场景**:当用户提供运单号查询运单详情时使用此工具
- create_waybill_d: 根据运单号创建D类运单,需要提供参数(运单号、Authorization)
- **注意**:当用户说"创建运单"或"申报"时,都应调用此工具
- **使用场景**:当用户输入"创建运单"或"申报"时,仅在提示词中没有 consignmentCode、consignmentId、loginName、userId 或这些参数为None时使用此工具
- **重要说明**:运单编号需要从用户输入中获取,不是从提示词中获取
- **优先级**:如果提示词中已有 consignmentCode、consignmentId、loginName、userId 且都不为None,则不应使用此工具,应使用 create_waybill_d_with_id
- create_waybill_d_with_id: 根据运单ID及运单号创建D类运单,需要提供参数(运单编号、运单ID、登录名、用户ID、Authorization)
- **使用场景**:当用户输入"创建运单"或"申报"时,如果提示词中已经有了 consignmentCode、consignmentId、loginName、userId,并且这几个参数都不为None,那么必须调用此工具创建运单
- **重要说明**:此工具的参数从提示词中获取,不需要用户再次输入。当提示词中这些参数齐全且都不为None时,必须优先使用此工具,而不是 create_waybill_d
- **可用参数**:您当前访问工具 create_waybill_d_with_id 的传参为 consignmentCode={consignmentCode_str}、consignmentId={consignmentId_str}、loginName={loginName_str}、userId={userId_str},如果参数有值则使用,没有值则不使用
- upload_clearance_file: 上传清关PDF文件,需要 code、slip_id、pdf_path、Authorization,上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,不用确认文件索引,工具会自动设置索引为0,请直接调用工具接口
- **使用场景**:当用户需要上传清关文件并关联运单信息(code、slip_id)时使用此工具
- upload_file_for_ocr: 上传文件进行OCR识别,只需要提供文件路径(pdf_path、Authorization),上传时文件路径不用确认,路径肯定是完整的,文件肯定是存在的,请直接调用工具接口
- **使用场景**:当用户只有一个上传文件地址,需要直接进行OCR识别时,使用此工具
- push_waybill_for_ocr: 根据运单ID推送OCR进行识别,需要提供运单ID(waybill_id、Authorization)参数
## query_waybill_list数据展示说明:
......@@ -219,6 +265,12 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
## create_waybill_d数据展示说明:
- 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
## create_waybill_d_with_id数据展示说明:
- 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
## upload_file_for_ocr数据展示说明:
- 按照数据返回的原本格式进行展示,不得增删前后缀或改写内容。
请根据用户的具体需求,选择合适的工具并提供帮助。"""
# 返回系统消息 + 原始消息
......@@ -231,7 +283,7 @@ def _create_system_prompt(state: Dict[str, Any], config: RunnableConfig) -> List
# 创建 ReAct 智能体
agent = create_react_agent(
model=model,
tools=[query_waybill_list, query_waybill_info, create_waybill_d, upload_clearance_file, push_waybill_for_ocr],
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],
pre_model_hook=pre_model_inspect_attachments,
prompt=_create_system_prompt,
)
......