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>
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
"""历史记录服务 — 检索历史和对话历史持久化到 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
|
||||
Reference in New Issue
Block a user