This commit is contained in:
2026-03-02 15:35:02 +08:00
parent a158cbbe9c
commit 460c2e87b8
26 changed files with 2040 additions and 265 deletions
+30
View File
@@ -0,0 +1,30 @@
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, Optional
class ErrorCode(str, Enum):
INVALID_WORKFLOW_TYPE = "INVALID_WORKFLOW_TYPE"
SQL_GENERATION_FAILED = "SQL_GENERATION_FAILED"
TABLE_MATCH_FAILED = "TABLE_MATCH_FAILED"
SQL_EXECUTION_FAILED = "SQL_EXECUTION_FAILED"
RAGFLOW_RETRIEVE_FAILED = "RAGFLOW_RETRIEVE_FAILED"
CONFIG_INVALID = "CONFIG_INVALID"
INTERNAL_ERROR = "INTERNAL_ERROR"
@dataclass
class AppError(Exception):
code: ErrorCode
message: str
status_code: int = 500
detail: Optional[Dict[str, Any]] = None
def to_dict(self) -> Dict[str, Any]:
return {
"code": self.code.value,
"message": self.message,
"detail": self.detail or {},
}
+20
View File
@@ -2,6 +2,11 @@ from __future__ import annotations
from typing import Optional
try:
import redis
except Exception:
redis = None
class CacheBase:
"""缓存接口"""
@@ -23,3 +28,18 @@ class NoopCache(CacheBase):
return None
class RedisCache(CacheBase):
"""Redis 缓存实现"""
def __init__(self, url: str, db: int = 0):
if redis is None:
raise ImportError("未安装 redis 依赖")
self._client = redis.Redis.from_url(url, db=db, decode_responses=True)
def get(self, key: str) -> Optional[str]:
return self._client.get(key)
def set(self, key: str, value: str, ttl: int) -> None:
self._client.set(key, value, ex=ttl)
+24 -2
View File
@@ -20,6 +20,15 @@ class RagflowClient:
def _build_url(self) -> str:
return self._base_url.rstrip("/") + "/" + self._retrieval_path.lstrip("/")
@staticmethod
def _normalize_dataset_ids(dataset_id: Optional[str]) -> list[str]:
"""将配置值规范化为 RAGFlow 需要的 list[string]"""
if not dataset_id:
return []
# 兼容逗号分隔配置
parts = [p.strip() for p in str(dataset_id).split(",") if p.strip()]
return parts
def retrieve(self, query: str, top_k: int = 3, dataset_id: Optional[str] = None, document_ids: Optional[str] = None) -> Dict[str, Any]:
"""检索匹配文档"""
if not self._base_url or not self._retrieval_path:
@@ -29,10 +38,12 @@ class RagflowClient:
url = self._build_url()
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
payload = {
"dataset_ids": dataset_id or "",
"query": query,
"dataset_ids": self._normalize_dataset_ids(dataset_id),
"question": query,
"top_k": top_k,
}
# 兼容部分版本字段
payload["query"] = query
if document_ids:
payload["document_ids"] = document_ids
@@ -57,6 +68,17 @@ def extract_table_name(record: Dict[str, Any]) -> Optional[str]:
return record.get(key)
content = record.get("content") or record.get("text") or ""
# 兼容 content 为 JSON 字符串:{"table":"xxx", ...}
try:
parsed = json.loads(str(content))
if isinstance(parsed, dict):
for key in ("table", "table_name"):
if parsed.get(key):
return str(parsed.get(key))
except Exception:
pass
for line in str(content).splitlines():
if line.lower().startswith("table:"):
return line.split(":", 1)[1].strip()
+273 -183
View File
@@ -7,45 +7,18 @@ import httpx
from config import Config
def _build_document_for_table(table: str, templates: List[str]) -> str:
"""构建表名检索文档 - 使用更标准的格式"""
lines = [
f"# 表名检索模板: {table}",
"",
"## 可用模板:",
""
]
for i, t in enumerate(templates, 1):
lines.append(f"{i}. {t}")
lines.extend(["", f"表名: {table}", "类型: 表名检索模板"])
return "\n".join(lines)
def _dump_json_content(data: Dict[str, Any]) -> str:
return json.dumps(data, ensure_ascii=False, indent=2)
def _build_sql_gen_document(table: str, prompt: Dict[str, any]) -> str:
"""构建 SQL 生成文档 - 使用更标准的格式"""
system_prompt = prompt.get("system_prompt", "")
business_prompt = prompt.get("business_prompt", "")
constraints = prompt.get("constraints", [])
lines = [
f"# SQL 生成提示词: {table}",
"",
"## 系统提示词:",
system_prompt,
"",
"## 业务提示词:",
business_prompt,
""
]
if constraints:
lines.extend(["## 约束条件:", ""])
for i, c in enumerate(constraints, 1):
lines.append(f"{i}. {c}")
lines.append("")
lines.extend([f"表名: {table}", "类型: SQL 生成提示词"])
return "\n".join(lines)
def _extract_tables_map(data: Dict[str, Any]) -> Dict[str, Any]:
"""兼容两种结构:{"tables": {...}} 或直接 {...}"""
tables = data.get("tables") if isinstance(data, dict) else None
if isinstance(tables, dict):
return tables
if isinstance(data, dict):
return data
return {}
class RagflowSync:
@@ -58,143 +31,271 @@ class RagflowSync:
self._table_retrieval_dataset_id = (cfg.get("table_retrieval_dataset_id") or "").strip()
self._sql_gen_dataset_id = (cfg.get("sql_gen_dataset_id") or "").strip()
def _validate_common(self) -> None:
if not self._base_url:
raise RuntimeError("未配置 ragflow.url")
if not self._upload_path:
raise RuntimeError("未配置 ragflow.upload 上传接口,请在 config/config.ini 中设置")
if "{dataset_id}" not in self._upload_path:
raise RuntimeError("上传接口路径必须包含 {dataset_id} 占位符")
if self._upload_mode not in ("overwrite", "append"):
raise RuntimeError("ragflow.upload_mode 仅支持 overwrite 或 append")
def _post(self, documents: List[Dict[str, Any]], dataset_id: str):
"""上传文档到指定知识库 - 使用 multipart/form-data 格式"""
if not self._base_url:
raise RuntimeError("未配置 ragflow.url")
# 构建正确的 URL
upload_path = self._upload_path.replace("{dataset_id}", dataset_id)
url = self._base_url + "/" + upload_path.lstrip("/")
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
# 由于接口使用 multipart/form-data,我们需要创建临时文件
import tempfile
# 创建临时文件并写入文档内容
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
# 将文档内容写入文件
for doc in documents:
content = doc.get('content', '')
f.write(content + '\n\n')
temp_file_path = f.name
try:
# 使用 multipart/form-data 上传文件
files = {'file': open(temp_file_path, 'rb')}
print(f"请求 URL: {url}") # 调试信息
print(f"上传文件: {temp_file_path}") # 调试信息
with httpx.Client(timeout=60) as client:
response = client.post(url, files=files, headers=headers)
response.raise_for_status()
result = response.json()
print(f"RAGFlow 上传响应: {result}") # 调试信息
return result
finally:
# 清理临时文件
import os
if os.path.exists(temp_file_path):
os.unlink(temp_file_path)
def upload_documents(self, dataset_id: str, documents: List[Dict[str, Any]]) -> Dict[str, Any]:
"""上传文档到指定知识库 - 使用 multipart/form-data 格式
根据官方文档: POST /api/v1/datasets/{dataset_id}/documents
"""
"""上传文档到指定知识库(每个文档单独上传)"""
if not self._base_url:
raise RuntimeError("未配置 ragflow.url")
if not dataset_id:
raise RuntimeError("dataset_id 为空,无法上传文档")
if not documents:
raise RuntimeError("没有可上传的文档内容")
# 构建正确的 URL
url = f"{self._base_url}/api/v1/datasets/{dataset_id}/documents"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
# 由于接口使用 multipart/form-data,我们需要创建临时文件
import tempfile
import os
# 创建临时文件并写入文档内容
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False, encoding='utf-8') as f:
# 将文档内容写入文件
for doc in documents:
content = doc.get('content', '')
f.write(content + '\n\n')
temp_file_path = f.name
try:
# 使用 multipart/form-data 上传文件
# 确保文件在 with 块内打开和关闭
with open(temp_file_path, 'rb') as file_obj:
files = {'file': file_obj}
print(f"上传文档 URL: {url}") # 调试信息
print(f"上传文件: {temp_file_path}") # 调试信息
with httpx.Client(timeout=60) as client:
response = client.post(url, files=files, headers=headers)
results: List[Dict[str, Any]] = []
with httpx.Client(timeout=60) as client:
for idx, doc in enumerate(documents, start=1):
content = str(doc.get("content", ""))
filename = str(doc.get("filename") or f"doc_{idx}.txt")
files = {"file": (filename, content.encode("utf-8"), "text/plain")}
print(f"上传文档 URL: {url}")
print(f"上传文件名: {filename}")
response = client.post(url, files=files, headers=headers)
response.raise_for_status()
result = response.json()
print(f"RAGFlow 上传响应: {result}") # 调试信息
# 检查文档处理状态
if result.get('code') == 0 and result.get('data'):
doc_id = result['data'][0].get('id')
if doc_id:
print(f"文档已上传,ID: {doc_id}")
print("注意: 文档处理需要时间,请等待 RAGFlow 完成分块处理")
print("可以在 RAGFlow 界面查看处理进度")
return result
finally:
# 清理临时文件
if os.path.exists(temp_file_path):
try:
os.unlink(temp_file_path)
except PermissionError:
# 如果文件被占用,等待一下再重试
import time
time.sleep(0.1)
try:
os.unlink(temp_file_path)
except PermissionError:
print(f"警告: 无法删除临时文件 {temp_file_path}")
print(f"RAGFlow 上传响应: {result}")
results.append(result)
def update_dataset(self, dataset_id: str, config: Dict[str, Any]) -> Dict[str, Any]:
"""更新知识库配置
根据官方文档: PUT /api/v1/datasets/{dataset_id}
"""
if not self._base_url:
raise RuntimeError("未配置 ragflow.url")
dataset_detail = self._get_dataset_detail(dataset_id)
chunk_method = self._extract_chunk_method(dataset_detail)
if chunk_method is None:
chunk_method = self._extract_chunk_method_from_upload_results(results)
# 参考 Java 实现:查询知识库文档 ID 后统一调用 chunks 解析
doc_ids = self._list_document_ids(dataset_id)
parse_results = self._auto_parse_documents(dataset_id, doc_ids)
upload_status = self._build_parse_status_from_upload_results(results)
return {
"ok": True,
"count": len(results),
"results": results,
"chunk_method": chunk_method,
"upload_status": upload_status,
"parse": parse_results,
}
def _get_dataset_detail(self, dataset_id: str) -> Dict[str, Any]:
"""查询知识库详情(用于读取 chunk_method)"""
url = f"{self._base_url}/api/v1/datasets/{dataset_id}"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
with httpx.Client(timeout=30) as client:
resp = client.get(url, headers=headers)
resp.raise_for_status()
return resp.json()
@staticmethod
def _extract_chunk_method(dataset_detail: Dict[str, Any]) -> Any:
"""从知识库详情提取 chunk_method"""
data = dataset_detail.get("data")
if isinstance(data, dict):
if "chunk_method" in data:
return data.get("chunk_method")
parser_cfg = data.get("parser_config") or {}
if isinstance(parser_cfg, dict):
return parser_cfg.get("chunk_method")
return None
@staticmethod
def _extract_chunk_method_from_upload_results(upload_results: List[Dict[str, Any]]) -> Any:
"""从上传响应中提取 chunk_method(兼容不同版本返回结构)"""
for item in upload_results:
data = item.get("data")
records = data if isinstance(data, list) else [data] if isinstance(data, dict) else []
for rec in records:
if not isinstance(rec, dict):
continue
if rec.get("chunk_method"):
return rec.get("chunk_method")
parser_cfg = rec.get("parser_config") or {}
if isinstance(parser_cfg, dict) and parser_cfg.get("chunk_method"):
return parser_cfg.get("chunk_method")
return None
@staticmethod
def _extract_uploaded_doc_ids(upload_results: List[Dict[str, Any]]) -> List[str]:
"""从上传结果中提取文档 ID"""
ids: List[str] = []
for item in upload_results:
data = item.get("data")
if isinstance(data, list):
for d in data:
if isinstance(d, dict) and d.get("id"):
ids.append(str(d.get("id")))
elif isinstance(data, dict) and data.get("id"):
ids.append(str(data.get("id")))
return ids
@staticmethod
def _build_parse_status_from_upload_results(upload_results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""根据上传返回构造解析状态(上传接口已触发解析,无需额外 parse API)"""
details: List[Dict[str, Any]] = []
for item in upload_results:
data = item.get("data")
records = data if isinstance(data, list) else [data] if isinstance(data, dict) else []
for rec in records:
if not isinstance(rec, dict):
continue
details.append(
{
"doc_id": rec.get("id"),
"name": rec.get("name") or rec.get("location"),
"run": rec.get("run"),
"chunk_method": rec.get("chunk_method")
or (rec.get("parser_config") or {}).get("chunk_method"),
}
)
return {
"ok": True,
"trigger": "upload_endpoint",
"message": "文档上传接口已触发解析流程,无需单独调用 parse API",
"count": len(details),
"details": details,
}
def _auto_parse_documents(self, dataset_id: str, doc_ids: List[str]) -> Dict[str, Any]:
"""调用官方 chunks 接口触发解析"""
if not doc_ids:
return {"ok": False, "message": "未提取到文档ID,无法触发解析", "count": 0, "details": []}
url = f"{self._base_url}/api/v1/datasets/{dataset_id}/chunks"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self._api_key}" if self._api_key else ""
}
print(f"更新知识库 URL: {url}") # 调试信息
print(f"更新配置: {config}") # 调试信息
"Authorization": f"Bearer {self._api_key}",
} if self._api_key else {"Content-Type": "application/json"}
payload = {"document_ids": doc_ids}
with httpx.Client(timeout=60) as client:
response = client.put(url, json=config, headers=headers)
response.raise_for_status()
result = response.json()
print(f"RAGFlow 更新响应: {result}") # 调试信息
return result
resp = client.post(url, headers=headers, json=payload)
if resp.status_code >= 400:
return {
"ok": False,
"trigger": "chunks_api",
"status": resp.status_code,
"message": resp.text,
"count": len(doc_ids),
"details": [{"doc_id": d} for d in doc_ids],
}
body: Any
try:
body = resp.json()
except Exception:
body = resp.text
return {
"ok": True,
"trigger": "chunks_api",
"count": len(doc_ids),
"details": [{"doc_id": d} for d in doc_ids],
"response": body,
}
def _list_document_ids(self, dataset_id: str) -> List[str]:
"""获取知识库中的全部文档 ID(用于覆盖更新)"""
if not self._base_url:
raise RuntimeError("未配置 ragflow.url")
if not dataset_id:
raise RuntimeError("dataset_id 为空,无法查询文档")
url = f"{self._base_url}/api/v1/datasets/{dataset_id}/documents"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
ids: List[str] = []
page = 1
page_size = 100
with httpx.Client(timeout=60) as client:
while True:
resp = client.get(url, headers=headers, params={"page": page, "page_size": page_size})
resp.raise_for_status()
body = resp.json()
data = body.get("data")
if isinstance(data, dict):
docs = data.get("docs") or data.get("list") or []
elif isinstance(data, list):
docs = data
else:
docs = []
if not docs:
break
for item in docs:
if isinstance(item, dict) and item.get("id"):
ids.append(str(item.get("id")))
if len(docs) < page_size:
break
page += 1
return ids
def _delete_documents(self, dataset_id: str, doc_ids: List[str]) -> Dict[str, Any]:
"""按 ID 删除文档"""
if not doc_ids:
return {"ok": True, "deleted": 0}
url = f"{self._base_url}/api/v1/datasets/{dataset_id}/documents"
headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {}
payload = {"ids": doc_ids}
with httpx.Client(timeout=60) as client:
resp = client.request("DELETE", url, headers=headers, json=payload)
resp.raise_for_status()
return resp.json()
def replace_documents(self, dataset_id: str, documents: List[Dict[str, Any]]) -> Dict[str, Any]:
"""覆盖更新:先删后传,避免“update 变新增”"""
ids = self._list_document_ids(dataset_id)
if ids:
self._delete_documents(dataset_id, ids)
return self.upload_documents(dataset_id, documents)
def update_table_retrieval_documents(self) -> Dict[str, Any]:
"""更新表名检索文档(仅文档内容)"""
if not self._table_retrieval_dataset_id:
raise RuntimeError("未配置 ragflow.table_retrieval_dataset_id,无法更新表名检索文档")
root = os.path.dirname(os.path.dirname(__file__))
tables_file = os.path.join(root, "config", "table_retrieval_prompts", "tables.json")
with open(tables_file, "r", encoding="utf-8") as f:
data = json.load(f)
tables = _extract_tables_map(data)
documents = [
{
"filename": f"{k}.txt",
"content": _dump_json_content({"table": k, "templates": v}),
}
for k, v in tables.items()
]
return self.replace_documents(self._table_retrieval_dataset_id, documents)
def update_sql_gen_documents(self) -> Dict[str, Any]:
"""更新 SQL 生成文档(仅文档内容)"""
if not self._sql_gen_dataset_id:
raise RuntimeError("未配置 ragflow.sql_gen_dataset_id,无法更新 SQL 生成文档")
root = os.path.dirname(os.path.dirname(__file__))
prompts_dir = os.path.join(root, "config", "sql_gen_prompts")
documents: List[Dict[str, Any]] = []
for name in os.listdir(prompts_dir):
if not name.endswith(".json"):
continue
path = os.path.join(prompts_dir, name)
with open(path, "r", encoding="utf-8") as f:
prompt = json.load(f)
table = prompt.get("table") or os.path.splitext(name)[0]
documents.append({"filename": f"{table}.txt", "content": _dump_json_content(prompt)})
return self.replace_documents(self._sql_gen_dataset_id, documents)
def upload_table_retrieval(self) -> Dict[str, Any]:
"""上传表名检索模板文档 - 直接上传整个 JSON 文件"""
@@ -210,22 +311,17 @@ class RagflowSync:
# 读取整个 JSON 文件内容
with open(tables_file, "r", encoding="utf-8") as f:
data = json.load(f)
# 将 JSON 内容转换为字符串
json_content = json.dumps(data, ensure_ascii=False, indent=2)
# 构建文档
doc = {
"content": f"# 表名检索模板库\n\n以下是所有表名检索模板的 JSON 数据:\n\n```json\n{json_content}\n```\n\n包含的表:{list(data.keys())}",
"metadata": {"type": "table_retrieval_templates", "format": "json"},
"title": "表名检索模板库",
"type": "table_template_library"
}
print(f"生成的表名检索文档: {doc}")
# 上传整个 JSON 文件内容
return self.upload_documents(self._table_retrieval_dataset_id, [doc])
tables = _extract_tables_map(data)
# 每个 key 一个文档,配合 One 解析时每个表单独成块
documents = [
{
"filename": f"{k}.txt",
"content": _dump_json_content({"table": k, "templates": v}),
}
for k, v in tables.items()
]
return self.upload_documents(self._table_retrieval_dataset_id, documents)
def upload_sql_gen(self) -> Dict[str, Any]:
"""上传 SQL 生成提示词文档"""
@@ -247,13 +343,7 @@ class RagflowSync:
with open(path, "r", encoding="utf-8") as f:
prompt = json.load(f)
table = prompt.get("table") or os.path.splitext(name)[0]
doc = {
"content": _build_sql_gen_document(table, prompt),
"metadata": {"table": table},
"title": f"SQL Prompt: {table}",
"type": "sql_prompt"
}
documents.append(doc)
print(f"生成的 SQL 提示词文档: {doc}")
json_content = _dump_json_content(prompt)
documents.append({"filename": f"{table}.txt", "content": json_content})
return self.upload_documents(self._sql_gen_dataset_id, documents)
+70 -2
View File
@@ -2,6 +2,9 @@ import json
import os
from typing import Any, Dict, Optional
from config import Config
from services.cache import NoopCache, RedisCache
class SqlPromptManager:
"""按表名读取 SQL 提示词"""
@@ -9,18 +12,83 @@ class SqlPromptManager:
def __init__(self, base_dir: Optional[str] = None):
root_dir = os.path.dirname(os.path.dirname(__file__))
self._base_dir = base_dir or os.path.join(root_dir, "config", "sql_gen_prompts")
self._cache = self._init_cache()
self._cache_ttl = self._get_cache_ttl()
@staticmethod
def _get_cache_ttl() -> int:
redis_cfg = Config.get_section("redis")
try:
return int(redis_cfg.get("sql_prompt_ttl", 600))
except Exception:
return 600
@staticmethod
def _init_cache():
redis_cfg = Config.get_section("redis")
enabled = str(redis_cfg.get("enabled", "false")).lower() in ("1", "true", "yes")
if not enabled:
return NoopCache()
# 优先使用完整 URL;否则使用 host/port/password/database 拼接
url = redis_cfg.get("url")
db = int(redis_cfg.get("db", redis_cfg.get("database", 0)))
if not url:
host = redis_cfg.get("host")
port = redis_cfg.get("port", "6379")
password = redis_cfg.get("password", "")
database = redis_cfg.get("database", str(db))
if host:
auth = f":{password}@" if password else ""
url = f"redis://{auth}{host}:{port}/{database}"
if not url:
return NoopCache()
try:
return RedisCache(url=url, db=db)
except Exception:
return NoopCache()
@staticmethod
def _safe_filename(name: str) -> str:
return name.replace("..", "").replace("/", "_").replace("\\", "_")
@staticmethod
def _cache_key(table_name: str, mtime: float) -> str:
return f"sql_prompt:{table_name}:{int(mtime)}"
def get_prompt(self, table_name: str) -> Optional[Dict[str, Any]]:
"""读取指定表的提示词 JSON"""
if not table_name:
return None
filename = self._safe_filename(table_name) + ".json"
safe_name = self._safe_filename(table_name)
filename = safe_name + ".json"
path = os.path.join(self._base_dir, filename)
if not os.path.exists(path):
return None
mtime = os.path.getmtime(path)
key = self._cache_key(safe_name, mtime)
cached = self._cache.get(key)
if cached:
try:
return json.loads(cached)
except Exception:
pass
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
prompt = json.load(f)
self._cache.set(key, json.dumps(prompt, ensure_ascii=False), self._cache_ttl)
return prompt
_GLOBAL_SQL_PROMPT_MANAGER: Optional[SqlPromptManager] = None
def get_sql_prompt_manager(base_dir: Optional[str] = None) -> SqlPromptManager:
"""获取全局 SqlPromptManager(单例)"""
global _GLOBAL_SQL_PROMPT_MANAGER
if _GLOBAL_SQL_PROMPT_MANAGER is None:
_GLOBAL_SQL_PROMPT_MANAGER = SqlPromptManager(base_dir=base_dir)
return _GLOBAL_SQL_PROMPT_MANAGER
+104
View File
@@ -0,0 +1,104 @@
from __future__ import annotations
import json
from datetime import datetime
from typing import Any, Dict, Optional
import pymysql
from config import Config
class StructuredLogger:
def __init__(self):
cfg = Config.get_section("logging_mysql")
self.enabled = str(cfg.get("enabled", "false")).lower() in ("1", "true", "yes")
self.host = cfg.get("host", "127.0.0.1")
self.port = int(cfg.get("port", 3306))
self.user = cfg.get("user", "root")
self.password = cfg.get("password", "")
self.database = cfg.get("database", "more_dots")
self.table = cfg.get("table", "structured_logs")
self.connect_timeout = int(cfg.get("connect_timeout", 5))
self._inited = False
def _get_conn(self):
return pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
charset="utf8mb4",
autocommit=True,
connect_timeout=self.connect_timeout,
)
def _ensure_table(self) -> None:
if self._inited or not self.enabled:
return
sql = f"""
CREATE TABLE IF NOT EXISTS {self.table} (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
trace_id VARCHAR(64) NOT NULL,
level VARCHAR(16) NOT NULL,
event VARCHAR(128) NOT NULL,
error_code VARCHAR(64) NULL,
payload JSON NULL,
created_at DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
"""
try:
with self._get_conn() as conn:
with conn.cursor() as cur:
cur.execute(sql)
self._inited = True
except Exception:
# 开发阶段容错,避免日志失败影响主流程
self.enabled = False
def log(self, level: str, event: str, trace_id: str, payload: Optional[Dict[str, Any]] = None, error_code: Optional[str] = None) -> None:
print(json.dumps({
"trace_id": trace_id,
"level": level,
"event": event,
"error_code": error_code,
"payload": payload or {},
"created_at": datetime.now().isoformat(),
}, ensure_ascii=False))
if not self.enabled:
return
self._ensure_table()
if not self.enabled:
return
insert_sql = f"INSERT INTO {self.table}(trace_id, level, event, error_code, payload, created_at) VALUES(%s,%s,%s,%s,%s,%s)"
try:
with self._get_conn() as conn:
with conn.cursor() as cur:
cur.execute(
insert_sql,
(
trace_id,
level,
event,
error_code,
json.dumps(payload or {}, ensure_ascii=False),
datetime.now(),
),
)
except Exception:
# 开发阶段容错,避免日志失败影响主流程
return
_GLOBAL_STRUCTURED_LOGGER: Optional[StructuredLogger] = None
def get_structured_logger() -> StructuredLogger:
global _GLOBAL_STRUCTURED_LOGGER
if _GLOBAL_STRUCTURED_LOGGER is None:
_GLOBAL_STRUCTURED_LOGGER = StructuredLogger()
return _GLOBAL_STRUCTURED_LOGGER
+13 -5
View File
@@ -11,6 +11,7 @@ class TemplateMatcher:
self._ragflow = RagflowClient()
cfg = Config.get_section("ragflow")
self._dataset_id = (cfg.get("table_retrieval_dataset_id") or "").strip()
self._top_k = int(cfg.get("retrieval_top_k", 3))
def _validate(self) -> None:
if not self._dataset_id:
@@ -20,17 +21,24 @@ class TemplateMatcher:
"""返回匹配的表名与原始响应"""
self._validate()
try:
response = self._ragflow.retrieve(normalized_text, top_k=3, dataset_id=self._dataset_id)
response = self._ragflow.retrieve(normalized_text, top_k=self._top_k, dataset_id=self._dataset_id)
except Exception as e:
return {"table_name": None, "raw": {"error": str(e)}}
candidates = []
data = response.get("data") if isinstance(response, dict) else None
records = []
if isinstance(data, list):
for item in data:
table_name = extract_table_name(item)
if table_name:
candidates.append(table_name)
records = data
elif isinstance(data, dict):
chunks = data.get("chunks")
if isinstance(chunks, list):
records = chunks
for item in records:
table_name = extract_table_name(item)
if table_name:
candidates.append(table_name)
matched = candidates[0] if candidates else None
return {"table_name": matched, "raw": response}