- Added new modules for advisor compliance, KYC sessions, and script templates, enhancing the advisor agent's capabilities. - Implemented a comprehensive API structure under the `/api/advisor-agent` prefix, ensuring clear organization and access to new features. - Established database models and repositories for compliance rules and KYC sessions, facilitating robust data management. - Integrated exception handling and response models to improve error management and user feedback. - Updated settings to include new configurations for compliance and KYC features, ensuring flexibility and adaptability. This update significantly expands the advisor agent's functionality, providing essential tools for compliance and customer interaction while maintaining a structured API design.
223 lines
7.0 KiB
Python
223 lines
7.0 KiB
Python
"""Import script templates from the Sprint 2 Markdown dataset."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import sys
|
|
from dataclasses import dataclass
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from app.advisor_db import AgentSessionLocal
|
|
from app.model.entities_advisor import ScriptTemplate
|
|
from app.model.advisor_schemas import ComplianceCheckRequest
|
|
from app.service.compliance_check_service import ComplianceCheckService
|
|
|
|
DEFAULT_DATASET = ROOT / "docs" / "开发文档" / "29-Sprint2首批话术模板数据集.md"
|
|
ACTOR_ID = "seed:template_test_data"
|
|
REQUIRED_HEADERS = [
|
|
"序号",
|
|
"scene",
|
|
"customer_type",
|
|
"title",
|
|
"content",
|
|
"tags",
|
|
"is_active",
|
|
"审核状态",
|
|
"审核人",
|
|
"备注",
|
|
]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SeedScriptTemplate:
|
|
sequence: int
|
|
scene: str
|
|
customer_type: str | None
|
|
title: str
|
|
content: str
|
|
tags: list[str]
|
|
is_active: bool
|
|
review_status: str
|
|
reviewer: str | None
|
|
note: str
|
|
|
|
@property
|
|
def is_approved(self) -> bool:
|
|
return self.review_status == "approved"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ImportResult:
|
|
total: int
|
|
created: int
|
|
updated: int
|
|
|
|
|
|
def load_templates_from_markdown(path: Path = DEFAULT_DATASET) -> list[SeedScriptTemplate]:
|
|
rows = _read_markdown_table(path)
|
|
templates: list[SeedScriptTemplate] = []
|
|
for row in rows:
|
|
sequence = int(row["序号"])
|
|
scene = row["scene"].strip()
|
|
title = row["title"].strip()
|
|
content = row["content"].strip()
|
|
if not scene:
|
|
raise ValueError(f"Missing scene at row {sequence}")
|
|
if not title:
|
|
raise ValueError(f"Missing title at row {sequence}")
|
|
if not content:
|
|
raise ValueError(f"Missing content at row {sequence}")
|
|
templates.append(
|
|
SeedScriptTemplate(
|
|
sequence=sequence,
|
|
scene=scene,
|
|
customer_type=row["customer_type"].strip() or None,
|
|
title=title,
|
|
content=content,
|
|
tags=_parse_tags(row["tags"]),
|
|
is_active=_parse_bool(row["is_active"]),
|
|
review_status=_normalize_review_status(row["审核状态"], row["备注"]),
|
|
reviewer=row["审核人"].strip() or None,
|
|
note=row["备注"].strip(),
|
|
)
|
|
)
|
|
return templates
|
|
|
|
|
|
def import_templates_from_markdown(
|
|
path: Path = DEFAULT_DATASET,
|
|
*,
|
|
actor_id: str = ACTOR_ID,
|
|
) -> ImportResult:
|
|
templates = load_templates_from_markdown(path)
|
|
compliance_service = ComplianceCheckService()
|
|
created = 0
|
|
updated = 0
|
|
|
|
with AgentSessionLocal() as session:
|
|
for seed_template in templates:
|
|
check_result = compliance_service.check_text(
|
|
ComplianceCheckRequest(text=seed_template.content, scene=seed_template.scene)
|
|
)
|
|
if check_result.risk_level == "BLOCK":
|
|
raise ValueError(f"Template row {seed_template.sequence} failed BLOCK compliance check")
|
|
|
|
existing = (
|
|
session.query(ScriptTemplate)
|
|
.filter(
|
|
ScriptTemplate.created_by == actor_id,
|
|
ScriptTemplate.scene == seed_template.scene,
|
|
ScriptTemplate.title == seed_template.title,
|
|
)
|
|
.one_or_none()
|
|
)
|
|
values = {
|
|
"customer_type": seed_template.customer_type,
|
|
"content": seed_template.content,
|
|
"tags": seed_template.tags,
|
|
"is_active": seed_template.is_active,
|
|
"is_approved": seed_template.is_approved,
|
|
"approved_by": seed_template.reviewer if seed_template.is_approved else None,
|
|
"approved_at": datetime.now(timezone.utc).replace(tzinfo=None) if seed_template.is_approved else None,
|
|
"updated_by": actor_id,
|
|
}
|
|
if existing is None:
|
|
session.add(
|
|
ScriptTemplate(
|
|
scene=seed_template.scene,
|
|
title=seed_template.title,
|
|
created_by=actor_id,
|
|
version=1,
|
|
usage_count=0,
|
|
**values,
|
|
)
|
|
)
|
|
created += 1
|
|
else:
|
|
if _content_changed(existing, values):
|
|
existing.version += 1
|
|
for key, value in values.items():
|
|
setattr(existing, key, value)
|
|
updated += 1
|
|
session.commit()
|
|
|
|
return ImportResult(total=len(templates), created=created, updated=updated)
|
|
|
|
|
|
def _content_changed(existing: ScriptTemplate, values: dict) -> bool:
|
|
return any(
|
|
[
|
|
existing.customer_type != values["customer_type"],
|
|
existing.content != values["content"],
|
|
existing.tags != values["tags"],
|
|
existing.is_active != values["is_active"],
|
|
existing.is_approved != values["is_approved"],
|
|
]
|
|
)
|
|
|
|
|
|
def _read_markdown_table(path: Path) -> list[dict[str, str]]:
|
|
if not path.exists():
|
|
raise FileNotFoundError(path)
|
|
|
|
table_lines = [
|
|
line.strip()
|
|
for line in path.read_text(encoding="utf-8").splitlines()
|
|
if line.strip().startswith("|") and line.strip().endswith("|")
|
|
]
|
|
if len(table_lines) < 3:
|
|
raise ValueError("Markdown table is missing or empty")
|
|
|
|
headers = _split_markdown_row(table_lines[0])
|
|
if headers != REQUIRED_HEADERS:
|
|
raise ValueError(f"Unexpected table headers: {headers}")
|
|
|
|
rows: list[dict[str, str]] = []
|
|
for line in table_lines[2:]:
|
|
cells = _split_markdown_row(line)
|
|
if len(cells) != len(headers):
|
|
raise ValueError(f"Column count mismatch: {line}")
|
|
rows.append(dict(zip(headers, cells, strict=True)))
|
|
return rows
|
|
|
|
|
|
def _split_markdown_row(line: str) -> list[str]:
|
|
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
|
|
|
|
def _parse_tags(value: str) -> list[str]:
|
|
return [tag.strip() for tag in value.split(";") if tag.strip()]
|
|
|
|
|
|
def _parse_bool(value: str) -> bool:
|
|
normalized = value.strip().lower()
|
|
if normalized == "true":
|
|
return True
|
|
if normalized == "false":
|
|
return False
|
|
raise ValueError(f"Expected true/false, got: {value}")
|
|
|
|
|
|
def _normalize_review_status(value: str, note: str) -> str:
|
|
if "已审核" in value:
|
|
return "approved"
|
|
if "测试数据" in value or "测试数据" in note:
|
|
return "test_data"
|
|
return "pending"
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Import Sprint 2 script templates.")
|
|
parser.add_argument("--dataset", type=Path, default=DEFAULT_DATASET)
|
|
args = parser.parse_args()
|
|
result = import_templates_from_markdown(args.dataset)
|
|
print(f"Imported script templates: total={result.total}, created={result.created}, updated={result.updated}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|