Files
group_fqcd_jr/tools/publish_advisor_demo_config.py
T
张胜宇 9675df8453 chore(sync): zsy_developcc 全量同步至 qyqy_develop(W26 口径)
- 分支内容对齐 qyqy_develop b6ec3aa,树完全一致(同步后 git diff 为空)
- 覆盖本轮全部交付:客服 Agent 重构(安全路由 / 五出口 / 记忆与画像 / RAG 全链路)
  + 开发文档 62 份编号体系(D1.1 v1.17 索引)
  + 新增 D2.10-客服Agent端到端答辩文档-2026-09-21.html
- 基线:e239eb7(2026-09-17 品牌口径统一快照),本提交为其直接后继
2026-09-21 21:26:30 +08:00

195 lines
6.4 KiB
Python

"""Publish the advisor Agent tool allowlist for an isolated demo environment.
The active ``config_release`` is environment data. A new demo database has no
release by default, so AdvisorAgent tool calls would correctly fail closed even
though the Agent code is present. This script creates a new release, carries
forward every existing item, and replaces only advisor tool entries.
"""
from __future__ import annotations
import argparse
import asyncio
import datetime as dt
import json
import sys
import uuid
from pathlib import Path
from typing import Any
import asyncmy
import httpx
import jwt
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.main import create_app # noqa: E402
ADMIN_ID = "9003"
AGENT_TYPE = "advisor"
INTENT_TOOLS: dict[str, tuple[str, ...]] = {
"fund_quote": ("query_fund_quote",),
"investment_goal": ("query_investment_goal",),
"portfolio_analysis": ("analyze_portfolio",),
"asset_allocation": ("generate_asset_allocation",),
"product_recommend": ("recommend_products",),
"comparison": ("compare_products",),
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Publish advisor demo tool allowlist")
parser.add_argument("--apply", action="store_true", help="Create, review, and activate release")
return parser.parse_args()
def issue_token(subject: str) -> str:
settings = get_settings()
private_key = Path(settings.jwt_private_key_path).read_text(encoding="utf-8")
now = dt.datetime.now(dt.UTC)
return jwt.encode(
{
"sub": subject,
"iss": settings.jwt_issuer,
"aud": settings.jwt_audience,
"exp": now + dt.timedelta(minutes=30),
"nbf": now - dt.timedelta(seconds=5),
"jti": str(uuid.uuid4()),
},
private_key,
algorithm="RS256",
)
async def active_items() -> list[dict[str, Any]]:
settings = get_settings()
dsn = settings.mysql_dsn.split("://", 1)[1]
credentials, location = dsn.split("@", 1)
user, password = credentials.split(":", 1)
host_port, database = location.split("/", 1)
host, _, port = host_port.partition(":")
connection = await asyncmy.connect(
host=host, port=int(port or 3306), user=user, password=password, db=database
)
try:
cursor = connection.cursor()
await cursor.execute(
"""
SELECT i.namespace, i.config_key, i.value_json, i.schema_version
FROM platform_config_item i
JOIN config_release r ON r.id = i.release_id
WHERE r.status = 'active'
"""
)
rows = await cursor.fetchall()
finally:
connection.close()
return [
{
"namespace": namespace,
"item_key": key,
"value_json": json.loads(value) if isinstance(value, str) else value,
"schema_version": schema_version,
}
for namespace, key, value, schema_version in rows
]
async def request(
client: httpx.AsyncClient,
path: str,
*,
auth: dict[str, str],
payload: dict[str, object] | None = None,
if_match: str | None = None,
) -> httpx.Response:
headers = {**auth, "Idempotency-Key": uuid.uuid4().hex}
if if_match:
headers["If-Match"] = if_match
return await client.post(path, json=payload, headers=headers)
async def etag(client: httpx.AsyncClient, path: str, auth: dict[str, str]) -> str | None:
return (await client.get(path, headers=auth)).headers.get("ETag")
def advisor_items() -> list[dict[str, object]]:
return [
{
"namespace": "agent_tools",
"item_key": f"{AGENT_TYPE}:{intent}",
"value_json": {"allowed_tools": list(tools)},
"schema_version": "1",
}
for intent, tools in INTENT_TOOLS.items()
]
async def publish() -> int:
app = create_app()
auth = {"Authorization": f"Bearer {issue_token(ADMIN_ID)}"}
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client:
inherited = await active_items()
replacement_keys = {("agent_tools", str(item["item_key"])) for item in advisor_items()}
carried = [
item
for item in inherited
if (str(item["namespace"]), str(item["item_key"])) not in replacement_keys
]
created = await request(
client,
"/api/v1/admin/config-releases",
auth=auth,
payload={
"release_no": f"advisor-demo-{uuid.uuid4().hex[:12]}",
"title": "Advisor demo tool allowlist",
"change_summary": "Publish advisor tool allowlists for the isolated demo environment",
},
)
if created.status_code != 201:
print(f"create release failed: {created.status_code} {created.text[:240]}")
return 1
release_id = int(created.json()["data"]["id"])
base = f"/api/v1/admin/config-releases/{release_id}"
for item in [*carried, *advisor_items()]:
response = await request(
client, f"{base}/platform-config-items", auth=auth, payload=item
)
if response.status_code != 201:
print(f"write {item['namespace']}/{item['item_key']} failed: {response.status_code}")
return 1
for suffix, payload in (
("validations", {}),
("reviews", {"decision": "approved", "comment": "isolated demo setup"}),
("activations", {}),
):
response = await request(
client,
f"{base}/{suffix}",
auth=auth,
payload=payload,
if_match=await etag(client, base, auth),
)
if response.status_code not in (200, 201):
print(f"{suffix} failed: {response.status_code} {response.text[:240]}")
return 1
print(f"published advisor demo release id={release_id}")
return 0
def main() -> int:
args = parse_args()
for intent, tools in INTENT_TOOLS.items():
print(f"advisor:{intent} -> {', '.join(tools)}")
if not args.apply:
print("dry run only; pass --apply to publish a new config release")
return 0
return asyncio.run(publish())
if __name__ == "__main__":
sys.exit(main())