Files
group_fqcd_jr/app/core/fund_contracts.py
T
张胜宇 3e24033f72 feat(W27): L0 表层判定层 + 出口 E6 行情 + 收益过滤槽位白名单 + 免责声明分档(设计与代码同轮完成)
设计与依据:新增 D3.9-客服Agent智能路由与行情出口设计-2026-09-21.md(CS-ARCH-2026-024)
DEC-W27-1~12 全部批准并落地。

## 新增(3 个源文件 + 3 个测试文件)
- app/core/exit_codes.py:出口码注册表(E0/E1/E2a~E2e/E3/E4/E5a~E5c/E6/E8/CHAT/LOGIN/CONTACT)
  + NON_BUSINESS_EXIT_CODES 白名单(免责分档的判据面)
- app/service/fund_trend_service.py(E6):summarize_trend() 纯函数 + query_fund_trend 工具
  读 fin_nav_history;按净值日个数(5/20/60/120)给区间涨跌 / 区间高低 / 来源与区间
- tests/unit/{service,tools} 新增 45 条守卫单测(E6 出口 / L0 表层判定 / 工具配置)

## 改动(重点)
- customer_service.py:
  · _route_surface()(L0-a 闲聊 / L0-b 行情 / L0-e 无信息量),位置在安全路由之后
  · drop_yield_claims() 重写为「入口闸 × 槽位白名单」+ fail-closed
    (旧黑名单会误删权重:A-06 业绩基准公式整行消失)
  · allowed_tools 补 query_fund_trend —— 修掉发布被拒 422「配置超出 Agent 工具上限」
    (该 422 是**子集**校验而非数量上限,根因即此项漏配)
  · 25 处 CoreResult 全部补 exit_code(AST 守卫保证零遗漏)
- customer_service_rules.py:闲聊判定重写(业务实体边界 + 词表 + 特征串 + 语气词)
  + is_low_information_message() + is_contact_inquiry()
- governance.py:免责声明按 exit_code 分档(业务档完整 / 非业务档轻型)
- actor.py:VISITOR_PERMISSIONS 补 fund:quote:read(E6 对访客开放,DEC-W27-11)
- tools/:seed_compliance_baseline 新增 TPL_DISCLAIMER_LIGHT;publish 工具改为
  「从生效版本派生原列表再追加」(_inherit_tools 取不到返回 None,防静默改窄)

## 实测
- 金标扩容 46 → 55(新增 Q 组行情 5 条 + C 组闲聊 4 条)
- M-1 55/55、M-4 55/55;M-5 与四项零容忍(M-7/M-8/M-9/M-10)全 0
- 原 46 条可比基线逐项不变(46/46);M-6 9.1%(5 条全在转人工白名单内)
- 全量 pytest 2094 passed / 3 skipped;ruff 零新增(余 5 条与 HEAD 逐条对应)
- 配置版本 244(cs-tools-75813de45421)已发布并激活

## 文档
- 新增 D3.9(设计专册);D1.1 §34 补「代码实施已同轮完成」并把旧表述作废
- D3.7:新增 Q 组判据表 + §6.4 第四次实测 + M-3/M-9 口径澄清
- D2.9:新增 §2.11(9 条新增用例的实测答复原文)+ 汇总表改双列口径
- D2.1:新增 v6.41 执行条目
- 权威副本(D:\桌面\金融\)→ 仓库镜像 全量比对一致(0 缺失 / 0 不一致)

## 诚实未做项
- 知识出口(E3/E4)侧的实体锚点闸门未做(故金标扩容 9 条而非 12 条,A-11~A-13 未成集)
- 阈值未重标(D3.9 §3.4 只给方法);M-3 分母未改(仅明示口径);
  D1.1 §0 与 §4.0 历史计数偏移(差 1)仍沿用
2026-09-21 22:32:39 +08:00

77 lines
2.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""基金行情公共契约。内部使用稳定英文字段,不暴露供应商字段名。"""
from datetime import date, datetime
from decimal import Decimal
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
class FundQuoteQuery(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
fund_codes: tuple[str, ...] = ()
fund_type: str | None = None
target_date: date | None = None
limit: int = Field(default=50, ge=1, le=1000)
@field_validator("fund_codes")
@classmethod
def validate_codes(cls, value: tuple[str, ...]) -> tuple[str, ...]:
for code in value:
if len(code) != 6 or not code.isdigit():
raise ValueError("fund code must be a six-digit number")
return tuple(dict.fromkeys(value))
class FundQuote(BaseModel):
model_config = ConfigDict(extra="forbid", frozen=True)
fund_code: str
fund_name: str
fund_type: str | None = None
nav: Decimal | None = None
nav_date: date | None = None
daily_change: Decimal | None = None
half_year_return: Decimal | None = None
one_year_return: Decimal | None = None
year_to_date_return: Decimal | None = None
since_inception_return: Decimal | None = None
quote_time: datetime
quote_source: Literal["eastmoney", "cache", "degraded"]
is_intraday: bool
degraded: bool = False
class FundTrendQuery(BaseModel):
"""`E6` 行情走势出口的入参(`W27` / `D3.9` §4.2)。
**只收实体,不收问句**:`fund_code` 与 `fund_name` 至少给一个 —— 由 Agent 侧
「表层判定层」抽好再传,工具自己不做自然语言理解。这样"抽错实体"与"查不到数据"
是两类可分开归因、分开回归的失败。
"""
model_config = ConfigDict(extra="forbid", frozen=True)
#: 6 位场内代码(最确定的实体)
fund_code: str | None = None
#: 产品名(需在 `fin_product.product_name` 里**唯一**命中,否则判 `ambiguous`)
fund_name: str | None = None
#: 取多少个**净值日**(不是自然日,见 `fund_trend_service` 的口径说明)
days: int = Field(default=120, ge=5, le=365)
@field_validator("fund_code")
@classmethod
def validate_code(cls, value: str | None) -> str | None:
if value is None:
return None
text = value.strip()
if len(text) != 6 or not text.isdigit():
raise ValueError("fund code must be a six-digit number")
return text
@model_validator(mode="after")
def require_entity(self) -> "FundTrendQuery":
if not (self.fund_code or (self.fund_name or "").strip()):
raise ValueError("fund_code or fund_name is required")
return self