foxclaw 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/.github/workflows/release.yml +5 -0
- package/python/CHANGELOG.md +100 -0
- package/python/CHANGELOG_CN.md +100 -0
- package/python/README.md +17 -2
- package/python/README_CN.md +38 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/CONTENT_GUARD.md +189 -0
- package/python/docs/CONTENT_GUARD_CN.md +189 -0
- package/python/docs/RELEASE.md +78 -37
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +74 -152
- package/python/foxclaw/agents/base.py +82 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +23 -3
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +59 -12
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/agents/rules/finance/RULES.md +131 -0
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +114 -0
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +50 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/behavior/__init__.py +20 -0
- package/python/foxclaw/behavior/behavior.yaml +62 -0
- package/python/foxclaw/behavior/spec.py +287 -0
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/content_guard.py +341 -0
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +53 -60
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +74 -150
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sensitive_write.py +283 -0
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +18 -201
- package/python/frontend/v3/assets/index-JKfHlb-a.css +1 -0
- package/python/frontend/v3/assets/index-tN90dpUH.js +34 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +42 -3
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +16 -44
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +17 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +4 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_agent_rules.py +81 -0
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_behavior_spec.py +198 -0
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_conflict_dismiss.py +143 -0
- package/python/tests/test_content_guard.py +177 -0
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sensitive_write.py +181 -0
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/build_content_lexicon.py +218 -0
- package/python/tools/bundle_web.py +98 -0
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/recheck_conflicts.py +156 -0
- package/python/tools/release_check.py +24 -15
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +31 -0
- package/python/vendor/hkc/hkc_api/schemas.py +11 -0
- package/python/vendor/hkc/hkc_kee/kee.py +124 -3
- package/python/vendor/hkc/hkc_sdk/base.py +22 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +10 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +19 -0
- package/python/foxclaw/api/app.py +0 -123
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
package/python/bench/cd_audit.py
CHANGED
|
@@ -14,8 +14,6 @@ from hkc.hkc_kde.extractor import Extractor
|
|
|
14
14
|
from hkc.hkc_api.ingest_jobs import IngestJobRegistry
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
# ── D1: IngestJobRegistry 4 thread 并发 ────────────────
|
|
18
|
-
|
|
19
17
|
def d1_ingest_4_workers():
|
|
20
18
|
"""D1. 4 worker 真并发 peak=4,4 任务 ~0.3s 完成"""
|
|
21
19
|
reg = IngestJobRegistry(max_workers=4)
|
|
@@ -36,7 +34,6 @@ def d1_ingest_4_workers():
|
|
|
36
34
|
t0 = time.time()
|
|
37
35
|
for j in jobs:
|
|
38
36
|
reg.submit(j, work)
|
|
39
|
-
# wait done
|
|
40
37
|
while True:
|
|
41
38
|
statuses = [reg.get(j.job_id)["status"] for j in jobs]
|
|
42
39
|
if all(s == "done" for s in statuses):
|
|
@@ -49,8 +46,6 @@ def d1_ingest_4_workers():
|
|
|
49
46
|
print(f" D1 PASS: peak={peak[0]}, 4 jobs in {elapsed*1000:.0f}ms (串行需 1.2s)")
|
|
50
47
|
|
|
51
48
|
|
|
52
|
-
# ── D2: Extractor 16 chunk 限流雪崩 ────────────────────
|
|
53
|
-
|
|
54
49
|
def d2_429_snowball():
|
|
55
50
|
"""D2. 16 chunk × 4 worker × 100% 429 永不成功,验证不死循环 + 总调用有上界"""
|
|
56
51
|
state = {"calls": 0}
|
|
@@ -62,8 +57,6 @@ def d2_429_snowball():
|
|
|
62
57
|
raise err
|
|
63
58
|
|
|
64
59
|
ext = Extractor(provider="anthropic", model="m")
|
|
65
|
-
# 替换 _extract_chunk:走 _retry_llm 但传极小 base_delay 测试用
|
|
66
|
-
# 同时 monkey-patch time.sleep 避免真睡
|
|
67
60
|
import hkc.hkc_kde.extractor as ex_mod
|
|
68
61
|
orig_sleep = ex_mod.time.sleep
|
|
69
62
|
sleeps = []
|
|
@@ -88,7 +81,6 @@ def d2_429_snowball():
|
|
|
88
81
|
result = ext.extract(chunks)
|
|
89
82
|
elapsed = time.time() - t0
|
|
90
83
|
|
|
91
|
-
# 期望:16 chunk 全 fail,总调用 = 16 * (1+3 retry) = 64,无死循环
|
|
92
84
|
success = sum(1 for item in result.items if item is not None)
|
|
93
85
|
assert success == 0, f"expected 0 success, got {success}"
|
|
94
86
|
assert state["calls"] == 64, f"snowball: expected 64 calls, got {state['calls']}"
|
|
@@ -99,17 +91,13 @@ def d2_429_snowball():
|
|
|
99
91
|
ex_mod.time.sleep = orig_sleep
|
|
100
92
|
|
|
101
93
|
|
|
102
|
-
# ── D3: progress_cb 线程安全 ─────────────────────────
|
|
103
|
-
|
|
104
94
|
def d3_progress_cb_thread_safe():
|
|
105
95
|
"""D3. progress_cb 在 as_completed 循环调,主线程串行;16 chunk 计数无重复无漏"""
|
|
106
96
|
ext = Extractor(provider="anthropic", model="m")
|
|
107
97
|
|
|
108
|
-
# 替换 _extract_chunk 为 fast mock:50% 概率 fail(模拟 chunk 失败)
|
|
109
98
|
import random
|
|
110
99
|
random.seed(42)
|
|
111
100
|
def mock_extract(chunk, timeout=60.0):
|
|
112
|
-
# 永远返 RawExtraction
|
|
113
101
|
from hkc.hkc_kde.models import RawExtraction
|
|
114
102
|
return RawExtraction()
|
|
115
103
|
ext._extract_chunk = mock_extract
|
|
@@ -131,21 +119,16 @@ def d3_progress_cb_thread_safe():
|
|
|
131
119
|
chunks = [MockChunk(i) for i in range(16)]
|
|
132
120
|
result = ext.extract(chunks, progress_cb=cb)
|
|
133
121
|
|
|
134
|
-
# 验证:done_calls 长度 16(每个 chunk 一次)
|
|
135
122
|
assert len(done_calls) == 16, f"expected 16 callbacks, got {len(done_calls)}"
|
|
136
|
-
# 验证:1..16 各一次(set 大小 = 16,排序后 1..16)
|
|
137
123
|
assert sorted(done_calls) == list(range(1, 17)), \
|
|
138
124
|
f"expected processed 1..16, got {sorted(done_calls)}"
|
|
139
125
|
print(f" D3 PASS: {len(done_calls)} callbacks, all 1..16 unique")
|
|
140
126
|
|
|
141
127
|
|
|
142
|
-
# ── D4: max_retries 可调 ──────────────────────────────
|
|
143
|
-
|
|
144
128
|
def d4_max_retries_configurable():
|
|
145
129
|
"""D4. _retry_llm 的 max_retries 真的生效"""
|
|
146
130
|
ext = Extractor(provider="mock", model="m")
|
|
147
131
|
|
|
148
|
-
# 测 max_retries=2(默认 3):4 次后 raise
|
|
149
132
|
call_count = [0]
|
|
150
133
|
def always_fail():
|
|
151
134
|
call_count[0] += 1
|
|
@@ -153,7 +136,6 @@ def d4_max_retries_configurable():
|
|
|
153
136
|
err.status_code = 429
|
|
154
137
|
raise err
|
|
155
138
|
|
|
156
|
-
# 用 monkey-patch 把 time.sleep 替换掉,这样不会真睡
|
|
157
139
|
import hkc.hkc_kde.extractor as ex_mod
|
|
158
140
|
orig_sleep = ex_mod.time.sleep
|
|
159
141
|
sleeps = []
|
|
@@ -164,7 +146,6 @@ def d4_max_retries_configurable():
|
|
|
164
146
|
assert False, "expected raise"
|
|
165
147
|
except Exception:
|
|
166
148
|
pass
|
|
167
|
-
# max_retries=2 + 1 = 3 次尝试
|
|
168
149
|
assert call_count[0] == 3, f"max_retries=2 expected 3 calls, got {call_count[0]}"
|
|
169
150
|
assert len(sleeps) == 2, f"max_retries=2 expected 2 sleeps, got {len(sleeps)}"
|
|
170
151
|
print(f" D4 PASS: max_retries=2 -> 3 calls, 2 sleeps")
|
|
@@ -172,8 +153,6 @@ def d4_max_retries_configurable():
|
|
|
172
153
|
ex_mod.time.sleep = orig_sleep
|
|
173
154
|
|
|
174
155
|
|
|
175
|
-
# ── D5: retry + progress_cb 顺序 ─────────────────────
|
|
176
|
-
|
|
177
156
|
def d5_retry_no_progress_leak():
|
|
178
157
|
"""D5. retry 期间 progress_cb 仍只调 1 次/chunk(不管 retry 几次)"""
|
|
179
158
|
import time as time_mod
|
|
@@ -209,9 +188,7 @@ def d5_retry_no_progress_leak():
|
|
|
209
188
|
cb_count[0] += 1
|
|
210
189
|
chunks = [MockChunk(i) for i in range(8)]
|
|
211
190
|
result = ext.extract(chunks, progress_cb=cb)
|
|
212
|
-
# 不管 retry 几次,每个 chunk 调 1 次 progress_cb
|
|
213
191
|
assert cb_count[0] == 8, f"expected 8 progress, got {cb_count[0]}"
|
|
214
|
-
# 8 chunk 全 fail,总调用 = 8 * 4 = 32
|
|
215
192
|
assert state["calls"] == 32, f"expected 32 calls, got {state['calls']}"
|
|
216
193
|
success = sum(1 for item in result.items if item is not None)
|
|
217
194
|
assert success == 0
|
package/python/bench/chaos.py
CHANGED
|
@@ -21,7 +21,6 @@ from pathlib import Path
|
|
|
21
21
|
|
|
22
22
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
23
23
|
|
|
24
|
-
# 关闭联网触发(chaos 只测 LLM 咽喉,不触发搜索)
|
|
25
24
|
import foxclaw.core.web_search as ws # noqa: E402
|
|
26
25
|
ws.needs_search = lambda q: False
|
|
27
26
|
|
|
@@ -37,7 +36,7 @@ def _make_router(primary="deepseek", fallback=None):
|
|
|
37
36
|
if fallback:
|
|
38
37
|
os.environ[PROVIDER_ENV[fallback]] = "chaos-test-key"
|
|
39
38
|
r = ModelRouter(provider=primary)
|
|
40
|
-
r._client = object()
|
|
39
|
+
r._client = object()
|
|
41
40
|
r._clients[primary] = object()
|
|
42
41
|
if fallback:
|
|
43
42
|
r._clients[fallback] = object()
|
|
@@ -87,11 +86,11 @@ def case4_circuit_breaker():
|
|
|
87
86
|
"""[4] 断路器:provider 失败后进入冷却,立即重试被跳过(不再紧密重试)。"""
|
|
88
87
|
r = _make_router("deepseek")
|
|
89
88
|
r._call_provider = _boom
|
|
90
|
-
r.complete(_SYS, _MSG)
|
|
89
|
+
r.complete(_SYS, _MSG)
|
|
91
90
|
cooled = "deepseek" in r._cooldown
|
|
92
91
|
t0 = time.perf_counter()
|
|
93
|
-
r.complete(_SYS, _MSG)
|
|
94
|
-
fast = (time.perf_counter() - t0) < 0.5
|
|
92
|
+
r.complete(_SYS, _MSG)
|
|
93
|
+
fast = (time.perf_counter() - t0) < 0.5
|
|
95
94
|
ok = cooled and fast and r.is_degraded()
|
|
96
95
|
return ok, (f"冷却中={cooled} 跳过耗时={((time.perf_counter()-t0)*1000):.0f}ms"), "断路器生效"
|
|
97
96
|
|
|
@@ -101,11 +100,10 @@ def case5_recovery():
|
|
|
101
100
|
r = _make_router("deepseek")
|
|
102
101
|
r._call_provider = _boom
|
|
103
102
|
r.complete(_SYS, _MSG)
|
|
104
|
-
assert "deepseek" in r._cooldown
|
|
105
|
-
r._cooldown["deepseek"] = 0
|
|
103
|
+
assert "deepseek" in r._cooldown
|
|
104
|
+
r._cooldown["deepseek"] = 0
|
|
106
105
|
r._call_provider = lambda *a, **k: ("恢复后的正常回复", None)
|
|
107
106
|
out = r.complete(_SYS, _MSG)
|
|
108
|
-
# 注:degraded 是「曾降级」的累积运维标志(不因恢复清零),恢复语义看「解冻 + 成功返回」。
|
|
109
107
|
ok = (out == "恢复后的正常回复") and ("deepseek" not in r._cooldown)
|
|
110
108
|
return ok, ("解冻=" + str("deepseek" not in r._cooldown)), out[:40]
|
|
111
109
|
|
|
@@ -26,7 +26,6 @@ from foxclaw.core.model_router import ModelRouter # noqa: E402
|
|
|
26
26
|
_SYS = "你是 Foxclaw 助手。"
|
|
27
27
|
_MSG = [{"role": "user", "content": "请只回复两个字:你好"}]
|
|
28
28
|
_MAX_TOKENS = 64
|
|
29
|
-
# stub/故障 的特征文案(出现即视为"非真实成功")
|
|
30
29
|
_BAD_MARKERS = ("【未执行】", "【模型返回为空】", "【全部模型失败】", "【模型流式中断】", "STUB")
|
|
31
30
|
|
|
32
31
|
|
|
@@ -19,8 +19,6 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'vendor'))
|
|
|
19
19
|
from hkc.hkc_kde.extractor import Extractor
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
# ── Mock 异常类型 ────────────────────────────────────
|
|
23
|
-
|
|
24
22
|
class FakeResponse:
|
|
25
23
|
def __init__(self, headers=None):
|
|
26
24
|
self.headers = headers or {}
|
|
@@ -53,7 +51,6 @@ class FakeRateLimitName(Exception):
|
|
|
53
51
|
"""模拟 openai.RateLimitError(type name 含 'RateLimit',无 status_code)"""
|
|
54
52
|
def __init__(self, msg="rate"):
|
|
55
53
|
super().__init__(msg)
|
|
56
|
-
# 不设 status_code;靠 type name 'RateLimit' 识别
|
|
57
54
|
|
|
58
55
|
|
|
59
56
|
class FakeValueError(Exception):
|
|
@@ -63,9 +60,6 @@ class FakeValueError(Exception):
|
|
|
63
60
|
self.status_code = 400
|
|
64
61
|
|
|
65
62
|
|
|
66
|
-
# ── 测试实例化 ───────────────────────────────────────
|
|
67
|
-
# KnowledgeExtractor __init__ 不调 LLM,只是记 provider/model 配置
|
|
68
|
-
# _retry_llm 是 instance method,任意实例都能调
|
|
69
63
|
ext = Extractor(provider="mock", model="mock-model")
|
|
70
64
|
|
|
71
65
|
|
|
@@ -133,8 +127,8 @@ def t3_429_exhaust():
|
|
|
133
127
|
assert False, "应该 raise"
|
|
134
128
|
except Fake429Error:
|
|
135
129
|
pass
|
|
136
|
-
assert calls[0] == 4
|
|
137
|
-
assert len(sleeps) == 3
|
|
130
|
+
assert calls[0] == 4
|
|
131
|
+
assert len(sleeps) == 3
|
|
138
132
|
print(" 3. 429 4 次后 raise (1+3 retry): PASS, calls={}, sleeps={}".format(calls[0], sleeps))
|
|
139
133
|
finally:
|
|
140
134
|
restore_sleep(ex, original)
|
|
@@ -207,7 +201,6 @@ def t8_backoff():
|
|
|
207
201
|
ext._retry_llm(func, max_retries=3, base_delay=1.0, max_delay=3.0)
|
|
208
202
|
except Fake429Error:
|
|
209
203
|
pass
|
|
210
|
-
# 3 次 retry:sleeps = [1, 2, 3] (4 cap at max_delay=3)
|
|
211
204
|
assert sleeps == [1.0, 2.0, 3.0], f"expected [1,2,3], got {sleeps}"
|
|
212
205
|
print(" 8. 退避 1→2→3 (max_delay cap): PASS, sleeps={}".format(sleeps))
|
|
213
206
|
finally:
|
|
@@ -23,13 +23,12 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
23
23
|
PORT = int(os.environ.get("FOXCLAW_BENCH_PORT", "8231"))
|
|
24
24
|
BASE = f"http://127.0.0.1:{PORT}"
|
|
25
25
|
|
|
26
|
-
# 一批覆盖不同分诊路径的任务模板(每个请求附唯一编号,便于校验会话唯一性)
|
|
27
26
|
TEMPLATES = [
|
|
28
|
-
"分析本月理财预算与收支",
|
|
29
|
-
"总结这篇论文的核心要点",
|
|
30
|
-
"安排明天上午的会议日程",
|
|
31
|
-
"整理并汇总这批资料",
|
|
32
|
-
"你能做什么",
|
|
27
|
+
"分析本月理财预算与收支",
|
|
28
|
+
"总结这篇论文的核心要点",
|
|
29
|
+
"安排明天上午的会议日程",
|
|
30
|
+
"整理并汇总这批资料",
|
|
31
|
+
"你能做什么",
|
|
33
32
|
]
|
|
34
33
|
|
|
35
34
|
|
|
@@ -46,7 +45,6 @@ def _start_server():
|
|
|
46
45
|
import uvicorn
|
|
47
46
|
os.environ.setdefault("FOXCLAW_DATA_DIR", tempfile.mkdtemp())
|
|
48
47
|
os.environ["FOXCLAW_SHADOW_ROUNDS"] = "50"
|
|
49
|
-
# 不配任何 LLM key → Router 自动降级 stub(不联网、瞬时返回)
|
|
50
48
|
for k in ("ANTHROPIC_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY",
|
|
51
49
|
"GOOGLE_API_KEY", "DASHSCOPE_API_KEY"):
|
|
52
50
|
os.environ.pop(k, None)
|
|
@@ -132,7 +130,6 @@ async def main_async(total, concurrency):
|
|
|
132
130
|
print("✗ 服务未就绪"); return 1
|
|
133
131
|
print("服务就绪。先测单请求基线(concurrency=1)...")
|
|
134
132
|
|
|
135
|
-
# 基线:串行 10 个请求,量"单请求真实成本"(无并发争用),用于解释并发下的延迟来源
|
|
136
133
|
base = await _run_load(10, 1)
|
|
137
134
|
base_lat = base["latencies"]
|
|
138
135
|
base_p50 = _pct(base_lat, 50)
|
|
@@ -21,10 +21,8 @@ import time
|
|
|
21
21
|
from pathlib import Path
|
|
22
22
|
from types import SimpleNamespace
|
|
23
23
|
|
|
24
|
-
# 必须在 import VectorIndex 之前设,否则模块级常量已固化
|
|
25
24
|
os.environ["HKC_VECTOR_IVF_THRESHOLD"] = "500"
|
|
26
25
|
|
|
27
|
-
# 让 vendor.hkc.* 可导入(脚本放在 bench/,不在 vendor 树内)
|
|
28
26
|
ROOT = Path(__file__).resolve().parent.parent
|
|
29
27
|
sys.path.insert(0, str(ROOT))
|
|
30
28
|
|
|
@@ -70,7 +68,6 @@ def main() -> None:
|
|
|
70
68
|
idx = VectorIndex(backend=backend)
|
|
71
69
|
print(f"index type 初始: {type(idx._index).__name__}")
|
|
72
70
|
|
|
73
|
-
# ── Phase 1: append < threshold,保持 Flat ──────────────────
|
|
74
71
|
phase1_n = threshold - 1
|
|
75
72
|
print(f"\n[Phase 1] append {phase1_n} 条(< {threshold},应保持 Flat)...")
|
|
76
73
|
for ku in kus[:phase1_n]:
|
|
@@ -79,7 +76,6 @@ def main() -> None:
|
|
|
79
76
|
med, mn, mx = bench_search(idx, "find something about topic 7")
|
|
80
77
|
print(f" search ×100: median={med:.2f}ms min={mn:.2f}ms max={mx:.2f}ms")
|
|
81
78
|
|
|
82
|
-
# ── Phase 2: append 触发升级 Flat → IVF ──────────────────
|
|
83
79
|
print(f"\n[Phase 2] append 第 {threshold} 条(应触发升级)...")
|
|
84
80
|
t0 = time.perf_counter()
|
|
85
81
|
idx.append_ku(kus[phase1_n])
|
|
@@ -91,7 +87,6 @@ def main() -> None:
|
|
|
91
87
|
med, mn, mx = bench_search(idx, "find something about topic 7")
|
|
92
88
|
print(f" search ×100: median={med:.2f}ms min={mn:.2f}ms max={mx:.2f}ms")
|
|
93
89
|
|
|
94
|
-
# ── Phase 3: append 1000 条,验证 IVF 追加正常 ──────────────────
|
|
95
90
|
rest = N - threshold
|
|
96
91
|
print(f"\n[Phase 3] append {rest} 条(应直接 add 到 IVF)...")
|
|
97
92
|
t0 = time.perf_counter()
|
|
@@ -103,7 +98,6 @@ def main() -> None:
|
|
|
103
98
|
med, mn, mx = bench_search(idx, "find something about topic 7")
|
|
104
99
|
print(f" search ×100: median={med:.2f}ms min={mn:.2f}ms max={mx:.2f}ms")
|
|
105
100
|
|
|
106
|
-
# ── Phase 4: save/load roundtrip(IVF 必须正确持久化) ──────────────────
|
|
107
101
|
print(f"\n[Phase 4] save/load roundtrip(IVF 持久化)...")
|
|
108
102
|
with tempfile.TemporaryDirectory() as td:
|
|
109
103
|
p = str(Path(td) / "vec")
|
|
@@ -120,7 +114,7 @@ def main() -> None:
|
|
|
120
114
|
print(f" loaded ntotal={n_loaded}, type={type_loaded}")
|
|
121
115
|
if n_loaded != N:
|
|
122
116
|
print(f" [FAIL] 数据丢失! 期望 {N} 实际 {n_loaded}")
|
|
123
|
-
elif "FlatIP" in type_loaded:
|
|
117
|
+
elif "FlatIP" in type_loaded:
|
|
124
118
|
print(f" [FAIL] IVF 没保存,降回 FlatIP")
|
|
125
119
|
else:
|
|
126
120
|
print(f" [OK] IVF 完整保存/加载(ntotal={n_loaded}, type={type_loaded})")
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# Content Gate and Lexicons
|
|
2
|
+
|
|
3
|
+
This layer does one thing: it makes a deterministic decision on **both the user request and the model output**, and cuts the turn off with a fixed line when something matches.
|
|
4
|
+
|
|
5
|
+
It does not call a model, does not do semantic understanding, and does not replace platform review or human moderation. The prompt layer keeps the model from crossing the line on its own; this layer holds the floor when either the model or the user gets around the prompt.
|
|
6
|
+
|
|
7
|
+
## Three sources of judgement
|
|
8
|
+
|
|
9
|
+
| Source | Where | Configurable | Covers |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| Built-in rules | `_BUILTIN_RULES` in `foxclaw/core/content_guard.py` | No (only `off` disables them) | Six **request-shaped** categories: drug/explosive manufacturing, fraud and money laundering, intrusion into someone else's systems, forged documents, suicide methods, sexual content involving minors |
|
|
12
|
+
| External lexicon | Files listed in `FOXCLAW_CONTENT_LEXICON` | Yes | Whatever you decide — political, sexual, violent categories all live here |
|
|
13
|
+
| Prompt | `KERNEL_PROMPT` §8 (Content boundaries) | Code change | Soft constraint; owns the tone and the refusal pattern |
|
|
14
|
+
|
|
15
|
+
The built-in rules are deliberately narrow: a **verb plus a target** has to appear together. "How do I make explosives" matches; "I bought a book on the history of explosives" does not. The goal is something that works out of the box without interrupting ordinary conversation, not full coverage of every violative phrasing.
|
|
16
|
+
|
|
17
|
+
**Large category lists are not built in.** Review requirements change, the blast radius of false positives is a deployment decision, and tens of thousands of terms do not belong in source. Whether to mount them, and which categories, is your call.
|
|
18
|
+
|
|
19
|
+
## Default behaviour
|
|
20
|
+
|
|
21
|
+
It is on out of the box, with no configuration:
|
|
22
|
+
|
|
23
|
+
- mode defaults to `enforce`
|
|
24
|
+
- only the built-in rules run when no lexicon is mounted
|
|
25
|
+
- a request that matches returns the fixed line immediately — **the request never reaches the model**
|
|
26
|
+
- an output that matches is replaced in full; no fragment of the original survives
|
|
27
|
+
- a streamed output that matches stops emitting at the match
|
|
28
|
+
|
|
29
|
+
The refusal follows the input language and exists in exactly two forms: `Let's talk about something else.` / `我们换个话题聊聊吧。`
|
|
30
|
+
|
|
31
|
+
No explanation, no elaboration, no counter-question, no alternative offered. A refusal must not become a channel for leaking information or continuing the thread.
|
|
32
|
+
|
|
33
|
+
## Three modes
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
FOXCLAW_CONTENT_GUARD_MODE=enforce # default: actually block
|
|
37
|
+
FOXCLAW_CONTENT_GUARD_MODE=audit # record only, for measuring false positives
|
|
38
|
+
FOXCLAW_CONTENT_GUARD_MODE=off # disabled
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
An unrecognised value falls back to `enforce` — it over-blocks rather than failing silently.
|
|
42
|
+
|
|
43
|
+
`audit` hits are written to the **process stdout**:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
[Foxclaw] ⚠ 内容命中(未拦截): category=politics word='...'
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Only `audit` prints. `enforce` hits are silent. If you need an audit trail, wire `Decision.to_dict()` at the call site, or run `audit` for a while to collect a baseline first.
|
|
50
|
+
|
|
51
|
+
## Where to put the configuration
|
|
52
|
+
|
|
53
|
+
Two paths, different behaviour:
|
|
54
|
+
|
|
55
|
+
- **`.env` (recommended)**: the API service loads `.env` before importing the package, so values placed there take effect. Empty values count as "not configured" and never override a real environment variable.
|
|
56
|
+
- **Process environment**: when you use `Foxclaw()` from the SDK, `.env` is not loaded for you — export the variables before the process starts.
|
|
57
|
+
|
|
58
|
+
The `.env` load can be skipped with `FOXCLAW_SKIP_DOTENV=1`.
|
|
59
|
+
|
|
60
|
+
## Lexicon format
|
|
61
|
+
|
|
62
|
+
One term per line, UTF-8, `#` for comments:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
# term<TAB>category; the category is optional
|
|
66
|
+
some-term politics
|
|
67
|
+
another-term sexual
|
|
68
|
+
term-without-category
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The details below come from the loader, not from convention:
|
|
72
|
+
|
|
73
|
+
| Item | Behaviour |
|
|
74
|
+
|---|---|
|
|
75
|
+
| Separator | `term<TAB>category`; with no TAB the whole line is the term and the category becomes `platform` |
|
|
76
|
+
| Comments | lines starting with `#` are skipped |
|
|
77
|
+
| Whitespace and punctuation | every term goes through `compact()`: whitespace and CJK/ASCII punctuation stripped, NFKC-normalised, lowercased |
|
|
78
|
+
| Length | anything shorter than 2 characters **after** `compact()` is dropped (single characters cause too much collateral damage and rarely catch real violations) |
|
|
79
|
+
| Same term, several categories | allowed; scan results are deduplicated by `(category, term)` and the first hit wins |
|
|
80
|
+
| Bad lines, unreadable files | skipped silently — a bad lexicon must not prevent the service from starting |
|
|
81
|
+
| When it takes effect | the index is built **at process start**; editing a lexicon requires a restart, there is no hot reload |
|
|
82
|
+
|
|
83
|
+
Prefer **absolute paths**. Relative paths resolve against the process working directory, which under systemd is often not the project root.
|
|
84
|
+
|
|
85
|
+
After a restart, confirm how many terms actually loaded:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
python -c "from foxclaw.core.content_guard import ContentGuard as C; print(C().lexicon_size)"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`0` means nothing was read.
|
|
92
|
+
|
|
93
|
+
## Converting a third-party list
|
|
94
|
+
|
|
95
|
+
Existing lists — the ~60k-term main dictionary of `houbb/sensitive-word`, or the versions already split into political / sexual / violent files — come in incompatible shapes: single file of bare terms or one file per category, GBK encoding, and the same term written three ways (`敏感词`, `敏 感 词`, `SENSITIVE`).
|
|
96
|
+
|
|
97
|
+
`tools/build_content_lexicon.py` does the conversion. **The script lives in the repository and is not shipped in the wheel** — if you installed from PyPI, take it from the GitHub repository:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# single-file dictionary, everything under the `platform` category
|
|
101
|
+
python tools/build_content_lexicon.py sensitive_word_dict.txt -o content_lexicon.txt
|
|
102
|
+
|
|
103
|
+
# per-category files: a CJK file name becomes the category (政治类.txt → 政治类)
|
|
104
|
+
python tools/build_content_lexicon.py dict/*.txt -o content_lexicon.txt
|
|
105
|
+
|
|
106
|
+
# keep only what you want
|
|
107
|
+
python tools/build_content_lexicon.py dict/*.txt --exclude 政治类,色情类 --exclude "暴力*" -o content_lexicon.txt
|
|
108
|
+
|
|
109
|
+
# single file, explicit category name
|
|
110
|
+
python tools/build_content_lexicon.py ads.txt --category ads -o content_lexicon.txt
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
It does four things:
|
|
114
|
+
|
|
115
|
+
1. **Detects the encoding**: `utf-8-sig` first, then `gb18030`. Many lists downloaded inside China are GBK, while the loader reads UTF-8 and ignores undecodable bytes — skip this step and terms go missing silently after mounting.
|
|
116
|
+
2. **Normalises during conversion**, using the loader's own `compact()`, so duplicate spellings collapse now instead of bloating the automaton later.
|
|
117
|
+
3. **Filters by category**: `--include` / `--exclude` accept wildcards, comma-separated and repeatable.
|
|
118
|
+
4. **Verifies by reading back**: loads the output through a real `ContentGuard` and samples terms to confirm they still match; exits with code 1 if that fails.
|
|
119
|
+
|
|
120
|
+
Common options:
|
|
121
|
+
|
|
122
|
+
| Option | Default | Meaning |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `-o, --output` | `content_lexicon.txt` | output path |
|
|
125
|
+
| `--category` | `platform` | category used when a file name has no CJK characters |
|
|
126
|
+
| `--include` / `--exclude` | none | category filters, wildcards supported |
|
|
127
|
+
| `--encoding` | `auto` | force a specific codec |
|
|
128
|
+
| `--min-len` | `2` | drop shorter terms (matches the loader) |
|
|
129
|
+
| `--max-len` | `0` | drop longer terms, `0` means no limit |
|
|
130
|
+
| `--sample` | `5` | terms sampled during verification |
|
|
131
|
+
| `--no-verify` | off | skip the read-back check |
|
|
132
|
+
|
|
133
|
+
At 60k terms, conversion plus load and verification takes about half a second.
|
|
134
|
+
|
|
135
|
+
Generated lexicons are ignored by git (`/content_lexicon*.txt`). Do not commit them.
|
|
136
|
+
|
|
137
|
+
## Run `audit` before you enforce
|
|
138
|
+
|
|
139
|
+
**This is the step most likely to be skipped, and the most expensive to skip.**
|
|
140
|
+
|
|
141
|
+
A 60k-term list inevitably contains a lot of two-character terms. Mount it under `enforce` and ordinary order discussions, manuscript reviews, or medical questions will get cut off — the whole output is replaced, the user gets no explanation, only a sudden "let's talk about something else".
|
|
142
|
+
|
|
143
|
+
Suggested sequence:
|
|
144
|
+
|
|
145
|
+
1. mount the lexicon, set `FOXCLAW_CONTENT_GUARD_MODE=audit`
|
|
146
|
+
2. use the product normally for a few days, collect the hit lines from stdout
|
|
147
|
+
3. review `category` and `word` for each: genuine violation or collateral damage
|
|
148
|
+
4. drop the noisy categories with `--exclude`, or regenerate with longer terms
|
|
149
|
+
5. switch to `enforce` once the rate is acceptable
|
|
150
|
+
|
|
151
|
+
There is no allowlist mechanism. The only levers against false positives are: mount fewer categories, use longer terms, raise `--min-len`.
|
|
152
|
+
|
|
153
|
+
## Fidelity, stated plainly
|
|
154
|
+
|
|
155
|
+
This is a **speed bump**, not a wall. It explicitly does not handle:
|
|
156
|
+
|
|
157
|
+
- pinyin, homophones, split characters, transliteration (endless, and worse for false positives)
|
|
158
|
+
- images, audio, attachment bodies
|
|
159
|
+
- multi-turn grooming (splitting a sentence across three turns)
|
|
160
|
+
- books, statutes, and medical texts imported into the knowledge base
|
|
161
|
+
|
|
162
|
+
That last point is intentional: **the filter only sits on "user request" and "model output"**, and never touches the user's own material. A history book that describes an explosive formula should not fail to import.
|
|
163
|
+
|
|
164
|
+
Streaming has an inherent limit: at the moment of the match it can only guarantee that nothing further is emitted — **a prefix already streamed out cannot be recalled**. The non-streaming path replaces the whole output and has no such gap.
|
|
165
|
+
|
|
166
|
+
## Troubleshooting
|
|
167
|
+
|
|
168
|
+
| Symptom | Check |
|
|
169
|
+
|---|---|
|
|
170
|
+
| Nothing is blocked at all | is `FOXCLAW_CONTENT_GUARD_MODE` set to `off`; did the variable actually reach the process |
|
|
171
|
+
| Built-in rules fire, lexicon does not | is `lexicon_size` 0; relative vs absolute path; file encoding |
|
|
172
|
+
| Some mounted terms never match | was the term under 2 characters after `compact()`; did a space become part of the term |
|
|
173
|
+
| Edited the lexicon, nothing changed | the process was not restarted; the index is built at start |
|
|
174
|
+
| Normal conversation gets cut | too many two-character terms; switch to `audit` and drop that category |
|
|
175
|
+
|
|
176
|
+
## Relationship to the prompt layer
|
|
177
|
+
|
|
178
|
+
Different jobs, both required:
|
|
179
|
+
|
|
180
|
+
- the prompt layer (`KERNEL_PROMPT` §8 plus `no_harmful_content` in `behavior.yaml`) owns the semantic boundary: what counts as violative, how a refusal should sound, how to tell the user's material from the user's instructions.
|
|
181
|
+
- the code layer (this document) owns the deterministic floor: a match is a match, no model judgement involved.
|
|
182
|
+
|
|
183
|
+
With only the prompt, a successful jailbreak leaves nothing behind it. With only the code, every edge case lives or dies by the lexicon, and answers turn mechanical. Together, the model knows where the line is and there is a backstop when it crosses anyway.
|
|
184
|
+
|
|
185
|
+
## Scope of responsibility
|
|
186
|
+
|
|
187
|
+
This repository ships no political, sexual, or violent term list. Whether to mount one, which categories, and which mode are deployment decisions — no policy is assumed here.
|
|
188
|
+
|
|
189
|
+
The layer is string matching after normalisation: it does not guarantee coverage and is not a content moderation or compliance service. Deployers who expose the service publicly are responsible for their own content compliance. The software is provided "as is", see [LICENSE](../LICENSE).
|