foxclaw 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +1 -8
- package/package.json +1 -1
- package/python/.env.example +8 -0
- package/python/.github/workflows/ci.yml +47 -2
- package/python/CHANGELOG.md +57 -0
- package/python/CHANGELOG_CN.md +57 -0
- package/python/README.md +11 -2
- package/python/README_CN.md +32 -20
- package/python/bench/bm25_audit.py +1 -14
- package/python/bench/bm25_regression.py +0 -1
- package/python/bench/bm25_stress.py +0 -4
- package/python/bench/cd_audit.py +0 -23
- package/python/bench/chaos.py +6 -8
- package/python/bench/e2e_real_models.py +0 -1
- package/python/bench/llm_retry_test.py +2 -9
- package/python/bench/load_test.py +5 -8
- package/python/bench/vector_ivf_upgrade.py +1 -7
- package/python/docs/RELEASE.md +4 -3
- package/python/docs/RUN_CN.md +2 -2
- package/python/evals/governance/cases.py +0 -37
- package/python/evals/governance/run_evals.py +5 -37
- package/python/examples/governance_middleware_demo.py +1 -6
- package/python/examples/langgraph_governance_demo.py +7 -11
- package/python/foxclaw/__init__.py +34 -152
- package/python/foxclaw/agents/base.py +65 -136
- package/python/foxclaw/agents/impl/_helpers.py +2 -10
- package/python/foxclaw/agents/impl/finance.py +4 -21
- package/python/foxclaw/agents/impl/office.py +7 -24
- package/python/foxclaw/agents/impl/supervisor.py +12 -79
- package/python/foxclaw/agents/mixins/_context.py +0 -23
- package/python/foxclaw/agents/mixins/augment.py +10 -16
- package/python/foxclaw/agents/mixins/llm.py +7 -6
- package/python/foxclaw/agents/mixins/memory.py +7 -1
- package/python/foxclaw/agents/mixins/preference.py +58 -8
- package/python/foxclaw/agents/mixins/prompt.py +0 -6
- package/python/foxclaw/agents/mixins/skillbox.py +6 -6
- package/python/foxclaw/agents/mixins/tool_exec.py +16 -42
- package/python/foxclaw/api/__init__.py +18 -77
- package/python/foxclaw/api/_state.py +43 -0
- package/python/foxclaw/api/{app.py → bootstrap.py} +5 -32
- package/python/foxclaw/api/middleware.py +5 -28
- package/python/foxclaw/api/routes/ahs.py +60 -73
- package/python/foxclaw/api/routes/attachments.py +5 -6
- package/python/foxclaw/api/routes/browser.py +7 -11
- package/python/foxclaw/api/routes/domain.py +18 -26
- package/python/foxclaw/api/routes/knowledge.py +3 -21
- package/python/foxclaw/api/routes/llm_admin.py +21 -33
- package/python/foxclaw/api/routes/memory.py +28 -12
- package/python/foxclaw/api/routes/orchestration.py +3 -5
- package/python/foxclaw/api/routes/recovery.py +4 -3
- package/python/foxclaw/api/routes/review.py +2 -11
- package/python/foxclaw/api/routes/search.py +3 -7
- package/python/foxclaw/api/routes/system.py +33 -49
- package/python/foxclaw/api/routes/task.py +31 -49
- package/python/foxclaw/api/routes/ws.py +2 -6
- package/python/foxclaw/api/spine.py +1 -4
- package/python/foxclaw/browser/action/action_executor.py +1 -5
- package/python/foxclaw/browser/action/action_types.py +0 -1
- package/python/foxclaw/browser/action/url_policy.py +1 -5
- package/python/foxclaw/browser/adapter/web_adapter.py +1 -1
- package/python/foxclaw/browser/driver/playwright_driver.py +20 -16
- package/python/foxclaw/browser/perception/dom_extractor.py +0 -1
- package/python/foxclaw/browser/perception/element_indexer.py +1 -2
- package/python/foxclaw/browser/service.py +7 -11
- package/python/foxclaw/capability/embedding_service.py +4 -6
- package/python/foxclaw/channels/__init__.py +9 -11
- package/python/foxclaw/channels/discord.py +4 -13
- package/python/foxclaw/channels/dispatcher.py +2 -27
- package/python/foxclaw/channels/feishu.py +10 -20
- package/python/foxclaw/channels/openclaw_bridge.py +4 -9
- package/python/foxclaw/channels/signing.py +9 -9
- package/python/foxclaw/channels/telegram.py +3 -4
- package/python/foxclaw/channels/weixin.py +7 -17
- package/python/foxclaw/cli.py +42 -13
- package/python/foxclaw/cognitive/reasoner.py +9 -10
- package/python/foxclaw/cognitive/state.py +18 -24
- package/python/foxclaw/core/audit_chain.py +1 -3
- package/python/foxclaw/core/audit_sink.py +3 -3
- package/python/foxclaw/core/compaction.py +8 -26
- package/python/foxclaw/core/config.py +2 -2
- package/python/foxclaw/core/confirm_nonce.py +6 -12
- package/python/foxclaw/core/envfile.py +2 -2
- package/python/foxclaw/core/failure_policy.py +17 -26
- package/python/foxclaw/core/gateway.py +6 -10
- package/python/foxclaw/core/governance_audit.py +3 -7
- package/python/foxclaw/core/idempotency.py +15 -22
- package/python/foxclaw/core/injection_guard.py +10 -29
- package/python/foxclaw/core/memory_adapter.py +30 -57
- package/python/foxclaw/core/metrics.py +4 -5
- package/python/foxclaw/core/model_router.py +36 -148
- package/python/foxclaw/core/paths.py +1 -1
- package/python/foxclaw/core/recovery.py +21 -34
- package/python/foxclaw/core/scheduler.py +23 -26
- package/python/foxclaw/core/secrets.py +0 -3
- package/python/foxclaw/core/sessions.py +4 -6
- package/python/foxclaw/core/settings.py +16 -32
- package/python/foxclaw/core/shared_write.py +11 -23
- package/python/foxclaw/core/steering_queue.py +12 -11
- package/python/foxclaw/core/system_bus.py +1 -1
- package/python/foxclaw/core/thought_chain.py +30 -32
- package/python/foxclaw/core/token_meter.py +9 -13
- package/python/foxclaw/core/tools.py +4 -12
- package/python/foxclaw/core/tracing.py +25 -40
- package/python/foxclaw/core/truth_pipeline.py +5 -9
- package/python/foxclaw/core/update_check.py +149 -0
- package/python/foxclaw/core/vec_recall_adapter.py +11 -37
- package/python/foxclaw/core/verify.py +6 -14
- package/python/foxclaw/core/web_search.py +4 -28
- package/python/foxclaw/finance/evidence.py +14 -18
- package/python/foxclaw/finance/fx.py +1 -3
- package/python/foxclaw/finance/graph.py +9 -18
- package/python/foxclaw/finance/models.py +20 -24
- package/python/foxclaw/finance/nlparse.py +12 -72
- package/python/foxclaw/finance/policy.py +8 -36
- package/python/foxclaw/finance/reasoner.py +3 -11
- package/python/foxclaw/finance/reasoning.py +13 -34
- package/python/foxclaw/finance/reconcile.py +5 -10
- package/python/foxclaw/finance/runtime.py +19 -46
- package/python/foxclaw/governance/middleware.py +13 -21
- package/python/foxclaw/harness/_base.py +2 -8
- package/python/foxclaw/harness/_policy.py +19 -38
- package/python/foxclaw/harness/drift.py +1 -13
- package/python/foxclaw/harness/event_bus.py +2 -5
- package/python/foxclaw/harness/harness.py +9 -97
- package/python/foxclaw/harness/loop_detection.py +11 -18
- package/python/foxclaw/itinerary/amap_client.py +2 -5
- package/python/foxclaw/itinerary/models.py +7 -7
- package/python/foxclaw/itinerary/reasoner.py +1 -6
- package/python/foxclaw/itinerary/reasoning.py +15 -21
- package/python/foxclaw/itinerary/runtime.py +8 -12
- package/python/foxclaw/kernel/capabilities.py +5 -6
- package/python/foxclaw/kernel/policy_gate.py +4 -10
- package/python/foxclaw/kernel/process_manager.py +17 -36
- package/python/foxclaw/kernel/scheduler.py +3 -9
- package/python/foxclaw/kernel/sentinel.py +13 -23
- package/python/foxclaw/kernel/sentinel_client.py +12 -15
- package/python/foxclaw/kernel/spine.py +9 -9
- package/python/foxclaw/knowledge/__init__.py +4 -20
- package/python/foxclaw/knowledge/truth_crystallizer.py +5 -8
- package/python/foxclaw/learning/knowledge_graph.py +4 -9
- package/python/foxclaw/learning/models.py +13 -13
- package/python/foxclaw/learning/reasoner.py +1 -4
- package/python/foxclaw/learning/runtime.py +19 -31
- package/python/foxclaw/learning/spaced_repetition.py +2 -7
- package/python/foxclaw/mcp_server.py +2 -3
- package/python/foxclaw/orchestration/__init__.py +16 -33
- package/python/foxclaw/pipeline/cognitive_pipeline.py +45 -97
- package/python/foxclaw/pipeline/stage_meta.py +21 -0
- package/python/foxclaw/pipeline/supervisor_stages.py +13 -42
- package/python/foxclaw/planning/llm_planner.py +3 -3
- package/python/foxclaw/planning/planner.py +6 -11
- package/python/foxclaw/search/agent_port.py +7 -11
- package/python/foxclaw/search/evidence.py +0 -4
- package/python/foxclaw/search/internal_search.py +3 -5
- package/python/foxclaw/search/models.py +11 -13
- package/python/foxclaw/search/planner.py +3 -3
- package/python/foxclaw/search/retriever.py +1 -1
- package/python/foxclaw/search/runtime.py +1 -8
- package/python/foxclaw/skills/__init__.py +46 -100
- package/python/foxclaw/skills/archive.py +111 -0
- package/python/foxclaw/skills/fetch.py +226 -0
- package/python/foxclaw/skills/hooks_spec.py +21 -25
- package/python/foxclaw/skills/install_cmd.py +216 -0
- package/python/foxclaw/skills/lifecycle.py +16 -42
- package/python/foxclaw/skills/markdown_skill.py +10 -9
- package/python/foxclaw/skills/mcp_client.py +5 -11
- package/python/foxclaw/skills/mcp_store.py +0 -3
- package/python/foxclaw/skills/permission.py +13 -48
- package/python/foxclaw/skills/pipeline.py +21 -53
- package/python/foxclaw/skills/policy.py +4 -10
- package/python/foxclaw/skills/review.py +3 -3
- package/python/frontend/deck/auth.js +0 -7
- package/python/frontend/deck/chat_model_picker.js +0 -54
- package/python/frontend/deck/domain.js +0 -20
- package/python/frontend/deck/main.js +0 -144
- package/python/frontend/deck/markdown.js +0 -4
- package/python/frontend/deck/settings.js +1 -18
- package/python/frontend/deck/state.js +0 -1
- package/python/frontend/deck/util.js +0 -2
- package/python/frontend/deck/visuals.js +0 -22
- package/python/frontend/planet/planet.js +0 -201
- package/python/frontend/v3/assets/index-CQSBdxk4.js +34 -0
- package/python/frontend/v3/assets/index-D5nIsjms.css +1 -0
- package/python/frontend/v3/index.html +3 -3
- package/python/frontend/widget_runtime.js +0 -43
- package/python/frontend/widgets.js +0 -57
- package/python/frontend_src/index.html +1 -1
- package/python/frontend_src/package-lock.json +2 -2
- package/python/frontend_src/package.json +1 -1
- package/python/frontend_src/src/App.vue +3 -7
- package/python/frontend_src/src/api.js +0 -7
- package/python/frontend_src/src/components/ChatDock.vue +27 -39
- package/python/frontend_src/src/components/DocViewer.vue +0 -9
- package/python/frontend_src/src/components/Inspector.vue +0 -6
- package/python/frontend_src/src/components/LeftRail.vue +60 -39
- package/python/frontend_src/src/components/MenuPopover.vue +9 -11
- package/python/frontend_src/src/components/MessageStream.vue +18 -30
- package/python/frontend_src/src/components/ModelPicker.vue +0 -5
- package/python/frontend_src/src/components/QuickIngest.vue +0 -6
- package/python/frontend_src/src/components/Stage.vue +71 -25
- package/python/frontend_src/src/components/ToolMenu.vue +0 -3
- package/python/frontend_src/src/components/TopBar.vue +38 -10
- package/python/frontend_src/src/components/panels/ActivityPanel.vue +0 -2
- package/python/frontend_src/src/components/panels/AgentsPanel.vue +2 -5
- package/python/frontend_src/src/components/panels/KnowledgePanel.vue +0 -16
- package/python/frontend_src/src/components/panels/MemoryPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/PanelBox.vue +6 -7
- package/python/frontend_src/src/components/panels/ReviewPanel.vue +0 -16
- package/python/frontend_src/src/components/panels/SettingsPanel.vue +142 -38
- package/python/frontend_src/src/components/panels/TasksPanel.vue +0 -1
- package/python/frontend_src/src/components/panels/ToolsPanel.vue +125 -19
- package/python/frontend_src/src/lib/fmt.js +0 -4
- package/python/frontend_src/src/lib/i18n.js +53 -0
- package/python/frontend_src/src/lib/locales/en.js +173 -0
- package/python/frontend_src/src/lib/locales/zh.js +174 -0
- package/python/frontend_src/src/lib/md.js +0 -14
- package/python/frontend_src/src/main.js +2 -0
- package/python/frontend_src/src/stores/app.js +17 -32
- package/python/frontend_src/src/stores/chat.js +0 -34
- package/python/frontend_src/src/stores/model.js +0 -8
- package/python/frontend_src/src/stores/panels.js +0 -41
- package/python/frontend_src/src/stores/profile.js +31 -0
- package/python/frontend_src/src/stores/system.js +25 -0
- package/python/frontend_src/vite.config.js +0 -7
- package/python/pyproject.toml +6 -3
- package/python/requirements.txt +311 -0
- package/python/tests/conftest.py +0 -22
- package/python/tests/run_all.py +3 -66
- package/python/tests/test_agent_lifecycle.py +4 -38
- package/python/tests/test_ahs_p2.py +4 -25
- package/python/tests/test_anthropic_tools.py +1 -15
- package/python/tests/test_api.py +19 -9
- package/python/tests/test_api_auth.py +1 -5
- package/python/tests/test_attachments.py +9 -25
- package/python/tests/test_audit_chain.py +2 -7
- package/python/tests/test_audit_sink.py +0 -6
- package/python/tests/test_browser.py +2 -6
- package/python/tests/test_browser_request_guard.py +0 -4
- package/python/tests/test_capabilities.py +0 -6
- package/python/tests/test_channel_signing.py +2 -9
- package/python/tests/test_channels.py +1 -11
- package/python/tests/test_config_layering.py +0 -4
- package/python/tests/test_confirm_nonce.py +1 -12
- package/python/tests/test_context_compaction_wiring.py +1 -7
- package/python/tests/test_dispatch.py +0 -6
- package/python/tests/test_domain_evals.py +11 -80
- package/python/tests/test_domain_import.py +0 -6
- package/python/tests/test_domain_reasoners.py +0 -14
- package/python/tests/test_e2e.py +0 -17
- package/python/tests/test_emb_cache.py +7 -17
- package/python/tests/test_embedding_singleton.py +1 -3
- package/python/tests/test_evals_smoke.py +0 -24
- package/python/tests/test_event_spine.py +10 -18
- package/python/tests/test_evidence_semantic.py +1 -14
- package/python/tests/test_evidence_sse.py +3 -6
- package/python/tests/test_file_ingest.py +0 -11
- package/python/tests/test_finance.py +0 -9
- package/python/tests/test_finance_ledger.py +5 -12
- package/python/tests/test_finance_persist.py +8 -12
- package/python/tests/test_gateway.py +3 -22
- package/python/tests/test_governance.py +5 -52
- package/python/tests/test_governance_audit.py +4 -39
- package/python/tests/test_governance_middleware.py +1 -7
- package/python/tests/test_hkc.py +6 -24
- package/python/tests/test_hkc_index_build_manager.py +0 -33
- package/python/tests/test_hkc_manifest_reuse.py +4 -27
- package/python/tests/test_hkc_search_event_sync.py +3 -54
- package/python/tests/test_import_path_guard.py +0 -6
- package/python/tests/test_injection_guard.py +18 -36
- package/python/tests/test_integration.py +5 -29
- package/python/tests/test_itinerary.py +0 -3
- package/python/tests/test_kee_semantic.py +0 -5
- package/python/tests/test_kernel.py +1 -11
- package/python/tests/test_learning.py +3 -12
- package/python/tests/test_llm_admin.py +4 -19
- package/python/tests/test_llm_planner.py +1 -23
- package/python/tests/test_markdown_skills.py +3 -14
- package/python/tests/test_markdown_skills_openclaw.py +4 -9
- package/python/tests/test_mcp_connectors.py +3 -15
- package/python/tests/test_mcp_guardian_discovery.py +2 -6
- package/python/tests/test_mcp_no_memory.py +3 -3
- package/python/tests/test_mcp_policy_gate.py +0 -1
- package/python/tests/test_mcp_server.py +6 -9
- package/python/tests/test_memory_recall.py +1 -4
- package/python/tests/test_metrics.py +0 -8
- package/python/tests/test_multi_chain.py +1 -13
- package/python/tests/test_multimodal.py +5 -18
- package/python/tests/test_negotiation.py +3 -13
- package/python/tests/test_orchestration.py +1 -11
- package/python/tests/test_orchestration_decoupled.py +0 -6
- package/python/tests/test_p3_p5_hardening.py +3 -17
- package/python/tests/test_parallel_dispatch.py +4 -12
- package/python/tests/test_pipeline.py +17 -58
- package/python/tests/test_plan_visible.py +4 -4
- package/python/tests/test_planner.py +0 -7
- package/python/tests/test_policy_gate.py +2 -3
- package/python/tests/test_profile.py +67 -0
- package/python/tests/test_readme_claims.py +163 -0
- package/python/tests/test_reasoner.py +2 -17
- package/python/tests/test_release_check.py +2 -13
- package/python/tests/test_resume_checkpoint.py +7 -15
- package/python/tests/test_routing.py +2 -12
- package/python/tests/test_scheduler.py +7 -10
- package/python/tests/test_search.py +1 -11
- package/python/tests/test_secrets.py +4 -10
- package/python/tests/test_semantic.py +1 -10
- package/python/tests/test_sessions.py +3 -4
- package/python/tests/test_settings.py +2 -18
- package/python/tests/test_shadow_learning.py +3 -43
- package/python/tests/test_shared_write.py +0 -7
- package/python/tests/test_skill_taint_gate.py +1 -10
- package/python/tests/test_skills.py +2 -18
- package/python/tests/test_skills_fetch.py +282 -0
- package/python/tests/test_stats_and_events.py +2 -5
- package/python/tests/test_streaming.py +2 -7
- package/python/tests/test_supervisor_pipeline.py +4 -5
- package/python/tests/test_supervisor_triage.py +4 -10
- package/python/tests/test_taint.py +2 -7
- package/python/tests/test_task_stream.py +1 -12
- package/python/tests/test_thinking_sse.py +0 -1
- package/python/tests/test_tool_center.py +1 -17
- package/python/tests/test_tool_parallel.py +2 -2
- package/python/tests/test_tool_params.py +1 -5
- package/python/tests/test_tool_use.py +0 -13
- package/python/tests/test_toplevel_concurrency.py +3 -4
- package/python/tests/test_tracing.py +7 -41
- package/python/tests/test_truth_crystallizer.py +4 -6
- package/python/tests/test_truth_flow.py +2 -7
- package/python/tests/test_update_check.py +95 -0
- package/python/tests/test_verify_evidence.py +8 -21
- package/python/tests/test_version_sync.py +35 -6
- package/python/tests/test_web_trigger.py +0 -17
- package/python/tools/check_faiss_type.py +0 -1
- package/python/tools/check_log.ps1 +0 -8
- package/python/tools/mypy_baseline.txt +12 -4
- package/python/tools/mypy_ratchet.py +52 -6
- package/python/tools/pack.py +0 -5
- package/python/tools/release_check.py +7 -7
- package/python/tools/restart_foxclaw.ps1 +5 -8
- package/python/tools/shared_write_audit.py +1 -15
- package/python/tools/size_check.py +1 -1
- package/python/tools/strip_comments.py +228 -0
- package/python/frontend/v3/assets/index-BD7VPbMV.js +0 -33
- package/python/frontend/v3/assets/index-CAvnYWGH.css +0 -1
|
@@ -64,7 +64,6 @@ def _read_custom_model(data_dir, name):
|
|
|
64
64
|
if not match:
|
|
65
65
|
return None
|
|
66
66
|
key = ""
|
|
67
|
-
# OS 凭据库优先,回退 base64(旧记录)/ 更早的明文字段
|
|
68
67
|
from ..core.secrets import load as _load_secret
|
|
69
68
|
key = _load_secret(name, match.get("api_key_obf") or "")
|
|
70
69
|
key = key or (match.get("api_key") or "")
|
|
@@ -101,7 +100,6 @@ def _selected_llm():
|
|
|
101
100
|
return None
|
|
102
101
|
|
|
103
102
|
source = (sel.get("source") or "").strip().lower()
|
|
104
|
-
# preset 落盘的是 provider;custom 落盘的是 name。两者都当"选中的名字"用。
|
|
105
103
|
name = (sel.get("name") or sel.get("provider") or "").strip()
|
|
106
104
|
model = (sel.get("model_id") or sel.get("model") or "").strip()
|
|
107
105
|
|
|
@@ -113,7 +111,6 @@ def _selected_llm():
|
|
|
113
111
|
return _to_hkc_provider("generic"), key, base, model or custom_model
|
|
114
112
|
|
|
115
113
|
if source == "preset" and name:
|
|
116
|
-
# 界面里配过的同名自定义项(更新的 key)优先,env 只作最后兜底
|
|
117
114
|
got = _read_custom_model(data_dir, name)
|
|
118
115
|
if got:
|
|
119
116
|
key, base, custom_model = got
|
|
@@ -144,7 +141,6 @@ def _detect_llm(api_key=None):
|
|
|
144
141
|
|
|
145
142
|
返回 ``(provider, key, base_url, model)``。
|
|
146
143
|
"""
|
|
147
|
-
# 1. 显式 HKC_LLM_PROVIDER 覆盖(向后兼容「HKC 独立配」场景)
|
|
148
144
|
env_provider = (os.environ.get("HKC_LLM_PROVIDER") or "").strip().lower()
|
|
149
145
|
if env_provider in ("anthropic", "deepseek", "openai", "openai-compatible"):
|
|
150
146
|
key = (os.environ.get("HKC_LLM_API_KEY")
|
|
@@ -158,15 +154,11 @@ def _detect_llm(api_key=None):
|
|
|
158
154
|
or "")
|
|
159
155
|
return env_provider, key or api_key, base, model
|
|
160
156
|
|
|
161
|
-
# 2. 界面里当前选中的模型(用户显式动作,优先于 env 默认)
|
|
162
157
|
selected = _selected_llm()
|
|
163
158
|
if selected:
|
|
164
159
|
provider, key, base, model = selected
|
|
165
160
|
return provider, (key or api_key or "").strip(), (base or "").strip(), model
|
|
166
161
|
|
|
167
|
-
# 3. 单入口 env。未显式指定 provider 时**复用主对话的自动探测**,
|
|
168
|
-
# 不能默认成 "generic" —— 那是个空 key 的壳,会让抽取打 api.openai.com
|
|
169
|
-
# 拿 Missing credentials,每次摄入都 0 条。
|
|
170
162
|
provider = (os.environ.get("LLM_PROVIDER") or "").strip().lower()
|
|
171
163
|
key = (os.environ.get("LLM_API_KEY") or api_key or "").strip()
|
|
172
164
|
base = os.environ.get("LLM_BASE_URL", "").strip()
|
|
@@ -194,7 +186,7 @@ class HKCService:
|
|
|
194
186
|
|
|
195
187
|
def __init__(self, container, client):
|
|
196
188
|
self._container = container
|
|
197
|
-
self.client = client
|
|
189
|
+
self.client = client
|
|
198
190
|
|
|
199
191
|
@property
|
|
200
192
|
def available(self) -> bool:
|
|
@@ -245,22 +237,18 @@ def init_hkc(data_dir, api_key=None):
|
|
|
245
237
|
return None
|
|
246
238
|
try:
|
|
247
239
|
provider, llm_key, llm_base_url, llm_model = _detect_llm(api_key)
|
|
248
|
-
# 【P2·单 LLM 入口】embedding 模型名优先 EMBEDDING_MODEL(系统唯一本地入口),
|
|
249
|
-
# 再 HKC_EMBEDDING_MODEL(向后兼容);其他 HKC 行为开关不变。
|
|
250
240
|
_emb_model = (os.environ.get("EMBEDDING_MODEL")
|
|
251
241
|
or os.environ.get("HKC_EMBEDDING_MODEL")) or None
|
|
252
242
|
container = HKCContainer(
|
|
253
243
|
data_dir=str(Path(data_dir) / "hkc"),
|
|
254
|
-
embedding_kind=os.environ.get("HKC_EMBEDDING", "auto"),
|
|
244
|
+
embedding_kind=os.environ.get("HKC_EMBEDDING", "auto"),
|
|
255
245
|
embedding_url=os.environ.get("HKC_EMBEDDING_URL", "http://localhost:8080"),
|
|
256
|
-
embedding_model=_emb_model,
|
|
246
|
+
embedding_model=_emb_model,
|
|
257
247
|
llm_api_key=llm_key or None,
|
|
258
248
|
llm_provider=provider,
|
|
259
249
|
llm_model=llm_model or None,
|
|
260
250
|
llm_base_url=llm_base_url or None,
|
|
261
251
|
).assemble()
|
|
262
|
-
# 注册为 hkc_api 全局单例:若将来挂 HKC 自身 /hkc REST 路由,
|
|
263
|
-
# 其 Depends(get_container) 能取到这同一个容器,而非"未初始化"。
|
|
264
252
|
from hkc_api import container as _cm
|
|
265
253
|
_cm._container = container
|
|
266
254
|
print(f"[Foxclaw] ✓ HKC 知识核心已进程内融合 (provider={provider}, "
|
|
@@ -275,8 +263,6 @@ def init_hkc(data_dir, api_key=None):
|
|
|
275
263
|
from .truth_crystallizer import TruthCrystallizer
|
|
276
264
|
|
|
277
265
|
|
|
278
|
-
# HKC 知识生命周期事件 → 桥接到 EventSpine 的类型。**刻意不含 memory.***(那是 HMR 域,
|
|
279
|
-
# Foxclaw 已有自己的记忆事件,转发 HKC 的会重复/混淆)。
|
|
280
266
|
_HKC_BRIDGED_EVENTS = (
|
|
281
267
|
"knowledge.created", "knowledge.updated", "knowledge.merged", "knowledge.deleted",
|
|
282
268
|
"conflict.detected", "conflict.resolved", "claim.status_changed",
|
|
@@ -351,7 +337,7 @@ def register_abilities_as_skills(registry, hkc) -> list:
|
|
|
351
337
|
registered = []
|
|
352
338
|
for key in keys:
|
|
353
339
|
try:
|
|
354
|
-
ab = client.compile_ability(key)
|
|
340
|
+
ab = client.compile_ability(key)
|
|
355
341
|
except Exception:
|
|
356
342
|
continue
|
|
357
343
|
sid = f"ability:{key}"
|
|
@@ -363,8 +349,6 @@ def register_abilities_as_skills(registry, hkc) -> list:
|
|
|
363
349
|
handler=_make_ability_handler(client, ab),
|
|
364
350
|
params={"query": "str"}))
|
|
365
351
|
registered.append(sid)
|
|
366
|
-
# 【深审修复·陈旧清理】重新同步时,移除此前注册但**本轮不再达标**的 ability skill
|
|
367
|
-
# (coverage 掉了/能力定义删了)——register-only 会残留陈旧项;prune 使 registry 反映当前覆盖。
|
|
368
352
|
fresh = set(registered)
|
|
369
353
|
for spec in registry.all():
|
|
370
354
|
if spec.id.startswith("ability:") and spec.id not in fresh:
|
|
@@ -15,17 +15,17 @@ Step1(叠加、默认关、行为不变):
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
import threading
|
|
17
17
|
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
-
from typing import Callable, Optional
|
|
18
|
+
from typing import Any, Callable, cast, Optional
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class TruthCrystallizer:
|
|
22
22
|
def __init__(self, hkc_provider: Callable[[], object], enabled: bool = False):
|
|
23
|
-
self._hkc_provider = hkc_provider
|
|
23
|
+
self._hkc_provider = hkc_provider
|
|
24
24
|
self.enabled = enabled
|
|
25
25
|
self._executor: Optional[ThreadPoolExecutor] = None
|
|
26
26
|
self._closed = False
|
|
27
27
|
self._lock = threading.Lock()
|
|
28
|
-
self._pending: list = []
|
|
28
|
+
self._pending: list = []
|
|
29
29
|
|
|
30
30
|
def _ex(self) -> Optional[ThreadPoolExecutor]:
|
|
31
31
|
with self._lock:
|
|
@@ -41,7 +41,7 @@ class TruthCrystallizer:
|
|
|
41
41
|
if not self.enabled:
|
|
42
42
|
return
|
|
43
43
|
p = getattr(msg, "payload", None) or {}
|
|
44
|
-
if p.get("blocked"):
|
|
44
|
+
if p.get("blocked"):
|
|
45
45
|
return
|
|
46
46
|
fact = ((p.get("truth") or {}).get("fact") or "").strip()
|
|
47
47
|
if not fact:
|
|
@@ -52,16 +52,13 @@ class TruthCrystallizer:
|
|
|
52
52
|
return
|
|
53
53
|
fut = ex.submit(self._crystallize, fact, domain)
|
|
54
54
|
with self._lock:
|
|
55
|
-
# 【深审修复·长期运行泄漏】_pending 原先只增不减(drain 仅在 close/测试时调用),
|
|
56
|
-
# 启用后每来一次 truth.appended 就永久累积一个已完成 Future → 无界内存泄漏。
|
|
57
|
-
# 改为每次提交时 prune 掉已完成的,_pending 只保留在飞行中的任务(仍支持测试 drain)。
|
|
58
55
|
self._pending = [f for f in self._pending if not f.done()]
|
|
59
56
|
self._pending.append(fut)
|
|
60
57
|
|
|
61
58
|
def _crystallize(self, fact: str, domain: str) -> bool:
|
|
62
59
|
"""后台执行:惰性取 HKC → ingest_text(KDE→KEE)。任何失败都吞掉,不影响主流程。"""
|
|
63
60
|
try:
|
|
64
|
-
hkc = self._hkc_provider()
|
|
61
|
+
hkc = cast(Any, self._hkc_provider())
|
|
65
62
|
except Exception:
|
|
66
63
|
hkc = None
|
|
67
64
|
if hkc is None or not getattr(hkc, "available", False):
|
|
@@ -17,9 +17,9 @@ class KnowledgeGraph:
|
|
|
17
17
|
DIM = 256
|
|
18
18
|
|
|
19
19
|
def __init__(self, embedder=None):
|
|
20
|
-
self.items: dict = {}
|
|
21
|
-
self.edges: list = []
|
|
22
|
-
self._embedder = embedder
|
|
20
|
+
self.items: dict = {}
|
|
21
|
+
self.edges: list = []
|
|
22
|
+
self._embedder = embedder
|
|
23
23
|
|
|
24
24
|
def add_item(self, item: KnowledgeItem):
|
|
25
25
|
self.items[item.id] = item
|
|
@@ -30,7 +30,6 @@ class KnowledgeGraph:
|
|
|
30
30
|
self.edges = [e for e in self.edges
|
|
31
31
|
if e.source_id != item_id and e.target_id != item_id]
|
|
32
32
|
|
|
33
|
-
# ── 三种连接 ──────────────────────────────────────────
|
|
34
33
|
def link_manual(self, source_id: str, target_id: str, reason: str = ""):
|
|
35
34
|
self.edges.append(KnowledgeEdge(
|
|
36
35
|
source_id=source_id, target_id=target_id,
|
|
@@ -72,7 +71,6 @@ class KnowledgeGraph:
|
|
|
72
71
|
new_edges += 1
|
|
73
72
|
return new_edges
|
|
74
73
|
|
|
75
|
-
# ── 图分析 ────────────────────────────────────────────
|
|
76
74
|
def neighbors(self, item_id: str) -> list:
|
|
77
75
|
"""某知识点的所有关联点。"""
|
|
78
76
|
out = []
|
|
@@ -105,7 +103,7 @@ class KnowledgeGraph:
|
|
|
105
103
|
return result
|
|
106
104
|
|
|
107
105
|
def stats(self) -> dict:
|
|
108
|
-
type_count = defaultdict(int)
|
|
106
|
+
type_count: defaultdict[str, int] = defaultdict(int)
|
|
109
107
|
for e in self.edges:
|
|
110
108
|
type_count[e.edge_type] += 1
|
|
111
109
|
return {
|
|
@@ -115,7 +113,6 @@ class KnowledgeGraph:
|
|
|
115
113
|
"clusters": len(self.clusters()),
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
# ── 向量工具 ──────────────────────────────────────────
|
|
119
116
|
def _embed(self, text: str):
|
|
120
117
|
if self._embedder is not None:
|
|
121
118
|
try:
|
|
@@ -138,8 +135,6 @@ class KnowledgeGraph:
|
|
|
138
135
|
|
|
139
136
|
@staticmethod
|
|
140
137
|
def _cosine(a, b) -> float:
|
|
141
|
-
# 自己归一化,不假设输入已是单位向量:外部 embedder(如 HMR)返回的向量
|
|
142
|
-
# 未必归一化,直接用点积当余弦会系统性偏高(threshold 失效,什么都连上)。
|
|
143
138
|
import math
|
|
144
139
|
dot = sum(x * y for x, y in zip(a, b))
|
|
145
140
|
na = math.sqrt(sum(x * x for x in a))
|
|
@@ -13,17 +13,17 @@ import uuid
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
EdgeType = Literal["same_topic", "concept_ref", "semantic", "manual"]
|
|
16
|
-
Grade = Literal[0, 1, 2, 3, 4, 5]
|
|
16
|
+
Grade = Literal[0, 1, 2, 3, 4, 5]
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
@dataclass
|
|
20
20
|
class KnowledgeItem:
|
|
21
21
|
"""一个知识条目 = 一次学习采集"""
|
|
22
|
-
content: str
|
|
23
|
-
topic: str = "general"
|
|
24
|
-
source: str = ""
|
|
22
|
+
content: str
|
|
23
|
+
topic: str = "general"
|
|
24
|
+
source: str = ""
|
|
25
25
|
tags: list = field(default_factory=list)
|
|
26
|
-
notes: list = field(default_factory=list)
|
|
26
|
+
notes: list = field(default_factory=list)
|
|
27
27
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
28
28
|
id: str = field(default_factory=lambda: f"ki_{uuid.uuid4().hex[:10]}")
|
|
29
29
|
|
|
@@ -35,19 +35,19 @@ class KnowledgeItem:
|
|
|
35
35
|
class ReviewState:
|
|
36
36
|
"""SM-2 间隔重复状态(固化层核心)"""
|
|
37
37
|
item_id: str
|
|
38
|
-
repetitions: int = 0
|
|
39
|
-
interval: int = 0
|
|
40
|
-
ease_factor: float = 2.5
|
|
38
|
+
repetitions: int = 0
|
|
39
|
+
interval: int = 0
|
|
40
|
+
ease_factor: float = 2.5
|
|
41
41
|
due_date: str = field(default_factory=lambda: datetime.now().isoformat())
|
|
42
42
|
last_review: Optional[str] = None
|
|
43
43
|
total_reviews: int = 0
|
|
44
|
-
lapses: int = 0
|
|
44
|
+
lapses: int = 0
|
|
45
45
|
|
|
46
46
|
@property
|
|
47
47
|
def mastery(self) -> float:
|
|
48
48
|
"""掌握度 0~1:综合连续正确次数和难度因子。"""
|
|
49
|
-
rep_score = min(self.repetitions / 6.0, 1.0)
|
|
50
|
-
ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3)
|
|
49
|
+
rep_score = min(self.repetitions / 6.0, 1.0)
|
|
50
|
+
ease_score = (self.ease_factor - 1.3) / (2.5 - 1.3)
|
|
51
51
|
ease_score = max(0.0, min(1.0, ease_score))
|
|
52
52
|
return round(0.6 * rep_score + 0.4 * ease_score, 3)
|
|
53
53
|
|
|
@@ -73,8 +73,8 @@ class KnowledgeEdge:
|
|
|
73
73
|
source_id: str
|
|
74
74
|
target_id: str
|
|
75
75
|
edge_type: EdgeType
|
|
76
|
-
weight: float = 1.0
|
|
77
|
-
reason: str = ""
|
|
76
|
+
weight: float = 1.0
|
|
77
|
+
reason: str = ""
|
|
78
78
|
|
|
79
79
|
def to_dict(self) -> dict:
|
|
80
80
|
return asdict(self)
|
|
@@ -11,7 +11,6 @@ from __future__ import annotations
|
|
|
11
11
|
from ..cognitive.reasoner import DomainReasoner, ReasonedVerdict, Evidence
|
|
12
12
|
from .spaced_repetition import SpacedRepetition
|
|
13
13
|
|
|
14
|
-
# 负荷偏重阈值:未来窗口日均复习量 ≥ 该值即算偏重,交给可选 LLM 复核是否提醒。
|
|
15
14
|
HEAVY_DAILY = 20.0
|
|
16
15
|
|
|
17
16
|
|
|
@@ -37,13 +36,12 @@ class LearningReviewReasoner(DomainReasoner):
|
|
|
37
36
|
return h or ["学习状态可能健康"]
|
|
38
37
|
|
|
39
38
|
def gather_evidence(self, states, obs: dict) -> list:
|
|
40
|
-
risky = SpacedRepetition.forgetting_risk(states, self.horizon_days)
|
|
39
|
+
risky = SpacedRepetition.forgetting_risk(states, self.horizon_days)
|
|
41
40
|
ev = [Evidence(factor="forgetting_risk",
|
|
42
41
|
detail=r["reason"] + f"(item {r['item_id']})", weight=1.0, raw=r)
|
|
43
42
|
for r in risky]
|
|
44
43
|
if not risky:
|
|
45
44
|
ev.append(Evidence("no_urgent_risk", "无即将到期且掌握度低的知识点", 0.5))
|
|
46
|
-
# informational(raw=None):学习负荷,不参与裁决
|
|
47
45
|
ev.append(Evidence("learning_load",
|
|
48
46
|
f"未来窗口共 {obs['total_due']} 项待复习,日均 {obs['daily_avg']}", 0.3))
|
|
49
47
|
return ev
|
|
@@ -51,7 +49,6 @@ class LearningReviewReasoner(DomainReasoner):
|
|
|
51
49
|
def arbitrate(self, states, obs, hyps, ev) -> ReasonedVerdict:
|
|
52
50
|
triggered = [e for e in ev if e.raw is not None]
|
|
53
51
|
if triggered:
|
|
54
|
-
# 【规则权威】存在高遗忘风险 → confirm(建议立即复习),绝不被 LLM 推翻/隐藏
|
|
55
52
|
top = triggered[:3]
|
|
56
53
|
return ReasonedVerdict(
|
|
57
54
|
"confirm", f"发现 {len(triggered)} 个高遗忘风险知识点,建议优先复习:"
|
|
@@ -20,21 +20,20 @@ class LearningRuntime:
|
|
|
20
20
|
"""office Agent 的学习内核。"""
|
|
21
21
|
|
|
22
22
|
def __init__(self, hmr=None, hkc_provider=None, persist_path=None):
|
|
23
|
-
self.items: dict = {}
|
|
24
|
-
self.review_states: dict = {}
|
|
23
|
+
self.items: dict = {}
|
|
24
|
+
self.review_states: dict = {}
|
|
25
25
|
self.sr = SpacedRepetition()
|
|
26
26
|
self.hmr = hmr
|
|
27
|
-
self._hkc_provider = hkc_provider
|
|
28
|
-
self._lock = threading.RLock()
|
|
27
|
+
self._hkc_provider = hkc_provider
|
|
28
|
+
self._lock = threading.RLock()
|
|
29
29
|
embedder = None
|
|
30
30
|
if hmr is not None and hasattr(hmr, "_hmr"):
|
|
31
31
|
embedder = self._make_embedder(hmr)
|
|
32
32
|
self.graph = KnowledgeGraph(embedder=embedder)
|
|
33
|
-
# persist_path 现在只作「离线兜底队列 + 旧 learning.json 迁移源」;知识真源是 HKC。
|
|
34
33
|
self._persist_path = Path(persist_path) if persist_path else None
|
|
35
|
-
self._hkc_loaded = False
|
|
36
|
-
self._load_file()
|
|
37
|
-
self._ensure_hkc_loaded()
|
|
34
|
+
self._hkc_loaded = False
|
|
35
|
+
self._load_file()
|
|
36
|
+
self._ensure_hkc_loaded()
|
|
38
37
|
|
|
39
38
|
def _make_embedder(self, hmr):
|
|
40
39
|
"""尝试复用 HMR 的 embedding;失败则返回 None(图谱自动降级哈希)。"""
|
|
@@ -46,7 +45,6 @@ class LearningRuntime:
|
|
|
46
45
|
pass
|
|
47
46
|
return None
|
|
48
47
|
|
|
49
|
-
# ── 存储:知识内容+笔记+复习状态 全落 HKC(真源);HKC 拿不到时落离线队列(兜底 A)──
|
|
50
48
|
def _graph_store(self):
|
|
51
49
|
"""惰性拿 HKC 的 graph_store。拿不到(HKC 未装配/未配/异常)返回 None。"""
|
|
52
50
|
if not self._hkc_provider:
|
|
@@ -126,9 +124,9 @@ class LearningRuntime:
|
|
|
126
124
|
if self._hkc_loaded:
|
|
127
125
|
return
|
|
128
126
|
gs = self._graph_store()
|
|
129
|
-
if gs is None:
|
|
127
|
+
if gs is None:
|
|
130
128
|
return
|
|
131
|
-
try:
|
|
129
|
+
try:
|
|
132
130
|
for ku in gs.query_all(limit=5000):
|
|
133
131
|
ex = getattr(ku, "extra", {}) or {}
|
|
134
132
|
if ex.get("kind") != "learning_note":
|
|
@@ -146,9 +144,9 @@ class LearningRuntime:
|
|
|
146
144
|
except Exception:
|
|
147
145
|
pass
|
|
148
146
|
except Exception:
|
|
149
|
-
return
|
|
147
|
+
return
|
|
150
148
|
self._hkc_loaded = True
|
|
151
|
-
if self._persist_path and self._persist_path.exists():
|
|
149
|
+
if self._persist_path and self._persist_path.exists():
|
|
152
150
|
self._save()
|
|
153
151
|
|
|
154
152
|
def _save(self):
|
|
@@ -166,11 +164,11 @@ class LearningRuntime:
|
|
|
166
164
|
if ok:
|
|
167
165
|
if self._persist_path and self._persist_path.exists():
|
|
168
166
|
try:
|
|
169
|
-
self._persist_path.unlink()
|
|
167
|
+
self._persist_path.unlink()
|
|
170
168
|
except Exception:
|
|
171
169
|
pass
|
|
172
170
|
return
|
|
173
|
-
if self._persist_path:
|
|
171
|
+
if self._persist_path:
|
|
174
172
|
try:
|
|
175
173
|
data = {
|
|
176
174
|
"items": [asdict(it) for it in items],
|
|
@@ -183,9 +181,8 @@ class LearningRuntime:
|
|
|
183
181
|
except Exception:
|
|
184
182
|
pass
|
|
185
183
|
|
|
186
|
-
# ── 采集 + 沉淀 ───────────────────────────────────────
|
|
187
184
|
def learn(self, content: str, topic: str = "general",
|
|
188
|
-
source: str = "", tags: list = None) -> dict:
|
|
185
|
+
source: str = "", tags: list | None = None) -> dict:
|
|
189
186
|
"""采集一个知识点 → 存进 HKC(知识真源)+ 间隔重复队列 + 图谱。
|
|
190
187
|
铁律:知识内容+笔记归 HKC,不进 HMR(HMR 只装 token 上下文/工作运行记忆)。"""
|
|
191
188
|
self._ensure_hkc_loaded()
|
|
@@ -195,7 +192,7 @@ class LearningRuntime:
|
|
|
195
192
|
self.items[item.id] = item
|
|
196
193
|
self.review_states[item.id] = ReviewState(item_id=item.id)
|
|
197
194
|
self.graph.add_item(item)
|
|
198
|
-
self._save()
|
|
195
|
+
self._save()
|
|
199
196
|
return {"item": item.to_dict(),
|
|
200
197
|
"review": self.review_states[item.id].to_dict()}
|
|
201
198
|
|
|
@@ -212,7 +209,6 @@ class LearningRuntime:
|
|
|
212
209
|
count += 1
|
|
213
210
|
return count
|
|
214
211
|
|
|
215
|
-
# ── 固化:复习 ────────────────────────────────────────
|
|
216
212
|
def review(self, item_id: str, grade: int) -> dict:
|
|
217
213
|
"""复习一个知识点,提交回忆质量评分(0~5),更新掌握度。"""
|
|
218
214
|
self._ensure_hkc_loaded()
|
|
@@ -237,7 +233,7 @@ class LearningRuntime:
|
|
|
237
233
|
gs = self._graph_store()
|
|
238
234
|
if gs is not None:
|
|
239
235
|
try:
|
|
240
|
-
gs.delete(item_id)
|
|
236
|
+
gs.delete(item_id)
|
|
241
237
|
except Exception:
|
|
242
238
|
pass
|
|
243
239
|
if found:
|
|
@@ -256,7 +252,7 @@ class LearningRuntime:
|
|
|
256
252
|
return {"error": "未知知识点"}
|
|
257
253
|
item.notes.append({"text": text, "ts": datetime.now().isoformat()})
|
|
258
254
|
notes = list(item.notes)
|
|
259
|
-
self._save()
|
|
255
|
+
self._save()
|
|
260
256
|
return {"notes": notes}
|
|
261
257
|
|
|
262
258
|
def due_today(self) -> list:
|
|
@@ -265,7 +261,6 @@ class LearningRuntime:
|
|
|
265
261
|
return [{"item": self.items[s.item_id].to_dict(),
|
|
266
262
|
"review": s.to_dict()} for s in due]
|
|
267
263
|
|
|
268
|
-
# ── 连接:构建图谱 ────────────────────────────────────
|
|
269
264
|
def build_connections(self, semantic_threshold: float = 0.55) -> dict:
|
|
270
265
|
"""自动构建知识连接(同主题 + 语义)。"""
|
|
271
266
|
topic_edges = self.graph.auto_link_by_topic()
|
|
@@ -283,13 +278,11 @@ class LearningRuntime:
|
|
|
283
278
|
"edge_type": etype, "weight": weight, "reason": reason})
|
|
284
279
|
return out
|
|
285
280
|
|
|
286
|
-
# ── 学习策略(对应财务守门人)─────────────────────────
|
|
287
281
|
def strategy_check(self, daily_cap: int = 20) -> list:
|
|
288
282
|
"""学习策略裁决:复习提醒、薄弱主题、负荷预警、连接建议。"""
|
|
289
283
|
verdicts = []
|
|
290
284
|
states = list(self.review_states.values())
|
|
291
285
|
|
|
292
|
-
# 到期复习
|
|
293
286
|
due = self.sr.due_items(states)
|
|
294
287
|
if due:
|
|
295
288
|
verdicts.append(LearningVerdict(
|
|
@@ -297,7 +290,6 @@ class LearningRuntime:
|
|
|
297
290
|
message=f"今天有 {len(due)} 个知识点待复习",
|
|
298
291
|
payload={"count": len(due)}))
|
|
299
292
|
|
|
300
|
-
# 遗忘风险(薄弱项)
|
|
301
293
|
risky = self.sr.forgetting_risk(states)
|
|
302
294
|
if risky:
|
|
303
295
|
verdicts.append(LearningVerdict(
|
|
@@ -305,7 +297,6 @@ class LearningRuntime:
|
|
|
305
297
|
message=f"{len(risky)} 个知识点处于高遗忘风险,建议优先复习",
|
|
306
298
|
payload={"risky": risky[:5]}))
|
|
307
299
|
|
|
308
|
-
# 负荷预警
|
|
309
300
|
load = self.sr.learning_load(states)
|
|
310
301
|
if load["total_due"] > daily_cap:
|
|
311
302
|
verdicts.append(LearningVerdict(
|
|
@@ -313,7 +304,6 @@ class LearningRuntime:
|
|
|
313
304
|
message=f"未来复习量 {load['total_due']} 超过每日上限 {daily_cap},建议分批",
|
|
314
305
|
payload=load))
|
|
315
306
|
|
|
316
|
-
# 孤立知识点(无连接)→ 建议建立关联
|
|
317
307
|
connected = set()
|
|
318
308
|
for e in self.graph.edges:
|
|
319
309
|
connected.add(e.source_id)
|
|
@@ -326,14 +316,12 @@ class LearningRuntime:
|
|
|
326
316
|
payload={"isolated_count": len(isolated)}))
|
|
327
317
|
return verdicts
|
|
328
318
|
|
|
329
|
-
# ── 综合分析(office Agent 核心输出)──────────────────
|
|
330
319
|
def analyze(self) -> dict:
|
|
331
|
-
self._ensure_hkc_loaded()
|
|
320
|
+
self._ensure_hkc_loaded()
|
|
332
321
|
states = list(self.review_states.values())
|
|
333
322
|
masteries = [s.mastery for s in states]
|
|
334
323
|
avg_mastery = round(sum(masteries) / len(masteries), 3) if masteries else 0.0
|
|
335
324
|
|
|
336
|
-
# 按主题统计掌握度
|
|
337
325
|
from collections import defaultdict
|
|
338
326
|
topic_mastery = defaultdict(list)
|
|
339
327
|
for iid, item in self.items.items():
|
|
@@ -346,7 +334,7 @@ class LearningRuntime:
|
|
|
346
334
|
"total_items": len(self.items),
|
|
347
335
|
"avg_mastery": avg_mastery,
|
|
348
336
|
"topic_mastery": topics,
|
|
349
|
-
"weakest_topic": (min(topics, key=topics
|
|
337
|
+
"weakest_topic": (min(topics, key=lambda t: topics[t]) if topics else None),
|
|
350
338
|
"due_today": len(self.sr.due_items(states)),
|
|
351
339
|
"forgetting_risk": self.sr.forgetting_risk(states),
|
|
352
340
|
"learning_load": self.sr.learning_load(states),
|
|
@@ -22,19 +22,16 @@ class SpacedRepetition:
|
|
|
22
22
|
@staticmethod
|
|
23
23
|
def review(state: ReviewState, grade: int) -> ReviewState:
|
|
24
24
|
"""根据本次回忆质量,更新间隔重复状态。返回更新后的 state。"""
|
|
25
|
-
# grade 钳制到 SM-2 定义域 [0,5],防止越界输入让 EF 公式算出无意义值
|
|
26
25
|
grade = max(0, min(5, int(grade)))
|
|
27
26
|
now = datetime.now()
|
|
28
27
|
state.total_reviews += 1
|
|
29
28
|
state.last_review = now.isoformat()
|
|
30
29
|
|
|
31
30
|
if grade < 3:
|
|
32
|
-
# 没记住:重置,短期内再复习
|
|
33
31
|
state.repetitions = 0
|
|
34
32
|
state.interval = 1
|
|
35
33
|
state.lapses += 1
|
|
36
34
|
else:
|
|
37
|
-
# 记住了:按 SM-2 推进间隔
|
|
38
35
|
if state.repetitions == 0:
|
|
39
36
|
state.interval = 1
|
|
40
37
|
elif state.repetitions == 1:
|
|
@@ -43,11 +40,9 @@ class SpacedRepetition:
|
|
|
43
40
|
state.interval = round(state.interval * state.ease_factor)
|
|
44
41
|
state.repetitions += 1
|
|
45
42
|
|
|
46
|
-
# 更新难度因子(SM-2 公式)
|
|
47
43
|
ef = state.ease_factor + (0.1 - (5 - grade) * (0.08 + (5 - grade) * 0.02))
|
|
48
44
|
state.ease_factor = max(1.3, round(ef, 3))
|
|
49
45
|
|
|
50
|
-
# 下次复习时间
|
|
51
46
|
state.due_date = (now + timedelta(days=state.interval)).isoformat()
|
|
52
47
|
return state
|
|
53
48
|
|
|
@@ -79,13 +74,13 @@ class SpacedRepetition:
|
|
|
79
74
|
def learning_load(states: list, days: int = 7) -> dict:
|
|
80
75
|
"""学习负荷(对应财务烧钱速度):未来 N 天的复习量。"""
|
|
81
76
|
from collections import defaultdict
|
|
82
|
-
by_day = defaultdict(int)
|
|
77
|
+
by_day: defaultdict[int, int] = defaultdict(int)
|
|
83
78
|
for s in states:
|
|
84
79
|
d = s.days_until_due
|
|
85
80
|
if 0 <= d <= days:
|
|
86
81
|
by_day[d] += 1
|
|
87
82
|
elif d < 0:
|
|
88
|
-
by_day[0] += 1
|
|
83
|
+
by_day[0] += 1
|
|
89
84
|
total_due = sum(by_day.values())
|
|
90
85
|
return {
|
|
91
86
|
"window_days": days,
|
|
@@ -90,7 +90,6 @@ def build_foxclaw_mcp(os_getter, expose_agents: bool = False) -> FastMCP:
|
|
|
90
90
|
"""os_getter:无参回调,返回当前 Foxclaw 实例(惰性,启动后才有)。
|
|
91
91
|
expose_agents:另开一道开关才暴露 Agent 调度(有副作用面,默认关)。"""
|
|
92
92
|
mcp = FastMCP("Foxclaw Knowledge", stateless_http=True, streamable_http_path="/")
|
|
93
|
-
# 限流(env 可配):读工具较宽松、ask_foxclaw(驱动 LLM,贵)较紧
|
|
94
93
|
read_limiter = _RateLimiter(_env_int("FOXCLAW_MCP_RATE_READ", 120))
|
|
95
94
|
agent_limiter = _RateLimiter(_env_int("FOXCLAW_MCP_RATE_AGENT", 20))
|
|
96
95
|
|
|
@@ -111,7 +110,7 @@ def build_foxclaw_mcp(os_getter, expose_agents: bool = False) -> FastMCP:
|
|
|
111
110
|
"type": str(getattr(h, "ku_type", ""))}
|
|
112
111
|
for h in (hits or [])]
|
|
113
112
|
except Exception:
|
|
114
|
-
return [{"error": "internal error"}]
|
|
113
|
+
return [{"error": "internal error"}]
|
|
115
114
|
|
|
116
115
|
@mcp.tool()
|
|
117
116
|
def knowledge_get(ku_id: str) -> dict:
|
|
@@ -172,7 +171,7 @@ def build_foxclaw_mcp(os_getter, expose_agents: bool = False) -> FastMCP:
|
|
|
172
171
|
|
|
173
172
|
def _work():
|
|
174
173
|
from foxclaw.agents.base import _suppress_memory
|
|
175
|
-
_suppress_memory.set(True)
|
|
174
|
+
_suppress_memory.set(True)
|
|
176
175
|
return os_.scheduler.run(
|
|
177
176
|
lambda: _a.run(os_.run(task, agent)),
|
|
178
177
|
agent_id=agent, task=task)
|