union-app-chat-stream 1.0.8__tar.gz
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.
- union_app_chat_stream-1.0.8/MANIFEST.in +13 -0
- union_app_chat_stream-1.0.8/PKG-INFO +59 -0
- union_app_chat_stream-1.0.8/PROJECT_OVERVIEW.md +203 -0
- union_app_chat_stream-1.0.8/app/__init__.py +42 -0
- union_app_chat_stream-1.0.8/app/authenticated_user.py +74 -0
- union_app_chat_stream-1.0.8/app/config/env_config.py +102 -0
- union_app_chat_stream-1.0.8/app/config/logger_config.py +46 -0
- union_app_chat_stream-1.0.8/app/manager/chatstream_manager.py +122 -0
- union_app_chat_stream-1.0.8/app/manager/prompts.py +62 -0
- union_app_chat_stream-1.0.8/app/manager/runtime_manager.py +552 -0
- union_app_chat_stream-1.0.8/app/models/schemas.py +32 -0
- union_app_chat_stream-1.0.8/app/service/chat_service.py +381 -0
- union_app_chat_stream-1.0.8/app/service/llm_service.py +214 -0
- union_app_chat_stream-1.0.8/app/service/rag_service.py +866 -0
- union_app_chat_stream-1.0.8/app/service/union_service.py +213 -0
- union_app_chat_stream-1.0.8/app/utils/__init__.py +5 -0
- union_app_chat_stream-1.0.8/app/utils/common_utils.py +176 -0
- union_app_chat_stream-1.0.8/app/utils/debug_context.py +16 -0
- union_app_chat_stream-1.0.8/app/utils/function_utils.py +371 -0
- union_app_chat_stream-1.0.8/app/utils/jwt_utils.py +39 -0
- union_app_chat_stream-1.0.8/app/views/__init__.py +6 -0
- union_app_chat_stream-1.0.8/app/views/view_chatstream.py +110 -0
- union_app_chat_stream-1.0.8/app/views/view_healthcheck.py +14 -0
- union_app_chat_stream-1.0.8/app/views/view_runtime.py +72 -0
- union_app_chat_stream-1.0.8/app/wsgi.py +37 -0
- union_app_chat_stream-1.0.8/ci.yml +14 -0
- union_app_chat_stream-1.0.8/deploy/autoconf/templates/env.j2 +25 -0
- union_app_chat_stream-1.0.8/deploy/autoconf.yml +15 -0
- union_app_chat_stream-1.0.8/deploy/scripts/healthcheck.sh +0 -0
- union_app_chat_stream-1.0.8/deploy/scripts/requirements.txt +53 -0
- union_app_chat_stream-1.0.8/deploy/scripts/start.sh +75 -0
- union_app_chat_stream-1.0.8/deploy/scripts/stop.sh +31 -0
- union_app_chat_stream-1.0.8/knowledge/000001-biz-offline-85b99bd43b-v1.md +88 -0
- union_app_chat_stream-1.0.8/knowledge/000002-biz-offline-717e8d823e-v1.md +90 -0
- union_app_chat_stream-1.0.8/knowledge/000003-biz-offline-c963227cc8-v1.md +84 -0
- union_app_chat_stream-1.0.8/knowledge/000004-biz-offline-2a5868e7da-v1.md +92 -0
- union_app_chat_stream-1.0.8/knowledge/000005-biz-offline-f9d9cf1a88-v1.md +79 -0
- union_app_chat_stream-1.0.8/knowledge/000006-biz-offline-c4fa2df3bd-v1.md +77 -0
- union_app_chat_stream-1.0.8/knowledge/000007-biz-offline-78304b70ca-v1.md +76 -0
- union_app_chat_stream-1.0.8/knowledge/000008-biz-offline-987ae67b35-v1.md +75 -0
- union_app_chat_stream-1.0.8/knowledge/000009-biz-offline-4d656bcea3-v1.md +85 -0
- union_app_chat_stream-1.0.8/knowledge/000010-sop-offline-a9e1050719-v1.md +100 -0
- union_app_chat_stream-1.0.8/knowledge/000011-biz-offline-5de0624891-v1.md +86 -0
- union_app_chat_stream-1.0.8/knowledge/000012-biz-offline-7dfacccba3-v1.md +82 -0
- union_app_chat_stream-1.0.8/knowledge/000013-biz-offline-5e1d29d2ed-v1.md +81 -0
- union_app_chat_stream-1.0.8/knowledge/000014-biz-offline-1d0ed8b841-v1.md +68 -0
- union_app_chat_stream-1.0.8/knowledge/000015-biz-offline-8a1376ee3e-v1.md +78 -0
- union_app_chat_stream-1.0.8/knowledge/000016-biz-offline-c8bfc2aa08-v1.md +99 -0
- union_app_chat_stream-1.0.8/knowledge/000017-biz-offline-9dffb28032-v1.md +88 -0
- union_app_chat_stream-1.0.8/knowledge/000018-biz-offline-f935bc9a6a-v1.md +80 -0
- union_app_chat_stream-1.0.8/knowledge/000019-biz-offline-858b3ecd89-v1.md +86 -0
- union_app_chat_stream-1.0.8/knowledge/000020-biz-offline-65cb5c4f40-v1.md +113 -0
- union_app_chat_stream-1.0.8/knowledge/000021-biz-offline-1bf211639c-v1.md +148 -0
- union_app_chat_stream-1.0.8/knowledge/000022-biz-offline-8c5a637879-v1.md +140 -0
- union_app_chat_stream-1.0.8/knowledge/000023-biz-offline-fe872b8712-v1.md +188 -0
- union_app_chat_stream-1.0.8/knowledge/000024-biz-offline-a85010c500-v1.md +133 -0
- union_app_chat_stream-1.0.8/knowledge/000025-biz-offline-8af58a3638-v1.md +136 -0
- union_app_chat_stream-1.0.8/knowledge/000026-biz-offline-6754102e93-v1.md +142 -0
- union_app_chat_stream-1.0.8/knowledge/000027-biz-offline-ea2e5ca5f9-v1.md +150 -0
- union_app_chat_stream-1.0.8/knowledge/000028-scenario-offline-dab45cebb4-v1.md +136 -0
- union_app_chat_stream-1.0.8/knowledge/000029-scenario-offline-5b8ae5ea9f-v1.md +143 -0
- union_app_chat_stream-1.0.8/knowledge/000030-scenario-offline-9a82d42f3f-v1.md +136 -0
- union_app_chat_stream-1.0.8/knowledge/000031-scenario-offline-cc2edc0197-v1.md +122 -0
- union_app_chat_stream-1.0.8/knowledge/000032-scenario-offline-e5f6e5cbfa-v1.md +122 -0
- union_app_chat_stream-1.0.8/knowledge/000033-scenario-offline-e1955849aa-v1.md +135 -0
- union_app_chat_stream-1.0.8/knowledge/000034-scenario-offline-3a13d49a3a-v1.md +138 -0
- union_app_chat_stream-1.0.8/knowledge/000035-scenario-offline-fd5560211f-v1.md +147 -0
- union_app_chat_stream-1.0.8/knowledge/000036-scenario-offline-function-call-mock-v1.md +134 -0
- union_app_chat_stream-1.0.8/package.json +20 -0
- union_app_chat_stream-1.0.8/pyproject.toml +17 -0
- union_app_chat_stream-1.0.8/requirements.txt +53 -0
- union_app_chat_stream-1.0.8/setup.cfg +4 -0
- union_app_chat_stream-1.0.8/tests/test_app_factory.py +803 -0
- union_app_chat_stream-1.0.8/tools/field_dictionary.yaml +28 -0
- union_app_chat_stream-1.0.8/tools/prompts.yaml +12 -0
- union_app_chat_stream-1.0.8/tools/tool_definitions.yaml +377 -0
- union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/PKG-INFO +59 -0
- union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/SOURCES.txt +79 -0
- union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/dependency_links.txt +1 -0
- union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/requires.txt +53 -0
- union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
include PROJECT_OVERVIEW.md
|
|
2
|
+
include requirements.txt
|
|
3
|
+
include ci.yml
|
|
4
|
+
include package.json
|
|
5
|
+
recursive-include app *.py
|
|
6
|
+
recursive-include deploy *.yml *.yaml *.txt *.sh *.j2
|
|
7
|
+
recursive-include tools *.yml *.yaml
|
|
8
|
+
recursive-include knowledge *.md
|
|
9
|
+
recursive-exclude deploy/offline-packages *
|
|
10
|
+
recursive-exclude app .env*
|
|
11
|
+
global-exclude *.py[cod]
|
|
12
|
+
global-exclude __pycache__
|
|
13
|
+
global-exclude .DS_Store
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: union-app-chat-stream
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: Union operations chat stream Flask application package.
|
|
5
|
+
License-Expression: LicenseRef-Proprietary
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Requires-Dist: annotated-types==0.7.0
|
|
8
|
+
Requires-Dist: anyio==4.6.2
|
|
9
|
+
Requires-Dist: blinker==1.8.2
|
|
10
|
+
Requires-Dist: certifi==2025.8.3
|
|
11
|
+
Requires-Dist: charset-normalizer==3.4.4
|
|
12
|
+
Requires-Dist: click==8.1.8
|
|
13
|
+
Requires-Dist: concurrent-log-handler==0.9.28
|
|
14
|
+
Requires-Dist: curlify==3.0.0
|
|
15
|
+
Requires-Dist: distro==1.9.0
|
|
16
|
+
Requires-Dist: dnspython==2.6.1
|
|
17
|
+
Requires-Dist: exceptiongroup==1.3.0
|
|
18
|
+
Requires-Dist: Flask==3.0.3
|
|
19
|
+
Requires-Dist: Flask-Cors==5.0.0
|
|
20
|
+
Requires-Dist: h11==0.16.0
|
|
21
|
+
Requires-Dist: h2==4.1.0
|
|
22
|
+
Requires-Dist: hpack==4.0.0
|
|
23
|
+
Requires-Dist: httpcore==1.0.9
|
|
24
|
+
Requires-Dist: httpx==0.28.1
|
|
25
|
+
Requires-Dist: Hypercorn==0.17.3
|
|
26
|
+
Requires-Dist: hyperframe==6.0.1
|
|
27
|
+
Requires-Dist: idna==3.10
|
|
28
|
+
Requires-Dist: itsdangerous==2.2.0
|
|
29
|
+
Requires-Dist: Jinja2==3.1.6
|
|
30
|
+
Requires-Dist: jiter==0.9.1
|
|
31
|
+
Requires-Dist: loguru==0.7.3
|
|
32
|
+
Requires-Dist: lxml==6.0.1
|
|
33
|
+
Requires-Dist: MarkupSafe==2.1.5
|
|
34
|
+
Requires-Dist: numpy<2.5.0,>=2.0.0
|
|
35
|
+
Requires-Dist: opencv-python>=4.13.0.90
|
|
36
|
+
Requires-Dist: openai==1.107.3
|
|
37
|
+
Requires-Dist: packaging==25.0
|
|
38
|
+
Requires-Dist: portalocker==3.0.0
|
|
39
|
+
Requires-Dist: priority==2.0.0
|
|
40
|
+
Requires-Dist: PyJWT==2.9.0
|
|
41
|
+
Requires-Dist: python-dateutil==2.9.0.post0
|
|
42
|
+
Requires-Dist: python-dotenv==1.0.1
|
|
43
|
+
Requires-Dist: pytz==2025.2
|
|
44
|
+
Requires-Dist: requests==2.32.5
|
|
45
|
+
Requires-Dist: six==1.17.0
|
|
46
|
+
Requires-Dist: sniffio==1.3.1
|
|
47
|
+
Requires-Dist: tqdm==4.67.3
|
|
48
|
+
Requires-Dist: typing_extensions==4.15.0
|
|
49
|
+
Requires-Dist: tzdata==2025.2
|
|
50
|
+
Requires-Dist: urllib3==1.26.20
|
|
51
|
+
Requires-Dist: Werkzeug==3.0.6
|
|
52
|
+
Requires-Dist: wsproto==1.2.0
|
|
53
|
+
Requires-Dist: zipp==3.20.2
|
|
54
|
+
Requires-Dist: pydantic==2.13.4
|
|
55
|
+
Requires-Dist: pyyaml==6.0.3
|
|
56
|
+
Requires-Dist: zai-sdk==0.2.2
|
|
57
|
+
Requires-Dist: chromadb==1.5.9
|
|
58
|
+
Requires-Dist: pysqlite3-binary==0.5.4
|
|
59
|
+
Requires-Dist: typer==0.15.1
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Project Overview
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
This repository contains a Python 3.12 Flask application for Union operations
|
|
6
|
+
chat and RAG-assisted troubleshooting. It exposes streaming chat APIs, runtime
|
|
7
|
+
management APIs, and a health check endpoint. The app is intended to run behind
|
|
8
|
+
Hypercorn in Linux x86_64 deployment environments.
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
- Language: Python 3.12
|
|
13
|
+
- Web framework: Flask running through Hypercorn ASGI
|
|
14
|
+
- LLM SDK: zai-sdk
|
|
15
|
+
- Vector database: ChromaDB
|
|
16
|
+
- Validation: Pydantic v2
|
|
17
|
+
- Logging: Loguru
|
|
18
|
+
- Runtime target: Linux x86_64
|
|
19
|
+
|
|
20
|
+
## Repository Layout
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
union-py-app/
|
|
24
|
+
├── app/ # Flask application package
|
|
25
|
+
│ ├── __init__.py # create_app() factory and blueprint setup
|
|
26
|
+
│ ├── wsgi.py # Application entrypoint
|
|
27
|
+
│ ├── authenticated_user.py # Session authentication middleware
|
|
28
|
+
│ ├── config/ # Configuration and logging
|
|
29
|
+
│ ├── manager/ # Business orchestration layer
|
|
30
|
+
│ ├── models/ # Pydantic schemas
|
|
31
|
+
│ ├── service/ # Core services: chat, RAG, LLM, union logic
|
|
32
|
+
│ ├── utils/ # Shared utility functions
|
|
33
|
+
│ └── views/ # Flask blueprints and HTTP routes
|
|
34
|
+
├── knowledge/ # RAG knowledge source files
|
|
35
|
+
├── tools/ # Tool definitions for model tool calling
|
|
36
|
+
├── scripts/ # Environment-specific launcher scripts
|
|
37
|
+
├── deploy/ # Deployment config, templates, and scripts
|
|
38
|
+
├── requirements.txt # Python dependencies
|
|
39
|
+
├── ci.yml # CI configuration
|
|
40
|
+
└── .gitignore
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Project-specific context belongs in this file. Do not add a repository-root
|
|
44
|
+
`AGENTS.md`; global working rules are provided outside the repo.
|
|
45
|
+
|
|
46
|
+
## Important Modules
|
|
47
|
+
|
|
48
|
+
- `app/__init__.py`: Builds the Flask app, loads configuration, initializes
|
|
49
|
+
managers in `app.extensions`, and registers blueprints.
|
|
50
|
+
- `app/wsgi.py`: Imports `create_app()` and exposes `app` for Hypercorn.
|
|
51
|
+
- `app/authenticated_user.py`: Validates login identity before requests.
|
|
52
|
+
- `app/config/env_config.py`: Defines Flask configuration values.
|
|
53
|
+
- `app/config/logger_config.py`: Initializes application logging.
|
|
54
|
+
- `app/manager/chatstream_manager.py`: Orchestrates chatstream routes, including
|
|
55
|
+
chat streaming and RAG status/rebuild operations.
|
|
56
|
+
- `app/manager/runtime_manager.py`: Handles runtime management behavior.
|
|
57
|
+
- `app/manager/prompts.py`: Stores prompt templates.
|
|
58
|
+
- `app/models/schemas.py`: Defines request and response schemas.
|
|
59
|
+
- `app/service/chat_service.py`: Implements streaming chat and tool-call loops.
|
|
60
|
+
- `app/service/rag_service.py`: Manages RAG indexing and retrieval.
|
|
61
|
+
- `app/service/llm_service.py`: Wraps LLM provider behavior.
|
|
62
|
+
- `app/service/union_service.py`: Contains Union operations business services.
|
|
63
|
+
- `app/utils/function_utils.py`: Loads YAML tool definitions and dispatches
|
|
64
|
+
tool calls.
|
|
65
|
+
- `app/views/view_chatstream.py`: Chatstream and RAG HTTP endpoints.
|
|
66
|
+
- `app/views/view_runtime.py`: Runtime HTTP endpoints.
|
|
67
|
+
- `app/views/view_healthcheck.py`: Health check HTTP endpoint.
|
|
68
|
+
|
|
69
|
+
## API Surface
|
|
70
|
+
|
|
71
|
+
| Method | Path | Blueprint | Purpose |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| POST | `/chatstream/v1/chat/stream` | chatstream | SSE streaming chat with RAG and tool calls |
|
|
74
|
+
| GET | `/chatstream/v1/rag/check` | chatstream | RAG knowledge and vector database status |
|
|
75
|
+
| POST | `/chatstream/v1/rag/force-rebuild` | chatstream | Force rebuild of the RAG knowledge base |
|
|
76
|
+
| GET | `/healthcheck.html` | healthcheck | Service health check |
|
|
77
|
+
|
|
78
|
+
## Environment Loading
|
|
79
|
+
|
|
80
|
+
Environment-specific launch scripts under `scripts/` contain the target
|
|
81
|
+
environment name on the first line:
|
|
82
|
+
|
|
83
|
+
- `scripts/start-BJ11.sh`: `prod.bj11`
|
|
84
|
+
- `scripts/start-BJ12.sh`: `test.bj12`
|
|
85
|
+
- `scripts/start-SH20.sh`: `prod.sh20`
|
|
86
|
+
- `scripts/start-SZ31.sh`: `prod.sz31`
|
|
87
|
+
|
|
88
|
+
The deploy launcher at `deploy/scripts/start.sh` reads that environment name,
|
|
89
|
+
exports `FLASK_ENV`, and starts the app through Hypercorn.
|
|
90
|
+
|
|
91
|
+
Environment files live under `app/` and are intentionally ignored by Git:
|
|
92
|
+
|
|
93
|
+
- `app/.env`
|
|
94
|
+
- `app/.env.dev`
|
|
95
|
+
- `app/.env.prod.bj11`
|
|
96
|
+
- `app/.env.prod.sh20`
|
|
97
|
+
- `app/.env.prod.sz31`
|
|
98
|
+
- `app/.env.test.bj12`
|
|
99
|
+
|
|
100
|
+
Treat all environment files, credentials, tokens, and production configuration
|
|
101
|
+
as sensitive. Do not commit them.
|
|
102
|
+
|
|
103
|
+
## Deployment
|
|
104
|
+
|
|
105
|
+
Deployment files are organized under `deploy/`:
|
|
106
|
+
|
|
107
|
+
- `deploy/autoconf.yml`: Autoconf mapping for deployment environments.
|
|
108
|
+
- `deploy/autoconf/templates/env.j2`: Template used to render environment
|
|
109
|
+
variables.
|
|
110
|
+
- `deploy/scripts/start.sh`: Creates or uses a virtual environment, installs
|
|
111
|
+
dependencies, and starts Hypercorn.
|
|
112
|
+
- `deploy/scripts/stop.sh`: Stops the Hypercorn process.
|
|
113
|
+
- `deploy/scripts/healthcheck.sh`: Deployment health check entrypoint.
|
|
114
|
+
- `deploy/scripts/requirements.txt`: Deployment dependency file. It should stay
|
|
115
|
+
in sync with root `requirements.txt`.
|
|
116
|
+
- `deploy/offline-packages/`: Expected location for offline wheel packages when
|
|
117
|
+
building a fully offline deployment bundle.
|
|
118
|
+
|
|
119
|
+
## Development Notes
|
|
120
|
+
|
|
121
|
+
- Read this file before changing project code.
|
|
122
|
+
- Keep changes scoped to the requested behavior.
|
|
123
|
+
- Prefer existing package boundaries: views call managers or services; services
|
|
124
|
+
contain core business logic; utils stay generic.
|
|
125
|
+
- Keep RAG knowledge content under `knowledge/`. The directory is tracked using
|
|
126
|
+
`.gitkeep`; knowledge documents are managed separately and ignored by default.
|
|
127
|
+
- Keep tool-call schema changes in `tools/tool_definitions.yaml` aligned with
|
|
128
|
+
`app/utils/function_utils.py` and the services that implement tools.
|
|
129
|
+
- Avoid committing generated caches, virtual environments, local environment
|
|
130
|
+
files, or machine-specific artifacts.
|
|
131
|
+
- Before publishing this project to npm, bump the package version forward and
|
|
132
|
+
verify the packed artifact includes the full `app/`, `tools/`, and
|
|
133
|
+
`knowledge/` directories plus `deploy/` excluding `deploy/offline-packages/`;
|
|
134
|
+
keep the existing `.gitignore` rule that ignores local `knowledge/` files.
|
|
135
|
+
- npm publishes must include `app/.env*` environment files, but only after
|
|
136
|
+
temporarily masking every sensitive value in those files, including keys,
|
|
137
|
+
URLs, tokens, secrets, cookies, passwords, and values that expose business
|
|
138
|
+
terms such as `网联`, `网联清算`, `nucc`, `uops`, or `联合运维`. After the npm
|
|
139
|
+
publish finishes, restore the local `.env*` files back to their original
|
|
140
|
+
plaintext values and remove the temporary masking changes.
|
|
141
|
+
- Keep `app/.env*` files human-editable: preserve their key order, comments,
|
|
142
|
+
and blank-line grouping when normalizing, masking, or restoring them.
|
|
143
|
+
|
|
144
|
+
## GLM Model Capability Reference
|
|
145
|
+
|
|
146
|
+
- Use `/Users/simon/Documents/glm-model-doc.md` as the local long-term reference
|
|
147
|
+
for GLM model capabilities. When a change or design decision depends on GLM
|
|
148
|
+
behavior such as Thinking, Interleaved Thinking, Preserved Thinking,
|
|
149
|
+
turn-level thinking, streaming, function calling, tool streaming, or agent
|
|
150
|
+
loops, read that document before assuming API behavior.
|
|
151
|
+
- For complex chat, RAG, or tool-call work, prefer designs that match GLM's
|
|
152
|
+
native tool-calling and thinking model instead of adding ad hoc orchestration
|
|
153
|
+
first. In particular, RAG can be exposed as a normal tool when the model needs
|
|
154
|
+
to decide search intent from conversation history.
|
|
155
|
+
- When using GLM Thinking with tools, preserve the model's `reasoning_content`
|
|
156
|
+
according to the document's requirements. Interleaved thinking allows the
|
|
157
|
+
model to continue reasoning between tool calls and after tool results; for
|
|
158
|
+
preserved thinking, pass complete, unmodified reasoning content back in later
|
|
159
|
+
messages and use `clear_thinking: false` where applicable.
|
|
160
|
+
- Do not rely on prompt text alone for critical tool hierarchy or evidence
|
|
161
|
+
policy. Keep executable tool definitions in `tools/tool_definitions.yaml`, and
|
|
162
|
+
treat knowledge-base "related functions" as routing hints unless code
|
|
163
|
+
explicitly promotes them to executable tools.
|
|
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
|
+
|
|
181
|
+
## Validation
|
|
182
|
+
|
|
183
|
+
Use the smallest reliable checks that cover the change:
|
|
184
|
+
|
|
185
|
+
- Syntax check: `python3 -m compileall app`
|
|
186
|
+
- RAG smoke check when touching RAG code:
|
|
187
|
+
`python3 -c "from app import create_app; app = create_app(); print(app.extensions['chatstream_manager'].check_rag())"`
|
|
188
|
+
- Endpoint smoke checks should cover `/healthcheck.html` and the relevant
|
|
189
|
+
`/chatstream/v1/*` route when route behavior changes.
|
|
190
|
+
- When dependency files change, verify root `requirements.txt` and
|
|
191
|
+
`deploy/scripts/requirements.txt` stay aligned.
|
|
192
|
+
|
|
193
|
+
## Known Structure Gaps To Confirm
|
|
194
|
+
|
|
195
|
+
These are structure differences observed during the initial repository check:
|
|
196
|
+
|
|
197
|
+
- `deploy/offline-packages/` is expected by the original structure but is not
|
|
198
|
+
currently present.
|
|
199
|
+
- Shell scripts are present but may need executable permissions for deployment.
|
|
200
|
+
- `tools/tool_definitions.yaml` currently exists but may need project-specific
|
|
201
|
+
tool definitions populated.
|
|
202
|
+
- `scripts/healthcheck.sh` and `deploy/scripts/healthcheck.sh` currently exist
|
|
203
|
+
but may need health check logic populated.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from flask import Flask
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _load_dotenv_for_env():
|
|
8
|
+
config_name = os.getenv("FLASK_ENV", "dev")
|
|
9
|
+
dotenv_file = os.path.join(os.path.dirname(__file__), f".env.{config_name}")
|
|
10
|
+
load_dotenv(dotenv_file, override=True, verbose=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def create_app():
|
|
14
|
+
_load_dotenv_for_env()
|
|
15
|
+
|
|
16
|
+
app = Flask(__name__)
|
|
17
|
+
from app.config.env_config import Config
|
|
18
|
+
app.config.from_object(Config)
|
|
19
|
+
|
|
20
|
+
from .authenticated_user import FlaskSessionClient
|
|
21
|
+
FlaskSessionClient(app)
|
|
22
|
+
|
|
23
|
+
from app.manager.chatstream_manager import ChatstreamManager
|
|
24
|
+
from app.manager.runtime_manager import RuntimeManager
|
|
25
|
+
from app.service.chat_service import ChatService
|
|
26
|
+
from app.service.llm_service import llm_service
|
|
27
|
+
from app.service.rag_service import RagService
|
|
28
|
+
from app.service.union_service import union_service
|
|
29
|
+
|
|
30
|
+
llm_service.initialize(app.config)
|
|
31
|
+
union_service.initialize(app.config)
|
|
32
|
+
rag_service = RagService(app.config)
|
|
33
|
+
chat_service = ChatService(app.config, rag_service, union_service)
|
|
34
|
+
|
|
35
|
+
app.extensions["chatstream_manager"] = ChatstreamManager(app.config, chat_service, rag_service)
|
|
36
|
+
app.extensions["runtime_manager"] = RuntimeManager(union_service, llm_service)
|
|
37
|
+
|
|
38
|
+
from .views import blueprints
|
|
39
|
+
|
|
40
|
+
for bp in blueprints:
|
|
41
|
+
app.register_blueprint(bp)
|
|
42
|
+
return app
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from flask import Flask, request, jsonify, g, has_request_context
|
|
2
|
+
from werkzeug.exceptions import HTTPException
|
|
3
|
+
from loguru import logger
|
|
4
|
+
from app.utils import common_utils
|
|
5
|
+
from app.config.logger_config import setup_logger
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
|
|
8
|
+
_current_ip: ContextVar[str] = ContextVar("current_ip", default="unknown_ip")
|
|
9
|
+
|
|
10
|
+
logger = setup_logger()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FlaskSessionClient:
|
|
14
|
+
def __init__(self, app):
|
|
15
|
+
self.auth_user_server = app.config['GET_USE_INFO_URL']
|
|
16
|
+
self.permissions = app.config['PERMISSIONS']
|
|
17
|
+
self.init_app(app)
|
|
18
|
+
|
|
19
|
+
def init_app(self, app: Flask):
|
|
20
|
+
def patcher(record):
|
|
21
|
+
record["extra"]["ip"] = _current_ip.get()
|
|
22
|
+
|
|
23
|
+
logger.configure(patcher=patcher)
|
|
24
|
+
|
|
25
|
+
@app.before_request
|
|
26
|
+
def require_auth():
|
|
27
|
+
try:
|
|
28
|
+
if has_request_context():
|
|
29
|
+
ip = common_utils.get_client_ip()
|
|
30
|
+
except LookupError:
|
|
31
|
+
ip = "unknown_context-lost"
|
|
32
|
+
_current_ip.set(ip)
|
|
33
|
+
|
|
34
|
+
if request.method == 'OPTIONS':
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
public_paths = [
|
|
38
|
+
'/healthcheck.html',
|
|
39
|
+
'/favicon.ico'
|
|
40
|
+
]
|
|
41
|
+
if any(request.path == path or request.path.startswith(path) for path in public_paths):
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
jsessionid = request.headers.get('Cookie')
|
|
45
|
+
|
|
46
|
+
if not jsessionid:
|
|
47
|
+
logger.error("未登录")
|
|
48
|
+
return jsonify({"error": "auth error"}), 401
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
response = common_utils.call_https_api(url=self.auth_user_server,
|
|
52
|
+
headers={'Cookie': 'CASSESSIONID=' + jsessionid},
|
|
53
|
+
method='GET', verify_ssl=False)
|
|
54
|
+
if response.get('status_code') != 200 or not response.get('data'):
|
|
55
|
+
logger.error("验证失败")
|
|
56
|
+
return jsonify({"error": "auth error"}), 401
|
|
57
|
+
user = response.get('data')
|
|
58
|
+
if user['loginName'] and self.permissions and self.permissions not in user['permissions']:
|
|
59
|
+
return jsonify({"error": "auth error"}), 401
|
|
60
|
+
g.current_user = response.get('data')
|
|
61
|
+
g.current_user['jsessionid'] = jsessionid
|
|
62
|
+
|
|
63
|
+
return
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.error(f"auth 验证失败: {str(e)},路径: {request.path}")
|
|
66
|
+
return jsonify({"error": "auth error"}), 401
|
|
67
|
+
|
|
68
|
+
@app.errorhandler(Exception)
|
|
69
|
+
def handle_exception(e):
|
|
70
|
+
if isinstance(e, HTTPException):
|
|
71
|
+
return e
|
|
72
|
+
|
|
73
|
+
logger.error(f"未处理的异常: {str(e)}")
|
|
74
|
+
return jsonify({"error": "Internal Server Error"}), 500
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _env_bool(name, default=False):
|
|
5
|
+
value = os.getenv(name)
|
|
6
|
+
if value is None:
|
|
7
|
+
return default
|
|
8
|
+
return value.lower() in {"1", "true", "yes", "on"}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _env_int(name, default):
|
|
12
|
+
value = os.getenv(name)
|
|
13
|
+
if value is None:
|
|
14
|
+
return default
|
|
15
|
+
try:
|
|
16
|
+
return int(value)
|
|
17
|
+
except ValueError:
|
|
18
|
+
return default
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _env_float(name, default):
|
|
22
|
+
value = os.getenv(name)
|
|
23
|
+
if value is None:
|
|
24
|
+
return default
|
|
25
|
+
try:
|
|
26
|
+
return float(value)
|
|
27
|
+
except ValueError:
|
|
28
|
+
return default
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _env_list(name, default=None):
|
|
32
|
+
value = os.getenv(name)
|
|
33
|
+
if value is None:
|
|
34
|
+
return default or []
|
|
35
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Config:
|
|
39
|
+
SECRET_KEY = os.getenv('SECRET_KEY')
|
|
40
|
+
GET_USE_INFO_URL = os.getenv('GET_USE_INFO_URL')
|
|
41
|
+
GET_ORG_INFO_URL = os.getenv('GET_ORG_INFO_URL')
|
|
42
|
+
GET_JIRA_INFO_URL = os.getenv('GET_JIRA_INFO_URL')
|
|
43
|
+
GET_BIGDATA_URL = os.getenv('GET_BIGDATA_URL')
|
|
44
|
+
GET_UNION_BASE_URL = os.getenv('GET_UNION_BASE_URL')
|
|
45
|
+
GET_ORG_INFO_URL_TOKEN = os.getenv('GET_ORG_INFO_URL_TOKEN')
|
|
46
|
+
GET_JIRA_INFO_URL_TOKEN = os.getenv('GET_JIRA_INFO_URL_TOKEN')
|
|
47
|
+
PERMISSIONS = os.getenv('PERMISSIONS')
|
|
48
|
+
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
|
49
|
+
CONSOLE_STDOUT = os.getenv("CONSOLE_STDOUT", "FALSE")
|
|
50
|
+
LOG_DIR = os.getenv("LOG_DIR", "/data/appLogs")
|
|
51
|
+
FLASK_ENV = os.getenv("FLASK_ENV", "prod")
|
|
52
|
+
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", f"union-fall-back-secret-for-{FLASK_ENV}")
|
|
53
|
+
JWT_EXPIRATION_SECOND = os.getenv("JWT_EXPIRATION_SECOND", 900)
|
|
54
|
+
JWT_RENEW_SECOND = os.getenv("JWT_RENEW_SECOND", 120)
|
|
55
|
+
|
|
56
|
+
# 大模型地址
|
|
57
|
+
LLM_URL = os.getenv('LLM_URL')
|
|
58
|
+
LLM_KEY = os.getenv('LLM_KEY')
|
|
59
|
+
LLM_MODEL = os.getenv('LLM_MODEL') # 默认模型名称
|
|
60
|
+
LLM_MAX_TOKENS = _env_int("LLM_MAX_TOKENS", 4096)
|
|
61
|
+
LLM_TEMPERATURE = _env_float("LLM_TEMPERATURE", 0.7)
|
|
62
|
+
LLM_TOP_P = _env_float("LLM_TOP_P", 0.9)
|
|
63
|
+
|
|
64
|
+
SYSTEM_PROMPT = os.getenv("SYSTEM_PROMPT", "")
|
|
65
|
+
|
|
66
|
+
FILTER_ENABLED = _env_bool("FILTER_ENABLED", False)
|
|
67
|
+
FILTER_ALLOWED_KEYWORDS = _env_list("FILTER_ALLOWED_KEYWORDS")
|
|
68
|
+
FILTER_REJECTION_MESSAGE = os.getenv(
|
|
69
|
+
"FILTER_REJECTION_MESSAGE",
|
|
70
|
+
"抱歉,我是联合运维智能客服,只能回答与联合运维相关的问题。",
|
|
71
|
+
)
|
|
72
|
+
|
|
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
|
+
])
|
|
80
|
+
|
|
81
|
+
CONVERSATION_MAX_HISTORY = _env_int("CONVERSATION_MAX_HISTORY", 20)
|
|
82
|
+
CONVERSATION_TTL = _env_int("CONVERSATION_TTL", 3600)
|
|
83
|
+
|
|
84
|
+
RAG_ENABLED = _env_bool("RAG_ENABLED", True)
|
|
85
|
+
RAG_KNOWLEDGE_DIR = os.getenv("RAG_KNOWLEDGE_DIR", "knowledge")
|
|
86
|
+
RAG_PERSIST_DIR = os.getenv("RAG_PERSIST_DIR", ".chroma")
|
|
87
|
+
RAG_COLLECTION = os.getenv("RAG_COLLECTION", "ops_knowledge")
|
|
88
|
+
RAG_EMBEDDING_MODEL = os.getenv("RAG_EMBEDDING_MODEL", "embedding-3")
|
|
89
|
+
RAG_EMBEDDING_MAX_CHARS = _env_int("RAG_EMBEDDING_MAX_CHARS", 6000)
|
|
90
|
+
RAG_EMBEDDING_BATCH_SIZE = _env_int("RAG_EMBEDDING_BATCH_SIZE", 8)
|
|
91
|
+
RAG_TOP_K = _env_int("RAG_TOP_K", 5)
|
|
92
|
+
RAG_SEMANTIC_CANDIDATE_K = _env_int("RAG_SEMANTIC_CANDIDATE_K", 40)
|
|
93
|
+
RAG_CONTEXT_K = _env_int("RAG_CONTEXT_K", 8)
|
|
94
|
+
RAG_EXACT_CONTEXT_K = _env_int("RAG_EXACT_CONTEXT_K", 6)
|
|
95
|
+
RAG_EXACT_PER_FILE_CONTEXT_K = _env_int("RAG_EXACT_PER_FILE_CONTEXT_K", 1)
|
|
96
|
+
RAG_PER_FILE_CONTEXT_K = _env_int("RAG_PER_FILE_CONTEXT_K", 2)
|
|
97
|
+
RAG_CHUNK_SIZE = _env_int("RAG_CHUNK_SIZE", 1200)
|
|
98
|
+
RAG_REBUILD_ON_STARTUP = _env_bool("RAG_REBUILD_ON_STARTUP", False)
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def init_app(cls, app):
|
|
102
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def setup_logger():
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
env = os.getenv("FLASK_ENV", "prod")
|
|
9
|
+
log_level = os.getenv("LOG_LEVEL", "INFO")
|
|
10
|
+
console_stdout = os.getenv("CONSOLE_STDOUT", "FALSE")
|
|
11
|
+
log_dir = os.getenv("LOG_DIR", "/data/appLogs")
|
|
12
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
13
|
+
app_log_path = os.path.join(log_dir, "union-py-app.log")
|
|
14
|
+
error_log_path = os.path.join(log_dir, "union-py-error.log")
|
|
15
|
+
logger.remove()
|
|
16
|
+
|
|
17
|
+
logger.configure(extra={"ip": "unknown_ip"})
|
|
18
|
+
log_format = "{time:YYYY-MM-DD HH:mm:ss}|{level:8}|{extra[ip]}|{name}:{function}:{line}|{message}"
|
|
19
|
+
|
|
20
|
+
logger.add(
|
|
21
|
+
app_log_path,
|
|
22
|
+
rotation="500 MB",
|
|
23
|
+
retention="30 days",
|
|
24
|
+
compression="gz",
|
|
25
|
+
level=log_level,
|
|
26
|
+
format=log_format,
|
|
27
|
+
enqueue=True,
|
|
28
|
+
encoding="utf=8"
|
|
29
|
+
)
|
|
30
|
+
logger.add(
|
|
31
|
+
error_log_path,
|
|
32
|
+
rotation="500 MB",
|
|
33
|
+
retention="30 days",
|
|
34
|
+
compression="gz",
|
|
35
|
+
level="ERROR",
|
|
36
|
+
encoding="utf=8"
|
|
37
|
+
)
|
|
38
|
+
if console_stdout:
|
|
39
|
+
logger.add(
|
|
40
|
+
sink=lambda msg: print(msg, end=""),
|
|
41
|
+
level=log_level,
|
|
42
|
+
format=log_format,
|
|
43
|
+
enqueue=True
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
return logger
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
import time
|
|
3
|
+
from typing import Dict, Generator, List, Optional
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from app.models.schemas import ChatResponse
|
|
7
|
+
from app.service.chat_service import ChatService
|
|
8
|
+
from app.service.rag_service import RagService
|
|
9
|
+
from app.utils.function_utils import tools
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChatstreamManager:
|
|
13
|
+
def __init__(self, config, chat_service: ChatService, rag_service: RagService):
|
|
14
|
+
self._chat_service = chat_service
|
|
15
|
+
self._rag_service = rag_service
|
|
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] = {}
|
|
19
|
+
self._max_history = config["CONVERSATION_MAX_HISTORY"]
|
|
20
|
+
self._ttl = config["CONVERSATION_TTL"]
|
|
21
|
+
self._lock = threading.Lock()
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def normalize_conversation_id(conversation_id: Optional[str]) -> str:
|
|
25
|
+
normalized = (conversation_id or "").strip()
|
|
26
|
+
return normalized or f"conv-{uuid4().hex}"
|
|
27
|
+
|
|
28
|
+
def check_rag(self) -> Dict:
|
|
29
|
+
return self._rag_service.check()
|
|
30
|
+
|
|
31
|
+
def force_rebuild_rag(self) -> Dict:
|
|
32
|
+
return self._rag_service.force_rebuild()
|
|
33
|
+
|
|
34
|
+
def _cleanup_expired(self):
|
|
35
|
+
now = time.time()
|
|
36
|
+
expired = [cid for cid, c in self._conversations.items() if now - c["last_active"] > self._ttl]
|
|
37
|
+
for cid in expired:
|
|
38
|
+
del self._conversations[cid]
|
|
39
|
+
|
|
40
|
+
def _ensure_conversation(self, conversation_id: str):
|
|
41
|
+
if conversation_id not in self._conversations:
|
|
42
|
+
self._conversations[conversation_id] = {
|
|
43
|
+
"messages": [],
|
|
44
|
+
"created_at": time.time(),
|
|
45
|
+
"last_active": time.time(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def _get_history(self, conversation_id: str) -> List[Dict[str, str]]:
|
|
49
|
+
with self._lock:
|
|
50
|
+
self._cleanup_expired()
|
|
51
|
+
self._ensure_conversation(conversation_id)
|
|
52
|
+
return list(self._conversations[conversation_id]["messages"])
|
|
53
|
+
|
|
54
|
+
def _append_exchange(self, conversation_id: str, user_question: str, assistant_answer: str):
|
|
55
|
+
with self._lock:
|
|
56
|
+
self._ensure_conversation(conversation_id)
|
|
57
|
+
conversation = self._conversations[conversation_id]
|
|
58
|
+
conversation["messages"].extend([
|
|
59
|
+
{"role": "user", "content": user_question},
|
|
60
|
+
{"role": "assistant", "content": assistant_answer},
|
|
61
|
+
])
|
|
62
|
+
conversation["last_active"] = time.time()
|
|
63
|
+
max_messages = self._max_history * 2
|
|
64
|
+
if len(conversation["messages"]) > max_messages:
|
|
65
|
+
conversation["messages"] = conversation["messages"][-max_messages:]
|
|
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
|
+
|
|
85
|
+
def chat_stream(
|
|
86
|
+
self,
|
|
87
|
+
conversation_id: Optional[str],
|
|
88
|
+
question: str,
|
|
89
|
+
jsessionid: str,
|
|
90
|
+
) -> Generator[ChatResponse, None, None]:
|
|
91
|
+
normalized_conversation_id = self.normalize_conversation_id(conversation_id)
|
|
92
|
+
abort_event = self._start_stream(jsessionid, normalized_conversation_id)
|
|
93
|
+
history = self._get_history(normalized_conversation_id)
|
|
94
|
+
answer_parts: List[str] = []
|
|
95
|
+
saved = False
|
|
96
|
+
|
|
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
|
|
118
|
+
|
|
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)
|