union-app-chat-stream 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -156,14 +156,14 @@ class ChatService:
156
156
  def tool_result_event(tool_result: str) -> ChatResponse:
157
157
  return ChatResponse(conversationId=conversation_id, tool_result=_public_tool_result(tool_result))
158
158
 
159
- def heartbeat_event(tool_name: str, elapsed_seconds: float) -> ChatResponse:
159
+ def heartbeat_event(tool_name: str, elapsed_seconds: float, message: str = "") -> ChatResponse:
160
160
  return ChatResponse(
161
161
  conversationId=conversation_id,
162
162
  heartbeat={
163
163
  "type": "tool_call_running",
164
164
  "tool": tool_name,
165
165
  "elapsedSeconds": round(elapsed_seconds, 3),
166
- "message": f"工具 {tool_name} 仍在执行,请继续等待。",
166
+ "message": message or f"工具 {tool_name} 仍在执行,请继续等待。",
167
167
  },
168
168
  )
169
169
 
@@ -241,16 +241,23 @@ class ChatService:
241
241
  logger.info(f"执行工具调用。conversation_id={conversation_id} tool={name} args={_preview(args, 200)}")
242
242
  yield tool_call_event(f"\n[调用工具: {name}({args})]\n")
243
243
 
244
+ retry_notice = {"message": ""}
245
+
246
+ def retry_callback(message: str) -> None:
247
+ retry_notice["message"] = message
248
+
244
249
  tool_context = ToolContext(
245
250
  union_service=self._union_service,
246
251
  rag_service=self._rag,
247
252
  jsessionid=jsessionid,
253
+ retry_callback=retry_callback,
248
254
  )
249
255
  result = yield from self._call_function_with_heartbeats(
250
256
  name,
251
257
  args,
252
258
  tool_context,
253
259
  heartbeat_event,
260
+ retry_notice,
254
261
  )
255
262
  logger.info(f"工具调用完成。conversation_id={conversation_id} tool={name} result_preview={_preview(result, 300)}")
256
263
  yield tool_result_event(result)
@@ -291,6 +298,7 @@ class ChatService:
291
298
  args: str,
292
299
  tool_context: ToolContext,
293
300
  heartbeat_event,
301
+ retry_notice,
294
302
  ) -> Generator[ChatResponse, None, str]:
295
303
  interval = self._tool_heartbeat_interval
296
304
  if interval <= 0:
@@ -304,7 +312,7 @@ class ChatService:
304
312
  try:
305
313
  return future.result(timeout=interval)
306
314
  except FutureTimeoutError:
307
- yield heartbeat_event(name, time.monotonic() - started_at)
315
+ yield heartbeat_event(name, time.monotonic() - started_at, retry_notice.get("message", ""))
308
316
  finally:
309
317
  executor.shutdown(wait=False, cancel_futures=True)
310
318
 
@@ -340,6 +348,6 @@ class ChatService:
340
348
  fn = getattr(tc, "function", None)
341
349
  if fn is not None:
342
350
  if getattr(fn, "name", None):
343
- slot["function"]["name"] += fn.name
351
+ slot["function"]["name"] = fn.name
344
352
  if getattr(fn, "arguments", None):
345
353
  slot["function"]["arguments"] += fn.arguments
@@ -12,7 +12,6 @@ class UnionService:
12
12
 
13
13
  # 常量定义
14
14
  API_MAX_RETRIES = 10 # API最大重试次数
15
- BIGDATA_API_TIMEOUT = 300
16
15
  BIGDATA_INTERFACE_FULL_LINK = "running_cnt.full_link_monthly"
17
16
  BIGDATA_INTERFACE_BANK_MONTHLY = "running_cnt.bank_monthly"
18
17
 
@@ -72,6 +71,8 @@ class UnionService:
72
71
  jsessionid: str,
73
72
  description: str = "查询联合运维数据",
74
73
  path: str = "",
74
+ timeout: Optional[int] = None,
75
+ retry_callback=None,
75
76
  ) -> Tuple[Optional[Any], str]:
76
77
  """按 tool definition 中配置的 path 和 JSON 参数查询联合运维 API。"""
77
78
  url = self._build_union_url(self._union_base_url, path)
@@ -81,13 +82,17 @@ class UnionService:
81
82
  return None, f"{description}失败: 登录态ID为空"
82
83
 
83
84
  try:
84
- response = common_utils.call_https_api(
85
- url=url,
86
- headers=self._get_union_headers(jsessionid),
87
- json_data=payload,
88
- timeout=self.BIGDATA_API_TIMEOUT,
89
- max_retries=self.API_MAX_RETRIES,
90
- )
85
+ kwargs = {
86
+ "url": url,
87
+ "headers": self._get_union_headers(jsessionid),
88
+ "json_data": payload,
89
+ "max_retries": self.API_MAX_RETRIES,
90
+ }
91
+ if timeout is not None:
92
+ kwargs["timeout"] = timeout
93
+ if retry_callback:
94
+ kwargs["retry_callback"] = retry_callback
95
+ response = common_utils.call_https_api(**kwargs)
91
96
  if not response.get("success", True):
