| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- from typing import Literal
- from datetime import date
- class QueryDestinationTrailerListTool:
- name = 'query_destination_trailer_list'
- route_path = '/mcp/tools/queryDestinationTrailerList'
- STAGES = (
- '待安排拖车', '待提柜', '待还柜(海)', '已还柜(海)', '已提货(空)', 'DO单制作',
- )
- DO_FORBIDDEN = (
- 'providers_id', 'warehouse_id', 'is_direct_send', 'bill_status',
- 'cabinet_type', 'shipping_method', 'eta_start', 'eta_end',
- 'pickup_time_start', 'pickup_time_end',
- 'container_return_time_start', 'container_return_time_end',
- )
- def __init__(self, api_client=None):
- self.api_client = api_client
- def metadata(self):
- number_array = {
- 'type': 'array', 'minItems': 1, 'maxItems': 200,
- 'items': {
- 'type': 'string', 'minLength': 1, 'maxLength': 100,
- 'pattern': '.*\\S.*',
- },
- }
- date_field = {
- 'type': 'string', 'format': 'date',
- 'pattern': '^\\d{4}-\\d{2}-\\d{2}$',
- }
- properties = {
- 'trailer_stage': {
- 'type': 'string',
- 'enum': list[Literal['待安排拖车', '待提柜', '待还柜(海)', '已还柜(海)', '已提货(空)', 'DO单制作']](self.STAGES),
- 'description': (
- '海外提柜类型。必须先调用list_destination_trailer_filter_options并选择'
- '“海外提柜类型”取得六个中文值之一;用户未明确时必须先询问,不得默认某一类'
- '或六类一起查,也不得改走排舱列表。'
- ),
- },
- 'bl_numbers': dict(
- number_array,
- description='提单号数组。仅当用户明确说提单号时使用;不得放入柜号或其他号码。',
- ),
- 'container_codes': dict(
- number_array,
- description='柜号数组。仅当用户明确说柜号时使用;不得放入提单号或其他号码。',
- ),
- 'providers_id': {
- 'type': 'integer', 'minimum': 1,
- 'description': '拖车行筛选值,必须先通过筛选项工具取得。',
- },
- 'warehouse_id': {
- 'type': 'integer', 'minimum': 1,
- 'description': '海外仓筛选值,必须先通过筛选项工具取得;只命中非直送。',
- },
- 'is_direct_send': {
- 'type': 'integer', 'enum': [0, 1],
- 'description': '直送柜筛选值,先通过筛选项工具取得。',
- },
- 'bill_status': {
- 'type': 'integer', 'enum': [10, 20, 30, 35, 40],
- 'description': '应付费用状态筛选值,先通过筛选项工具取得。',
- },
- 'cabinet_type': {
- 'type': 'integer', 'enum': [1, 2, 3, 4],
- 'description': '整拼类型筛选值,先通过筛选项工具取得。',
- },
- 'shipping_method': {
- 'type': 'integer', 'enum': [1, 2, 3],
- 'description': '运输方式筛选值,先通过筛选项工具取得。',
- },
- 'eta_start': dict(date_field),
- 'eta_end': dict(date_field),
- 'pickup_time_start': dict(date_field),
- 'pickup_time_end': dict(date_field),
- 'container_return_time_start': dict(date_field),
- 'container_return_time_end': dict(date_field),
- 'page': {'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 1},
- 'limit': {'type': 'integer', 'minimum': 1, 'maximum': 100, 'default': 20},
- }
- return {
- 'name': self.name,
- 'description': (
- '使用场景:用户要看海外提柜进度或DO单制作列表时,先确认六个类型之一即可查询当前页签;'
- '提单号、柜号、日期和其他筛选均为可选。传了号码则类型必须明确,'
- '号码类型不明确时必须先询问用户,'
- '不得根据号码格式猜测,不得跨字段或跨工具试查,也不得把海外提柜当成排舱列表调用'
- 'query_outbound_list。日期若传入必须成对且最多31个目的港当地日历日。'
- '禁止使用:安排拖车、确认还柜、港后数据/应付/入仓单/POD/'
- '装柜顺序导出,以及混查订单号、拖车单号、排舱单号或SO。参数只用于工具内部调用;'
- '最终回答只能展示中文业务名称;描述筛选条件时不得展示筛选字段的英文参数名。'
- ),
- 'input_schema': {
- 'type': 'object',
- 'properties': properties,
- 'required': ['trailer_stage'],
- 'additionalProperties': False,
- },
- }
- def call(
- self, trailer_stage, bl_numbers=None, container_codes=None,
- providers_id=None, warehouse_id=None, is_direct_send=None,
- bill_status=None, cabinet_type=None, shipping_method=None,
- eta_start=None, eta_end=None, pickup_time_start=None,
- pickup_time_end=None, container_return_time_start=None,
- container_return_time_end=None, page=1, limit=20,
- request_id='rq_query_destination_trailer_list',
- ):
- if self.api_client is None:
- raise RuntimeError(
- 'api client is required for query_destination_trailer_list'
- )
- stage = str(trailer_stage or '').strip()
- if stage not in self.STAGES:
- raise ValueError('trailer_stage is invalid')
- payload = {'trailer_stage': stage}
- total = 0
- for field, values in (
- ('bl_numbers', bl_numbers),
- ('container_codes', container_codes),
- ):
- if values is None:
- continue
- cleaned = self._number_list(values, field)
- payload[field] = cleaned
- total += len(values)
- if total > 200:
- raise ValueError('at most 200 numbers are allowed')
- for start_name, start_value, end_value in (
- ('eta_start', eta_start, eta_end),
- ('pickup_time_start', pickup_time_start, pickup_time_end),
- (
- 'container_return_time_start',
- container_return_time_start,
- container_return_time_end,
- ),
- ):
- end_name = start_name.replace('_start', '_end')
- start = self._optional_date(start_value, start_name)
- end = self._optional_date(end_value, end_name)
- if bool(start) != bool(end):
- raise ValueError(start_name + ' range is invalid')
- if start:
- if (date.fromisoformat(end) - date.fromisoformat(start)).days > 30:
- raise ValueError(start_name + ' range is invalid')
- if end < start:
- raise ValueError(start_name + ' range is invalid')
- payload[start_name] = start
- payload[end_name] = end
- for field, value, allowed in (
- ('providers_id', providers_id, None),
- ('warehouse_id', warehouse_id, None),
- ('is_direct_send', is_direct_send, (0, 1)),
- ('bill_status', bill_status, (10, 20, 30, 35, 40)),
- ('cabinet_type', cabinet_type, (1, 2, 3, 4)),
- ('shipping_method', shipping_method, (1, 2, 3)),
- ):
- if value is None:
- continue
- checked = self._integer(value, field, 0 if field == 'is_direct_send' else 1)
- if allowed is not None and checked not in allowed:
- raise ValueError(field + ' is invalid')
- payload[field] = checked
- if stage == 'DO单制作':
- for field in self.DO_FORBIDDEN:
- if field in payload:
- raise ValueError('DO stage does not accept extra filters')
- payload['page'] = self._bounded_integer(page, 'page')
- payload['limit'] = self._bounded_integer(limit, 'limit')
- return self.api_client.call_tool(
- self.name, self.route_path, payload, request_id,
- )
- @staticmethod
- def _number_list(values, field):
- if not isinstance(values, list) or not values:
- raise ValueError(field + ' is invalid')
- result = []
- for value in values:
- if not isinstance(value, str):
- raise ValueError(field + ' is invalid')
- value = value.strip()
- if not value or len(value) > 100:
- raise ValueError(field + ' is invalid')
- if value not in result:
- result.append(value)
- return result
- @staticmethod
- def _optional_date(value, field):
- if value is None or value == '':
- return ''
- if not isinstance(value, str):
- raise ValueError(field + ' is invalid')
- try:
- parsed = date.fromisoformat(value)
- except ValueError:
- raise ValueError(field + ' is invalid')
- if parsed.isoformat() != value:
- raise ValueError(field + ' is invalid')
- return value
- @staticmethod
- def _integer(value, field, minimum=None):
- if isinstance(value, bool) or not isinstance(value, int):
- raise ValueError(field + ' is invalid')
- if minimum is not None and value < minimum:
- raise ValueError(field + ' is invalid')
- return value
- @staticmethod
- def _bounded_integer(value, field):
- if isinstance(value, bool) or not isinstance(value, int):
- raise ValueError(field + ' is invalid')
- if value < 1 or value > 100:
- raise ValueError(field + ' is invalid')
- return value
|