已合并
fix: 解决调用ir_execution_service stream接口时,接口日志中缺少llm调用日志的问题 #253
fix: 解决调用ir_execution_service stream接口时,接口日志中缺少llm调用日志的问题 #253
已合并
GYHHelloworld创建于 5月12日
4 个文件变更+392-0
@@ -44,6 +44,7 @@ from .runtime_support.core_log_bridge import install_core_log_bridge
44from .runtime_support.interface_logger import (44from .runtime_support.interface_logger import (
45 init_interface_logger_from_env,45 init_interface_logger_from_env,
46 install_core_interface_sink,46 install_core_interface_sink,
47+ install_runner_llm_stream_interface_callbacks,
47 install_runner_tool_interface_callbacks,48 install_runner_tool_interface_callbacks,
48 log_server,49 log_server,
49 set_request_context,50 set_request_context,
@@ -58,6 +59,7 @@ install_runner_tool_alarm_callbacks()
58init_interface_logger_from_env()59init_interface_logger_from_env()
59install_core_interface_sink()60install_core_interface_sink()
60install_runner_tool_interface_callbacks()61install_runner_tool_interface_callbacks()
62+install_runner_llm_stream_interface_callbacks()
61 63 
62# Workflow 默认超时较短,复杂 DSL 续跑时容易误判超时。64# Workflow 默认超时较短,复杂 DSL 续跑时容易误判超时。
63os.environ.setdefault("WORKFLOW_EXECUTE_TIMEOUT", "300")65os.environ.setdefault("WORKFLOW_EXECUTE_TIMEOUT", "300")
@@ -26,6 +26,43 @@ _SERVER_SOURCE_IP: contextvars.ContextVar[str] = contextvars.ContextVar("interfa
26_TOOL_INTERFACE_T0: dict[str, float] = {}26_TOOL_INTERFACE_T0: dict[str, float] = {}
27_RUNNER_TOOL_INTERFACE_REGISTERED = False27_RUNNER_TOOL_INTERFACE_REGISTERED = False
28 28 
29+_RUNNER_LLM_STREAM_INTERFACE_REGISTERED = False
30+ 
31+ 
32+def _llm_stream_output_looks_terminal(*, result: Any) -> bool:
33+ """判断是否为单次流式 HTTP 的收尾 payload(避免对 per_item 的每个 chunk 打 interface)。"""
34+ if isinstance(result, list):
35+ return True
36+ if result is None:
37+ return False
38+ if getattr(result, "usage_metadata", None) is not None:
39+ return True
40+ fr = getattr(result, "finish_reason", None)
41+ if fr is None:
42+ return False
43+ s = str(fr).strip().lower()
44+ return bool(s) and s not in ("null", "none")
45+ 
46+ 
47+def _model_name_from_configs(*, model_config: Any, model_client_config: Any) -> str:
48+ if model_config is not None:
49+ mn = getattr(model_config, "model_name", None)
50+ if mn:
51+ return str(mn)
52+ m = getattr(model_config, "model", None)
53+ if m:
54+ return str(m)
55+ return ""
56+ 
57+ 
58+def _model_provider_str(model_client_config: Any) -> str:
59+ if model_client_config is None:
60+ return ""
61+ cp = getattr(model_client_config, "client_provider", None)
62+ if cp is None:
63+ return ""
64+ return str(getattr(cp, "value", cp))
65+ 
29 66 
30def set_request_context(*, request_id: str, source_ip: str = "") -> None:67def set_request_context(*, request_id: str, source_ip: str = "") -> None:
31 _REQUEST_ID.set(str(request_id or "").strip())68 _REQUEST_ID.set(str(request_id or "").strip())
@@ -357,3 +394,56 @@ def install_runner_tool_interface_callbacks() -> None:
357 _RUNNER_TOOL_INTERFACE_REGISTERED = True394 _RUNNER_TOOL_INTERFACE_REGISTERED = True
358 _LOG.info("Interface runner tool callbacks installed.")395 _LOG.info("Interface runner tool callbacks installed.")
359 396 
397+ 
398+def install_runner_llm_stream_interface_callbacks() -> None:
399+ """流式 LLM 成功结束时写一条 interface(补充 core 内 stream 无「API response received」日志的空档)。
400+ 
401+ Model.stream 对 LLM_STREAM_OUTPUT 使用 emit_after 默认 per_item,每个 chunk 触发一次;
402+ 仅在收尾 chunk(finish_reason / usage_metadata)或 once 模式下的 list 结果时记录,避免刷屏。
403+ 耗时固定为 0(不在此维护起止时钟)。流式失败由 core_log_bridge 写 interface。
404+ """
405+ global _RUNNER_LLM_STREAM_INTERFACE_REGISTERED
406+ if _RUNNER_LLM_STREAM_INTERFACE_REGISTERED:
407+ return
408+ 
409+ from openjiuwen.core.runner import Runner
410+ from openjiuwen.core.runner.callback.events import LLMCallEvents
411+ 
412+ fw = Runner.callback_framework
413+ 
414+ async def _on_llm_stream_output(
415+ *,
416+ result: Any = None,
417+ model_config: Any = None,
418+ model_client_config: Any = None,
419+ **_: Any,
420+ ) -> None:
421+ if not _llm_stream_output_looks_terminal(result=result):
422+ return
423+ mn = _model_name_from_configs(
424+ model_config=model_config, model_client_config=model_client_config
425+ )
426+ prov = _model_provider_str(model_client_config)
427+ add_info: dict[str, Any] = {
428+ "source": "runner_callback",
429+ "event_type": "llm_stream_end",
430+ "is_stream": True,
431+ "model_name": mn,
432+ "model_provider": prov,
433+ }
434+ if isinstance(result, list):
435+ add_info["chunk_count"] = len(result)
436+ log_client(
437+ interface_name="llm.call",
438+ cost_ms=0.0,
439+ ok=True,
440+ return_code=0,
441+ return_info="stream completed",
442+ dest_ip="",
443+ add_info=add_info,
444+ )
445+ 
446+ fw.register_sync(LLMCallEvents.LLM_STREAM_OUTPUT, _on_llm_stream_output, priority=-1000)
447+ _RUNNER_LLM_STREAM_INTERFACE_REGISTERED = True
448+ _LOG.info("Interface runner LLM stream callbacks installed.")
449+ 
@@ -0,0 +1,166 @@
1+{
2+ "id": "f6753e87-8e2f-491c-acb1-9f33c3fc6746",
3+ "version": "draft",
4+ "name": "llm_json_2_lr",
5+ "description": "测试大模型输出为json:包含支持的Array类型:[String]/[Integer]/[Number]/[Boolean]/[Object]/[Array] ",
6+ "start_id": [
7+ "start_Ag0l2"
8+ ],
9+ "end_id": [
10+ "end_Ag0l2"
11+ ],
12+ "components": [
13+ {
14+ "id": "start_Ag0l2",
15+ "version": "",
16+ "name": "开始",
17+ "description": "",
18+ "type": 1,
19+ "type_version": "1.0.0",
20+ "inputs": {
21+ "query": "${query}"
22+ },
23+ "outputs": {},
24+ "branches": [],
25+ "configs": {}
26+ },
27+ {
28+ "id": "end_Ag0l2",
29+ "version": "",
30+ "name": "结束",
31+ "description": "",
32+ "type": 2,
33+ "type_version": "1.0.0",
34+ "inputs": {
35+ "result": "${llm_JtqFn}"
36+ },
37+ "outputs": {},
38+ "branches": [],
39+ "configs": {
40+ "response_template": "",
41+ "stream_output": true
42+ }
43+ },
44+ {
45+ "id": "llm_JtqFn",
46+ "version": "",
47+ "name": "大模型1",
48+ "description": "",
49+ "type": 3,
50+ "type_version": "1.0.0",
51+ "inputs": {
52+ "input": "${start_Ag0l2.query}"
53+ },
54+ "outputs": {
55+ "string_array": "${string_array}",
56+ "integer_array": "${integer_array}",
57+ "boolean_array": "${boolean_array}",
58+ "number_array": "${number_array}"
59+ },
60+ "branches": [],
61+ "configs": {
62+ "model": {
63+ "model_id": "106",
64+ "model_client_config": {
65+ "client_provider": "openai",
66+ "api_key": null,
67+ "api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1",
68+ "timeout": 90,
69+ "model_name": "qwen-plus",
70+ "parameters": {
71+ "temperature": 0.2,
72+ "max_tokens": 4000,
73+ "top_p": 0.2
74+ }
75+ },
76+ "request_config": {
77+ "model_name": "qwen-plus",
78+ "temperature": 0.2,
79+ "top_p": 0.2,
80+ "stream": true
81+ }
82+ },
83+ "template_content": [
84+ {
85+ "role": "system",
86+ "content": "请根据描述,输出一个json对象:\n对象必须包含以下 5 个字段,每个字段是一个数组:\n1. string_array:字符串数组,至少包含3个水果名,如 [\"苹果\", \"香蕉\"]\n2. integer_array:整数数组,至少包含3个正整数\n3. boolean_array:布尔数组,包含3个值,只能是 true 或 false\n4. number_array:浮点数数组,至少包含3个浮点数"
87+ },
88+ {
89+ "role": "user",
90+ "content": "{{input}}"
91+ }
92+ ],
93+ "response_format_type": "json",
94+ "output_config": {
95+ "string_array": {
96+ "type": "array",
97+ "description": "",
98+ "items": {
99+ "type": "string"
100+ },
101+ "required": true
102+ },
103+ "integer_array": {
104+ "type": "array",
105+ "description": "",
106+ "items": {
107+ "type": "integer"
108+ },
109+ "required": true
110+ },
111+ "boolean_array": {
112+ "type": "array",
113+ "description": "",
114+ "items": {
115+ "type": "boolean"
116+ },
117+ "required": true
118+ },
119+ "number_array": {
120+ "type": "array",
121+ "description": "",
122+ "items": {
123+ "type": "number"
124+ },
125+ "required": true
126+ }
127+ },
128+ "enable_history": false
129+ }
130+ }
131+ ],
132+ "connections": [
133+ {
134+ "source": "start_Ag0l2",
135+ "branch_id": "",
136+ "target": "llm_JtqFn"
137+ },
138+ {
139+ "source": "llm_JtqFn",
140+ "branch_id": "",
141+ "target": "end_Ag0l2"
142+ }
143+ ],
144+ "inputs": {
145+ "type": "object",
146+ "properties": {
147+ "query": {
148+ "type": "string",
149+ "description": ""
150+ }
151+ },
152+ "required": [
153+ "query"
154+ ]
155+ },
156+ "outputs": {
157+ "result": {
158+ "type": "ref",
159+ "description": ""
160+ }
161+ },
162+ "configs": {},
163+ "dependencies": {
164+ "workflows": []
165+ }
166+}
@@ -0,0 +1,134 @@
1+{
2+ "id": "44b9a508-d73c-414d-8401-1413804f72a6",
3+ "version": "draft",
4+ "name": "test_llm_end_002",
5+ "description": "start-llm-end",
6+ "start_id": [
7+ "start_X66EV"
8+ ],
9+ "end_id": [
10+ "end_X66EV"
11+ ],
12+ "components": [
13+ {
14+ "id": "start_X66EV",
15+ "version": "",
16+ "name": "开始",
17+ "description": "",
18+ "type": 1,
19+ "type_version": "1.0.0",
20+ "inputs": {
21+ "query": "${query}"
22+ },
23+ "outputs": {},
24+ "branches": [],
25+ "configs": {}
26+ },
27+ {
28+ "id": "end_X66EV",
29+ "version": "",
30+ "name": "结束",
31+ "description": "",
32+ "type": 2,
33+ "type_version": "1.0.0",
34+ "inputs": {
35+ "result": "${llm_mqZ1U.output}"
36+ },
37+ "outputs": {},
38+ "branches": [],
39+ "configs": {
40+ "response_template": "end组件的输出{{result}}",
41+ "stream_output": true
42+ }
43+ },
44+ {
45+ "id": "llm_mqZ1U",
46+ "version": "",
47+ "name": "大模型",
48+ "description": "",
49+ "type": 3,
50+ "type_version": "1.0.0",
51+ "inputs": {
52+ "input": "${start_X66EV.query}"
53+ },
54+ "outputs": {
55+ "output": "${output}"
56+ },
57+ "branches": [],
58+ "configs": {
59+ "model": {
60+ "model_id": "132",
61+ "model_client_config": {
62+ "client_provider": "openai",
63+ "api_key": null,
64+ "api_base": "https://dashscope.aliyuncs.com/compatible-mode/v1/",
65+ "timeout": 300,
66+ "model_name": "qwen-plus",
67+ "parameters": {
68+ "temperature": 0.7,
69+ "max_tokens": 4000,
70+ "top_p": 0.9
71+ }
72+ },
73+ "request_config": {
74+ "model_name": "qwen-plus",
75+ "temperature": 0.7,
76+ "top_p": 0.9,
77+ "stream": true
78+ }
79+ },
80+ "template_content": [
81+ {
82+ "role": "system",
83+ "content": ""
84+ },
85+ {
86+ "role": "user",
87+ "content": "{{input}}"
88+ }
89+ ],
90+ "response_format_type": "text",
91+ "output_config": {
92+ "output": {
93+ "type": "string",
94+ "description": "",
95+ "required": true
96+ }
97+ },
98+ "enable_history": false
99+ }
100+ }
101+ ],
102+ "connections": [
103+ {
104+ "source": "start_X66EV",
105+ "branch_id": "",
106+ "target": "llm_mqZ1U"
107+ },
108+ {
109+ "source": "llm_mqZ1U",
110+ "branch_id": "",
111+ "target": "end_X66EV"
112+ }
113+ ],
114+ "inputs": {
115+ "type": "object",
116+ "properties": {
117+ "query": {
118+ "type": "string",
119+ "description": ""
120+ }
121+ },
122+ "required": []
123+ },
124+ "outputs": {
125+ "result": {
126+ "type": "ref",
127+ "description": ""
128+ }
129+ },
130+ "configs": {},
131+ "dependencies": {
132+ "workflows": []
133+ }
134+}