641e33b834
- 语义检索(FAISS + embedding)+ 精确查找(法规名+条号) - RAG 问答(SSE 流式,支持 thinking 折叠显示) - 法规浏览(原文阅读) - 历史记录(检索+对话持久化到 SQLite) - 设置页(系统提示词/模板/LLM 参数可配置) - 检索质量评估脚本 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
301 lines
9.0 KiB
Python
301 lines
9.0 KiB
Python
"""历史记录服务 — 检索历史和对话历史持久化到 SQLite
|
|
|
|
表结构:
|
|
- search_history: 检索历史(查询词、过滤条件、结果数)
|
|
- chat_history: 对话历史(问题、模板、thinking、答案、引用条文)
|
|
"""
|
|
import sqlite3
|
|
import time
|
|
import json
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from app.config import SQLITE_PATH
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_conn() -> sqlite3.Connection:
|
|
"""获取 SQLite 连接(自动建表)"""
|
|
SQLITE_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
conn = sqlite3.connect(str(SQLITE_PATH), check_same_thread=False)
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS search_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
query TEXT NOT NULL,
|
|
category TEXT,
|
|
province TEXT,
|
|
city TEXT,
|
|
result_count INTEGER DEFAULT 0,
|
|
top_k INTEGER DEFAULT 20,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS chat_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
question TEXT NOT NULL,
|
|
template TEXT DEFAULT 'simple',
|
|
category TEXT,
|
|
province TEXT,
|
|
thinking TEXT,
|
|
answer TEXT,
|
|
citations TEXT,
|
|
thinking_enabled INTEGER DEFAULT 1,
|
|
status TEXT DEFAULT 'done',
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_search_history_created
|
|
ON search_history(created_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_chat_history_created
|
|
ON chat_history(created_at DESC);
|
|
""")
|
|
return conn
|
|
|
|
|
|
def add_search_history(
|
|
query: str,
|
|
category: Optional[str] = None,
|
|
province: Optional[str] = None,
|
|
city: Optional[str] = None,
|
|
result_count: int = 0,
|
|
top_k: int = 20,
|
|
) -> int:
|
|
"""记录检索历史
|
|
|
|
Returns:
|
|
记录 ID,失败返回 0
|
|
"""
|
|
try:
|
|
conn = _get_conn()
|
|
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
cursor = conn.execute(
|
|
"""INSERT INTO search_history
|
|
(query, category, province, city, result_count, top_k, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)""",
|
|
(query[:200], category, province, city, result_count, top_k, ts),
|
|
)
|
|
conn.commit()
|
|
rid = cursor.lastrowid
|
|
conn.close()
|
|
return rid
|
|
except Exception as e:
|
|
logger.error(f"记录检索历史失败: {e}")
|
|
return 0
|
|
|
|
|
|
def add_chat_history(
|
|
question: str,
|
|
template: str = "simple",
|
|
category: Optional[str] = None,
|
|
province: Optional[str] = None,
|
|
thinking: str = "",
|
|
answer: str = "",
|
|
citations: str = "[]",
|
|
thinking_enabled: bool = True,
|
|
status: str = "done",
|
|
) -> int:
|
|
"""记录对话历史
|
|
|
|
Args:
|
|
citations: JSON 字符串,引用条文列表
|
|
|
|
Returns:
|
|
记录 ID,失败返回 0
|
|
"""
|
|
try:
|
|
conn = _get_conn()
|
|
ts = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
cursor = conn.execute(
|
|
"""INSERT INTO chat_history
|
|
(question, template, category, province, thinking, answer,
|
|
citations, thinking_enabled, status, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
(question[:1000], template, category, province,
|
|
thinking, answer, citations, 1 if thinking_enabled else 0,
|
|
status, ts),
|
|
)
|
|
conn.commit()
|
|
rid = cursor.lastrowid
|
|
conn.close()
|
|
return rid
|
|
except Exception as e:
|
|
logger.error(f"记录对话历史失败: {e}")
|
|
return 0
|
|
|
|
|
|
def update_chat_history(
|
|
chat_id: int,
|
|
thinking: str = "",
|
|
answer: str = "",
|
|
citations: str = "[]",
|
|
status: str = "done",
|
|
) -> bool:
|
|
"""更新对话历史(流式生成完成后更新完整内容)
|
|
|
|
Returns:
|
|
True 成功
|
|
"""
|
|
if not chat_id:
|
|
return False
|
|
try:
|
|
conn = _get_conn()
|
|
conn.execute(
|
|
"""UPDATE chat_history
|
|
SET thinking = ?, answer = ?, citations = ?, status = ?
|
|
WHERE id = ?""",
|
|
(thinking, answer, citations, status, chat_id),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"更新对话历史失败: {e}")
|
|
return False
|
|
|
|
|
|
def get_search_history(
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
) -> dict:
|
|
"""获取检索历史列表(分页,最新在前)
|
|
|
|
Returns:
|
|
{"items": [...], "total": int, "page": int, "page_size": int}
|
|
"""
|
|
try:
|
|
conn = _get_conn()
|
|
total = conn.execute("SELECT COUNT(*) FROM search_history").fetchone()[0]
|
|
offset = (page - 1) * page_size
|
|
rows = conn.execute(
|
|
"""SELECT id, query, category, province, city, result_count, top_k, created_at
|
|
FROM search_history
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?""",
|
|
(page_size, offset),
|
|
).fetchall()
|
|
conn.close()
|
|
items = [
|
|
{
|
|
"id": r[0], "query": r[1], "category": r[2], "province": r[3],
|
|
"city": r[4], "result_count": r[5], "top_k": r[6], "created_at": r[7],
|
|
}
|
|
for r in rows
|
|
]
|
|
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
|
except Exception as e:
|
|
logger.error(f"获取检索历史失败: {e}")
|
|
return {"items": [], "total": 0, "page": page, "page_size": page_size}
|
|
|
|
|
|
def get_chat_history(
|
|
page: int = 1,
|
|
page_size: int = 20,
|
|
) -> dict:
|
|
"""获取对话历史列表(分页,最新在前)
|
|
|
|
Returns:
|
|
{"items": [...], "total": int, "page": int, "page_size": int}
|
|
"""
|
|
try:
|
|
conn = _get_conn()
|
|
total = conn.execute("SELECT COUNT(*) FROM chat_history").fetchone()[0]
|
|
offset = (page - 1) * page_size
|
|
rows = conn.execute(
|
|
"""SELECT id, question, template, category, province,
|
|
thinking, answer, citations, thinking_enabled, status, created_at
|
|
FROM chat_history
|
|
ORDER BY created_at DESC
|
|
LIMIT ? OFFSET ?""",
|
|
(page_size, offset),
|
|
).fetchall()
|
|
conn.close()
|
|
items = []
|
|
for r in rows:
|
|
items.append({
|
|
"id": r[0], "question": r[1], "template": r[2], "category": r[3],
|
|
"province": r[4], "thinking": r[5], "answer": r[6],
|
|
"citations": json.loads(r[7]) if r[7] else [],
|
|
"thinking_enabled": bool(r[8]), "status": r[9], "created_at": r[10],
|
|
})
|
|
return {"items": items, "total": total, "page": page, "page_size": page_size}
|
|
except Exception as e:
|
|
logger.error(f"获取对话历史失败: {e}")
|
|
return {"items": [], "total": 0, "page": page, "page_size": page_size}
|
|
|
|
|
|
def get_chat_detail(chat_id: int) -> Optional[dict]:
|
|
"""获取单条对话历史详情"""
|
|
try:
|
|
conn = _get_conn()
|
|
row = conn.execute(
|
|
"""SELECT id, question, template, category, province,
|
|
thinking, answer, citations, thinking_enabled, status, created_at
|
|
FROM chat_history WHERE id = ?""",
|
|
(chat_id,),
|
|
).fetchone()
|
|
conn.close()
|
|
if not row:
|
|
return None
|
|
return {
|
|
"id": row[0], "question": row[1], "template": row[2], "category": row[3],
|
|
"province": row[4], "thinking": row[5], "answer": row[6],
|
|
"citations": json.loads(row[7]) if row[7] else [],
|
|
"thinking_enabled": bool(row[8]), "status": row[9], "created_at": row[10],
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"获取对话详情失败: {e}")
|
|
return None
|
|
|
|
|
|
def delete_search_history(history_id: int) -> bool:
|
|
"""删除单条检索历史"""
|
|
try:
|
|
conn = _get_conn()
|
|
conn.execute("DELETE FROM search_history WHERE id = ?", (history_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"删除检索历史失败: {e}")
|
|
return False
|
|
|
|
|
|
def delete_chat_history(history_id: int) -> bool:
|
|
"""删除单条对话历史"""
|
|
try:
|
|
conn = _get_conn()
|
|
conn.execute("DELETE FROM chat_history WHERE id = ?", (history_id,))
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"删除对话历史失败: {e}")
|
|
return False
|
|
|
|
|
|
def clear_search_history() -> bool:
|
|
"""清空全部检索历史"""
|
|
try:
|
|
conn = _get_conn()
|
|
conn.execute("DELETE FROM search_history")
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"清空检索历史失败: {e}")
|
|
return False
|
|
|
|
|
|
def clear_chat_history() -> bool:
|
|
"""清空全部对话历史"""
|
|
try:
|
|
conn = _get_conn()
|
|
conn.execute("DELETE FROM chat_history")
|
|
conn.commit()
|
|
conn.close()
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"清空对话历史失败: {e}")
|
|
return False
|