query_container_timeliness_list.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. from datetime import datetime
  2. COLUMNS = (
  3. ('ship_company', '船公司'),
  4. ('bl_number', '提单号'),
  5. ('container_code', '柜号'),
  6. ('container_type', '柜型'),
  7. ('is_direct_send_name', '是否直送'),
  8. ('shipping_method', '运输方式'),
  9. ('route', '航线'),
  10. ('vessel_name', '航名航次'),
  11. ('wharf_name', '到港码头'),
  12. ('providers_name', '拖车行'),
  13. ('dt_status_name', '柜子状态'),
  14. ('warehouse_name', '交货仓库'),
  15. ('order_total', '件数'),
  16. ('total_volume', '体积'),
  17. ('total_weight', '重量'),
  18. ('loading_name', '起运港'),
  19. ('destination_name', '目的港'),
  20. ('delivery_address', '派送地址'),
  21. ('has_throw_off_name', '甩柜'),
  22. ('throw_off_time', '甩柜时间'),
  23. ('ed_check_name', '报关查验'),
  24. ('ed_check_time', '报关查验时间'),
  25. ('qg_check_name', '清关查验'),
  26. ('check_time', '清关查验时间'),
  27. ('in_inspection_site', '进查验场时间'),
  28. ('out_inspection_site', '出查验场时间'),
  29. ('is_close_area_name', '封闭区'),
  30. ('start_close_area', '进封闭区时间'),
  31. ('end_close_area', '出封闭区时间'),
  32. ('outbound_date', '出库时间'),
  33. ('export_release_time', '出口报关放行时间'),
  34. ('etd', '干线预计出发时间'),
  35. ('eta', '干线预计到达时间'),
  36. ('gxyjsx', '干线预计时效'),
  37. ('departure_time', '干线实际出发时间'),
  38. ('arrival_time', '干线实际到达时间'),
  39. ('gxsjsx', '干线实际时效'),
  40. ('hcsx', '航程时效'),
  41. ('start_clearance_time', '开始清关时间'),
  42. ('import_release_time', '海外清关放行时间'),
  43. ('wharf_wait_time', '码头等待时间'),
  44. ('pickup_time', '码头提柜时间'),
  45. ('second_pickup_time', '二次提柜时间'),
  46. ('tgsx', '提柜时效'),
  47. ('max_appointment_delivery_time', 'APPT时间'),
  48. ('delivery_end_time', '卡车实际派送时间'),
  49. ('oversea_warehouse_name', '海外仓库名称'),
  50. ('inbound_date', '到仓时间'),
  51. ('inbound_time', '海外仓入库时间'),
  52. ('container_return_date', '还空柜时间'),
  53. ('hksx', '还空时效'),
  54. ('amazon_wait_time', '亚马逊等待时间'),
  55. ('inner_remark', '内部备注'),
  56. ('financial_remark', '财务备注'),
  57. )
  58. class QueryContainerTimelinessListTool:
  59. name = 'query_container_timeliness_list'
  60. route_path = '/mcp/tools/queryContainerTimelinessList'
  61. def __init__(self, api_client=None):
  62. self.api_client = api_client
  63. def metadata(self):
  64. date_field = {
  65. 'type': 'string',
  66. 'format': 'date',
  67. 'pattern': '^\\d{4}-\\d{2}-\\d{2}$',
  68. }
  69. number_array = {
  70. 'type': 'array',
  71. 'minItems': 1,
  72. 'maxItems': 200,
  73. 'items': {
  74. 'type': 'string',
  75. 'minLength': 1,
  76. 'maxLength': 100,
  77. 'pattern': '.*\\S.*',
  78. },
  79. }
  80. return {
  81. 'name': self.name,
  82. 'description': (
  83. '使用场景:用户要看柜子时效统计报表表格(已出库排舱单一行一柜)时,'
  84. '对照后台admin/Report/outboundReport导出列,固定54列,不含序号。'
  85. '必须先确认柜号、提单号或最多31个起运港当地日历日的干线实际出发时间闭区间;'
  86. '没有号码时干线实际出发时间必填,有号码时时间可选。柜号与提单号可同时传入并按AND收窄。'
  87. '号码类型不明确时必须先询问用户:“请确认使用哪种号码查询:柜号还是提单号?”确认前不得调用。'
  88. '不得根据号码格式猜测,不得跨字段或跨工具试查。'
  89. '禁止使用:导出文件、排舱列表query_outbound_list、海外提柜列表、后台页面筛选'
  90. '(船公司、港口、出库时间、查验勾选等)。看表格用本工具;用户明确要下载文件时改走'
  91. 'export_container_timeliness_report。'
  92. '参数只用于工具内部调用;最终回答只能展示中文业务名称;描述筛选条件时不得展示筛选字段的'
  93. '英文参数名。'
  94. ),
  95. 'input_schema': {
  96. 'type': 'object',
  97. 'properties': {
  98. 'container_codes': dict(
  99. number_array,
  100. description=(
  101. '柜号数组。仅当用户明确说柜号时使用;'
  102. '不得放入其他类型号码。'
  103. ),
  104. ),
  105. 'bl_numbers': dict(
  106. number_array,
  107. description=(
  108. '提单号数组。仅当用户明确说提单号时使用;'
  109. '不得放入其他类型号码。'
  110. ),
  111. ),
  112. 'departure_time_start': dict(
  113. date_field,
  114. description='干线实际出发时间开始日期,起运港当地日历日 YYYY-MM-DD。',
  115. ),
  116. 'departure_time_end': dict(
  117. date_field,
  118. description=(
  119. '干线实际出发时间结束日期,起运港当地日历日 YYYY-MM-DD,'
  120. '且与开始日期跨度不超过31个日历日。'
  121. ),
  122. ),
  123. 'page': {
  124. 'type': 'integer',
  125. 'minimum': 1,
  126. 'maximum': 100,
  127. 'default': 1,
  128. },
  129. 'limit': {
  130. 'type': 'integer',
  131. 'minimum': 1,
  132. 'maximum': 100,
  133. 'default': 20,
  134. },
  135. },
  136. 'required': [],
  137. 'additionalProperties': False,
  138. },
  139. }
  140. def call(
  141. self,
  142. container_codes=None,
  143. bl_numbers=None,
  144. departure_time_start=None,
  145. departure_time_end=None,
  146. page=1,
  147. limit=20,
  148. request_id='rq_query_container_timeliness_list',
  149. ):
  150. if self.api_client is None:
  151. raise RuntimeError(
  152. 'api client is required for query_container_timeliness_list'
  153. )
  154. payload = self._locator_payload(
  155. container_codes,
  156. bl_numbers,
  157. departure_time_start,
  158. departure_time_end,
  159. )
  160. payload['page'] = self._bounded_integer(page, 'page')
  161. payload['limit'] = self._bounded_integer(limit, 'limit')
  162. return self.api_client.call_tool(
  163. self.name, self.route_path, payload, request_id,
  164. )
  165. @classmethod
  166. def _locator_payload(
  167. cls,
  168. container_codes,
  169. bl_numbers,
  170. departure_time_start,
  171. departure_time_end,
  172. ):
  173. payload = {}
  174. if container_codes is not None:
  175. payload['container_codes'] = cls._number_list(
  176. 'container_codes', container_codes
  177. )
  178. if bl_numbers is not None:
  179. payload['bl_numbers'] = cls._number_list('bl_numbers', bl_numbers)
  180. total = len(payload.get('container_codes', [])) + len(
  181. payload.get('bl_numbers', [])
  182. )
  183. if total > 200:
  184. raise ValueError('at most 200 container codes and bl numbers combined')
  185. has_start = departure_time_start is not None
  186. has_end = departure_time_end is not None
  187. if has_start != has_end:
  188. raise ValueError('departure time range requires both start and end')
  189. if has_start:
  190. start = cls._date(departure_time_start, 'departure_time_start')
  191. end = cls._date(departure_time_end, 'departure_time_end')
  192. cls._assert_window(start, end)
  193. payload['departure_time_start'] = start
  194. payload['departure_time_end'] = end
  195. elif 'container_codes' not in payload and 'bl_numbers' not in payload:
  196. raise ValueError(
  197. 'provide container codes, bl numbers, or a departure time range'
  198. )
  199. return payload
  200. @staticmethod
  201. def _date(value, field):
  202. text = str(value or '').strip()
  203. try:
  204. parsed = datetime.strptime(text, '%Y-%m-%d').date()
  205. except ValueError:
  206. raise ValueError(field + ' is invalid')
  207. return parsed.strftime('%Y-%m-%d')
  208. @staticmethod
  209. def _assert_window(start, end):
  210. start_date = datetime.strptime(start, '%Y-%m-%d').date()
  211. end_date = datetime.strptime(end, '%Y-%m-%d').date()
  212. if end_date < start_date or (end_date - start_date).days > 30:
  213. raise ValueError('departure time window is invalid')
  214. @staticmethod
  215. def _number_list(field, values):
  216. if not isinstance(values, list) or not values:
  217. raise ValueError(field + ' must be a non-empty list')
  218. cleaned = []
  219. for value in values:
  220. if not isinstance(value, str):
  221. raise ValueError(field + ' items must be strings')
  222. item = value.strip()
  223. if not item or len(item) > 100:
  224. raise ValueError(field + ' items must be 1 to 100 chars')
  225. if item not in cleaned:
  226. cleaned.append(item)
  227. if len(cleaned) > 200:
  228. raise ValueError('at most 200 ' + field)
  229. return cleaned
  230. @staticmethod
  231. def _bounded_integer(value, field):
  232. if isinstance(value, bool) or not isinstance(value, int):
  233. raise ValueError(field + ' is invalid')
  234. if value < 1 or value > 100:
  235. raise ValueError(field + ' is invalid')
  236. return value