merge: 合并主干 qyqy_develop(PR #7 之后)并对齐两套投影实现
共同祖先 bbf623a;主干 54 个提交、118 个文件;本线 25 个文件;9 个冲突文件。 主干这次把 **ZSY 的整条投影实现合进来了(PR #7)**,而本线此前的提交正是 移植并修正同一套代码 —— 因此冲突的本质是"同一功能两份实现并存",取舍错了会把 已修好的缺陷又带回来。逐项取舍与理由见 `docs/39-主干合并对策记录.md`。 ## 取舍(9 个冲突) 取本线: - `app/infrastructure/milvus_profile_projection.py` —— 主干是 ZSY 原版,含两处必炸点: ① `customer_id` 要求 int 而本仓所有生产者都写 `str` ⇒ 每个事件必然失败; ② 不可投影的 `memory_key` 直接 raise ⇒ 一条 `constraint:` 记忆毒死整客户整批。 本线版已放宽为「接受纯数字字符串」与「跳过并留痕」。 - `memory_sync_outbox_worker.py` / `conversation_privacy.py` / `risk_questionnaire.py` —— 代码逐行一致,仅注释与说明文字详略不同(`risk_questionnaire.py` 两边**独立做了 完全相同的修复**,都改成 re-export `app.model.profile`)。 - 两个投影测试文件 —— 本线是他那份的**超集**(4→10、4→5 例,包含他全部用例)。 两边合并: - `app/worker/runtime.py`:`__init__` 两边各加一个参数,都要。 - `app/service/agent/implementations/customer_service.py`:import 取并集; `COMPANY` 取主干的「奶龙基金责任有限公司」("奶龙"是本项目实际品牌名,主干多处出现), `HOTLINE`/`SERVICE_HOURS` **取本线的修复**(主干仍是占位符 `400-XXX-XXXX`, 本线已改为引用 `customer_service_rules` 的唯一来源 —— 这是 A1 缺陷修复, 否则同一客服给客户两个不同号码)。 - `AGENTS.md`:表数/Agent 清单取主干(90/89、7 个 Agent),本线的 `-X utf8` 与两条 outbox 易错点保留,测试基线按合并后实测重算。 ## 消费端只保留一套(本次最重要的一处) 合并后曾出现**两套消费者读同一个 `memory_sync_outbox`**:`__main__.py`(PR #7) 与 `runtime.consume_profile_projections()`(本线),而**两者的 neo4j handler 不同** —— 前者用 ZSY 的 `Neo4jProfileProjection`(按客户各建私有节点), 后者用主干 `ProfileGraphProjectionService`(共享 tag 节点、只投影已确认事实)。 同一事件被谁领到结果不定,等于"同一事实在图里有两种说法",正是**方案 A 要避免的状态**。 现只保留 runtime 那一套(带 `memory_sources` 兜底、neo4j 复用主干服务), 删除 `__main__.py` 的重复接线;装配入口职责仍在该文件(注入 `relationships` / `projection_cleaner`),Milvus 客户端由 `bootstrap` 工厂惰性构造、缺配置时显式降级。 副作用:`app/infrastructure/neo4j_profile_projection.py` 不再被生产代码引用,成为 **死代码**(本线未删,属架构师线,其单测仍在)—— 待架构师决定删或明确分工。 ## 顺带修掉的 3 个继承缺陷(主干同样存在,PR #7 后未整套复跑故未发现) 1. `tools/seed_test_rbac.py` **少建 `review_t`(9004)账号** —— 两个集成测试都依赖它 ("账号存在但无权限应返回 200 空集而非 404"、`PLACEHOLDER_ACCOUNTS`)。 同时把用户↔角色绑定从 `zip(..., strict=True)` 改为**显式配对表**:原写法隐含 "USERS 与 ROLES 一一对应",一加不绑角色的账号就 ValueError、整个种子跑不完 (commit 在最后,外部表现是"什么都没发生")。 2. `CustomerProfileCandidateService._write_profile_snapshot` **漏写 `current_customer_id`** —— 该列不是生成列而是普通可空列 + 唯一键 `uk_profile_snapshot_current`, 不写则唯一键形同虚设(多个 NULL 不冲突),且旧当前版本也没清该列、补写就会撞键。 现旧值置 None、新值显式写入(与 `ProfileGenerationService._clear_current` 一致)。 3. 集成测试前置未记录 —— 13 个登录/RBAC 用例因 401 而红,实为"测试账号不存在", 跑 `seed_test_rbac.py` + `set_user_password.py` 后转绿;已在 `AGENTS.md` 记明, 避免被误判成代码缺陷。 ## 文档 - 新增 `docs/39-主干合并对策记录.md`(逐文件取舍 + 理由 + 遗留) - `docs/37` 订正一处过时说法:曾写 `current_customer_id` 无人使用且故意不映射, 实际 `app/model/profile.py` 已映射且有人使用(详见该文档 §6.2 的订正块) - 文档编号:主干已占 29–36,本线两份文档让号至 `docs/37`、`docs/38` ## 验证(合并后实测) - `pytest tests`(全量)→ `2 failed, 1396 passed, 2 skipped` - `pytest tests/integration` → `102 passed, 1 skipped`(修上述 1、2 后从 15 failed 归零) - `mypy app` → `Success: no issues found in 245 source files` - `tools/audit_schema.py` → 89 张业务表无缺失/意外(未改动任何表结构) - `tools/check_authoritative_docs.py` → 52 份文档无编号冲突 - `tools/check_rbac_seed_consistency.py` → 通过 那 2 个失败是既有环境项(`test_offsite_document_recognition_adapter.py` 断言请求体 中文原文而 httpx 序列化成 `\uXXXX`),与本次合并无关。
This commit is contained in:
@@ -27,7 +27,7 @@ from app.core.contracts import (
|
||||
RequestContext,
|
||||
SourceReference,
|
||||
)
|
||||
from app.core.customer_service_rules import CONTACT_HOURS, CONTACT_PHONE
|
||||
from app.core.customer_service_rules import CONTACT_HOURS, CONTACT_PHONE, route_message
|
||||
from app.core.errors import ForbiddenAgentError
|
||||
from app.service.agent.base import BaseAgent
|
||||
from app.service.model_gateway import DatabaseModelEndpointResolver
|
||||
@@ -151,7 +151,7 @@ TOP_K = 5
|
||||
MAX_ANSWER_CHARS = 1200
|
||||
REFERENCE_LIMIT = 3
|
||||
|
||||
COMPANY = "南方科技"
|
||||
COMPANY = "奶龙基金责任有限公司"
|
||||
# 客服热线与工作时间:**唯一来源是 `app/core/customer_service_rules.py`**,这里只做转发。
|
||||
#
|
||||
# 为什么必须转发而不是各写一份:这两处曾一度不一致 —— `customer_service_rules.CONTACT_PHONE`
|
||||
@@ -191,7 +191,7 @@ class CustomerServiceAgent(BaseAgent):
|
||||
definition = AgentDefinition(
|
||||
agent_type=AGENT_TYPE,
|
||||
version="1.0.0",
|
||||
allowed_roles=("customer",),
|
||||
allowed_roles=("visitor", "customer"),
|
||||
allowed_portals=("api",),
|
||||
# 代码上限:实际可用范围由发布配置的意图白名单收窄(两者取交集)
|
||||
allowed_tools=(TOOL_NAME, SUITABILITY_TOOL, PROFILE_TOOL_NAME),
|
||||
@@ -199,9 +199,25 @@ class CustomerServiceAgent(BaseAgent):
|
||||
INTENT_FAQ, INTENT_PRODUCT, INTENT_POLICY, INTENT_SUITABILITY,
|
||||
INTENT_CHITCHAT, INTENT_TRANSFER,
|
||||
),
|
||||
# 客服不隐式召回长期画像;已登录用户的画像查询必须显式调用受控工具。
|
||||
recalls_customer_memory=False,
|
||||
)
|
||||
|
||||
def __init__(self, definition: AgentDefinition | None = None) -> None:
|
||||
"""Use the class definition for direct tests and factory-created instances alike."""
|
||||
super().__init__(definition or self.definition)
|
||||
|
||||
async def handle(self, request: AgentRequest, context: RequestContext) -> CoreResult:
|
||||
# 先执行确定性的安全与权限边界路由;这些分支不查知识库、不调用模型,
|
||||
# 从根上阻断账户敏感数据、凭据泄露、诈骗和代办交易等越界请求。
|
||||
safety = route_message(request.message)
|
||||
if safety is not None:
|
||||
return CoreResult(
|
||||
text=safety.reply,
|
||||
intent=IntentResult(intent=safety.intent, confidence=1.0),
|
||||
transfer_required=safety.transfer_required,
|
||||
transfer_reason=safety.transfer_reason,
|
||||
)
|
||||
# 画像问题优先处理(确定性关键词,不走意图分类):知识库答不了"我的风险等级是多少",
|
||||
# 那需要读该客户的画像数据,必须走 `query_customer_profile` 工具取权威字段。
|
||||
# 放在意图分发**之前**是有意的:让画像能力不依赖意图分类是否恰好给出 faq。
|
||||
@@ -515,6 +531,12 @@ class CustomerServiceAgent(BaseAgent):
|
||||
# ---- 出口二:闲聊(提示词走发布配置) ----
|
||||
|
||||
async def _chitchat(self, request: AgentRequest) -> CoreResult:
|
||||
# 连续闲聊超过三轮后只做一次自然的业务引导,避免模型无限延续闲聊。
|
||||
if request.metadata.chitchat_streak == 4:
|
||||
return CoreResult(
|
||||
text="您好呀,您是想了解基金产品、申赎规则或其他公开业务信息吗?",
|
||||
intent=self._classified_intent,
|
||||
)
|
||||
system, template = await self._chitchat_prompt()
|
||||
message = request.message[:500]
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user