| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188 |
- class ArrangeDestinationTrailerTool:
- name = 'arrange_destination_trailer'
- route_path = '/mcp/tools/arrangeDestinationTrailer'
- CHANGE_REASONS = (
- '直送爆仓被拒',
- '客户HOLD货',
- '拿不到APPT',
- '集装箱装箱不当,亚马逊无法卸货',
- '货物不符合直送标准',
- )
- def __init__(self, api_client=None):
- self.api_client = api_client
- def metadata(self):
- text = {
- 'type': 'string',
- 'minLength': 1,
- 'maxLength': 200,
- 'pattern': '.*\\S.*',
- }
- number = dict(text, maxLength=100)
- return {
- 'name': self.name,
- 'description': (
- '首次安排海外拖车行,对齐后台待安排拖车页的「安排拖车行」。'
- '只接受提单号或柜号,可以只给一个,也可以两个都给并且同时满足;'
- '必须正好命中当前员工能看到的一柜。不得传内部编号,不得修改已有提柜单。'
- '拖车行可以说「名称-代码」、名称或代码。海外仓只说仓库代码。'
- '港后跟进人说员工姓名;不说则保持现在的人,说「空」才清空。'
- '直送柜不说则保持现状,只能说是或否。'
- '选否时要说海外仓;联系人和电话不说则用仓库主数据。'
- '选是时不能再说海外仓、联系人或电话。'
- '本来就是直送、预选海外仓有外部仓库代码时,仍要按订单给重量和体积;'
- '重量体积不合法时整次拒绝,不会先生成拖车单。'
- '本来是直送、这次改为否时,必须说直送改拆;选是还要说页面上的改拆原因原文。'
- '预计提柜时间可选,格式 YYYY-MM-DD HH:MM:SS,按目的港当地时间。'
- '需要上传 BOL 的柜子不要调用,请到页面安排。'
- '有外部仓库代码但不要 BOL 时,可以按订单号改重量或体积。'
- '不该出现的字段不要传。失败时不会返回可选列表。'
- '成功只返回拖车单号、提单号、柜号。'
- '参数名仅用于工具调用;向用户回答时只用中文业务名称。'
- ),
- 'input_schema': {
- 'type': 'object',
- 'properties': {
- 'bl_number': dict(number, description='提单号。与柜号至少给一个。'),
- 'container_code': dict(number, description='柜号。与提单号至少给一个。'),
- 'trailer_provider': dict(
- text,
- description='拖车行。可说名称-代码、名称或代码,必须正好命中一家。',
- ),
- 'is_direct_send': {
- 'type': 'string',
- 'enum': ['是', '否'],
- 'description': '直送柜。不传则保持这一柜现在的值。',
- },
- 'warehouse_code': dict(number, description='海外仓仓库代码。直送为否时必填。'),
- 'warehouse_contact': dict(text, description='仓库联系人。不传则用海外仓主数据。'),
- 'warehouse_tel': dict(text, description='仓库联系电话。不传则用海外仓主数据。'),
- 'follow_up_user': dict(
- text,
- description='港后跟进人姓名。说「空」表示清空。不传则保持现在的人。',
- ),
- 'est_pickup_time': {
- 'type': 'string',
- 'pattern': '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}$',
- 'description': '预计提柜时间,目的港当地时间。',
- },
- 'is_direct_change': {
- 'type': 'string',
- 'enum': ['是', '否'],
- 'description': '直送改拆。仅本来是直送、这次改为否时填写。',
- },
- 'direct_change_reason': {
- 'type': 'string',
- 'enum': list(self.CHANGE_REASONS),
- 'description': '改拆原因,必须是页面上的整句原文。',
- },
- 'order_measures': {
- 'type': 'array',
- 'maxItems': 500,
- 'items': {
- 'type': 'object',
- 'properties': {
- 'order_number': dict(number, description='这一柜上的订单号。'),
- 'weight': {
- 'type': 'string',
- 'minLength': 1,
- 'maxLength': 20,
- 'description': '重量,千克,大于 0 小于 50000,最多三位小数。',
- },
- 'volume': {
- 'type': 'string',
- 'minLength': 1,
- 'maxLength': 20,
- 'description': '体积,CBM,大于 0 小于 10000,最多三位小数。',
- },
- },
- 'required': ['order_number'],
- 'additionalProperties': False,
- },
- },
- },
- 'required': ['trailer_provider'],
- 'additionalProperties': False,
- },
- }
- def call(
- self,
- trailer_provider,
- bl_number=None,
- container_code=None,
- is_direct_send=None,
- warehouse_code=None,
- warehouse_contact=None,
- warehouse_tel=None,
- follow_up_user=None,
- est_pickup_time=None,
- is_direct_change=None,
- direct_change_reason=None,
- order_measures=None,
- request_id='rq_arrange_destination_trailer',
- ):
- if self.api_client is None:
- raise RuntimeError('api client is required for arrange_destination_trailer')
- payload = {
- 'trailer_provider': self._text(trailer_provider, 'trailer_provider', 200),
- }
- if bl_number is not None:
- payload['bl_number'] = self._text(bl_number, 'bl_number', 100)
- if container_code is not None:
- payload['container_code'] = self._text(container_code, 'container_code', 100)
- if bl_number is None and container_code is None:
- raise ValueError('bl number or container code is required')
- self._choice(payload, 'is_direct_send', is_direct_send, ('是', '否'))
- self._optional(payload, 'warehouse_code', warehouse_code, 100)
- self._optional(payload, 'warehouse_contact', warehouse_contact, 200)
- self._optional(payload, 'warehouse_tel', warehouse_tel, 200)
- self._optional(payload, 'follow_up_user', follow_up_user, 200)
- if est_pickup_time is not None:
- text = self._text(est_pickup_time, 'est_pickup_time', 19)
- if len(text) != 19:
- raise ValueError('est_pickup_time is invalid')
- payload['est_pickup_time'] = text
- self._choice(payload, 'is_direct_change', is_direct_change, ('是', '否'))
- self._choice(payload, 'direct_change_reason', direct_change_reason, self.CHANGE_REASONS)
- if order_measures is not None:
- payload['order_measures'] = self._measures(order_measures)
- return self.api_client.call_tool(
- self.name, self.route_path, payload, request_id,
- )
- def _optional(self, payload, field, value, limit):
- if value is not None:
- payload[field] = self._text(value, field, limit)
- def _choice(self, payload, field, value, allowed):
- if value is None:
- return
- if value not in allowed:
- raise ValueError(field + ' is invalid')
- payload[field] = value
- @staticmethod
- def _text(value, field, limit):
- if not isinstance(value, str):
- raise ValueError(field + ' is invalid')
- text = value.strip()
- if not text or len(text) > limit:
- raise ValueError(field + ' is invalid')
- return text
- def _measures(self, rows):
- if not isinstance(rows, list) or len(rows) > 500:
- raise ValueError('order_measures is invalid')
- result = []
- for row in rows:
- if not isinstance(row, dict) or 'order_number' not in row:
- raise ValueError('order_measures is invalid')
- item = {'order_number': self._text(row['order_number'], 'order_number', 100)}
- for key in ('weight', 'volume'):
- if key in row and row[key] is not None:
- item[key] = self._text(row[key], key, 20)
- result.append(item)
- return result
|