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.
Files changed (81) hide show
  1. union_app_chat_stream-1.0.8/MANIFEST.in +13 -0
  2. union_app_chat_stream-1.0.8/PKG-INFO +59 -0
  3. union_app_chat_stream-1.0.8/PROJECT_OVERVIEW.md +203 -0
  4. union_app_chat_stream-1.0.8/app/__init__.py +42 -0
  5. union_app_chat_stream-1.0.8/app/authenticated_user.py +74 -0
  6. union_app_chat_stream-1.0.8/app/config/env_config.py +102 -0
  7. union_app_chat_stream-1.0.8/app/config/logger_config.py +46 -0
  8. union_app_chat_stream-1.0.8/app/manager/chatstream_manager.py +122 -0
  9. union_app_chat_stream-1.0.8/app/manager/prompts.py +62 -0
  10. union_app_chat_stream-1.0.8/app/manager/runtime_manager.py +552 -0
  11. union_app_chat_stream-1.0.8/app/models/schemas.py +32 -0
  12. union_app_chat_stream-1.0.8/app/service/chat_service.py +381 -0
  13. union_app_chat_stream-1.0.8/app/service/llm_service.py +214 -0
  14. union_app_chat_stream-1.0.8/app/service/rag_service.py +866 -0
  15. union_app_chat_stream-1.0.8/app/service/union_service.py +213 -0
  16. union_app_chat_stream-1.0.8/app/utils/__init__.py +5 -0
  17. union_app_chat_stream-1.0.8/app/utils/common_utils.py +176 -0
  18. union_app_chat_stream-1.0.8/app/utils/debug_context.py +16 -0
  19. union_app_chat_stream-1.0.8/app/utils/function_utils.py +371 -0
  20. union_app_chat_stream-1.0.8/app/utils/jwt_utils.py +39 -0
  21. union_app_chat_stream-1.0.8/app/views/__init__.py +6 -0
  22. union_app_chat_stream-1.0.8/app/views/view_chatstream.py +110 -0
  23. union_app_chat_stream-1.0.8/app/views/view_healthcheck.py +14 -0
  24. union_app_chat_stream-1.0.8/app/views/view_runtime.py +72 -0
  25. union_app_chat_stream-1.0.8/app/wsgi.py +37 -0
  26. union_app_chat_stream-1.0.8/ci.yml +14 -0
  27. union_app_chat_stream-1.0.8/deploy/autoconf/templates/env.j2 +25 -0
  28. union_app_chat_stream-1.0.8/deploy/autoconf.yml +15 -0
  29. union_app_chat_stream-1.0.8/deploy/scripts/healthcheck.sh +0 -0
  30. union_app_chat_stream-1.0.8/deploy/scripts/requirements.txt +53 -0
  31. union_app_chat_stream-1.0.8/deploy/scripts/start.sh +75 -0
  32. union_app_chat_stream-1.0.8/deploy/scripts/stop.sh +31 -0
  33. union_app_chat_stream-1.0.8/knowledge/000001-biz-offline-85b99bd43b-v1.md +88 -0
  34. union_app_chat_stream-1.0.8/knowledge/000002-biz-offline-717e8d823e-v1.md +90 -0
  35. union_app_chat_stream-1.0.8/knowledge/000003-biz-offline-c963227cc8-v1.md +84 -0
  36. union_app_chat_stream-1.0.8/knowledge/000004-biz-offline-2a5868e7da-v1.md +92 -0
  37. union_app_chat_stream-1.0.8/knowledge/000005-biz-offline-f9d9cf1a88-v1.md +79 -0
  38. union_app_chat_stream-1.0.8/knowledge/000006-biz-offline-c4fa2df3bd-v1.md +77 -0
  39. union_app_chat_stream-1.0.8/knowledge/000007-biz-offline-78304b70ca-v1.md +76 -0
  40. union_app_chat_stream-1.0.8/knowledge/000008-biz-offline-987ae67b35-v1.md +75 -0
  41. union_app_chat_stream-1.0.8/knowledge/000009-biz-offline-4d656bcea3-v1.md +85 -0
  42. union_app_chat_stream-1.0.8/knowledge/000010-sop-offline-a9e1050719-v1.md +100 -0
  43. union_app_chat_stream-1.0.8/knowledge/000011-biz-offline-5de0624891-v1.md +86 -0
  44. union_app_chat_stream-1.0.8/knowledge/000012-biz-offline-7dfacccba3-v1.md +82 -0
  45. union_app_chat_stream-1.0.8/knowledge/000013-biz-offline-5e1d29d2ed-v1.md +81 -0
  46. union_app_chat_stream-1.0.8/knowledge/000014-biz-offline-1d0ed8b841-v1.md +68 -0
  47. union_app_chat_stream-1.0.8/knowledge/000015-biz-offline-8a1376ee3e-v1.md +78 -0
  48. union_app_chat_stream-1.0.8/knowledge/000016-biz-offline-c8bfc2aa08-v1.md +99 -0
  49. union_app_chat_stream-1.0.8/knowledge/000017-biz-offline-9dffb28032-v1.md +88 -0
  50. union_app_chat_stream-1.0.8/knowledge/000018-biz-offline-f935bc9a6a-v1.md +80 -0
  51. union_app_chat_stream-1.0.8/knowledge/000019-biz-offline-858b3ecd89-v1.md +86 -0
  52. union_app_chat_stream-1.0.8/knowledge/000020-biz-offline-65cb5c4f40-v1.md +113 -0
  53. union_app_chat_stream-1.0.8/knowledge/000021-biz-offline-1bf211639c-v1.md +148 -0
  54. union_app_chat_stream-1.0.8/knowledge/000022-biz-offline-8c5a637879-v1.md +140 -0
  55. union_app_chat_stream-1.0.8/knowledge/000023-biz-offline-fe872b8712-v1.md +188 -0
  56. union_app_chat_stream-1.0.8/knowledge/000024-biz-offline-a85010c500-v1.md +133 -0
  57. union_app_chat_stream-1.0.8/knowledge/000025-biz-offline-8af58a3638-v1.md +136 -0
  58. union_app_chat_stream-1.0.8/knowledge/000026-biz-offline-6754102e93-v1.md +142 -0
  59. union_app_chat_stream-1.0.8/knowledge/000027-biz-offline-ea2e5ca5f9-v1.md +150 -0
  60. union_app_chat_stream-1.0.8/knowledge/000028-scenario-offline-dab45cebb4-v1.md +136 -0
  61. union_app_chat_stream-1.0.8/knowledge/000029-scenario-offline-5b8ae5ea9f-v1.md +143 -0
  62. union_app_chat_stream-1.0.8/knowledge/000030-scenario-offline-9a82d42f3f-v1.md +136 -0
  63. union_app_chat_stream-1.0.8/knowledge/000031-scenario-offline-cc2edc0197-v1.md +122 -0
  64. union_app_chat_stream-1.0.8/knowledge/000032-scenario-offline-e5f6e5cbfa-v1.md +122 -0
  65. union_app_chat_stream-1.0.8/knowledge/000033-scenario-offline-e1955849aa-v1.md +135 -0
  66. union_app_chat_stream-1.0.8/knowledge/000034-scenario-offline-3a13d49a3a-v1.md +138 -0
  67. union_app_chat_stream-1.0.8/knowledge/000035-scenario-offline-fd5560211f-v1.md +147 -0
  68. union_app_chat_stream-1.0.8/knowledge/000036-scenario-offline-function-call-mock-v1.md +134 -0
  69. union_app_chat_stream-1.0.8/package.json +20 -0
  70. union_app_chat_stream-1.0.8/pyproject.toml +17 -0
  71. union_app_chat_stream-1.0.8/requirements.txt +53 -0
  72. union_app_chat_stream-1.0.8/setup.cfg +4 -0
  73. union_app_chat_stream-1.0.8/tests/test_app_factory.py +803 -0
  74. union_app_chat_stream-1.0.8/tools/field_dictionary.yaml +28 -0
  75. union_app_chat_stream-1.0.8/tools/prompts.yaml +12 -0
  76. union_app_chat_stream-1.0.8/tools/tool_definitions.yaml +377 -0
  77. union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/PKG-INFO +59 -0
  78. union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/SOURCES.txt +79 -0
  79. union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/dependency_links.txt +1 -0
  80. union_app_chat_stream-1.0.8/union_app_chat_stream.egg-info/requires.txt +53 -0
  81. 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)