arrange_destination_trailer.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. class ArrangeDestinationTrailerTool:
  2. name = 'arrange_destination_trailer'
  3. route_path = '/mcp/tools/arrangeDestinationTrailer'
  4. CHANGE_REASONS = (
  5. '直送爆仓被拒',
  6. '客户HOLD货',
  7. '拿不到APPT',
  8. '集装箱装箱不当,亚马逊无法卸货',
  9. '货物不符合直送标准',
  10. )
  11. def __init__(self, api_client=None):
  12. self.api_client = api_client
  13. def metadata(self):
  14. text = {
  15. 'type': 'string',
  16. 'minLength': 1,
  17. 'maxLength': 200,
  18. 'pattern': '.*\\S.*',
  19. }
  20. number = dict(text, maxLength=100)
  21. return {
  22. 'name': self.name,
  23. 'description': (
  24. '首次安排海外拖车行,对齐后台待安排拖车页的「安排拖车行」。'
  25. '只接受提单号或柜号,可以只给一个,也可以两个都给并且同时满足;'
  26. '必须正好命中当前员工能看到的一柜。不得传内部编号,不得修改已有提柜单。'
  27. '拖车行可以说「名称-代码」、名称或代码。海外仓只说仓库代码。'
  28. '港后跟进人说员工姓名;不说则保持现在的人,说「空」才清空。'
  29. '直送柜不说则保持现状,只能说是或否。'
  30. '选否时要说海外仓;联系人和电话不说则用仓库主数据。'
  31. '选是时不能再说海外仓、联系人或电话。'
  32. '本来就是直送、预选海外仓有外部仓库代码时,仍要按订单给重量和体积;'
  33. '重量体积不合法时整次拒绝,不会先生成拖车单。'
  34. '本来是直送、这次改为否时,必须说直送改拆;选是还要说页面上的改拆原因原文。'
  35. '预计提柜时间可选,格式 YYYY-MM-DD HH:MM:SS,按目的港当地时间。'
  36. '需要上传 BOL 的柜子不要调用,请到页面安排。'
  37. '有外部仓库代码但不要 BOL 时,可以按订单号改重量或体积。'
  38. '不该出现的字段不要传。失败时不会返回可选列表。'
  39. '成功只返回拖车单号、提单号、柜号。'
  40. '参数名仅用于工具调用;向用户回答时只用中文业务名称。'
  41. ),
  42. 'input_schema': {
  43. 'type': 'object',
  44. 'properties': {
  45. 'bl_number': dict(number, description='提单号。与柜号至少给一个。'),
  46. 'container_code': dict(number, description='柜号。与提单号至少给一个。'),
  47. 'trailer_provider': dict(
  48. text,
  49. description='拖车行。可说名称-代码、名称或代码,必须正好命中一家。',
  50. ),
  51. 'is_direct_send': {
  52. 'type': 'string',
  53. 'enum': ['是', '否'],
  54. 'description': '直送柜。不传则保持这一柜现在的值。',
  55. },
  56. 'warehouse_code': dict(number, description='海外仓仓库代码。直送为否时必填。'),
  57. 'warehouse_contact': dict(text, description='仓库联系人。不传则用海外仓主数据。'),
  58. 'warehouse_tel': dict(text, description='仓库联系电话。不传则用海外仓主数据。'),
  59. 'follow_up_user': dict(
  60. text,
  61. description='港后跟进人姓名。说「空」表示清空。不传则保持现在的人。',
  62. ),
  63. 'est_pickup_time': {
  64. 'type': 'string',
  65. 'pattern': '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$',
  66. 'description': '预计提柜时间,目的港当地时间。',
  67. },
  68. 'is_direct_change': {
  69. 'type': 'string',
  70. 'enum': ['是', '否'],
  71. 'description': '直送改拆。仅本来是直送、这次改为否时填写。',
  72. },
  73. 'direct_change_reason': {
  74. 'type': 'string',
  75. 'enum': list(self.CHANGE_REASONS),
  76. 'description': '改拆原因,必须是页面上的整句原文。',
  77. },
  78. 'order_measures': {
  79. 'type': 'array',
  80. 'maxItems': 500,
  81. 'items': {
  82. 'type': 'object',
  83. 'properties': {
  84. 'order_number': dict(number, description='这一柜上的订单号。'),
  85. 'weight': {
  86. 'type': 'string',
  87. 'minLength': 1,
  88. 'maxLength': 20,
  89. 'description': '重量,千克,大于 0 小于 50000,最多三位小数。',
  90. },
  91. 'volume': {
  92. 'type': 'string',
  93. 'minLength': 1,
  94. 'maxLength': 20,
  95. 'description': '体积,CBM,大于 0 小于 10000,最多三位小数。',
  96. },
  97. },
  98. 'required': ['order_number'],
  99. 'additionalProperties': False,
  100. },
  101. },
  102. },
  103. 'required': ['trailer_provider'],
  104. 'additionalProperties': False,
  105. },
  106. }
  107. def call(
  108. self,
  109. trailer_provider,
  110. bl_number=None,
  111. container_code=None,
  112. is_direct_send=None,
  113. warehouse_code=None,
  114. warehouse_contact=None,
  115. warehouse_tel=None,
  116. follow_up_user=None,
  117. est_pickup_time=None,
  118. is_direct_change=None,
  119. direct_change_reason=None,
  120. order_measures=None,
  121. request_id='rq_arrange_destination_trailer',
  122. ):
  123. if self.api_client is None:
  124. raise RuntimeError('api client is required for arrange_destination_trailer')
  125. payload = {
  126. 'trailer_provider': self._text(trailer_provider, 'trailer_provider', 200),
  127. }
  128. if bl_number is not None:
  129. payload['bl_number'] = self._text(bl_number, 'bl_number', 100)
  130. if container_code is not None:
  131. payload['container_code'] = self._text(container_code, 'container_code', 100)
  132. if bl_number is None and container_code is None:
  133. raise ValueError('bl number or container code is required')
  134. self._choice(payload, 'is_direct_send', is_direct_send, ('是', '否'))
  135. self._optional(payload, 'warehouse_code', warehouse_code, 100)
  136. self._optional(payload, 'warehouse_contact', warehouse_contact, 200)
  137. self._optional(payload, 'warehouse_tel', warehouse_tel, 200)
  138. self._optional(payload, 'follow_up_user', follow_up_user, 200)
  139. if est_pickup_time is not None:
  140. text = self._text(est_pickup_time, 'est_pickup_time', 19)
  141. if len(text) != 19:
  142. raise ValueError('est_pickup_time is invalid')
  143. payload['est_pickup_time'] = text
  144. self._choice(payload, 'is_direct_change', is_direct_change, ('是', '否'))
  145. self._choice(payload, 'direct_change_reason', direct_change_reason, self.CHANGE_REASONS)
  146. if order_measures is not None:
  147. payload['order_measures'] = self._measures(order_measures)
  148. return self.api_client.call_tool(
  149. self.name, self.route_path, payload, request_id,
  150. )
  151. def _optional(self, payload, field, value, limit):
  152. if value is not None:
  153. payload[field] = self._text(value, field, limit)
  154. def _choice(self, payload, field, value, allowed):
  155. if value is None:
  156. return
  157. if value not in allowed:
  158. raise ValueError(field + ' is invalid')
  159. payload[field] = value
  160. @staticmethod
  161. def _text(value, field, limit):
  162. if not isinstance(value, str):
  163. raise ValueError(field + ' is invalid')
  164. text = value.strip()
  165. if not text or len(text) > limit:
  166. raise ValueError(field + ' is invalid')
  167. return text
  168. def _measures(self, rows):
  169. if not isinstance(rows, list) or len(rows) > 500:
  170. raise ValueError('order_measures is invalid')
  171. result = []
  172. for row in rows:
  173. if not isinstance(row, dict) or 'order_number' not in row:
  174. raise ValueError('order_measures is invalid')
  175. item = {'order_number': self._text(row['order_number'], 'order_number', 100)}
  176. for key in ('weight', 'volume'):
  177. if key in row and row[key] is not None:
  178. item[key] = self._text(row[key], key, 20)
  179. result.append(item)
  180. return result