docs: add phase one integration gate
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
"""只读核验一期客服 Agent 的数据库、知识库和向量运行环境。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
# 直接执行 tools 脚本时优先解析当前工作树,避免误导入相邻 worktree 的 app 包。
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from app.core.config import get_settings # noqa: E402
|
||||
from app.infrastructure.db import SessionFactory # noqa: E402
|
||||
|
||||
COLLECTIONS = (
|
||||
"fin_faq_collection",
|
||||
"fin_product_collection",
|
||||
"fin_policy_collection",
|
||||
)
|
||||
EXPECTED_KNOWLEDGE_COUNTS = {
|
||||
"fin_faq_collection": 15,
|
||||
"fin_product_collection": 26,
|
||||
"fin_policy_collection": 11,
|
||||
}
|
||||
|
||||
|
||||
def _failures(values: Iterable[str]) -> list[str]:
|
||||
"""统一收集失败项,保证脚本最后一次性输出可操作结果。"""
|
||||
return [value for value in values if value]
|
||||
|
||||
|
||||
async def _database_checks() -> list[str]:
|
||||
"""只读检查管理员、Embedding 端点、配置版本与知识发布状态。"""
|
||||
failures: list[str] = []
|
||||
async with SessionFactory() as session:
|
||||
admin = await session.execute(text("""
|
||||
SELECT id FROM sys_user
|
||||
WHERE id = 9003 AND user_no = 'SYS-KNOWLEDGE-ADMIN'
|
||||
AND user_type IN ('employee', 'admin')
|
||||
AND status IN ('正常', 'active')
|
||||
"""))
|
||||
if admin.scalar_one_or_none() is None:
|
||||
failures.append("缺少启用的 SYS-KNOWLEDGE-ADMIN(9003)")
|
||||
|
||||
endpoint = await session.execute(text("""
|
||||
SELECT endpoint_code, model_name, secret_ref, capabilities, status
|
||||
FROM model_endpoint_config
|
||||
WHERE endpoint_code = 'knowledge-embedding-qwen-v3'
|
||||
AND status = 'active'
|
||||
"""))
|
||||
endpoint_row = endpoint.mappings().first()
|
||||
if endpoint_row is None:
|
||||
failures.append("Qwen Embedding 端点未激活")
|
||||
else:
|
||||
if endpoint_row["model_name"] != "text-embedding-v3":
|
||||
failures.append("Embedding 模型不是 text-embedding-v3")
|
||||
if not str(endpoint_row["secret_ref"]).startswith("env:"):
|
||||
failures.append("Embedding 密钥不是 env: 引用")
|
||||
|
||||
release = await session.execute(text("""
|
||||
SELECT id FROM config_release
|
||||
WHERE release_no = 'customer-service-phase1-public-kb-v1'
|
||||
AND status = 'active'
|
||||
"""))
|
||||
release_id = release.scalar_one_or_none()
|
||||
if release_id is None:
|
||||
failures.append("一期客服公开检索配置未激活")
|
||||
else:
|
||||
tools = await session.execute(text("""
|
||||
SELECT config_key, value_json
|
||||
FROM platform_config_item
|
||||
WHERE release_id = :release_id AND namespace = 'agent_tools'
|
||||
"""), {"release_id": release_id})
|
||||
configured: dict[str, Any] = {}
|
||||
for row in tools.mappings():
|
||||
raw_value = row["value_json"]
|
||||
configured[str(row["config_key"])] = (
|
||||
json.loads(raw_value) if isinstance(raw_value, str) else raw_value
|
||||
)
|
||||
expected_keys = {
|
||||
"customer_service:public_knowledge",
|
||||
"customer_service:faq",
|
||||
"customer_service:product_inquiry",
|
||||
"customer_service:policy_explain",
|
||||
}
|
||||
if set(configured) != expected_keys:
|
||||
failures.append("一期客服工具白名单缺失或包含额外意图")
|
||||
if any(value != {"allowed_tools": ["query_knowledge"]} for value in configured.values()):
|
||||
failures.append("一期客服工具白名单不是仅 query_knowledge")
|
||||
|
||||
knowledge = await session.execute(text("""
|
||||
SELECT milvus_collection, review_status, status, COUNT(*) AS count
|
||||
FROM fin_knowledge_meta
|
||||
GROUP BY milvus_collection, review_status, status
|
||||
"""))
|
||||
actual: dict[str, int] = {}
|
||||
for row in knowledge.mappings():
|
||||
if row["review_status"] == "published" and row["status"] == "active":
|
||||
actual[str(row["milvus_collection"])] = int(row["count"])
|
||||
if actual != EXPECTED_KNOWLEDGE_COUNTS:
|
||||
failures.append(f"公开知识数量不符合预期: {actual}")
|
||||
return failures
|
||||
|
||||
|
||||
async def _milvus_checks() -> list[str]:
|
||||
"""只读检查三类集合的存在、维度、主键和行数。"""
|
||||
settings = get_settings()
|
||||
failures: list[str] = []
|
||||
try:
|
||||
from pymilvus import AsyncMilvusClient # type: ignore[import-untyped]
|
||||
|
||||
client: Any = AsyncMilvusClient(
|
||||
uri=settings.resolved_milvus_uri, token=settings.milvus_token or None
|
||||
)
|
||||
for collection in COLLECTIONS:
|
||||
if not await client.has_collection(collection_name=collection):
|
||||
failures.append(f"集合不存在: {collection}")
|
||||
continue
|
||||
description = await client.describe_collection(collection_name=collection)
|
||||
fields = {field["name"]: field for field in description.get("fields", [])}
|
||||
embedding = fields.get("embedding", {})
|
||||
if embedding.get("params", {}).get("dim") != 1024:
|
||||
failures.append(f"集合 {collection} 不是 1024 维")
|
||||
if not fields.get("knowledge_id", {}).get("is_primary"):
|
||||
failures.append(f"集合 {collection} 缺少 knowledge_id 主键")
|
||||
await client.load_collection(collection_name=collection)
|
||||
stats = await client.get_collection_stats(collection_name=collection)
|
||||
expected = EXPECTED_KNOWLEDGE_COUNTS[collection]
|
||||
if int(stats.get("row_count", -1)) != expected:
|
||||
failures.append(f"集合 {collection} 行数不符合预期: {stats}")
|
||||
await client.close()
|
||||
except Exception as exc:
|
||||
failures.append(f"Milvus 检查失败: {type(exc).__name__}")
|
||||
return failures
|
||||
|
||||
|
||||
async def verify() -> int:
|
||||
"""执行所有只读门禁并返回适合 CI 的退出码。"""
|
||||
failures = _failures([*(await _database_checks()), *(await _milvus_checks())])
|
||||
settings = get_settings()
|
||||
print({
|
||||
"milvus_uri_mode": "local" if settings.milvus_local_uri else "remote",
|
||||
"knowledge_embedding_endpoint": settings.knowledge_embedding_endpoint_code,
|
||||
"expected_public_records": sum(EXPECTED_KNOWLEDGE_COUNTS.values()),
|
||||
"failures": failures,
|
||||
})
|
||||
return 1 if failures else 0
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""命令行入口;保留无参数形式,便于整合测试直接调用。"""
|
||||
argparse.ArgumentParser(description=__doc__).parse_args()
|
||||
raise SystemExit(asyncio.run(verify()))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user