query_receive_volume_list.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. from datetime import datetime
  2. class QueryReceiveVolumeListTool:
  3. name = 'query_receive_volume_list'
  4. route_path = '/mcp/tools/queryReceiveVolumeList'
  5. def __init__(self, api_client=None):
  6. self.api_client = api_client
  7. def metadata(self):
  8. date_field = {
  9. 'type': 'string',
  10. 'format': 'date',
  11. 'pattern': '^\\d{4}-\\d{2}-\\d{2}$',
  12. }
  13. id_array = {
  14. 'type': 'array',
  15. 'minItems': 1,
  16. 'maxItems': 200,
  17. 'items': {'type': 'integer', 'minimum': 1},
  18. }
  19. return {
  20. 'name': self.name,
  21. 'description': (
  22. '使用场景:用户要看收货量明细(已入库头程订单一行一单)时,'
  23. '必须先确认员工时区下最多31个日历日的入库时间闭区间,再调用'
  24. 'list_receive_volume_filter_options取得客户、商务经理、客户经理、'
  25. '货物类型或物流产品的可传值。对照后台admin/Report/receiveForm的'
  26. '已入库集合,不是按客户汇总,也不是业务发生时间。'
  27. '禁止使用:导出、散货/整柜页脚、订单号筛选,不得改走query_order、'
  28. 'query_order_exact或后台收货量导出。参数只用于工具内部调用;'
  29. '最终回答只能展示中文业务名称;描述筛选条件时不得展示筛选字段的'
  30. '英文参数名。'
  31. ),
  32. 'input_schema': {
  33. 'type': 'object',
  34. 'properties': {
  35. 'inbound_date_start': dict(
  36. date_field,
  37. description='入库时间开始日期,员工时区 YYYY-MM-DD。',
  38. ),
  39. 'inbound_date_end': dict(
  40. date_field,
  41. description='入库时间结束日期,员工时区 YYYY-MM-DD,且与开始日期跨度不超过31个日历日。',
  42. ),
  43. 'customer_ids': dict(
  44. id_array,
  45. description='客户ID数组。必须先调用list_receive_volume_filter_options选择「客户名称」取得value。',
  46. ),
  47. 'sales_ids': dict(
  48. id_array,
  49. description='商务经理员工ID数组。必须先调用筛选项取得value。',
  50. ),
  51. 'merchandiser_ids': dict(
  52. id_array,
  53. description='客户经理员工ID数组。必须先调用筛选项取得value。',
  54. ),
  55. 'packing_types': {
  56. 'type': 'array',
  57. 'minItems': 1,
  58. 'maxItems': 200,
  59. 'items': {
  60. 'type': 'string',
  61. 'minLength': 1,
  62. 'maxLength': 100,
  63. 'pattern': '.*\\S.*',
  64. },
  65. 'description': '货物类型名称数组。必须先调用筛选项选择「货物类型」取得value。',
  66. },
  67. 'product_ids': dict(
  68. id_array,
  69. description='物流产品ID数组。必须先调用筛选项选择「物流产品」取得value。',
  70. ),
  71. 'page': {
  72. 'type': 'integer',
  73. 'minimum': 1,
  74. 'maximum': 100,
  75. 'default': 1,
  76. },
  77. 'limit': {
  78. 'type': 'integer',
  79. 'minimum': 1,
  80. 'maximum': 100,
  81. 'default': 20,
  82. },
  83. },
  84. 'required': ['inbound_date_start', 'inbound_date_end'],
  85. 'additionalProperties': False,
  86. },
  87. }
  88. def call(
  89. self,
  90. inbound_date_start,
  91. inbound_date_end,
  92. customer_ids=None,
  93. sales_ids=None,
  94. merchandiser_ids=None,
  95. packing_types=None,
  96. product_ids=None,
  97. page=1,
  98. limit=20,
  99. request_id='rq_query_receive_volume_list',
  100. ):
  101. if self.api_client is None:
  102. raise RuntimeError(
  103. 'api client is required for query_receive_volume_list'
  104. )
  105. payload = {
  106. 'inbound_date_start': self._date(inbound_date_start, 'inbound_date_start'),
  107. 'inbound_date_end': self._date(inbound_date_end, 'inbound_date_end'),
  108. }
  109. self._assert_window(
  110. payload['inbound_date_start'], payload['inbound_date_end']
  111. )
  112. if customer_ids is not None:
  113. payload['customer_ids'] = self._id_list(customer_ids, 'customer_ids')
  114. if sales_ids is not None:
  115. payload['sales_ids'] = self._id_list(sales_ids, 'sales_ids')
  116. if merchandiser_ids is not None:
  117. payload['merchandiser_ids'] = self._id_list(
  118. merchandiser_ids, 'merchandiser_ids'
  119. )
  120. if packing_types is not None:
  121. payload['packing_types'] = self._name_list(
  122. packing_types, 'packing_types'
  123. )
  124. if product_ids is not None:
  125. payload['product_ids'] = self._id_list(product_ids, 'product_ids')
  126. payload['page'] = self._bounded_integer(page, 'page')
  127. payload['limit'] = self._bounded_integer(limit, 'limit')
  128. return self.api_client.call_tool(
  129. self.name, self.route_path, payload, request_id,
  130. )
  131. @staticmethod
  132. def _date(value, field):
  133. text = str(value or '').strip()
  134. try:
  135. parsed = datetime.strptime(text, '%Y-%m-%d').date()
  136. except ValueError:
  137. raise ValueError(field + ' is invalid')
  138. if parsed.strftime('%Y-%m-%d') != text:
  139. raise ValueError(field + ' is invalid')
  140. return text
  141. @staticmethod
  142. def _assert_window(start, end):
  143. start_date = datetime.strptime(start, '%Y-%m-%d').date()
  144. end_date = datetime.strptime(end, '%Y-%m-%d').date()
  145. if end_date < start_date or (end_date - start_date).days > 30:
  146. raise ValueError('inbound date window is invalid')
  147. @staticmethod
  148. def _id_list(values, field):
  149. if not isinstance(values, list) or not values:
  150. raise ValueError(field + ' is invalid')
  151. result = []
  152. for value in values:
  153. if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
  154. raise ValueError(field + ' is invalid')
  155. if value not in result:
  156. result.append(value)
  157. if len(result) > 200:
  158. raise ValueError('at most 200 ids are allowed')
  159. return result
  160. @staticmethod
  161. def _name_list(values, field):
  162. if not isinstance(values, list) or not values:
  163. raise ValueError(field + ' is invalid')
  164. result = []
  165. for value in values:
  166. if not isinstance(value, str):
  167. raise ValueError(field + ' is invalid')
  168. value = value.strip()
  169. if not value or len(value) > 100:
  170. raise ValueError(field + ' is invalid')
  171. if value not in result:
  172. result.append(value)
  173. if len(result) > 200:
  174. raise ValueError('at most 200 names are allowed')
  175. return result
  176. @staticmethod
  177. def _bounded_integer(value, field):
  178. if isinstance(value, bool) or not isinstance(value, int):
  179. raise ValueError(field + ' is invalid')
  180. if value < 1 or value > 100:
  181. raise ValueError(field + ' is invalid')
  182. return value