92
97
  raise RuntimeError(response.get("error_msg", "请求失败"))
93
98
  status_code = response.get("status_code")
@@ -1,5 +1,5 @@
1
1
  import requests
2
- from typing import Optional, Dict, Any
2
+ from typing import Optional, Dict, Any, Callable
3
3
  from requests.exceptions import RequestException, Timeout, ConnectionError
4
4
  import json
5
5
  import time
@@ -30,12 +30,13 @@ def call_https_api(
30
30
  data: Optional[Dict[str, Any]] = None,
31
31
  json_data: Optional[Dict[str, Any]] = None,
32
32
  headers: Optional[Dict[str, str]] = None,
33
- timeout: int = 10,
33
+ timeout: int = 10,
34
34
  verify_ssl: bool = False,
35
35
  auth: Optional[tuple] = None,
36
36
  proxies: Optional[Dict[str, Any]] = None,
37
37
  max_retries: int = 0,
38
- retry_delay: float = 1.0
38
+ retry_delay: float = 1.0,
39
+ retry_callback: Optional[Callable[[str], None]] = None,
39
40
 
40
41
  ) -> Dict[str, Any]:
41
42
  default_headers = {
@@ -81,14 +82,20 @@ def call_https_api(
81
82
  logger.info(f"请求成功:{response.status_code},返回值:{response.text}")
82
83
  return result
83
84
  except ConnectionError as e:
84
- logger.error(f"连接失败:{e}")
85
+ error_msg = f"连接失败:{e}"
86
+ logger.error(error_msg)
85
87
  except Timeout as e:
86
- logger.error(f"请求超时:{e}")
88
+ error_msg = f"请求超时:{e}"
89
+ logger.error(error_msg)
87
90
  except RequestException as e:
88
- logger.error(f"请求异常:{e}")
91
+ error_msg = f"请求异常:{e}"
92
+ logger.error(error_msg)
89
93
  except Exception as e:
90
- logger.error(f"未知错误:{e}")
94
+ error_msg = f"未知错误:{e}"
95
+ logger.error(error_msg)
91
96
  if attempt < max_retries:
97
+ if retry_callback:
98
+ retry_callback(f"{error_msg},正在重试(第{attempt + 2}次)")
92
99
  time.sleep(retry_delay)
93
100
  else:
94
101
  break
@@ -14,6 +14,7 @@ import yaml
14
14
 
15
15
  PROJECT_ROOT = Path(__file__).resolve().parents[2]
16
16
  DEFAULT_TOOL_DEFINITIONS_PATH = PROJECT_ROOT / "tools" / "tool_definitions.yaml"
17
+ FIELD_DICTIONARY_PATH = PROJECT_ROOT / "tools" / "field_dictionary.yaml"
17
18
 
18
19
 
19
20
  @dataclass
@@ -21,6 +22,7 @@ class ToolContext:
21
22
  union_service: Optional[Any] = None
22
23
  rag_service: Optional[Any] = None
23
24
  jsessionid: str = ""
25
+ retry_callback: Optional[Callable[[str], None]] = None
24
26
 
25
27
 
26
28
  ToolFunction = Callable[[Dict[str, Any], ToolContext], Dict[str, Any]]
@@ -46,6 +48,44 @@ def _load_yaml(path: Path) -> Dict[str, Any]:
46
48
  return data if isinstance(data, dict) else {}
47
49
 
48
50
 
51
+ def _normalize_field_name(name: str) -> str:
52
+ return re.sub(r"[\s_-]+", "", str(name)).lower()
53
+
54
+
55
+ @lru_cache(maxsize=1)
56
+ def _field_dictionary_index() -> Dict[str, Any]:
57
+ if not FIELD_DICTIONARY_PATH.exists():
58
+ return {"fallback_description": "", "exact": {}, "normalized": {}, "relations": []}
59
+
60
+ config = _load_yaml(FIELD_DICTIONARY_PATH)
61
+ if not isinstance(config, dict):
62
+ config = {}
63
+
64
+ exact: Dict[str, Dict[str, Any]] = {}
65
+ normalized: Dict[str, Dict[str, Any]] = {}
66
+ fields = config.get("fields") if isinstance(config.get("fields"), dict) else {}
67
+ for canonical, raw_info in fields.items():
68
+ if not isinstance(raw_info, dict):
69
+ continue
70
+ info = {key: value for key, value in raw_info.items() if key not in {"aliases", "patterns"}}
71
+ info["canonical"] = canonical
72
+ exact[canonical] = info
73
+ normalized[_normalize_field_name(canonical)] = info
74
+ aliases = raw_info.get("aliases") if isinstance(raw_info.get("aliases"), list) else []
75
+ for alias in aliases:
76
+ alias = str(alias)
77
+ exact[alias] = info
78
+ normalized[_normalize_field_name(alias)] = info
79
+
80
+ relations = config.get("relations") if isinstance(config.get("relations"), list) else []
81
+ return {
82
+ "fallback_description": str(config.get("fallback_description") or ""),
83
+ "exact": exact,
84
+ "normalized": normalized,
85
+ "relations": relations,
86
+ }
87
+
88
+
49
89
  @lru_cache(maxsize=1)
50
90
  def _tool_definition_config() -> Dict[str, Any]:
51
91
  path = Path(os.environ.get("TOOL_DEFINITIONS_PATH", DEFAULT_TOOL_DEFINITIONS_PATH))
@@ -54,6 +94,7 @@ def _tool_definition_config() -> Dict[str, Any]:
54
94
 
55
95
  def reload_tool_configs():
56
96
  _tool_definition_config.cache_clear()
97
+ _field_dictionary_index.cache_clear()
57
98
 
58
99
 
59
100
  def _raw_tool_definitions() -> List[Dict[str, Any]]:
@@ -177,6 +218,56 @@ def _render_value(value: Any, args: Dict[str, Any], *, allow_missing: bool = Fal
177
218
  return value
178
219
 
179
220
 
221
+ def _collect_data_fields(data: Any, sample_size: int = 5) -> List[str]:
222
+ if not isinstance(data, list):
223
+ return []
224
+ fields: List[str] = []
225
+ seen = set()
226
+ for item in data[:sample_size]:
227
+ if not isinstance(item, dict):
228
+ continue
229
+ for key in item:
230
+ key = str(key)
231
+ if key not in seen:
232
+ seen.add(key)
233
+ fields.append(key)
234
+ return fields
235
+
236
+
237
+ def _build_field_context(data: Any) -> Dict[str, Any]:
238
+ fields = _collect_data_fields(data)
239
+ if not fields:
240
+ return {}
241
+
242
+ index = _field_dictionary_index()
243
+ fallback = index["fallback_description"]
244
+ descriptions: Dict[str, Dict[str, Any]] = {}
245
+ canonical_fields = set()
246
+ for field in fields:
247
+ info = index["exact"].get(field) or index["normalized"].get(_normalize_field_name(field))
248
+ if info:
249
+ descriptions[field] = dict(info)
250
+ canonical_fields.add(str(info["canonical"]))
251
+ elif fallback:
252
+ descriptions[field] = {"description": fallback}
253
+
254
+ relations = []
255
+ for relation in index["relations"]:
256
+ if not isinstance(relation, dict):
257
+ continue
258
+ when_all = relation.get("when_all")
259
+ description = relation.get("description")
260
+ if isinstance(when_all, list) and description and all(str(field) in canonical_fields for field in when_all):
261
+ relations.append(str(description))
262
+
263
+ context: Dict[str, Any] = {}
264
+ if descriptions:
265
+ context["field_descriptions"] = descriptions
266
+ if relations:
267
+ context["field_relations"] = relations
268
+ return context
269
+
270
+
180
271
  def _base_result(spec: Dict[str, Any], args: Dict[str, Any]) -> Dict[str, Any]:
181
272
  result = {
182
273
  "tool_name": spec["name"],
@@ -213,7 +304,12 @@ def _run_backend_tool(
213
304
  if context.union_service is None:
214
305
  raise ValueError("Union服务未初始化")
215
306
  method = getattr(context.union_service, method_name)
216
- data, message = method(payload, context.jsessionid, description=description, path=path)
307
+ kwargs = {"description": description, "path": path}
308
+ if isinstance(backend.get("timeout"), int):
309
+ kwargs["timeout"] = backend["timeout"]
310
+ if context.retry_callback:
311
+ kwargs["retry_callback"] = context.retry_callback
312
+ data, message = method(payload, context.jsessionid, **kwargs)
217
313
  elif service_name == "rag_service":
218
314
  if context.rag_service is None:
219
315
  raise ValueError("知识库检索服务未初始化")
@@ -236,6 +332,7 @@ def _run_backend_tool(
236
332
  })
237
333
  if path:
238
334
  result["backend"]["path"] = path
335
+ result.update(_build_field_context(data))
239
336
  return result
240
337
 
241
338
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "union-app-chat-stream",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "Union operations chat stream Flask application package.",
5
5
  "license": "UNLICENSED",
6
6
  "files": [
@@ -0,0 +1,28 @@
1
+ fallback_description: 字段未命中全局字段字典。请结合字段名、字段值和上下文谨慎理解,不要编造枚举含义。
2
+ fields:
3
+ sysSuccessPercent:
4
+ meaning: 系统成功率,表示成功交易占总交易的比例,通常越高越好。
5
+ aliases:
6
+ - sysSuccessPercent
7
+ - avgSuccessPercent
8
+ - avg_success_percent
9
+ - successPer
10
+ failRate:
11
+ meaning: 失败率,通常表示失败交易笔数占总交易笔数的比例。
12
+ aliases:
13
+ - failRate
14
+ - fail_rate
15
+ faultLevel:
16
+ meaning: 故障等级,表示故障严重程度。
17
+ enum:
18
+ P1: 重大故障
19
+ P2: 较大故障
20
+ P3: 一般故障
21
+ aliases:
22
+ - faultLevel
23
+ - fault_level
24
+ relations:
25
+ - when_all:
26
+ - sysSuccessPercent
27
+ - failRate
28
+ description: 系统成功率和失败率通常反向相关。成功率下降时,应同时检查失败率是否上升。