mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
fix: 让 Agent Chat 默认遵循 REPORT_LANGUAGE (#2102)
* fix: apply report language to agent chat defaults * fix(review-feedback-2102): Treat null report language as missing and update those
This commit is contained in:
@@ -62,6 +62,18 @@ class ChatRequest(BaseModel):
|
||||
"""Return skill ids from the unified request shape."""
|
||||
return self.skills
|
||||
|
||||
|
||||
def _build_agent_chat_context(request: ChatRequest, config, skills: Optional[List[str]]) -> Dict[str, Any]:
|
||||
"""Build the shared context contract for regular and streaming Agent Chat."""
|
||||
context = dict(request.context or {})
|
||||
if skills is not None:
|
||||
context["skills"] = skills
|
||||
report_language = context.get("report_language")
|
||||
if report_language is None or (isinstance(report_language, str) and not report_language.strip()):
|
||||
context["report_language"] = config.report_language
|
||||
return context
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
success: bool
|
||||
content: str
|
||||
@@ -204,12 +216,7 @@ async def agent_chat(request: ChatRequest):
|
||||
skills = request.effective_skills
|
||||
executor = _build_executor(config, skills or None)
|
||||
|
||||
# Pass explicit skills into context for the orchestrator.
|
||||
# Direct assignment so caller-provided skills always take precedence
|
||||
# over any stale value carried in the context dict.
|
||||
ctx = dict(request.context or {})
|
||||
if skills is not None:
|
||||
ctx["skills"] = skills
|
||||
ctx = _build_agent_chat_context(request, config, skills)
|
||||
|
||||
# Offload the blocking call to a thread to avoid blocking the event loop.
|
||||
loop = asyncio.get_running_loop()
|
||||
@@ -473,12 +480,8 @@ async def agent_chat_stream(request: ChatRequest):
|
||||
)
|
||||
_ACTIVE_CODEX_STREAMS[request_id] = cancel_event
|
||||
|
||||
# Pass explicit skills into context for the orchestrator.
|
||||
# Direct assignment so caller-provided skills always take precedence.
|
||||
skills = request.effective_skills
|
||||
stream_ctx = dict(request.context or {})
|
||||
if skills is not None:
|
||||
stream_ctx["skills"] = skills
|
||||
stream_ctx = _build_agent_chat_context(request, config, skills)
|
||||
|
||||
def progress_callback(event: dict):
|
||||
if backend_id == "codex_app_server" and cancel_event.is_set():
|
||||
|
||||
@@ -578,10 +578,10 @@ const settingsHelpZhCN: SettingsHelpMap = {
|
||||
},
|
||||
'settings.notification.report_output': {
|
||||
title: '报告输出设置',
|
||||
summary: '控制通知报告的详细程度、语言和模板输出。',
|
||||
usage: 'REPORT_TYPE 可选 simple/full/brief,REPORT_LANGUAGE 可选 zh/en。',
|
||||
valueNotes: ['报告语言影响默认模板和通知文案,不等同于前端界面语言。'],
|
||||
impact: ['影响通知正文长度、语言和阅读体验。'],
|
||||
summary: '控制通知报告的详细程度、默认输出语言和模板输出。',
|
||||
usage: 'REPORT_TYPE 可选 simple/full/brief,REPORT_LANGUAGE 可选 zh/en/ko;Agent Chat 只有在未显式传入 context.report_language 时才继承这里的默认语言。',
|
||||
valueNotes: ['报告语言会影响默认模板、通知文案,以及未单独指定语言的 Agent Chat 回复;它不等同于前端界面语言。'],
|
||||
impact: ['影响通知正文长度、语言和未显式指定语言的 Agent Chat 阅读体验。'],
|
||||
notes: ['full 报告可能更长,部分平台可能触发分段发送。'],
|
||||
},
|
||||
'settings.system.WEBUI_HOST': {
|
||||
@@ -1773,10 +1773,10 @@ const settingsHelpEnUS: SettingsHelpMap = {
|
||||
},
|
||||
'settings.notification.report_output': {
|
||||
title: 'Report Output',
|
||||
summary: 'Controls notification detail level, language, and template output.',
|
||||
usage: 'REPORT_TYPE supports simple/full/brief. REPORT_LANGUAGE supports zh/en.',
|
||||
valueNotes: ['Report language affects default report and notification text, not the Web UI language.'],
|
||||
impact: ['Affects notification length, language, and readability.'],
|
||||
summary: 'Controls notification detail level, default output language, and template output.',
|
||||
usage: 'REPORT_TYPE supports simple/full/brief. REPORT_LANGUAGE supports zh/en/ko. Agent Chat inherits this default only when context.report_language is omitted.',
|
||||
valueNotes: ['Report language affects default templates, notification text, and Agent Chat replies that do not explicitly set a language; it does not change the Web UI language.'],
|
||||
impact: ['Affects notification length, language, and the readability of Agent Chat replies that rely on the default language.'],
|
||||
notes: ['Full reports can be long and may be split by some platforms.'],
|
||||
},
|
||||
'settings.system.WEBUI_HOST': {
|
||||
|
||||
@@ -309,7 +309,7 @@ const fieldDescriptionMap: Record<string, string> = {
|
||||
WEBHOOK_VERIFY_SSL: '发送 HTTPS Webhook 时是否校验证书,公网环境建议保持开启。',
|
||||
SINGLE_STOCK_NOTIFY: '启用后按个股分别推送通知;关闭则合并为单条消息。',
|
||||
REPORT_TYPE: '通知报告展示粒度(如 simple/full/brief)。',
|
||||
REPORT_LANGUAGE: '通知报告语言(zh/en)。',
|
||||
REPORT_LANGUAGE: '报告与 Agent Chat 默认输出语言(zh/en/ko);仅在问股未显式传入 context.report_language 时生效。',
|
||||
REPORT_TEMPLATES_DIR: '自定义报告模板目录路径。',
|
||||
REPORT_INTEGRITY_ENABLED: '启用报告完整性检查,避免发送缺字段或异常内容。',
|
||||
REPORT_RENDERER_ENABLED: '启用报告渲染器,将结构化数据渲染为最终通知内容。',
|
||||
@@ -400,6 +400,7 @@ const fieldOptionLabelMap: Record<string, Record<string, string>> = {
|
||||
REPORT_LANGUAGE: {
|
||||
zh: '中文',
|
||||
en: '英文',
|
||||
ko: '韩文',
|
||||
chinese: '中文',
|
||||
english: '英文',
|
||||
},
|
||||
@@ -483,6 +484,7 @@ const fieldOptionLabelMapEn: Record<string, Record<string, string>> = {
|
||||
REPORT_LANGUAGE: {
|
||||
zh: 'Chinese',
|
||||
en: 'English',
|
||||
ko: 'Korean',
|
||||
chinese: 'Chinese',
|
||||
english: 'English',
|
||||
},
|
||||
|
||||
@@ -131,6 +131,7 @@ describe('systemConfigI18n option label localization', () => {
|
||||
['REPORT_TYPE', 'brief', undefined, '简报'],
|
||||
['REPORT_LANGUAGE', 'zh', 'Chinese', '中文'],
|
||||
['REPORT_LANGUAGE', 'en', 'English', '英文'],
|
||||
['REPORT_LANGUAGE', 'ko', 'Korean', '韩文'],
|
||||
['NOTIFICATION_MIN_SEVERITY', '', 'Not set', '未设置'],
|
||||
['NOTIFICATION_MIN_SEVERITY', 'info', 'info', '信息'],
|
||||
['NOTIFICATION_MIN_SEVERITY', 'warning', 'warning', '警告'],
|
||||
|
||||
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
- [修复] GitHub Actions PR Review 流程中的 `_event_payload()` 此前用 `except (OSError, ValueError): return {}` 把「事件文件缺失」「文件不可读」「JSON 非法」三类异常统一吞成空对象,下游只表现为 `PR number is unavailable` 无法定位根因;现保留空对象降级行为不变,但分别对三类失败输出不含载荷内容的警告(仅含异常类型与 `GITHUB_EVENT_PATH` 源路径),并补齐三类降级路径与「坏载荷导致 PR 编号不可用」链路的回归测试(fixes #2070)
|
||||
- [修复] DataFetcherManager 港股路由:4-5 位纯数字裸港股码(如 `02513`、`00700`、`0001`)此前仅 `_is_hk_market` 单侧识别,`AkshareFetcher._is_hk_code` 与 `LongbridgeFetcher._is_hk_code` 内仍只接受 5 位裸数字,导致配置了 Yfinance/Akshare/Longbridge 的港股日线/实时链路对 4 位裸港股码静默失败。本 PR 同步三处 `_is_hk_code` 契约到 4-5 位裸数字,并新增 `DataFetcherManager` 港股路由回归测试,避免上游路由判 HK、下游 provider 不识别的部分调用链断口(fixes #2091)
|
||||
- [修复] Web 设置页和通知测试入口补齐普通钉钉群机器人配置,支持安全遮罩地保存 `DINGTALK_WEBHOOK_URL` / `DINGTALK_SECRET`、查看专属帮助并发送钉钉测试通知(refs #1957)。
|
||||
- [修复] Agent Chat 普通与流式接口在请求未指定 `report_language` 时继承全局 `REPORT_LANGUAGE`,显式请求值仍保持优先,避免回复语言与报告配置不一致。
|
||||
|
||||
## [3.27.0] - 2026-07-19
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ daily_stock_analysis/
|
||||
|------------|------|:----:|
|
||||
| `SINGLE_STOCK_NOTIFY` | 单股推送模式:设为 `true` 则每分析完一只股票立即推送 | 可选 |
|
||||
| `REPORT_TYPE` | 报告类型:`simple`(精简)、`full`(完整)、`brief`(3-5句概括),Docker环境推荐设为 `full` | 可选 |
|
||||
| `REPORT_LANGUAGE` | 报告输出语言:`zh`(默认中文) / `en`(英文) / `ko`(韩文);会同步影响 Prompt、模板、通知 fallback 与 Web 报告页固定文案。`ko` 复用英文结构骨架并通过输出语言指令约束模型用韩文输出,通知按报告语言渲染本地化标签。仓库自带 `00-daily-analysis.yml` 已显式映射该变量,直接在 Actions Secrets/Variables 中配置即可生效 | 可选 |
|
||||
| `REPORT_LANGUAGE` | 报告与 Agent Chat 的默认输出语言:`zh`(默认中文) / `en`(英文) / `ko`(韩文);会同步影响 Prompt、模板、通知 fallback、Web 报告页固定文案,以及未显式传入 `context.report_language` 的问股回复。`ko` 复用英文结构骨架并通过输出语言指令约束模型用韩文输出,通知按报告语言渲染本地化标签。仓库自带 `00-daily-analysis.yml` 已显式映射该变量,直接在 Actions Secrets/Variables 中配置即可生效 | 可选 |
|
||||
| `REPORT_SUMMARY_ONLY` | 仅分析结果摘要:设为 `true` 时只推送汇总,不含个股详情;多股时适合快速浏览(默认 false,Issue #262) | 可选 |
|
||||
| `REPORT_SHOW_LLM_MODEL` | 通知报告底部是否显示本次分析使用的 LLM 模型名称,默认 `true`;设为 `false` 可隐藏运行时模型信息。该变量仅调整展示,不影响 provider/model/Base URL、LiteLLM 路由或运行时模型保存/迁移/清理语义。 | 可选 |
|
||||
| `REPORT_TEMPLATES_DIR` | Jinja2 模板目录(相对项目根,默认 `templates`) | 可选 |
|
||||
@@ -143,7 +143,7 @@ daily_stock_analysis/
|
||||
|
||||
> 兼容性说明:`REPORT_SHOW_LLM_MODEL` 维持默认 `true` 的原始展示语义,关闭时只影响底部模型文案输出。该配置不会变更 provider/model/Base URL、LiteLLM 路由、模型保存、迁移或清理语义;回退方式为恢复或删除该变量,并设为 `true`。
|
||||
|
||||
> 说明:`REPORT_LANGUAGE` 只影响报告文本与 Web 报告页固定文案;WebUI 页面语言(导航、登录页、侧边栏、设置页、通用控件)使用独立状态,不与其联动。
|
||||
> 说明:`REPORT_LANGUAGE` 影响报告文本、Web 报告页固定文案与未显式指定语言的 Agent Chat 回复;WebUI 页面语言(导航、登录页、侧边栏、设置页、通用控件)使用独立状态,不与其联动。
|
||||
> WebUI 语言状态保存在浏览器 `localStorage` 的 `dsa.uiLanguage`,启动顺序为:
|
||||
> 1) 明确选择(`localStorage.dsa.uiLanguage`,仅支持 `zh`/`en`)
|
||||
> 2) 浏览器语言检测(`navigator.languages` / `navigator.language`,`zh-*` 或 `en-*`)
|
||||
@@ -1576,9 +1576,9 @@ FastAPI 提供 RESTful API 服务,支持配置管理和触发分析。
|
||||
|
||||
### 与本变更相关的产品行为
|
||||
|
||||
- Web 语言状态采用两层机制:`dsa.uiLanguage`(浏览器持久化)与 `REPORT_LANGUAGE`(报告输出)解耦。
|
||||
- Web 语言状态采用两层机制:`dsa.uiLanguage`(浏览器持久化)与 `REPORT_LANGUAGE`(报告及问股默认输出)解耦。
|
||||
- `dsa.uiLanguage` 只决定 WebUI 文案与导航语言(`zh` / `en`),取值优先级为本地持久化值 -> 浏览器语言 -> 默认 `zh`。
|
||||
- `REPORT_LANGUAGE` 控制报告文本、股票简称本地化与报告页固定文案(`zh` / `en` / `ko`)。
|
||||
- `REPORT_LANGUAGE` 控制报告文本、股票简称本地化、报告页固定文案,以及未提供 `context.report_language` 的 Agent Chat 回复(`zh` / `en` / `ko`)。
|
||||
- 页面语言切换为用户体验增强,不属于回归验证证据记录范围;截图与命令请按 PR 流程在 PR 描述中单独维护。
|
||||
- 本改动仅新增请求级报告语言覆盖参数,不改变 `provider`/`model`/`base_url` 的配置迁移与清理逻辑。
|
||||
|
||||
@@ -1641,7 +1641,7 @@ FastAPI 提供 RESTful API 服务,支持配置管理和触发分析。
|
||||
> 说明:`GET /api/v1/usage/dashboard` 复用 `llm_usage` 审计表,不新增配置项或数据库迁移。接口仅返回已落库的调用次数、Prompt/Completion/Total Token 聚合、模型维度用量和最近调用记录,不推导模型上下文窗口或 provider 元数据。
|
||||
> 说明(Issue #1520):列表中的模型名展示字段仅来源于历史快照中的 `model_used`,仅用于历史回溯展示,不影响运行时模型模型路由(`litellm_model`、`llm_model_list`)、Provider、Base URL 与配置迁移/清理语义。回退方式为回退本次提交,现网历史查询/抽屉/接口链路兼容性保持不变。
|
||||
> 说明:历史详情、同步分析响应和 completed 任务状态会在 `report.details.analysis_context_pack_overview` 返回低敏输入数据块 overview;其中同步分析响应依赖本次已持久化的 `analysis_history.context_snapshot`,`SAVE_CONTEXT_SNAPSHOT=false` 时新记录不保证返回 overview。`details.context_snapshot` 会剥离该顶层字段,不返回完整 `AnalysisContextPack` 或 Prompt summary。
|
||||
> 说明:`POST /api/v1/agent/chat` 与 `POST /api/v1/agent/chat/stream` 会把前端传入的 `context.stock_code` 作为问股当前标的基线,但服务端会先重新判定 stock scope。前端从历史报告进入问股后会持续发送 active stock context;切回或重载已有会话时,会根据已加载的历史用户消息恢复基础 `{stock_code, stock_name: null}`。服务端会在每轮消息中重新判定 `maintain` / `switch` / `compare`:未明确切换时,带 `stock_code` 的股票工具调用只能访问当前标的;显式切换会清理旧标的历史摘要和预取数据;含比较/对比/vs/差异/相比等明确比较意图或多个非当前明确股票代码的问题允许本轮明确出现的多个代码,但不改写当前标的。若模型误把 TTM、PE、MACD、KDJ 等金融缩写、移动均线语境下的 `MA` 指标词,或 SH/SZ/BJ/HK/SS 等交易所片段当成股票代码调用工具,后端会返回不可重试的 `stock_scope_violation` 工具结果,而不会执行对应股票工具。工具名只解析注册表中的精确名称;任何 provider namespace 或 suffix 都不会路由到已有工具。
|
||||
> 说明:`POST /api/v1/agent/chat` 与 `POST /api/v1/agent/chat/stream` 会把前端传入的 `context.stock_code` 作为问股当前标的基线,并在 `context.report_language` 缺失时使用全局 `REPORT_LANGUAGE`;调用方显式提供的 `context.report_language` 保持优先。服务端会先重新判定 stock scope。前端从历史报告进入问股后会持续发送 active stock context;切回或重载已有会话时,会根据已加载的历史用户消息恢复基础 `{stock_code, stock_name: null}`。服务端会在每轮消息中重新判定 `maintain` / `switch` / `compare`:未明确切换时,带 `stock_code` 的股票工具调用只能访问当前标的;显式切换会清理旧标的历史摘要和预取数据;含比较/对比/vs/差异/相比等明确比较意图或多个非当前明确股票代码的问题允许本轮明确出现的多个代码,但不改写当前标的。若模型误把 TTM、PE、MACD、KDJ 等金融缩写、移动均线语境下的 `MA` 指标词,或 SH/SZ/BJ/HK/SS 等交易所片段当成股票代码调用工具,后端会返回不可重试的 `stock_scope_violation` 工具结果,而不会执行对应股票工具。工具名只解析注册表中的精确名称;任何 provider namespace 或 suffix 都不会路由到已有工具。
|
||||
> 说明:`POST /api/v1/backtest/run` 新增 `analysis_date_from` / `analysis_date_to`(`YYYY-MM-DD`)请求参数用于按历史分析日期筛选候选;若 `analysis_date_from > analysis_date_to`,接口返回 400 `invalid_params`。
|
||||
> 说明:回测执行成功但无新入库结果时,`BacktestRunResponse.message` 返回可读诊断说明,`diagnostics` 返回排查上下文(示例:`empty_reason`、`analysis_date_from`、`analysis_date_to`、`eval_window_days`、`min_age_days`、`limit`)。
|
||||
> 说明:`GET /api/v1/backtest/results`、`GET /api/v1/backtest/performance`、`GET /api/v1/backtest/performance/{code}` 同步支持 `analysis_date_from`、`analysis_date_to`;不传时保持历史行为。
|
||||
|
||||
@@ -115,7 +115,7 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions`
|
||||
|------------|------|:----:|
|
||||
| `SINGLE_STOCK_NOTIFY` | Single stock push mode: set to `true` to push immediately after each stock analysis | Optional |
|
||||
| `REPORT_TYPE` | Report type: `simple` (concise), `full` (complete), `brief` (3-5 sentences), Docker recommended: `full` | Optional |
|
||||
| `REPORT_LANGUAGE` | Report output language: `zh` (default Chinese) / `en` (English) / `ko` (Korean); also updates prompt instructions, templates, notification fallbacks, and fixed copy in the Web report view. `ko` reuses the English structural scaffolding and constrains the model to Korean output via an output-language directive; notifications render localized labels by report language. The bundled `00-daily-analysis.yml` already maps this variable, so setting it in Actions Secrets/Variables works out of the box | Optional |
|
||||
| `REPORT_LANGUAGE` | Default output language for reports and Agent Chat: `zh` (default Chinese) / `en` (English) / `ko` (Korean); also updates prompt instructions, templates, notification fallbacks, fixed copy in the Web report view, and Ask Stock replies that omit `context.report_language`. `ko` reuses the English structural scaffolding and constrains the model to Korean output via an output-language directive; notifications render localized labels by report language. The bundled `00-daily-analysis.yml` already maps this variable, so setting it in Actions Secrets/Variables works out of the box | Optional |
|
||||
| `REPORT_SHOW_LLM_MODEL` | Whether notification report footers show the LLM model used for analysis. Defaults to `true`; set to `false` to hide runtime model metadata. This switch only affects presentation and does not change provider/model/Base URL, LiteLLM routing, or runtime model save/migration/cleanup behavior. | Optional |
|
||||
| `REPORT_TEMPLATES_DIR` | Jinja2 template directory (relative to project root, default `templates`) | Optional |
|
||||
| `REPORT_RENDERER_ENABLED` | Enable Jinja2 template rendering (default `false`, zero regression) | Optional |
|
||||
@@ -136,7 +136,7 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions`
|
||||
|
||||
> Compatibility note: `REPORT_SHOW_LLM_MODEL` keeps the previous default-visible behavior (`true`) and only changes report footer rendering. It does not alter provider/model/Base URL, LiteLLM routing, or runtime model persistence/migration/cleanup semantics. Rollback is to remove the variable or set it back to `true`.
|
||||
|
||||
> `REPORT_LANGUAGE` only affects report text and report page fixed copy. Web UI chrome language (navigation, login, settings, shell labels, shared controls) is intentionally independent and stored in browser `localStorage` as `dsa.uiLanguage`.
|
||||
> `REPORT_LANGUAGE` affects report text, report-page fixed copy, and Agent Chat replies that do not explicitly select a language. Web UI chrome language (navigation, login, settings, shell labels, shared controls) is intentionally independent and stored in browser `localStorage` as `dsa.uiLanguage`.
|
||||
> UI language resolution is: explicit localStorage value (`zh` or `en`) -> browser language (`navigator.languages` / `navigator.language`) -> default `zh`.
|
||||
|
||||
#### Other Configuration
|
||||
@@ -1415,7 +1415,7 @@ FastAPI provides RESTful API service for configuration management and triggering
|
||||
|
||||
For this feature, the product behavior is:
|
||||
|
||||
- UI language is independent from report language: `dsa.uiLanguage` (browser persistence) controls shell/login/settings text, while `REPORT_LANGUAGE` controls report text and report-page fixed copy (`zh`/`en`/`ko`).
|
||||
- UI language is independent from generated-content language: `dsa.uiLanguage` (browser persistence) controls shell/login/settings text, while `REPORT_LANGUAGE` controls report text, report-page fixed copy, and Agent Chat replies that omit `context.report_language` (`zh`/`en`/`ko`).
|
||||
- `dsa.uiLanguage` follows local persistence -> browser language -> default `zh`.
|
||||
- This change only adds request-scope report language override parameters; it does not modify `provider`, `model`, `base_url`, or migration/cleanup behavior.
|
||||
- PR-level verification output, screenshots, and command logs are maintained in PR description, not in this usage guide.
|
||||
@@ -1474,7 +1474,7 @@ For this feature, the product behavior is:
|
||||
> Note: `GET /api/v1/usage/dashboard` reuses the existing `llm_usage` audit table and adds no configuration key or database migration. It returns only persisted call counts, prompt/completion/total token aggregates, model-level usage, and recent call records; it does not infer model context windows or provider metadata.
|
||||
> Issue #1520 compatibility note: The `model`/`model_used` returned here is read-only historical snapshot metadata from each record, used only for trend drawer/history display. It does not alter runtime model/model-provider/base URL resolution, config migration, or cleanup semantics in the analysis path. Rollback is by reverting this commit; history query, API response shapes, and UI drawer consumption remain compatible.
|
||||
> Note: history detail, sync analysis responses, and completed task status responses expose a low-sensitivity input data-block overview at `report.details.analysis_context_pack_overview`; sync analysis responses depend on the just-persisted `analysis_history.context_snapshot`, so new records do not guarantee the overview when `SAVE_CONTEXT_SNAPSHOT=false`. `details.context_snapshot` strips that top-level field and does not return the full `AnalysisContextPack` or prompt summary.
|
||||
> Note: `POST /api/v1/agent/chat` and `POST /api/v1/agent/chat/stream` use the frontend-provided `context.stock_code` as the active Ask Stock baseline only after server-side stock-scope resolution. Each turn is classified as `maintain`, `switch`, or `compare`: unchanged follow-ups can call stock-scoped tools only for the current stock; explicit switches clear stale stock summaries and prefetched context; comparison prompts such as compare/vs/difference allow the explicitly mentioned codes for that turn without rewriting the current stock. If a model attempts to call a stock tool with financial abbreviations such as TTM, PE, MACD, KDJ, contextual indicator tokens such as `MA` in moving-average prompts, or exchange fragments such as SH/SZ/BJ/HK/SS, the backend returns a non-retriable `stock_scope_violation` tool result instead of executing that stock tool. Tool names are resolved only by exact registry name; provider namespaces or suffixes are not routed to existing tools.
|
||||
> Note: `POST /api/v1/agent/chat` and `POST /api/v1/agent/chat/stream` use the frontend-provided `context.stock_code` as the active Ask Stock baseline and fall back to global `REPORT_LANGUAGE` when `context.report_language` is absent; an explicitly supplied `context.report_language` keeps precedence. Stock scope is still resolved server-side. Each turn is classified as `maintain`, `switch`, or `compare`: unchanged follow-ups can call stock-scoped tools only for the current stock; explicit switches clear stale stock summaries and prefetched context; comparison prompts such as compare/vs/difference allow the explicitly mentioned codes for that turn without rewriting the current stock. If a model attempts to call a stock tool with financial abbreviations such as TTM, PE, MACD, KDJ, contextual indicator tokens such as `MA` in moving-average prompts, or exchange fragments such as SH/SZ/BJ/HK/SS, the backend returns a non-retriable `stock_scope_violation` tool result instead of executing that stock tool. Tool names are resolved only by exact registry name; provider namespaces or suffixes are not routed to existing tools.
|
||||
> Note: `POST /api/v1/backtest/run` adds `analysis_date_from` / `analysis_date_to` (`YYYY-MM-DD`) to filter candidates by analysis date range. When `analysis_date_from > analysis_date_to`, it returns 400 `invalid_params`.
|
||||
> Note: When backtest runs successfully but yields no new persisted rows, `BacktestRunResponse.message` carries a readable diagnostic and `diagnostics` returns troubleshooting context (for example `empty_reason`, `analysis_date_from`, `analysis_date_to`, `eval_window_days`, `min_age_days`, `limit`).
|
||||
> Note: `GET /api/v1/backtest/results`, `GET /api/v1/backtest/performance`, and `GET /api/v1/backtest/performance/{code}` all support `analysis_date_from` and `analysis_date_to` consistently. Omitting them keeps historical default behavior.
|
||||
|
||||
@@ -2575,7 +2575,7 @@ class Config:
|
||||
raw = (value or "").strip()
|
||||
if raw and not is_supported_report_language_value(raw):
|
||||
logging.getLogger(__name__).warning(
|
||||
"REPORT_LANGUAGE '%s' invalid, fallback to 'zh' (valid: zh/en)",
|
||||
"REPORT_LANGUAGE '%s' invalid, fallback to 'zh' (valid: zh/en/ko)",
|
||||
value,
|
||||
)
|
||||
return normalized
|
||||
|
||||
@@ -2578,7 +2578,7 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
||||
},
|
||||
"REPORT_LANGUAGE": {
|
||||
"title": "Report Language",
|
||||
"description": "Default output language for reports and notification templates. Supported values: zh, en.",
|
||||
"description": "Default output language for reports, Agent Chat fallback replies, and notification templates. Supported values: zh, en, ko.",
|
||||
"category": "notification",
|
||||
"data_type": "string",
|
||||
"ui_control": "select",
|
||||
@@ -2597,6 +2597,7 @@ _FIELD_DEFINITIONS: Dict[str, Dict[str, Any]] = {
|
||||
"examples": [
|
||||
"REPORT_LANGUAGE=zh",
|
||||
"REPORT_LANGUAGE=en",
|
||||
"REPORT_LANGUAGE=ko",
|
||||
],
|
||||
"docs": [
|
||||
{
|
||||
|
||||
@@ -29,20 +29,24 @@ def teardown_function() -> None:
|
||||
|
||||
|
||||
def _litellm_config(**overrides):
|
||||
return SimpleNamespace(
|
||||
agent_backend="auto",
|
||||
is_agent_available=lambda: True,
|
||||
**overrides,
|
||||
)
|
||||
values = {
|
||||
"agent_backend": "auto",
|
||||
"is_agent_available": lambda: True,
|
||||
"report_language": "zh",
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
def _codex_config(**overrides):
|
||||
return SimpleNamespace(
|
||||
agent_backend="codex_app_server",
|
||||
agent_arch="single",
|
||||
agent_orchestrator_timeout_s=600,
|
||||
**overrides,
|
||||
)
|
||||
values = {
|
||||
"agent_backend": "codex_app_server",
|
||||
"agent_arch": "single",
|
||||
"agent_orchestrator_timeout_s": 600,
|
||||
"report_language": "zh",
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
def _result(*, backend: str = "litellm", success: bool = True, error_code=None):
|
||||
@@ -118,7 +122,7 @@ def test_agent_chat_forwards_stock_context_to_executor(tmp_path: Path) -> None:
|
||||
executor.chat.return_value = _result()
|
||||
|
||||
with patch("api.middlewares.auth.is_auth_enabled", return_value=False), \
|
||||
patch("api.v1.endpoints.agent.get_config", return_value=_litellm_config()), \
|
||||
patch("api.v1.endpoints.agent.get_config", return_value=_litellm_config(report_language="en")), \
|
||||
patch("api.v1.endpoints.agent._build_executor", return_value=executor):
|
||||
response = TestClient(create_app(static_dir=tmp_path / "static")).post(
|
||||
"/api/v1/agent/chat",
|
||||
@@ -131,7 +135,101 @@ def test_agent_chat_forwards_stock_context_to_executor(tmp_path: Path) -> None:
|
||||
|
||||
assert response.status_code == 200
|
||||
kwargs = executor.chat.call_args.kwargs
|
||||
assert kwargs["context"] == {"stock_code": "600519", "stock_name": "匿名标的"}
|
||||
assert kwargs["context"] == {
|
||||
"stock_code": "600519",
|
||||
"stock_name": "匿名标的",
|
||||
"report_language": "en",
|
||||
}
|
||||
|
||||
|
||||
def test_agent_chat_preserves_explicit_report_language(tmp_path: Path) -> None:
|
||||
executor = MagicMock()
|
||||
executor.chat.return_value = _result()
|
||||
|
||||
with patch("api.middlewares.auth.is_auth_enabled", return_value=False), \
|
||||
patch("api.v1.endpoints.agent.get_config", return_value=_litellm_config(report_language="en")), \
|
||||
patch("api.v1.endpoints.agent._build_executor", return_value=executor):
|
||||
response = TestClient(create_app(static_dir=tmp_path / "static")).post(
|
||||
"/api/v1/agent/chat",
|
||||
json={
|
||||
"message": "분석해 주세요",
|
||||
"session_id": "explicit-language",
|
||||
"context": {"report_language": "ko"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert executor.chat.call_args.kwargs["context"]["report_language"] == "ko"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provided_language", [None, "", " "])
|
||||
def test_agent_chat_treats_null_or_blank_report_language_as_missing(
|
||||
tmp_path: Path, provided_language
|
||||
) -> None:
|
||||
executor = MagicMock()
|
||||
executor.chat.return_value = _result()
|
||||
|
||||
with patch("api.middlewares.auth.is_auth_enabled", return_value=False), \
|
||||
patch("api.v1.endpoints.agent.get_config", return_value=_litellm_config(report_language="en")), \
|
||||
patch("api.v1.endpoints.agent._build_executor", return_value=executor):
|
||||
response = TestClient(create_app(static_dir=tmp_path / "static")).post(
|
||||
"/api/v1/agent/chat",
|
||||
json={
|
||||
"message": "analyze",
|
||||
"session_id": "default-language",
|
||||
"context": {"report_language": provided_language},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert executor.chat.call_args.kwargs["context"]["report_language"] == "en"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provided_language", [None, "", " "])
|
||||
def test_agent_chat_stream_treats_null_or_blank_report_language_as_missing(
|
||||
tmp_path: Path, provided_language
|
||||
) -> None:
|
||||
executor = _executor(_result(backend="litellm"))
|
||||
|
||||
with patch("api.middlewares.auth.is_auth_enabled", return_value=False), \
|
||||
patch("api.v1.endpoints.agent.get_config", return_value=_litellm_config(report_language="en")), \
|
||||
patch("api.v1.endpoints.agent._build_executor", return_value=executor):
|
||||
response = TestClient(create_app(static_dir=tmp_path / "static")).post(
|
||||
"/api/v1/agent/chat/stream",
|
||||
json={
|
||||
"message": "analyze",
|
||||
"session_id": "stream-default-language",
|
||||
"context": {"report_language": provided_language},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
events = _sse_events(response.text)
|
||||
assert [event["type"] for event in events] == ["accepted", "done"]
|
||||
assert executor.prepare_turn.call_args.kwargs["context"]["report_language"] == "en"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("provided_language, expected_language", [
|
||||
(None, "en"),
|
||||
("", "en"),
|
||||
(" ", "en"),
|
||||
("ko", "ko"),
|
||||
])
|
||||
def test_build_agent_chat_context_normalizes_default_report_language(
|
||||
provided_language, expected_language
|
||||
) -> None:
|
||||
request = agent_endpoint.ChatRequest(
|
||||
message="question",
|
||||
context={"report_language": provided_language} if provided_language is not None else {"report_language": None},
|
||||
)
|
||||
|
||||
context = agent_endpoint._build_agent_chat_context(
|
||||
request,
|
||||
_litellm_config(report_language="en"),
|
||||
skills=None,
|
||||
)
|
||||
|
||||
assert context["report_language"] == expected_language
|
||||
|
||||
|
||||
def test_codex_agent_chat_rejects_non_streaming_entrypoint(tmp_path: Path) -> None:
|
||||
@@ -226,7 +324,7 @@ def test_stream_prepares_and_persists_before_accepted_then_starts_backend() -> N
|
||||
executor.prepare_turn.assert_called_once_with(
|
||||
message="分析 AAPL",
|
||||
session_id="accepted-session",
|
||||
context={"stock_code": "AAPL"},
|
||||
context={"stock_code": "AAPL", "report_language": "zh"},
|
||||
)
|
||||
executor.execute_turn.assert_not_called()
|
||||
rest = [json.loads(chunk.removeprefix("data: ").strip()) async for chunk in iterator]
|
||||
@@ -379,16 +477,21 @@ def test_codex_stop_rejects_unknown_or_finished_request() -> None:
|
||||
def test_litellm_stream_keeps_existing_execution_signature(tmp_path: Path) -> None:
|
||||
executor = _executor(_result(backend="litellm"))
|
||||
with patch("api.middlewares.auth.is_auth_enabled", return_value=False), \
|
||||
patch("api.v1.endpoints.agent.get_config", return_value=_litellm_config()), \
|
||||
patch("api.v1.endpoints.agent.get_config", return_value=_litellm_config(report_language="en")), \
|
||||
patch("api.v1.endpoints.agent._build_executor", return_value=executor):
|
||||
response = TestClient(create_app(static_dir=tmp_path / "static")).post(
|
||||
"/api/v1/agent/chat/stream",
|
||||
json={"message": "question", "session_id": "litellm-session"},
|
||||
json={
|
||||
"message": "question",
|
||||
"session_id": "litellm-session",
|
||||
"context": {"report_language": "ko"},
|
||||
},
|
||||
)
|
||||
|
||||
events = _sse_events(response.text)
|
||||
assert [event["type"] for event in events] == ["accepted", "done"]
|
||||
assert events[0]["backend"] == "litellm"
|
||||
assert executor.prepare_turn.call_args.kwargs["context"]["report_language"] == "ko"
|
||||
assert "cancel_event" not in executor.execute_turn.call_args.kwargs
|
||||
|
||||
|
||||
|
||||
@@ -343,7 +343,10 @@ class AgentSkillsEndpointTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_chat_request_empty_skills_clears_context_without_triggering_activate_all(self) -> None:
|
||||
config = SimpleNamespace(is_agent_available=lambda: True)
|
||||
config = SimpleNamespace(
|
||||
is_agent_available=lambda: True,
|
||||
report_language="zh",
|
||||
)
|
||||
executor = MagicMock()
|
||||
executor.chat.return_value = SimpleNamespace(success=True, content="ok", error=None)
|
||||
request = agent.ChatRequest(message="hello", skills=[], context={"skills": ["old_skill"]})
|
||||
|
||||
Reference in New Issue
Block a user