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,789 @@
|
|
|
1
|
+
"""
|
|
2
|
+
hkc-core / graph / sqlite_store.py
|
|
3
|
+
SQLite + NetworkX 实现的 GraphStore。
|
|
4
|
+
|
|
5
|
+
SQLite → 持久化存储,全量数据
|
|
6
|
+
NetworkX → 内存图,启动时从 SQLite 重建,用于路径查询
|
|
7
|
+
"""
|
|
8
|
+
import json
|
|
9
|
+
import sqlite3
|
|
10
|
+
import threading
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
import networkx as nx
|
|
16
|
+
|
|
17
|
+
from .base import GraphStore
|
|
18
|
+
from ..models.enums import KUType, ClaimStatus
|
|
19
|
+
from ..models.ku import (
|
|
20
|
+
KU, Relation, ConflictCard,
|
|
21
|
+
EntityKU, ConceptKU, FactKU, ClaimKU,
|
|
22
|
+
EvidenceKU, AbilityKU,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── 序列化 / 反序列化 ────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
def _loads_dict(raw) -> dict:
|
|
29
|
+
"""extra 列容错解析:坏 JSON / 非 dict 一律当空字典,不抛异常打断写入。"""
|
|
30
|
+
try:
|
|
31
|
+
val = json.loads(raw) if raw else {}
|
|
32
|
+
except Exception:
|
|
33
|
+
return {}
|
|
34
|
+
return val if isinstance(val, dict) else {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _dump(value):
|
|
38
|
+
"""JSON 列(list/dict)写库前序列化,标量原样返回。"""
|
|
39
|
+
return json.dumps(value, ensure_ascii=False) if isinstance(value, (list, dict)) else value
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _ku_to_row(ku: KU) -> tuple:
|
|
43
|
+
d = ku.to_dict()
|
|
44
|
+
# Column order: ku_id, name, ku_type, summary, confidence, status,
|
|
45
|
+
# domain, version, tags, sources, relations, extra, created_at, updated_at
|
|
46
|
+
return (
|
|
47
|
+
d["ku_id"],
|
|
48
|
+
d["name"],
|
|
49
|
+
d["ku_type"],
|
|
50
|
+
d["summary"],
|
|
51
|
+
d["confidence"],
|
|
52
|
+
d["status"],
|
|
53
|
+
d["domain"],
|
|
54
|
+
d["version"],
|
|
55
|
+
json.dumps(d["tags"], ensure_ascii=False),
|
|
56
|
+
json.dumps(d["sources"], ensure_ascii=False),
|
|
57
|
+
json.dumps(d["relations"], ensure_ascii=False),
|
|
58
|
+
json.dumps(d["extra"], ensure_ascii=False),
|
|
59
|
+
d["created_at"],
|
|
60
|
+
d["updated_at"],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _row_to_ku(row: tuple) -> KU:
|
|
65
|
+
# Schema CREATE TABLE order:
|
|
66
|
+
# ku_id, ku_type, name, summary, confidence, status,
|
|
67
|
+
# domain, version, tags, sources, relations, extra, created_at, updated_at
|
|
68
|
+
(ku_id, ku_type, name, summary, confidence, status,
|
|
69
|
+
domain, version, tags, sources, relations, extra,
|
|
70
|
+
created_at, updated_at) = row
|
|
71
|
+
|
|
72
|
+
tags = json.loads(tags)
|
|
73
|
+
sources = json.loads(sources)
|
|
74
|
+
relations = json.loads(relations)
|
|
75
|
+
extra = json.loads(extra)
|
|
76
|
+
|
|
77
|
+
base = dict(
|
|
78
|
+
ku_id=ku_id, name=name, summary=summary,
|
|
79
|
+
confidence=confidence, status=status, domain=domain,
|
|
80
|
+
version=version, tags=tags, sources=sources,
|
|
81
|
+
relations=relations, extra=extra,
|
|
82
|
+
created_at=created_at, updated_at=updated_at,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
t = ku_type
|
|
86
|
+
if t == KUType.ENTITY.value:
|
|
87
|
+
from ..models.enums import EntityType
|
|
88
|
+
ku = EntityKU(**base)
|
|
89
|
+
raw_etype = extra.get("entity_type", "Person")
|
|
90
|
+
try:
|
|
91
|
+
ku.entity_type = EntityType(raw_etype)
|
|
92
|
+
except ValueError:
|
|
93
|
+
ku.entity_type = EntityType.PERSON
|
|
94
|
+
ku.aliases = extra.get("aliases", [])
|
|
95
|
+
ku.birth = extra.get("birth", "")
|
|
96
|
+
ku.active = extra.get("active", True)
|
|
97
|
+
ku.source_text = extra.get("source_text", "")
|
|
98
|
+
elif t == KUType.CONCEPT.value:
|
|
99
|
+
ku = ConceptKU(**base)
|
|
100
|
+
ku.aka = extra.get("aka", [])
|
|
101
|
+
ku.definition = extra.get("definition", "")
|
|
102
|
+
ku.source_text = extra.get("source_text", "")
|
|
103
|
+
elif t == KUType.FACT.value:
|
|
104
|
+
ku = FactKU(**base)
|
|
105
|
+
ku.statement = extra.get("statement", "")
|
|
106
|
+
ku.verifiable = extra.get("verifiable", True)
|
|
107
|
+
ku.source_ref = extra.get("source_ref", "")
|
|
108
|
+
ku.source_text = extra.get("source_text", "")
|
|
109
|
+
elif t == KUType.CLAIM.value:
|
|
110
|
+
ku = ClaimKU(**base)
|
|
111
|
+
ku.statement = extra.get("statement", "")
|
|
112
|
+
ku.claim_status = ClaimStatus(extra.get("claim_status", "pending"))
|
|
113
|
+
ku.supports = extra.get("supports", [])
|
|
114
|
+
ku.contradicts = extra.get("contradicts", [])
|
|
115
|
+
ku.conflict_refs = extra.get("conflict_refs", [])
|
|
116
|
+
ku.source_text = extra.get("source_text", "")
|
|
117
|
+
elif t == KUType.EVIDENCE.value:
|
|
118
|
+
from ..models.enums import SourceType
|
|
119
|
+
ku = EvidenceKU(**base)
|
|
120
|
+
ku.source = extra.get("source", "")
|
|
121
|
+
try:
|
|
122
|
+
ku.source_type = SourceType(extra.get("source_type", "Book"))
|
|
123
|
+
except ValueError:
|
|
124
|
+
ku.source_type = SourceType.BOOK
|
|
125
|
+
ku.author = extra.get("author", "")
|
|
126
|
+
ku.year = extra.get("year", 0)
|
|
127
|
+
ku.page = extra.get("page", 0)
|
|
128
|
+
ku.quote = extra.get("quote", "")
|
|
129
|
+
ku.supports = extra.get("supports", [])
|
|
130
|
+
ku.contradicts = extra.get("contradicts", [])
|
|
131
|
+
elif t == KUType.ABILITY.value:
|
|
132
|
+
ku = AbilityKU(**base)
|
|
133
|
+
ku.ability_key = extra.get("ability_key", "")
|
|
134
|
+
ku.skills = extra.get("skills", [])
|
|
135
|
+
ku.workflows = extra.get("workflows", [])
|
|
136
|
+
ku.coverage = extra.get("coverage", {})
|
|
137
|
+
ku.knowledge_refs = extra.get("knowledge_refs", [])
|
|
138
|
+
ku.package_path = extra.get("package_path", "")
|
|
139
|
+
ku.pkg_version = extra.get("pkg_version", "1.0.0")
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError(f"Unknown ku_type: {t}")
|
|
142
|
+
|
|
143
|
+
return ku
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _rel_to_row(rel: Relation) -> tuple:
|
|
147
|
+
return (
|
|
148
|
+
rel.rel_id, rel.from_ku, rel.to_ku,
|
|
149
|
+
rel.rel_type, rel.weight, rel.source, rel.created_at,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _row_to_rel(row: tuple) -> Relation:
|
|
154
|
+
rel_id, from_ku, to_ku, rel_type, weight, source, created_at = row
|
|
155
|
+
return Relation(
|
|
156
|
+
rel_id=rel_id, from_ku=from_ku, to_ku=to_ku,
|
|
157
|
+
rel_type=rel_type, weight=weight,
|
|
158
|
+
source=source or "", created_at=created_at,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _card_to_row(card: ConflictCard) -> tuple:
|
|
163
|
+
return (
|
|
164
|
+
card.conflict_id, card.claim_a_id, card.claim_b_id,
|
|
165
|
+
card.domain, card.status.value,
|
|
166
|
+
card.resolution_strategy.value,
|
|
167
|
+
card.resolution_note, card.resolved_by,
|
|
168
|
+
card.resolved_at, card.created_at,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _row_to_card(row: tuple) -> ConflictCard:
|
|
173
|
+
from ..models.enums import ConflictStatus, ResolutionStrategy
|
|
174
|
+
(conflict_id, claim_a_id, claim_b_id, domain, status,
|
|
175
|
+
strategy, note, resolved_by, resolved_at, created_at) = row
|
|
176
|
+
return ConflictCard(
|
|
177
|
+
conflict_id=conflict_id,
|
|
178
|
+
claim_a_id=claim_a_id,
|
|
179
|
+
claim_b_id=claim_b_id,
|
|
180
|
+
domain=domain or "",
|
|
181
|
+
status=ConflictStatus(status),
|
|
182
|
+
resolution_strategy=ResolutionStrategy(strategy),
|
|
183
|
+
resolution_note=note or "",
|
|
184
|
+
resolved_by=resolved_by,
|
|
185
|
+
resolved_at=resolved_at,
|
|
186
|
+
created_at=created_at,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── SQLiteGraphStore ─────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
class SQLiteGraphStore(GraphStore):
|
|
193
|
+
"""
|
|
194
|
+
v1 GraphStore 实现。
|
|
195
|
+
- SQLite 负责持久化
|
|
196
|
+
- NetworkX DiGraph 负责内存图查询(路径、邻居)
|
|
197
|
+
- 所有写操作同时更新两者,保持一致
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
def __init__(self, db_path: str):
|
|
201
|
+
self._lock = threading.RLock()
|
|
202
|
+
db_file = Path(db_path)
|
|
203
|
+
db_file.parent.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
self._db = sqlite3.connect(str(db_file), check_same_thread=False)
|
|
205
|
+
self._db.row_factory = sqlite3.Row
|
|
206
|
+
self._graph: nx.DiGraph = nx.DiGraph()
|
|
207
|
+
self._init_schema()
|
|
208
|
+
self._load_graph()
|
|
209
|
+
|
|
210
|
+
# ── Schema ────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
def _init_schema(self):
|
|
213
|
+
# Set pragmas explicitly (executescript auto-commits, pragma must be separate)
|
|
214
|
+
self._db.execute("PRAGMA journal_mode=WAL")
|
|
215
|
+
self._db.commit()
|
|
216
|
+
self._db.executescript("""
|
|
217
|
+
PRAGMA foreign_keys=ON;
|
|
218
|
+
|
|
219
|
+
CREATE TABLE IF NOT EXISTS knowledge_units (
|
|
220
|
+
ku_id TEXT PRIMARY KEY,
|
|
221
|
+
ku_type TEXT NOT NULL,
|
|
222
|
+
name TEXT NOT NULL,
|
|
223
|
+
summary TEXT DEFAULT '',
|
|
224
|
+
confidence REAL DEFAULT 1.0,
|
|
225
|
+
status TEXT DEFAULT 'active',
|
|
226
|
+
domain TEXT DEFAULT '',
|
|
227
|
+
version INTEGER DEFAULT 1,
|
|
228
|
+
tags TEXT DEFAULT '[]',
|
|
229
|
+
sources TEXT DEFAULT '[]',
|
|
230
|
+
relations TEXT DEFAULT '[]',
|
|
231
|
+
extra TEXT DEFAULT '{}',
|
|
232
|
+
created_at TEXT,
|
|
233
|
+
updated_at TEXT
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
CREATE INDEX IF NOT EXISTS idx_ku_type ON knowledge_units(ku_type);
|
|
237
|
+
CREATE INDEX IF NOT EXISTS idx_ku_domain ON knowledge_units(domain);
|
|
238
|
+
CREATE INDEX IF NOT EXISTS idx_ku_status ON knowledge_units(status);
|
|
239
|
+
|
|
240
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS ku_fts
|
|
241
|
+
USING fts5(ku_id UNINDEXED, name, summary, domain);
|
|
242
|
+
|
|
243
|
+
CREATE TABLE IF NOT EXISTS relations (
|
|
244
|
+
rel_id TEXT PRIMARY KEY,
|
|
245
|
+
from_ku TEXT NOT NULL,
|
|
246
|
+
to_ku TEXT NOT NULL,
|
|
247
|
+
rel_type TEXT NOT NULL,
|
|
248
|
+
weight REAL DEFAULT 1.0,
|
|
249
|
+
source TEXT DEFAULT '',
|
|
250
|
+
created_at TEXT,
|
|
251
|
+
FOREIGN KEY(from_ku) REFERENCES knowledge_units(ku_id),
|
|
252
|
+
FOREIGN KEY(to_ku) REFERENCES knowledge_units(ku_id)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_rel_from ON relations(from_ku);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_rel_to ON relations(to_ku);
|
|
257
|
+
CREATE INDEX IF NOT EXISTS idx_rel_type ON relations(rel_type);
|
|
258
|
+
|
|
259
|
+
CREATE TABLE IF NOT EXISTS conflict_cards (
|
|
260
|
+
conflict_id TEXT PRIMARY KEY,
|
|
261
|
+
claim_a_id TEXT NOT NULL,
|
|
262
|
+
claim_b_id TEXT NOT NULL,
|
|
263
|
+
domain TEXT DEFAULT '',
|
|
264
|
+
status TEXT DEFAULT 'open',
|
|
265
|
+
strategy TEXT DEFAULT 'evidence_weight',
|
|
266
|
+
note TEXT DEFAULT '',
|
|
267
|
+
resolved_by TEXT,
|
|
268
|
+
resolved_at TEXT,
|
|
269
|
+
created_at TEXT,
|
|
270
|
+
FOREIGN KEY(claim_a_id) REFERENCES knowledge_units(ku_id),
|
|
271
|
+
FOREIGN KEY(claim_b_id) REFERENCES knowledge_units(ku_id)
|
|
272
|
+
);
|
|
273
|
+
""")
|
|
274
|
+
self._db.commit()
|
|
275
|
+
|
|
276
|
+
def _load_graph(self):
|
|
277
|
+
"""启动时从 SQLite 重建 NetworkX 图。"""
|
|
278
|
+
rows = self._db.execute(
|
|
279
|
+
"SELECT from_ku, to_ku, rel_type, weight FROM relations"
|
|
280
|
+
).fetchall()
|
|
281
|
+
for row in rows:
|
|
282
|
+
self._graph.add_edge(
|
|
283
|
+
row["from_ku"], row["to_ku"],
|
|
284
|
+
rel_type=row["rel_type"],
|
|
285
|
+
weight=row["weight"],
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# ── 单个 KU ──────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
# patch_ku 允许直接写的普通列(JSON 列会按 list/dict 自动序列化)。
|
|
291
|
+
# 刻意不含 ku_id / created_at / updated_at:前者是主键,后者由存储层维护。
|
|
292
|
+
_PATCH_COLUMNS = frozenset({
|
|
293
|
+
"name", "summary", "confidence", "status", "domain",
|
|
294
|
+
"version", "tags", "sources", "relations",
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
def get(self, ku_id: str) -> Optional[KU]:
|
|
298
|
+
with self._lock:
|
|
299
|
+
row = self._db.execute(
|
|
300
|
+
"SELECT * FROM knowledge_units WHERE ku_id = ?", (ku_id,)
|
|
301
|
+
).fetchone()
|
|
302
|
+
return _row_to_ku(tuple(row)) if row else None
|
|
303
|
+
|
|
304
|
+
def put(self, ku: KU) -> None:
|
|
305
|
+
with self._lock:
|
|
306
|
+
ku.updated_at = datetime.now(timezone.utc).isoformat()
|
|
307
|
+
self._db.execute("""
|
|
308
|
+
INSERT INTO knowledge_units
|
|
309
|
+
(ku_id,name,ku_type,summary,confidence,status,domain,
|
|
310
|
+
version,tags,sources,relations,extra,created_at,updated_at)
|
|
311
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
312
|
+
ON CONFLICT(ku_id) DO UPDATE SET
|
|
313
|
+
name=excluded.name, summary=excluded.summary,
|
|
314
|
+
confidence=excluded.confidence, status=excluded.status,
|
|
315
|
+
domain=excluded.domain, version=excluded.version,
|
|
316
|
+
tags=excluded.tags, sources=excluded.sources,
|
|
317
|
+
relations=excluded.relations, extra=excluded.extra,
|
|
318
|
+
updated_at=excluded.updated_at
|
|
319
|
+
""", _ku_to_row(ku))
|
|
320
|
+
self._db.commit()
|
|
321
|
+
# 同步 FTS:content table 不支持 OR REPLACE,用 DELETE + INSERT
|
|
322
|
+
self._db.execute("DELETE FROM ku_fts WHERE ku_id=?", (ku.ku_id,))
|
|
323
|
+
self._db.execute(
|
|
324
|
+
"INSERT INTO ku_fts(ku_id,name,summary,domain) VALUES (?,?,?,?)",
|
|
325
|
+
(ku.ku_id, ku.name, ku.summary, ku.domain)
|
|
326
|
+
)
|
|
327
|
+
self._db.commit()
|
|
328
|
+
|
|
329
|
+
def patch_ku(
|
|
330
|
+
self,
|
|
331
|
+
ku_id: str,
|
|
332
|
+
columns: Optional[dict] = None,
|
|
333
|
+
extra: Optional[dict] = None,
|
|
334
|
+
) -> int:
|
|
335
|
+
"""只写「指定的列」和「extra 里指定的键」,**其余列一个字节都不动**。
|
|
336
|
+
|
|
337
|
+
为什么需要:``put()`` 是整行 UPSERT。复习评分只改 ``extra["review"]``,走 put()
|
|
338
|
+
会把整行其它列按「写入前的快照」回写 —— KEE 在同一个窗口里刚合并进去的
|
|
339
|
+
sources / confidence / supports 会被悄悄冲掉;反过来,KEE 的整行写也会把复习
|
|
340
|
+
进度冲掉。``extra`` 是 review / supports / claim_status / source_text 共用的
|
|
341
|
+
JSON 列,所以「只改其中一两个键」的写入必须走这里,而不是 put()。
|
|
342
|
+
|
|
343
|
+
返回受影响行数:0 表示该 KU 不存在,调用方应回退到 ``put()``(那是插入语义)。
|
|
344
|
+
"""
|
|
345
|
+
cols = {k: v for k, v in (columns or {}).items() if k in self._PATCH_COLUMNS}
|
|
346
|
+
with self._lock:
|
|
347
|
+
if not cols and not extra:
|
|
348
|
+
return 1 if self._db.execute(
|
|
349
|
+
"SELECT 1 FROM knowledge_units WHERE ku_id=?", (ku_id,)
|
|
350
|
+
).fetchone() else 0
|
|
351
|
+
|
|
352
|
+
sets: list[str] = []
|
|
353
|
+
args: list = []
|
|
354
|
+
for k, v in cols.items():
|
|
355
|
+
sets.append(f"{k}=?")
|
|
356
|
+
args.append(_dump(v))
|
|
357
|
+
if extra:
|
|
358
|
+
row = self._db.execute(
|
|
359
|
+
"SELECT extra FROM knowledge_units WHERE ku_id=?", (ku_id,)
|
|
360
|
+
).fetchone()
|
|
361
|
+
if row is None:
|
|
362
|
+
return 0
|
|
363
|
+
cur = _loads_dict(row["extra"])
|
|
364
|
+
cur.update(extra)
|
|
365
|
+
sets.append("extra=?")
|
|
366
|
+
args.append(json.dumps(cur, ensure_ascii=False))
|
|
367
|
+
sets.append("updated_at=?")
|
|
368
|
+
args.append(datetime.now(timezone.utc).isoformat())
|
|
369
|
+
args.append(ku_id)
|
|
370
|
+
|
|
371
|
+
cur2 = self._db.execute(
|
|
372
|
+
f"UPDATE knowledge_units SET {', '.join(sets)} WHERE ku_id=?", args
|
|
373
|
+
)
|
|
374
|
+
self._db.commit()
|
|
375
|
+
n = cur2.rowcount
|
|
376
|
+
# 改了 FTS 索引覆盖的三列才需要同步,否则多一次写纯属浪费
|
|
377
|
+
if n and any(k in cols for k in ("name", "summary", "domain")):
|
|
378
|
+
row2 = self._db.execute(
|
|
379
|
+
"SELECT name, summary, domain FROM knowledge_units WHERE ku_id=?", (ku_id,)
|
|
380
|
+
).fetchone()
|
|
381
|
+
if row2 is not None:
|
|
382
|
+
self._db.execute("DELETE FROM ku_fts WHERE ku_id=?", (ku_id,))
|
|
383
|
+
self._db.execute(
|
|
384
|
+
"INSERT INTO ku_fts(ku_id,name,summary,domain) VALUES (?,?,?,?)",
|
|
385
|
+
(ku_id, row2["name"] or "", row2["summary"] or "", row2["domain"] or ""),
|
|
386
|
+
)
|
|
387
|
+
self._db.commit()
|
|
388
|
+
return n if n > 0 else 0
|
|
389
|
+
|
|
390
|
+
def update_extra(self, ku_id: str, fn) -> Optional[dict]:
|
|
391
|
+
"""在锁内「读 extra → 交给 fn 算 → 写回 extra」,整段原子。
|
|
392
|
+
|
|
393
|
+
给「读-改-写必须无竞态」的场景用(复习评分:读 review 状态 → SM-2 推演 →
|
|
394
|
+
写回)。``fn(extra)`` 返回新 dict 则落盘,返回 None 表示不改。
|
|
395
|
+
返回落盘后的 extra;None 表示该 KU 已不存在。
|
|
396
|
+
"""
|
|
397
|
+
with self._lock:
|
|
398
|
+
row = self._db.execute(
|
|
399
|
+
"SELECT extra FROM knowledge_units WHERE ku_id=?", (ku_id,)
|
|
400
|
+
).fetchone()
|
|
401
|
+
if row is None:
|
|
402
|
+
return None
|
|
403
|
+
extra = _loads_dict(row["extra"])
|
|
404
|
+
new_extra = fn(extra)
|
|
405
|
+
if new_extra is None:
|
|
406
|
+
return extra
|
|
407
|
+
if not isinstance(new_extra, dict):
|
|
408
|
+
raise TypeError("update_extra 的回调必须返回 dict 或 None")
|
|
409
|
+
self._db.execute(
|
|
410
|
+
"UPDATE knowledge_units SET extra=?, updated_at=? WHERE ku_id=?",
|
|
411
|
+
(json.dumps(new_extra, ensure_ascii=False),
|
|
412
|
+
datetime.now(timezone.utc).isoformat(), ku_id),
|
|
413
|
+
)
|
|
414
|
+
self._db.commit()
|
|
415
|
+
return new_extra
|
|
416
|
+
|
|
417
|
+
def delete(self, ku_id: str) -> bool:
|
|
418
|
+
"""软删除(将状态置为 deleted,保留行供审计)。不级联。
|
|
419
|
+
|
|
420
|
+
返回:
|
|
421
|
+
True — 此前该 id 存在且未被删,本次成功置为 deleted
|
|
422
|
+
False — id 不存在或已经是 deleted
|
|
423
|
+
|
|
424
|
+
阶段 B 设计:本方法**不**发事件,事件由调用方按业务语义发。
|
|
425
|
+
- KEE dedup 删重复 KU → 不发(重复 KU 从未进 vector,无外部观测需要)
|
|
426
|
+
- routes.delete_source 级联删 → 调用方负责 publish KNOWLEDGE_DELETED
|
|
427
|
+
原因:事件是「对知识库的外部观测」,不是「存储层副作用」。
|
|
428
|
+
若存储层自动发,会与 KEE / 路由层的事件总线污染(KEE 内部 delete 走 store
|
|
429
|
+
不应该被业务订阅者看到,否则会误触发 indexing 重新处理同一 ID)。
|
|
430
|
+
"""
|
|
431
|
+
with self._lock:
|
|
432
|
+
cur = self._db.execute(
|
|
433
|
+
"UPDATE knowledge_units SET status='deleted', "
|
|
434
|
+
"updated_at=? WHERE ku_id=? AND status != 'deleted'",
|
|
435
|
+
(datetime.now(timezone.utc).isoformat(), ku_id)
|
|
436
|
+
)
|
|
437
|
+
self._db.commit()
|
|
438
|
+
return cur.rowcount > 0
|
|
439
|
+
|
|
440
|
+
def clear(self) -> int:
|
|
441
|
+
"""清空整个知识库:硬删除所有 KU / 关系 / 冲突卡 / 全文索引 + 重置内存图。
|
|
442
|
+
返回清空前的 KU 行数。用于"清库重来"(不可逆)。"""
|
|
443
|
+
with self._lock:
|
|
444
|
+
try:
|
|
445
|
+
n = self._db.execute("SELECT COUNT(*) FROM knowledge_units").fetchone()[0]
|
|
446
|
+
except Exception:
|
|
447
|
+
n = 0
|
|
448
|
+
# 先删引用方(relations 外键指向 knowledge_units),knowledge_units 放最后,
|
|
449
|
+
# 否则先删父表会触发 FOREIGN KEY constraint failed。
|
|
450
|
+
for tbl in ("relations", "conflict_cards", "ku_fts", "knowledge_units"):
|
|
451
|
+
try:
|
|
452
|
+
self._db.execute(f"DELETE FROM {tbl}")
|
|
453
|
+
except Exception:
|
|
454
|
+
pass # 个别表(如 ku_fts)在旧库可能不存在
|
|
455
|
+
self._db.commit()
|
|
456
|
+
self._graph.clear()
|
|
457
|
+
return n
|
|
458
|
+
|
|
459
|
+
# ── 批量 ─────────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
def batch_write(self, kus: list[KU]) -> None:
|
|
462
|
+
with self._lock:
|
|
463
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
464
|
+
rows = []
|
|
465
|
+
for ku in kus:
|
|
466
|
+
ku.updated_at = now
|
|
467
|
+
rows.append(_ku_to_row(ku))
|
|
468
|
+
self._db.executemany("""
|
|
469
|
+
INSERT INTO knowledge_units
|
|
470
|
+
(ku_id,name,ku_type,summary,confidence,status,domain,
|
|
471
|
+
version,tags,sources,relations,extra,created_at,updated_at)
|
|
472
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
473
|
+
ON CONFLICT(ku_id) DO UPDATE SET
|
|
474
|
+
name=excluded.name, summary=excluded.summary,
|
|
475
|
+
confidence=excluded.confidence, status=excluded.status,
|
|
476
|
+
domain=excluded.domain, version=excluded.version,
|
|
477
|
+
tags=excluded.tags, sources=excluded.sources,
|
|
478
|
+
relations=excluded.relations, extra=excluded.extra,
|
|
479
|
+
updated_at=excluded.updated_at
|
|
480
|
+
""", rows)
|
|
481
|
+
self._db.commit()
|
|
482
|
+
# 同步 FTS
|
|
483
|
+
for ku in kus:
|
|
484
|
+
self._db.execute("DELETE FROM ku_fts WHERE ku_id=?", (ku.ku_id,))
|
|
485
|
+
self._db.execute(
|
|
486
|
+
"INSERT INTO ku_fts(ku_id,name,summary,domain) VALUES (?,?,?,?)",
|
|
487
|
+
(ku.ku_id, ku.name, ku.summary, ku.domain)
|
|
488
|
+
)
|
|
489
|
+
self._db.commit()
|
|
490
|
+
|
|
491
|
+
# ── 查询 ─────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
def query_by_type(
|
|
494
|
+
self,
|
|
495
|
+
ku_type: KUType,
|
|
496
|
+
domain: str = "",
|
|
497
|
+
status: str = "active",
|
|
498
|
+
limit: int = 100,
|
|
499
|
+
) -> list[KU]:
|
|
500
|
+
with self._lock:
|
|
501
|
+
if domain:
|
|
502
|
+
rows = self._db.execute(
|
|
503
|
+
"SELECT * FROM knowledge_units "
|
|
504
|
+
"WHERE ku_type=? AND domain=? AND status=? LIMIT ?",
|
|
505
|
+
(ku_type.value, domain, status, limit)
|
|
506
|
+
).fetchall()
|
|
507
|
+
else:
|
|
508
|
+
rows = self._db.execute(
|
|
509
|
+
"SELECT * FROM knowledge_units "
|
|
510
|
+
"WHERE ku_type=? AND status=? LIMIT ?",
|
|
511
|
+
(ku_type.value, status, limit)
|
|
512
|
+
).fetchall()
|
|
513
|
+
return [_row_to_ku(tuple(r)) for r in rows]
|
|
514
|
+
|
|
515
|
+
def query_by_domain(self, domain: str, limit: int = 200) -> list[KU]:
|
|
516
|
+
with self._lock:
|
|
517
|
+
rows = self._db.execute(
|
|
518
|
+
"SELECT * FROM knowledge_units "
|
|
519
|
+
"WHERE domain=? AND status!='deleted' LIMIT ?",
|
|
520
|
+
(domain, limit)
|
|
521
|
+
).fetchall()
|
|
522
|
+
return [_row_to_ku(tuple(r)) for r in rows]
|
|
523
|
+
|
|
524
|
+
def search_by_name(self, name: str, fuzzy: bool = True) -> list[KU]:
|
|
525
|
+
with self._lock:
|
|
526
|
+
if fuzzy:
|
|
527
|
+
rows = self._db.execute(
|
|
528
|
+
"SELECT ku.* FROM knowledge_units ku "
|
|
529
|
+
"JOIN ku_fts ON ku.ku_id = ku_fts.ku_id "
|
|
530
|
+
"WHERE ku_fts MATCH ? AND ku.status!='deleted' LIMIT 20",
|
|
531
|
+
(name,)
|
|
532
|
+
).fetchall()
|
|
533
|
+
else:
|
|
534
|
+
rows = self._db.execute(
|
|
535
|
+
"SELECT * FROM knowledge_units "
|
|
536
|
+
"WHERE name=? AND status!='deleted'",
|
|
537
|
+
(name,)
|
|
538
|
+
).fetchall()
|
|
539
|
+
return [_row_to_ku(tuple(r)) for r in rows]
|
|
540
|
+
|
|
541
|
+
# ── 关系 ─────────────────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
def add_relation(self, rel: Relation) -> None:
|
|
544
|
+
with self._lock:
|
|
545
|
+
self._db.execute("""
|
|
546
|
+
INSERT OR IGNORE INTO relations
|
|
547
|
+
(rel_id,from_ku,to_ku,rel_type,weight,source,created_at)
|
|
548
|
+
VALUES (?,?,?,?,?,?,?)
|
|
549
|
+
""", _rel_to_row(rel))
|
|
550
|
+
self._db.commit()
|
|
551
|
+
self._graph.add_edge(
|
|
552
|
+
rel.from_ku, rel.to_ku,
|
|
553
|
+
rel_type=rel.rel_type,
|
|
554
|
+
weight=rel.weight,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def batch_add_relations(self, rels: "list[Relation]") -> None:
|
|
558
|
+
"""批量写关系:一次 executemany + 单次 commit,再同步内存图。
|
|
559
|
+
|
|
560
|
+
替代逐条 add_relation(每条一次 commit=一次 fsync):大文档关系发现产出
|
|
561
|
+
上千条关系时,逐条 commit 的 fsync 风暴会让摄入 job 卡数分钟。此处一次落盘。
|
|
562
|
+
"""
|
|
563
|
+
if not rels:
|
|
564
|
+
return
|
|
565
|
+
with self._lock:
|
|
566
|
+
self._db.executemany("""
|
|
567
|
+
INSERT OR IGNORE INTO relations
|
|
568
|
+
(rel_id,from_ku,to_ku,rel_type,weight,source,created_at)
|
|
569
|
+
VALUES (?,?,?,?,?,?,?)
|
|
570
|
+
""", [_rel_to_row(r) for r in rels])
|
|
571
|
+
self._db.commit()
|
|
572
|
+
for r in rels:
|
|
573
|
+
self._graph.add_edge(
|
|
574
|
+
r.from_ku, r.to_ku,
|
|
575
|
+
rel_type=r.rel_type,
|
|
576
|
+
weight=r.weight,
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
def delete_relation(self, rel_id: str) -> None:
|
|
580
|
+
with self._lock:
|
|
581
|
+
row = self._db.execute(
|
|
582
|
+
"SELECT from_ku,to_ku,rel_type FROM relations WHERE rel_id=?",
|
|
583
|
+
(rel_id,)
|
|
584
|
+
).fetchone()
|
|
585
|
+
self._db.execute("DELETE FROM relations WHERE rel_id=?", (rel_id,))
|
|
586
|
+
self._db.commit()
|
|
587
|
+
if row:
|
|
588
|
+
from_ku, to_ku, _ = row[0], row[1], row[2]
|
|
589
|
+
# 同步移除图中的边(若存在)
|
|
590
|
+
if self._graph.has_edge(from_ku, to_ku):
|
|
591
|
+
try:
|
|
592
|
+
self._graph.remove_edge(from_ku, to_ku)
|
|
593
|
+
except Exception:
|
|
594
|
+
pass
|
|
595
|
+
|
|
596
|
+
def redirect_relations(self, from_ku_id: str, to_ku_id: str) -> int:
|
|
597
|
+
"""把 from_ku_id 的所有关系重定向到 to_ku_id;丢弃自环;去重。"""
|
|
598
|
+
if from_ku_id == to_ku_id:
|
|
599
|
+
return 0
|
|
600
|
+
with self._lock:
|
|
601
|
+
rows = self._db.execute(
|
|
602
|
+
"SELECT rel_id,from_ku,to_ku,rel_type,weight,source,created_at "
|
|
603
|
+
"FROM relations WHERE from_ku=? OR to_ku=?",
|
|
604
|
+
(from_ku_id, from_ku_id)
|
|
605
|
+
).fetchall()
|
|
606
|
+
|
|
607
|
+
redirected = 0
|
|
608
|
+
for r in rows:
|
|
609
|
+
rel = _row_to_rel(tuple(r))
|
|
610
|
+
new_from = to_ku_id if rel.from_ku == from_ku_id else rel.from_ku
|
|
611
|
+
new_to = to_ku_id if rel.to_ku == from_ku_id else rel.to_ku
|
|
612
|
+
|
|
613
|
+
# 删除旧关系(表+图)
|
|
614
|
+
self.delete_relation(rel.rel_id)
|
|
615
|
+
|
|
616
|
+
# 自环丢弃(重定向后两端相同)
|
|
617
|
+
if new_from == new_to:
|
|
618
|
+
continue
|
|
619
|
+
|
|
620
|
+
# 检查是否已存在等价关系(避免重复)
|
|
621
|
+
existing = self.get_relations(new_from, rel_type=rel.rel_type, direction="out")
|
|
622
|
+
if any(e.to_ku == new_to for e in existing):
|
|
623
|
+
continue
|
|
624
|
+
|
|
625
|
+
rel.from_ku = new_from
|
|
626
|
+
rel.to_ku = new_to
|
|
627
|
+
self.add_relation(rel)
|
|
628
|
+
redirected += 1
|
|
629
|
+
|
|
630
|
+
return redirected
|
|
631
|
+
|
|
632
|
+
def get_relations(
|
|
633
|
+
self,
|
|
634
|
+
ku_id: str,
|
|
635
|
+
rel_type: str = "",
|
|
636
|
+
direction: str = "both",
|
|
637
|
+
) -> list[Relation]:
|
|
638
|
+
with self._lock:
|
|
639
|
+
if direction == "out":
|
|
640
|
+
q = "SELECT * FROM relations WHERE from_ku=?"
|
|
641
|
+
params = [ku_id]
|
|
642
|
+
elif direction == "in":
|
|
643
|
+
q = "SELECT * FROM relations WHERE to_ku=?"
|
|
644
|
+
params = [ku_id]
|
|
645
|
+
else:
|
|
646
|
+
q = "SELECT * FROM relations WHERE from_ku=? OR to_ku=?"
|
|
647
|
+
params = [ku_id, ku_id]
|
|
648
|
+
if rel_type:
|
|
649
|
+
q += " AND rel_type=?"
|
|
650
|
+
params.append(rel_type)
|
|
651
|
+
rows = self._db.execute(q, params).fetchall()
|
|
652
|
+
return [_row_to_rel(tuple(r)) for r in rows]
|
|
653
|
+
|
|
654
|
+
def get_all_relations(self, limit: int = 5000) -> list[Relation]:
|
|
655
|
+
with self._lock:
|
|
656
|
+
rows = self._db.execute(
|
|
657
|
+
"SELECT * FROM relations LIMIT ?", (limit,)
|
|
658
|
+
).fetchall()
|
|
659
|
+
return [_row_to_rel(tuple(r)) for r in rows]
|
|
660
|
+
|
|
661
|
+
def neighbors(
|
|
662
|
+
self,
|
|
663
|
+
ku_id: str,
|
|
664
|
+
rel_type: str = "",
|
|
665
|
+
depth: int = 1,
|
|
666
|
+
) -> list[KU]:
|
|
667
|
+
with self._lock:
|
|
668
|
+
if depth == 1:
|
|
669
|
+
# 直接邻居,从内存图取 ID,再去 SQLite 取完整数据
|
|
670
|
+
out_nodes = list(self._graph.successors(ku_id))
|
|
671
|
+
in_nodes = list(self._graph.predecessors(ku_id))
|
|
672
|
+
neighbor_ids = set(out_nodes + in_nodes)
|
|
673
|
+
else:
|
|
674
|
+
# BFS 多层
|
|
675
|
+
neighbor_ids = set()
|
|
676
|
+
frontier = {ku_id}
|
|
677
|
+
for _ in range(depth):
|
|
678
|
+
next_frontier = set()
|
|
679
|
+
for node in frontier:
|
|
680
|
+
next_frontier.update(self._graph.successors(node))
|
|
681
|
+
next_frontier.update(self._graph.predecessors(node))
|
|
682
|
+
neighbor_ids.update(next_frontier)
|
|
683
|
+
frontier = next_frontier
|
|
684
|
+
neighbor_ids.discard(ku_id)
|
|
685
|
+
|
|
686
|
+
result = []
|
|
687
|
+
for nid in neighbor_ids:
|
|
688
|
+
ku = self.get(nid)
|
|
689
|
+
if ku:
|
|
690
|
+
result.append(ku)
|
|
691
|
+
return result
|
|
692
|
+
|
|
693
|
+
def shortest_path(self, from_id: str, to_id: str) -> list[str]:
|
|
694
|
+
try:
|
|
695
|
+
return nx.shortest_path(self._graph, from_id, to_id)
|
|
696
|
+
except (nx.NetworkXNoPath, nx.NodeNotFound):
|
|
697
|
+
return []
|
|
698
|
+
|
|
699
|
+
# ── Conflict Cards ────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
def save_conflict(self, card: ConflictCard) -> None:
|
|
702
|
+
with self._lock:
|
|
703
|
+
self._db.execute("""
|
|
704
|
+
INSERT OR REPLACE INTO conflict_cards
|
|
705
|
+
(conflict_id,claim_a_id,claim_b_id,domain,status,
|
|
706
|
+
strategy,note,resolved_by,resolved_at,created_at)
|
|
707
|
+
VALUES (?,?,?,?,?,?,?,?,?,?)
|
|
708
|
+
""", _card_to_row(card))
|
|
709
|
+
self._db.commit()
|
|
710
|
+
|
|
711
|
+
def get_conflict(self, conflict_id: str) -> Optional[ConflictCard]:
|
|
712
|
+
with self._lock:
|
|
713
|
+
row = self._db.execute(
|
|
714
|
+
"SELECT * FROM conflict_cards WHERE conflict_id=?",
|
|
715
|
+
(conflict_id,)
|
|
716
|
+
).fetchone()
|
|
717
|
+
return _row_to_card(tuple(row)) if row else None
|
|
718
|
+
|
|
719
|
+
def list_conflicts(
|
|
720
|
+
self,
|
|
721
|
+
status: str = "open",
|
|
722
|
+
domain: str = "",
|
|
723
|
+
) -> list[ConflictCard]:
|
|
724
|
+
with self._lock:
|
|
725
|
+
if domain:
|
|
726
|
+
rows = self._db.execute(
|
|
727
|
+
"SELECT * FROM conflict_cards WHERE status=? AND domain=?",
|
|
728
|
+
(status, domain)
|
|
729
|
+
).fetchall()
|
|
730
|
+
else:
|
|
731
|
+
rows = self._db.execute(
|
|
732
|
+
"SELECT * FROM conflict_cards WHERE status=?", (status,)
|
|
733
|
+
).fetchall()
|
|
734
|
+
return [_row_to_card(tuple(r)) for r in rows]
|
|
735
|
+
|
|
736
|
+
def find_claims_by_domain(
|
|
737
|
+
self,
|
|
738
|
+
domain: str,
|
|
739
|
+
status: ClaimStatus = None,
|
|
740
|
+
) -> list[ClaimKU]:
|
|
741
|
+
with self._lock:
|
|
742
|
+
if status:
|
|
743
|
+
rows = self._db.execute(
|
|
744
|
+
"SELECT * FROM knowledge_units "
|
|
745
|
+
"WHERE ku_type='Claim' AND domain=? "
|
|
746
|
+
"AND json_extract(extra,'$.claim_status')=?",
|
|
747
|
+
(domain, status.value)
|
|
748
|
+
).fetchall()
|
|
749
|
+
else:
|
|
750
|
+
rows = self._db.execute(
|
|
751
|
+
"SELECT * FROM knowledge_units "
|
|
752
|
+
"WHERE ku_type='Claim' AND domain=?",
|
|
753
|
+
(domain,)
|
|
754
|
+
).fetchall()
|
|
755
|
+
return [_row_to_ku(tuple(r)) for r in rows]
|
|
756
|
+
|
|
757
|
+
# ── 统계 ─────────────────────────────────────────────────
|
|
758
|
+
|
|
759
|
+
def stats(self) -> dict:
|
|
760
|
+
with self._lock:
|
|
761
|
+
type_counts = {}
|
|
762
|
+
rows = self._db.execute(
|
|
763
|
+
"SELECT ku_type, COUNT(*) as cnt "
|
|
764
|
+
"FROM knowledge_units WHERE status!='deleted' "
|
|
765
|
+
"GROUP BY ku_type"
|
|
766
|
+
).fetchall()
|
|
767
|
+
for row in rows:
|
|
768
|
+
type_counts[row["ku_type"]] = row["cnt"]
|
|
769
|
+
|
|
770
|
+
rel_count = self._db.execute(
|
|
771
|
+
"SELECT COUNT(*) FROM relations"
|
|
772
|
+
).fetchone()[0]
|
|
773
|
+
|
|
774
|
+
conflict_count = self._db.execute(
|
|
775
|
+
"SELECT COUNT(*) FROM conflict_cards WHERE status='open'"
|
|
776
|
+
).fetchone()[0]
|
|
777
|
+
|
|
778
|
+
return {
|
|
779
|
+
"ku_counts": type_counts,
|
|
780
|
+
"total_kus": sum(type_counts.values()),
|
|
781
|
+
"total_relations": rel_count,
|
|
782
|
+
"open_conflicts": conflict_count,
|
|
783
|
+
"graph_nodes": self._graph.number_of_nodes(),
|
|
784
|
+
"graph_edges": self._graph.number_of_edges(),
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
def close(self) -> None:
|
|
788
|
+
"""显式关闭数据库连接。测试和应用退出时调用。"""
|
|
789
|
+
self._db.close()
|