foxclaw 0.1.0 → 0.1.3
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 +4 -10
- package/package.json +46 -38
- package/python/.dockerignore +35 -0
- package/python/.env.example +173 -0
- package/python/.gitattributes +46 -0
- package/python/.github/workflows/ci.yml +167 -0
- package/python/.github/workflows/release.yml +183 -0
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/Dockerfile +64 -0
- package/python/LICENSE +21 -0
- package/python/MANIFEST.in +14 -0
- package/python/README.md +201 -0
- package/python/README_CN.md +336 -0
- package/python/bench/bm25_audit.py +186 -0
- package/python/bench/bm25_regression.py +177 -0
- package/python/bench/bm25_stress.py +155 -0
- package/python/bench/cd_audit.py +215 -0
- package/python/bench/chaos.py +140 -0
- package/python/bench/concurrency.py +103 -0
- package/python/bench/e2e_real_models.py +94 -0
- package/python/bench/llm_retry_test.py +241 -0
- package/python/bench/load_test.py +193 -0
- package/python/bench/vector_ivf_upgrade.py +128 -0
- package/python/docs/CHANNELS.md +195 -0
- package/python/docs/GOVERNANCE.md +164 -0
- package/python/docs/RELEASE.md +110 -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 +115 -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 +368 -0
- package/python/examples/governance_middleware_demo.py +72 -0
- package/python/examples/langgraph_governance_demo.py +104 -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 +408 -0
- package/python/foxclaw/agents/__init__.py +12 -0
- package/python/foxclaw/agents/agents.py +35 -0
- package/python/foxclaw/agents/base.py +794 -0
- package/python/foxclaw/agents/impl/__init__.py +1 -0
- package/python/foxclaw/agents/impl/_helpers.py +70 -0
- package/python/foxclaw/agents/impl/finance.py +231 -0
- package/python/foxclaw/agents/impl/office.py +350 -0
- package/python/foxclaw/agents/impl/supervisor.py +592 -0
- package/python/foxclaw/agents/mixins/_context.py +128 -0
- package/python/foxclaw/agents/mixins/augment.py +143 -0
- package/python/foxclaw/agents/mixins/llm.py +67 -0
- package/python/foxclaw/agents/mixins/memory.py +41 -0
- package/python/foxclaw/agents/mixins/preference.py +131 -0
- package/python/foxclaw/agents/mixins/prompt.py +71 -0
- package/python/foxclaw/agents/mixins/skillbox.py +58 -0
- package/python/foxclaw/agents/mixins/tool_exec.py +417 -0
- package/python/foxclaw/api/__init__.py +224 -0
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/bootstrap.py +96 -0
- package/python/foxclaw/api/middleware.py +294 -0
- package/python/foxclaw/api/routes/ahs.py +404 -0
- package/python/foxclaw/api/routes/attachments.py +116 -0
- package/python/foxclaw/api/routes/browser.py +35 -0
- package/python/foxclaw/api/routes/domain.py +306 -0
- package/python/foxclaw/api/routes/knowledge.py +145 -0
- package/python/foxclaw/api/routes/llm_admin.py +449 -0
- package/python/foxclaw/api/routes/memory.py +105 -0
- package/python/foxclaw/api/routes/orchestration.py +39 -0
- package/python/foxclaw/api/routes/recovery.py +44 -0
- package/python/foxclaw/api/routes/review.py +266 -0
- package/python/foxclaw/api/routes/search.py +33 -0
- package/python/foxclaw/api/routes/system.py +311 -0
- package/python/foxclaw/api/routes/task.py +263 -0
- package/python/foxclaw/api/routes/ws.py +64 -0
- package/python/foxclaw/api/spine.py +52 -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 +50 -0
- package/python/foxclaw/browser/action/action_types.py +44 -0
- package/python/foxclaw/browser/action/action_verifier.py +68 -0
- package/python/foxclaw/browser/action/url_policy.py +143 -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 +170 -0
- package/python/foxclaw/browser/perception/__init__.py +1 -0
- package/python/foxclaw/browser/perception/dom_extractor.py +42 -0
- package/python/foxclaw/browser/perception/element_indexer.py +56 -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 +144 -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 +65 -0
- package/python/foxclaw/channels/__init__.py +138 -0
- package/python/foxclaw/channels/discord.py +175 -0
- package/python/foxclaw/channels/dispatcher.py +179 -0
- package/python/foxclaw/channels/feishu.py +215 -0
- package/python/foxclaw/channels/openclaw_bridge.py +151 -0
- package/python/foxclaw/channels/signing.py +187 -0
- package/python/foxclaw/channels/telegram.py +120 -0
- package/python/foxclaw/channels/weixin.py +215 -0
- package/python/foxclaw/cli.py +183 -0
- package/python/foxclaw/cognitive/__init__.py +4 -0
- package/python/foxclaw/cognitive/reasoner.py +81 -0
- package/python/foxclaw/cognitive/state.py +141 -0
- package/python/foxclaw/core/__init__.py +9 -0
- package/python/foxclaw/core/audit_chain.py +131 -0
- package/python/foxclaw/core/audit_sink.py +100 -0
- package/python/foxclaw/core/compaction.py +265 -0
- package/python/foxclaw/core/config.py +65 -0
- package/python/foxclaw/core/confirm_nonce.py +151 -0
- package/python/foxclaw/core/dispatch.py +27 -0
- package/python/foxclaw/core/envfile.py +48 -0
- package/python/foxclaw/core/failure_policy.py +142 -0
- package/python/foxclaw/core/gateway.py +81 -0
- package/python/foxclaw/core/governance_audit.py +171 -0
- package/python/foxclaw/core/idempotency.py +282 -0
- package/python/foxclaw/core/injection_guard.py +202 -0
- package/python/foxclaw/core/memory_adapter.py +489 -0
- package/python/foxclaw/core/metrics.py +88 -0
- package/python/foxclaw/core/model_router.py +985 -0
- package/python/foxclaw/core/paths.py +103 -0
- package/python/foxclaw/core/recovery.py +349 -0
- package/python/foxclaw/core/scheduler.py +141 -0
- package/python/foxclaw/core/secrets.py +131 -0
- package/python/foxclaw/core/sessions.py +155 -0
- package/python/foxclaw/core/settings.py +414 -0
- package/python/foxclaw/core/shared_write.py +186 -0
- package/python/foxclaw/core/steering_queue.py +154 -0
- package/python/foxclaw/core/system_bus.py +84 -0
- package/python/foxclaw/core/thought_chain.py +316 -0
- package/python/foxclaw/core/token_meter.py +231 -0
- package/python/foxclaw/core/tools.py +164 -0
- package/python/foxclaw/core/tracing.py +375 -0
- package/python/foxclaw/core/truth_pipeline.py +110 -0
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +477 -0
- package/python/foxclaw/core/verify.py +173 -0
- package/python/foxclaw/core/web_search.py +218 -0
- package/python/foxclaw/finance/__init__.py +15 -0
- package/python/foxclaw/finance/evidence.py +149 -0
- package/python/foxclaw/finance/fx.py +98 -0
- package/python/foxclaw/finance/graph.py +179 -0
- package/python/foxclaw/finance/models.py +146 -0
- package/python/foxclaw/finance/nlparse.py +515 -0
- package/python/foxclaw/finance/policy.py +301 -0
- package/python/foxclaw/finance/reasoner.py +121 -0
- package/python/foxclaw/finance/reasoning.py +319 -0
- package/python/foxclaw/finance/reconcile.py +175 -0
- package/python/foxclaw/finance/runtime.py +500 -0
- package/python/foxclaw/governance/__init__.py +24 -0
- package/python/foxclaw/governance/middleware.py +176 -0
- package/python/foxclaw/harness/__init__.py +17 -0
- package/python/foxclaw/harness/_base.py +66 -0
- package/python/foxclaw/harness/_policy.py +340 -0
- package/python/foxclaw/harness/drift.py +77 -0
- package/python/foxclaw/harness/event_bus.py +28 -0
- package/python/foxclaw/harness/harness.py +358 -0
- package/python/foxclaw/harness/loop_detection.py +145 -0
- package/python/foxclaw/itinerary/__init__.py +10 -0
- package/python/foxclaw/itinerary/amap_client.py +114 -0
- package/python/foxclaw/itinerary/models.py +80 -0
- package/python/foxclaw/itinerary/reasoner.py +96 -0
- package/python/foxclaw/itinerary/reasoning.py +168 -0
- package/python/foxclaw/itinerary/runtime.py +144 -0
- package/python/foxclaw/kernel/__init__.py +13 -0
- package/python/foxclaw/kernel/capabilities.py +72 -0
- package/python/foxclaw/kernel/policy_gate.py +108 -0
- package/python/foxclaw/kernel/process_manager.py +186 -0
- package/python/foxclaw/kernel/scheduler.py +125 -0
- package/python/foxclaw/kernel/sentinel.py +443 -0
- package/python/foxclaw/kernel/sentinel_client.py +243 -0
- package/python/foxclaw/kernel/spine.py +97 -0
- package/python/foxclaw/knowledge/__init__.py +360 -0
- package/python/foxclaw/knowledge/truth_crystallizer.py +89 -0
- package/python/foxclaw/learning/__init__.py +12 -0
- package/python/foxclaw/learning/knowledge_graph.py +144 -0
- package/python/foxclaw/learning/models.py +93 -0
- package/python/foxclaw/learning/reasoner.py +67 -0
- package/python/foxclaw/learning/runtime.py +351 -0
- package/python/foxclaw/learning/spaced_repetition.py +91 -0
- package/python/foxclaw/mcp_server.py +188 -0
- package/python/foxclaw/orchestration/__init__.py +167 -0
- package/python/foxclaw/pipeline/__init__.py +23 -0
- package/python/foxclaw/pipeline/cognitive_pipeline.py +429 -0
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +201 -0
- package/python/foxclaw/planning/__init__.py +17 -0
- package/python/foxclaw/planning/llm_planner.py +232 -0
- package/python/foxclaw/planning/planner.py +94 -0
- package/python/foxclaw/search/__init__.py +24 -0
- package/python/foxclaw/search/agent_port.py +190 -0
- package/python/foxclaw/search/evidence.py +109 -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 +71 -0
- package/python/foxclaw/search/models.py +127 -0
- package/python/foxclaw/search/planner.py +44 -0
- package/python/foxclaw/search/retriever.py +39 -0
- package/python/foxclaw/search/runtime.py +127 -0
- package/python/foxclaw/search/strategy.py +44 -0
- package/python/foxclaw/skills/__init__.py +559 -0
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +69 -0
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +252 -0
- package/python/foxclaw/skills/markdown_skill.py +175 -0
- package/python/foxclaw/skills/markdown_store.py +39 -0
- package/python/foxclaw/skills/mcp_client.py +184 -0
- package/python/foxclaw/skills/mcp_store.py +40 -0
- package/python/foxclaw/skills/permission.py +145 -0
- package/python/foxclaw/skills/pipeline.py +354 -0
- package/python/foxclaw/skills/policy.py +74 -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 +25 -0
- package/python/frontend/deck/chat_model_picker.js +483 -0
- package/python/frontend/deck/domain.js +276 -0
- package/python/frontend/deck/main.js +1806 -0
- package/python/frontend/deck/markdown.js +44 -0
- package/python/frontend/deck/settings.js +307 -0
- package/python/frontend/deck/state.js +5 -0
- package/python/frontend/deck/util.js +4 -0
- package/python/frontend/deck/visuals.js +190 -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 +1436 -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-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +15 -0
- package/python/frontend/widget_runtime.js +252 -0
- package/python/frontend/widgets.js +639 -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 +58 -0
- package/python/frontend_src/src/api.js +82 -0
- package/python/frontend_src/src/components/ChatDock.vue +365 -0
- package/python/frontend_src/src/components/ComposerMenu.vue +75 -0
- package/python/frontend_src/src/components/DocViewer.vue +162 -0
- package/python/frontend_src/src/components/Inspector.vue +235 -0
- package/python/frontend_src/src/components/LeftRail.vue +415 -0
- package/python/frontend_src/src/components/MenuPopover.vue +128 -0
- package/python/frontend_src/src/components/MessageStream.vue +389 -0
- package/python/frontend_src/src/components/ModelPicker.vue +350 -0
- package/python/frontend_src/src/components/QuickIngest.vue +112 -0
- package/python/frontend_src/src/components/Stage.vue +272 -0
- package/python/frontend_src/src/components/ToolMenu.vue +141 -0
- package/python/frontend_src/src/components/TopBar.vue +118 -0
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +82 -0
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +83 -0
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +425 -0
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +117 -0
- package/python/frontend_src/src/components/panels/PanelBox.vue +50 -0
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +292 -0
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +358 -0
- package/python/frontend_src/src/components/panels/TasksPanel.vue +73 -0
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +746 -0
- package/python/frontend_src/src/components/panels/UsagePanel.vue +105 -0
- package/python/frontend_src/src/lib/fmt.js +44 -0
- 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 +190 -0
- package/python/frontend_src/src/main.js +9 -0
- package/python/frontend_src/src/stores/app.js +91 -0
- package/python/frontend_src/src/stores/chat.js +450 -0
- package/python/frontend_src/src/stores/model.js +96 -0
- package/python/frontend_src/src/stores/panels.js +289 -0
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -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 +39 -0
- package/python/pyproject.toml +268 -0
- package/python/requirements.txt +311 -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 +54 -0
- package/python/tests/run_all.py +200 -0
- package/python/tests/test_agent_lifecycle.py +326 -0
- package/python/tests/test_ahs_p2.py +393 -0
- package/python/tests/test_anthropic_tools.py +192 -0
- package/python/tests/test_api.py +97 -0
- package/python/tests/test_api_auth.py +79 -0
- package/python/tests/test_api_host_guard.py +69 -0
- package/python/tests/test_attachments.py +151 -0
- package/python/tests/test_audit_chain.py +116 -0
- package/python/tests/test_audit_sink.py +130 -0
- package/python/tests/test_browser.py +149 -0
- package/python/tests/test_browser_request_guard.py +180 -0
- package/python/tests/test_capabilities.py +152 -0
- package/python/tests/test_channel_signing.py +185 -0
- package/python/tests/test_channel_trust.py +109 -0
- package/python/tests/test_channels.py +174 -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 +61 -0
- package/python/tests/test_confirm_nonce.py +186 -0
- package/python/tests/test_context_compaction_wiring.py +124 -0
- package/python/tests/test_dispatch.py +89 -0
- package/python/tests/test_domain_evals.py +334 -0
- package/python/tests/test_domain_import.py +72 -0
- package/python/tests/test_domain_reasoners.py +134 -0
- package/python/tests/test_e2e.py +161 -0
- package/python/tests/test_emb_cache.py +97 -0
- package/python/tests/test_embedding_singleton.py +87 -0
- package/python/tests/test_envfile.py +83 -0
- package/python/tests/test_evals_smoke.py +90 -0
- package/python/tests/test_event_spine.py +174 -0
- package/python/tests/test_evidence_semantic.py +88 -0
- package/python/tests/test_evidence_sse.py +94 -0
- package/python/tests/test_file_ingest.py +135 -0
- package/python/tests/test_finance.py +144 -0
- package/python/tests/test_finance_ledger.py +245 -0
- package/python/tests/test_finance_persist.py +174 -0
- package/python/tests/test_gateway.py +125 -0
- package/python/tests/test_governance.py +421 -0
- package/python/tests/test_governance_audit.py +324 -0
- package/python/tests/test_governance_middleware.py +137 -0
- package/python/tests/test_hkc.py +268 -0
- package/python/tests/test_hkc_index_build_manager.py +306 -0
- package/python/tests/test_hkc_manifest_reuse.py +294 -0
- package/python/tests/test_hkc_search_event_sync.py +482 -0
- package/python/tests/test_import_path_guard.py +155 -0
- package/python/tests/test_injection_guard.py +193 -0
- package/python/tests/test_integration.py +117 -0
- package/python/tests/test_itinerary.py +100 -0
- package/python/tests/test_kee_semantic.py +84 -0
- package/python/tests/test_kernel.py +212 -0
- package/python/tests/test_learning.py +101 -0
- package/python/tests/test_llm_admin.py +145 -0
- package/python/tests/test_llm_planner.py +344 -0
- package/python/tests/test_markdown_skills.py +62 -0
- package/python/tests/test_markdown_skills_openclaw.py +66 -0
- package/python/tests/test_mcp_connectors.py +97 -0
- package/python/tests/test_mcp_guardian_discovery.py +87 -0
- package/python/tests/test_mcp_no_memory.py +43 -0
- package/python/tests/test_mcp_policy_gate.py +76 -0
- package/python/tests/test_mcp_server.py +92 -0
- package/python/tests/test_memory_recall.py +92 -0
- package/python/tests/test_metrics.py +58 -0
- package/python/tests/test_multi_chain.py +90 -0
- package/python/tests/test_multimodal.py +122 -0
- package/python/tests/test_negotiation.py +136 -0
- package/python/tests/test_orchestration.py +109 -0
- package/python/tests/test_orchestration_decoupled.py +103 -0
- package/python/tests/test_p3_p5_hardening.py +324 -0
- package/python/tests/test_parallel_dispatch.py +207 -0
- package/python/tests/test_pipeline.py +492 -0
- package/python/tests/test_plan_visible.py +86 -0
- package/python/tests/test_planner.py +88 -0
- package/python/tests/test_policy_gate.py +108 -0
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +170 -0
- package/python/tests/test_release_check.py +252 -0
- package/python/tests/test_resume_checkpoint.py +148 -0
- package/python/tests/test_routing.py +85 -0
- package/python/tests/test_scheduler.py +138 -0
- package/python/tests/test_search.py +194 -0
- package/python/tests/test_secrets.py +153 -0
- package/python/tests/test_semantic.py +69 -0
- package/python/tests/test_sessions.py +90 -0
- package/python/tests/test_settings.py +265 -0
- package/python/tests/test_shadow_learning.py +290 -0
- package/python/tests/test_shared_write.py +200 -0
- package/python/tests/test_skill_taint_gate.py +145 -0
- package/python/tests/test_skills.py +117 -0
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +162 -0
- package/python/tests/test_streaming.py +55 -0
- package/python/tests/test_supervisor_pipeline.py +176 -0
- package/python/tests/test_supervisor_triage.py +102 -0
- package/python/tests/test_taint.py +112 -0
- package/python/tests/test_task_stream.py +74 -0
- package/python/tests/test_thinking_sse.py +66 -0
- package/python/tests/test_thinking_stream.py +76 -0
- package/python/tests/test_tool_center.py +113 -0
- package/python/tests/test_tool_parallel.py +113 -0
- package/python/tests/test_tool_params.py +99 -0
- package/python/tests/test_tool_use.py +229 -0
- package/python/tests/test_toplevel_concurrency.py +116 -0
- package/python/tests/test_tracing.py +256 -0
- package/python/tests/test_truth_crystallizer.py +125 -0
- package/python/tests/test_truth_flow.py +88 -0
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_url_policy.py +53 -0
- package/python/tests/test_verify_evidence.py +212 -0
- package/python/tests/test_version_sync.py +100 -0
- package/python/tests/test_web_trigger.py +82 -0
- package/python/tools/check_faiss_type.py +30 -0
- package/python/tools/check_log.ps1 +38 -0
- package/python/tools/ivf_status.py +72 -0
- package/python/tools/mypy_baseline.txt +13 -0
- package/python/tools/mypy_ratchet.py +143 -0
- package/python/tools/pack.py +173 -0
- package/python/tools/release_check.py +349 -0
- package/python/tools/restart_foxclaw.ps1 +71 -0
- package/python/tools/shared_write_audit.py +190 -0
- package/python/tools/size_baseline.txt +28 -0
- package/python/tools/size_check.py +102 -0
- package/python/tools/strip_comments.py +228 -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,416 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hkc-api / container.py
|
|
3
|
+
应用容器:按依赖顺序装配 HKC 所有组件。
|
|
4
|
+
|
|
5
|
+
装配顺序(依赖从下到上):
|
|
6
|
+
1. IDGenerator ← 无依赖
|
|
7
|
+
2. GraphStore ← 无依赖
|
|
8
|
+
3. EventBus ← 无依赖
|
|
9
|
+
4. KEE ← GraphStore + EventBus + IDGen(订阅 knowledge.created)
|
|
10
|
+
5. ACE ← GraphStore + EventBus + IDGen
|
|
11
|
+
6. KDE ← GraphStore + EventBus + IDGen
|
|
12
|
+
7. HybridSearch ← GraphStore(+ 可选向量后端)
|
|
13
|
+
|
|
14
|
+
容器是单例,整个 API 进程共享一套组件。
|
|
15
|
+
"""
|
|
16
|
+
# TODO: Replace with proper package install before v1 release
|
|
17
|
+
|
|
18
|
+
import logging
|
|
19
|
+
import threading
|
|
20
|
+
from collections import deque
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HKCContainer:
|
|
28
|
+
"""HKC 组件容器。一次装配,全程共享。"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
data_dir: str = "./hkc_data",
|
|
33
|
+
embedding_kind: str = "stub", # stub | local | tei
|
|
34
|
+
embedding_url: str = "http://localhost:8080",
|
|
35
|
+
embedding_model: Optional[str] = None, # local/auto 后端的模型名(如 BAAI/bge-m3)
|
|
36
|
+
llm_api_key: Optional[str] = None,
|
|
37
|
+
llm_model: Optional[str] = None,
|
|
38
|
+
llm_provider: str = "anthropic",
|
|
39
|
+
llm_base_url: Optional[str] = None,
|
|
40
|
+
):
|
|
41
|
+
self.data_dir = Path(data_dir)
|
|
42
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
self._embedding_kind = embedding_kind
|
|
45
|
+
self._embedding_url = embedding_url
|
|
46
|
+
self._embedding_model = embedding_model
|
|
47
|
+
self._llm_api_key = llm_api_key
|
|
48
|
+
self._llm_model = llm_model
|
|
49
|
+
self._llm_provider = llm_provider
|
|
50
|
+
self._llm_base_url = llm_base_url
|
|
51
|
+
|
|
52
|
+
# 组件占位
|
|
53
|
+
self.id_gen = None
|
|
54
|
+
self.graph_store = None
|
|
55
|
+
self.event_bus = None
|
|
56
|
+
self.kee = None
|
|
57
|
+
self.ace = None
|
|
58
|
+
self.kde = None
|
|
59
|
+
self.search = None
|
|
60
|
+
self.ingest_jobs = None # 异步摄入任务注册表(方案A)
|
|
61
|
+
self.synthesis = None # 综合页 LLM 综述(派生只读视图,不进知识库)
|
|
62
|
+
self.crystallizer = None # Crystallizer 摄入适配器(集成层入口:外部推知识候选)
|
|
63
|
+
self.index_build_manager = None # 阶段 C:后台冷构建管理器(无磁盘索引时跑)
|
|
64
|
+
|
|
65
|
+
# B3:IndexBuildManager 跑时(building)_sync_ku 入队,跑完(ready)flush。
|
|
66
|
+
# maxlen 防 OOM(超出从头丢,降级而非阻断,摄入仍走同步路径)
|
|
67
|
+
self._pending_ops: deque = deque(maxlen=10000)
|
|
68
|
+
self._pending_lock = threading.Lock()
|
|
69
|
+
|
|
70
|
+
self._assembled = False
|
|
71
|
+
|
|
72
|
+
def assemble(self) -> "HKCContainer":
|
|
73
|
+
"""按依赖顺序装配所有组件。幂等。"""
|
|
74
|
+
if self._assembled:
|
|
75
|
+
return self
|
|
76
|
+
|
|
77
|
+
from hkc_core.utils.id_gen import IDGenerator
|
|
78
|
+
from hkc_core.graph.sqlite_store import SQLiteGraphStore
|
|
79
|
+
from hkc_kep.event_bus import EventBus
|
|
80
|
+
from hkc_kee.kee import KnowledgeEvolutionEngine
|
|
81
|
+
from hkc_ace.ace import AbilityCompilerEngine
|
|
82
|
+
from hkc_kde.kde import KnowledgeDigestEngine
|
|
83
|
+
from hkc_search.hybrid import HybridSearch
|
|
84
|
+
from hkc_search.vector_search import VectorIndex
|
|
85
|
+
from hkc_search.embedding_backends import make_backend
|
|
86
|
+
|
|
87
|
+
# 1. ID 生成器
|
|
88
|
+
self.id_gen = IDGenerator(str(self.data_dir / "id_counters.db"))
|
|
89
|
+
|
|
90
|
+
# 2. GraphStore
|
|
91
|
+
self.graph_store = SQLiteGraphStore(str(self.data_dir / "hkc.db"))
|
|
92
|
+
|
|
93
|
+
# 3. EventBus
|
|
94
|
+
self.event_bus = EventBus(str(self.data_dir / "events.db"))
|
|
95
|
+
|
|
96
|
+
# 4. KEE(构造时自动订阅 knowledge.created)
|
|
97
|
+
self.kee = KnowledgeEvolutionEngine(
|
|
98
|
+
self.graph_store, self.event_bus, self.id_gen
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# 5. ACE
|
|
102
|
+
self.ace = AbilityCompilerEngine(
|
|
103
|
+
self.graph_store, self.event_bus, self.id_gen,
|
|
104
|
+
output_dir=str(self.data_dir / "abilities"),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 6. KDE
|
|
108
|
+
self.kde = KnowledgeDigestEngine(
|
|
109
|
+
self.graph_store, self.event_bus, self.id_gen,
|
|
110
|
+
api_key=self._llm_api_key, model=self._llm_model,
|
|
111
|
+
provider=self._llm_provider, base_url=self._llm_base_url,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# 7. HybridSearch(向量后端可配置)
|
|
115
|
+
backend_kwargs = {}
|
|
116
|
+
if self._embedding_kind == "tei":
|
|
117
|
+
backend_kwargs["base_url"] = self._embedding_url
|
|
118
|
+
# local/auto 后端可指定模型名(如 BAAI/bge-m3,1024 维,维度由模型自动校正);
|
|
119
|
+
# 不传则用 LocalSTBackend 默认模型,行为与此前一致。tei 不接受 model_name,不传。
|
|
120
|
+
if self._embedding_model and self._embedding_kind in ("local", "auto"):
|
|
121
|
+
backend_kwargs["model_name"] = self._embedding_model
|
|
122
|
+
backend = make_backend(self._embedding_kind, **backend_kwargs)
|
|
123
|
+
vector_index = VectorIndex(backend=backend)
|
|
124
|
+
self.search = HybridSearch(self.graph_store, vector_index=vector_index)
|
|
125
|
+
|
|
126
|
+
# 启动时加载持久化索引 + 准备 BM25。阶段 C 后,**全量 build 由 IndexBuildManager
|
|
127
|
+
# 后台线程承担**,assemble() 内部只做「加载已存在」+「BM25 重建」,保证 Lifespan
|
|
128
|
+
# 启动不被 30-60 分钟的冷编码卡住。IndexBuildManager 见下段。
|
|
129
|
+
self._load_persisted_index_and_bm25()
|
|
130
|
+
|
|
131
|
+
# 阶段 C:起后台冷构建 manager。它会自动:
|
|
132
|
+
# · vector 已覆盖库 → 立即返,标 ready
|
|
133
|
+
# · vector 部分覆盖 / 为空 → 启 daemon 线程,扫描 + encode + append_bulk
|
|
134
|
+
# 全部完成后,manager 内部 save() 落盘,下次启动 assemble() 直接加载。
|
|
135
|
+
from hkc_search.index_build_manager import IndexBuildManager
|
|
136
|
+
import os as _os
|
|
137
|
+
self.index_build_manager = IndexBuildManager(
|
|
138
|
+
data_dir=str(self.data_dir),
|
|
139
|
+
hybrid_search=self.search,
|
|
140
|
+
graph_store=self.graph_store,
|
|
141
|
+
batch_size=int(_os.environ.get("HKC_BUILD_BATCH_SIZE", "64")),
|
|
142
|
+
max_failures=int(_os.environ.get("HKC_BUILD_MAX_FAILURES", "100")),
|
|
143
|
+
checkpoint_every_batches=int(_os.environ.get("HKC_BUILD_CHECKPOINT_BATCHES", "5")),
|
|
144
|
+
on_ready=lambda: self.flush_pending_ops(), # B3:ready 时 flush pending 队列
|
|
145
|
+
)
|
|
146
|
+
self.index_build_manager.start()
|
|
147
|
+
|
|
148
|
+
# 新知识写入后自动更新搜索索引
|
|
149
|
+
self._wire_search_index_updates()
|
|
150
|
+
|
|
151
|
+
# 8. 异步摄入任务注册表(后台线程池执行文件摄入)
|
|
152
|
+
from hkc_api.ingest_jobs import IngestJobRegistry
|
|
153
|
+
self.ingest_jobs = IngestJobRegistry(max_workers=4)
|
|
154
|
+
|
|
155
|
+
# 9. 综合页 LLM 综述服务(派生只读视图;缓存独立 synthesis.db,不进知识库,守 Principle 07)
|
|
156
|
+
from hkc_api.synthesis import SynthesisService
|
|
157
|
+
self.synthesis = SynthesisService(
|
|
158
|
+
self.graph_store, self.search, self.kde,
|
|
159
|
+
str(self.data_dir / "synthesis.db"),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# 10. Crystallizer 摄入适配器(集成层入口):外部系统经 POST /knowledge/crystallize
|
|
163
|
+
# 推知识候选,走 HKCIngress → KDE → KEE。守边界:只产候选,不做知识身份判定。
|
|
164
|
+
from hkc_crystallizer.ingress import HKCIngress
|
|
165
|
+
self.crystallizer = HKCIngress(self.kde)
|
|
166
|
+
|
|
167
|
+
self._embedding_name = backend.name # 实际生效的向量后端名(供 /stats 展示)
|
|
168
|
+
self._assembled = True
|
|
169
|
+
logger.info("HKC 容器装配完成: data_dir=%s embedding=%s",
|
|
170
|
+
self.data_dir, backend.name)
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def _load_persisted_index_and_bm25(self):
|
|
174
|
+
"""阶段 C:启动期**只**加载磁盘索引 + 重建 BM25,**不**做全量 encode,
|
|
175
|
+
**不**做增量补算 —— 这两件事交给 IndexBuildManager 后台线程。
|
|
176
|
+
- vector.load:从 search_vector.faiss / .meta 加载(阶段 A 8 步校验失败 → _reset)
|
|
177
|
+
- bm25.build:从 store 拉 active KU 重建(快,毫秒级,不会卡启动)
|
|
178
|
+
设计目标:assemble() 整体在秒级返回(在 1.6 万 KU 库上,< 5s),
|
|
179
|
+
不再因为 30-60 分钟的冷编码被 Lifespan 启动超时打断。
|
|
180
|
+
"""
|
|
181
|
+
vec_path = str(self.data_dir / "search_vector")
|
|
182
|
+
try:
|
|
183
|
+
kus = self.graph_store.query_all(limit=99999999) or []
|
|
184
|
+
except Exception:
|
|
185
|
+
kus = []
|
|
186
|
+
loaded_n = 0
|
|
187
|
+
if kus:
|
|
188
|
+
try:
|
|
189
|
+
if self.search.vector.load(vec_path):
|
|
190
|
+
loaded_n = self.search.vector.size()
|
|
191
|
+
logger.info(
|
|
192
|
+
"向量索引已从磁盘加载: %d 条 (库内共 %d 条,余下交 manager 后台)",
|
|
193
|
+
loaded_n, len(kus))
|
|
194
|
+
except Exception as e:
|
|
195
|
+
logger.warning("加载持久化向量索引失败,manager 会兜底: %s", e)
|
|
196
|
+
# BM25 必须重建一次(它不持久化;且重建开销极小,毫秒级)
|
|
197
|
+
if kus:
|
|
198
|
+
try:
|
|
199
|
+
self.search.bm25.build(kus)
|
|
200
|
+
except Exception as e:
|
|
201
|
+
logger.warning("BM25 重建失败(非致命,search 会回退 bm25=None 保护): %s", e)
|
|
202
|
+
if not kus:
|
|
203
|
+
logger.info("库为空,无需构建索引")
|
|
204
|
+
|
|
205
|
+
def _wire_search_index_updates(self):
|
|
206
|
+
"""订阅 KU 事件,统一走 _sync_ku 五分支。
|
|
207
|
+
|
|
208
|
+
阶段 B 修正:
|
|
209
|
+
① CREATED 处理器必须**查 status** 后再走 _sync_ku(ku_id) —— 重复 KU 已被
|
|
210
|
+
KEE 软删,append_ku 会把同 id 推进 vector,导致检索返回「已删」内容。
|
|
211
|
+
② UPDATED 处理器也要查 status —— KEE 状态机把 Claim 从 active 改成
|
|
212
|
+
superseded/rejected,vector 必须同步 remove,否则检索召回已废弃 KU。
|
|
213
|
+
③ DELETED 处理器不查 store(被删了 get=None),直接 vector.remove_ku。
|
|
214
|
+
|
|
215
|
+
_sync_ku 五分支:
|
|
216
|
+
1) store.get(ku_id) is None → vector.remove(兜底)
|
|
217
|
+
2) status == 'deleted' → vector.remove
|
|
218
|
+
3) status not in {active} → vector.remove (superseded/rejected/disputed)
|
|
219
|
+
4) active + 不在 vector → vector.append_ku
|
|
220
|
+
5) active + 在 vector + text_hash 变 → vector.update_ku(remove+append)
|
|
221
|
+
5b) active + 在 vector + text_hash 同 → noop (同 hash 跳过 encode)
|
|
222
|
+
|
|
223
|
+
五分支都包在 try/except:单个 KU 同步失败不阻断其它 KU(继续下一条)。
|
|
224
|
+
"""
|
|
225
|
+
from hkc_kep.event_bus import KEPEvents
|
|
226
|
+
|
|
227
|
+
def _on_ku_event(event_name: str, event: dict):
|
|
228
|
+
ku_ids = event.get("payload", {}).get("ku_ids", [])
|
|
229
|
+
if not ku_ids and "ku_id" in event.get("payload", {}):
|
|
230
|
+
ku_ids = [event["payload"]["ku_id"]]
|
|
231
|
+
for ku_id in ku_ids:
|
|
232
|
+
self._sync_ku(ku_id)
|
|
233
|
+
|
|
234
|
+
self.event_bus.subscribe(KEPEvents.KNOWLEDGE_CREATED,
|
|
235
|
+
lambda e: _on_ku_event("CREATED", e))
|
|
236
|
+
self.event_bus.subscribe(KEPEvents.KNOWLEDGE_UPDATED,
|
|
237
|
+
lambda e: _on_ku_event("UPDATED", e))
|
|
238
|
+
self.event_bus.subscribe(KEPEvents.KNOWLEDGE_DELETED,
|
|
239
|
+
lambda e: _on_ku_event("DELETED", e))
|
|
240
|
+
|
|
241
|
+
def _sync_ku(self, ku_id: str) -> None:
|
|
242
|
+
"""B3 入口:IndexBuildManager building 时入队(微秒返回,不持任何锁);
|
|
243
|
+
其他状态走 _do_sync_ku(原五分支逻辑)。
|
|
244
|
+
单一 KU 失败不抛,只 log warning —— 摄入任务里 50 条 KU 失败 1 条不应拖累其余。
|
|
245
|
+
|
|
246
|
+
B3 设计要点(2026-10-16 修正):
|
|
247
|
+
- **不**在 _sync_ku 入口同步 flush(早期版本有 bug:每次入队都
|
|
248
|
+
同步调 flush_pending_ops(),IndexBuildManager 跑时持 hybrid._write_lock
|
|
249
|
+
3-5s/batch(内含 encode),user 上传线程 acquire 同一锁等 3-5s
|
|
250
|
+
→ 整个 ingest pipeline 卡死 30-50s)。
|
|
251
|
+
- 依赖 on_ready 回调集中 flush(IndexBuildManager._mark_ready_locked
|
|
252
|
+
触发,state=ready 时 hybrid._write_lock 不再被持,flush 立即完成)。
|
|
253
|
+
- 兜底:on_ready 漏掉的极端情况,user 后续上传走 _do_sync_ku
|
|
254
|
+
同步路径(state 已变 ready,走 else 分支)。
|
|
255
|
+
"""
|
|
256
|
+
# B3:building 期间走队列,绕开 _sync_ku 内部的 graph_store.get + hybrid.* 锁
|
|
257
|
+
if (self.index_build_manager is not None
|
|
258
|
+
and self.index_build_manager.state == "building"):
|
|
259
|
+
with self._pending_lock:
|
|
260
|
+
self._pending_ops.append(("sync", ku_id))
|
|
261
|
+
# 不在入口同步 flush —— 由 on_ready 回调集中处理
|
|
262
|
+
return
|
|
263
|
+
self._do_sync_ku(ku_id)
|
|
264
|
+
|
|
265
|
+
def _do_sync_ku(self, ku_id: str) -> None:
|
|
266
|
+
"""五分支同步单条 KU 到 vector(原 _sync_ku 逻辑,B3 入口和 flush 共用)。
|
|
267
|
+
单一 KU 失败不抛,只 log warning。
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
ku = self.graph_store.get(ku_id)
|
|
271
|
+
except Exception as e:
|
|
272
|
+
logger.warning("_sync_ku 读 store 失败(%s): %s", ku_id, e)
|
|
273
|
+
return
|
|
274
|
+
if ku is None or getattr(ku, "status", "") == "deleted":
|
|
275
|
+
# 1) / 2) — KU 不存在或已删,删 vector
|
|
276
|
+
try:
|
|
277
|
+
self.search.remove_ku(ku_id)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning("vector.remove_ku(%s) 失败: %s", ku_id, e)
|
|
280
|
+
return
|
|
281
|
+
if getattr(ku, "status", "active") not in ("active",):
|
|
282
|
+
# 3) — superseded / rejected / disputed(非 active 状态)也不该被检索召回
|
|
283
|
+
try:
|
|
284
|
+
self.search.remove_ku(ku_id)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning("vector.remove_ku(%s) 失败: %s", ku_id, e)
|
|
287
|
+
return
|
|
288
|
+
# Claim 还要看 claim_status(可能 active 但 claim_status 是 superseded)
|
|
289
|
+
if getattr(ku, "ku_type", None) and getattr(ku.ku_type, "value", "") == "Claim":
|
|
290
|
+
extra = getattr(ku, "to_dict", lambda: {} )().get("extra", {}) or {}
|
|
291
|
+
cs = extra.get("claim_status", "active")
|
|
292
|
+
if cs in ("superseded", "rejected", "disputed"):
|
|
293
|
+
try:
|
|
294
|
+
self.search.remove_ku(ku_id)
|
|
295
|
+
except Exception as e:
|
|
296
|
+
logger.warning("vector.remove_ku(%s) 失败: %s", ku_id, e)
|
|
297
|
+
return
|
|
298
|
+
# 4) / 5) / 5b)
|
|
299
|
+
in_vector = ku_id in (self.search.vector._ku_ids or [])
|
|
300
|
+
if not in_vector:
|
|
301
|
+
try:
|
|
302
|
+
self.search.append_ku(ku)
|
|
303
|
+
except Exception as e:
|
|
304
|
+
logger.warning("vector.append_ku(%s) 失败: %s", ku_id, e)
|
|
305
|
+
else:
|
|
306
|
+
try:
|
|
307
|
+
# update_ku 内部用 text_hash 检测:同 hash 跳过 encode,变才重编
|
|
308
|
+
self.search.update_ku(ku)
|
|
309
|
+
except Exception as e:
|
|
310
|
+
logger.warning("vector.update_ku(%s) 失败: %s", ku_id, e)
|
|
311
|
+
|
|
312
|
+
def flush_pending_ops(self) -> int:
|
|
313
|
+
"""B3:flush 队列里 _sync_ku 攒下的待处理 op。
|
|
314
|
+
- atomic swap:持 pending_lock 取队列副本 + 清空(几微秒)
|
|
315
|
+
- 释放 lock 后批量执行(slow 操作在锁外,调 _do_sync_ku 走原五分支)
|
|
316
|
+
- 返回 flush 数量(供 on_ready 回调 / 调试)
|
|
317
|
+
设计:IndexBuildManager ready 时调一次;每次 _sync_ku 入队也自检
|
|
318
|
+
flush(避免 ready 事件错过)。风险:deque maxlen=10000 超出从头丢;
|
|
319
|
+
但 _sync_ku 入队时自检 state,ready 后立即 flush,正常不积压。"""
|
|
320
|
+
with self._pending_lock:
|
|
321
|
+
if not self._pending_ops:
|
|
322
|
+
return 0
|
|
323
|
+
pending = list(self._pending_ops)
|
|
324
|
+
self._pending_ops.clear()
|
|
325
|
+
flushed = 0
|
|
326
|
+
for op in pending:
|
|
327
|
+
try:
|
|
328
|
+
op_type, ku_id = op[0], op[1]
|
|
329
|
+
if op_type == "sync":
|
|
330
|
+
# 调 _do_sync_ku(跳过 B3 检测,强制同步执行,避免再次入队死循环)
|
|
331
|
+
self._do_sync_ku(ku_id)
|
|
332
|
+
flushed += 1
|
|
333
|
+
except Exception as e:
|
|
334
|
+
logger.warning("flush_pending_ops 失败[%s/%s]: %s",
|
|
335
|
+
op[0], op[1], e)
|
|
336
|
+
return flushed
|
|
337
|
+
|
|
338
|
+
def stats(self) -> dict:
|
|
339
|
+
"""返回系统整体统计。"""
|
|
340
|
+
if not self._assembled:
|
|
341
|
+
return {"assembled": False}
|
|
342
|
+
s = self.graph_store.stats()
|
|
343
|
+
s["assembled"] = True
|
|
344
|
+
s["data_dir"] = str(self.data_dir.resolve()) # 绝对路径,前端显示 + 排查"连错目录"
|
|
345
|
+
s["embedding"] = getattr(self, "_embedding_name", "") # 生效的向量后端(stub 无语义)
|
|
346
|
+
s["bm25_size"] = self.search.bm25.size()
|
|
347
|
+
s["vector_size"] = self.search.vector.size()
|
|
348
|
+
# 阶段 C:把后台构建状态也吐出,前端可做「正在编码 N/M」提示
|
|
349
|
+
if self.index_build_manager is not None:
|
|
350
|
+
s["index_build"] = self.index_build_manager.stats()
|
|
351
|
+
else:
|
|
352
|
+
s["index_build"] = {"index_state": "idle"}
|
|
353
|
+
s["abilities"] = self.ace.list_available()
|
|
354
|
+
# B3 监控:IndexBuildManager building 期间 _sync_ku 入队的 KU 数
|
|
355
|
+
# (= 待 flush 积压)。ready 后被 on_ready 回调一次性清零
|
|
356
|
+
s["pending_ops"] = len(self._pending_ops)
|
|
357
|
+
return s
|
|
358
|
+
|
|
359
|
+
def close(self):
|
|
360
|
+
"""关闭资源。每次 reset_container(测试 tearDown / 服务热重载)都会调,
|
|
361
|
+
必须把线程池和额外的 SQLite 连接一并释放,否则会累积线程/文件描述符泄漏。"""
|
|
362
|
+
# 阶段 C:先停后台构建线程(它持有 self.search / self.graph_store 引用),
|
|
363
|
+
# 再释放 SQLite。否则若线程在 close 期间还在跑,SQLite 已被 close 句柄释放,
|
|
364
|
+
# 线程内 append_bulk 会爆「database is closed」。
|
|
365
|
+
if getattr(self, "index_build_manager", None):
|
|
366
|
+
try:
|
|
367
|
+
self.index_build_manager.cancel()
|
|
368
|
+
# 限时 join:不阻塞 close 太久(2s 内退出是合理预期,batch_size=64 bge-m3
|
|
369
|
+
# 约 3-5s,但 cancel 标志会让它在 batch 边界停)。
|
|
370
|
+
self.index_build_manager.join(timeout=2.0)
|
|
371
|
+
except Exception:
|
|
372
|
+
pass
|
|
373
|
+
if self.ingest_jobs:
|
|
374
|
+
try: self.ingest_jobs.shutdown()
|
|
375
|
+
except Exception: pass
|
|
376
|
+
if self.synthesis:
|
|
377
|
+
try: self.synthesis.cache.close()
|
|
378
|
+
except Exception: pass
|
|
379
|
+
# 4 个 SQLite 连接必须全部释放,否则 Windows 下数据目录删不掉(WinError 32)、
|
|
380
|
+
# 长期运行累积 fd 泄漏。此前只关了 graph_store,漏了 event_bus/id_gen(融合时暴露)。
|
|
381
|
+
if getattr(self, "event_bus", None):
|
|
382
|
+
try: self.event_bus.close()
|
|
383
|
+
except Exception: pass
|
|
384
|
+
if getattr(self, "id_gen", None):
|
|
385
|
+
try: self.id_gen.close()
|
|
386
|
+
except Exception: pass
|
|
387
|
+
if self.graph_store:
|
|
388
|
+
self.graph_store.close()
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
# ── 全局单例 ─────────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
_container: Optional[HKCContainer] = None
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def get_container() -> HKCContainer:
|
|
397
|
+
"""获取全局容器(FastAPI 依赖注入用)。"""
|
|
398
|
+
global _container
|
|
399
|
+
if _container is None:
|
|
400
|
+
raise RuntimeError("容器未初始化,请先调用 init_container()")
|
|
401
|
+
return _container
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def init_container(**kwargs) -> HKCContainer:
|
|
405
|
+
"""初始化全局容器。"""
|
|
406
|
+
global _container
|
|
407
|
+
_container = HKCContainer(**kwargs).assemble()
|
|
408
|
+
return _container
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def reset_container():
|
|
412
|
+
"""重置全局容器(测试用)。"""
|
|
413
|
+
global _container
|
|
414
|
+
if _container:
|
|
415
|
+
_container.close()
|
|
416
|
+
_container = None
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hkc-api / ingest_jobs.py
|
|
3
|
+
异步摄入任务注册表(方案A)。
|
|
4
|
+
|
|
5
|
+
上传文件后立即返回 job_id,真正的摄入(解析 + 逐 chunk 调 LLM,耗时数分钟)
|
|
6
|
+
在后台线程池里跑,前端轮询 job 状态拿真进度,期间可继续上传下一个。
|
|
7
|
+
|
|
8
|
+
- 内存态(崩溃后 job 丢失可接受,重传即可);线程安全。
|
|
9
|
+
- 进度来自 Extractor 逐 chunk 回调 progress_cb(processed, total)。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
import threading, time, uuid, logging
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
from dataclasses import dataclass, field, asdict
|
|
15
|
+
from typing import Callable, Optional
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class IngestJob:
|
|
22
|
+
job_id: str
|
|
23
|
+
filename: str
|
|
24
|
+
status: str = "queued" # queued | running | done | error
|
|
25
|
+
processed: int = 0 # 已抽取 chunk 数
|
|
26
|
+
total: int = 0 # chunk 总数(截断后的实际处理数)
|
|
27
|
+
ku_count: int = 0 # 产出 KU 数
|
|
28
|
+
error: str = ""
|
|
29
|
+
created_at: float = field(default_factory=time.time)
|
|
30
|
+
updated_at: float = field(default_factory=time.time)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class IngestJobRegistry:
|
|
34
|
+
"""维护 job_id → IngestJob,并用线程池在后台执行摄入。"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, max_workers: int = 4, max_jobs: int = 200):
|
|
37
|
+
self._jobs: dict[str, IngestJob] = {}
|
|
38
|
+
self._lock = threading.RLock()
|
|
39
|
+
self._max_jobs = max_jobs # 内存里最多保留多少条 job 记录
|
|
40
|
+
self._executor = ThreadPoolExecutor(
|
|
41
|
+
max_workers=max_workers, thread_name_prefix="ingest"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def create(self, filename: str) -> IngestJob:
|
|
45
|
+
job = IngestJob(job_id="job_" + uuid.uuid4().hex[:12], filename=filename)
|
|
46
|
+
with self._lock:
|
|
47
|
+
self._jobs[job.job_id] = job
|
|
48
|
+
self._evict_locked()
|
|
49
|
+
return job
|
|
50
|
+
|
|
51
|
+
def _evict_locked(self):
|
|
52
|
+
"""超过上限时,淘汰最旧的【已结束】job(done/error)。绝不淘汰进行中的
|
|
53
|
+
(queued/running)——前端只轮询进行中的,最旧的已完成 job 早已无人查看。
|
|
54
|
+
若全是进行中的,宁可暂时超限也不丢活跃 job。须在持锁下调用。"""
|
|
55
|
+
over = len(self._jobs) - self._max_jobs
|
|
56
|
+
if over <= 0:
|
|
57
|
+
return
|
|
58
|
+
finished = sorted(
|
|
59
|
+
(j for j in self._jobs.values() if j.status in ("done", "error")),
|
|
60
|
+
key=lambda j: j.created_at,
|
|
61
|
+
)
|
|
62
|
+
for j in finished[:over]:
|
|
63
|
+
self._jobs.pop(j.job_id, None)
|
|
64
|
+
|
|
65
|
+
def get(self, job_id: str) -> Optional[dict]:
|
|
66
|
+
"""返回 job 状态快照(dict)。不存在返回 None。"""
|
|
67
|
+
with self._lock:
|
|
68
|
+
job = self._jobs.get(job_id)
|
|
69
|
+
return asdict(job) if job else None
|
|
70
|
+
|
|
71
|
+
def submit(self, job: IngestJob, work: Callable[[Callable[[int, int], None]], list]) -> None:
|
|
72
|
+
"""
|
|
73
|
+
提交后台执行。work 接收一个 progress(processed,total) 回调,
|
|
74
|
+
返回写入的 KU 列表(用于 ku_count)。异常会被捕获记入 job.error。
|
|
75
|
+
"""
|
|
76
|
+
self._executor.submit(self._run, job, work)
|
|
77
|
+
|
|
78
|
+
def _run(self, job: IngestJob, work):
|
|
79
|
+
with self._lock:
|
|
80
|
+
job.status = "running"; job.updated_at = time.time()
|
|
81
|
+
|
|
82
|
+
def progress(processed: int, total: int):
|
|
83
|
+
with self._lock:
|
|
84
|
+
job.processed = processed; job.total = total
|
|
85
|
+
job.updated_at = time.time()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
kus = work(progress)
|
|
89
|
+
with self._lock:
|
|
90
|
+
job.status = "done"
|
|
91
|
+
job.ku_count = len(kus) if kus else 0
|
|
92
|
+
if job.total == 0: # 空文档等:没触发过进度回调
|
|
93
|
+
job.total = job.processed
|
|
94
|
+
job.updated_at = time.time()
|
|
95
|
+
logger.info("摄入 job 完成: %s (%s), %d KU", job.job_id, job.filename, job.ku_count)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
with self._lock:
|
|
98
|
+
job.status = "error"; job.error = str(e)[:200]
|
|
99
|
+
job.updated_at = time.time()
|
|
100
|
+
logger.exception("摄入 job 失败: %s (%s)", job.job_id, job.filename)
|
|
101
|
+
|
|
102
|
+
def shutdown(self):
|
|
103
|
+
self._executor.shutdown(wait=False)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hkc-api / main.py
|
|
3
|
+
HKC REST API 主应用。
|
|
4
|
+
|
|
5
|
+
启动(激活 venv 后统一用 python -m uvicorn):
|
|
6
|
+
python -m uvicorn hkc_api.main:app --host 127.0.0.1 --port 8000
|
|
7
|
+
|
|
8
|
+
环境变量:
|
|
9
|
+
HKC_DATA_DIR 数据目录(默认 ./hkc_data)
|
|
10
|
+
HKC_EMBEDDING stub | local | tei(默认 stub)
|
|
11
|
+
HKC_EMBEDDING_URL TEI 服务地址(embedding=tei 时用)
|
|
12
|
+
HKC_EMBEDDING_MODEL local/auto 后端的模型名(如 BAAI/bge-m3;留空用内置默认)
|
|
13
|
+
HKC_LLM_PROVIDER anthropic | deepseek | openai | openai-compatible(默认 anthropic)
|
|
14
|
+
HKC_LLM_API_KEY 通用 key;未设则按 provider 回退读 DEEPSEEK_API_KEY / OPENAI_API_KEY / ANTHROPIC_API_KEY
|
|
15
|
+
HKC_LLM_MODEL / HKC_LLM_BASE_URL 模型名 / 自定义 API 地址(本地模型时)
|
|
16
|
+
HKC_API_KEY 设了则请求需带 X-API-Key(可选鉴权)
|
|
17
|
+
"""
|
|
18
|
+
# TODO: Replace with proper package install before v1 release
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
from contextlib import asynccontextmanager
|
|
23
|
+
|
|
24
|
+
from fastapi import FastAPI
|
|
25
|
+
|
|
26
|
+
from .container import init_container, get_container, reset_container
|
|
27
|
+
from .schemas import StatsResponse
|
|
28
|
+
from .routes import knowledge, search, ability, config
|
|
29
|
+
|
|
30
|
+
logging.basicConfig(
|
|
31
|
+
level=logging.INFO,
|
|
32
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
33
|
+
)
|
|
34
|
+
logger = logging.getLogger("hkc_api")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@asynccontextmanager
|
|
38
|
+
async def lifespan(app: FastAPI):
|
|
39
|
+
"""启动时装配容器,关闭时清理。"""
|
|
40
|
+
# 【P2·单 LLM 入口】默认从 LLM_* 读,保留 HKC_LLM_* 显式覆盖向后兼容
|
|
41
|
+
_provider = (os.environ.get("HKC_LLM_PROVIDER")
|
|
42
|
+
or os.environ.get("LLM_PROVIDER")
|
|
43
|
+
or "anthropic").strip().lower()
|
|
44
|
+
# API key:优先 HKC_LLM_API_KEY,再 LLM_API_KEY,再各 provider 旧 env 兜底
|
|
45
|
+
_llm_key = (os.environ.get("HKC_LLM_API_KEY")
|
|
46
|
+
or os.environ.get("LLM_API_KEY")
|
|
47
|
+
or os.environ.get("DEEPSEEK_API_KEY")
|
|
48
|
+
or os.environ.get("OPENAI_API_KEY")
|
|
49
|
+
or os.environ.get("ANTHROPIC_API_KEY"))
|
|
50
|
+
# base_url 同源(单入口)
|
|
51
|
+
_llm_base = (os.environ.get("HKC_LLM_BASE_URL")
|
|
52
|
+
or os.environ.get("LLM_BASE_URL")) or None
|
|
53
|
+
# 模型名同源
|
|
54
|
+
_llm_model = (os.environ.get("HKC_LLM_MODEL")
|
|
55
|
+
or os.environ.get("LLM_MODEL")) or None
|
|
56
|
+
# embedding 模型:优先 EMBEDDING_MODEL(系统唯一本地入口),再 HKC_EMBEDDING_MODEL
|
|
57
|
+
_emb_model = (os.environ.get("EMBEDDING_MODEL")
|
|
58
|
+
or os.environ.get("HKC_EMBEDDING_MODEL")) or None
|
|
59
|
+
init_container(
|
|
60
|
+
data_dir = os.environ.get("HKC_DATA_DIR", "./hkc_data"),
|
|
61
|
+
embedding_kind = os.environ.get("HKC_EMBEDDING", "stub"),
|
|
62
|
+
embedding_url = os.environ.get("HKC_EMBEDDING_URL", "http://localhost:8080"),
|
|
63
|
+
embedding_model = _emb_model, # 空串→None,用默认模型
|
|
64
|
+
llm_api_key = _llm_key,
|
|
65
|
+
llm_model = _llm_model, # None → 按 provider 默认
|
|
66
|
+
llm_provider = _provider,
|
|
67
|
+
llm_base_url = _llm_base, # None → provider 默认
|
|
68
|
+
)
|
|
69
|
+
logger.info("HKC API 启动完成")
|
|
70
|
+
yield
|
|
71
|
+
reset_container()
|
|
72
|
+
logger.info("HKC API 已关闭")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
app = FastAPI(
|
|
76
|
+
title="Foxclaw Knowledge Core API",
|
|
77
|
+
description="知识从存储进化成能力。KDE 摄入 → KEE 演化 → ACE 编译 → Search 检索。",
|
|
78
|
+
version="1.0.0",
|
|
79
|
+
lifespan=lifespan,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# 可选 API 鉴权:设了环境变量 HKC_API_KEY 才启用;未设=开放(本地默认,行为不变)。
|
|
83
|
+
# 在 CORS 之前注册 → CORS 处于更外层,即便鉴权返回 401 也带上 CORS 头,浏览器能读到。
|
|
84
|
+
_api_key = os.environ.get("HKC_API_KEY", "").strip()
|
|
85
|
+
if _api_key:
|
|
86
|
+
from starlette.requests import Request
|
|
87
|
+
from starlette.responses import JSONResponse
|
|
88
|
+
_OPEN_PATHS = {"/", "/health", "/docs", "/openapi.json", "/redoc"}
|
|
89
|
+
|
|
90
|
+
@app.middleware("http")
|
|
91
|
+
async def _require_api_key(request: Request, call_next):
|
|
92
|
+
if request.method == "OPTIONS" or request.url.path in _OPEN_PATHS:
|
|
93
|
+
return await call_next(request)
|
|
94
|
+
if request.headers.get("x-api-key", "") != _api_key:
|
|
95
|
+
return JSONResponse(
|
|
96
|
+
status_code=401,
|
|
97
|
+
content={"detail": "缺少或错误的 API Key(请求头需带 X-API-Key)"},
|
|
98
|
+
)
|
|
99
|
+
return await call_next(request)
|
|
100
|
+
|
|
101
|
+
# CORS:允许前端(hkc-ui)从浏览器调用。HKC 是本地知识工具,默认放开所有来源;
|
|
102
|
+
# 部署到受控网络时可用 HKC_CORS_ORIGINS(逗号分隔)收紧。
|
|
103
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
104
|
+
_origins = os.environ.get("HKC_CORS_ORIGINS", "*")
|
|
105
|
+
app.add_middleware(
|
|
106
|
+
CORSMiddleware,
|
|
107
|
+
allow_origins=[o.strip() for o in _origins.split(",")] if _origins != "*" else ["*"],
|
|
108
|
+
allow_methods=["*"],
|
|
109
|
+
allow_headers=["*"],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# 注册路由
|
|
113
|
+
app.include_router(knowledge.router)
|
|
114
|
+
app.include_router(search.router)
|
|
115
|
+
app.include_router(ability.router)
|
|
116
|
+
app.include_router(config.router)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.get("/")
|
|
120
|
+
def root():
|
|
121
|
+
return {
|
|
122
|
+
"name": "Foxclaw Knowledge Core",
|
|
123
|
+
"version": "1.0.0",
|
|
124
|
+
"docs": "/docs",
|
|
125
|
+
"modules": ["KDE", "KEE", "ACE", "Search"],
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@app.get("/health")
|
|
130
|
+
def health():
|
|
131
|
+
return {"status": "ok"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.get("/stats", response_model=StatsResponse, tags=["system"])
|
|
135
|
+
def stats():
|
|
136
|
+
c = get_container()
|
|
137
|
+
return StatsResponse(**c.stats())
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.get("/events")
|
|
141
|
+
def recent_events(limit: int = 20):
|
|
142
|
+
"""查看最近的 KEP 事件(调试用)。"""
|
|
143
|
+
c = get_container()
|
|
144
|
+
return {"events": c.event_bus.recent_events(limit=limit)}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main():
|
|
148
|
+
import uvicorn
|
|
149
|
+
uvicorn.run(
|
|
150
|
+
app,
|
|
151
|
+
host=os.environ.get("HKC_HOST", "127.0.0.1"),
|
|
152
|
+
port=int(os.environ.get("HKC_PORT", "8000")),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
if __name__ == "__main__":
|
|
157
|
+
main()
|
|
File without changes
|