prompt-manager-client 0.2.0__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.
- prompt_manager_client-0.2.0/PKG-INFO +22 -0
- prompt_manager_client-0.2.0/README.md +227 -0
- prompt_manager_client-0.2.0/client_sdk/__init__.py +3 -0
- prompt_manager_client-0.2.0/client_sdk/prompt_client.py +431 -0
- prompt_manager_client-0.2.0/prompt_manager_client.egg-info/PKG-INFO +22 -0
- prompt_manager_client-0.2.0/prompt_manager_client.egg-info/SOURCES.txt +9 -0
- prompt_manager_client-0.2.0/prompt_manager_client.egg-info/dependency_links.txt +1 -0
- prompt_manager_client-0.2.0/prompt_manager_client.egg-info/requires.txt +8 -0
- prompt_manager_client-0.2.0/prompt_manager_client.egg-info/top_level.txt +1 -0
- prompt_manager_client-0.2.0/pyproject.toml +33 -0
- prompt_manager_client-0.2.0/setup.cfg +4 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prompt-manager-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 提示词管理系统 - 客户端 SDK。支持三维管理、变量解析、热更新。
|
|
5
|
+
Author: prompt-manager
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, http://localhost:8900
|
|
8
|
+
Project-URL: Source, https://github.com/your-org/prompt-manager
|
|
9
|
+
Keywords: prompt,prompt-manager,llm
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Provides-Extra: ws
|
|
18
|
+
Requires-Dist: websocket-client; extra == "ws"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build; extra == "dev"
|
|
21
|
+
Requires-Dist: twine; extra == "dev"
|
|
22
|
+
Requires-Dist: websocket-client; extra == "dev"
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Prompt Manager - 提示词 & 知识库管理系统
|
|
2
|
+
|
|
3
|
+
统一的提示词和知识库管理平台,支持多科室、多平台的三维管理,提供 Web 管理界面和 Python 客户端 SDK。
|
|
4
|
+
|
|
5
|
+
## 项目结构
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
prompt_manager/
|
|
9
|
+
├── backend/ # 后端服务
|
|
10
|
+
│ ├── main.py # FastAPI 主应用(API 路由)
|
|
11
|
+
│ ├── database.py # SQLite 数据库层(CRUD + 缓存)
|
|
12
|
+
│ ├── models.py # Pydantic 数据模型
|
|
13
|
+
│ ├── requirements.txt # Python 依赖
|
|
14
|
+
│ ├── start.sh # 后端启动脚本
|
|
15
|
+
│ └── prompt_manager.db # SQLite 数据库文件(自动创建)
|
|
16
|
+
├── frontend/
|
|
17
|
+
│ └── index.html # 单页面管理界面(Vue 3)
|
|
18
|
+
├── client_sdk/ # 客户端 SDK
|
|
19
|
+
│ ├── __init__.py
|
|
20
|
+
│ ├── prompt_client.py # PromptClient 类
|
|
21
|
+
│ └── pyproject.toml # SDK 构建配置
|
|
22
|
+
├── knowledge_retriever.py # 知识库检索模块
|
|
23
|
+
├── pyproject.toml # 项目构建配置
|
|
24
|
+
└── start.sh # 一键启动脚本
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 功能概览
|
|
28
|
+
|
|
29
|
+
### 提示词管理
|
|
30
|
+
|
|
31
|
+
| 功能 | 说明 |
|
|
32
|
+
|------|------|
|
|
33
|
+
| 三维管理 | 科室(口腔/植发/皮肤/眼科/儿科/医美)× 平台(百度/小红书/抖音/快手/微信)× 场景 |
|
|
34
|
+
| 变量引擎 | 支持 `{公司}`、`{域中文}`、`{时间}`、`{轮次}` 等占位符自动解析 |
|
|
35
|
+
| 版本管理 | 自动版本递增,支持一键回滚到历史版本 |
|
|
36
|
+
| 热更新 | 修改即时生效,通过轮询或 WebSocket 推送到客户端 |
|
|
37
|
+
|
|
38
|
+
### 知识库管理
|
|
39
|
+
|
|
40
|
+
| 功能 | 说明 |
|
|
41
|
+
|------|------|
|
|
42
|
+
| 唯一索引 | 每个科室+平台组合只保留一个知识库 |
|
|
43
|
+
| 条目管理 | 逐条编辑、删除、新增,每页 10 条分页 |
|
|
44
|
+
| 本地持久化 | 数据存储在 SQLite,自动持久化 |
|
|
45
|
+
| 智能检索 | 支持关键词检索(TF-IDF)和语义检索(可选) |
|
|
46
|
+
|
|
47
|
+
## 快速开始
|
|
48
|
+
|
|
49
|
+
### 1. 启动服务
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 方式一:一键启动
|
|
53
|
+
cd prompt_manager
|
|
54
|
+
bash start.sh
|
|
55
|
+
|
|
56
|
+
# 方式二:手动启动
|
|
57
|
+
cd backend
|
|
58
|
+
pip install -r requirements.txt
|
|
59
|
+
python main.py
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
服务启动后访问:
|
|
63
|
+
- 管理界面:http://localhost:8900
|
|
64
|
+
- API 文档:http://localhost:8900/docs
|
|
65
|
+
|
|
66
|
+
### 2. 客户端 SDK 使用
|
|
67
|
+
|
|
68
|
+
#### 安装
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# 方式一:本地安装
|
|
72
|
+
cd client_sdk
|
|
73
|
+
pip install -e .
|
|
74
|
+
|
|
75
|
+
# 方式二:直接复制 prompt_client.py 到项目中
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### 提示词相关
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from prompt_client import PromptClient
|
|
82
|
+
|
|
83
|
+
client = PromptClient(base_url="http://localhost:8900")
|
|
84
|
+
|
|
85
|
+
# 预加载
|
|
86
|
+
client.preload()
|
|
87
|
+
|
|
88
|
+
# 获取提示词内容
|
|
89
|
+
content = client.get_content("hair_xhs_system")
|
|
90
|
+
|
|
91
|
+
# 获取已解析变量的提示词(最常用)
|
|
92
|
+
prompt = client.get_resolved(
|
|
93
|
+
name="hair_xhs_system",
|
|
94
|
+
robot_id="9125",
|
|
95
|
+
current_round=3,
|
|
96
|
+
action_desc="\n回复意图:问诊、套联"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# 启动后台热更新(30秒轮询一次)
|
|
100
|
+
client.start_auto_update(interval=30)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
#### 知识库相关
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# 获取知识库条目列表
|
|
107
|
+
items = client.get_knowledge(department="hair", platform="xhs")
|
|
108
|
+
# -> ["雄激素性脱发表现为...", "米诺地尔是常用药物...", ...]
|
|
109
|
+
|
|
110
|
+
# 获取合并文本
|
|
111
|
+
text = client.get_knowledge_text(department="hair", platform="xhs")
|
|
112
|
+
# -> "雄激素性脱发表现为...\n米诺地尔是常用药物..."
|
|
113
|
+
|
|
114
|
+
# 刷新知识库缓存
|
|
115
|
+
client.refresh_knowledge(department="hair", platform="xhs")
|
|
116
|
+
|
|
117
|
+
# 预加载全部(提示词 + 知识库)
|
|
118
|
+
client.preload()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. 知识库检索
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from knowledge_retriever import KnowledgeRetriever
|
|
125
|
+
|
|
126
|
+
# 关键词检索(默认,无需额外依赖)
|
|
127
|
+
retriever = KnowledgeRetriever(base_url="http://localhost:8900")
|
|
128
|
+
retriever.preload(department="hair", platform="xhs")
|
|
129
|
+
|
|
130
|
+
history = [
|
|
131
|
+
{"role": "user", "content": "最近掉头发很严重怎么办"},
|
|
132
|
+
{"role": "assistant", "content": "请问您掉发持续多长时间了?"}
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# 检索相关知识
|
|
136
|
+
results = retriever.retrieve(history, department="hair", platform="xhs", top_k=5)
|
|
137
|
+
|
|
138
|
+
# 获取拼接文本(直接插入 prompt)
|
|
139
|
+
knowledge_text = retriever.retrieve_as_text(
|
|
140
|
+
history, department="hair", platform="xbs",
|
|
141
|
+
top_k=5, separator="\n"
|
|
142
|
+
)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
#### 语义检索(可选)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
pip install sentence-transformers
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
retriever = KnowledgeRetriever(
|
|
153
|
+
base_url="http://localhost:8900",
|
|
154
|
+
use_semantic=True,
|
|
155
|
+
semantic_model="shibing624/text2vec-base-chinese"
|
|
156
|
+
)
|
|
157
|
+
# 使用方式与关键词检索完全相同
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## API 接口
|
|
161
|
+
|
|
162
|
+
### 提示词 API
|
|
163
|
+
|
|
164
|
+
| 方法 | 路径 | 说明 |
|
|
165
|
+
|------|------|------|
|
|
166
|
+
| POST | `/api/prompts` | 创建提示词 |
|
|
167
|
+
| GET | `/api/prompts` | 列表查询(支持科室/平台/场景/关键词过滤) |
|
|
168
|
+
| GET | `/api/prompts/{id}` | 获取详情 |
|
|
169
|
+
| PUT | `/api/prompts/{id}` | 更新内容 |
|
|
170
|
+
| DELETE | `/api/prompts/{id}` | 删除 |
|
|
171
|
+
| GET | `/api/prompts/{id}/versions` | 获取版本历史 |
|
|
172
|
+
| POST | `/api/prompts/{id}/rollback` | 回滚到指定版本 |
|
|
173
|
+
| GET | `/api/v1/fetch/{name}` | 按名称获取(客户端用) |
|
|
174
|
+
| GET | `/api/v1/sync` | 全量同步(客户端用) |
|
|
175
|
+
| POST | `/api/v1/resolve` | 变量解析 |
|
|
176
|
+
|
|
177
|
+
### 知识库 API
|
|
178
|
+
|
|
179
|
+
| 方法 | 路径 | 说明 |
|
|
180
|
+
|------|------|------|
|
|
181
|
+
| POST | `/api/knowledge` | 创建知识库(科室+平台唯一) |
|
|
182
|
+
| GET | `/api/knowledge` | 列表查询(支持科室/平台/关键词过滤) |
|
|
183
|
+
| GET | `/api/knowledge/{id}` | 获取详情 |
|
|
184
|
+
| PUT | `/api/knowledge/{id}` | 更新内容 |
|
|
185
|
+
| DELETE | `/api/knowledge/{id}` | 删除 |
|
|
186
|
+
|
|
187
|
+
### WebSocket
|
|
188
|
+
|
|
189
|
+
| 路径 | 说明 |
|
|
190
|
+
|------|------|
|
|
191
|
+
| `/ws/updates` | 实时推送提示词/知识库变更 |
|
|
192
|
+
|
|
193
|
+
## 科室与平台编码
|
|
194
|
+
|
|
195
|
+
| 科室代码 | 中文名 | 平台代码 | 中文名 |
|
|
196
|
+
|----------|--------|----------|--------|
|
|
197
|
+
| hair | 植发科 | xhs | 小红书 |
|
|
198
|
+
| dentistry | 口腔科 | bd | 百度 |
|
|
199
|
+
| dermatology | 皮肤科 | dy | 抖音 |
|
|
200
|
+
| ophthalmology | 眼科 | kuaishou | 快手 |
|
|
201
|
+
| pediatrics | 儿科 | wechat | 微信 |
|
|
202
|
+
| beauty | 医美 | general | 通用 |
|
|
203
|
+
| general | 通用 | | |
|
|
204
|
+
|
|
205
|
+
## 客户端 SDK 构建
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cd client_sdk
|
|
209
|
+
|
|
210
|
+
# 构建
|
|
211
|
+
pip install build
|
|
212
|
+
python -m build
|
|
213
|
+
|
|
214
|
+
# 本地安装
|
|
215
|
+
pip install -e .
|
|
216
|
+
|
|
217
|
+
# 发布到 PyPI(可选)
|
|
218
|
+
pip install twine
|
|
219
|
+
twine upload dist/*
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 技术栈
|
|
223
|
+
|
|
224
|
+
- **后端**:FastAPI + SQLite + Pydantic
|
|
225
|
+
- **前端**:Vue 3(CDN)+ 单 HTML 文件
|
|
226
|
+
- **客户端 SDK**:纯 Python,零依赖(语义检索可选 sentence-transformers)
|
|
227
|
+
- **数据库**:SQLite,自动迁移
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
提示词 & 知识库管理系统 - 客户端 SDK
|
|
5
|
+
|
|
6
|
+
专为模型服务设计,支持:
|
|
7
|
+
1. 按科室+平台+场景获取提示词
|
|
8
|
+
2. 按科室+平台获取知识库内容
|
|
9
|
+
3. 变量自动解析(公司、时间、轮次等)
|
|
10
|
+
4. 热更新(轮询/WebSocket)
|
|
11
|
+
5. 本地缓存,读取零延迟
|
|
12
|
+
|
|
13
|
+
使用示例:
|
|
14
|
+
from prompt_client import PromptClient
|
|
15
|
+
|
|
16
|
+
client = PromptClient(base_url="http://localhost:8900")
|
|
17
|
+
|
|
18
|
+
# 获取已解析的提示词
|
|
19
|
+
prompt = client.get_resolved(
|
|
20
|
+
name="hair_xhs_system",
|
|
21
|
+
robot_id="9125",
|
|
22
|
+
current_round=3,
|
|
23
|
+
action_desc="\\n回复意图:问诊、套联"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# 获取知识库
|
|
27
|
+
kb_items = client.get_knowledge(department="hair", platform="xhs")
|
|
28
|
+
# kb_items -> ["知识条目1", "知识条目2", ...]
|
|
29
|
+
|
|
30
|
+
# 启动后台热更新
|
|
31
|
+
client.start_auto_update(interval=30)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import time
|
|
35
|
+
import threading
|
|
36
|
+
import logging
|
|
37
|
+
import json
|
|
38
|
+
from typing import Optional, Dict, List, Callable
|
|
39
|
+
from urllib.request import urlopen, Request
|
|
40
|
+
from urllib.error import URLError, HTTPError
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger("prompt_client")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class PromptClient:
|
|
46
|
+
"""
|
|
47
|
+
提示词 & 知识库管理客户端
|
|
48
|
+
|
|
49
|
+
特性:
|
|
50
|
+
- 三维管理:科室 + 平台 + 场景
|
|
51
|
+
- 知识库管理:科室 + 平台 唯一索引
|
|
52
|
+
- 本地缓存,读取零延迟
|
|
53
|
+
- 变量自动解析
|
|
54
|
+
- 支持轮询和WebSocket热更新
|
|
55
|
+
- 线程安全
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, base_url: str = "http://localhost:8900"):
|
|
59
|
+
self.base_url = base_url.rstrip("/")
|
|
60
|
+
# 提示词缓存: {name: {content, version, department, platform, scene, variables, updated_at}}
|
|
61
|
+
self._cache: Dict[str, dict] = {}
|
|
62
|
+
# 已解析缓存: {cache_key: (resolved_content, version)}
|
|
63
|
+
self._resolved_cache: Dict[str, tuple] = {}
|
|
64
|
+
# 知识库缓存: {(department, platform): {id, content, updated_at}}
|
|
65
|
+
self._kb_cache: Dict[tuple, dict] = {}
|
|
66
|
+
self._lock = threading.Lock()
|
|
67
|
+
self._running = False
|
|
68
|
+
self._poll_thread: Optional[threading.Thread] = None
|
|
69
|
+
self._on_update_callback: Optional[Callable] = None
|
|
70
|
+
|
|
71
|
+
# ==================== 提示词接口 ====================
|
|
72
|
+
|
|
73
|
+
def get(self, name: str) -> Optional[dict]:
|
|
74
|
+
"""获取提示词原始数据(从缓存,零延迟)"""
|
|
75
|
+
with self._lock:
|
|
76
|
+
if name in self._cache:
|
|
77
|
+
return dict(self._cache[name])
|
|
78
|
+
self._fetch_one(name)
|
|
79
|
+
with self._lock:
|
|
80
|
+
return dict(self._cache[name]) if name in self._cache else None
|
|
81
|
+
|
|
82
|
+
def get_content(self, name: str, default: str = "") -> str:
|
|
83
|
+
"""获取提示词原始内容"""
|
|
84
|
+
data = self.get(name)
|
|
85
|
+
return data["content"] if data else default
|
|
86
|
+
|
|
87
|
+
def get_resolved(
|
|
88
|
+
self,
|
|
89
|
+
name: str,
|
|
90
|
+
robot_id: str = "",
|
|
91
|
+
department: str = "",
|
|
92
|
+
current_round: int = 0,
|
|
93
|
+
action_desc: str = "",
|
|
94
|
+
knowledge_desc: str = "",
|
|
95
|
+
warmup_desc: str = "",
|
|
96
|
+
connect_desc: str = "",
|
|
97
|
+
extra_variables: dict = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""
|
|
100
|
+
获取已解析变量的提示词(最常用接口)
|
|
101
|
+
|
|
102
|
+
内部会自动解析 {公司}, {域中文}, {时间}, {轮次} 等变量,
|
|
103
|
+
并调用远程 resolve API 处理更复杂的变量替换。
|
|
104
|
+
"""
|
|
105
|
+
extra_variables = extra_variables or {}
|
|
106
|
+
|
|
107
|
+
# 先确保缓存中有数据
|
|
108
|
+
data = self.get(name)
|
|
109
|
+
if not data:
|
|
110
|
+
return ""
|
|
111
|
+
|
|
112
|
+
# 构建缓存key
|
|
113
|
+
cache_key = f"{name}::{robot_id}::{department}::{current_round}::{action_desc}::{knowledge_desc}::{warmup_desc}::{connect_desc}"
|
|
114
|
+
|
|
115
|
+
with self._lock:
|
|
116
|
+
if cache_key in self._resolved_cache:
|
|
117
|
+
resolved, version = self._resolved_cache[cache_key]
|
|
118
|
+
if version == data["version"]:
|
|
119
|
+
return resolved
|
|
120
|
+
|
|
121
|
+
# 调用远程解析API
|
|
122
|
+
try:
|
|
123
|
+
payload = json.dumps({
|
|
124
|
+
"name": name,
|
|
125
|
+
"robot_id": robot_id,
|
|
126
|
+
"department": department,
|
|
127
|
+
"current_round": current_round,
|
|
128
|
+
"action_desc": action_desc,
|
|
129
|
+
"knowledge_desc": knowledge_desc,
|
|
130
|
+
"warmup_desc": warmup_desc,
|
|
131
|
+
"connect_desc": connect_desc,
|
|
132
|
+
"extra_variables": extra_variables
|
|
133
|
+
}).encode()
|
|
134
|
+
req = Request(
|
|
135
|
+
f"{self.base_url}/api/v1/resolve",
|
|
136
|
+
data=payload,
|
|
137
|
+
headers={"Content-Type": "application/json"}
|
|
138
|
+
)
|
|
139
|
+
with urlopen(req, timeout=10) as resp:
|
|
140
|
+
result = json.loads(resp.read().decode())
|
|
141
|
+
resolved = result["resolved_content"]
|
|
142
|
+
with self._lock:
|
|
143
|
+
self._resolved_cache[cache_key] = (resolved, result["version"])
|
|
144
|
+
return resolved
|
|
145
|
+
except Exception as e:
|
|
146
|
+
logger.warning(f"远程解析失败,使用本地缓存: {e}")
|
|
147
|
+
return data.get("content", "")
|
|
148
|
+
|
|
149
|
+
def get_version(self, name: str) -> int:
|
|
150
|
+
"""获取提示词版本号"""
|
|
151
|
+
with self._lock:
|
|
152
|
+
return self._cache.get(name, {}).get("version", 0)
|
|
153
|
+
|
|
154
|
+
def get_all_names(self) -> list:
|
|
155
|
+
"""获取所有缓存中的提示词名称"""
|
|
156
|
+
with self._lock:
|
|
157
|
+
return list(self._cache.keys())
|
|
158
|
+
|
|
159
|
+
# ==================== 知识库接口 ====================
|
|
160
|
+
|
|
161
|
+
def get_knowledge(self, department: str, platform: str) -> List[str]:
|
|
162
|
+
"""
|
|
163
|
+
根据科室和平台获取知识库内容列表
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
department: 科室代码,如 hair, dentistry, dermatology 等
|
|
167
|
+
platform: 平台代码,如 xhs, bd, dy 等
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
知识条目列表,如 ["条目1", "条目2", ...],未找到返回空列表
|
|
171
|
+
"""
|
|
172
|
+
key = (department, platform)
|
|
173
|
+
with self._lock:
|
|
174
|
+
if key in self._kb_cache:
|
|
175
|
+
content = self._kb_cache[key].get("content", "[]")
|
|
176
|
+
return self._parse_kb_content(content)
|
|
177
|
+
|
|
178
|
+
# 从服务端拉取
|
|
179
|
+
self._fetch_knowledge(department, platform)
|
|
180
|
+
with self._lock:
|
|
181
|
+
if key in self._kb_cache:
|
|
182
|
+
content = self._kb_cache[key].get("content", "[]")
|
|
183
|
+
return self._parse_kb_content(content)
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
def get_knowledge_text(self, department: str, platform: str, separator: str = "\n") -> str:
|
|
187
|
+
"""
|
|
188
|
+
根据科室和平台获取知识库内容,合并为单个字符串
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
department: 科室代码
|
|
192
|
+
platform: 平台代码
|
|
193
|
+
separator: 条目之间的分隔符,默认换行
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
合并后的知识库文本
|
|
197
|
+
"""
|
|
198
|
+
items = self.get_knowledge(department, platform)
|
|
199
|
+
return separator.join(items) if items else ""
|
|
200
|
+
|
|
201
|
+
def get_all_knowledge(self) -> Dict[str, List[str]]:
|
|
202
|
+
"""
|
|
203
|
+
获取所有缓存中的知识库
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
字典,key 为 "科室/平台",value 为条目列表
|
|
207
|
+
"""
|
|
208
|
+
result = {}
|
|
209
|
+
with self._lock:
|
|
210
|
+
for (dept, plat), data in self._kb_cache.items():
|
|
211
|
+
result[f"{dept}/{plat}"] = self._parse_kb_content(data.get("content", "[]"))
|
|
212
|
+
return result
|
|
213
|
+
|
|
214
|
+
def refresh_knowledge(self, department: str = None, platform: str = None):
|
|
215
|
+
"""
|
|
216
|
+
刷新知识库缓存
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
department: 科室代码,为空则刷新全部
|
|
220
|
+
platform: 平台代码,为空则刷新对应科室的全部或全部
|
|
221
|
+
"""
|
|
222
|
+
if department and platform:
|
|
223
|
+
self._fetch_knowledge(department, platform)
|
|
224
|
+
else:
|
|
225
|
+
self._sync_all_knowledge(department=department, platform=platform)
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _parse_kb_content(content_json: str) -> List[str]:
|
|
229
|
+
"""解析知识库 content JSON 为列表"""
|
|
230
|
+
try:
|
|
231
|
+
arr = json.loads(content_json or "[]")
|
|
232
|
+
return arr if isinstance(arr, list) else []
|
|
233
|
+
except (json.JSONDecodeError, TypeError):
|
|
234
|
+
return []
|
|
235
|
+
|
|
236
|
+
def _fetch_knowledge(self, department: str, platform: str) -> bool:
|
|
237
|
+
"""从服务端获取单个知识库"""
|
|
238
|
+
result = self._http_get(f"/api/knowledge?department={department}&platform={platform}")
|
|
239
|
+
if not result or not result.get("items"):
|
|
240
|
+
return False
|
|
241
|
+
item = result["items"][0]
|
|
242
|
+
key = (department, platform)
|
|
243
|
+
with self._lock:
|
|
244
|
+
old = self._kb_cache.get(key, {})
|
|
245
|
+
self._kb_cache[key] = item
|
|
246
|
+
if old.get("updated_at") != item.get("updated_at"):
|
|
247
|
+
logger.info(f"知识库 [{department}/{platform}] 已更新")
|
|
248
|
+
if self._on_update_callback:
|
|
249
|
+
try:
|
|
250
|
+
self._on_update_callback(f"kb:{department}/{platform}", item)
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.error(f"知识库更新回调失败: {e}")
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
def _sync_all_knowledge(self, department: str = None, platform: str = None):
|
|
256
|
+
"""同步所有知识库"""
|
|
257
|
+
params = []
|
|
258
|
+
if department:
|
|
259
|
+
params.append(f"department={department}")
|
|
260
|
+
if platform:
|
|
261
|
+
params.append(f"platform={platform}")
|
|
262
|
+
query = "&".join(params)
|
|
263
|
+
path = f"/api/knowledge?{query}" if query else "/api/knowledge"
|
|
264
|
+
result = self._http_get(path)
|
|
265
|
+
if not result:
|
|
266
|
+
return
|
|
267
|
+
for item in result.get("items", []):
|
|
268
|
+
key = (item.get("department", ""), item.get("platform", ""))
|
|
269
|
+
if not key[0] or not key[1]:
|
|
270
|
+
continue
|
|
271
|
+
with self._lock:
|
|
272
|
+
old = self._kb_cache.get(key, {})
|
|
273
|
+
self._kb_cache[key] = item
|
|
274
|
+
if old.get("updated_at") != item.get("updated_at"):
|
|
275
|
+
logger.info(f"[同步] 知识库 [{key[0]}/{key[1]}] 已更新")
|
|
276
|
+
|
|
277
|
+
# ==================== 通用接口 ====================
|
|
278
|
+
|
|
279
|
+
def preload(self):
|
|
280
|
+
"""全量预加载所有提示词和知识库"""
|
|
281
|
+
self._sync_all()
|
|
282
|
+
self._sync_all_knowledge()
|
|
283
|
+
|
|
284
|
+
def on_update(self, callback: Callable[[str, dict], None]):
|
|
285
|
+
"""注册更新回调: callback(key, data),key 对提示词为名称,对知识库为 kb:科室/平台"""
|
|
286
|
+
self._on_update_callback = callback
|
|
287
|
+
|
|
288
|
+
def start_auto_update(self, interval: int = 30, names: list = None):
|
|
289
|
+
"""启动轮询热更新(提示词+知识库)"""
|
|
290
|
+
if self._running:
|
|
291
|
+
return
|
|
292
|
+
self._running = True
|
|
293
|
+
self._poll_thread = threading.Thread(
|
|
294
|
+
target=self._poll_loop, args=(interval, names),
|
|
295
|
+
daemon=True, name="prompt-poll"
|
|
296
|
+
)
|
|
297
|
+
self._poll_thread.start()
|
|
298
|
+
logger.info(f"轮询热更新已启动,间隔 {interval}s")
|
|
299
|
+
|
|
300
|
+
def start_ws_update(self):
|
|
301
|
+
"""启动WebSocket实时更新"""
|
|
302
|
+
try:
|
|
303
|
+
import websocket
|
|
304
|
+
except ImportError:
|
|
305
|
+
logger.error("需要安装 websocket-client: pip install websocket-client")
|
|
306
|
+
return
|
|
307
|
+
if self._ws_thread and hasattr(self, '_ws_thread') and self._ws_thread.is_alive():
|
|
308
|
+
return
|
|
309
|
+
self._running = True
|
|
310
|
+
self._ws_thread = threading.Thread(
|
|
311
|
+
target=self._ws_loop, daemon=True, name="prompt-ws"
|
|
312
|
+
)
|
|
313
|
+
self._ws_thread.start()
|
|
314
|
+
|
|
315
|
+
def stop(self):
|
|
316
|
+
"""停止后台更新"""
|
|
317
|
+
self._running = False
|
|
318
|
+
if self._poll_thread:
|
|
319
|
+
self._poll_thread.join(timeout=5)
|
|
320
|
+
logger.info("热更新已停止")
|
|
321
|
+
|
|
322
|
+
# ========== 内部方法 ==========
|
|
323
|
+
def _http_get(self, path: str) -> Optional[dict]:
|
|
324
|
+
try:
|
|
325
|
+
req = Request(f"{self.base_url}{path}")
|
|
326
|
+
with urlopen(req, timeout=10) as resp:
|
|
327
|
+
return json.loads(resp.read().decode())
|
|
328
|
+
except (URLError, HTTPError, TimeoutError) as e:
|
|
329
|
+
logger.warning(f"请求失败 {path}: {e}")
|
|
330
|
+
return None
|
|
331
|
+
|
|
332
|
+
def _fetch_one(self, name: str) -> bool:
|
|
333
|
+
with self._lock:
|
|
334
|
+
current_version = self._cache.get(name, {}).get("version", 0)
|
|
335
|
+
result = self._http_get(f"/api/v1/fetch/{name}?current_version={current_version}")
|
|
336
|
+
if result is None:
|
|
337
|
+
if current_version == 0:
|
|
338
|
+
result = self._http_get(f"/api/v1/fetch/{name}")
|
|
339
|
+
if not result:
|
|
340
|
+
return False
|
|
341
|
+
else:
|
|
342
|
+
return True
|
|
343
|
+
with self._lock:
|
|
344
|
+
old_version = self._cache.get(name, {}).get("version", 0)
|
|
345
|
+
self._cache[name] = result
|
|
346
|
+
if old_version != result.get("version", 0):
|
|
347
|
+
logger.info(f"提示词 '{name}' 已更新到 v{result['version']}")
|
|
348
|
+
if self._on_update_callback:
|
|
349
|
+
try:
|
|
350
|
+
self._on_update_callback(name, result)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
logger.error(f"更新回调失败: {e}")
|
|
353
|
+
return True
|
|
354
|
+
|
|
355
|
+
def _sync_all(self):
|
|
356
|
+
result = self._http_get("/api/v1/sync")
|
|
357
|
+
if not result:
|
|
358
|
+
return
|
|
359
|
+
for item in result.get("prompts", []):
|
|
360
|
+
name = item.get("name")
|
|
361
|
+
if not name:
|
|
362
|
+
continue
|
|
363
|
+
with self._lock:
|
|
364
|
+
old_v = self._cache.get(name, {}).get("version", 0)
|
|
365
|
+
if item.get("version", 0) > old_v:
|
|
366
|
+
self._cache[name] = item
|
|
367
|
+
logger.info(f"[同步] '{name}' v{old_v} -> v{item['version']}")
|
|
368
|
+
|
|
369
|
+
def _poll_loop(self, interval, names):
|
|
370
|
+
while self._running:
|
|
371
|
+
try:
|
|
372
|
+
if names:
|
|
373
|
+
for n in names:
|
|
374
|
+
self._fetch_one(n)
|
|
375
|
+
else:
|
|
376
|
+
self._sync_all()
|
|
377
|
+
self._sync_all_knowledge()
|
|
378
|
+
except Exception as e:
|
|
379
|
+
logger.error(f"轮询异常: {e}")
|
|
380
|
+
for _ in range(interval):
|
|
381
|
+
if not self._running:
|
|
382
|
+
break
|
|
383
|
+
time.sleep(1)
|
|
384
|
+
|
|
385
|
+
def _ws_loop(self):
|
|
386
|
+
import websocket
|
|
387
|
+
|
|
388
|
+
def on_message(ws, message):
|
|
389
|
+
try:
|
|
390
|
+
msg = json.loads(message)
|
|
391
|
+
name = msg.get("name")
|
|
392
|
+
if name and msg.get("event") in ("created", "updated", "rollback"):
|
|
393
|
+
self._fetch_one(name)
|
|
394
|
+
# 清除已解析缓存
|
|
395
|
+
with self._lock:
|
|
396
|
+
keys_to_remove = [k for k in self._resolved_cache if k.startswith(f"{name}::")]
|
|
397
|
+
for k in keys_to_remove:
|
|
398
|
+
del self._resolved_cache[k]
|
|
399
|
+
# 知识库更新
|
|
400
|
+
if msg.get("type") == "knowledge_updated":
|
|
401
|
+
dept = msg.get("department")
|
|
402
|
+
plat = msg.get("platform")
|
|
403
|
+
if dept and plat:
|
|
404
|
+
self._fetch_knowledge(dept, plat)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
logger.error(f"WS消息处理失败: {e}")
|
|
407
|
+
|
|
408
|
+
def on_error(ws, error):
|
|
409
|
+
logger.warning(f"WS错误: {error}")
|
|
410
|
+
|
|
411
|
+
def on_close(ws, *args):
|
|
412
|
+
logger.info("WS连接关闭")
|
|
413
|
+
|
|
414
|
+
ws_url = self.base_url.replace("http://", "ws://").replace("https://", "wss://")
|
|
415
|
+
while self._running:
|
|
416
|
+
try:
|
|
417
|
+
ws = websocket.WebSocketApp(
|
|
418
|
+
f"{ws_url}/ws/updates",
|
|
419
|
+
on_message=on_message, on_error=on_error, on_close=on_close
|
|
420
|
+
)
|
|
421
|
+
ws.run_forever(ping_interval=30, ping_timeout=10)
|
|
422
|
+
except Exception as e:
|
|
423
|
+
logger.error(f"WS异常: {e}")
|
|
424
|
+
if self._running:
|
|
425
|
+
time.sleep(5)
|
|
426
|
+
|
|
427
|
+
def __repr__(self):
|
|
428
|
+
with self._lock:
|
|
429
|
+
return (f"PromptClient(cached_prompts={len(self._cache)}, "
|
|
430
|
+
f"cached_kb={len(self._kb_cache)}, "
|
|
431
|
+
f"names={list(self._cache.keys())})")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prompt-manager-client
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: 提示词管理系统 - 客户端 SDK。支持三维管理、变量解析、热更新。
|
|
5
|
+
Author: prompt-manager
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, http://localhost:8900
|
|
8
|
+
Project-URL: Source, https://github.com/your-org/prompt-manager
|
|
9
|
+
Keywords: prompt,prompt-manager,llm
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Provides-Extra: ws
|
|
18
|
+
Requires-Dist: websocket-client; extra == "ws"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build; extra == "dev"
|
|
21
|
+
Requires-Dist: twine; extra == "dev"
|
|
22
|
+
Requires-Dist: websocket-client; extra == "dev"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
client_sdk/__init__.py
|
|
4
|
+
client_sdk/prompt_client.py
|
|
5
|
+
prompt_manager_client.egg-info/PKG-INFO
|
|
6
|
+
prompt_manager_client.egg-info/SOURCES.txt
|
|
7
|
+
prompt_manager_client.egg-info/dependency_links.txt
|
|
8
|
+
prompt_manager_client.egg-info/requires.txt
|
|
9
|
+
prompt_manager_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
client_sdk
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prompt-manager-client"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "提示词管理系统 - 客户端 SDK。支持三维管理、变量解析、热更新。"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
keywords = ["prompt", "prompt-manager", "llm"]
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "prompt-manager"},
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.8",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "http://localhost:8900"
|
|
26
|
+
Source = "https://github.com/your-org/prompt-manager"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
ws = ["websocket-client"]
|
|
30
|
+
dev = ["build", "twine", "websocket-client"]
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
include = ["client_sdk*"]
|