output_presenter.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. import json
  2. import logging
  3. from constants import DEVICE_INVALID_MESSAGE
  4. logger = logging.getLogger(__name__)
  5. # Builds AI-safe MCP results for the explicitly supported tool set.
  6. class OutputPresenter:
  7. TABLE_TOOLS = frozenset((
  8. 'query_order_exact',
  9. 'query_track',
  10. 'query_customs_declaration_files',
  11. 'query_outbound_list',
  12. ))
  13. DETAIL_TOOLS = frozenset(('query_outbound_detail',))
  14. OPTION_TOOLS = frozenset((
  15. 'list_order_filter_options',
  16. 'list_outbound_filter_options',
  17. 'list_pending_outbound_export_filter_options',
  18. ))
  19. EXPORT_TOOLS = frozenset((
  20. 'export_pending_outbound_orders',
  21. 'export_out_of_province_port_data',
  22. ))
  23. SAFE_TOOLS = TABLE_TOOLS | DETAIL_TOOLS | OPTION_TOOLS | EXPORT_TOOLS
  24. TABLE_COLUMNS = {
  25. 'query_order_exact': {
  26. 'order_number': ('订单号',),
  27. 'reference_number': ('客户参考号',),
  28. 'status_txt_name': ('状态',),
  29. 'check_status_txt_name': ('是否已查验',),
  30. 'customer_name': ('客户名称',),
  31. 'customer_account_type_name': ('客户属性',),
  32. 'inbound_date': ('入库时间',),
  33. 'wo_num': ('未完成工单',),
  34. 'product_name': ('物流产品',),
  35. 'inbound_pieces': ('件数',),
  36. 'inbound_volume': ('体积(CBM)',),
  37. 'inbound_weight': ('重量(KG)',),
  38. 'pro_cn_name': ('品名',),
  39. 'export_declaration_type': ('报关方式',),
  40. 'merge_declare_number': ('合并报关单号',),
  41. 'delivery_address': ('派送地址',),
  42. 'container_code': ('柜号',),
  43. 'out_status_txt': ('排舱单状态',),
  44. 'hinge_of_destination': ('目的港',),
  45. 'etd': ('ETD',),
  46. 'atd': ('ATD',),
  47. 'eta': ('ETA',),
  48. 'ata': ('ATA',),
  49. 'release_time': ('清关放行时间',),
  50. 'oversea_inbound_date': ('海外入库时间',),
  51. 'appt_time': ('APPT时间',),
  52. 'est_loading_time': ('预计装柜时间',),
  53. 'pickup_time': ('海外提柜时间',),
  54. 'delivery_way_title': ('派送方式',),
  55. 'tracking_number': ('快递单号', '承运商跟踪号码'),
  56. 'shipment_id': ('Shipment ID',),
  57. 'goods_attribute': ('商品属性',),
  58. 'sku': ('SKU',),
  59. 'sales_user': ('商务经理',),
  60. 'service_user': ('客户经理',),
  61. 'department_name': ('事业部',),
  62. 'remark': ('订单备注',),
  63. 'importer_name': ('进口商',),
  64. 'warehouse_name': ('交货仓库',),
  65. 'paid_status_name': ('付款状态',),
  66. },
  67. 'query_track': {
  68. 'status': ('轨迹节点', '轨迹状态,如:开始集港、离港放行、清关、配送等'),
  69. 'location': ('轨迹地点', '发生地点'),
  70. 'time': ('时间', '轨迹发生时间(已转换为用户时区)'),
  71. 'content': ('轨迹内容', '详细描述'),
  72. 'tracking_number': ('跟踪号', '快递单号'),
  73. 'shipment_id': ('Shipment ID', '包裹ID'),
  74. },
  75. 'query_customs_declaration_files': {
  76. 'outbound_number': ('排舱单号',),
  77. 'order_number': ('订单号',),
  78. 'file_name': ('文件名',),
  79. 'file_type': ('文件类型',),
  80. 'file_url': ('文件链接',),
  81. },
  82. 'query_outbound_list': {
  83. 'outbound_number': ('排舱单号',),
  84. 'direct_send': ('直送柜',),
  85. 'remark': ('备注',),
  86. 'cargo_type': ('超长超重',),
  87. 'warehouse_name': ('集货仓库',),
  88. 'status': ('状态',),
  89. 'so_number': ('SO号',),
  90. 'container_code': ('柜号',),
  91. 'seal_number': ('封条号',),
  92. 'container_type': ('柜型',),
  93. 'shipping_method': ('运输方式',),
  94. 'total_volume': ('体积(CBM)',),
  95. 'total_weight': ('重量(KG)',),
  96. 'ship_schedule': ('船期',),
  97. 'closing_time': ('截关时间',),
  98. 'est_loading_time': ('预计装柜时间',),
  99. 'operator': ('操作人',),
  100. 'operation_modes': ('拖报清方式',),
  101. 'operation_time': ('操作时间',),
  102. },
  103. 'query_outbound_detail': {
  104. 'order_number': ('订单号',),
  105. 'cargo_type': ('超长超重',),
  106. 'customs_files': ('报关资料',),
  107. 'reference_number': ('客户参考号',),
  108. 'customer_name': ('客户名称',),
  109. 'declaration_type': ('报关方式',),
  110. 'merge_declare_number': ('合并报关单号',),
  111. 'order_remark': ('订单备注',),
  112. 'bl_number': ('提单号',),
  113. 'container_code': ('柜号',),
  114. 'customer_service': ('客户经理',),
  115. 'est_inbound_date': ('预计入库时间',),
  116. 'inbound_date': ('实际入库时间',),
  117. 'status': ('状态',),
  118. 'pieces': ('预报件数 / 入库件数',),
  119. 'weight': ('预报实重(KG) / 入库实重(KG)',),
  120. 'volume': ('预报体积(m³) / 入库体积(m³)',),
  121. 'pickup_place': ('交货地',),
  122. 'product_name': ('物流产品',),
  123. 'goods_name': ('品名',),
  124. 'closing_time': ('截关时间',),
  125. 'clearance_remark': ('报关备注',),
  126. 'import_clearance_remark': ('清关备注',),
  127. 'delivery_address': ('派送地址',),
  128. 'delivery_type': ('派送类型',),
  129. 'delivery_way': ('派送方式',),
  130. 'channel': ('派送渠道',),
  131. 'country_name': ('目的国',),
  132. 'importer_name': ('进口商',),
  133. 'vat_type': ('清关税号',),
  134. 'packing_type': ('货物类型',),
  135. 'is_abnormal': ('是否问题件',),
  136. 'package_method': ('包装类型',),
  137. 'order_reply': ('到货回复',),
  138. },
  139. }
  140. OUTBOUND_DETAIL_SUMMARY = {
  141. 'bl_number': '提单号',
  142. 'container_code': '柜号',
  143. 'container_type': '柜型',
  144. 'total_volume': '总体积',
  145. 'total_weight': '总重量',
  146. 'total_pieces': '总件数',
  147. 'sku': 'SKU',
  148. 'buy_declaration_count': '买单报关数量',
  149. 'general_declaration_count': '一般贸易报关数量',
  150. 'must_load': '必装单量/体积',
  151. 'backup_load': '备装单量/体积',
  152. }
  153. FIELD_LABELS = {
  154. 'query_order_exact': {
  155. 'order_number': '订单号',
  156. 'order_numbers': '订单号',
  157. 'reference_number': '客户参考号',
  158. 'reference_numbers': '客户参考号',
  159. 'tracking_number': '快递单号',
  160. 'tracking_numbers': '快递单号',
  161. 'outbound_number': '排舱单号',
  162. 'outbound_numbers': '排舱单号',
  163. 'container_code': '柜号',
  164. 'container_codes': '柜号',
  165. 'so_number': 'SO号',
  166. 'so_numbers': 'SO号',
  167. 'shipment_id': 'Shipment ID',
  168. 'receiver_country': '收货国家',
  169. 'product_ids': '物流产品',
  170. 'customer_ids': '客户',
  171. 'sales_id': '销售人员',
  172. 'warehouse_ids': '仓库',
  173. 'department_id': '事业部',
  174. 'inbound_date_start': '入库开始日期',
  175. 'inbound_date_end': '入库结束日期',
  176. 'outbound_date_start': '出库开始日期',
  177. 'outbound_date_end': '出库结束日期',
  178. 'page': '页码',
  179. 'limit': '每页数量',
  180. },
  181. 'query_track': {
  182. 'order_id': '订单ID',
  183. 'order_number': '订单号',
  184. 'tracking_number': '快递单号',
  185. 'page': '页码',
  186. 'limit': '每页数量',
  187. },
  188. 'query_customs_declaration_files': {
  189. 'outbound_numbers': '排舱单号',
  190. 'order_numbers': '订单号',
  191. 'page': '页码',
  192. 'limit': '每页数量',
  193. },
  194. 'query_outbound_list': {
  195. 'outbound_numbers': '排舱单号',
  196. 'order_numbers': '订单号',
  197. 'container_codes': '柜号',
  198. 'so_numbers': 'SO号',
  199. 'bl_numbers': '提单号',
  200. 'outbound_status': '排舱状态',
  201. 'shipping_method': '运输方式',
  202. 'warehouse_id': '集货仓库',
  203. 'is_direct_send': '是否直送柜',
  204. 'page': '页码',
  205. 'limit': '每页数量',
  206. },
  207. 'query_outbound_detail': {
  208. 'outbound_number': '排舱单号',
  209. 'page': '页码',
  210. 'limit': '每页数量',
  211. },
  212. 'list_order_filter_options': {
  213. 'filter_type': '筛选项类型',
  214. 'keyword': '关键词',
  215. 'page': '页码',
  216. 'limit': '每页数量',
  217. },
  218. 'list_outbound_filter_options': {
  219. 'filter_type': '筛选类别',
  220. 'keyword': '显示名称',
  221. 'page': '页码',
  222. 'limit': '每页数量',
  223. },
  224. 'list_pending_outbound_export_filter_options': {
  225. 'filter_type': '筛选项类型',
  226. 'product_type_id': '产品分类',
  227. 'keyword': '关键词',
  228. 'page': '页码',
  229. 'limit': '每页数量',
  230. },
  231. 'export_pending_outbound_orders': {
  232. 'number': '单号',
  233. 'product_type_id': '产品分类',
  234. 'product_id': '物流产品',
  235. 'order_warehouse_id': '集货仓库',
  236. 'receiver_country': '目的国',
  237. 'is_remove': '是否装柜剔除',
  238. 'address': '派送地址',
  239. 'inbound_date': '入库时间',
  240. 'status': '订单状态',
  241. 'merge_declare_number': '合并报关单号',
  242. 'importer_id': '进口商',
  243. 'packing_type': '货物类型',
  244. 'is_battery': '带电属性',
  245. 'is_magnetic': '带磁属性',
  246. 'is_wood': '带木属性',
  247. 'is_other': '其他商品属性',
  248. 'is_fda': 'FDA产品属性',
  249. 'is_toy': '玩具属性',
  250. 'is_ultra_limit': '超长超重属性',
  251. 'is_sensitive': '敏感货属性',
  252. 'is_food': '食品属性',
  253. 'no_property': '无属性',
  254. },
  255. 'export_out_of_province_port_data': {
  256. 'outbound_numbers': '排舱单号',
  257. 'container_codes': '柜号',
  258. 'bl_numbers': '提单号',
  259. 'so_numbers': 'SO号',
  260. 'file_type': '资料类型',
  261. },
  262. }
  263. ERROR_MESSAGES = {
  264. 'MCP_1101': '设备配置已失效,请重新生成配置',
  265. 'MCP_1102': '设备会话已过期,请重新连接',
  266. 'MCP_1103': '员工账号不可用,请联系管理员',
  267. 'MCP_1104': '身份信息已失效,请重新登录或重新生成设备配置',
  268. 'MCP_1201': '工具当前不可用',
  269. 'MCP_1202': '工具当前不可用',
  270. 'MCP_1301': '没有权限使用此工具',
  271. 'MCP_1501': '目标数据不可用',
  272. 'MCP_1502': '结果过多,请缩小查询范围',
  273. 'MCP_1601': '没有可导出的数据',
  274. 'MCP_9001': '工具调用失败,请稍后重试',
  275. }
  276. NON_RETRYABLE_CODES = frozenset((
  277. 'MCP_1101',
  278. 'MCP_1102',
  279. 'MCP_1103',
  280. 'MCP_1104',
  281. 'MCP_1201',
  282. 'MCP_1202',
  283. 'MCP_1301',
  284. 'MCP_1401',
  285. 'MCP_1501',
  286. 'MCP_1502',
  287. 'MCP_1601',
  288. ))
  289. def handles(self, tool_name):
  290. return isinstance(tool_name, str) and tool_name in self.SAFE_TOOLS
  291. # Convert a backend envelope into text and structured display data.
  292. def present(self, tool_name, tool_result):
  293. if not self.handles(tool_name) or not isinstance(tool_result, dict):
  294. return self._format_error()
  295. code = str(tool_result.get('code') or '').strip()
  296. meta = self._build_meta(tool_result.get('meta'))
  297. if code not in ('MCP_0000', '0'):
  298. return self._business_error(
  299. tool_name,
  300. code or 'MCP_9001',
  301. tool_result.get('msg'),
  302. meta,
  303. )
  304. data = tool_result.get('data')
  305. if not isinstance(data, dict):
  306. return self._format_error(meta)
  307. if tool_name in self.DETAIL_TOOLS:
  308. return self._present_outbound_detail(
  309. tool_name,
  310. data,
  311. tool_result.get('meta'),
  312. meta,
  313. )
  314. if tool_name in self.TABLE_TOOLS:
  315. return self._present_table(
  316. tool_name,
  317. data,
  318. tool_result.get('meta'),
  319. meta,
  320. )
  321. if tool_name in self.OPTION_TOOLS:
  322. return self._present_options(data, tool_result.get('meta'), meta)
  323. return self._present_export(data, meta)
  324. # Map local tool exceptions without exposing internal error details.
  325. def present_exception(self, tool_name, exception):
  326. if not self.handles(tool_name):
  327. return self._format_error()
  328. message = str(exception or '').strip()
  329. if isinstance(exception, ValueError):
  330. label = self._find_field_label(tool_name, message)
  331. return self._error_result(
  332. 'MCP_1401',
  333. self._parameter_message(label),
  334. False,
  335. )
  336. if message == DEVICE_INVALID_MESSAGE:
  337. return self._error_result('MCP_1101', DEVICE_INVALID_MESSAGE, False)
  338. if message.lower().startswith('tool disabled'):
  339. return self._error_result('MCP_1202', '工具当前不可用', False)
  340. return self._error_result(
  341. 'MCP_9001',
  342. '工具调用失败,请稍后重试',
  343. True,
  344. )
  345. def _present_table(self, tool_name, data, raw_meta, meta):
  346. columns = data.get('columns')
  347. records = data.get('records')
  348. if not isinstance(columns, list) or not columns or not isinstance(records, list):
  349. return self._format_error(meta)
  350. allowed_columns = self.TABLE_COLUMNS.get(tool_name)
  351. if not isinstance(allowed_columns, dict):
  352. return self._format_error(meta)
  353. headers = []
  354. keys = []
  355. for column in columns:
  356. if not isinstance(column, dict):
  357. return self._format_error(meta)
  358. key = column.get('key')
  359. label = column.get('name')
  360. if not isinstance(key, str) or not key.strip():
  361. return self._format_error(meta)
  362. if not isinstance(label, str) or not label.strip():
  363. return self._format_error(meta)
  364. key = key.strip()
  365. definition = allowed_columns.get(key)
  366. if not isinstance(definition, tuple) or not definition:
  367. return self._format_error(meta)
  368. keys.append(key)
  369. header = {'label': definition[0]}
  370. if len(definition) > 1:
  371. header['description'] = definition[1]
  372. headers.append(header)
  373. rows = []
  374. for record in records:
  375. if not isinstance(record, dict):
  376. return self._format_error(meta)
  377. rows.append([
  378. '' if record.get(key) is None else record.get(key, '')
  379. for key in keys
  380. ])
  381. content = {
  382. 'summary': self._safe_text(data.get('summary')),
  383. 'headers': headers,
  384. 'rows': rows,
  385. }
  386. tips = self._safe_tips(data.get('tips'))
  387. if tips:
  388. content['tips'] = tips
  389. pagination = self._build_pagination(raw_meta)
  390. if pagination:
  391. content['pagination'] = pagination
  392. return self._success_result(content, self._render_table(content), meta)
  393. def _present_outbound_detail(self, tool_name, data, raw_meta, meta):
  394. summary = data.get('summary')
  395. columns = data.get('columns')
  396. records = data.get('records')
  397. if not isinstance(summary, dict):
  398. return self._format_error(meta)
  399. if not isinstance(columns, list) or not columns or not isinstance(records, list):
  400. return self._format_error(meta)
  401. if any(key not in summary for key in self.OUTBOUND_DETAIL_SUMMARY):
  402. return self._format_error(meta)
  403. allowed_columns = self.TABLE_COLUMNS.get(tool_name)
  404. headers = []
  405. keys = []
  406. for column in columns:
  407. if not isinstance(column, dict):
  408. return self._format_error(meta)
  409. key = column.get('key')
  410. definition = allowed_columns.get(key) if isinstance(key, str) else None
  411. if not isinstance(definition, tuple) or not definition:
  412. return self._format_error(meta)
  413. keys.append(key)
  414. headers.append({'label': definition[0]})
  415. rows = []
  416. for record in records:
  417. if not isinstance(record, dict):
  418. return self._format_error(meta)
  419. row = []
  420. for key in keys:
  421. value = '' if record.get(key) is None else record.get(key, '')
  422. if key == 'customs_files':
  423. if not isinstance(value, list):
  424. return self._format_error(meta)
  425. files = []
  426. for item in value:
  427. if not isinstance(item, dict):
  428. return self._format_error(meta)
  429. files.append({
  430. '文件名称': item.get('file_name', ''),
  431. '文件类型': item.get('file_type', ''),
  432. '文件链接': item.get('file_url', ''),
  433. })
  434. value = files
  435. row.append(value)
  436. rows.append(row)
  437. summary_keys = list(self.OUTBOUND_DETAIL_SUMMARY.keys())
  438. content = {
  439. 'summary': {
  440. 'headers': [
  441. {'label': self.OUTBOUND_DETAIL_SUMMARY[key]}
  442. for key in summary_keys
  443. ],
  444. 'row': [summary.get(key, '') for key in summary_keys],
  445. },
  446. 'details': {
  447. 'headers': headers,
  448. 'rows': rows,
  449. },
  450. }
  451. tips = self._safe_tips(data.get('tips'))
  452. if tips:
  453. content['details']['tips'] = tips
  454. pagination = self._build_pagination(raw_meta)
  455. if pagination:
  456. content['details']['pagination'] = pagination
  457. return self._success_result(
  458. content,
  459. self._render_outbound_detail(content),
  460. meta,
  461. )
  462. @classmethod
  463. def _render_outbound_detail(cls, content):
  464. lines = ['排舱汇总:']
  465. summary = content['summary']
  466. for header, value in zip(summary['headers'], summary['row']):
  467. lines.append('- {0}: {1}'.format(header['label'], value))
  468. lines.append('订单明细:')
  469. details = content['details']
  470. lines.append('表头共 {0} 列:'.format(len(details['headers'])))
  471. for index, header in enumerate(details['headers'], start=1):
  472. lines.append('{0}. {1}'.format(index, header['label']))
  473. for index, row in enumerate(details['rows'], start=1):
  474. lines.append('记录 {0}:'.format(index))
  475. for header, value in zip(details['headers'], row):
  476. if isinstance(value, (dict, list)):
  477. value = json.dumps(value, ensure_ascii=False)
  478. lines.append('- {0}: {1}'.format(header['label'], value))
  479. tips = details.get('tips') or []
  480. if tips:
  481. lines.append('提示:{0}'.format(';'.join(tips)))
  482. return '\n'.join(lines)
  483. def _present_options(self, data, raw_meta, meta):
  484. records = data.get('records')
  485. if not isinstance(records, list):
  486. return self._format_error(meta)
  487. rows = []
  488. for record in records:
  489. if not isinstance(record, dict):
  490. return self._format_error(meta)
  491. rows.append([
  492. record.get('value', ''),
  493. self._safe_text(record.get('label')),
  494. self._safe_text(record.get('code')),
  495. ])
  496. content = {
  497. 'headers': [
  498. {'label': '可传值'},
  499. {'label': '显示名称'},
  500. {'label': '业务编码'},
  501. ],
  502. 'rows': rows,
  503. }
  504. pagination = self._build_pagination(raw_meta)
  505. if pagination:
  506. content['pagination'] = pagination
  507. return self._success_result(content, self._render_table(content), meta)
  508. def _present_export(self, data, meta):
  509. url = data.get('file_url')
  510. if not isinstance(url, str) or not url.strip():
  511. return self._format_error(meta)
  512. content = {
  513. 'message': '文件已生成',
  514. 'files': [{'label': '导出文件', 'url': url.strip()}],
  515. }
  516. text = '文件已生成\n- 导出文件: {0}'.format(url.strip())
  517. return self._success_result(content, text, meta)
  518. def _business_error(self, tool_name, code, raw_message, meta):
  519. if code not in self.ERROR_MESSAGES and code != 'MCP_1401':
  520. logger.warning(
  521. 'Unknown MCP business error code',
  522. extra={
  523. 'request_id': meta.get('request_id', ''),
  524. 'tool_code': tool_name,
  525. 'backend_code': code,
  526. 'response_code': 'MCP_9001',
  527. 'diagnostic_reason': 'UNEXPECTED_EXCEPTION',
  528. },
  529. )
  530. code = 'MCP_9001'
  531. if code == 'MCP_1401':
  532. label = self._find_field_label(tool_name, raw_message)
  533. message = self._parameter_message(label)
  534. else:
  535. message = self.ERROR_MESSAGES[code]
  536. retryable = code not in self.NON_RETRYABLE_CODES
  537. return self._error_result(code, message, retryable, meta)
  538. def _find_field_label(self, tool_name, raw_message):
  539. message = str(raw_message or '')
  540. fields = self.FIELD_LABELS.get(tool_name, {})
  541. for field in sorted(fields, key=len, reverse=True):
  542. if field in message:
  543. return fields[field]
  544. return ''
  545. @staticmethod
  546. def _parameter_message(label):
  547. return '{0}参数不正确'.format(label) if label else '工具参数不正确,请检查后重试'
  548. @staticmethod
  549. def _build_meta(raw_meta):
  550. if not isinstance(raw_meta, dict):
  551. return {}
  552. request_id = raw_meta.get('request_id')
  553. if not isinstance(request_id, str) or not request_id.strip():
  554. return {}
  555. return {'request_id': request_id.strip()}
  556. @staticmethod
  557. def _build_pagination(raw_meta):
  558. if not isinstance(raw_meta, dict):
  559. return {}
  560. return {
  561. key: raw_meta[key]
  562. for key in ('page', 'limit', 'has_more', 'total')
  563. if key in raw_meta
  564. }
  565. @staticmethod
  566. def _safe_text(value):
  567. if value is None:
  568. return ''
  569. return str(value).strip()
  570. @classmethod
  571. def _safe_tips(cls, value):
  572. if not isinstance(value, list):
  573. return []
  574. return [cls._safe_text(item) for item in value if cls._safe_text(item)]
  575. @classmethod
  576. def _render_table(cls, content):
  577. headers = content.get('headers') or []
  578. rows = content.get('rows') or []
  579. lines = []
  580. summary = cls._safe_text(content.get('summary'))
  581. if summary:
  582. lines.append(summary)
  583. lines.append('表头共 {0} 列:'.format(len(headers)))
  584. for index, header in enumerate(headers, start=1):
  585. lines.append('{0}. {1}'.format(index, header['label']))
  586. for index, row in enumerate(rows, start=1):
  587. lines.append('记录 {0}:'.format(index))
  588. for header, value in zip(headers, row):
  589. if isinstance(value, (dict, list)):
  590. value = json.dumps(value, ensure_ascii=False)
  591. lines.append('- {0}: {1}'.format(header['label'], value))
  592. tips = content.get('tips') or []
  593. if tips:
  594. lines.append('提示:{0}'.format(';'.join(tips)))
  595. return '\n'.join(lines)
  596. @staticmethod
  597. def _success_result(content, text, meta):
  598. return {
  599. 'structured_content': content,
  600. 'text': text,
  601. 'is_error': False,
  602. 'meta': meta,
  603. }
  604. @staticmethod
  605. def _error_result(code, message, retryable, meta=None):
  606. return {
  607. 'structured_content': {
  608. 'code': code,
  609. 'message': message,
  610. 'retryable': retryable,
  611. },
  612. 'text': '{0}: {1}'.format(code, message),
  613. 'is_error': True,
  614. 'meta': meta or {},
  615. }
  616. @classmethod
  617. def _format_error(cls, meta=None):
  618. return cls._error_result(
  619. 'MCP_9001',
  620. '工具返回格式异常',
  621. True,
  622. meta,
  623. )