Files
law-kb/app/services/history.py
T
freedak 641e33b834 feat: QYLAW 法律法规知识库
- 语义检索(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>
2026-08-07 14:55:25 +08:00

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