union-app-chat-stream 1.0.3 → 1.0.5
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/PROJECT_OVERVIEW.md +16 -0
- package/app/.env +14 -14
- package/app/.env.dev +14 -14
- package/app/.env.prod.bj11 +14 -14
- package/app/.env.prod.sh20 +14 -14
- package/app/.env.prod.sz31 +14 -14
- package/app/.env.test.bj12 +14 -14
- package/app/config/env_config.py +6 -0
- package/app/manager/chatstream_manager.py +47 -15
- package/app/models/schemas.py +5 -3
- package/app/service/chat_service.py +119 -2
- package/app/service/union_service.py +7 -0
- package/app/utils/common_utils.py +1 -1
- package/app/views/view_chatstream.py +62 -5
- package/app/wsgi.py +1 -1
- package/package.json +3 -1
- package/tools/prompts.yaml +2 -0
- package/app/__pycache__/__init__.cpython-312.pyc +0 -0
- package/app/__pycache__/authenticated_user.cpython-312.pyc +0 -0
- package/app/__pycache__/extensions.cpython-312.pyc +0 -0
- package/app/__pycache__/wsgi.cpython-312.pyc +0 -0
- package/app/config/__pycache__/config_loader.cpython-312.pyc +0 -0
- package/app/config/__pycache__/env_config.cpython-312.pyc +0 -0
- package/app/config/__pycache__/logger_config.cpython-312.pyc +0 -0
- package/app/manager/__init__.py +0 -4
- package/app/manager/__pycache__/__init__.cpython-312.pyc +0 -0
- package/app/manager/__pycache__/chatstream_manager.cpython-312.pyc +0 -0
- package/app/manager/__pycache__/prompts.cpython-312.pyc +0 -0
- package/app/manager/__pycache__/runtime_manager.cpython-312.pyc +0 -0
- package/app/manager/__pycache__/toolcall_manager.cpython-312.pyc +0 -0
- package/app/models/__pycache__/schemas.cpython-312.pyc +0 -0
- package/app/service/__init__.py +0 -4
- package/app/service/__pycache__/__init__.cpython-312.pyc +0 -0
- package/app/service/__pycache__/chat_service.cpython-312.pyc +0 -0
- package/app/service/__pycache__/llm_service.cpython-312.pyc +0 -0
- package/app/service/__pycache__/rag_service.cpython-312.pyc +0 -0
- package/app/service/__pycache__/tool_call_service.cpython-312.pyc +0 -0
- package/app/service/__pycache__/union_service.cpython-312.pyc +0 -0
- package/app/utils/__pycache__/__init__.cpython-312.pyc +0 -0
- package/app/utils/__pycache__/common_utils.cpython-312.pyc +0 -0
- package/app/utils/__pycache__/debug_context.cpython-312.pyc +0 -0
- package/app/utils/__pycache__/function_utils.cpython-312.pyc +0 -0
- package/app/utils/__pycache__/jwt_utils.cpython-312.pyc +0 -0
- package/app/views/__pycache__/__init__.cpython-312.pyc +0 -0
- package/app/views/__pycache__/view_chatstream.cpython-312.pyc +0 -0
- package/app/views/__pycache__/view_healthcheck.cpython-312.pyc +0 -0
- package/app/views/__pycache__/view_runtime.cpython-312.pyc +0 -0
package/PROJECT_OVERVIEW.md
CHANGED
|
@@ -162,6 +162,22 @@ Deployment files are organized under `deploy/`:
|
|
|
162
162
|
treat knowledge-base "related functions" as routing hints unless code
|
|
163
163
|
explicitly promotes them to executable tools.
|
|
164
164
|
|
|
165
|
+
## Streaming Tool-Call Contract
|
|
166
|
+
|
|
167
|
+
- External tool calls may run for minutes or longer because some tools query
|
|
168
|
+
large data platforms such as Hive. Do not treat a long-running tool call as a
|
|
169
|
+
timeout by default.
|
|
170
|
+
- During tool execution, `/chatstream/v1/chat/stream` emits SSE `heartbeat`
|
|
171
|
+
events every `TOOL_CALL_HEARTBEAT_INTERVAL` seconds. The payload is the normal
|
|
172
|
+
chat response JSON with a `heartbeat` object, including `type`, `tool`,
|
|
173
|
+
`elapsedSeconds`, and `message`.
|
|
174
|
+
- Frontends should listen for both `message` and `heartbeat` SSE events.
|
|
175
|
+
Heartbeat events mean the stream is alive and the current tool is still
|
|
176
|
+
running; they are not model content and should not be rendered as answer text.
|
|
177
|
+
- Tool execution failures return a `message` event with `finish_reason="error"`
|
|
178
|
+
and `errorMsg`. If tool-calling reaches `TOOLS_MAX_ROUNDS`, return
|
|
179
|
+
`finish_reason="error"` and `errorMsg="工具调用轮数达到上限(N轮)"`.
|
|
180
|
+
|
|
165
181
|
## Validation
|
|
166
182
|
|
|
167
183
|
Use the smallest reliable checks that cover the change:
|
package/app/.env
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Flask and request authentication
|
|
2
|
-
SECRET_KEY=<
|
|
2
|
+
SECRET_KEY=<SECRET_KEY>
|
|
3
3
|
|
|
4
4
|
# External API endpoints
|
|
5
|
-
GET_USE_INFO_URL
|
|
6
|
-
GET_ORG_INFO_URL
|
|
7
|
-
GET_JIRA_INFO_URL
|
|
8
|
-
GET_BIGDATA_URL
|
|
9
|
-
GET_UNION_BASE_URL
|
|
5
|
+
GET_USE_INFO_URL=https://10.47.214.188:8443/common/getUserInfo
|
|
6
|
+
GET_ORG_INFO_URL=https://10.47.214.188:8443/api/getOrgInfoByOrgCode
|
|
7
|
+
GET_JIRA_INFO_URL=https://10.47.214.188:8443/api/getjiraData
|
|
8
|
+
GET_BIGDATA_URL=http://172.31.3.134:8080/vmock/ai/fullLinkData
|
|
9
|
+
GET_UNION_BASE_URL=http://127.0.0.1:8089/
|
|
10
10
|
|
|
11
11
|
# Legacy external API tokens
|
|
12
|
-
GET_ORG_INFO_URL_TOKEN=<
|
|
13
|
-
GET_JIRA_INFO_URL_TOKEN=<
|
|
12
|
+
GET_ORG_INFO_URL_TOKEN=<GET_ORG_INFO_URL_TOKEN>
|
|
13
|
+
GET_JIRA_INFO_URL_TOKEN=<GET_JIRA_INFO_URL_TOKEN>
|
|
14
14
|
|
|
15
15
|
# Authorization and logging
|
|
16
16
|
PERMISSIONS=
|
|
@@ -20,25 +20,25 @@ LOG_DIR=/data/appLogs
|
|
|
20
20
|
|
|
21
21
|
# Runtime environment and JWT
|
|
22
22
|
FLASK_ENV=dev
|
|
23
|
-
JWT_SECRET_KEY=<
|
|
23
|
+
JWT_SECRET_KEY=<JWT_SECRET_KEY>
|
|
24
24
|
JWT_EXPIRATION_SECOND=900
|
|
25
25
|
JWT_RENEW_SECOND=700
|
|
26
26
|
|
|
27
27
|
# LLM provider
|
|
28
|
-
LLM_URL
|
|
29
|
-
LLM_KEY
|
|
28
|
+
LLM_URL=https://open.bigmodel.cn/api/paas/v4/
|
|
29
|
+
LLM_KEY=f024b21a682248999b42f696a42dfaad.PIySxpJN8xM1evpZ
|
|
30
30
|
LLM_MODEL=GLM-4.7-Flash
|
|
31
31
|
LLM_MAX_TOKENS=4096
|
|
32
32
|
LLM_TEMPERATURE=0.7
|
|
33
33
|
LLM_TOP_P=0.9
|
|
34
34
|
|
|
35
35
|
# Chat behavior
|
|
36
|
-
SYSTEM_PROMPT
|
|
36
|
+
SYSTEM_PROMPT=你是网络支付清算平台(网联平台)联合运维的智能客服助手,面向联合运维成员单位提供咨询与指引服务。
|
|
37
37
|
|
|
38
38
|
# Business filter
|
|
39
39
|
FILTER_ENABLED=false
|
|
40
|
-
FILTER_ALLOWED_KEYWORDS
|
|
41
|
-
FILTER_REJECTION_MESSAGE
|
|
40
|
+
FILTER_ALLOWED_KEYWORDS=网联,联合运维,运维,生产变更,变更,生产运行,运行,系统成功率,业务成功率,异常,故障,定级,关闭渠道,联合处置,大型单位,中型单位,小型单位,银行,支付,清算
|
|
41
|
+
FILTER_REJECTION_MESSAGE=抱歉,我是联合运维智能客服,只能回答与联合运维相关的问题,例如生产变更、生产运行、故障定级、周期评价、业务范围等。请重新描述您的问题。
|
|
42
42
|
|
|
43
43
|
# Tool and conversation settings
|
|
44
44
|
TOOLS_MAX_ROUNDS=5
|
package/app/.env.dev
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Flask and request authentication
|
|
2
|
-
SECRET_KEY=<
|
|
2
|
+
SECRET_KEY=<SECRET_KEY>
|
|
3
3
|
|
|
4
4
|
# External API endpoints
|
|
5
|
-
GET_USE_INFO_URL
|
|
6
|
-
GET_ORG_INFO_URL
|
|
7
|
-
GET_JIRA_INFO_URL
|
|
8
|
-
GET_BIGDATA_URL
|
|
9
|
-
GET_UNION_BASE_URL
|
|
5
|
+
GET_USE_INFO_URL=https://10.47.214.188:8443/common/getUserInfo
|
|
6
|
+
GET_ORG_INFO_URL=https://10.47.214.188:8443/api/getOrgInfoByOrgCode
|
|
7
|
+
GET_JIRA_INFO_URL=http://172.31.3.134:8080/vmock/jira-data
|
|
8
|
+
GET_BIGDATA_URL=http://172.31.3.134:8080/vmock/ai/fullLinkData
|
|
9
|
+
GET_UNION_BASE_URL=http://127.0.0.1:8089/
|
|
10
10
|
|
|
11
11
|
# Legacy external API tokens
|
|
12
|
-
GET_ORG_INFO_URL_TOKEN=<
|
|
13
|
-
GET_JIRA_INFO_URL_TOKEN=<
|
|
12
|
+
GET_ORG_INFO_URL_TOKEN=<GET_ORG_INFO_URL_TOKEN>
|
|
13
|
+
GET_JIRA_INFO_URL_TOKEN=<GET_JIRA_INFO_URL_TOKEN>
|
|
14
14
|
|
|
15
15
|
# Authorization and logging
|
|
16
16
|
PERMISSIONS=
|
|
@@ -20,25 +20,25 @@ LOG_DIR=/Users/simon/code/union-py-app/data/appLogs
|
|
|
20
20
|
|
|
21
21
|
# Runtime environment and JWT
|
|
22
22
|
FLASK_ENV=dev
|
|
23
|
-
JWT_SECRET_KEY=<
|
|
23
|
+
JWT_SECRET_KEY=<JWT_SECRET_KEY>
|
|
24
24
|
JWT_EXPIRATION_SECOND=900
|
|
25
25
|
JWT_RENEW_SECOND=700
|
|
26
26
|
|
|
27
27
|
# LLM provider
|
|
28
|
-
LLM_URL
|
|
29
|
-
LLM_KEY
|
|
28
|
+
LLM_URL=https://open.bigmodel.cn/api/paas/v4/
|
|
29
|
+
LLM_KEY=f024b21a682248999b42f696a42dfaad.PIySxpJN8xM1evpZ
|
|
30
30
|
LLM_MODEL=GLM-4.7-Flash
|
|
31
31
|
LLM_MAX_TOKENS=4096
|
|
32
32
|
LLM_TEMPERATURE=0.7
|
|
33
33
|
LLM_TOP_P=0.9
|
|
34
34
|
|
|
35
35
|
# Chat behavior
|
|
36
|
-
SYSTEM_PROMPT
|
|
36
|
+
SYSTEM_PROMPT=你是网络支付清算平台(网联平台)联合运维的智能客服助手,面向联合运维成员单位提供咨询与指引服务。
|
|
37
37
|
|
|
38
38
|
# Business filter
|
|
39
39
|
FILTER_ENABLED=false
|
|
40
|
-
FILTER_ALLOWED_KEYWORDS
|
|
41
|
-
FILTER_REJECTION_MESSAGE
|
|
40
|
+
FILTER_ALLOWED_KEYWORDS=网联,联合运维,运维,生产变更,变更,生产运行,运行,系统成功率,业务成功率,异常,故障,定级,关闭渠道,联合处置,大型单位,中型单位,小型单位,银行,支付,清算
|
|
41
|
+
FILTER_REJECTION_MESSAGE=抱歉,我是联合运维智能客服,只能回答与联合运维相关的问题,例如生产变更、生产运行、故障定级、周期评价、业务范围等。请重新描述您的问题。
|
|
42
42
|
|
|
43
43
|
# Tool and conversation settings
|
|
44
44
|
TOOLS_MAX_ROUNDS=5
|
package/app/.env.prod.bj11
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Flask and request authentication
|
|
2
|
-
SECRET_KEY=<
|
|
2
|
+
SECRET_KEY=<SECRET_KEY>
|
|
3
3
|
|
|
4
4
|
# External API endpoints
|
|
5
|
-
GET_USE_INFO_URL
|
|
6
|
-
GET_ORG_INFO_URL
|
|
7
|
-
GET_JIRA_INFO_URL
|
|
8
|
-
GET_BIGDATA_URL
|
|
9
|
-
GET_UNION_BASE_URL
|
|
5
|
+
GET_USE_INFO_URL=https://10.16.100.236:8443/common/getUserInfo
|
|
6
|
+
GET_ORG_INFO_URL=https://10.16.100.236:8443/api/getOrgInfoByOrgCode
|
|
7
|
+
GET_JIRA_INFO_URL=https://10.16.100.236:8443/api/getjiraData
|
|
8
|
+
GET_BIGDATA_URL=https://10.16.100.236:8443/union-op/bigdata/query
|
|
9
|
+
GET_UNION_BASE_URL=https://10.16.100.236:8443/
|
|
10
10
|
|
|
11
11
|
# Legacy external API tokens
|
|
12
|
-
GET_ORG_INFO_URL_TOKEN=<
|
|
13
|
-
GET_JIRA_INFO_URL_TOKEN=<
|
|
12
|
+
GET_ORG_INFO_URL_TOKEN=<GET_ORG_INFO_URL_TOKEN>
|
|
13
|
+
GET_JIRA_INFO_URL_TOKEN=<GET_JIRA_INFO_URL_TOKEN>
|
|
14
14
|
|
|
15
15
|
# Authorization and logging
|
|
16
16
|
PERMISSIONS=
|
|
@@ -20,25 +20,25 @@ LOG_DIR=/data/appLogs
|
|
|
20
20
|
|
|
21
21
|
# Runtime environment and JWT
|
|
22
22
|
FLASK_ENV=prod.bj11
|
|
23
|
-
JWT_SECRET_KEY=<
|
|
23
|
+
JWT_SECRET_KEY=<JWT_SECRET_KEY>
|
|
24
24
|
JWT_EXPIRATION_SECOND=900
|
|
25
25
|
JWT_RENEW_SECOND=700
|
|
26
26
|
|
|
27
27
|
# LLM provider
|
|
28
|
-
LLM_URL=<
|
|
29
|
-
LLM_KEY=<
|
|
28
|
+
LLM_URL=<LLM_URL>
|
|
29
|
+
LLM_KEY=<LLM_KEY>
|
|
30
30
|
LLM_MODEL=glm-5
|
|
31
31
|
LLM_MAX_TOKENS=4096
|
|
32
32
|
LLM_TEMPERATURE=0.7
|
|
33
33
|
LLM_TOP_P=0.9
|
|
34
34
|
|
|
35
35
|
# Chat behavior
|
|
36
|
-
SYSTEM_PROMPT
|
|
36
|
+
SYSTEM_PROMPT=你是网络支付清算平台(网联平台)联合运维的智能客服助手,面向联合运维成员单位提供咨询与指引服务。
|
|
37
37
|
|
|
38
38
|
# Business filter
|
|
39
39
|
FILTER_ENABLED=false
|
|
40
|
-
FILTER_ALLOWED_KEYWORDS
|
|
41
|
-
FILTER_REJECTION_MESSAGE
|
|
40
|
+
FILTER_ALLOWED_KEYWORDS=网联,联合运维,运维,生产变更,变更,生产运行,运行,系统成功率,业务成功率,异常,故障,定级,关闭渠道,联合处置,大型单位,中型单位,小型单位,银行,支付,清算
|
|
41
|
+
FILTER_REJECTION_MESSAGE=抱歉,我是联合运维智能客服,只能回答与联合运维相关的问题,例如生产变更、生产运行、故障定级、周期评价、业务范围等。请重新描述您的问题。
|
|
42
42
|
|
|
43
43
|
# Tool and conversation settings
|
|
44
44
|
TOOLS_MAX_ROUNDS=5
|
package/app/.env.prod.sh20
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Flask and request authentication
|
|
2
|
-
SECRET_KEY=<
|
|
2
|
+
SECRET_KEY=<SECRET_KEY>
|
|
3
3
|
|
|
4
4
|
# External API endpoints
|
|
5
|
-
GET_USE_INFO_URL
|
|
6
|
-
GET_ORG_INFO_URL
|
|
7
|
-
GET_JIRA_INFO_URL
|
|
8
|
-
GET_BIGDATA_URL
|
|
9
|
-
GET_UNION_BASE_URL
|
|
5
|
+
GET_USE_INFO_URL=https://10.32.100.236:8443/common/getUserInfo
|
|
6
|
+
GET_ORG_INFO_URL=https://10.32.100.236:8443/api/getOrgInfoByOrgCode
|
|
7
|
+
GET_JIRA_INFO_URL=https://10.32.100.236:8443/api/getjiraData
|
|
8
|
+
GET_BIGDATA_URL=https://10.32.100.236:8443/union-op/bigdata/query
|
|
9
|
+
GET_UNION_BASE_URL=https://10.16.100.236:8443/
|
|
10
10
|
|
|
11
11
|
# Legacy external API tokens
|
|
12
|
-
GET_ORG_INFO_URL_TOKEN=<
|
|
13
|
-
GET_JIRA_INFO_URL_TOKEN=<
|
|
12
|
+
GET_ORG_INFO_URL_TOKEN=<GET_ORG_INFO_URL_TOKEN>
|
|
13
|
+
GET_JIRA_INFO_URL_TOKEN=<GET_JIRA_INFO_URL_TOKEN>
|
|
14
14
|
|
|
15
15
|
# Authorization and logging
|
|
16
16
|
PERMISSIONS=
|
|
@@ -20,25 +20,25 @@ LOG_DIR=/data/appLogs
|
|
|
20
20
|
|
|
21
21
|
# Runtime environment and JWT
|
|
22
22
|
FLASK_ENV=prod.sh20
|
|
23
|
-
JWT_SECRET_KEY=<
|
|
23
|
+
JWT_SECRET_KEY=<JWT_SECRET_KEY>
|
|
24
24
|
JWT_EXPIRATION_SECOND=900
|
|
25
25
|
JWT_RENEW_SECOND=700
|
|
26
26
|
|
|
27
27
|
# LLM provider
|
|
28
|
-
LLM_URL=<
|
|
29
|
-
LLM_KEY=<
|
|
28
|
+
LLM_URL=<LLM_URL>
|
|
29
|
+
LLM_KEY=<LLM_KEY>
|
|
30
30
|
LLM_MODEL=glm-5
|
|
31
31
|
LLM_MAX_TOKENS=4096
|
|
32
32
|
LLM_TEMPERATURE=0.7
|
|
33
33
|
LLM_TOP_P=0.9
|
|
34
34
|
|
|
35
35
|
# Chat behavior
|
|
36
|
-
SYSTEM_PROMPT
|
|
36
|
+
SYSTEM_PROMPT=你是网络支付清算平台(网联平台)联合运维的智能客服助手,面向联合运维成员单位提供咨询与指引服务。
|
|
37
37
|
|
|
38
38
|
# Business filter
|
|
39
39
|
FILTER_ENABLED=false
|
|
40
|
-
FILTER_ALLOWED_KEYWORDS
|
|
41
|
-
FILTER_REJECTION_MESSAGE
|
|
40
|
+
FILTER_ALLOWED_KEYWORDS=网联,联合运维,运维,生产变更,变更,生产运行,运行,系统成功率,业务成功率,异常,故障,定级,关闭渠道,联合处置,大型单位,中型单位,小型单位,银行,支付,清算
|
|
41
|
+
FILTER_REJECTION_MESSAGE=抱歉,我是联合运维智能客服,只能回答与联合运维相关的问题,例如生产变更、生产运行、故障定级、周期评价、业务范围等。请重新描述您的问题。
|
|
42
42
|
|
|
43
43
|
# Tool and conversation settings
|
|
44
44
|
TOOLS_MAX_ROUNDS=5
|
package/app/.env.prod.sz31
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Flask and request authentication
|
|
2
|
-
SECRET_KEY=<
|
|
2
|
+
SECRET_KEY=<SECRET_KEY>
|
|
3
3
|
|
|
4
4
|
# External API endpoints
|
|
5
|
-
GET_USE_INFO_URL
|
|
6
|
-
GET_ORG_INFO_URL
|
|
7
|
-
GET_JIRA_INFO_URL
|
|
8
|
-
GET_BIGDATA_URL
|
|
9
|
-
GET_UNION_BASE_URL
|
|
5
|
+
GET_USE_INFO_URL=https://10.80.100.236:8443/common/getUserInfo
|
|
6
|
+
GET_ORG_INFO_URL=https://10.80.100.236:8443/api/getOrgInfoByOrgCode
|
|
7
|
+
GET_JIRA_INFO_URL=https://10.80.100.236:8443/api/getjiraData
|
|
8
|
+
GET_BIGDATA_URL=https://10.80.100.236:8443/union-op/bigdata/query
|
|
9
|
+
GET_UNION_BASE_URL=https://10.16.100.236:8443/
|
|
10
10
|
|
|
11
11
|
# Legacy external API tokens
|
|
12
|
-
GET_ORG_INFO_URL_TOKEN=<
|
|
13
|
-
GET_JIRA_INFO_URL_TOKEN=<
|
|
12
|
+
GET_ORG_INFO_URL_TOKEN=<GET_ORG_INFO_URL_TOKEN>
|
|
13
|
+
GET_JIRA_INFO_URL_TOKEN=<GET_JIRA_INFO_URL_TOKEN>
|
|
14
14
|
|
|
15
15
|
# Authorization and logging
|
|
16
16
|
PERMISSIONS=
|
|
@@ -20,25 +20,25 @@ LOG_DIR=/data/appLogs
|
|
|
20
20
|
|
|
21
21
|
# Runtime environment and JWT
|
|
22
22
|
FLASK_ENV=prod.sz31
|
|
23
|
-
JWT_SECRET_KEY=<
|
|
23
|
+
JWT_SECRET_KEY=<JWT_SECRET_KEY>
|
|
24
24
|
JWT_EXPIRATION_SECOND=900
|
|
25
25
|
JWT_RENEW_SECOND=700
|
|
26
26
|
|
|
27
27
|
# LLM provider
|
|
28
|
-
LLM_URL=<
|
|
29
|
-
LLM_KEY=<
|
|
28
|
+
LLM_URL=<LLM_URL>
|
|
29
|
+
LLM_KEY=<LLM_KEY>
|
|
30
30
|
LLM_MODEL=glm-5
|
|
31
31
|
LLM_MAX_TOKENS=4096
|
|
32
32
|
LLM_TEMPERATURE=0.7
|
|
33
33
|
LLM_TOP_P=0.9
|
|
34
34
|
|
|
35
35
|
# Chat behavior
|
|
36
|
-
SYSTEM_PROMPT
|
|
36
|
+
SYSTEM_PROMPT=你是网络支付清算平台(网联平台)联合运维的智能客服助手,面向联合运维成员单位提供咨询与指引服务。
|
|
37
37
|
|
|
38
38
|
# Business filter
|
|
39
39
|
FILTER_ENABLED=false
|
|
40
|
-
FILTER_ALLOWED_KEYWORDS
|
|
41
|
-
FILTER_REJECTION_MESSAGE
|
|
40
|
+
FILTER_ALLOWED_KEYWORDS=网联,联合运维,运维,生产变更,变更,生产运行,运行,系统成功率,业务成功率,异常,故障,定级,关闭渠道,联合处置,大型单位,中型单位,小型单位,银行,支付,清算
|
|
41
|
+
FILTER_REJECTION_MESSAGE=抱歉,我是联合运维智能客服,只能回答与联合运维相关的问题,例如生产变更、生产运行、故障定级、周期评价、业务范围等。请重新描述您的问题。
|
|
42
42
|
|
|
43
43
|
# Tool and conversation settings
|
|
44
44
|
TOOLS_MAX_ROUNDS=5
|
package/app/.env.test.bj12
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# Flask and request authentication
|
|
2
|
-
SECRET_KEY=<
|
|
2
|
+
SECRET_KEY=<SECRET_KEY>
|
|
3
3
|
|
|
4
4
|
# External API endpoints
|
|
5
|
-
GET_USE_INFO_URL
|
|
6
|
-
GET_ORG_INFO_URL
|
|
7
|
-
GET_JIRA_INFO_URL
|
|
8
|
-
GET_BIGDATA_URL
|
|
9
|
-
GET_UNION_BASE_URL
|
|
5
|
+
GET_USE_INFO_URL=https://10.47.214.188:8443/common/getUserInfo
|
|
6
|
+
GET_ORG_INFO_URL=https://10.47.214.188:8443/api/getOrgInfoByOrgCode
|
|
7
|
+
GET_JIRA_INFO_URL=http://172.31.3.134:8080/vmock/jira-data
|
|
8
|
+
GET_BIGDATA_URL=http://172.31.3.134:8080/vmock/ai/fullLinkData
|
|
9
|
+
GET_UNION_BASE_URL=http://172.31.3.134:8080/
|
|
10
10
|
|
|
11
11
|
# Legacy external API tokens
|
|
12
|
-
GET_ORG_INFO_URL_TOKEN=<
|
|
13
|
-
GET_JIRA_INFO_URL_TOKEN=<
|
|
12
|
+
GET_ORG_INFO_URL_TOKEN=<GET_ORG_INFO_URL_TOKEN>
|
|
13
|
+
GET_JIRA_INFO_URL_TOKEN=<GET_JIRA_INFO_URL_TOKEN>
|
|
14
14
|
|
|
15
15
|
# Authorization and logging
|
|
16
16
|
PERMISSIONS=
|
|
@@ -20,25 +20,25 @@ LOG_DIR=/data/appLogs
|
|
|
20
20
|
|
|
21
21
|
# Runtime environment and JWT
|
|
22
22
|
FLASK_ENV=test.prod.bj12
|
|
23
|
-
JWT_SECRET_KEY=<
|
|
23
|
+
JWT_SECRET_KEY=<JWT_SECRET_KEY>
|
|
24
24
|
JWT_EXPIRATION_SECOND=900
|
|
25
25
|
JWT_RENEW_SECOND=700
|
|
26
26
|
|
|
27
27
|
# LLM provider
|
|
28
|
-
LLM_URL=<
|
|
29
|
-
LLM_KEY=<
|
|
28
|
+
LLM_URL=<LLM_URL>
|
|
29
|
+
LLM_KEY=<LLM_KEY>
|
|
30
30
|
LLM_MODEL=glm-5
|
|
31
31
|
LLM_MAX_TOKENS=4096
|
|
32
32
|
LLM_TEMPERATURE=0.7
|
|
33
33
|
LLM_TOP_P=0.9
|
|
34
34
|
|
|
35
35
|
# Chat behavior
|
|
36
|
-
SYSTEM_PROMPT
|
|
36
|
+
SYSTEM_PROMPT=你是网络支付清算平台(网联平台)联合运维的智能客服助手,面向联合运维成员单位提供咨询与指引服务。
|
|
37
37
|
|
|
38
38
|
# Business filter
|
|
39
39
|
FILTER_ENABLED=false
|
|
40
|
-
FILTER_ALLOWED_KEYWORDS
|
|
41
|
-
FILTER_REJECTION_MESSAGE
|
|
40
|
+
FILTER_ALLOWED_KEYWORDS=网联,联合运维,运维,生产变更,变更,生产运行,运行,系统成功率,业务成功率,异常,故障,定级,关闭渠道,联合处置,大型单位,中型单位,小型单位,银行,支付,清算
|
|
41
|
+
FILTER_REJECTION_MESSAGE=抱歉,我是联合运维智能客服,只能回答与联合运维相关的问题,例如生产变更、生产运行、故障定级、周期评价、业务范围等。请重新描述您的问题。
|
|
42
42
|
|
|
43
43
|
# Tool and conversation settings
|
|
44
44
|
TOOLS_MAX_ROUNDS=5
|
package/app/config/env_config.py
CHANGED
|
@@ -71,6 +71,12 @@ class Config:
|
|
|
71
71
|
)
|
|
72
72
|
|
|
73
73
|
TOOLS_MAX_ROUNDS = _env_int("TOOLS_MAX_ROUNDS", 5)
|
|
74
|
+
TOOL_CALL_HEARTBEAT_INTERVAL = _env_float("TOOL_CALL_HEARTBEAT_INTERVAL", 15.0)
|
|
75
|
+
CHAT_OPENING_QUESTIONS = _env_list("CHAT_OPENING_QUESTIONS", [
|
|
76
|
+
"上周全链路运行质量如何",
|
|
77
|
+
"最近有哪些成员机构交易异常",
|
|
78
|
+
"当前系统运行风险点有哪些",
|
|
79
|
+
])
|
|
74
80
|
|
|
75
81
|
CONVERSATION_MAX_HISTORY = _env_int("CONVERSATION_MAX_HISTORY", 20)
|
|
76
82
|
CONVERSATION_TTL = _env_int("CONVERSATION_TTL", 3600)
|
|
@@ -14,6 +14,8 @@ class ChatstreamManager:
|
|
|
14
14
|
self._chat_service = chat_service
|
|
15
15
|
self._rag_service = rag_service
|
|
16
16
|
self._conversations: Dict[str, Dict] = {}
|
|
17
|
+
# ponytail: process-local guard; use shared storage only if workers need cross-process cancellation.
|
|
18
|
+
self._active_streams: Dict[str, Dict] = {}
|
|
17
19
|
self._max_history = config["CONVERSATION_MAX_HISTORY"]
|
|
18
20
|
self._ttl = config["CONVERSATION_TTL"]
|
|
19
21
|
self._lock = threading.Lock()
|
|
@@ -62,6 +64,24 @@ class ChatstreamManager:
|
|
|
62
64
|
if len(conversation["messages"]) > max_messages:
|
|
63
65
|
conversation["messages"] = conversation["messages"][-max_messages:]
|
|
64
66
|
|
|
67
|
+
def _start_stream(self, jsessionid: str, conversation_id: str) -> threading.Event:
|
|
68
|
+
abort_event = threading.Event()
|
|
69
|
+
with self._lock:
|
|
70
|
+
active = self._active_streams.get(jsessionid)
|
|
71
|
+
if active and active["conversation_id"] != conversation_id:
|
|
72
|
+
active["abort_event"].set()
|
|
73
|
+
self._active_streams[jsessionid] = {
|
|
74
|
+
"conversation_id": conversation_id,
|
|
75
|
+
"abort_event": abort_event,
|
|
76
|
+
}
|
|
77
|
+
return abort_event
|
|
78
|
+
|
|
79
|
+
def _finish_stream(self, jsessionid: str, abort_event: threading.Event):
|
|
80
|
+
with self._lock:
|
|
81
|
+
active = self._active_streams.get(jsessionid)
|
|
82
|
+
if active and active["abort_event"] is abort_event:
|
|
83
|
+
del self._active_streams[jsessionid]
|
|
84
|
+
|
|
65
85
|
def chat_stream(
|
|
66
86
|
self,
|
|
67
87
|
conversation_id: Optional[str],
|
|
@@ -69,22 +89,34 @@ class ChatstreamManager:
|
|
|
69
89
|
jsessionid: str,
|
|
70
90
|
) -> Generator[ChatResponse, None, None]:
|
|
71
91
|
normalized_conversation_id = self.normalize_conversation_id(conversation_id)
|
|
92
|
+
abort_event = self._start_stream(jsessionid, normalized_conversation_id)
|
|
72
93
|
history = self._get_history(normalized_conversation_id)
|
|
73
94
|
answer_parts: List[str] = []
|
|
74
|
-
|
|
95
|
+
saved = False
|
|
75
96
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
try:
|
|
98
|
+
for chunk in self._chat_service.tool_call_stream(
|
|
99
|
+
normalized_conversation_id,
|
|
100
|
+
question,
|
|
101
|
+
tools,
|
|
102
|
+
history,
|
|
103
|
+
jsessionid,
|
|
104
|
+
):
|
|
105
|
+
if abort_event.is_set():
|
|
106
|
+
yield ChatResponse(
|
|
107
|
+
conversationId=normalized_conversation_id,
|
|
108
|
+
content="当前对话已被新的对话替换,已停止。",
|
|
109
|
+
finish_reason="abort",
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
if chunk.content:
|
|
113
|
+
answer_parts.append(chunk.content)
|
|
114
|
+
if chunk.finish_reason and answer_parts and not saved:
|
|
115
|
+
self._append_exchange(normalized_conversation_id, question, "".join(answer_parts))
|
|
116
|
+
saved = True
|
|
117
|
+
yield chunk
|
|
88
118
|
|
|
89
|
-
|
|
90
|
-
|
|
119
|
+
if answer_parts and not saved:
|
|
120
|
+
self._append_exchange(normalized_conversation_id, question, "".join(answer_parts))
|
|
121
|
+
finally:
|
|
122
|
+
self._finish_stream(jsessionid, abort_event)
|
package/app/models/schemas.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
from typing import Literal, Optional
|
|
1
|
+
from typing import Any, Dict, Literal, Optional
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class ChatRequest(BaseModel):
|
|
7
7
|
"""聊天请求模型"""
|
|
8
|
-
model_config = ConfigDict(extra="
|
|
8
|
+
model_config = ConfigDict(extra="ignore")
|
|
9
9
|
|
|
10
10
|
conversation_id: Optional[str] = Field(
|
|
11
11
|
default=None,
|
|
@@ -24,7 +24,9 @@ class ChatResponse(BaseModel):
|
|
|
24
24
|
reasoning_content: Optional[str] = Field(default=None, description="模型推理内容增量")
|
|
25
25
|
tool_call: Optional[str] = Field(default=None, description="工具调用信息")
|
|
26
26
|
tool_result: Optional[str] = Field(default=None, description="工具执行结果")
|
|
27
|
-
|
|
27
|
+
heartbeat: Optional[Dict[str, Any]] = Field(default=None, description="长耗时工具调用心跳")
|
|
28
|
+
error_msg: Optional[str] = Field(default=None, alias="errorMsg", description="错误信息")
|
|
29
|
+
finish_reason: Optional[Literal["stop", "error", "rejected", "done", "abort"]] = Field(
|
|
28
30
|
default=None,
|
|
29
31
|
description="结束原因;中间流式增量为空",
|
|
30
32
|
)
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
1
5
|
from pathlib import Path
|
|
2
6
|
from typing import Dict, Generator, List
|
|
3
7
|
|
|
@@ -14,6 +18,36 @@ def _preview(text: str, limit: int = 300) -> str:
|
|
|
14
18
|
return str(text).replace("\n", " ")[:limit]
|
|
15
19
|
|
|
16
20
|
|
|
21
|
+
PUBLIC_TOOL_RESULT_FIELDS = ("tool_name", "display_name", "arguments", "status", "message")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _public_tool_result(tool_result: str) -> str:
|
|
25
|
+
try:
|
|
26
|
+
payload = json.loads(tool_result)
|
|
27
|
+
except json.JSONDecodeError:
|
|
28
|
+
payload = {}
|
|
29
|
+
|
|
30
|
+
if not isinstance(payload, dict):
|
|
31
|
+
payload = {}
|
|
32
|
+
|
|
33
|
+
public_payload = {key: payload[key] for key in PUBLIC_TOOL_RESULT_FIELDS if key in payload}
|
|
34
|
+
public_payload.setdefault("message", "工具调用完成")
|
|
35
|
+
return json.dumps(public_payload, ensure_ascii=False)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _time_context() -> str:
|
|
43
|
+
now = datetime.now(timezone.utc).astimezone(timezone(timedelta(hours=8)))
|
|
44
|
+
return (
|
|
45
|
+
"当前基准时间:"
|
|
46
|
+
f"{now:%Y-%m-%d %H:%M:%S} UTC+8。"
|
|
47
|
+
"用户问题中的“当前、近期、上周、昨天、今天”等相对时间,均按该基准时间计算。"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
17
51
|
def _load_prompts() -> Dict[str, str]:
|
|
18
52
|
path = Path(__file__).resolve().parents[2] / "tools" / "prompts.yaml"
|
|
19
53
|
if not path.exists():
|
|
@@ -55,6 +89,7 @@ class ChatService:
|
|
|
55
89
|
self._top_p = config["LLM_TOP_P"]
|
|
56
90
|
self._system_prompt = config["SYSTEM_PROMPT"]
|
|
57
91
|
self._tools_max_rounds = config["TOOLS_MAX_ROUNDS"]
|
|
92
|
+
self._tool_heartbeat_interval = float(config.get("TOOL_CALL_HEARTBEAT_INTERVAL", 15.0))
|
|
58
93
|
self._rag = rag_service
|
|
59
94
|
self._union_service = union_service
|
|
60
95
|
|
|
@@ -76,6 +111,7 @@ class ChatService:
|
|
|
76
111
|
messages = []
|
|
77
112
|
if self._system_prompt:
|
|
78
113
|
messages.append({"role": "system", "content": self._system_prompt})
|
|
114
|
+
messages.append({"role": "system", "content": _time_context()})
|
|
79
115
|
messages.extend(history)
|
|
80
116
|
messages.append({"role": "user", "content": user_question})
|
|
81
117
|
return messages
|
|
@@ -118,12 +154,32 @@ class ChatService:
|
|
|
118
154
|
return ChatResponse(conversationId=conversation_id, tool_call=tool_call)
|
|
119
155
|
|
|
120
156
|
def tool_result_event(tool_result: str) -> ChatResponse:
|
|
121
|
-
return ChatResponse(conversationId=conversation_id, tool_result=tool_result)
|
|
157
|
+
return ChatResponse(conversationId=conversation_id, tool_result=_public_tool_result(tool_result))
|
|
158
|
+
|
|
159
|
+
def heartbeat_event(tool_name: str, elapsed_seconds: float) -> ChatResponse:
|
|
160
|
+
return ChatResponse(
|
|
161
|
+
conversationId=conversation_id,
|
|
162
|
+
heartbeat={
|
|
163
|
+
"type": "tool_call_running",
|
|
164
|
+
"tool": tool_name,
|
|
165
|
+
"elapsedSeconds": round(elapsed_seconds, 3),
|
|
166
|
+
"message": f"工具 {tool_name} 仍在执行,请继续等待。",
|
|
167
|
+
},
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def error_event(error_msg: str) -> ChatResponse:
|
|
171
|
+
return ChatResponse(
|
|
172
|
+
conversationId=conversation_id,
|
|
173
|
+
content=f"[错误] {error_msg}",
|
|
174
|
+
errorMsg=error_msg,
|
|
175
|
+
finish_reason="error",
|
|
176
|
+
)
|
|
122
177
|
|
|
123
178
|
try:
|
|
124
179
|
messages = self._build_tool_messages(history, question)
|
|
125
180
|
max_rounds = self._tools_max_rounds
|
|
126
181
|
final_answer = ""
|
|
182
|
+
completed_without_tool_call = False
|
|
127
183
|
|
|
128
184
|
logger.info(f"开始模型流式调用。conversation_id={conversation_id} model={self._model} question={_preview(question, 120)}")
|
|
129
185
|
for round_idx in range(max_rounds):
|
|
@@ -168,6 +224,7 @@ class ChatService:
|
|
|
168
224
|
|
|
169
225
|
if not tool_calls_map:
|
|
170
226
|
final_answer = current_content
|
|
227
|
+
completed_without_tool_call = True
|
|
171
228
|
break
|
|
172
229
|
|
|
173
230
|
assistant_tool_calls = [tool_calls_map[i] for i in sorted(tool_calls_map)]
|
|
@@ -189,16 +246,33 @@ class ChatService:
|
|
|
189
246
|
rag_service=self._rag,
|
|
190
247
|
jsessionid=jsessionid,
|
|
191
248
|
)
|
|
192
|
-
result =
|
|
249
|
+
result = yield from self._call_function_with_heartbeats(
|
|
250
|
+
name,
|
|
251
|
+
args,
|
|
252
|
+
tool_context,
|
|
253
|
+
heartbeat_event,
|
|
254
|
+
)
|
|
193
255
|
logger.info(f"工具调用完成。conversation_id={conversation_id} tool={name} result_preview={_preview(result, 300)}")
|
|
194
256
|
yield tool_result_event(result)
|
|
195
257
|
|
|
258
|
+
tool_error = self._extract_tool_error(result)
|
|
259
|
+
if tool_error:
|
|
260
|
+
logger.error(f"工具调用失败。conversation_id={conversation_id} tool={name} error={tool_error}")
|
|
261
|
+
yield error_event(f"工具调用失败: {tool_error}")
|
|
262
|
+
return
|
|
263
|
+
|
|
196
264
|
messages.append({
|
|
197
265
|
"role": "tool",
|
|
198
266
|
"content": result,
|
|
199
267
|
"tool_call_id": tc["id"],
|
|
200
268
|
})
|
|
201
269
|
|
|
270
|
+
if not completed_without_tool_call:
|
|
271
|
+
error_msg = f"工具调用轮数达到上限({max_rounds}轮)"
|
|
272
|
+
logger.error(f"对话异常结束。conversation_id={conversation_id} error={error_msg}")
|
|
273
|
+
yield error_event(error_msg)
|
|
274
|
+
return
|
|
275
|
+
|
|
202
276
|
logger.info(f"对话完成。conversation_id={conversation_id} final_answer_chars={len(final_answer)} final_answer_preview={_preview(final_answer)}")
|
|
203
277
|
yield ChatResponse(conversationId=conversation_id, finish_reason="stop")
|
|
204
278
|
|
|
@@ -207,9 +281,52 @@ class ChatService:
|
|
|
207
281
|
yield ChatResponse(
|
|
208
282
|
conversationId=conversation_id,
|
|
209
283
|
content=f"[错误] 模型调用异常: {str(e)}",
|
|
284
|
+
errorMsg=f"模型调用异常: {str(e)}",
|
|
210
285
|
finish_reason="error",
|
|
211
286
|
)
|
|
212
287
|
|
|
288
|
+
def _call_function_with_heartbeats(
|
|
289
|
+
self,
|
|
290
|
+
name: str,
|
|
291
|
+
args: str,
|
|
292
|
+
tool_context: ToolContext,
|
|
293
|
+
heartbeat_event,
|
|
294
|
+
) -> Generator[ChatResponse, None, str]:
|
|
295
|
+
interval = self._tool_heartbeat_interval
|
|
296
|
+
if interval <= 0:
|
|
297
|
+
return call_function(name, args, tool_context)
|
|
298
|
+
|
|
299
|
+
executor = ThreadPoolExecutor(max_workers=1)
|
|
300
|
+
future = executor.submit(call_function, name, args, tool_context)
|
|
301
|
+
started_at = time.monotonic()
|
|
302
|
+
try:
|
|
303
|
+
while True:
|
|
304
|
+
try:
|
|
305
|
+
return future.result(timeout=interval)
|
|
306
|
+
except FutureTimeoutError:
|
|
307
|
+
yield heartbeat_event(name, time.monotonic() - started_at)
|
|
308
|
+
finally:
|
|
309
|
+
executor.shutdown(wait=False, cancel_futures=True)
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def _extract_tool_error(result: str) -> str:
|
|
313
|
+
try:
|
|
314
|
+
payload = json.loads(result)
|
|
315
|
+
except json.JSONDecodeError:
|
|
316
|
+
return ""
|
|
317
|
+
if not isinstance(payload, dict):
|
|
318
|
+
return ""
|
|
319
|
+
|
|
320
|
+
error = payload.get("error")
|
|
321
|
+
if error:
|
|
322
|
+
return str(error)
|
|
323
|
+
|
|
324
|
+
status = payload.get("status")
|
|
325
|
+
if status and status != "success":
|
|
326
|
+
message = payload.get("message") or f"工具返回状态: {status}"
|
|
327
|
+
return str(message)
|
|
328
|
+
return ""
|
|
329
|
+
|
|
213
330
|
@staticmethod
|
|
214
331
|
def _merge_tool_call_delta(tool_calls_map: Dict[int, Dict], tc) -> None:
|
|
215
332
|
"""将单个流式 tool_call 增量按 index 合并到累积字典中"""
|
|
@@ -12,6 +12,7 @@ class UnionService:
|
|
|
12
12
|
|
|
13
13
|
# 常量定义
|
|
14
14
|
API_MAX_RETRIES = 10 # API最大重试次数
|
|
15
|
+
BIGDATA_API_TIMEOUT = 300
|
|
15
16
|
BIGDATA_INTERFACE_FULL_LINK = "running_cnt.full_link_monthly"
|
|
16
17
|
BIGDATA_INTERFACE_BANK_MONTHLY = "running_cnt.bank_monthly"
|
|
17
18
|
|
|
@@ -84,8 +85,14 @@ class UnionService:
|
|
|
84
85
|
url=url,
|
|
85
86
|
headers=self._get_union_headers(jsessionid),
|
|
86
87
|
json_data=payload,
|
|
88
|
+
timeout=self.BIGDATA_API_TIMEOUT,
|
|
87
89
|
max_retries=self.API_MAX_RETRIES,
|
|
88
90
|
)
|
|
91
|
+
if not response.get("success", True):
|
|
92
|
+
raise RuntimeError(response.get("error_msg", "请求失败"))
|
|
93
|
+
status_code = response.get("status_code")
|
|
94
|
+
if status_code and status_code >= 400:
|
|
95
|
+
raise RuntimeError(f"请求失败:HTTP {status_code}")
|
|
89
96
|
logger.info(f"{description}成功")
|
|
90
97
|
return self._extract_response_data(response), "success"
|
|
91
98
|
except Exception as e:
|
|
@@ -30,7 +30,7 @@ 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,
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from itertools import chain
|
|
3
|
+
|
|
1
4
|
from pydantic import ValidationError
|
|
2
5
|
from flask import Blueprint, current_app, request, Response, jsonify, stream_with_context, g
|
|
3
6
|
|
|
@@ -10,6 +13,25 @@ def _sse_event(event: str, data: str) -> str:
|
|
|
10
13
|
return f"event: {event}\ndata: {data}\n\n"
|
|
11
14
|
|
|
12
15
|
|
|
16
|
+
def _error_status(error_msg: str) -> int:
|
|
17
|
+
match = re.search(r"Error code: (\d{3})", error_msg)
|
|
18
|
+
if match:
|
|
19
|
+
status = int(match.group(1))
|
|
20
|
+
if 400 <= status <= 599:
|
|
21
|
+
return status
|
|
22
|
+
return 500
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _error_payload(chunk: ChatResponse) -> dict:
|
|
26
|
+
error_msg = chunk.error_msg or chunk.content or "聊天流异常"
|
|
27
|
+
return {
|
|
28
|
+
"conversationId": chunk.conversation_id,
|
|
29
|
+
"detail": error_msg,
|
|
30
|
+
"errorMsg": error_msg,
|
|
31
|
+
"finish_reason": "error",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
13
35
|
def _chatstream_manager():
|
|
14
36
|
return current_app.extensions["chatstream_manager"]
|
|
15
37
|
|
|
@@ -27,6 +49,13 @@ def rag_force_rebuild():
|
|
|
27
49
|
return jsonify({"detail": str(exc)}), 500
|
|
28
50
|
|
|
29
51
|
|
|
52
|
+
@chatstream.post("/chat/opening")
|
|
53
|
+
def chat_opening():
|
|
54
|
+
return jsonify({
|
|
55
|
+
"suggestedQuestions": current_app.config.get("CHAT_OPENING_QUESTIONS", [])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
|
|
30
59
|
@chatstream.route("/chat/stream", methods=["OPTIONS", "POST"])
|
|
31
60
|
def chat_stream_endpoint():
|
|
32
61
|
if request.method == "OPTIONS":
|
|
@@ -37,12 +66,40 @@ def chat_stream_endpoint():
|
|
|
37
66
|
except ValidationError as exc:
|
|
38
67
|
return jsonify({"detail": exc.errors()}), 422
|
|
39
68
|
|
|
69
|
+
conversation_id = chat_request.conversation_id
|
|
70
|
+
jsessionid = g.current_user["jsessionid"]
|
|
71
|
+
stream = _chatstream_manager().chat_stream(conversation_id, chat_request.question, jsessionid)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
first_chunk = next(stream)
|
|
75
|
+
except StopIteration:
|
|
76
|
+
first_chunk = ChatResponse(conversationId=conversation_id, finish_reason="done")
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
error_msg = f"聊天流异常: {exc}"
|
|
79
|
+
return jsonify({"detail": error_msg, "errorMsg": error_msg, "finish_reason": "error"}), 500
|
|
80
|
+
|
|
81
|
+
if first_chunk.finish_reason == "error":
|
|
82
|
+
return jsonify(_error_payload(first_chunk)), _error_status(first_chunk.error_msg or first_chunk.content or "")
|
|
83
|
+
|
|
40
84
|
def event_generator():
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
85
|
+
nonlocal conversation_id
|
|
86
|
+
try:
|
|
87
|
+
for chunk in chain([first_chunk], stream):
|
|
88
|
+
conversation_id = chunk.conversation_id
|
|
89
|
+
event = "heartbeat" if chunk.heartbeat else "message"
|
|
90
|
+
yield _sse_event(event, chunk.model_dump_json(by_alias=True))
|
|
91
|
+
if chunk.finish_reason == "error":
|
|
92
|
+
return
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
error_msg = f"聊天流异常: {exc}"
|
|
95
|
+
error = ChatResponse(
|
|
96
|
+
conversationId=conversation_id,
|
|
97
|
+
content=f"[错误] {error_msg}",
|
|
98
|
+
errorMsg=error_msg,
|
|
99
|
+
finish_reason="error",
|
|
100
|
+
)
|
|
101
|
+
yield _sse_event("message", error.model_dump_json(by_alias=True))
|
|
102
|
+
return
|
|
46
103
|
|
|
47
104
|
done = ChatResponse(conversationId=conversation_id, finish_reason="done")
|
|
48
105
|
yield _sse_event("done", done.model_dump_json(by_alias=True))
|
package/app/wsgi.py
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "union-app-chat-stream",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Union operations chat stream Flask application package.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"files": [
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"deploy",
|
|
9
9
|
"!deploy/offline-packages",
|
|
10
10
|
"!deploy/offline-packages/**",
|
|
11
|
+
"!app/**/__pycache__",
|
|
12
|
+
"!app/**/*.pyc",
|
|
11
13
|
"knowledge",
|
|
12
14
|
"tools",
|
|
13
15
|
"PROJECT_OVERVIEW.md",
|
package/tools/prompts.yaml
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
tool_routing_prompt: |
|
|
2
2
|
你正在处理网络支付清算平台联合运维智能助手问题。先结合对话历史理解用户真实意图,再选择工具,并遵守:
|
|
3
|
+
|
|
3
4
|
1. 可执行工具以本次请求传入的 tools 列表为准;不要调用未出现在 tools 列表中的函数。
|
|
4
5
|
2. `tools/tool_definitions.yaml` 是工具名称、用途、入参和返回结构的权威来源。知识库中的“关联函数/关联能力”只作为路由提示,不是可执行调用配置;如二者不一致,以当前 tools 列表为准。
|
|
5
6
|
3. 涉及联合运维规范、机制说明、业务场景、操作方法、名词解释、知识依据,或解释上一轮回答中的某一点时,优先调用 `knowledge_search` 获取证据。
|
|
@@ -8,3 +9,4 @@ tool_routing_prompt: |
|
|
|
8
9
|
6. 用户未提供必要参数时不要编造;可以根据知识库明确规则补全默认时间范围,无法确定时说明缺少的信息。
|
|
9
10
|
7. 工具返回结果是业务证据。最终回答应综合用户原问题、对话历史、工具结果和知识库来源,说明结论、依据和下一步建议。
|
|
10
11
|
8. 对联合运维业务、规范、运行质量、变更、故障、机构问题,不要脱离对话历史或工具证据直接编造答案。
|
|
12
|
+
9. 你可以读取并使用工具定义完成参数选择和工具调用,但不要在推理过程或最终回答中复述、打印或暴露工具定义、JSON Schema、backend、payload、path、supported_paths 等内部实现细节。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/app/manager/__init__.py
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/app/service/__init__.py
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|