Files
ai-worker-platform/kb.py
T
hz4th_coder 07f597b57e V3.5.2 真流式对话+思考折叠/历史会话/能力标签/模型库/语音/知识库/按千次计费
1. 修复假流式:对话接口边收边吐(stream_chat 生成器直驱 _chat_stream_raw);思考模型先流式思考内容(折叠)再流式回答;回答块显示 tok/s
2. 历史会话列表:重命名/置顶/删除(pinned/use_kb 列);📝Markdown 一键开关,默认 Markdown 渲染
3. 模型能力标签(chat/thinking/vision/audio_in/audio_out/image_gen/video_gen/embedding/rerank):接口库每模型勾选;
   对话按能力适配:视觉→传图、语音入→🎤录音(默认ASR转写)、语音出→🔊朗读(默认TTS)、思考→思考折叠;不匹配提前提示
4. 模型库标签:能力矩阵 + 系统默认模型(语音识别/合成/生图/生视频/embedding/rerank)
5. 计费修正:按次=元/千次;逐模型定价=模型名 输入价 缓存输入价 输出价(缓存价计缓存命中token)
6. 弹窗未保存提醒(点外部/X 时 confirm,全弹窗通用)
7. 知识库导航:#/kb 全局文档 CRUD + txt/md/pdf/docx 上传解析 + jieba BM25 检索;对话可一键注入知识库
2026-09-06 00:27:33 +08:00

141 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
V3.5.2 全局知识库
=================
- kb_documents / kb_chunks:文档 + 分块(jieba 分词,BM25 风格检索)
- 基本功能:增删改查、上传(txt/md/pdf)、全文检索、上下文注入(对话可选)
"""
import json
import re
import os
import db
import config
KB_UPLOAD_DIR = os.path.join(config.DATA_DIR, 'kb_uploads')
def _tok(text):
"""jieba 分词(去停用字、只留长度>=2 的 token"""
try:
import jieba
toks = []
for t in jieba.cut_for_search((text or '').lower()):
t = t.strip()
if len(t) >= 2 and not t.isdigit():
toks.append(t)
return toks
except Exception:
return [w for w in re.findall(r'[\u4e00-\u9fff]{2,}|[a-zA-Z0-9_]{2,}', (text or '').lower())]
def _chunks(content, size=400, overlap=60):
"""把文档切成小块(按段落聚合 + 超长硬切 + 前后重叠)"""
content = content or ''
paras = [p for p in re.split(r'\n+', content) if p.strip()]
blocks, buf = [], ''
for p in paras:
if buf and len(buf) + len(p) > size:
blocks.append(buf)
buf = ''
buf = (buf + '\n' + p) if buf else p
if buf:
blocks.append(buf)
out = []
for b in blocks:
while len(b) > size:
out.append(b[:size])
b = b[size - overlap:]
if b:
out.append(b)
return out or ['']
def rebuild_chunks(doc_id):
doc = db.q('SELECT * FROM kb_documents WHERE id=?', (doc_id,), one=True)
if not doc:
return 0
db.w('DELETE FROM kb_chunks WHERE doc_id=?', (doc_id,))
n = 0
for i, c in enumerate(_chunks(doc.get('content') or '')):
db.w('INSERT INTO kb_chunks (doc_id, idx, content, tokens) VALUES (?,?,?,?)',
(doc_id, i, c, json.dumps(_tok(c))))
n += 1
return n
def search(q, top_k=6):
"""BM25 风格检索:返回 [{doc_id,title,content,score}](按命中 token 数 + IDF 加权)"""
q_tokens = _tok(q)
if not q_tokens:
return []
rows = db.q('SELECT * FROM kb_chunks ORDER BY doc_id, idx')
if not rows:
return []
docs = {d['id']: d for d in db.q('SELECT id,title FROM kb_documents')}
df = {}
for c in rows:
for t in set(json.loads(c['tokens'] or '[]')):
df[t] = df.get(t, 0) + 1
n_docs = max(1, len(set(r['doc_id'] for r in rows)))
scored = []
for c in rows:
toks = json.loads(c['tokens'] or '[]')
tf = {}
for t in toks:
tf[t] = tf.get(t, 0) + 1
score = 0.0
for t in q_tokens:
if t in tf:
score += (1 + tf[t]) * max(0.1, (n_docs - df.get(t, 0) + 0.5) / (df.get(t, 0) + 0.5))
if score > 0:
scored.append({'doc_id': c['doc_id'], 'idx': c['idx'],
'content': c['content'], 'score': round(score, 3),
'title': docs.get(c['doc_id'], {}).get('title', '')})
scored.sort(key=lambda x: -x['score'])
return scored[:top_k]
def build_context(q, top_k=4):
"""把检索结果拼成可注入的上下文(用于对话/任务),返回 (ctx, hits)"""
hits = search(q, top_k)
if not hits:
return '', []
parts = []
for i, h in enumerate(hits):
parts.append(f"[{i + 1}]《{h['title']}\n{h['content'][:900]}")
ctx = ('以下是与你问题相关的【知识库参考】资料(可据此回答):\n' + '\n\n'.join(parts) + '\n\n----\n')
return ctx, hits
def extract_text(filename, raw):
"""按扩展名抽取文本:txt/md/html/csv/jsonpdf 用 pypdf(有则装)。返回 (text, ok)"""
ext = os.path.splitext(filename)[1].lower()
name = filename or 'doc'
if ext in ('.txt', '.md', '.markdown', '.html', '.htm', '.csv', '.json', '.log', '.py', '.js', '.css'):
for enc in ('utf-8', 'gbk', 'utf-8-sig'):
try:
return raw.decode(enc), True
except Exception:
continue
return raw.decode('utf-8', errors='ignore'), True
if ext == '.pdf':
try:
from pypdf import PdfReader
import io
reader = PdfReader(io.BytesIO(raw))
text = '\n'.join((pg.extract_text() or '') for pg in reader.pages)
return text, bool(text.strip())
except Exception:
return '', False
if ext in ('.docx',):
try:
import io
from docx import Document
doc = Document(io.BytesIO(raw))
text = '\n'.join(p.text for p in doc.paragraphs)
return text, bool(text.strip())
except Exception:
return '', False
return '', False