Fix(agent / skills) issue 307 content filter resilience#308
Conversation
When LLM content moderation (e.g. DashScope/Qwen 绿网) blocks individual news items during event-driven analysis, the agent now skips the filtered item and continues instead of aborting the entire run. 4-layer fix: - Provider: content_filter_triggered flag on LLMResponse (chat.py) - Agent loop: skip-and-continue with trace entry + system message (loop.py) - Swarm worker: same pattern with event emission (worker.py) - Result surfacing: ratio tracking with 5% threshold warning in run_card Additional: - Preflight displays current CONTENT_FILTER_WARNING_THRESHOLD - .env.example documents the new env var - set_env_override tool for runtime NL adjustment (immediate + persistent) - System prompt updated with Runtime Configuration section Trigger scenario: vibe-trading run -p "Run an event-driven backtest on 159516.SZ using recent news and announcements for sentiment scoring" Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/qwen3.7-max Co-Authored-By: opencode <noreply@ai-tool.com> Co-Authored-By: Claude Code <noreply@anthropic.com> AI-Contributed/Feature: 196/196 AI-Contributed/UT: 373/373
…nt tool Wave 2 implementation: - Agent loop: track content_filter ratio, surface warnings in result dict when ratio > 5% threshold (configurable via CONTENT_FILTER_WARNING_THRESHOLD) - Swarm worker: same ratio tracking, add content_filter_warnings to WorkerResult - Backtest engines: wire content_filter_warnings from config to run_card - Preflight: display current CONTENT_FILTER_WARNING_THRESHOLD at startup - .env.example: document new env var with default 0.05 (5%) - env_overrides.py: set_env_override() for immediate + persistent adjustment - env_override_tool.py: register as agent tool, gated by VIBE_TRADING_ENABLE_SHELL_TOOLS - context.py: add Runtime Configuration section to system prompt Tests: 4 new test files, 28 new test cases covering ratio tracking, run_card wiring, preflight display, and env override behavior. Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/qwen3.7-max Co-Authored-By: opencode <noreply@ai-tool.com> Co-Authored-By: Claude Code <noreply@anthropic.com> AI-Contributed/Feature: 6/6 AI-Contributed/UT: 419/419
Simulates a full agent run with interleaved content_filter hits: - 2/5 iterations trigger content_filter (40% > 5% threshold) - Verifies: run succeeds, warnings surfaced, trace entries correct, warning message contains ratio and provider-switch recommendation - Clean run test: 0/5 filtered, no warnings, no trace entries Uses real TraceWriter (no mocking), interleaved LLM stub pattern matching the real-world scenario of event-driven backtest on 159516.SZ where semiconductor news triggers DashScope moderation. Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/qwen3.7-max Co-Authored-By: opencode <noreply@ai-tool.com> Co-Authored-By: Claude Code <noreply@anthropic.com> AI-Contributed/Feature: 0/0 AI-Contributed/UT: 220/220
… return paths F1 plan compliance audit found that 5/8 WorkerResult return paths in worker.py did not pass content_filter_warnings, silently dropping accumulated warnings via the default empty list. Fix: extract ratio calculation into _compute_content_filter_warnings() helper and call it at every mid-loop WorkerResult construction: - timeout (~line 449) - token_limit (~line 468) - LLM call failure (~line 573) - incomplete mid-loop (~line 630) - completed mid-loop (~line 642) Pre-loop prompt template failure path unchanged (counter not yet init). Also fixes ruff F841 unused variable in preflight.py:112. Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/qwen3.7-max Co-Authored-By: opencode <noreply@ai-tool.com> Co-Authored-By: Claude Code <noreply@anthropic.com> AI-Contributed/Feature: 58/58 AI-Contributed/UT: 0/0
Simplify the PR by removing the generic env-override tool entirely (-176 LOC, -4 files). The preflight display and .env.example docs already cover threshold discoverability. Also fixes Chinese text in chat.py docstring and context.py system prompt to meet the English-only constraint, and hardens loop.py to use getattr for duck-typed mock LLM compatibility. Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/glm-5.2 Co-Authored-By: opencode <noreply@ai-tool.com> Co-Authored-By: Claude Code <noreply@anthropic.com> AI-Contributed/Feature: 101/101 AI-Contributed/UT: 89/89
- Extract shared content_filter.py module (DRY: threshold parsing, warning computation, skip message constant) - Fix message role inconsistency: loop.py now uses 'system' (was 'user') - Move content_filter check before has_tool_calls in loop.py (matches worker's defensive placement) - Add consecutive-skip circuit breaker (10 consecutive -> early exit) in both loop.py and worker.py - Guard float() env var parsing with try/except (was unguarded in hot paths, only preflight had the guard) - Add content_filter mapping in openai_codex _map_finish_reason - Update CHANGELOG.md and README.md env var table - Add circuit breaker tests for both loop and worker - Fix pre-existing F841 lint error in test_run_card_content_filter.py Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/glm-5.2 Co-Authored-By: opencode <noreply@ai-tool.com> Co-Authored-By: Claude Code <noreply@anthropic.com> AI-Contributed/Feature: 254/254 AI-Contributed/UT: 27/27
Code Review Summary — Content-Filter ResilienceA 5-agent parallel review was conducted on this branch. All identified issues have been fixed and verified. Review Process
Issues Found & FixedMAJOR (blocking — all fixed)
Non-blocking (all addressed)
New Files
Verification
Commit
|
…mmit Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/glm-5.2 Co-Authored-By: opencode <noreply@ai-tool.com> AI-Contributed/Feature: 12/12 AI-Contributed/UT: 0/0
Gemini surfaces content moderation via uppercase FinishReason enum values (SAFETY, RECITATION, BLOCKLIST, PROHIBITED_CONTENT, SPII, IMAGE_SAFETY, IMAGE_PROHIBITED_CONTENT, IMAGE_RECITATION) instead of OpenAI's lowercase "content_filter". Google's OpenAI-compatible endpoint passes these through unmapped, so the existing finish_reason == "content_filter" check silently missed every Gemini content-filter hit — the circuit breaker, skip logic, and warning pipeline from HKUDS#307 were all inactive for the Gemini provider. Add GEMINI_SAFETY_FINISH_REASONS and is_content_filter_triggered() to content_filter.py; ChatLLM._parse_response now calls it instead of the bare equality check. Case-insensitive on the Gemini side so a provider that lowercases the value still matches. Evidence: 7 independent GitHub sources (4 official repos — google-gemini, langchain-ai, firebase, microsoft) confirm Gemini returns uppercase SAFETY, not content_filter, and that client-side mapping is required. Regression coverage: 25 new tests in test_content_filter_gemini_detection.py covering all 8 Gemini safety reasons, OpenAI content_filter regression, case-insensitivity, non-string inputs, and _parse_response integration. Co-Authored-By: OpenCode <noreply@opencode.ai> AI-Model: alibaba-cn/glm-5.2 Co-Authored-By: opencode <noreply@ai-tool.com> Co-Authored-By: Claude Code <noreply@anthropic.com> AI-Contributed/Feature: 36/36 AI-Contributed/UT: 83/83
补充修复:Gemini provider 的 content filter 静默漏检(commit
|
| Gemini 原生值 | 含义 |
|---|---|
SAFETY |
安全策略拦截 |
RECITATION |
版权/复述拦截 |
BLOCKLIST |
术语黑名单 |
PROHIBITED_CONTENT |
违禁内容 |
SPII |
敏感个人信息 |
IMAGE_SAFETY / IMAGE_PROHIBITED_CONTENT / IMAGE_RECITATION |
图像安全 |
Google 的 OpenAI 兼容端点不映射这些值到 "content_filter",而是透传原生大写枚举。现有检测只匹配 "content_filter",所以 Gemini 的每次 content filter 命中都漏检。
证据(7 个独立 GitHub 来源,含 4 个官方仓库)
- google-gemini 官方仓库测试 fixture 用
"finish_reason": "SAFETY" - langchain-ai/langchain-google 官方集成测试用
"finish_reason": "SAFETY" - firebase/flutterfire 官方解析测试用
"finishReason": "SAFETY" - microsoft/semantic-kernel 官方 SSE 解析测试含真实
finishReason: "SAFETY"chunk - bytedance/deer-flow 为 Gemini 单独写
GeminiSafetyDetector(与 OpenAI 的content_filter检测器并存) - BerriAI/litellm 维护显式映射表
SAFETY → content_filter - traceloop/openllmetry 测试断言
extract_finish_reasons(["STOP","SAFETY"]) == ["stop","content_filter"]
修复
content_filter.py新增GEMINI_SAFETY_FINISH_REASONS集合 +is_content_filter_triggered()helper,集中"什么算 content filter"的知识(识别 OpenAIcontent_filter+ Gemini 8 种安全枚举,大小写不敏感)chat.py_parse_response从finish_reason == "content_filter"改为调用is_content_filter_triggered(finish_reason)- 设计上为未来 Claude
stop_reason="refusal"映射预留扩展点(仅需在 helper 加一行)
测试
新增 test_content_filter_gemini_detection.py(25 个测试):8 个 Gemini 安全枚举参数化覆盖 + OpenAI content_filter 回归 + 大小写不敏感 / 非字符串输入 / _parse_response 集成。
全量 content filter 套件 47/47 通过(含 E2E),0 回归。
Claude 说明
项目当前无 Claude/Anthropic provider,暂不适用。但调研确认 Claude 有 stop_reason="refusal"(对标 OpenAI content_filter,仅 Fable 5 产生)+ stop_details 分类(cyber/bio/frontier_llm/reasoning_extraction/military_weapons),未来接入时需映射。
|
Thanks for putting this together and for linking it back to #307. We’ve seen the PR and will review it as soon as we can. Because this touches provider/agent-loop behavior, we’ll pay particular attention to deterministic tests and making sure the content-filter path degrades gracefully without hiding unrelated provider failures. |
|
Merged via squash in We’ll keep watching provider-specific moderation behavior in follow-ups. |
Summary
Why
Changes
Current changes only work for OPEN AI API models.
TODO: support gemini and claude
Test Plan
pytest --ignore=agent/tests/e2e_backtest --tb=short -q)Checklist
src/agent/,src/session/,src/providers/) without prior discussionCloses #307