feat:新增投顾agent和nl2sqlagent
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
"""基金业务语义目录:把自然语言术语映射为受权威 Schema 约束的数据库概念。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
DEFAULT_CATALOG_PATH = Path(__file__).with_name("semantic_catalog.json")
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def _load_catalog_cached(path: str) -> dict[str, Any]:
|
||||
payload = json.loads(Path(path).read_text(encoding="utf-8"))
|
||||
return validate_semantic_catalog(payload)
|
||||
|
||||
|
||||
def load_semantic_catalog(path: str | Path | None = None) -> dict[str, Any]:
|
||||
"""加载可替换的基金业务语义目录。"""
|
||||
return _load_catalog_cached(str(Path(path or DEFAULT_CATALOG_PATH).resolve()))
|
||||
|
||||
|
||||
def validate_semantic_catalog(catalog: dict[str, Any]) -> dict[str, Any]:
|
||||
"""校验语义目录结构,保证目录错误在加载阶段暴露。"""
|
||||
if not isinstance(catalog, dict) or not str(catalog.get("version", "")).strip():
|
||||
raise ValueError("语义目录必须包含 version")
|
||||
terms = catalog.get("terms")
|
||||
if not isinstance(terms, list):
|
||||
raise ValueError("语义目录必须包含 terms 数组")
|
||||
for item in terms:
|
||||
if not isinstance(item, dict) or not str(item.get("term", "")).strip():
|
||||
raise ValueError("语义目录 term 无效")
|
||||
if "enabled" in item and not isinstance(item["enabled"], bool):
|
||||
raise ValueError("语义目录 enabled 必须是布尔值")
|
||||
for key in ("aliases", "tables", "fields"):
|
||||
if not isinstance(item.get(key), list) or not item[key]:
|
||||
raise ValueError(f"语义目录 {key} 无效")
|
||||
relationships = catalog.get("relationships", [])
|
||||
if not isinstance(relationships, list):
|
||||
raise ValueError("语义目录 relationships 必须是数组")
|
||||
for item in relationships:
|
||||
if not isinstance(item, dict) or not all(
|
||||
str(item.get(key, "")).strip() for key in ("left", "right", "meaning")
|
||||
):
|
||||
raise ValueError("语义目录关联关系无效")
|
||||
return catalog
|
||||
|
||||
|
||||
def clear_semantic_catalog_cache() -> None:
|
||||
"""清理目录缓存,使下一次请求重新读取文件。"""
|
||||
_load_catalog_cached.cache_clear()
|
||||
|
||||
|
||||
def refresh_semantic_catalog(path: str | Path | None = None) -> dict[str, Any]:
|
||||
"""校验并刷新语义目录;新目录无效时保留当前缓存。"""
|
||||
catalog_path = Path(path or DEFAULT_CATALOG_PATH).resolve()
|
||||
payload = json.loads(catalog_path.read_text(encoding="utf-8"))
|
||||
catalog = validate_semantic_catalog(payload)
|
||||
previous = get_semantic_catalog_info(catalog_path)
|
||||
_load_catalog_cached.cache_clear()
|
||||
_load_catalog_cached(str(catalog_path))
|
||||
current = _build_semantic_catalog_info(catalog, catalog_path)
|
||||
return {
|
||||
**current,
|
||||
"previous_version": previous["version"],
|
||||
"changed": previous["digest"] != current["digest"],
|
||||
}
|
||||
|
||||
|
||||
def _build_semantic_catalog_info(catalog: dict[str, Any], path: Path) -> dict[str, Any]:
|
||||
"""生成不含业务明细的目录摘要。"""
|
||||
digest = hashlib.sha256(
|
||||
json.dumps(catalog, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
).hexdigest()
|
||||
enabled_count = sum(item.get("enabled", True) for item in catalog["terms"])
|
||||
return {
|
||||
"version": catalog["version"],
|
||||
"term_count": len(catalog["terms"]),
|
||||
"enabled_term_count": enabled_count,
|
||||
"disabled_term_count": len(catalog["terms"]) - enabled_count,
|
||||
"relationship_count": len(catalog.get("relationships", [])),
|
||||
"digest": digest,
|
||||
"path": str(path),
|
||||
}
|
||||
|
||||
|
||||
def get_semantic_catalog_info(path: str | Path | None = None) -> dict[str, Any]:
|
||||
"""返回不含业务明细的语义目录摘要。"""
|
||||
catalog = load_semantic_catalog(path)
|
||||
return _build_semantic_catalog_info(catalog, Path(path or DEFAULT_CATALOG_PATH).resolve())
|
||||
|
||||
|
||||
def resolve_semantics(question: str, *, catalog: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
"""根据问题匹配业务术语,返回未经过 Schema 过滤的候选语义。"""
|
||||
text = (question or "").strip()
|
||||
catalog = catalog or load_semantic_catalog()
|
||||
matched = [
|
||||
item
|
||||
for item in catalog["terms"]
|
||||
if item.get("enabled", True)
|
||||
if any(alias in text for alias in item.get("aliases", []))
|
||||
]
|
||||
tables = sorted({table for item in matched for table in item["tables"]})
|
||||
fields = sorted({field for item in matched for field in item["fields"]})
|
||||
metrics = [
|
||||
{
|
||||
"term": item["term"],
|
||||
"field": item["fields"][0],
|
||||
"hint": item.get("metric_hint", item.get("value_hint", "")),
|
||||
}
|
||||
for item in matched
|
||||
if "metric_hint" in item
|
||||
]
|
||||
return {
|
||||
"terms": [item["term"] for item in matched],
|
||||
"tables": tables,
|
||||
"fields": fields,
|
||||
"metrics": metrics,
|
||||
"relationships": list(catalog.get("relationships", [])),
|
||||
}
|
||||
|
||||
|
||||
def build_semantic_context(question: str, schema: dict[str, Any]) -> dict[str, Any]:
|
||||
"""只保留当前权威 Schema 中存在的表、字段和关联,避免语义目录越权扩张。"""
|
||||
resolved = resolve_semantics(question)
|
||||
schema_tables = {
|
||||
str(item.get("table_name"))
|
||||
for item in schema.get("tables", [])
|
||||
if item.get("table_name")
|
||||
}
|
||||
schema_fields = {
|
||||
(str(item.get("table_name")), str(item.get("field_name")))
|
||||
for item in schema.get("columns", [])
|
||||
if item.get("table_name") and item.get("field_name")
|
||||
}
|
||||
tables = sorted(set(resolved["tables"]) & schema_tables)
|
||||
fields = sorted(
|
||||
{
|
||||
field
|
||||
for table, field in schema_fields
|
||||
if table in tables and field in set(resolved["fields"])
|
||||
}
|
||||
)
|
||||
metrics = [
|
||||
metric
|
||||
for metric in resolved["metrics"]
|
||||
if any(metric["field"] == field for _, field in schema_fields if _ in tables)
|
||||
]
|
||||
relationships = [
|
||||
relation
|
||||
for relation in resolved["relationships"]
|
||||
if relation["left"].split(".", 1)[0] in tables
|
||||
and relation["right"].split(".", 1)[0] in tables
|
||||
]
|
||||
return {
|
||||
"terms": resolved["terms"],
|
||||
"tables": tables,
|
||||
"fields": fields,
|
||||
"metrics": metrics,
|
||||
"relationships": relationships,
|
||||
}
|
||||
Reference in New Issue
Block a user