query_destination_trailer_list.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. from typing import Literal
  2. from datetime import date
  3. class QueryDestinationTrailerListTool:
  4. name = 'query_destination_trailer_list'
  5. route_path = '/mcp/tools/queryDestinationTrailerList'
  6. STAGES = (
  7. '待安排拖车', '待提柜', '待还柜(海)', '已还柜(海)', '已提货(空)', 'DO单制作',
  8. )
  9. DO_FORBIDDEN = (
  10. 'providers_id', 'warehouse_id', 'is_direct_send', 'bill_status',
  11. 'cabinet_type', 'shipping_method', 'eta_start', 'eta_end',
  12. 'pickup_time_start', 'pickup_time_end',
  13. 'container_return_time_start', 'container_return_time_end',
  14. )
  15. def __init__(self, api_client=None):
  16. self.api_client = api_client
  17. def metadata(self):
  18. number_array = {
  19. 'type': 'array', 'minItems': 1, 'maxItems': 200,
  20. 'items': {
  21. 'type': 'string', 'minLength': 1, 'maxLength': 100,
  22. 'pattern': '.*\\S.*',
  23. },
  24. }
  25. date_field = {
  26. 'type': 'string', 'format': 'date',
  27. 'pattern': '^\\d{4}-\\d{2}-\\d{2}$',
  28. }
  29. properties = {
  30. 'trailer_stage': {
  31. 'type': 'string',
  32. 'enum': list[Literal['待安排拖车', '待提柜', '待还柜(海)', '已还柜(海)', '已提货(空)', 'DO单制作']](self.STAGES),
  33. 'description': (
  34. '海外提柜类型。必须先调用list_destination_trailer_filter_options并选择'
  35. '“海外提柜类型”取得六个中文值之一;用户未明确时必须先询问,不得默认某一类'
  36. '或六类一起查,也不得改走排舱列表。'
  37. ),
  38. },
  39. 'bl_numbers': dict(
  40. number_array,
  41. description='提单号数组。仅当用户明确说提单号时使用;不得放入柜号或其他号码。',
  42. ),
  43. 'container_codes': dict(
  44. number_array,
  45. description='柜号数组。仅当用户明确说柜号时使用;不得放入提单号或其他号码。',
  46. ),
  47. 'providers_id': {
  48. 'type': 'integer', 'minimum': 1,
  49. 'description': '拖车行筛选值,必须先通过筛选项工具取得。',
  50. },
  51. 'warehouse_id': {
  52. 'type': 'integer', 'minimum': 1,
  53. 'description': '海外仓筛选值,必须先通过筛选项工具取得;只命中非直送。',
  54. },
  55. 'is_direct_send': {
  56. 'type': 'integer', 'enum': [0, 1],
  57. 'description': '直送柜筛选值,先通过筛选项工具取得。',
  58. },
  59. 'bill_status': {
  60. 'type': 'integer', 'enum': [10, 20, 30, 35, 40],
  61. 'description': '应付费用状态筛选值,先通过筛选项工具取得。',
  62. },
  63. 'cabinet_type': {
  64. 'type': 'integer', 'enum': [1, 2, 3, 4],
  65. 'description': '整拼类型筛选值,先通过筛选项工具取得。',
  66. },
  67. 'shipping_method': {
  68. 'type': 'integer', 'enum': [1, 2, 3],
  69. 'description': '运输方式筛选值,先通过筛选项工具取得。',
  70. },
  71. 'eta_start': dict(date_field),
  72. 'eta_end': dict(date_field),
  73. 'pickup_time_start': dict(date_field),
  74. 'pickup_time_end': dict(date_field),
  75. 'container_return_time_start': dict(date_field),
  76. 'container_return_time_end': dict(date_field),
  77. 'page': {'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 1},
  78. 'limit': {'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 20},
  79. }
  80. return {
  81. 'name': self.name,
  82. 'description': (
  83. '使用场景:用户要看海外提柜进度或DO单制作列表时,先确认六个类型之一即可查询当前页签;'
  84. '提单号、柜号、日期和其他筛选均为可选。传了号码则类型必须明确,'
  85. '号码类型不明确时必须先询问用户,'
  86. '不得根据号码格式猜测,不得跨字段或跨工具试查,也不得把海外提柜当成排舱列表调用'
  87. 'query_outbound_list。日期若传入必须成对且最多31个目的港当地日历日。'
  88. '禁止使用:安排拖车、确认还柜、港后数据/应付/入仓单/POD/'
  89. '装柜顺序导出,以及混查订单号、拖车单号、排舱单号或SO。参数只用于工具内部调用;'
  90. '最终回答只能展示中文业务名称;描述筛选条件时不得展示筛选字段的英文参数名。'
  91. ),
  92. 'input_schema': {
  93. 'type': 'object',
  94. 'properties': properties,
  95. 'required': ['trailer_stage'],
  96. 'additionalProperties': False,
  97. },
  98. }
  99. def call(
  100. self, trailer_stage, bl_numbers=None, container_codes=None,
  101. providers_id=None, warehouse_id=None, is_direct_send=None,
  102. bill_status=None, cabinet_type=None, shipping_method=None,
  103. eta_start=None, eta_end=None, pickup_time_start=None,
  104. pickup_time_end=None, container_return_time_start=None,
  105. container_return_time_end=None, page=1, limit=20,
  106. request_id='rq_query_destination_trailer_list',
  107. ):
  108. if self.api_client is None:
  109. raise RuntimeError(
  110. 'api client is required for query_destination_trailer_list'
  111. )
  112. stage = str(trailer_stage or '').strip()
  113. if stage not in self.STAGES:
  114. raise ValueError('trailer_stage is invalid')
  115. payload = {'trailer_stage': stage}
  116. total = 0
  117. for field, values in (
  118. ('bl_numbers', bl_numbers),
  119. ('container_codes', container_codes),
  120. ):
  121. if values is None:
  122. continue
  123. cleaned = self._number_list(values, field)
  124. payload[field] = cleaned
  125. total += len(values)
  126. if total > 200:
  127. raise ValueError('at most 200 numbers are allowed')
  128. for start_name, start_value, end_value in (
  129. ('eta_start', eta_start, eta_end),
  130. ('pickup_time_start', pickup_time_start, pickup_time_end),
  131. (
  132. 'container_return_time_start',
  133. container_return_time_start,
  134. container_return_time_end,
  135. ),
  136. ):
  137. end_name = start_name.replace('_start', '_end')
  138. start = self._optional_date(start_value, start_name)
  139. end = self._optional_date(end_value, end_name)
  140. if bool(start) != bool(end):
  141. raise ValueError(start_name + ' range is invalid')
  142. if start:
  143. if (date.fromisoformat(end) - date.fromisoformat(start)).days > 30:
  144. raise ValueError(start_name + ' range is invalid')
  145. if end < start:
  146. raise ValueError(start_name + ' range is invalid')
  147. payload[start_name] = start
  148. payload[end_name] = end
  149. for field, value, allowed in (
  150. ('providers_id', providers_id, None),
  151. ('warehouse_id', warehouse_id, None),
  152. ('is_direct_send', is_direct_send, (0, 1)),
  153. ('bill_status', bill_status, (10, 20, 30, 35, 40)),
  154. ('cabinet_type', cabinet_type, (1, 2, 3, 4)),
  155. ('shipping_method', shipping_method, (1, 2, 3)),
  156. ):
  157. if value is None:
  158. continue
  159. checked = self._integer(value, field, 0 if field == 'is_direct_send' else 1)
  160. if allowed is not None and checked not in allowed:
  161. raise ValueError(field + ' is invalid')
  162. payload[field] = checked
  163. if stage == 'DO单制作':
  164. for field in self.DO_FORBIDDEN:
  165. if field in payload:
  166. raise ValueError('DO stage does not accept extra filters')
  167. payload['page'] = self._bounded_integer(page, 'page')
  168. payload['limit'] = self._bounded_integer(limit, 'limit')
  169. return self.api_client.call_tool(
  170. self.name, self.route_path, payload, request_id,
  171. )
  172. @staticmethod
  173. def _number_list(values, field):
  174. if not isinstance(values, list) or not values:
  175. raise ValueError(field + ' is invalid')
  176. result = []
  177. for value in values:
  178. if not isinstance(value, str):
  179. raise ValueError(field + ' is invalid')
  180. value = value.strip()
  181. if not value or len(value) > 100:
  182. raise ValueError(field + ' is invalid')
  183. if value not in result:
  184. result.append(value)
  185. return result
  186. @staticmethod
  187. def _optional_date(value, field):
  188. if value is None or value == '':
  189. return ''
  190. if not isinstance(value, str):
  191. raise ValueError(field + ' is invalid')
  192. try:
  193. parsed = date.fromisoformat(value)
  194. except ValueError:
  195. raise ValueError(field + ' is invalid')
  196. if parsed.isoformat() != value:
  197. raise ValueError(field + ' is invalid')
  198. return value
  199. @staticmethod
  200. def _integer(value, field, minimum=None):
  201. if isinstance(value, bool) or not isinstance(value, int):
  202. raise ValueError(field + ' is invalid')
  203. if minimum is not None and value < minimum:
  204. raise ValueError(field + ' is invalid')
  205. return value
  206. @staticmethod
  207. def _bounded_integer(value, field):
  208. if isinstance(value, bool) or not isinstance(value, int):
  209. raise ValueError(field + ' is invalid')
  210. if value < 1 or value > 100:
  211. raise ValueError(field + ' is invalid')
  212. return value