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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""可选的新版本检查(默认关)。
|
|
2
|
+
|
|
3
|
+
为什么默认关:foxclaw 的定位是本地优先 —— 默认外呼会让「断网也能用」多一个
|
|
4
|
+
例外,也给安全审计多一个出网点。**只有** 设置 ``FOXCLAW_UPDATE_CHECK=1`` 时才会
|
|
5
|
+
访问 PyPI,其余情况 ``status()`` 里 ``enabled=False``、``update_available=False``。
|
|
6
|
+
|
|
7
|
+
失败一律静默:查版本是锦上添花,不能因为它让 ``/api/system/info`` 变慢或报错。
|
|
8
|
+
- 结果按 24h 缓存到 ``<FOXCLAW_DATA_DIR>/update_check.json``;
|
|
9
|
+
- 查询在 daemon 线程里跑,不阻塞请求;
|
|
10
|
+
- 查失败时保留上次成功的结果(只是不刷新时间),避免一次网络抖动让提示消失。
|
|
11
|
+
|
|
12
|
+
只发一个 GET ``https://pypi.org/pypi/foxclaw/json``,不带任何本机信息。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import threading
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
_PYPI_URL = "https://pypi.org/pypi/foxclaw/json"
|
|
25
|
+
_TTL = 24 * 3600
|
|
26
|
+
_TIMEOUT = 3.0
|
|
27
|
+
|
|
28
|
+
_lock = threading.Lock()
|
|
29
|
+
_inflight = False
|
|
30
|
+
_last_error: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _enabled() -> bool:
|
|
34
|
+
return os.environ.get("FOXCLAW_UPDATE_CHECK", "").strip().lower() in {"1", "true", "yes", "on"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _current() -> str:
|
|
38
|
+
from foxclaw import __version__
|
|
39
|
+
|
|
40
|
+
return __version__
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _data_dir() -> Path:
|
|
44
|
+
return Path(os.environ.get("FOXCLAW_DATA_DIR") or "./foxclaw_data").expanduser()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _cache_path() -> Path:
|
|
48
|
+
return _data_dir() / "update_check.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def parse_version(value: str) -> tuple[int, ...]:
|
|
52
|
+
"""``"0.1.10"`` → ``(0, 1, 10)``。只取各段的前导数字,预发布/本地后缀丢掉。
|
|
53
|
+
|
|
54
|
+
不引入 ``packaging``:这里只需要「谁更新」,自己比数字段足够,也少一个依赖。
|
|
55
|
+
"""
|
|
56
|
+
parts: list[int] = []
|
|
57
|
+
for chunk in str(value or "").strip().split("."):
|
|
58
|
+
digits = ""
|
|
59
|
+
for ch in chunk:
|
|
60
|
+
if not ch.isdigit():
|
|
61
|
+
break
|
|
62
|
+
digits += ch
|
|
63
|
+
if not digits:
|
|
64
|
+
break
|
|
65
|
+
parts.append(int(digits))
|
|
66
|
+
return tuple(parts)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_newer(latest: str, current: str) -> bool:
|
|
70
|
+
"""latest 是否严格新于 current(逐段比,缺的段补 0)。"""
|
|
71
|
+
left, right = parse_version(latest), parse_version(current)
|
|
72
|
+
width = max(len(left), len(right))
|
|
73
|
+
return left + (0,) * (width - len(left)) > right + (0,) * (width - len(right))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_cache() -> dict[str, Any]:
|
|
77
|
+
try:
|
|
78
|
+
data = json.loads(_cache_path().read_text(encoding="utf-8"))
|
|
79
|
+
except Exception:
|
|
80
|
+
return {}
|
|
81
|
+
return data if isinstance(data, dict) else {}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _write_cache(data: dict[str, Any]) -> None:
|
|
85
|
+
try:
|
|
86
|
+
path = _cache_path()
|
|
87
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
|
89
|
+
except Exception:
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _fetch_latest() -> str:
|
|
94
|
+
import urllib.request
|
|
95
|
+
|
|
96
|
+
req = urllib.request.Request(
|
|
97
|
+
_PYPI_URL,
|
|
98
|
+
headers={"Accept": "application/json", "User-Agent": f"foxclaw/{_current()}"},
|
|
99
|
+
)
|
|
100
|
+
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp: # noqa: S310 - 地址是常量 https
|
|
101
|
+
payload = json.loads(resp.read().decode("utf-8", "replace"))
|
|
102
|
+
version = ((payload or {}).get("info") or {}).get("version") or ""
|
|
103
|
+
if not version:
|
|
104
|
+
raise ValueError("PyPI 响应里没有 info.version")
|
|
105
|
+
return str(version)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _refresh() -> None:
|
|
109
|
+
global _inflight, _last_error
|
|
110
|
+
try:
|
|
111
|
+
latest = _fetch_latest()
|
|
112
|
+
_write_cache({"latest": latest, "checked_at": time.time(), "current": _current()})
|
|
113
|
+
_last_error = None
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
_last_error = f"{type(exc).__name__}: {exc}"
|
|
116
|
+
finally:
|
|
117
|
+
with _lock:
|
|
118
|
+
_inflight = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def maybe_refresh() -> None:
|
|
122
|
+
"""启用且缓存过期 → 后台查一次。幂等、非阻塞,可放心在每个 system/info 里调。"""
|
|
123
|
+
global _inflight
|
|
124
|
+
if not _enabled():
|
|
125
|
+
return
|
|
126
|
+
checked = _read_cache().get("checked_at")
|
|
127
|
+
if isinstance(checked, (int, float)) and time.time() - checked < _TTL:
|
|
128
|
+
return
|
|
129
|
+
with _lock:
|
|
130
|
+
if _inflight:
|
|
131
|
+
return
|
|
132
|
+
_inflight = True
|
|
133
|
+
threading.Thread(target=_refresh, name="foxclaw-update-check", daemon=True).start()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def status() -> dict[str, Any]:
|
|
137
|
+
"""给 ``/api/system/info`` 的只读快照。未启用时 latest 仍是缓存值,但不报可更新。"""
|
|
138
|
+
enabled = _enabled()
|
|
139
|
+
current = _current()
|
|
140
|
+
cache = _read_cache()
|
|
141
|
+
latest = str(cache.get("latest") or "")
|
|
142
|
+
return {
|
|
143
|
+
"enabled": enabled,
|
|
144
|
+
"current": current,
|
|
145
|
+
"latest": latest or None,
|
|
146
|
+
"update_available": bool(enabled and latest and is_newer(latest, current)),
|
|
147
|
+
"checked_at": cache.get("checked_at"),
|
|
148
|
+
"error": _last_error,
|
|
149
|
+
}
|
|
@@ -31,9 +31,8 @@ from pathlib import Path
|
|
|
31
31
|
from types import SimpleNamespace
|
|
32
32
|
|
|
33
33
|
from .shared_write import guard_shared_write
|
|
34
|
+
from typing import Any
|
|
34
35
|
|
|
35
|
-
# Foxclaw 的 type 白名单 → vecRecall 的 memory_type(episodic/semantic/procedural)。
|
|
36
|
-
# 分错不影响读写,但影响 vecRecall 的遗忘曲线与 procedural 精确匹配。
|
|
37
36
|
_FOXCLAW_TO_VR = {
|
|
38
37
|
"concept": "semantic",
|
|
39
38
|
"project": "semantic",
|
|
@@ -52,7 +51,6 @@ _VR_TO_FOXCLAW = {
|
|
|
52
51
|
}
|
|
53
52
|
_VALID_TYPES = set(_FOXCLAW_TO_VR)
|
|
54
53
|
|
|
55
|
-
# 偏好类记忆走 topic_key(精确查询),因为"总是注入的偏好"不该做语义模糊匹配。
|
|
56
54
|
_PREFERENCE_TAG = "preference"
|
|
57
55
|
|
|
58
56
|
|
|
@@ -77,7 +75,7 @@ class _HKCSharedEmbedding:
|
|
|
77
75
|
def __init__(self, model_name: str):
|
|
78
76
|
self.model_name = model_name
|
|
79
77
|
self._model = None
|
|
80
|
-
self._dim = None
|
|
78
|
+
self._dim: int | None = None
|
|
81
79
|
|
|
82
80
|
def _load(self):
|
|
83
81
|
if self._model is None:
|
|
@@ -85,8 +83,6 @@ class _HKCSharedEmbedding:
|
|
|
85
83
|
from hkc_search.embedding_backends import get_st_model
|
|
86
84
|
model = get_st_model(self.model_name)
|
|
87
85
|
if model is None:
|
|
88
|
-
# 不回退到直连 SentenceTransformer:那会绕过 HKC 的进程级共享缓存、
|
|
89
|
-
# 多加载一份模型(bge-m3 常驻约 1GB)。失败就明确报错让工厂回退 HMR。
|
|
90
86
|
raise RuntimeError(
|
|
91
87
|
f"embedding 模型不可用: {self.model_name}(HKC 共享缓存加载失败)")
|
|
92
88
|
self._model = model
|
|
@@ -105,7 +101,6 @@ class _HKCSharedEmbedding:
|
|
|
105
101
|
if self._dim is None:
|
|
106
102
|
try:
|
|
107
103
|
model = self._load()
|
|
108
|
-
# ST 6.x 把 get_sentence_embedding_dimension 改名了,两个都试
|
|
109
104
|
getter = (getattr(model, "get_embedding_dimension", None)
|
|
110
105
|
or getattr(model, "get_sentence_embedding_dimension", None))
|
|
111
106
|
self._dim = (int(getter() or 0) or None) if getter else None
|
|
@@ -120,7 +115,7 @@ class VecRecallAdapter:
|
|
|
120
115
|
backend = "vecRecall"
|
|
121
116
|
|
|
122
117
|
def __init__(self, storage_path: str = "./vecrecall_data", wing: str = "foxclaw",
|
|
123
|
-
use_chain: bool = False, embedding_model: str = None,
|
|
118
|
+
use_chain: bool = False, embedding_model: str | None = None,
|
|
124
119
|
dedup: bool = True, embedding_backend=None, vector_backend=None):
|
|
125
120
|
"""`embedding_backend` / `vector_backend` 用于注入(测试传轻量实现)。
|
|
126
121
|
|
|
@@ -133,9 +128,6 @@ class VecRecallAdapter:
|
|
|
133
128
|
self.wing = wing
|
|
134
129
|
self.storage_path = str(storage_path)
|
|
135
130
|
self.version = self._resolve_version()
|
|
136
|
-
# vecRecall 本体没有任何锁(只有 REST server 层有 RLock),而它用的是
|
|
137
|
-
# check_same_thread=False 的单条 sqlite 连接 + 内存 dict 索引。并行子 Agent
|
|
138
|
-
# 同时 add/search 属于未定义行为,和 HMR 是同一类坑 —— 这里沿用同一套串行化。
|
|
139
131
|
self._lock = threading.RLock()
|
|
140
132
|
|
|
141
133
|
self._emb = embedding_backend or _HKCSharedEmbedding(
|
|
@@ -152,7 +144,6 @@ class VecRecallAdapter:
|
|
|
152
144
|
**extra,
|
|
153
145
|
)
|
|
154
146
|
|
|
155
|
-
# ── 构造辅助 ──────────────────────────────────────────
|
|
156
147
|
@staticmethod
|
|
157
148
|
def _resolve_version() -> str:
|
|
158
149
|
try:
|
|
@@ -162,7 +153,7 @@ class VecRecallAdapter:
|
|
|
162
153
|
return "unknown"
|
|
163
154
|
|
|
164
155
|
@staticmethod
|
|
165
|
-
def _resolve_model(embedding_model: str = None) -> str:
|
|
156
|
+
def _resolve_model(embedding_model: str | None = None) -> str:
|
|
166
157
|
"""与 HMRAdapter 同一条解析链,保证两套后端用同一个模型(分数与维度可比)。"""
|
|
167
158
|
return (embedding_model
|
|
168
159
|
or os.environ.get("EMBEDDING_MODEL")
|
|
@@ -188,14 +179,13 @@ class VecRecallAdapter:
|
|
|
188
179
|
except Exception:
|
|
189
180
|
return
|
|
190
181
|
dims = {int(r[0]) for r in rows}
|
|
191
|
-
want = getattr(self._emb, "dim", None)
|
|
182
|
+
want = getattr(self._emb, "dim", None)
|
|
192
183
|
if dims and want and dims != {int(want)}:
|
|
193
184
|
raise RuntimeError(
|
|
194
185
|
f"已有向量维度 {sorted(dims)} 与当前嵌入维度 {want} 不一致。"
|
|
195
186
|
f"换 embedding 模型必须重建索引:删除 {self.storage_path} 后重启"
|
|
196
187
|
)
|
|
197
188
|
|
|
198
|
-
# ── 摄入 ──────────────────────────────────────────────
|
|
199
189
|
_AUTO_TITLE = re.compile(r"^[A-Za-z_]+\s+\d{1,2}:\d{2}$")
|
|
200
190
|
|
|
201
191
|
@staticmethod
|
|
@@ -214,8 +204,8 @@ class VecRecallAdapter:
|
|
|
214
204
|
s = re.split(r"[。!?!?\n\r]", s, 1)[0].strip()
|
|
215
205
|
return " ".join(s.split())[:80]
|
|
216
206
|
|
|
217
|
-
def ingest(self, content: str, type: str = "concept", agent_id: str = None,
|
|
218
|
-
title: str = None, tags: list = None, shared: bool = False, **kwargs):
|
|
207
|
+
def ingest(self, content: str, type: str = "concept", agent_id: str | None = None,
|
|
208
|
+
title: str | None = None, tags: list | None = None, shared: bool = False, **kwargs):
|
|
219
209
|
"""统一摄入。签名与 `HMRAdapter.ingest` 一致。
|
|
220
210
|
|
|
221
211
|
隔离承载:`agent_id` → owner,`shared=True` → visibility="shared"。
|
|
@@ -231,8 +221,6 @@ class VecRecallAdapter:
|
|
|
231
221
|
if not title:
|
|
232
222
|
title = self._derive_title(content) or ""
|
|
233
223
|
|
|
234
|
-
# 【P0-3】共享层写入闸门:必须在落库之前判定,语义与 HMR 路径完全一致,
|
|
235
|
-
# 否则同一个策略在两个后端上表现不同。
|
|
236
224
|
if shared:
|
|
237
225
|
guard_shared_write(self, agent_id, content, mtype, meta_tags)
|
|
238
226
|
|
|
@@ -240,8 +228,6 @@ class VecRecallAdapter:
|
|
|
240
228
|
if "confidence" in kwargs:
|
|
241
229
|
metadata["confidence"] = kwargs["confidence"]
|
|
242
230
|
|
|
243
|
-
# execution 是审计证据:被遗忘曲线衰减或被去重合并,就无法再证明"当时确实
|
|
244
|
-
# 这么做了"。vecRecall 的 immutable 同时免于衰减与去重,正好对上这个语义。
|
|
245
231
|
retention = "immutable" if mtype == "execution" else "decay"
|
|
246
232
|
topic_key = kwargs.get("topic_key") or (
|
|
247
233
|
_PREFERENCE_TAG if _PREFERENCE_TAG in meta_tags else "")
|
|
@@ -260,8 +246,7 @@ class VecRecallAdapter:
|
|
|
260
246
|
topic_key=topic_key,
|
|
261
247
|
)
|
|
262
248
|
|
|
263
|
-
|
|
264
|
-
def _visible(self, mem, agent_id: str, include_shared: bool) -> bool:
|
|
249
|
+
def _visible(self, mem, agent_id: str | None, include_shared: bool) -> bool:
|
|
265
250
|
"""可见性判定,与 vecRecall `_visible_to` 规则保持同源。
|
|
266
251
|
|
|
267
252
|
- shared 记忆:仅当 include_shared 时可见
|
|
@@ -276,7 +261,7 @@ class VecRecallAdapter:
|
|
|
276
261
|
return include_shared
|
|
277
262
|
return owner == agent_id or not owner
|
|
278
263
|
|
|
279
|
-
def recall(self, query: str = "", agent_id: str = None, top_k: int = 5,
|
|
264
|
+
def recall(self, query: str = "", agent_id: str | None = None, top_k: int = 5,
|
|
280
265
|
include_shared: bool = True, **kwargs):
|
|
281
266
|
"""统一召回,返回 list[dict]。
|
|
282
267
|
|
|
@@ -300,9 +285,6 @@ class VecRecallAdapter:
|
|
|
300
285
|
results = list(getattr(bundle, "l2_topic_context", []) or []) + \
|
|
301
286
|
list(getattr(bundle, "l3_deep_results", []) or [])
|
|
302
287
|
if not results and (query or "").strip():
|
|
303
|
-
# L2 有 0.55 阈值:命中偏弱时整层为空。HMR 的 recall 是无条件
|
|
304
|
-
# 返回 top_k,为不改变上层"总能拿到几条"的预期,退化到全量语义
|
|
305
|
-
# 检索,再在下面用 _visible 补上隔离(search 本身不隔离)。
|
|
306
288
|
results = list(self._vr.search(query, n=fetch_k) or [])
|
|
307
289
|
except Exception:
|
|
308
290
|
return []
|
|
@@ -384,8 +366,7 @@ class VecRecallAdapter:
|
|
|
384
366
|
row["_ts"] = getattr(m, "timestamp", "") or ""
|
|
385
367
|
rows.append(row)
|
|
386
368
|
|
|
387
|
-
rows.sort(key=lambda x: str(x.get("_ts")), reverse=True)
|
|
388
|
-
# 同一偏好反复表达会存成多条,去重保留最新,否则会挤满 limit
|
|
369
|
+
rows.sort(key=lambda x: str(x.get("_ts")), reverse=True)
|
|
389
370
|
deduped, seen_c = [], set()
|
|
390
371
|
for x in rows:
|
|
391
372
|
if x["content"] in seen_c:
|
|
@@ -401,7 +382,6 @@ class VecRecallAdapter:
|
|
|
401
382
|
except Exception:
|
|
402
383
|
return False
|
|
403
384
|
|
|
404
|
-
# ── 展示 ──────────────────────────────────────────────
|
|
405
385
|
def recent(self, limit: int = 20) -> list:
|
|
406
386
|
"""最近的记忆条目(新→旧),供前端右栏"记忆信息条"。"""
|
|
407
387
|
rows = []
|
|
@@ -435,11 +415,6 @@ class VecRecallAdapter:
|
|
|
435
415
|
})
|
|
436
416
|
return rows
|
|
437
417
|
|
|
438
|
-
# ── HMR 专有接口的占位 ────────────────────────────────
|
|
439
|
-
# vecRecall 没有"会话工作区 / 运行时快照"这两个概念。留占位而不是让调用方
|
|
440
|
-
# AttributeError:上层有统一的"后端接口"假设,缺方法会炸在一个与被调用能力
|
|
441
|
-
# 无关的地方。agents/base.py 取到 workspace 后并不读其字段(active_goal 的
|
|
442
|
-
# 写入已移除),所以空壳没有实际成本。
|
|
443
418
|
def get_workspace(self, agent_id: str):
|
|
444
419
|
return SimpleNamespace(agent_id=agent_id, active_goal="", status="idle",
|
|
445
420
|
push_task=lambda _t: None)
|
|
@@ -453,14 +428,13 @@ class VecRecallAdapter:
|
|
|
453
428
|
def restore_runtime_state(self):
|
|
454
429
|
return None
|
|
455
430
|
|
|
456
|
-
# ── 状态 ──────────────────────────────────────────────
|
|
457
431
|
def get_system_status(self):
|
|
458
432
|
return {"backend": self.backend, "version": self.version, "wing": self.wing}
|
|
459
433
|
|
|
460
434
|
def stats(self) -> dict:
|
|
461
435
|
"""记忆统计:前端记忆中心展示真实数字。字段与 HMRAdapter.stats 一致。"""
|
|
462
436
|
total = shared_count = relations = 0
|
|
463
|
-
by_agent = {}
|
|
437
|
+
by_agent: dict[str, Any] = {}
|
|
464
438
|
embedding = type(self._emb).__name__
|
|
465
439
|
try:
|
|
466
440
|
with self._lock:
|
|
@@ -25,17 +25,15 @@ from dataclasses import dataclass, field
|
|
|
25
25
|
from urllib.parse import urlparse
|
|
26
26
|
|
|
27
27
|
from . import failure_policy as _policy
|
|
28
|
+
from typing import Callable
|
|
28
29
|
|
|
29
30
|
VERIFIED = "verified"
|
|
30
31
|
UNVERIFIED = "unverified"
|
|
31
32
|
FAILED = "failed"
|
|
32
33
|
|
|
33
|
-
#: 每条工具结果最多挂几条证据(再多就成了噪声,用户不会看)
|
|
34
34
|
MAX_EVIDENCE = 8
|
|
35
35
|
|
|
36
36
|
_URL_RE = re.compile(r"https?://[^\s<>\"'))\]}]+")
|
|
37
|
-
#: 只保留"真实存在"的路径 —— 正则从文本里认出来的路径九成是误报(URL 片段、示例路径),
|
|
38
|
-
#: 只有能在磁盘上 stat 到的才算证据。这不是限制,是"证据"二字的含义。
|
|
39
37
|
_WIN_PATH_RE = re.compile(r"[A-Za-z]:[\\/][^\s\"'<>|*?,。;]+")
|
|
40
38
|
_POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
|
|
41
39
|
|
|
@@ -44,10 +42,10 @@ _POSIX_PATH_RE = re.compile(r"(?<![\w:])/(?:[\w.\-]+/)+[\w.\-]+")
|
|
|
44
42
|
class Evidence:
|
|
45
43
|
"""一条可核对的凭据:一个链接、一个真实存在的文件、或一次工具的原始命中。"""
|
|
46
44
|
|
|
47
|
-
kind: str
|
|
48
|
-
ref: str
|
|
49
|
-
label: str
|
|
50
|
-
source: str
|
|
45
|
+
kind: str
|
|
46
|
+
ref: str
|
|
47
|
+
label: str
|
|
48
|
+
source: str
|
|
51
49
|
at: float = field(default_factory=time.time)
|
|
52
50
|
|
|
53
51
|
def to_dict(self) -> dict:
|
|
@@ -121,14 +119,10 @@ def extract_evidence(source: str, text: str, limit: int = MAX_EVIDENCE) -> list[
|
|
|
121
119
|
return out
|
|
122
120
|
|
|
123
121
|
|
|
124
|
-
|
|
125
|
-
#: 没登记的工具走通用规则(见 verify),不会被当成"已验证"。
|
|
126
|
-
_CHECKERS: dict[str, "callable"] = {
|
|
127
|
-
# 联网搜索:结论必须能指回来源,否则就是"模型自己写的" —— 标未核验而不是验证通过
|
|
122
|
+
_CHECKERS: dict[str, Callable[..., tuple[bool, str]]] = {
|
|
128
123
|
"web_search": lambda args, text: (
|
|
129
124
|
(True, "") if _URL_RE.search(text or "")
|
|
130
125
|
else (False, "搜索结果没有可核对的来源链接")),
|
|
131
|
-
# 个人记忆:召回为空是合法结果(确实没记过),非空即有凭据
|
|
132
126
|
"recall_memory": lambda args, text: (
|
|
133
127
|
(True, "") if (text or "").strip().startswith("【召回的相关记忆】")
|
|
134
128
|
else (False, "未返回记忆条目")),
|
|
@@ -157,9 +151,7 @@ def verify(name: str, args: dict | None, text: str,
|
|
|
157
151
|
|
|
158
152
|
read_only = bool((capability or {}).get("read_only"))
|
|
159
153
|
if read_only:
|
|
160
|
-
# 只读 + 有返回 = 这一步确实发生过(内容是否可信由污点/证据层管)
|
|
161
154
|
return Verification(VERIFIED, evidence=ev)
|
|
162
|
-
# 有副作用但没登记后置条件:不能证明效果已发生 → 未核验(不许声称完成)
|
|
163
155
|
return Verification(UNVERIFIED, reason="该工具未声明可观察的后置条件", evidence=ev)
|
|
164
156
|
|
|
165
157
|
|
|
@@ -23,40 +23,23 @@ import urllib.error
|
|
|
23
23
|
from . import settings
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
# 触发联网搜索的关键词:问到实时/时效性信息时才搜
|
|
27
|
-
# ── 是否需要联网:话题信号(强) vs 时间词(弱) vs 个人数据(否决)──────────────
|
|
28
|
-
# 教训:曾把纯时间词(今天/本月/最近…)也当触发,结果「分析一下本月支出」这类**问自己账本**
|
|
29
|
-
# 的任务被误判为需要联网 → 白跑一次 ~22s 的付费深度搜索。而 Foxclaw 的领域(个人财务/
|
|
30
|
-
# 行程/学习)天生满是时间词,这不是个别误判,是**一整类合法任务**在白交时间和钱。
|
|
31
|
-
# 现在:时间词单独**不足以**触发;必须出现"外部世界话题";且个人数据一律否决。
|
|
32
|
-
# (与既有的 now⊂knowledge / current⊂concurrent 词边界修复同源:都是过度触发。)
|
|
33
|
-
|
|
34
|
-
# 强触发:出现即说明要的是外部实时信息
|
|
35
26
|
TOPIC_TRIGGERS = [
|
|
36
27
|
"版本", "几个版本", "第几", "价格", "多少钱", "汇率", "股价", "新闻",
|
|
37
28
|
"发布", "上线", "更新到", "迭代", "排行", "排名", "天气",
|
|
38
|
-
# 问"某产品/技术的模型/型号/出没出/更没更"——都是外部时效信息,过去漏搜→编造。
|
|
39
|
-
# 只留明确指向外部产品的词;"有哪些/可用/支持"太宽(会误伤"我有哪些任务"),不加。
|
|
40
29
|
"模型", "型号", "出了没", "发布没", "更新没", "新版",
|
|
41
30
|
"latest", "news", "version", "price", "models",
|
|
42
31
|
]
|
|
43
|
-
# 弱信号:纯时间词。单独出现不触发(个人数据问题天生带这些词)
|
|
44
32
|
TIME_WORDS = [
|
|
45
33
|
"最新", "现在", "目前", "今天", "今日", "昨天", "本周", "本月", "今年",
|
|
46
34
|
"最近", "当前", "today", "current", "now",
|
|
47
35
|
]
|
|
48
|
-
# 否决:问的是用户自己的账/行程/学习/记忆 —— 这些答案在本地领域内核里,不在网上。
|
|
49
|
-
# 注意别放"我的"这类过宽的词:「我的股票现在多少钱」是真需要联网的。
|
|
50
36
|
PERSONAL_MARKERS = [
|
|
51
37
|
"支出", "收入", "预算", "交易", "账单", "花了", "消费", "记账", "结余", "余额",
|
|
52
38
|
"行程", "日程", "出行", "复习", "知识点", "学习计划", "记忆",
|
|
53
39
|
]
|
|
54
40
|
|
|
55
|
-
SEARCH_TRIGGERS = TOPIC_TRIGGERS
|
|
41
|
+
SEARCH_TRIGGERS = TOPIC_TRIGGERS
|
|
56
42
|
|
|
57
|
-
# 中文触发词按「子串」匹配(中文无词边界);英文触发词按「词边界」匹配,
|
|
58
|
-
# 否则短词会误命中常见词:"now"⊂"knowledge"、"current"⊂"concurrent"、
|
|
59
|
-
# "now"⊂"known" —— 在这个满口 knowledge 的项目里会频繁无谓地打百度。
|
|
60
43
|
_CJK_TRIGGERS = [k for k in SEARCH_TRIGGERS if not k.isascii()]
|
|
61
44
|
_ASCII_TRIGGERS = [k.lower() for k in SEARCH_TRIGGERS if k.isascii()]
|
|
62
45
|
_ASCII_RE = (
|
|
@@ -85,16 +68,12 @@ def needs_search(query: str) -> bool:
|
|
|
85
68
|
return False
|
|
86
69
|
q = query.lower()
|
|
87
70
|
if is_personal_query(q):
|
|
88
|
-
return False
|
|
89
|
-
# 自我模型/身份问题(你是什么模型 / 你现在是哪个模型 …)→ 由 ModelRouter 运行时注入本地即答,不联网。
|
|
90
|
-
# 否则会因含"模型"触发词白跑一次 ~20s 百度搜索。"你"与"模型"相邻(≤8字)才算自问;
|
|
91
|
-
# "你帮我查 X 的模型"(相距远)仍走搜索。
|
|
71
|
+
return False
|
|
92
72
|
if re.search(r"你[^,。?!]{0,8}模型", q):
|
|
93
73
|
return False
|
|
94
74
|
if any(k in q for k in _CJK_TRIGGERS):
|
|
95
|
-
return True
|
|
75
|
+
return True
|
|
96
76
|
return bool(_ASCII_RE and _ASCII_RE.search(q))
|
|
97
|
-
# 注:只含时间词(今天/本月/最近…)而无话题 → 不触发(见上方 TIME_WORDS 说明)
|
|
98
77
|
|
|
99
78
|
|
|
100
79
|
def _key() -> str:
|
|
@@ -139,7 +118,7 @@ def _search_answer_inner(query: str, key: str) -> dict:
|
|
|
139
118
|
"stream": False,
|
|
140
119
|
"model": _model(),
|
|
141
120
|
"enable_deep_search": _deep(),
|
|
142
|
-
"enable_corner_markers": False,
|
|
121
|
+
"enable_corner_markers": False,
|
|
143
122
|
}).encode("utf-8")
|
|
144
123
|
req = urllib.request.Request(
|
|
145
124
|
f"{_base()}/chat/completions",
|
|
@@ -193,7 +172,6 @@ def format_for_prompt(query: str, payload) -> str:
|
|
|
193
172
|
refs = payload.get("references", [])
|
|
194
173
|
if not answer and not refs:
|
|
195
174
|
return ""
|
|
196
|
-
# 我方的作答指令(可信,放在 <untrusted> 之外);检索到的外部内容(不可信,放在 <untrusted> 之内)。
|
|
197
175
|
framing = (
|
|
198
176
|
f"以下是针对「{query}」的实时联网搜索结果(来自百度 AI 搜索)。\n"
|
|
199
177
|
f"【严格要求】只能依据下面 <untrusted> 数据块里的搜索结果作答:结果里没有出现的型号 / "
|
|
@@ -215,13 +193,11 @@ def format_for_prompt(query: str, payload) -> str:
|
|
|
215
193
|
if u:
|
|
216
194
|
inner += f"\n链接:{u}"
|
|
217
195
|
inner += f"\n[webpage {i} end]\n"
|
|
218
|
-
# 隔离外部内容:搜索结果是外部不可控数据,包裹进 <untrusted> 防间接注入(见 injection_guard)。
|
|
219
196
|
from . import injection_guard as _ig
|
|
220
197
|
block = _ig.wrap_untrusted(inner.rstrip(), source="web_search")
|
|
221
198
|
closing = ("\n请**严格基于以上 <untrusted> 内的搜索结果**回答用户的问题;结果里没提到的内容,"
|
|
222
199
|
"就明确说「搜索结果中未提及」,绝不编造型号/版本号。")
|
|
223
200
|
return framing + block + closing
|
|
224
|
-
# 列表形式(兼容)
|
|
225
201
|
results = payload or []
|
|
226
202
|
if not results:
|
|
227
203
|
return ""
|
|
@@ -17,9 +17,9 @@ from datetime import datetime
|
|
|
17
17
|
@dataclass
|
|
18
18
|
class RiskFactor:
|
|
19
19
|
"""单个风险因子:是什么、证据、权重。"""
|
|
20
|
-
factor: str
|
|
21
|
-
evidence: str
|
|
22
|
-
weight: float
|
|
20
|
+
factor: str
|
|
21
|
+
evidence: str
|
|
22
|
+
weight: float
|
|
23
23
|
|
|
24
24
|
def to_dict(self) -> dict:
|
|
25
25
|
return {"factor": self.factor, "evidence": self.evidence,
|
|
@@ -29,14 +29,14 @@ class RiskFactor:
|
|
|
29
29
|
@dataclass
|
|
30
30
|
class EvidenceChain:
|
|
31
31
|
"""结构化决策证据链 —— 财务决策的可审计输出。"""
|
|
32
|
-
decision_type: str
|
|
33
|
-
risk_score: float = 0.0
|
|
34
|
-
risk_factors: list = field(default_factory=list)
|
|
35
|
-
mitigating_factors: list = field(default_factory=list)
|
|
36
|
-
rationale: str = ""
|
|
37
|
-
recommended_action: str = ""
|
|
38
|
-
confidence: float = 0.0
|
|
39
|
-
additional_checks: list = field(default_factory=list)
|
|
32
|
+
decision_type: str
|
|
33
|
+
risk_score: float = 0.0
|
|
34
|
+
risk_factors: list = field(default_factory=list)
|
|
35
|
+
mitigating_factors: list = field(default_factory=list)
|
|
36
|
+
rationale: str = ""
|
|
37
|
+
recommended_action: str = ""
|
|
38
|
+
confidence: float = 0.0
|
|
39
|
+
additional_checks: list = field(default_factory=list)
|
|
40
40
|
created_at: str = field(default_factory=lambda: datetime.now().isoformat(timespec="seconds"))
|
|
41
41
|
|
|
42
42
|
def add_factor(self, factor: str, evidence: str, weight: float):
|
|
@@ -53,10 +53,10 @@ class EvidenceChain:
|
|
|
53
53
|
self.risk_score = 0.0
|
|
54
54
|
return 0.0
|
|
55
55
|
weights = sorted((f.weight for f in self.risk_factors), reverse=True)
|
|
56
|
-
score = weights[0]
|
|
56
|
+
score = weights[0]
|
|
57
57
|
for w in weights[1:]:
|
|
58
|
-
score += w * (1 - score) * 0.5
|
|
59
|
-
mitigation = min(0.2, 0.1 * len(self.mitigating_factors))
|
|
58
|
+
score += w * (1 - score) * 0.5
|
|
59
|
+
mitigation = min(0.2, 0.1 * len(self.mitigating_factors))
|
|
60
60
|
self.risk_score = max(0.0, min(1.0, round(score - mitigation, 3)))
|
|
61
61
|
return self.risk_score
|
|
62
62
|
|
|
@@ -100,7 +100,6 @@ class FinanceExplainer:
|
|
|
100
100
|
cat = b.get("category", "?")
|
|
101
101
|
if ratio >= 1.0:
|
|
102
102
|
over = b.get("spent", 0) - b.get("monthly_limit", 0)
|
|
103
|
-
# 权重随超支程度递增:超10%→0.5,超50%→0.65,超100%+→0.8(上限)
|
|
104
103
|
w = min(0.8, 0.45 + (ratio - 1.0) * 0.35)
|
|
105
104
|
ec.add_factor("超预算", f"{cat} 已用 {ratio*100:.0f}%,超支 ¥{over:.0f}",
|
|
106
105
|
weight=w)
|
|
@@ -109,13 +108,11 @@ class FinanceExplainer:
|
|
|
109
108
|
ec.add_factor("接近预算上限",
|
|
110
109
|
f"{cat} 已用 {ratio*100:.0f}%,接近上限", weight=0.25)
|
|
111
110
|
near_budgets.append(cat)
|
|
112
|
-
# 缓解因素:有预算分类未超支
|
|
113
111
|
healthy = [b.get("category") for b in budget_status
|
|
114
112
|
if b.get("usage_ratio", 0) < 0.8]
|
|
115
113
|
if healthy:
|
|
116
114
|
ec.mitigating_factors.append(f"{len(healthy)} 个分类预算健康")
|
|
117
115
|
ec.compute_risk()
|
|
118
|
-
# 理由 + 建议
|
|
119
116
|
if over_budgets:
|
|
120
117
|
ec.rationale = f"{','.join(over_budgets)} 已超预算,需立即关注"
|
|
121
118
|
ec.recommended_action = f"削减 {over_budgets[0]} 支出或调整预算"
|
|
@@ -134,7 +131,6 @@ class FinanceExplainer:
|
|
|
134
131
|
ec = EvidenceChain(decision_type="anomaly", confidence=0.85)
|
|
135
132
|
for a in anomalies[:5]:
|
|
136
133
|
z = a.get("z_score", 0)
|
|
137
|
-
# z 分数越高,权重越大(裁剪到 0.6)
|
|
138
134
|
w = min(0.6, 0.15 + z * 0.05)
|
|
139
135
|
ec.add_factor("异常大额支出",
|
|
140
136
|
f"{a.get('entity','?')} ¥{abs(a.get('amount',0)):.0f}"
|
|
@@ -15,7 +15,6 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
BASE_CURRENCY = "CNY"
|
|
17
17
|
|
|
18
|
-
# 参考价(1 单位外币 = 多少 CNY)。宁可粗,但要能改。
|
|
19
18
|
DEFAULT_RATES = {
|
|
20
19
|
"CNY": 1.0,
|
|
21
20
|
"USD": 7.2,
|
|
@@ -33,7 +32,7 @@ DEFAULT_RATES = {
|
|
|
33
32
|
class FxTable:
|
|
34
33
|
"""币种 → 本位币折算率,带落盘(原子写)。"""
|
|
35
34
|
|
|
36
|
-
def __init__(self, path: str = None):
|
|
35
|
+
def __init__(self, path: str | None = None):
|
|
37
36
|
self.path = Path(path) if path else None
|
|
38
37
|
self.rates = dict(DEFAULT_RATES)
|
|
39
38
|
self.note = "static"
|
|
@@ -54,7 +53,6 @@ class FxTable:
|
|
|
54
53
|
if data.get("note"):
|
|
55
54
|
self.note = data["note"]
|
|
56
55
|
except Exception:
|
|
57
|
-
# 汇率表坏了不能拖垮记账:退回出厂参考价,下次保存时自动覆盖
|
|
58
56
|
pass
|
|
59
57
|
|
|
60
58
|
def save(self) -> bool:
|