foxclaw 0.1.0 → 0.1.2
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/README.md +54 -29
- package/bin/foxclaw.cjs +51 -0
- package/index.js +11 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +165 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +122 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +192 -0
- package/python/README_CN.md +324 -0
- package/python/bench/bm25_audit.py +199 -0
- package/python/bench/bm25_regression.py +178 -0
- package/python/bench/bm25_stress.py +159 -0
- package/python/bench/cd_audit.py +238 -0
- package/python/bench/chaos.py +142 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +95 -0
- package/python/bench/llm_retry_test.py +248 -0
- package/python/bench/load_test.py +196 -0
- package/python/bench/vector_ivf_upgrade.py +134 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +109 -0
- package/python/docs/RUN.md +75 -0
- package/python/docs/RUN_CN.md +121 -0
- package/python/docs//351/205/215/347/275/256/346/214/207/345/215/227.md +152 -0
- package/python/evals/__init__.py +0 -0
- package/python/evals/governance/README.md +61 -0
- package/python/evals/governance/__init__.py +0 -0
- package/python/evals/governance/cases.py +152 -0
- package/python/evals/governance/compare_embeddings.py +94 -0
- package/python/evals/governance/convergence.svg +11 -0
- package/python/evals/governance/report.json +371 -0
- package/python/evals/governance/run_evals.py +400 -0
- package/python/examples/governance_middleware_demo.py +77 -0
- package/python/examples/langgraph_governance_demo.py +108 -0
- package/python/examples/sample_itinerary.csv +6 -0
- package/python/examples/sample_knowledge.csv +9 -0
- package/python/examples/sample_transactions.csv +14 -0
- package/python/foxclaw/__init__.py +526 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +865 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +78 -0
- package/python/foxclaw/agents/impl/finance.py +248 -0
- package/python/foxclaw/agents/impl/office.py +367 -0
- package/python/foxclaw/agents/impl/supervisor.py +659 -0
- package/python/foxclaw/agents/mixins/_context.py +151 -0
- package/python/foxclaw/agents/mixins/augment.py +149 -0
- package/python/foxclaw/agents/mixins/llm.py +66 -0
- package/python/foxclaw/agents/mixins/memory.py +35 -0
- package/python/foxclaw/agents/mixins/preference.py +81 -0
- package/python/foxclaw/agents/mixins/prompt.py +77 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +443 -0
- package/python/foxclaw/api/__init__.py +283 -0
- package/python/foxclaw/api/app.py +123 -0
- package/python/foxclaw/api/middleware.py +317 -0
- package/python/foxclaw/api/routes/ahs.py +417 -0
- package/python/foxclaw/api/routes/attachments.py +117 -0
- package/python/foxclaw/api/routes/browser.py +39 -0
- package/python/foxclaw/api/routes/domain.py +314 -0
- package/python/foxclaw/api/routes/knowledge.py +163 -0
- package/python/foxclaw/api/routes/llm_admin.py +461 -0
- package/python/foxclaw/api/routes/memory.py +89 -0
- package/python/foxclaw/api/routes/orchestration.py +41 -0
- package/python/foxclaw/api/routes/recovery.py +43 -0
- package/python/foxclaw/api/routes/review.py +275 -0
- package/python/foxclaw/api/routes/search.py +37 -0
- package/python/foxclaw/api/routes/system.py +327 -0
- package/python/foxclaw/api/routes/task.py +281 -0
- package/python/foxclaw/api/routes/ws.py +68 -0
- package/python/foxclaw/api/spine.py +55 -0
- package/python/foxclaw/browser/__init__.py +13 -0
- package/python/foxclaw/browser/action/__init__.py +1 -0
- package/python/foxclaw/browser/action/action_executor.py +54 -0
- package/python/foxclaw/browser/action/action_types.py +45 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +147 -0
- package/python/foxclaw/browser/adapter/__init__.py +1 -0
- package/python/foxclaw/browser/adapter/base_adapter.py +23 -0
- package/python/foxclaw/browser/adapter/web_adapter.py +46 -0
- package/python/foxclaw/browser/driver/__init__.py +1 -0
- package/python/foxclaw/browser/driver/base_driver.py +61 -0
- package/python/foxclaw/browser/driver/playwright_driver.py +166 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +43 -0
- package/python/foxclaw/browser/perception/element_indexer.py +57 -0
- package/python/foxclaw/browser/perception/semantic_reducer.py +33 -0
- package/python/foxclaw/browser/perception/snapshot.py +29 -0
- package/python/foxclaw/browser/service.py +148 -0
- package/python/foxclaw/browser/session/__init__.py +1 -0
- package/python/foxclaw/browser/session/session_manager.py +57 -0
- package/python/foxclaw/capability/__init__.py +12 -0
- package/python/foxclaw/capability/embedding_service.py +67 -0
- package/python/foxclaw/channels/__init__.py +140 -0
- package/python/foxclaw/channels/discord.py +184 -0
- package/python/foxclaw/channels/dispatcher.py +204 -0
- package/python/foxclaw/channels/feishu.py +225 -0
- package/python/foxclaw/channels/openclaw_bridge.py +156 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +121 -0
- package/python/foxclaw/channels/weixin.py +225 -0
- package/python/foxclaw/cli.py +154 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +82 -0
- package/python/foxclaw/cognitive/state.py +147 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +133 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +283 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +157 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +151 -0
- package/python/foxclaw/core/gateway.py +85 -0
- package/python/foxclaw/core/governance_audit.py +175 -0
- package/python/foxclaw/core/idempotency.py +289 -0
- package/python/foxclaw/core/injection_guard.py +221 -0
- package/python/foxclaw/core/memory_adapter.py +516 -0
- package/python/foxclaw/core/metrics.py +89 -0
- package/python/foxclaw/core/model_router.py +1097 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +362 -0
- package/python/foxclaw/core/scheduler.py +144 -0
- package/python/foxclaw/core/secrets.py +134 -0
- package/python/foxclaw/core/sessions.py +157 -0
- package/python/foxclaw/core/settings.py +430 -0
- package/python/foxclaw/core/shared_write.py +198 -0
- package/python/foxclaw/core/steering_queue.py +153 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +318 -0
- package/python/foxclaw/core/token_meter.py +235 -0
- package/python/foxclaw/core/tools.py +172 -0
- package/python/foxclaw/core/tracing.py +390 -0
- package/python/foxclaw/core/truth_pipeline.py +114 -0
- package/python/foxclaw/core/vec_recall_adapter.py +503 -0
- package/python/foxclaw/core/verify.py +181 -0
- package/python/foxclaw/core/web_search.py +242 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +153 -0
- package/python/foxclaw/finance/fx.py +100 -0
- package/python/foxclaw/finance/graph.py +188 -0
- package/python/foxclaw/finance/models.py +150 -0
- package/python/foxclaw/finance/nlparse.py +575 -0
- package/python/foxclaw/finance/policy.py +329 -0
- package/python/foxclaw/finance/reasoner.py +129 -0
- package/python/foxclaw/finance/reasoning.py +340 -0
- package/python/foxclaw/finance/reconcile.py +180 -0
- package/python/foxclaw/finance/runtime.py +527 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +184 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +72 -0
- package/python/foxclaw/harness/_policy.py +359 -0
- package/python/foxclaw/harness/drift.py +89 -0
- package/python/foxclaw/harness/event_bus.py +31 -0
- package/python/foxclaw/harness/harness.py +446 -0
- package/python/foxclaw/harness/loop_detection.py +152 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +117 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +101 -0
- package/python/foxclaw/itinerary/reasoning.py +174 -0
- package/python/foxclaw/itinerary/runtime.py +148 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +73 -0
- package/python/foxclaw/kernel/policy_gate.py +114 -0
- package/python/foxclaw/kernel/process_manager.py +205 -0
- package/python/foxclaw/kernel/scheduler.py +131 -0
- package/python/foxclaw/kernel/sentinel.py +453 -0
- package/python/foxclaw/kernel/sentinel_client.py +246 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +376 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +92 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +149 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +70 -0
- package/python/foxclaw/learning/runtime.py +363 -0
- package/python/foxclaw/learning/spaced_repetition.py +96 -0
- package/python/foxclaw/mcp_server.py +189 -0
- package/python/foxclaw/orchestration/__init__.py +184 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +481 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +230 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +99 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +194 -0
- package/python/foxclaw/search/evidence.py +113 -0
- package/python/foxclaw/search/external_search.py +50 -0
- package/python/foxclaw/search/hkc_read_port.py +60 -0
- package/python/foxclaw/search/internal_search.py +73 -0
- package/python/foxclaw/search/models.py +129 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +134 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +613 -0
- package/python/foxclaw/skills/hooks_spec.py +73 -0
- package/python/foxclaw/skills/lifecycle.py +278 -0
- package/python/foxclaw/skills/markdown_skill.py +174 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +190 -0
- package/python/foxclaw/skills/mcp_store.py +43 -0
- package/python/foxclaw/skills/permission.py +180 -0
- package/python/foxclaw/skills/pipeline.py +386 -0
- package/python/foxclaw/skills/policy.py +80 -0
- package/python/foxclaw/skills/review.py +72 -0
- package/python/foxclaw/skills/store.py +57 -0
- package/python/frontend/assets/core_bg.jpg +0 -0
- package/python/frontend/deck/auth.js +32 -0
- package/python/frontend/deck/chat_model_picker.js +537 -0
- package/python/frontend/deck/domain.js +296 -0
- package/python/frontend/deck/main.js +1950 -0
- package/python/frontend/deck/markdown.js +48 -0
- package/python/frontend/deck/settings.js +324 -0
- package/python/frontend/deck/state.js +6 -0
- package/python/frontend/deck/util.js +6 -0
- package/python/frontend/deck/visuals.js +212 -0
- package/python/frontend/deck.css +855 -0
- package/python/frontend/deck.html +686 -0
- package/python/frontend/deck_v2.html +341 -0
- package/python/frontend/planet/index.html +266 -0
- package/python/frontend/planet/planet.css +579 -0
- package/python/frontend/planet/planet.js +1637 -0
- package/python/frontend/planet/vendor/three.core.min.js +6 -0
- package/python/frontend/planet/vendor/three.module.min.js +6 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +33 -0
- package/python/frontend/v3/assets/index-CAvnYWGH.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +295 -0
- package/python/frontend/widgets.js +696 -0
- package/python/frontend_src/index.html +14 -0
- package/python/frontend_src/package-lock.json +1376 -0
- package/python/frontend_src/package.json +19 -0
- package/python/frontend_src/src/App.vue +62 -0
- package/python/frontend_src/src/api.js +89 -0
- package/python/frontend_src/src/components/ChatDock.vue +377 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +171 -0
- package/python/frontend_src/src/components/Inspector.vue +241 -0
- package/python/frontend_src/src/components/LeftRail.vue +394 -0
- package/python/frontend_src/src/components/MenuPopover.vue +130 -0
- package/python/frontend_src/src/components/MessageStream.vue +401 -0
- package/python/frontend_src/src/components/ModelPicker.vue +355 -0
- package/python/frontend_src/src/components/QuickIngest.vue +118 -0
- package/python/frontend_src/src/components/Stage.vue +226 -0
- package/python/frontend_src/src/components/ToolMenu.vue +144 -0
- package/python/frontend_src/src/components/TopBar.vue +90 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +84 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +86 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +441 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +118 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +51 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +308 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +254 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +74 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +640 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +48 -0
- package/python/frontend_src/src/lib/md.js +204 -0
- package/python/frontend_src/src/main.js +7 -0
- package/python/frontend_src/src/stores/app.js +106 -0
- package/python/frontend_src/src/stores/chat.js +484 -0
- package/python/frontend_src/src/stores/model.js +104 -0
- package/python/frontend_src/src/stores/panels.js +330 -0
- package/python/frontend_src/src/styles/base.css +138 -0
- package/python/frontend_src/src/styles/panel.css +117 -0
- package/python/frontend_src/src/styles/tokens.css +78 -0
- package/python/frontend_src/vite.config.js +46 -0
- package/python/pyproject.toml +265 -0
- package/python/skills/README.md +123 -0
- package/python/skills/execution-contract/SKILL.md +30 -0
- package/python/skills/general-assistant/SKILL.md +34 -0
- package/python/skills/planner/SKILL.md +33 -0
- package/python/skills/schedule-itinerary/SKILL.md +35 -0
- package/python/skills/skill-composition/SKILL.md +31 -0
- package/python/skills/skill-guard/SKILL.md +67 -0
- package/python/skills/skill-memory/SKILL.md +50 -0
- package/python/skills/skill-router/SKILL.md +35 -0
- package/python/tests/conftest.py +76 -0
- package/python/tests/run_all.py +263 -0
- package/python/tests/test_agent_lifecycle.py +360 -0
- package/python/tests/test_ahs_p2.py +414 -0
- package/python/tests/test_anthropic_tools.py +206 -0
- package/python/tests/test_api.py +87 -0
- package/python/tests/test_api_auth.py +83 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +167 -0
- package/python/tests/test_audit_chain.py +121 -0
- package/python/tests/test_audit_sink.py +136 -0
- package/python/tests/test_browser.py +153 -0
- package/python/tests/test_browser_request_guard.py +184 -0
- package/python/tests/test_capabilities.py +158 -0
- package/python/tests/test_channel_signing.py +192 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +184 -0
- package/python/tests/test_cli.py +46 -0
- package/python/tests/test_compaction_toolpair.py +119 -0
- package/python/tests/test_config_layering.py +65 -0
- package/python/tests/test_confirm_nonce.py +197 -0
- package/python/tests/test_context_compaction_wiring.py +130 -0
- package/python/tests/test_dispatch.py +95 -0
- package/python/tests/test_domain_evals.py +403 -0
- package/python/tests/test_domain_import.py +78 -0
- package/python/tests/test_domain_reasoners.py +148 -0
- package/python/tests/test_e2e.py +178 -0
- package/python/tests/test_emb_cache.py +107 -0
- package/python/tests/test_embedding_singleton.py +89 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +114 -0
- package/python/tests/test_event_spine.py +182 -0
- package/python/tests/test_evidence_semantic.py +101 -0
- package/python/tests/test_evidence_sse.py +97 -0
- package/python/tests/test_file_ingest.py +146 -0
- package/python/tests/test_finance.py +153 -0
- package/python/tests/test_finance_ledger.py +252 -0
- package/python/tests/test_finance_persist.py +178 -0
- package/python/tests/test_gateway.py +144 -0
- package/python/tests/test_governance.py +468 -0
- package/python/tests/test_governance_audit.py +359 -0
- package/python/tests/test_governance_middleware.py +143 -0
- package/python/tests/test_hkc.py +286 -0
- package/python/tests/test_hkc_index_build_manager.py +339 -0
- package/python/tests/test_hkc_manifest_reuse.py +317 -0
- package/python/tests/test_hkc_search_event_sync.py +533 -0
- package/python/tests/test_import_path_guard.py +161 -0
- package/python/tests/test_injection_guard.py +211 -0
- package/python/tests/test_integration.py +141 -0
- package/python/tests/test_itinerary.py +103 -0
- package/python/tests/test_kee_semantic.py +89 -0
- package/python/tests/test_kernel.py +222 -0
- package/python/tests/test_learning.py +110 -0
- package/python/tests/test_llm_admin.py +160 -0
- package/python/tests/test_llm_planner.py +366 -0
- package/python/tests/test_markdown_skills.py +73 -0
- package/python/tests/test_markdown_skills_openclaw.py +71 -0
- package/python/tests/test_mcp_connectors.py +109 -0
- package/python/tests/test_mcp_guardian_discovery.py +91 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +77 -0
- package/python/tests/test_mcp_server.py +95 -0
- package/python/tests/test_memory_recall.py +95 -0
- package/python/tests/test_metrics.py +66 -0
- package/python/tests/test_multi_chain.py +102 -0
- package/python/tests/test_multimodal.py +135 -0
- package/python/tests/test_negotiation.py +146 -0
- package/python/tests/test_orchestration.py +119 -0
- package/python/tests/test_orchestration_decoupled.py +109 -0
- package/python/tests/test_p3_p5_hardening.py +338 -0
- package/python/tests/test_parallel_dispatch.py +215 -0
- package/python/tests/test_pipeline.py +533 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +95 -0
- package/python/tests/test_policy_gate.py +109 -0
- package/python/tests/test_reasoner.py +185 -0
- package/python/tests/test_release_check.py +263 -0
- package/python/tests/test_resume_checkpoint.py +156 -0
- package/python/tests/test_routing.py +95 -0
- package/python/tests/test_scheduler.py +141 -0
- package/python/tests/test_search.py +204 -0
- package/python/tests/test_secrets.py +159 -0
- package/python/tests/test_semantic.py +78 -0
- package/python/tests/test_sessions.py +91 -0
- package/python/tests/test_settings.py +281 -0
- package/python/tests/test_shadow_learning.py +330 -0
- package/python/tests/test_shared_write.py +207 -0
- package/python/tests/test_skill_taint_gate.py +154 -0
- package/python/tests/test_skills.py +133 -0
- package/python/tests/test_stats_and_events.py +165 -0
- package/python/tests/test_streaming.py +60 -0
- package/python/tests/test_supervisor_pipeline.py +177 -0
- package/python/tests/test_supervisor_triage.py +108 -0
- package/python/tests/test_taint.py +117 -0
- package/python/tests/test_task_stream.py +85 -0
- package/python/tests/test_thinking_sse.py +67 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +129 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +103 -0
- package/python/tests/test_tool_use.py +242 -0
- package/python/tests/test_toplevel_concurrency.py +117 -0
- package/python/tests/test_tracing.py +290 -0
- package/python/tests/test_truth_crystallizer.py +127 -0
- package/python/tests/test_truth_flow.py +93 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +225 -0
- package/python/tests/test_version_sync.py +71 -0
- package/python/tests/test_web_trigger.py +99 -0
- package/python/tools/check_faiss_type.py +31 -0
- package/python/tools/check_log.ps1 +46 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +5 -0
- package/python/tools/mypy_ratchet.py +97 -0
- package/python/tools/pack.py +178 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +74 -0
- package/python/tools/shared_write_audit.py +204 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/verify_env.bat +2 -0
- package/python/tools/verify_env.py +25 -0
- package/python/vendor/HMR_LICENSE +37 -0
- package/python/vendor/HMR_README.md +65 -0
- package/python/vendor/hkc/LICENSE +37 -0
- package/python/vendor/hkc/hkc_ace/__init__.py +0 -0
- package/python/vendor/hkc/hkc_ace/ace.py +457 -0
- package/python/vendor/hkc/hkc_ace/skill_taxonomy.json +465 -0
- package/python/vendor/hkc/hkc_api/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/container.py +416 -0
- package/python/vendor/hkc/hkc_api/ingest_jobs.py +103 -0
- package/python/vendor/hkc/hkc_api/main.py +157 -0
- package/python/vendor/hkc/hkc_api/routes/__init__.py +0 -0
- package/python/vendor/hkc/hkc_api/routes/ability.py +151 -0
- package/python/vendor/hkc/hkc_api/routes/config.py +51 -0
- package/python/vendor/hkc/hkc_api/routes/knowledge.py +630 -0
- package/python/vendor/hkc/hkc_api/routes/search.py +88 -0
- package/python/vendor/hkc/hkc_api/schemas.py +153 -0
- package/python/vendor/hkc/hkc_api/synthesis.py +179 -0
- package/python/vendor/hkc/hkc_core/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/graph/base.py +188 -0
- package/python/vendor/hkc/hkc_core/graph/sqlite_store.py +789 -0
- package/python/vendor/hkc/hkc_core/models/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/models/enums.py +65 -0
- package/python/vendor/hkc/hkc_core/models/ku.py +307 -0
- package/python/vendor/hkc/hkc_core/utils/__init__.py +0 -0
- package/python/vendor/hkc/hkc_core/utils/id_gen.py +113 -0
- package/python/vendor/hkc/hkc_crystallizer/__init__.py +44 -0
- package/python/vendor/hkc/hkc_crystallizer/adapters.py +94 -0
- package/python/vendor/hkc/hkc_crystallizer/candidate.py +145 -0
- package/python/vendor/hkc/hkc_crystallizer/crystallizer.py +179 -0
- package/python/vendor/hkc/hkc_crystallizer/ingress.py +90 -0
- package/python/vendor/hkc/hkc_kde/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/chunker.py +197 -0
- package/python/vendor/hkc/hkc_kde/extractor.py +623 -0
- package/python/vendor/hkc/hkc_kde/kde.py +298 -0
- package/python/vendor/hkc/hkc_kde/loaders/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kde/loaders/ebook_loader.py +153 -0
- package/python/vendor/hkc/hkc_kde/loaders/office_loader.py +194 -0
- package/python/vendor/hkc/hkc_kde/loaders/pdf_loader.py +162 -0
- package/python/vendor/hkc/hkc_kde/loaders/text_loader.py +157 -0
- package/python/vendor/hkc/hkc_kde/loaders/web_loader.py +91 -0
- package/python/vendor/hkc/hkc_kde/models.py +74 -0
- package/python/vendor/hkc/hkc_kde/packager.py +329 -0
- package/python/vendor/hkc/hkc_kde/relation_disc.py +151 -0
- package/python/vendor/hkc/hkc_kee/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kee/dedup.py +227 -0
- package/python/vendor/hkc/hkc_kee/fingerprint.py +106 -0
- package/python/vendor/hkc/hkc_kee/kee.py +541 -0
- package/python/vendor/hkc/hkc_kep/__init__.py +0 -0
- package/python/vendor/hkc/hkc_kep/event_bus.py +205 -0
- package/python/vendor/hkc/hkc_sdk/__init__.py +63 -0
- package/python/vendor/hkc/hkc_sdk/base.py +151 -0
- package/python/vendor/hkc/hkc_sdk/direct_client.py +153 -0
- package/python/vendor/hkc/hkc_sdk/exceptions.py +39 -0
- package/python/vendor/hkc/hkc_sdk/http_client.py +166 -0
- package/python/vendor/hkc/hkc_sdk/models.py +171 -0
- package/python/vendor/hkc/hkc_search/__init__.py +0 -0
- package/python/vendor/hkc/hkc_search/bm25.py +325 -0
- package/python/vendor/hkc/hkc_search/embedding_backends.py +374 -0
- package/python/vendor/hkc/hkc_search/graph_search.py +132 -0
- package/python/vendor/hkc/hkc_search/hybrid.py +331 -0
- package/python/vendor/hkc/hkc_search/index_build_manager.py +378 -0
- package/python/vendor/hkc/hkc_search/vector_search.py +484 -0
- package/python/vendor/hmr/__init__.py +0 -0
- package/python/vendor/hmr/core/__init__.py +0 -0
- package/python/vendor/hmr/core/hmr.py +684 -0
- package/python/vendor/hmr/core/models.py +310 -0
- package/python/vendor/hmr/engines/__init__.py +0 -0
- package/python/vendor/hmr/engines/evolution.py +710 -0
- package/python/vendor/hmr/engines/jit_compiler.py +472 -0
- package/python/vendor/hmr/engines/lifecycle.py +264 -0
- package/python/vendor/hmr/engines/policy.py +625 -0
- package/python/vendor/hmr/engines/recall.py +260 -0
- package/python/vendor/hmr/engines/runtime_state.py +158 -0
- package/python/vendor/hmr/engines/scheduler.py +323 -0
- package/python/vendor/hmr/engines/semantic.py +150 -0
- package/python/vendor/hmr/engines/temporal.py +261 -0
- package/python/vendor/hmr/engines/thought_chain.py +635 -0
- package/python/vendor/hmr/graph/__init__.py +0 -0
- package/python/vendor/hmr/graph/cwg.py +338 -0
- package/python/vendor/hmr/graph/memory_graph.py +530 -0
- package/python/vendor/hmr/storage/__init__.py +0 -0
- package/python/vendor/hmr/storage/memory_fs.py +300 -0
- package/python/vendor/hmr/storage/vector_store.py +520 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
知识核心(HKC 融入)测试 —— 全离线,embedding=stub,不需 LLM。
|
|
3
|
+
|
|
4
|
+
覆盖:
|
|
5
|
+
1. init_hkc 同进程装配 → available;空库混合检索返回 [];stats 有 data_dir
|
|
6
|
+
2. H1 资源修复:close() 后能 rmtree 数据目录(无 Windows WinError 32)——
|
|
7
|
+
即 event_bus/id_gen/graph_store/synthesis 四个 SQLite 句柄全部释放
|
|
8
|
+
3. close() 幂等
|
|
9
|
+
4. 经 Foxclaw 惰性装配:未访问 .hkc 前不开句柄;访问后装配;aclose() 释放后可删目录
|
|
10
|
+
|
|
11
|
+
摄入(ingest)需真实 LLM,不在离线回归内跑;这里只验证融合层的装配/降级/资源契约。
|
|
12
|
+
"""
|
|
13
|
+
import sys, tempfile, shutil, asyncio
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
16
|
+
|
|
17
|
+
from foxclaw.knowledge import init_hkc
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_assemble_search_close():
|
|
21
|
+
print("\n【1】init_hkc 装配 + 空库检索 + stats + close 后可删目录(H1)")
|
|
22
|
+
d = tempfile.mkdtemp(prefix="hkc_test_")
|
|
23
|
+
ok_rmtree = False
|
|
24
|
+
try:
|
|
25
|
+
svc = init_hkc(d, api_key=None)
|
|
26
|
+
assert svc is not None and svc.available, "依赖已装,HKC 应装配成功"
|
|
27
|
+
print(" ✓ 同进程装配成功 available=True")
|
|
28
|
+
|
|
29
|
+
hits = svc.client.search("任意不存在的查询", mode="hybrid", top_k=5)
|
|
30
|
+
assert isinstance(hits, list) and len(hits) == 0
|
|
31
|
+
print(f" ✓ 空库混合检索返回空: {hits}")
|
|
32
|
+
|
|
33
|
+
s = svc.stats()
|
|
34
|
+
assert "data_dir" in s and "total_kus" in s
|
|
35
|
+
print(f" ✓ stats: total_kus={s.get('total_kus')} embedding={s.get('embedding')!r}")
|
|
36
|
+
|
|
37
|
+
svc.close()
|
|
38
|
+
svc.close() # 幂等
|
|
39
|
+
print(" ✓ close() 幂等")
|
|
40
|
+
finally:
|
|
41
|
+
# 关键:close 后 4 个 SQLite 句柄全释放,才能删掉目录(此前 events.db/id_counters.db 泄漏)
|
|
42
|
+
shutil.rmtree(d) # 不 ignore_errors:泄漏则抛 OSError → 测试失败
|
|
43
|
+
ok_rmtree = True
|
|
44
|
+
assert ok_rmtree
|
|
45
|
+
print(" ✓ close 后 rmtree 成功 → H1 资源泄漏修复生效(无 WinError 32)")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_via_foxclawos_lazy_teardown():
|
|
49
|
+
print("\n【2】经 Foxclaw 惰性装配 + aclose teardown 全链路")
|
|
50
|
+
d = tempfile.mkdtemp(prefix="hkc_os_")
|
|
51
|
+
try:
|
|
52
|
+
from foxclaw import Foxclaw
|
|
53
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
54
|
+
# 惰性:未访问 .hkc 前不该装配(不开句柄)
|
|
55
|
+
assert os_._hkc_service is None and os_._hkc_inited is False
|
|
56
|
+
print(" ✓ 构造 OS 后未装配 HKC(惰性,未开 db 句柄)")
|
|
57
|
+
|
|
58
|
+
hkc = os_.hkc # 触发装配
|
|
59
|
+
assert os_._hkc_inited is True
|
|
60
|
+
assert hkc is not None and hkc.available
|
|
61
|
+
print(" ✓ 首次访问 os_.hkc 触发装配 available=True")
|
|
62
|
+
|
|
63
|
+
hits = hkc.client.search("空库查询", top_k=3)
|
|
64
|
+
assert hits == []
|
|
65
|
+
print(" ✓ 经 DirectClient 检索空库返回 []")
|
|
66
|
+
|
|
67
|
+
asyncio.run(os_.aclose())
|
|
68
|
+
print(" ✓ aclose() 释放浏览器 + HKC 资源")
|
|
69
|
+
finally:
|
|
70
|
+
shutil.rmtree(d) # aclose 已释放 HKC 句柄,应能删
|
|
71
|
+
print(" ✓ teardown 后 rmtree 成功(HKC 句柄经 aclose 释放)")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_ability_readonly_surface():
|
|
75
|
+
print("\n【3】第2档 Step1:ACE 能力只读面(list/compile 证据 gate/neighbors)——无 LLM")
|
|
76
|
+
d = tempfile.mkdtemp(prefix="hkc_abil_")
|
|
77
|
+
svc = None
|
|
78
|
+
try:
|
|
79
|
+
svc = init_hkc(d, api_key=None)
|
|
80
|
+
assert svc is not None and svc.available
|
|
81
|
+
from hkc_sdk.exceptions import HKCInsufficientCoverage, HKCNotFoundError
|
|
82
|
+
# list_abilities:返回 taxonomy 里定义的能力 key(静态,无需摄入)
|
|
83
|
+
keys = svc.client.list_abilities()
|
|
84
|
+
assert isinstance(keys, list) and len(keys) > 0, f"应有内置能力定义,实得 {keys}"
|
|
85
|
+
print(f" ✓ list_abilities 返回 {len(keys)} 个能力: {keys[:4]}...")
|
|
86
|
+
# compile_ability:空知识库 → 覆盖不足 → 证据 gate(抛 HKCInsufficientCoverage,带 missing/coverage)
|
|
87
|
+
gated = False
|
|
88
|
+
try:
|
|
89
|
+
svc.client.compile_ability(keys[0])
|
|
90
|
+
except HKCInsufficientCoverage as e:
|
|
91
|
+
gated = True
|
|
92
|
+
assert hasattr(e, "missing_skills") and hasattr(e, "coverage")
|
|
93
|
+
assert gated, "空知识库编译能力应被证据 gate 拦(覆盖不足)"
|
|
94
|
+
print(" ✓ 空库 compile_ability 被证据 gate 拦(覆盖不足→能力不予授予)")
|
|
95
|
+
# neighbors:不存在 KU → SDK 抛 HKCNotFoundError(契约)。注:/api/knowledge/ku/{id}/neighbors
|
|
96
|
+
# 端点已在 V3 接口清理删除(前端零用 + HKC router 的 /knowledge/ku/{id}/neighbors 有等价),
|
|
97
|
+
# 此处只验 SDK 层契约;SDK neighbors 仍被 ACE 能力/图查询内部使用,保留测试。
|
|
98
|
+
raised = False
|
|
99
|
+
try:
|
|
100
|
+
svc.client.neighbors("KU_nonexistent", max_depth=2, top_k=10)
|
|
101
|
+
except HKCNotFoundError:
|
|
102
|
+
raised = True
|
|
103
|
+
assert raised, "不存在 KU 应抛 HKCNotFoundError(SDK 契约)"
|
|
104
|
+
print(" ✓ neighbors(不存在 KU) 抛 HKCNotFoundError(SDK 契约;API 端点已删,见 V3 清理)")
|
|
105
|
+
finally:
|
|
106
|
+
if svc is not None:
|
|
107
|
+
svc.close()
|
|
108
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
109
|
+
print(" ✓ 只读能力面成立:枚举 + 证据 gate + 图邻域(全无 LLM/零成本)")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_agent_available_abilities():
|
|
113
|
+
print("\n【4】第2档 Step2:Agent.available_abilities() 证据驱动自省(只读,不改执行)")
|
|
114
|
+
d = tempfile.mkdtemp(prefix="hkc_selfintro_")
|
|
115
|
+
os_ = None
|
|
116
|
+
try:
|
|
117
|
+
from foxclaw import Foxclaw
|
|
118
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
119
|
+
# 空知识库:所有能力都应 sufficient=False(证据 gate:没知识不予授予)
|
|
120
|
+
abils = os_.finance.available_abilities()
|
|
121
|
+
assert isinstance(abils, list) and len(abils) > 0, f"应枚举出能力,实得 {abils}"
|
|
122
|
+
assert all(a["sufficient"] is False for a in abils), \
|
|
123
|
+
f"空库→全部证据 gate 拦(sufficient=False),实得 {[a['sufficient'] for a in abils]}"
|
|
124
|
+
assert all("missing_skills" in a for a in abils), "未达标应附 missing_skills"
|
|
125
|
+
print(f" ✓ 空库自省:{len(abils)} 个能力全 sufficient=False(证据 gate)")
|
|
126
|
+
# 各 Agent 看到一致(知识全局共享)
|
|
127
|
+
assert len(os_.office.available_abilities()) == len(abils)
|
|
128
|
+
print(" ✓ 各 Agent 一致(HKC 知识全局共享)")
|
|
129
|
+
asyncio.run(os_.aclose())
|
|
130
|
+
finally:
|
|
131
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
132
|
+
print(" ✓ 能力自省成立:证据驱动、只读、无 LLM")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_ability_context_injection():
|
|
136
|
+
print("\n【5】第2档 Step3:能力→上下文注入(opt-in;默认关不改行为;fail-safe)")
|
|
137
|
+
from types import SimpleNamespace
|
|
138
|
+
d = tempfile.mkdtemp(prefix="hkc_inject_")
|
|
139
|
+
os_ = None
|
|
140
|
+
try:
|
|
141
|
+
from foxclaw import Foxclaw
|
|
142
|
+
os_ = Foxclaw(data_dir=d, shadow_rounds=50)
|
|
143
|
+
ag = os_.finance
|
|
144
|
+
base = [{"role": "user", "content": "原始问题"}]
|
|
145
|
+
|
|
146
|
+
# (a) 默认关:不注入(行为逐字不变)
|
|
147
|
+
assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
|
|
148
|
+
|
|
149
|
+
# (b) 启用但无达标能力(真实空 HKC)→ no-op
|
|
150
|
+
ag.ABILITY_AUGMENT = True
|
|
151
|
+
assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
|
|
152
|
+
|
|
153
|
+
# (c) 启用 + 注册假 HKC(有达标能力 + 检索命中)→ 注入知识上下文到 messages 前
|
|
154
|
+
class _C:
|
|
155
|
+
def list_abilities(self): return ["writing_expert"]
|
|
156
|
+
def compile_ability(self, k):
|
|
157
|
+
return SimpleNamespace(ability_key=k, display_name="写作专家",
|
|
158
|
+
domain="writing", coverage={"修辞": 0.9}, skills=[])
|
|
159
|
+
def search(self, q, mode="hybrid", top_k=10, domain=""):
|
|
160
|
+
return [SimpleNamespace(name="修辞", summary="比喻与排比",
|
|
161
|
+
ku_id="K1", score=0.9, ku_type="Concept")]
|
|
162
|
+
os_.capabilities.register("hkc", SimpleNamespace(available=True, client=_C()))
|
|
163
|
+
out = ag._ability_augment("帮我做写作专家风格分析", list(base))
|
|
164
|
+
assert len(out) == 2 and "能力知识增强" in out[0]["content"] and "修辞" in out[0]["content"]
|
|
165
|
+
assert out[1] == base[0], "原消息应保留在注入内容之后"
|
|
166
|
+
print(" ✓ 默认关不注入;空库 no-op;启用+达标+命中→注入知识上下文")
|
|
167
|
+
|
|
168
|
+
# (d) fail-safe:能力增强内部异常(search 抛错)→ 原样返回,不影响主流程
|
|
169
|
+
class _Cbad(_C):
|
|
170
|
+
def search(self, *a, **k): raise RuntimeError("boom")
|
|
171
|
+
os_.capabilities.register("hkc", SimpleNamespace(available=True, client=_Cbad()))
|
|
172
|
+
assert ag._ability_augment("帮我做写作专家分析", list(base)) == base
|
|
173
|
+
print(" ✓ 增强内部异常 → fail-safe 原样返回(不影响主流程)")
|
|
174
|
+
asyncio.run(os_.aclose())
|
|
175
|
+
finally:
|
|
176
|
+
shutil.rmtree(d, ignore_errors=True)
|
|
177
|
+
print(" ✓ 能力上下文注入成立:opt-in、匹配、fail-safe、默认行为不变")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_ability_as_skill_step4():
|
|
181
|
+
print("\n【6】第2档 Step4:能力→SkillRegistry(证据 gate + read 经 PolicyGate + 可路由)")
|
|
182
|
+
from types import SimpleNamespace
|
|
183
|
+
from foxclaw.knowledge import register_abilities_as_skills
|
|
184
|
+
from foxclaw.skills import SkillRegistry, SkillAdapter
|
|
185
|
+
|
|
186
|
+
class _C:
|
|
187
|
+
def list_abilities(self): return ["writing_expert", "quant_analyst"]
|
|
188
|
+
def compile_ability(self, k):
|
|
189
|
+
if k == "quant_analyst":
|
|
190
|
+
raise RuntimeError("coverage 不足") # 任何异常 → 证据 gate 跳过
|
|
191
|
+
return SimpleNamespace(ability_key=k, display_name="写作专家",
|
|
192
|
+
domain="writing", coverage={"修辞": 0.9})
|
|
193
|
+
def search(self, q, mode="hybrid", top_k=10, domain=""):
|
|
194
|
+
return [SimpleNamespace(name="修辞", summary="比喻与排比",
|
|
195
|
+
ku_id="K1", score=0.9, ku_type="Concept")]
|
|
196
|
+
|
|
197
|
+
hkc = SimpleNamespace(available=True, client=_C())
|
|
198
|
+
reg = SkillRegistry()
|
|
199
|
+
ids = register_abilities_as_skills(reg, hkc)
|
|
200
|
+
# 证据 gate:只注册达标的 writing_expert;覆盖不足的 quant_analyst 不注册
|
|
201
|
+
assert ids == ["ability:writing_expert"], ids
|
|
202
|
+
assert reg.get("ability:quant_analyst") is None, "覆盖不足→不注册(证据 gate)"
|
|
203
|
+
spec = reg.get("ability:writing_expert")
|
|
204
|
+
assert spec is not None and spec.side_effect == "read", "能力 skill 为 read(无外部副作用)"
|
|
205
|
+
|
|
206
|
+
# 经 SkillAdapter(→PolicyGate 四阶段)调用:read → 放行 → handler 返回 domain 内知识
|
|
207
|
+
res = SkillAdapter(reg).invoke(spec, {"query": "写作风格"})
|
|
208
|
+
assert res.success and res.output.get("ability") == "writing_expert", res.to_dict()
|
|
209
|
+
assert res.output["knowledge"] and res.output["knowledge"][0]["name"] == "修辞"
|
|
210
|
+
print(" ✓ 证据 gate:达标能力注册为 read skill、覆盖不足不注册;经 PolicyGate 放行返回知识")
|
|
211
|
+
|
|
212
|
+
# 降级:HKC 不可用 / registry 缺失 → 不注册、不崩
|
|
213
|
+
assert register_abilities_as_skills(SkillRegistry(), None) == []
|
|
214
|
+
assert register_abilities_as_skills(None, hkc) == []
|
|
215
|
+
print(" ✓ HKC 不可用/registry 缺失 → 优雅返回 []")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_ability_skill_prune_and_concurrency():
|
|
219
|
+
print("\n【7】深审修复:ability skill 陈旧清理(re-sync)+ SkillRegistry 线程安全")
|
|
220
|
+
import threading
|
|
221
|
+
from types import SimpleNamespace
|
|
222
|
+
from foxclaw.knowledge import register_abilities_as_skills
|
|
223
|
+
from foxclaw.skills import SkillRegistry, SkillSpec
|
|
224
|
+
|
|
225
|
+
# (a) 陈旧清理:先达标注册 → 知识掉了不再达标 → re-sync 移除陈旧 ability skill
|
|
226
|
+
class _Cdrop:
|
|
227
|
+
def __init__(self): self.ok = True
|
|
228
|
+
def list_abilities(self): return ["writing_expert"]
|
|
229
|
+
def compile_ability(self, k):
|
|
230
|
+
if not self.ok:
|
|
231
|
+
raise RuntimeError("coverage 掉了")
|
|
232
|
+
return SimpleNamespace(ability_key=k, display_name="写作专家",
|
|
233
|
+
domain="writing", coverage={})
|
|
234
|
+
def search(self, *a, **k): return []
|
|
235
|
+
c = _Cdrop()
|
|
236
|
+
hkc = SimpleNamespace(available=True, client=c)
|
|
237
|
+
reg = SkillRegistry()
|
|
238
|
+
assert register_abilities_as_skills(reg, hkc) == ["ability:writing_expert"]
|
|
239
|
+
assert reg.get("ability:writing_expert") is not None
|
|
240
|
+
c.ok = False # 知识掉了 → 不再达标
|
|
241
|
+
assert register_abilities_as_skills(reg, hkc) == []
|
|
242
|
+
assert reg.get("ability:writing_expert") is None, "陈旧 ability skill 应在 re-sync 时被移除"
|
|
243
|
+
print(" ✓ re-sync:覆盖掉了的 ability skill 被清理(不残留)")
|
|
244
|
+
|
|
245
|
+
# (b) 线程安全:并发 register + match/all/catalog 无 "dict changed size" 异常
|
|
246
|
+
reg2 = SkillRegistry()
|
|
247
|
+
errs = []
|
|
248
|
+
def reg_worker():
|
|
249
|
+
try:
|
|
250
|
+
for i in range(400):
|
|
251
|
+
reg2.register(SkillSpec(id=f"s{i % 20}", name="x", description="",
|
|
252
|
+
kind="local", triggers=["x"]))
|
|
253
|
+
except Exception as e:
|
|
254
|
+
errs.append(repr(e))
|
|
255
|
+
def read_worker():
|
|
256
|
+
try:
|
|
257
|
+
for _ in range(400):
|
|
258
|
+
reg2.match("任意任务 x"); reg2.all(); reg2.catalog()
|
|
259
|
+
except Exception as e:
|
|
260
|
+
errs.append(repr(e))
|
|
261
|
+
ts = [threading.Thread(target=reg_worker) for _ in range(3)] + \
|
|
262
|
+
[threading.Thread(target=read_worker) for _ in range(3)]
|
|
263
|
+
for t in ts: t.start()
|
|
264
|
+
for t in ts: t.join()
|
|
265
|
+
assert not errs, f"并发 register/match 应无异常,实得 {errs[:2]}"
|
|
266
|
+
print(" ✓ 6 线程并发 register/match/all/catalog 无异常(SkillRegistry 加锁生效)")
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def main():
|
|
270
|
+
print("=" * 60)
|
|
271
|
+
print("知识核心 · HKC 融入(全离线,embedding=stub)")
|
|
272
|
+
print("=" * 60)
|
|
273
|
+
test_assemble_search_close()
|
|
274
|
+
test_via_foxclawos_lazy_teardown()
|
|
275
|
+
test_ability_readonly_surface()
|
|
276
|
+
test_agent_available_abilities()
|
|
277
|
+
test_ability_context_injection()
|
|
278
|
+
test_ability_as_skill_step4()
|
|
279
|
+
test_ability_skill_prune_and_concurrency()
|
|
280
|
+
print("\n" + "=" * 60)
|
|
281
|
+
print("✓ HKC 融入验证完成:装配/检索/stats + H1 资源修复 + 惰性/teardown")
|
|
282
|
+
print("=" * 60)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
main()
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HKC 后台索引构建测试(阶段 C 提交)。
|
|
3
|
+
|
|
4
|
+
覆盖范围:
|
|
5
|
+
S1 无索引启动 + 库有数据 → assemble 立即返;manager 后台编码;
|
|
6
|
+
等 join 后 vector_size == source_total,index_state=ready。
|
|
7
|
+
S2 旧快照启动(磁盘 faiss 完整覆盖库) → assemble 立即返;
|
|
8
|
+
manager.start() 返 None(无需构建);vector_size == 磁盘中数。
|
|
9
|
+
S3 构建中重启 → 续跑(落盘 faiss 删光,manager 仍从空开始;但状态机重启后
|
|
10
|
+
state 仍为上次 cancelled/ready,这是 next.start() 决策依据)
|
|
11
|
+
S4 构建中查询:manager 跑后台编码的同时,前端调 c.search.search()
|
|
12
|
+
→ 不抛错;能拿到当前 partial 结果。
|
|
13
|
+
|
|
14
|
+
设计原则(同 P0/P1/P2):
|
|
15
|
+
- 不 mock:真 SQLite + 真 faiss + StubBackend + 真 HKCContainer + 真 IndexBuildManager。
|
|
16
|
+
- vendor 隔离硬阻断:模块级解析后立即自检 __file__,不是 vendor 就 RuntimeError。
|
|
17
|
+
- 不依赖网络 / 真实模型 / 真实数据目录;全程 tmp_path。
|
|
18
|
+
- 每个测试独立 tmp_path;HKCContainer init/reset 严格 try/finally。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── vendor 隔离硬阻断 ────────────────────────────────────────────
|
|
28
|
+
from hkc_search.index_build_manager import IndexBuildManager, _BUILD_STATES
|
|
29
|
+
from hkc_search.vector_search import VectorIndex
|
|
30
|
+
from hkc_search.embedding_backends import StubBackend
|
|
31
|
+
from hkc_api.container import HKCContainer, init_container, reset_container
|
|
32
|
+
from hkc_core.graph.sqlite_store import SQLiteGraphStore
|
|
33
|
+
from hkc_core.models.ku import ConceptKU
|
|
34
|
+
|
|
35
|
+
_VENDOR_ROOT = Path(__file__).resolve().parents[1] / "vendor" / "hkc"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _assert_vendor_path(obj, name: str) -> None:
|
|
39
|
+
mod = obj.__module__
|
|
40
|
+
f = getattr(sys.modules[mod], "__file__", "") or ""
|
|
41
|
+
assert str(Path(f).resolve()).startswith(str(_VENDOR_ROOT.resolve())), (
|
|
42
|
+
f"vendor isolation failed: {name}.__module__={mod!r} __file__={f!r} "
|
|
43
|
+
f"not under vendor/hkc. Test is running against wrong HKC copy."
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
for _cls_name, _cls in [
|
|
48
|
+
("IndexBuildManager", IndexBuildManager),
|
|
49
|
+
("VectorIndex", VectorIndex),
|
|
50
|
+
("StubBackend", StubBackend),
|
|
51
|
+
("HKCContainer", HKCContainer),
|
|
52
|
+
("SQLiteGraphStore", SQLiteGraphStore),
|
|
53
|
+
("ConceptKU", ConceptKU),
|
|
54
|
+
]:
|
|
55
|
+
_assert_vendor_path(_cls, _cls_name)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── Helpers ─────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
def _seed_kus(c: HKCContainer, ids: list[str]) -> None:
|
|
61
|
+
"""直接 put 进 store,不发出 KNOWLEDGE_CREATED 事件。
|
|
62
|
+
用于构造 '库有数据但 vector 没条' 的初始状态,触发 manager 全量构建。
|
|
63
|
+
"""
|
|
64
|
+
for i, ku_id in enumerate(ids):
|
|
65
|
+
ku = ConceptKU(
|
|
66
|
+
ku_id=ku_id,
|
|
67
|
+
name=f"ku_{i}",
|
|
68
|
+
summary=f"ku {i} 的描述是若干中文文字以填充文本",
|
|
69
|
+
domain="d",
|
|
70
|
+
)
|
|
71
|
+
c.graph_store.put(ku)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _wait_idle(c: HKCContainer, timeout: float = 30.0) -> dict:
|
|
75
|
+
"""等 manager 跑完一轮变成 ready/failed/cancelled,返 snapshot。timeout 内的 poll。"""
|
|
76
|
+
deadline = time.time() + timeout
|
|
77
|
+
mgr = c.index_build_manager
|
|
78
|
+
last_state = None
|
|
79
|
+
while time.time() < deadline:
|
|
80
|
+
s = mgr.stats()
|
|
81
|
+
if s["index_state"] in ("ready", "failed", "cancelled", "idle"):
|
|
82
|
+
return s
|
|
83
|
+
last_state = s
|
|
84
|
+
time.sleep(0.05)
|
|
85
|
+
raise AssertionError(
|
|
86
|
+
f"等超时 {timeout}s;最后状态: {last_state}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _seed_and_start_build(c: HKCContainer, ids: list[str]) -> None:
|
|
91
|
+
"""init_container 在装配期会扫描库。此时若库空,manager 会立即 idle;
|
|
92
|
+
seed KU 后必须显式再 start() 触发 full build(在已加载的 vector 上 append)。
|
|
93
|
+
装配时已自动 start 过一次(空库 → idle),这次手动 start 触发增量构建。
|
|
94
|
+
"""
|
|
95
|
+
_seed_kus(c, ids)
|
|
96
|
+
r = c.index_build_manager.start()
|
|
97
|
+
# size>0 不应返 None(已加载的 faiss 有数据但库无相同数据时可能返 None;此测试场景是库空)
|
|
98
|
+
# 这里 size=0 + source>0 → 必起构建 → start 返 task_id
|
|
99
|
+
assert r is not None, (
|
|
100
|
+
f"_seed_and_start_build 应触发新构建,但 start() 返 None。"
|
|
101
|
+
f"当前 stats: {c.index_build_manager.stats()}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── S1:无索引启动,manager 后台跑全量构建 ───────────────────
|
|
106
|
+
|
|
107
|
+
class TestStartNoIndexManagerBuildsBackground:
|
|
108
|
+
def test_assemble_returns_fast_manager_builds_then_ready(self, tmp_path):
|
|
109
|
+
"""库有 5 KU 但磁盘无索引 → seed + start → manager 后台构建;
|
|
110
|
+
join 后 vector_size == source_total 且 state=ready。"""
|
|
111
|
+
data_dir = str(tmp_path)
|
|
112
|
+
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
113
|
+
try:
|
|
114
|
+
_seed_and_start_build(c, [f"KU_S1_{i}" for i in range(5)])
|
|
115
|
+
mgr = c.index_build_manager
|
|
116
|
+
assert mgr is not None
|
|
117
|
+
final = _wait_idle(c, timeout=30)
|
|
118
|
+
assert final["index_state"] == "ready", (
|
|
119
|
+
f"应在 ready,实际 {final}"
|
|
120
|
+
)
|
|
121
|
+
assert final["source_total"] == 5, final
|
|
122
|
+
assert final["ready"] == 5, final
|
|
123
|
+
assert final["failed"] == 0, final
|
|
124
|
+
# 内存索引真存在
|
|
125
|
+
assert c.search.vector.size() == 5
|
|
126
|
+
finally:
|
|
127
|
+
c.close()
|
|
128
|
+
reset_container()
|
|
129
|
+
|
|
130
|
+
def test_assemble_does_not_block_when_manager_builds(self, tmp_path):
|
|
131
|
+
"""关键不变量:装配本身**不阻塞**全量 encode。
|
|
132
|
+
装配返回(manager 已 start)再 seed → 整体构建必须由 manager 后台承担。"""
|
|
133
|
+
data_dir = str(tmp_path)
|
|
134
|
+
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
135
|
+
try:
|
|
136
|
+
# 装配已经返回(库空,manager 立即 idle)。seed + 重 start 让它编码。
|
|
137
|
+
_seed_and_start_build(c, [f"KU_FAST_{i}" for i in range(2)])
|
|
138
|
+
s = c.index_build_manager.stats()
|
|
139
|
+
assert s["index_state"] in _BUILD_STATES
|
|
140
|
+
final = _wait_idle(c, timeout=10)
|
|
141
|
+
assert final["index_state"] == "ready"
|
|
142
|
+
assert final["ready"] == 2
|
|
143
|
+
finally:
|
|
144
|
+
c.close()
|
|
145
|
+
reset_container()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ── S2:旧快照启动(磁盘 faiss 已完整) → 不重起构建 ──────────
|
|
149
|
+
|
|
150
|
+
class TestStartWithPersistedSnapshotNoRebuild:
|
|
151
|
+
def test_manager_skips_when_disk_snapshot_covers_source(self, tmp_path):
|
|
152
|
+
"""先启动一次让 manager 构建完 → close;重启 → manager 看到 faiss 已非空
|
|
153
|
+
→ start() 返 None,不再构建。"""
|
|
154
|
+
data_dir = str(tmp_path)
|
|
155
|
+
# 第一轮:seed + start → manager 构建 → 落盘
|
|
156
|
+
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
157
|
+
try:
|
|
158
|
+
_seed_and_start_build(c, [f"KU_S2A_{i}" for i in range(4)])
|
|
159
|
+
_wait_idle(c, timeout=20)
|
|
160
|
+
assert c.search.vector.size() == 4
|
|
161
|
+
finally:
|
|
162
|
+
c.close()
|
|
163
|
+
reset_container()
|
|
164
|
+
|
|
165
|
+
# 第二轮:simulate 重启 —— 装配后,manager.start() 应返 None (已有快照)
|
|
166
|
+
c2 = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
167
|
+
try:
|
|
168
|
+
s = c2.index_build_manager.stats()
|
|
169
|
+
assert s["index_state"] == "ready", (
|
|
170
|
+
f"已有磁盘快照时 manager 应跳过,实际 {s}"
|
|
171
|
+
)
|
|
172
|
+
assert s["vector_size"] == 4, (
|
|
173
|
+
f"vector_size 应从磁盘恢复,实际 {s}"
|
|
174
|
+
)
|
|
175
|
+
# 显式再调 start() 也返 None(幂等)
|
|
176
|
+
r = c2.index_build_manager.start()
|
|
177
|
+
assert r is None, (
|
|
178
|
+
f"已有快照时 start() 应返 None,实际 {r!r}"
|
|
179
|
+
)
|
|
180
|
+
finally:
|
|
181
|
+
c2.close()
|
|
182
|
+
reset_container()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ── S3:构建中重启 → 续跑 ───────────────────────────────
|
|
186
|
+
|
|
187
|
+
class TestMidBuildRestartPicksUp:
|
|
188
|
+
def test_resume_after_disk_vector_partial(self, tmp_path):
|
|
189
|
+
"""模拟 '上次构建到一半被杀掉':磁盘 faiss 有 5 条全量,manager 看到库空
|
|
190
|
+
就不重跑(构不了啥)。验证 restart 链路:装配→加载 faiss→manager 跳过。
|
|
191
|
+
真正的「mid-build 续跑」语义由 state.json + vector 缺漏检测;
|
|
192
|
+
但因为 manager 的 start() 总是基于「当前 vector vs 当前库」做决策,
|
|
193
|
+
磁盘有 5 条 + 库 5 条相同 → 跳过(等价于 S2 的 happy path)。"""
|
|
194
|
+
data_dir = str(tmp_path)
|
|
195
|
+
# 第一轮:build 5 条 → save → close
|
|
196
|
+
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
197
|
+
try:
|
|
198
|
+
_seed_and_start_build(c, [f"KU_S3_{i}" for i in range(5)])
|
|
199
|
+
_wait_idle(c, timeout=20)
|
|
200
|
+
assert c.search.vector.size() == 5
|
|
201
|
+
finally:
|
|
202
|
+
c.close()
|
|
203
|
+
reset_container()
|
|
204
|
+
|
|
205
|
+
# 模拟"上次只跑了部分,faiss 是部分加载的" — 删 vector 文件(变 0 条)
|
|
206
|
+
# manager 装配时会 _load_persisted_index_and_bm25 → load 失败(因为已被我们删)→ 0 条
|
|
207
|
+
# 然后 manager.start() 看到 size=0 < source=5 → 起新构建
|
|
208
|
+
vec_path = tmp_path / "search_vector"
|
|
209
|
+
if (vec_path.with_suffix(".faiss")).exists():
|
|
210
|
+
(vec_path.with_suffix(".faiss")).unlink()
|
|
211
|
+
if (vec_path.with_suffix(".meta")).exists():
|
|
212
|
+
(vec_path.with_suffix(".meta")).unlink()
|
|
213
|
+
|
|
214
|
+
# 写 state.json 模拟"上次未完成",确认 manager.start 决策时能读到历史
|
|
215
|
+
import json as _json
|
|
216
|
+
state = {
|
|
217
|
+
"task_id": "build-deadbeef",
|
|
218
|
+
"index_version": "v-stale",
|
|
219
|
+
"state": "building",
|
|
220
|
+
"source_total": 5,
|
|
221
|
+
"scanned": 3,
|
|
222
|
+
"ready": 3,
|
|
223
|
+
"failed": 0,
|
|
224
|
+
"failures": [],
|
|
225
|
+
"started_at": "2025-01-01T00:00:00+00:00",
|
|
226
|
+
"finished_at": None,
|
|
227
|
+
"last_checkpoint_at": "2025-01-01T00:00:01+00:00",
|
|
228
|
+
"error_summary": "",
|
|
229
|
+
}
|
|
230
|
+
(tmp_path / IndexBuildManager.STATE_FILE).write_text(
|
|
231
|
+
_json.dumps(state, ensure_ascii=False, indent=2),
|
|
232
|
+
encoding="utf-8",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# 第二轮:重启
|
|
236
|
+
c2 = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
237
|
+
try:
|
|
238
|
+
# 装配已自动 start → 应启构建(因为 faiss 已删)
|
|
239
|
+
final = _wait_idle(c2, timeout=30)
|
|
240
|
+
assert final["index_state"] == "ready", (
|
|
241
|
+
f"重启后应到 ready,实际 {final}"
|
|
242
|
+
)
|
|
243
|
+
assert c2.search.vector.size() == 5, (
|
|
244
|
+
f"重启后 vector_size 应=5,实际 {c2.search.vector.size()}"
|
|
245
|
+
)
|
|
246
|
+
# 跑出了新 task_id(不是历史的 deadbeef)
|
|
247
|
+
assert final["task_id"] != "build-deadbeef", (
|
|
248
|
+
f"应起新 task_id,实际仍用历史的 {final['task_id']!r}"
|
|
249
|
+
)
|
|
250
|
+
finally:
|
|
251
|
+
c2.close()
|
|
252
|
+
reset_container()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ── S4:构建中查询 ──────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
class TestSearchDuringBuild:
|
|
258
|
+
def test_concurrent_search_does_not_crash(self, tmp_path):
|
|
259
|
+
"""manager 后台跑编码的同时,前调 c.search.search() → 不抛错;
|
|
260
|
+
任何阶段拿到的是 partial 但应是合法 SearchResult 列表。"""
|
|
261
|
+
data_dir = str(tmp_path)
|
|
262
|
+
c = init_container(data_dir=data_dir, embedding_kind="stub")
|
|
263
|
+
try:
|
|
264
|
+
_seed_and_start_build(c, [f"KU_S4_{i}" for i in range(6)])
|
|
265
|
+
# 不等;立即并发调 search
|
|
266
|
+
results = c.search.search("ku_1 描述", mode="hybrid", top_k=3)
|
|
267
|
+
assert isinstance(results, list)
|
|
268
|
+
for r in results:
|
|
269
|
+
assert hasattr(r, "ku_id")
|
|
270
|
+
assert hasattr(r, "score")
|
|
271
|
+
# 等构建完再做一次 search 确认 6 条都被 indexed
|
|
272
|
+
final = _wait_idle(c, timeout=30)
|
|
273
|
+
assert final["index_state"] == "ready"
|
|
274
|
+
assert c.search.vector.size() == 6
|
|
275
|
+
results2 = c.search.search("ku_2", mode="bm25", top_k=2)
|
|
276
|
+
assert isinstance(results2, list)
|
|
277
|
+
finally:
|
|
278
|
+
c.close()
|
|
279
|
+
reset_container()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ── 单元测:IndexBuildManager 状态机字段 ─────────────────
|
|
283
|
+
|
|
284
|
+
class TestManagerStats:
|
|
285
|
+
def test_stats_keys_present(self, tmp_path):
|
|
286
|
+
"""stats 字典字段稳定性(对外观测前端要靠这些 key)。"""
|
|
287
|
+
mgr = IndexBuildManager(
|
|
288
|
+
data_dir=str(tmp_path),
|
|
289
|
+
hybrid_search=_FakeHybrid(),
|
|
290
|
+
graph_store=_FakeEmptyGraphStore(),
|
|
291
|
+
)
|
|
292
|
+
s = mgr.stats()
|
|
293
|
+
for k in (
|
|
294
|
+
"index_state", "index_version", "task_id", "source_total",
|
|
295
|
+
"scanned", "ready", "pending", "failed",
|
|
296
|
+
"started_at", "finished_at", "last_checkpoint_at",
|
|
297
|
+
"error_summary", "failures_preview", "vector_size",
|
|
298
|
+
):
|
|
299
|
+
assert k in s, f"stats 缺字段 {k}, got {sorted(s.keys())}"
|
|
300
|
+
|
|
301
|
+
def test_idle_start_no_kus_yields_ready(self, tmp_path):
|
|
302
|
+
"""空 store + 空 faiss → start 起线程跑,扫描后 source_total=0 → 立即 ready。"""
|
|
303
|
+
mgr = IndexBuildManager(
|
|
304
|
+
data_dir=str(tmp_path),
|
|
305
|
+
hybrid_search=_FakeHybrid(),
|
|
306
|
+
graph_store=_FakeEmptyGraphStore(),
|
|
307
|
+
)
|
|
308
|
+
mgr.start()
|
|
309
|
+
mgr.join(timeout=5)
|
|
310
|
+
# 空 store 扫描到 0 条 → 直接 _mark_ready
|
|
311
|
+
s = mgr.stats()
|
|
312
|
+
assert s["index_state"] in ("ready", "cancelled", "failed"), s
|
|
313
|
+
assert s["source_total"] == 0, s
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ── Fakes(单元测用) ─────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
class _FakeHybrid:
|
|
319
|
+
"""空 hybrid:vector_size=0,_ku_ids 空列表,_lock 是 RLock,vector 假实例。"""
|
|
320
|
+
def __init__(self):
|
|
321
|
+
import threading as _t
|
|
322
|
+
self._lock = _t.RLock()
|
|
323
|
+
self.vector = _FakeVector()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class _FakeVector:
|
|
327
|
+
def __init__(self):
|
|
328
|
+
self._ku_ids: list = []
|
|
329
|
+
def size(self): return len(self._ku_ids)
|
|
330
|
+
def save(self, path): pass
|
|
331
|
+
def append_bulk(self, kus):
|
|
332
|
+
with _FakeVector._lock:
|
|
333
|
+
self._ku_ids.extend(k.ku_id for k in kus)
|
|
334
|
+
_lock = __import__("threading").RLock()
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class _FakeEmptyGraphStore:
|
|
338
|
+
def query_all(self, limit=99999999):
|
|
339
|
+
return []
|