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.
- package/app/.env +44 -44
- package/app/.env.dev +44 -44
- package/app/.env.prod.bj11 +44 -44
- package/app/.env.prod.sh20 +44 -44
- package/app/.env.prod.sz31 +44 -44
- package/app/.env.test.bj12 +44 -44
- package/app/service/chat_service.py +12 -4
- package/app/service/union_service.py +13 -8
- package/app/utils/common_utils.py +14 -7
- package/app/utils/function_utils.py +98 -1
- package/package.json +1 -1
- package/tools/field_dictionary.yaml +28 -0
- package/tools/tool_definitions.yaml +374 -300
|
@@ -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"]
|
|
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
|
-
|
|
85
|
-
url
|
|
86
|
-
headers
|
|
87
|
-
json_data
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
error_msg = f"连接失败:{e}"
|
|
86
|
+
logger.error(error_msg)
|
|
85
87
|
except Timeout as e:
|
|
86
|
-
|
|
88
|
+
error_msg = f"请求超时:{e}"
|
|
89
|
+
logger.error(error_msg)
|
|
87
90
|
except RequestException as e:
|
|
88
|
-
|
|
91
|
+
error_msg = f"请求异常:{e}"
|
|
92
|
+
logger.error(error_msg)
|
|
89
93
|
except Exception as e:
|
|
90
|
-
|
|
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
|
-
|
|
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
|
@@ -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: 系统成功率和失败率通常反向相关。成功率下降时,应同时检查失败率是否上升。
|