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,484 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hkc-search / vector_search.py
|
|
3
|
+
向量检索(Warm 层)。
|
|
4
|
+
|
|
5
|
+
向量生成委托给可插拔的 EmbeddingBackend(见 embedding_backends.py):
|
|
6
|
+
LocalSTBackend 本地 sentence-transformers
|
|
7
|
+
TEIBackend 远程 TEI 服务
|
|
8
|
+
StubBackend 离线/测试
|
|
9
|
+
|
|
10
|
+
FAISS IndexFlatIP 做近似最近邻,向量已 L2 归一化,内积 = 余弦相似度。
|
|
11
|
+
VectorIndex 不再关心模型如何加载,只调用 backend.encode()。
|
|
12
|
+
|
|
13
|
+
阶段 A:manifest 升级
|
|
14
|
+
────────────────────
|
|
15
|
+
旧 manifest(v1,无版本号)只存 `ku_ids / names / summaries / backend`,隐患:
|
|
16
|
+
1. 加载时不校验 backend/dim 与实际后端是否匹配 → 旧 faiss 向量配新 backend 会
|
|
17
|
+
让检索结果全错(向量空间不同)。
|
|
18
|
+
2. 不存文本 hash → 阶段 B update_ku 无法知道"内容是否真变",每条都重建向量
|
|
19
|
+
(5000 条 × 重新 encode 一次 ≈ 几十秒)。
|
|
20
|
+
3. 不存 dim → 切换后端后 faiss 向量维度可能与新后端 dim 不一致,加载即崩。
|
|
21
|
+
|
|
22
|
+
新 manifest(v2):
|
|
23
|
+
format_version: int (当前 2)
|
|
24
|
+
backend: str (后端名,如 "stub"/"local:bge-m3"/"tei:xxx")
|
|
25
|
+
dim: int (后端向量维度,加索引文件维度)
|
|
26
|
+
ku_ids: list[str]
|
|
27
|
+
names: list[str]
|
|
28
|
+
summaries: list[str]
|
|
29
|
+
text_hashes: list[str] ← 阶段 B 用,长度 == len(ku_ids)
|
|
30
|
+
|
|
31
|
+
加载 8 步校验:任何一步不过 → _reset() 清空,让上层走兜底(全量重建)。
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
import hashlib
|
|
35
|
+
import logging
|
|
36
|
+
import os
|
|
37
|
+
import pickle
|
|
38
|
+
import sys
|
|
39
|
+
import threading
|
|
40
|
+
from dataclasses import dataclass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
import numpy as np
|
|
44
|
+
from .embedding_backends import EmbeddingBackend, make_backend
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── manifest 常量 ─────────────────────────────────────────────────────
|
|
50
|
+
# 升级 manifest 格式时务必 +1,旧 manifest 在 load() 校验会失败→ 触发 _reset() → 兜底全量重建
|
|
51
|
+
_MANIFEST_FORMAT_VERSION = 2
|
|
52
|
+
# _ku_text() 的语义哈希:任何对 name/summary/tags/statement 的拼接逻辑修改,都会改
|
|
53
|
+
# text 拼接结果 → 旧 hash 必不匹配 → 阶段 B update_ku 会按"内容变了"重 encode,不会出错。
|
|
54
|
+
# 但若你**同时**改了 text 拼接 + 升级了 manifest version,二者会叠加;推荐:只改其一。
|
|
55
|
+
_KU_TEXT_FORMAT_VERSION = 1
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _hash_text(text: str) -> str:
|
|
59
|
+
"""稳定的内容指纹:阶段 B 用它做 'update_ku 实际是否真改了内容' 检测。
|
|
60
|
+
SHA1 16 字符足够防误判(碰撞概率 ~1/2^64);hex 短,存盘小。
|
|
61
|
+
故意不用 blake2 —— _compact 的内存清洗会让 hash 形式不可读,SHA1 hex 短更易排查。"""
|
|
62
|
+
return hashlib.sha1(text.encode("utf-8")).hexdigest()[:16]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# 阶段 C:Vector 索引自动升级阈值。
|
|
66
|
+
# 当累积向量数 >= 这个阈值后,IndexFlatIP(暴力精确)自动升级到 IndexIVFFlat(聚类倒排,10x 检索加速)。
|
|
67
|
+
# 通过环境变量 HKC_VECTOR_IVF_THRESHOLD 可调(默认 10000;测试时设 200 即可触发升级)。
|
|
68
|
+
# 设 0 = 禁用升级,始终用 IndexFlatIP(用于回归测试 / 精度对比)。
|
|
69
|
+
HKC_VECTOR_IVF_THRESHOLD = int(os.environ.get("HKC_VECTOR_IVF_THRESHOLD", "10000"))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class VectorResult:
|
|
74
|
+
ku_id: str
|
|
75
|
+
score: float # 余弦相似度 0-1
|
|
76
|
+
name: str
|
|
77
|
+
summary: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class VectorIndex:
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
backend: EmbeddingBackend | None = None,
|
|
85
|
+
model_name: str | None = None, # 向后兼容旧调用
|
|
86
|
+
):
|
|
87
|
+
"""
|
|
88
|
+
backend: 显式传入 embedding 后端。
|
|
89
|
+
不传则用 make_backend("auto")(本地优先,回退 stub)。
|
|
90
|
+
model_name: 兼容旧接口,等价于 make_backend("local", model_name=...)。
|
|
91
|
+
"""
|
|
92
|
+
if backend is not None:
|
|
93
|
+
self._backend = backend
|
|
94
|
+
elif model_name is not None:
|
|
95
|
+
self._backend = make_backend("local", model_name=model_name)
|
|
96
|
+
else:
|
|
97
|
+
self._backend = make_backend("auto")
|
|
98
|
+
|
|
99
|
+
self._index = None
|
|
100
|
+
self._ku_ids: list[str] = []
|
|
101
|
+
self._names: list[str] = []
|
|
102
|
+
self._summaries: list[str] = []
|
|
103
|
+
# 阶段 A:每条 KU 对应一个文本 hash(由 _ku_text(ku) 计算),与 _ku_ids 一一对应。
|
|
104
|
+
# 阶段 B 的 update_ku 用它做"内容真伪变化"检测 —— 不变则跳过 encode,变才重编。
|
|
105
|
+
# save/load 都要带;load 缺失则视为旧 v1 manifest,触发 _reset() 兜底。
|
|
106
|
+
self._text_hashes: list[str] = []
|
|
107
|
+
# B1+B2 写-写互斥锁(RLock 支持 update_ku 内部嵌套调
|
|
108
|
+
# remove_ku/append_ku 不死锁)。append_bulk 内部 encode 在锁外,
|
|
109
|
+
# 仅 faiss.add + 4 list extend 持锁(微秒级)—— 避免和 _sync_ku
|
|
110
|
+
# 抢锁导致 ingest 阻塞。读(search)不持此锁,与写不互斥。
|
|
111
|
+
self._write_lock = threading.RLock()
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def backend_name(self) -> str:
|
|
115
|
+
return self._backend.name
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def dim(self) -> int:
|
|
119
|
+
return self._backend.dim
|
|
120
|
+
|
|
121
|
+
# ── 构建 ──────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
def build(self, kus: list) -> None:
|
|
124
|
+
if not kus:
|
|
125
|
+
return
|
|
126
|
+
texts = [self._ku_text(ku) for ku in kus]
|
|
127
|
+
embeddings = self._backend.encode(texts) # encode 锁外(慢)
|
|
128
|
+
with self._write_lock: # 锁内(微秒)
|
|
129
|
+
self._init_index()
|
|
130
|
+
self._index.add(embeddings)
|
|
131
|
+
self._maybe_upgrade_to_ivf() # 阶段 C:达阈值时 Flat → IVF
|
|
132
|
+
self._ku_ids = [ku.ku_id for ku in kus]
|
|
133
|
+
self._names = [ku.name for ku in kus]
|
|
134
|
+
self._summaries = [ku.summary[:100] for ku in kus]
|
|
135
|
+
self._text_hashes = [_hash_text(t) for t in texts]
|
|
136
|
+
logger.info("向量索引构建完成,共 %d 条 (backend=%s)",
|
|
137
|
+
len(self._ku_ids), self._backend.name)
|
|
138
|
+
|
|
139
|
+
def append_ku(self, ku) -> None:
|
|
140
|
+
# 先 encode(触发后端维度校正),再用校正后的 dim 初始化索引
|
|
141
|
+
text = self._ku_text(ku)
|
|
142
|
+
vec = self._backend.encode([text]) # encode 锁外(慢)
|
|
143
|
+
with self._write_lock: # 锁内(微秒)
|
|
144
|
+
if self._index is None:
|
|
145
|
+
self._init_index()
|
|
146
|
+
self._index.add(vec)
|
|
147
|
+
self._maybe_upgrade_to_ivf() # 阶段 C:达阈值时 Flat → IVF
|
|
148
|
+
self._ku_ids.append(ku.ku_id)
|
|
149
|
+
self._names.append(ku.name)
|
|
150
|
+
self._summaries.append(ku.summary[:100])
|
|
151
|
+
self._text_hashes.append(_hash_text(text))
|
|
152
|
+
|
|
153
|
+
def append_bulk(self, kus: list) -> None:
|
|
154
|
+
"""批量追加 KU(一次 encode 调用,比循环 append_ku 快 ~10x)。
|
|
155
|
+
用于启动时把磁盘索引中缺失的 KU 增量补齐:N=几百到几千时秒级返回。
|
|
156
|
+
B1+B2:encode 在锁外(慢,几十~几百 ms/batch),仅 faiss.add + 4 list
|
|
157
|
+
extend 持 _write_lock(微秒级)—— 避免和 _sync_ku 抢锁导致 ingest
|
|
158
|
+
阻塞。RLock 允许同线程嵌套(update_ku 内部也调)。"""
|
|
159
|
+
if not kus:
|
|
160
|
+
return
|
|
161
|
+
texts = [self._ku_text(ku) for ku in kus]
|
|
162
|
+
embeddings = self._backend.encode(texts) # 锁外(慢)
|
|
163
|
+
with self._write_lock: # 锁内(微秒)
|
|
164
|
+
if self._index is None:
|
|
165
|
+
self._init_index()
|
|
166
|
+
self._index.add(embeddings)
|
|
167
|
+
self._maybe_upgrade_to_ivf() # 阶段 C:达阈值时 Flat → IVF
|
|
168
|
+
self._ku_ids .extend([ku.ku_id for ku in kus])
|
|
169
|
+
self._names .extend([ku.name for ku in kus])
|
|
170
|
+
self._summaries .extend([ku.summary[:100] for ku in kus])
|
|
171
|
+
self._text_hashes.extend([_hash_text(t) for t in texts])
|
|
172
|
+
|
|
173
|
+
def update_ku(self, ku) -> bool:
|
|
174
|
+
"""更新某 KU 的向量。返回是否真发生编码(同 hash 跳过)。
|
|
175
|
+
阶段 B:由 hybrid.update_ku 在 _lock 内调用,经 text_hash 检测:
|
|
176
|
+
- hash 未变 → 跳过 encode(节省 1 次大模型调用,大库下显著)
|
|
177
|
+
- hash 变 → remove 旧条 + append 新条
|
|
178
|
+
B1+B2:整体持 _write_lock(RLock 支持内部 remove_ku/append_ku
|
|
179
|
+
嵌套 acquire 不死锁);encode 在 append_ku 内已锁外。
|
|
180
|
+
注:IndexFlatIP add 总在末尾 push(id 自增),故 index == 在 5 个 list 里的位置。
|
|
181
|
+
"""
|
|
182
|
+
if self._index is None:
|
|
183
|
+
return False
|
|
184
|
+
if ku.ku_id not in self._ku_ids:
|
|
185
|
+
return False
|
|
186
|
+
text = self._ku_text(ku)
|
|
187
|
+
new_hash = _hash_text(text)
|
|
188
|
+
idx = self._ku_ids.index(ku.ku_id)
|
|
189
|
+
if self._text_hashes[idx] == new_hash:
|
|
190
|
+
return False # 同 hash → 跳过 encode
|
|
191
|
+
with self._write_lock:
|
|
192
|
+
self.remove_ku(ku.ku_id)
|
|
193
|
+
self.append_ku(ku)
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
def remove_ku(self, ku_id: str) -> bool:
|
|
197
|
+
"""从 faiss 删一条 + 同步 5 个 list。返回是否真删了。
|
|
198
|
+
阶段 B:用于 KEE 状态机变更(把 superseded/disputed 等从检索剔除)、
|
|
199
|
+
delete_source 级联删、manifest 校验失败后清空旧数据。
|
|
200
|
+
IndexFlatIP.add 永远 push 末尾,id 与 list 位置一一对应。
|
|
201
|
+
B1+B2:整体持 _write_lock(RLock 支持嵌套)。
|
|
202
|
+
|
|
203
|
+
faiss 1.13 注:Index.remove_ids(selector) 是 **in-place** 操作,直接修改
|
|
204
|
+
内部存储并返 int(剩余数量),**不**返新 Index。若用 `new_idx = idx.remove_ids()`
|
|
205
|
+
会让 new_idx 变成 int 而非 Index,后续操作全崩。**这是测试 E3/E4 早期失败
|
|
206
|
+
的根因**。
|
|
207
|
+
"""
|
|
208
|
+
if self._index is None:
|
|
209
|
+
return False
|
|
210
|
+
if ku_id not in self._ku_ids:
|
|
211
|
+
return False
|
|
212
|
+
with self._write_lock:
|
|
213
|
+
import faiss
|
|
214
|
+
idx = self._ku_ids.index(ku_id)
|
|
215
|
+
sel = faiss.IDSelectorBatch(np.array([idx], dtype=np.int64))
|
|
216
|
+
self._index.remove_ids(sel)
|
|
217
|
+
# 同步 4 个 list。faiss 内部已经把后续 id 重映射(连续 0..n-1),
|
|
218
|
+
# 我们的 _ku_ids/_names/... 是按位置存的,删 idx 后 4 list 同步剔 idx 位置。
|
|
219
|
+
self._ku_ids = self._ku_ids[:idx] + self._ku_ids[idx+1:]
|
|
220
|
+
self._names = self._names[:idx] + self._names[idx+1:]
|
|
221
|
+
self._summaries = self._summaries[:idx] + self._summaries[idx+1:]
|
|
222
|
+
self._text_hashes= self._text_hashes[:idx]+ self._text_hashes[idx+1:]
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
# ── 检索 ──────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
def search(self, query: str, top_k: int = 10) -> list[VectorResult]:
|
|
228
|
+
if self._index is None or self._index.ntotal == 0:
|
|
229
|
+
return []
|
|
230
|
+
vec = self._backend.encode([query])
|
|
231
|
+
k = min(top_k, self._index.ntotal)
|
|
232
|
+
distances, indices = self._index.search(vec, k)
|
|
233
|
+
|
|
234
|
+
results = []
|
|
235
|
+
for dist, idx in zip(distances[0], indices[0]):
|
|
236
|
+
if idx < 0 or idx >= len(self._ku_ids):
|
|
237
|
+
continue
|
|
238
|
+
score = float(dist)
|
|
239
|
+
if score < 0.1:
|
|
240
|
+
continue
|
|
241
|
+
results.append(VectorResult(
|
|
242
|
+
ku_id = self._ku_ids[idx],
|
|
243
|
+
score = score,
|
|
244
|
+
name = self._names[idx],
|
|
245
|
+
summary = self._summaries[idx],
|
|
246
|
+
))
|
|
247
|
+
return results
|
|
248
|
+
|
|
249
|
+
def size(self) -> int:
|
|
250
|
+
return self._index.ntotal if self._index else 0
|
|
251
|
+
|
|
252
|
+
# ── 持久化 ────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
def save(self, path: str) -> None:
|
|
255
|
+
import faiss
|
|
256
|
+
if self._index is None:
|
|
257
|
+
logger.warning("索引为空,跳过保存")
|
|
258
|
+
return
|
|
259
|
+
faiss.write_index(self._index, path + ".faiss")
|
|
260
|
+
# 阶段 A:manifest 升 v2,加 format_version/dim/text_hashes。
|
|
261
|
+
# 4 个 list 长度必须一致,save 前用 _check_invariant 校验,失败抛 AssertionError
|
|
262
|
+
# —— 不让不一致的 manifest 落盘(下次加载必 8 步校验失败→ _reset 兜底,徒增重建)。
|
|
263
|
+
self._check_invariant()
|
|
264
|
+
with open(path + ".meta", "wb") as f:
|
|
265
|
+
pickle.dump({
|
|
266
|
+
"format_version": _MANIFEST_FORMAT_VERSION,
|
|
267
|
+
"ku_text_version": _KU_TEXT_FORMAT_VERSION,
|
|
268
|
+
"backend": self._backend.name,
|
|
269
|
+
"dim": self._backend.dim,
|
|
270
|
+
"ku_ids": self._ku_ids,
|
|
271
|
+
"names": self._names,
|
|
272
|
+
"summaries": self._summaries,
|
|
273
|
+
"text_hashes": self._text_hashes,
|
|
274
|
+
}, f)
|
|
275
|
+
logger.info("向量索引已保存: %s (v%d, %d 条, dim=%d)",
|
|
276
|
+
path, _MANIFEST_FORMAT_VERSION,
|
|
277
|
+
len(self._ku_ids), self._backend.dim)
|
|
278
|
+
|
|
279
|
+
def load(self, path: str) -> bool:
|
|
280
|
+
"""加载磁盘索引。返回 True 表示成功,False 表示文件不存在 / 校验失败(_reset 已清空)。"""
|
|
281
|
+
import faiss
|
|
282
|
+
faiss_path = path + ".faiss"
|
|
283
|
+
meta_path = path + ".meta"
|
|
284
|
+
if not (os.path.exists(faiss_path) and os.path.exists(meta_path)):
|
|
285
|
+
return False
|
|
286
|
+
try:
|
|
287
|
+
with open(meta_path, "rb") as f:
|
|
288
|
+
meta = pickle.load(f)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.warning("meta 损坏无法加载,触发 _reset 兜底: %s", e)
|
|
291
|
+
self._reset()
|
|
292
|
+
return False
|
|
293
|
+
# 8 步校验:任一不过 → _reset() → 上层走兜底全量重建
|
|
294
|
+
try:
|
|
295
|
+
self._validate_manifest(meta, faiss_path)
|
|
296
|
+
except _ManifestValidationError as e:
|
|
297
|
+
logger.warning("manifest 校验失败[%s],触发 _reset 兜底: %s",
|
|
298
|
+
e.reason, e)
|
|
299
|
+
self._reset()
|
|
300
|
+
return False
|
|
301
|
+
# 全部通过:加载
|
|
302
|
+
self._index = faiss.read_index(faiss_path)
|
|
303
|
+
self._ku_ids = meta["ku_ids"]
|
|
304
|
+
self._names = meta["names"]
|
|
305
|
+
self._summaries = meta["summaries"]
|
|
306
|
+
self._text_hashes = meta["text_hashes"]
|
|
307
|
+
logger.info("向量索引已加载: %d 条 (v%d, backend=%s, dim=%d)",
|
|
308
|
+
self._index.ntotal, meta.get("format_version", 1),
|
|
309
|
+
meta.get("backend"), meta.get("dim"))
|
|
310
|
+
return True
|
|
311
|
+
|
|
312
|
+
def _validate_manifest(self, meta: dict, faiss_path: str) -> None:
|
|
313
|
+
"""阶段 A 8 步校验。任一不通过抛 _ManifestValidationError,load() 捕获后 _reset()。
|
|
314
|
+
|
|
315
|
+
8 步:
|
|
316
|
+
1. format_version 存在且 == _MANIFEST_FORMAT_VERSION(旧 v1 无此字段 → 不过)
|
|
317
|
+
2. ku_text_version 存在且 == _KU_TEXT_FORMAT_VERSION(_ku_text 拼接改了就过不了)
|
|
318
|
+
3. backend 字段存在(str)
|
|
319
|
+
4. dim 字段存在(int)
|
|
320
|
+
5. len(ku_ids) == len(names) == len(summaries) == len(text_hashes)
|
|
321
|
+
6. 索引文件 faiss 存在(已在 caller 校验)
|
|
322
|
+
7. faiss 索引的 d == meta["dim"]
|
|
323
|
+
8. faiss 索引 ntotal == len(meta["ku_ids"])
|
|
324
|
+
"""
|
|
325
|
+
if not isinstance(meta.get("format_version"), int):
|
|
326
|
+
raise _ManifestValidationError("missing_format_version",
|
|
327
|
+
"format_version 字段缺失或非 int(旧 v1 manifest)")
|
|
328
|
+
if meta["format_version"] != _MANIFEST_FORMAT_VERSION:
|
|
329
|
+
raise _ManifestValidationError(
|
|
330
|
+
"format_version_mismatch",
|
|
331
|
+
f"format_version={meta['format_version']} 但当前期望 {_MANIFEST_FORMAT_VERSION}",
|
|
332
|
+
)
|
|
333
|
+
if meta.get("ku_text_version") != _KU_TEXT_FORMAT_VERSION:
|
|
334
|
+
raise _ManifestValidationError(
|
|
335
|
+
"ku_text_version_mismatch",
|
|
336
|
+
f"ku_text_version={meta.get('ku_text_version')!r} 期望 {_KU_TEXT_FORMAT_VERSION}"
|
|
337
|
+
f"(_ku_text 拼接逻辑变了,旧 hash 无意义,必须全量重建)",
|
|
338
|
+
)
|
|
339
|
+
if not isinstance(meta.get("backend"), str):
|
|
340
|
+
raise _ManifestValidationError("missing_backend", "backend 字段缺失或非 str")
|
|
341
|
+
if not isinstance(meta.get("dim"), int) or meta["dim"] <= 0:
|
|
342
|
+
raise _ManifestValidationError("missing_dim", "dim 字段缺失或非法")
|
|
343
|
+
lens = (len(meta.get("ku_ids", [])),
|
|
344
|
+
len(meta.get("names", [])),
|
|
345
|
+
len(meta.get("summaries", [])),
|
|
346
|
+
len(meta.get("text_hashes", [])))
|
|
347
|
+
if len(set(lens)) != 1:
|
|
348
|
+
raise _ManifestValidationError(
|
|
349
|
+
"list_length_mismatch",
|
|
350
|
+
f"4 个 list 长度不一致: {lens}",
|
|
351
|
+
)
|
|
352
|
+
# 6/7/8 涉及 faiss 实际维度
|
|
353
|
+
import faiss
|
|
354
|
+
idx = faiss.read_index(faiss_path)
|
|
355
|
+
if idx.d != meta["dim"]:
|
|
356
|
+
raise _ManifestValidationError(
|
|
357
|
+
"faiss_dim_mismatch",
|
|
358
|
+
f"faiss d={idx.d} 但 meta dim={meta['dim']}",
|
|
359
|
+
)
|
|
360
|
+
if idx.ntotal != lens[0]:
|
|
361
|
+
raise _ManifestValidationError(
|
|
362
|
+
"faiss_ntotal_mismatch",
|
|
363
|
+
f"faiss ntotal={idx.ntotal} 但 meta ku_ids 数={lens[0]}",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def _reset(self) -> None:
|
|
367
|
+
"""加载失败兜底:清空所有 in-memory 状态,让上层走全量重建。
|
|
368
|
+
不删磁盘文件 —— 排查时常常需要看「旧 manifest 究竟哪里坏了」;上层兜底重建后
|
|
369
|
+
会写新 manifest 把旧 .meta 覆盖。"""
|
|
370
|
+
self._index = None
|
|
371
|
+
self._ku_ids = []
|
|
372
|
+
self._names = []
|
|
373
|
+
self._summaries = []
|
|
374
|
+
self._text_hashes = []
|
|
375
|
+
logger.warning("VectorIndex _reset: in-memory 索引已清空,等待上层兜底重建")
|
|
376
|
+
|
|
377
|
+
def _check_invariant(self) -> None:
|
|
378
|
+
"""save 前的内部一致性:5 个 list 长度 + dim 已初始化。"""
|
|
379
|
+
if self._index is None:
|
|
380
|
+
raise AssertionError("save() 时 _index 还没初始化")
|
|
381
|
+
n = self._index.ntotal
|
|
382
|
+
for name, lst in (("ku_ids", self._ku_ids),
|
|
383
|
+
("names", self._names),
|
|
384
|
+
("summaries", self._summaries),
|
|
385
|
+
("text_hashes", self._text_hashes)):
|
|
386
|
+
if len(lst) != n:
|
|
387
|
+
raise AssertionError(
|
|
388
|
+
f"save() invariant 失败:{name} len={len(lst)} 但 faiss ntotal={n}"
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# ── 内部方法 ─────────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
def _init_index(self) -> None:
|
|
394
|
+
try:
|
|
395
|
+
import faiss
|
|
396
|
+
self._index = faiss.IndexFlatIP(self._backend.dim)
|
|
397
|
+
except ImportError:
|
|
398
|
+
raise ImportError("faiss-cpu 未安装: pip install faiss-cpu")
|
|
399
|
+
|
|
400
|
+
def _maybe_upgrade_to_ivf(self) -> None:
|
|
401
|
+
"""累积向量数达阈值时,IndexFlatIP → IndexIVFFlat 自动升级。
|
|
402
|
+
|
|
403
|
+
设计:
|
|
404
|
+
- < HKC_VECTOR_IVF_THRESHOLD: 保持 IndexFlatIP(精确,无训练开销)
|
|
405
|
+
- >= HKC_VECTOR_IVF_THRESHOLD: 升级(10x 检索加速)
|
|
406
|
+
|
|
407
|
+
升级流程(在调用方的 _write_lock 内,持锁 1-3s):
|
|
408
|
+
1. reconstruct 所有当前向量(0..n-1)
|
|
409
|
+
2. nlist = max(64, int(sqrt(n)))
|
|
410
|
+
3. IndexIVFFlat(quantizer=Flat, dim, nlist, METRIC_IP)
|
|
411
|
+
4. train + add + nprobe=16(精度保留)
|
|
412
|
+
5. 替换 self._index
|
|
413
|
+
|
|
414
|
+
一次性:升级后 isinstance 检查直接 return,后续 append_ku 直接 add 到 IVF。
|
|
415
|
+
save/load 透明:faiss.write_index/read_index 自动处理 IVF,manifest v2 不变。
|
|
416
|
+
"""
|
|
417
|
+
import faiss
|
|
418
|
+
import numpy as np
|
|
419
|
+
if HKC_VECTOR_IVF_THRESHOLD <= 0:
|
|
420
|
+
return
|
|
421
|
+
# faiss.read_index() 反序列化会把 IndexFlatIP 降为基类 IndexFlat
|
|
422
|
+
# (丢失 Python 子类信息),用 isinstance 永远 False → 走 type name 判断
|
|
423
|
+
type_name = type(self._index).__name__
|
|
424
|
+
if "IVF" in type_name:
|
|
425
|
+
return # 已是 IVF
|
|
426
|
+
if type_name not in ("IndexFlat", "IndexFlatIP"):
|
|
427
|
+
return # 未知 Flat 变体,不动
|
|
428
|
+
n = self._index.ntotal
|
|
429
|
+
if n < HKC_VECTOR_IVF_THRESHOLD:
|
|
430
|
+
return
|
|
431
|
+
nlist = max(64, int(n ** 0.5))
|
|
432
|
+
# n ≥ HKC_VECTOR_IVF_THRESHOLD > 0 ⇒ n ≥ max(64, sqrt(n)) ⇒ n ≥ nlist
|
|
433
|
+
# (对 n≥1 必有 sqrt(n) ≤ n),因此 n >= nlist 在升级瞬间必然成立。
|
|
434
|
+
# faiss 1.13 train() 自己会校验 n >= nlist 并抛 RuntimeError,被外层 try/except 兜住,
|
|
435
|
+
# 不再单独 return(防御性分支只会让阈值恰好=1 且 n=1 这类边缘情况被静默忽略,
|
|
436
|
+
# 而那种情况本就该让 faiss 自己报错)。
|
|
437
|
+
try:
|
|
438
|
+
vectors = np.vstack(
|
|
439
|
+
[self._index.reconstruct(i) for i in range(n)]
|
|
440
|
+
).astype(np.float32)
|
|
441
|
+
# L2 归一化兜底:对已归一化向量(bge-m3 等)幂等;对未归一化
|
|
442
|
+
# backend 防止 IVF(IP 距离)与原 IndexFlatIP 行为不一致导致
|
|
443
|
+
# 搜索结果"看起来变差"。0 向量(L2 norm=0)用一个常量 norm 替代,
|
|
444
|
+
# 避免除零后所有分量变成 nan。
|
|
445
|
+
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
|
|
446
|
+
norms = np.where(norms > 0, norms, 1.0)
|
|
447
|
+
vectors = (vectors / norms).astype(np.float32)
|
|
448
|
+
quantizer = faiss.IndexFlatIP(self._backend.dim)
|
|
449
|
+
new_index = faiss.IndexIVFFlat(
|
|
450
|
+
quantizer, self._backend.dim, nlist, faiss.METRIC_INNER_PRODUCT)
|
|
451
|
+
new_index.train(vectors)
|
|
452
|
+
new_index.add(vectors)
|
|
453
|
+
new_index.nprobe = 16 # 搜 16 个聚类,精度保留(~99%)
|
|
454
|
+
self._index = new_index
|
|
455
|
+
msg = (f"VectorIndex 升级: Flat → IVF (n={n}, nlist={nlist}, "
|
|
456
|
+
f"nprobe=16, dim={self._backend.dim})")
|
|
457
|
+
logger.info(msg)
|
|
458
|
+
# logger 通过 root handler 输出,uvicorn 自带 logging 配置
|
|
459
|
+
# 不一定把 hkc_search 接到 stderr —— 加 stderr 直接 print 兜底,
|
|
460
|
+
# 下次阈值正常触发时(ivf_status 显示进度 100%),人工能直接看到。
|
|
461
|
+
print(msg, file=sys.stderr, flush=True)
|
|
462
|
+
except Exception as e:
|
|
463
|
+
warn_msg = f"VectorIndex 升级失败,继续用 Flat: {e}"
|
|
464
|
+
logger.warning(warn_msg)
|
|
465
|
+
print(warn_msg, file=sys.stderr, flush=True)
|
|
466
|
+
|
|
467
|
+
@staticmethod
|
|
468
|
+
def _ku_text(ku) -> str:
|
|
469
|
+
parts = [ku.name]
|
|
470
|
+
if ku.summary:
|
|
471
|
+
parts.append(ku.summary)
|
|
472
|
+
if ku.tags:
|
|
473
|
+
parts.append(" ".join(ku.tags))
|
|
474
|
+
stmt = getattr(ku, "statement", "")
|
|
475
|
+
if stmt:
|
|
476
|
+
parts.append(stmt)
|
|
477
|
+
return " ".join(parts)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
class _ManifestValidationError(Exception):
|
|
481
|
+
"""load() 8 步校验失败的私有异常类。reason 是给 logger 用的机器可读码。"""
|
|
482
|
+
def __init__(self, reason: str, msg: str):
|
|
483
|
+
super().__init__(msg)
|
|
484
|
+
self.reason = reason
|
|
File without changes
|
|
File without changes
|