@agentikos/omega-os 0.1.0 → 0.19.5
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 +56 -14
- package/bootstrap/lib/__pycache__/claude-code-settings.cpython-313.pyc +0 -0
- package/bootstrap/lib/__pycache__/llm-clis.cpython-313.pyc +0 -0
- package/bootstrap/lib/__pycache__/manifest-helpers.cpython-313.pyc +0 -0
- package/bootstrap/lib/claude-code-settings.py +176 -0
- package/bootstrap/lib/common.sh +457 -1
- package/bootstrap/lib/llm-clis.py +341 -0
- package/bootstrap/lib/manifest-helpers.py +384 -0
- package/bootstrap/lib/steps.sh +1000 -26
- package/bootstrap/manifest.example.yaml +93 -2
- package/bootstrap/templates/aisb/CLAUDE.md +305 -0
- package/bootstrap/templates/aisb/architect.md +204 -0
- package/bootstrap/templates/aisb/checkers/CLAUDE.md +9 -0
- package/bootstrap/templates/aisb/checkers/checker-architect.md +151 -0
- package/bootstrap/templates/aisb/checkers/checker-common.md +171 -0
- package/bootstrap/templates/aisb/checkers/checker-construct.md +129 -0
- package/bootstrap/templates/aisb/checkers/checker-keymaker.md +204 -0
- package/bootstrap/templates/aisb/checkers/checker-link.md +205 -0
- package/bootstrap/templates/aisb/checkers/checker-merovingian.md +219 -0
- package/bootstrap/templates/aisb/checkers/checker-morpheus.md +211 -0
- package/bootstrap/templates/aisb/checkers/checker-neo.md +177 -0
- package/bootstrap/templates/aisb/checkers/checker-niobe.md +156 -0
- package/bootstrap/templates/aisb/checkers/checker-oracle.md +164 -0
- package/bootstrap/templates/aisb/checkers/checker-seraph.md +187 -0
- package/bootstrap/templates/aisb/checkers/checker-smith.md +195 -0
- package/bootstrap/templates/aisb/checkers/checker-zion.md +113 -0
- package/bootstrap/templates/aisb/construct.md +135 -0
- package/bootstrap/templates/aisb/keymaker.md +227 -0
- package/bootstrap/templates/aisb/link.md +170 -0
- package/bootstrap/templates/aisb/lmc-protocol.md +57 -0
- package/bootstrap/templates/aisb/merovingian.md +159 -0
- package/bootstrap/templates/aisb/morpheus.md +243 -0
- package/bootstrap/templates/aisb/neo.md +147 -0
- package/bootstrap/templates/aisb/niobe.md +197 -0
- package/bootstrap/templates/aisb/oracle.md +244 -0
- package/bootstrap/templates/aisb/protocols/handoff-templates.md +204 -0
- package/bootstrap/templates/aisb/protocols/shared-protocol.md +248 -0
- package/bootstrap/templates/aisb/pythia.md +153 -0
- package/bootstrap/templates/aisb/seraph.md +315 -0
- package/bootstrap/templates/aisb/smith.md +202 -0
- package/bootstrap/templates/aisb/zion.md +172 -0
- package/bootstrap/templates/autonomous/audit-patrol.yaml +41 -0
- package/bootstrap/templates/autonomous/smith-reflect.yaml +43 -0
- package/bootstrap/templates/autonomous/ssh-key-rotate.yaml +46 -0
- package/bootstrap/templates/autonomous/support-agent.yaml +38 -0
- package/docs/AUDITS.md +85 -0
- package/docs/COMPLETION-PLAN.md +48 -0
- package/docs/GAP-ANALYSIS.md +214 -0
- package/docs/INSTALL.md +47 -9
- package/docs/MCP-AND-PLUGINS.md +31 -4
- package/docs/SIMULATION.md +171 -0
- package/docs/simulate.sh +211 -0
- package/install.sh +164 -17
- package/omega/Agentik_Engine/README.md +27 -10
- package/omega/Agentik_Engine/omega_engine/__init__.py +212 -2
- package/omega/Agentik_Engine/omega_engine/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/account.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/agent_messages.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/aisb_chat.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit_diff.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/audit_gate.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/auto_update.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/autonomous.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/backup.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cadence.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/classifier.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cleanup.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/cli.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/completions.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/costs.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/done_signal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/envelope.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/executor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/handoff.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/hermes.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/hermes_bootstrap.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/hermes_desktop.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/learning.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/managed_agent.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/memory.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/menu.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/mission.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/plan.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/project.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/prompts.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/provider.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/prune.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/pursue.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/reducer.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/router.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/skill_routing.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/smoke.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/store.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/sync.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/telegram_history.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/tmux.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/tools.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/understand_anything.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/updater.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/validate.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/vault.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/webhooks.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/__pycache__/worker.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/account.py +502 -0
- package/omega/Agentik_Engine/omega_engine/agent_messages.py +167 -0
- package/omega/Agentik_Engine/omega_engine/aisb_chat.py +128 -0
- package/omega/Agentik_Engine/omega_engine/audit_diff.py +99 -0
- package/omega/Agentik_Engine/omega_engine/audit_gate.py +149 -0
- package/omega/Agentik_Engine/omega_engine/audits/__init__.py +60 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/batcher.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/dispatcher.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/generator.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/history.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/__pycache__/pipeline.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/audits/batcher.py +218 -0
- package/omega/Agentik_Engine/omega_engine/audits/dispatcher.py +92 -0
- package/omega/Agentik_Engine/omega_engine/audits/generator.py +234 -0
- package/omega/Agentik_Engine/omega_engine/audits/history.py +168 -0
- package/omega/Agentik_Engine/omega_engine/audits/pipeline.py +198 -0
- package/omega/Agentik_Engine/omega_engine/auto_update.py +339 -0
- package/omega/Agentik_Engine/omega_engine/autonomous.py +538 -0
- package/omega/Agentik_Engine/omega_engine/backup.py +215 -0
- package/omega/Agentik_Engine/omega_engine/cadence.py +158 -0
- package/omega/Agentik_Engine/omega_engine/classifier.py +215 -0
- package/omega/Agentik_Engine/omega_engine/cleanup.py +673 -0
- package/omega/Agentik_Engine/omega_engine/cli.py +4564 -56
- package/omega/Agentik_Engine/omega_engine/completions.py +260 -0
- package/omega/Agentik_Engine/omega_engine/costs.py +100 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__init__.py +14 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/autonomous.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/engine.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/__pycache__/telegram.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/daemons/autonomous.py +56 -0
- package/omega/Agentik_Engine/omega_engine/daemons/engine.py +236 -0
- package/omega/Agentik_Engine/omega_engine/daemons/telegram.py +315 -0
- package/omega/Agentik_Engine/omega_engine/done_signal.py +154 -0
- package/omega/Agentik_Engine/omega_engine/educators/__init__.py +51 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/artifact.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/automation.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/base.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/claudecode.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/connection.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/coworker.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/loop.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/prompt.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/__pycache__/skill.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/educators/artifact.py +65 -0
- package/omega/Agentik_Engine/omega_engine/educators/automation.py +76 -0
- package/omega/Agentik_Engine/omega_engine/educators/base.py +327 -0
- package/omega/Agentik_Engine/omega_engine/educators/claudecode.py +71 -0
- package/omega/Agentik_Engine/omega_engine/educators/connection.py +75 -0
- package/omega/Agentik_Engine/omega_engine/educators/coworker.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/loop.py +82 -0
- package/omega/Agentik_Engine/omega_engine/educators/prompt.py +68 -0
- package/omega/Agentik_Engine/omega_engine/educators/skill.py +69 -0
- package/omega/Agentik_Engine/omega_engine/envelope.py +219 -0
- package/omega/Agentik_Engine/omega_engine/executor.py +195 -16
- package/omega/Agentik_Engine/omega_engine/genesis/__init__.py +134 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/orchestrator.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/phases.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/stack.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/__pycache__/state.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/genesis/orchestrator.py +262 -0
- package/omega/Agentik_Engine/omega_engine/genesis/phases.py +950 -0
- package/omega/Agentik_Engine/omega_engine/genesis/stack.py +324 -0
- package/omega/Agentik_Engine/omega_engine/genesis/state.py +353 -0
- package/omega/Agentik_Engine/omega_engine/handoff.py +459 -0
- package/omega/Agentik_Engine/omega_engine/hermes.py +426 -0
- package/omega/Agentik_Engine/omega_engine/hermes_bootstrap.py +382 -0
- package/omega/Agentik_Engine/omega_engine/hermes_desktop.py +469 -0
- package/omega/Agentik_Engine/omega_engine/integrations/__init__.py +30 -0
- package/omega/Agentik_Engine/omega_engine/integrations/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/integrations/__pycache__/graphify.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/integrations/graphify.py +234 -0
- package/omega/Agentik_Engine/omega_engine/learning.py +268 -0
- package/omega/Agentik_Engine/omega_engine/managed_agent.py +467 -0
- package/omega/Agentik_Engine/omega_engine/memory.py +271 -0
- package/omega/Agentik_Engine/omega_engine/menu.py +1065 -0
- package/omega/Agentik_Engine/omega_engine/migrations/__init__.py +144 -0
- package/omega/Agentik_Engine/omega_engine/migrations/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/migrations/__pycache__/v0_14_0.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/migrations/v0_14_0.py +29 -0
- package/omega/Agentik_Engine/omega_engine/mission.py +29 -14
- package/omega/Agentik_Engine/omega_engine/plan.py +846 -0
- package/omega/Agentik_Engine/omega_engine/prompts.py +158 -0
- package/omega/Agentik_Engine/omega_engine/provider.py +408 -13
- package/omega/Agentik_Engine/omega_engine/prune.py +151 -0
- package/omega/Agentik_Engine/omega_engine/pursue.py +205 -0
- package/omega/Agentik_Engine/omega_engine/rag/__init__.py +21 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/agentic.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/base.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/corrective.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/graph.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/hybrid.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/multimodal.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/__pycache__/router.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/rag/agentic.py +83 -0
- package/omega/Agentik_Engine/omega_engine/rag/base.py +42 -0
- package/omega/Agentik_Engine/omega_engine/rag/corrective.py +119 -0
- package/omega/Agentik_Engine/omega_engine/rag/graph.py +169 -0
- package/omega/Agentik_Engine/omega_engine/rag/hybrid.py +205 -0
- package/omega/Agentik_Engine/omega_engine/rag/multimodal.py +136 -0
- package/omega/Agentik_Engine/omega_engine/rag/router.py +110 -0
- package/omega/Agentik_Engine/omega_engine/reducer.py +21 -3
- package/omega/Agentik_Engine/omega_engine/router.py +28 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__init__.py +48 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/__init__.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/auditor.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/finder.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/installer.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/__pycache__/marketplaces.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/auditor.py +232 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/finder.py +94 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/installer.py +129 -0
- package/omega/Agentik_Engine/omega_engine/skill_discovery/marketplaces.py +80 -0
- package/omega/Agentik_Engine/omega_engine/skill_routing.py +388 -0
- package/omega/Agentik_Engine/omega_engine/smoke.py +81 -0
- package/omega/Agentik_Engine/omega_engine/store.py +132 -25
- package/omega/Agentik_Engine/omega_engine/sync.py +445 -0
- package/omega/Agentik_Engine/omega_engine/telegram_history.py +260 -0
- package/omega/Agentik_Engine/omega_engine/tmux.py +526 -0
- package/omega/Agentik_Engine/omega_engine/tools.py +272 -0
- package/omega/Agentik_Engine/omega_engine/understand_anything.py +275 -0
- package/omega/Agentik_Engine/omega_engine/updater.py +70 -0
- package/omega/Agentik_Engine/omega_engine/validate.py +186 -0
- package/omega/Agentik_Engine/omega_engine/vault.py +342 -0
- package/omega/Agentik_Engine/omega_engine/webhooks.py +262 -0
- package/omega/Agentik_Engine/omega_engine/worker.py +526 -0
- package/omega/Agentik_Engine/pyproject.toml +1 -1
- package/omega/Agentik_Engine/tests/__pycache__/test_account.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_account.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_adversarial.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_adversarial.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_agents_envelope.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_agents_envelope.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audit_arsenal.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audits_pipeline.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_audits_pipeline.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_auto_update_and_migrations.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_auto_update_and_migrations.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_autonomous.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_autonomous.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_educators.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_educators.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_executor.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_genesis_and_plan.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_genesis_and_plan.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_graphify.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_graphify.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_handoff.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_handoff.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_and_ua.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_and_ua.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_bootstrap_and_desktop.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_hermes_bootstrap_and_desktop.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_steps.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_steps.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_ux.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_install_ux.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_installer_wiring.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_intelligence.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_intelligence.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_llm_clis_and_uninstall.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_llm_clis_and_uninstall.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_managed_agent.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_managed_agent.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_max_provider_and_menu.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_max_provider_and_menu.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_menu_coverage.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_menu_coverage.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_mission.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_progress.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_project.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_pursue_cadence.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_pursue_cadence.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_rag.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_rag.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_reducer.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_report.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_role_aliases_and_ssot.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_role_aliases_and_ssot.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_discovery_and_gate.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_discovery_and_gate.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_power.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_power.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_routing.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_skill_routing.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_snapshot_partial.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_snapshot_partial.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_telegram_history.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_telegram_history.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tmux_and_aisb_chat.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tmux_and_aisb_chat.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tools_and_sync.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_tools_and_sync.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_v06_features.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_v06_features.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_vault.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_vault.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_webhooks_and_readiness.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_webhooks_and_readiness.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_worker_and_cleanup.cpython-313-pytest-8.4.2.pyc +0 -0
- package/omega/Agentik_Engine/tests/__pycache__/test_worker_and_cleanup.cpython-313.pyc +0 -0
- package/omega/Agentik_Engine/tests/test_account.py +338 -0
- package/omega/Agentik_Engine/tests/test_adversarial.py +351 -0
- package/omega/Agentik_Engine/tests/test_agents_envelope.py +274 -0
- package/omega/Agentik_Engine/tests/test_audits_pipeline.py +348 -0
- package/omega/Agentik_Engine/tests/test_auto_update_and_migrations.py +394 -0
- package/omega/Agentik_Engine/tests/test_autonomous.py +361 -0
- package/omega/Agentik_Engine/tests/test_educators.py +233 -0
- package/omega/Agentik_Engine/tests/test_genesis_and_plan.py +573 -0
- package/omega/Agentik_Engine/tests/test_graphify.py +190 -0
- package/omega/Agentik_Engine/tests/test_handoff.py +311 -0
- package/omega/Agentik_Engine/tests/test_hermes_and_ua.py +387 -0
- package/omega/Agentik_Engine/tests/test_hermes_bootstrap_and_desktop.py +358 -0
- package/omega/Agentik_Engine/tests/test_install_steps.py +359 -0
- package/omega/Agentik_Engine/tests/test_install_ux.py +151 -0
- package/omega/Agentik_Engine/tests/test_installer_wiring.py +496 -0
- package/omega/Agentik_Engine/tests/test_intelligence.py +285 -0
- package/omega/Agentik_Engine/tests/test_llm_clis_and_uninstall.py +228 -0
- package/omega/Agentik_Engine/tests/test_managed_agent.py +363 -0
- package/omega/Agentik_Engine/tests/test_max_provider_and_menu.py +231 -0
- package/omega/Agentik_Engine/tests/test_menu_coverage.py +72 -0
- package/omega/Agentik_Engine/tests/test_pursue_cadence.py +217 -0
- package/omega/Agentik_Engine/tests/test_rag.py +287 -0
- package/omega/Agentik_Engine/tests/test_role_aliases_and_ssot.py +207 -0
- package/omega/Agentik_Engine/tests/test_skill_discovery_and_gate.py +337 -0
- package/omega/Agentik_Engine/tests/test_skill_power.py +259 -0
- package/omega/Agentik_Engine/tests/test_skill_routing.py +189 -0
- package/omega/Agentik_Engine/tests/test_snapshot_partial.py +172 -0
- package/omega/Agentik_Engine/tests/test_telegram_history.py +209 -0
- package/omega/Agentik_Engine/tests/test_tmux_and_aisb_chat.py +223 -0
- package/omega/Agentik_Engine/tests/test_tools_and_sync.py +312 -0
- package/omega/Agentik_Engine/tests/test_v06_features.py +370 -0
- package/omega/Agentik_Engine/tests/test_vault.py +173 -0
- package/omega/Agentik_Engine/tests/test_webhooks_and_readiness.py +277 -0
- package/omega/Agentik_Engine/tests/test_worker_and_cleanup.py +541 -0
- package/omega/Agentik_Extra/etc/secrets/.vault-key +3 -0
- package/omega/Agentik_Extra/etc/secrets/.vault-pub +1 -0
- package/omega/Agentik_Runtime/audits.db +0 -0
- package/omega/Agentik_SSOT/VERSION +1 -1
- package/omega/Agentik_SSOT/claude-plugins/claude-plugins.yaml +100 -0
- package/omega/Agentik_SSOT/docs/LAYERS.md +90 -0
- package/omega/Agentik_SSOT/docs/USER-JOURNEY.md +283 -0
- package/omega/Agentik_SSOT/marketplaces/design-discipline.yaml +86 -0
- package/omega/Agentik_SSOT/skills/a11yaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/apiaudit/SKILL.md +157 -0
- package/omega/Agentik_SSOT/skills/automationaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/cadence/SKILL.md +76 -0
- package/omega/Agentik_SSOT/skills/codeaudit/SKILL.md +153 -0
- package/omega/Agentik_SSOT/skills/copyaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/dataaudit/SKILL.md +157 -0
- package/omega/Agentik_SSOT/skills/debugaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/dispatch/SKILL.md +79 -0
- package/omega/Agentik_SSOT/skills/dxaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/featureaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/flowaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/genesis/SKILL.md +116 -0
- package/omega/Agentik_SSOT/skills/handoff/SKILL.md +117 -0
- package/omega/Agentik_SSOT/skills/logicaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/motionaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/perfaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/plan/SKILL.md +127 -0
- package/omega/Agentik_SSOT/skills/pursue/SKILL.md +68 -0
- package/omega/Agentik_SSOT/skills/rag-route.md +82 -0
- package/omega/Agentik_SSOT/skills/refontaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/retentionaudit/SKILL.md +165 -0
- package/omega/Agentik_SSOT/skills/secaudit/SKILL.md +157 -0
- package/omega/Agentik_SSOT/skills/seoaudit/SKILL.md +161 -0
- package/omega/Agentik_SSOT/skills/skill-auditor/SKILL.md +83 -0
- package/omega/Agentik_SSOT/skills/skill-finder/SKILL.md +116 -0
- package/omega/Agentik_SSOT/skills/uiuxaudit/SKILL.md +165 -0
- package/package.json +2 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Graph retriever — entity-relation graph with depth-limited expansion.
|
|
2
|
+
|
|
3
|
+
Plain Python: dict-of-sets adjacency, typed edges. Persists to JSON. The
|
|
4
|
+
retrieval contract picks seed nodes from query tokens, expands `depth` hops,
|
|
5
|
+
and returns each visited node as a Document.
|
|
6
|
+
|
|
7
|
+
This is the "second strategy" in the multi-RAG router: when a query is
|
|
8
|
+
relational ("who works on X", "what depends on Y"), the graph wins over
|
|
9
|
+
hybrid.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
from collections import defaultdict, deque
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Iterable
|
|
18
|
+
|
|
19
|
+
from omega_engine.rag.base import Document, RetrievalResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_TOKEN_RE = re.compile(r"[A-Za-z0-9_]+")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _tokens(text: str) -> list[str]:
|
|
26
|
+
return [t.lower() for t in _TOKEN_RE.findall(text)]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class GraphRetriever:
|
|
30
|
+
"""In-memory entity-relation graph with JSON persistence.
|
|
31
|
+
|
|
32
|
+
Edge tuples are `(neighbour, edge_type)`. The graph is undirected for
|
|
33
|
+
expansion — adding `(a, b, "depends_on")` makes `b` a neighbour of `a`
|
|
34
|
+
AND `a` a neighbour of `b`. The edge type travels with the neighbour so
|
|
35
|
+
the caller can filter on it.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
strategy = "graph"
|
|
39
|
+
|
|
40
|
+
def __init__(self, json_path: str | Path | None = None) -> None:
|
|
41
|
+
self.json_path = str(json_path) if json_path is not None else None
|
|
42
|
+
self._adj: dict[str, set[tuple[str, str]]] = defaultdict(set)
|
|
43
|
+
self._node_text: dict[str, str] = {} # optional node text body
|
|
44
|
+
self._node_meta: dict[str, dict[str, Any]] = {}
|
|
45
|
+
if self.json_path and Path(self.json_path).exists():
|
|
46
|
+
self._load()
|
|
47
|
+
|
|
48
|
+
# ----- mutation -----
|
|
49
|
+
|
|
50
|
+
def add_node(
|
|
51
|
+
self,
|
|
52
|
+
node: str,
|
|
53
|
+
text: str | None = None,
|
|
54
|
+
metadata: dict[str, Any] | None = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self._adj.setdefault(node, set())
|
|
57
|
+
if text is not None:
|
|
58
|
+
self._node_text[node] = text
|
|
59
|
+
if metadata is not None:
|
|
60
|
+
self._node_meta[node] = dict(metadata)
|
|
61
|
+
|
|
62
|
+
def add_edge(self, a: str, b: str, edge_type: str = "rel") -> None:
|
|
63
|
+
"""Add an undirected typed edge between two nodes. Idempotent."""
|
|
64
|
+
self.add_node(a)
|
|
65
|
+
self.add_node(b)
|
|
66
|
+
self._adj[a].add((b, edge_type))
|
|
67
|
+
self._adj[b].add((a, edge_type))
|
|
68
|
+
if self.json_path:
|
|
69
|
+
self._save()
|
|
70
|
+
|
|
71
|
+
def neighbors(self, node: str, depth: int = 1) -> list[str]:
|
|
72
|
+
"""BFS up to `depth` hops. Returns nodes in discovery order, excluding
|
|
73
|
+
the seed itself."""
|
|
74
|
+
if depth <= 0 or node not in self._adj:
|
|
75
|
+
return []
|
|
76
|
+
seen: set[str] = {node}
|
|
77
|
+
out: list[str] = []
|
|
78
|
+
q: deque[tuple[str, int]] = deque([(node, 0)])
|
|
79
|
+
while q:
|
|
80
|
+
cur, d = q.popleft()
|
|
81
|
+
if d == depth:
|
|
82
|
+
continue
|
|
83
|
+
for nbr, _etype in self._adj[cur]:
|
|
84
|
+
if nbr in seen:
|
|
85
|
+
continue
|
|
86
|
+
seen.add(nbr)
|
|
87
|
+
out.append(nbr)
|
|
88
|
+
q.append((nbr, d + 1))
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
# ----- retrieval -----
|
|
92
|
+
|
|
93
|
+
def retrieve(self, query: str, k: int = 5, depth: int = 2) -> RetrievalResult:
|
|
94
|
+
"""Pick seeds whose name overlaps a query token; expand `depth` hops.
|
|
95
|
+
|
|
96
|
+
Score = closeness (closer hops rank higher) + seed-name overlap.
|
|
97
|
+
"""
|
|
98
|
+
if not self._adj:
|
|
99
|
+
return RetrievalResult(query=query, documents=[], score=0.0,
|
|
100
|
+
strategy=self.strategy)
|
|
101
|
+
|
|
102
|
+
q_tokens = set(_tokens(query))
|
|
103
|
+
seeds: list[str] = []
|
|
104
|
+
for node in self._adj:
|
|
105
|
+
if any(t in node.lower() for t in q_tokens):
|
|
106
|
+
seeds.append(node)
|
|
107
|
+
if not seeds:
|
|
108
|
+
# No direct hit — fall back to seeding from every node, capped.
|
|
109
|
+
# Lets the graph still surface SOMETHING for ambient queries.
|
|
110
|
+
seeds = list(self._adj.keys())[:3]
|
|
111
|
+
|
|
112
|
+
scored: dict[str, float] = {}
|
|
113
|
+
for seed in seeds:
|
|
114
|
+
scored.setdefault(seed, max(scored.get(seed, 0.0), 1.0))
|
|
115
|
+
# BFS, score = 1 / (1 + hops)
|
|
116
|
+
seen: set[str] = {seed}
|
|
117
|
+
q: deque[tuple[str, int]] = deque([(seed, 0)])
|
|
118
|
+
while q:
|
|
119
|
+
cur, d = q.popleft()
|
|
120
|
+
if d == depth:
|
|
121
|
+
continue
|
|
122
|
+
for nbr, _etype in self._adj[cur]:
|
|
123
|
+
if nbr in seen:
|
|
124
|
+
continue
|
|
125
|
+
seen.add(nbr)
|
|
126
|
+
s = 1.0 / (1.0 + d + 1)
|
|
127
|
+
scored[nbr] = max(scored.get(nbr, 0.0), s)
|
|
128
|
+
q.append((nbr, d + 1))
|
|
129
|
+
|
|
130
|
+
ranked = sorted(scored.items(), key=lambda p: p[1], reverse=True)[:k]
|
|
131
|
+
docs: list[Document] = []
|
|
132
|
+
for node, s in ranked:
|
|
133
|
+
text = self._node_text.get(node, node)
|
|
134
|
+
meta = dict(self._node_meta.get(node, {}))
|
|
135
|
+
meta["score"] = s
|
|
136
|
+
meta["edges"] = sorted(
|
|
137
|
+
(nbr, etype) for nbr, etype in self._adj[node]
|
|
138
|
+
)
|
|
139
|
+
docs.append(Document(id=node, text=text, metadata=meta))
|
|
140
|
+
agg = sum(s for _, s in ranked) / len(ranked) if ranked else 0.0
|
|
141
|
+
return RetrievalResult(query=query, documents=docs, score=agg,
|
|
142
|
+
strategy=self.strategy)
|
|
143
|
+
|
|
144
|
+
# ----- persistence -----
|
|
145
|
+
|
|
146
|
+
def _save(self) -> None:
|
|
147
|
+
assert self.json_path is not None
|
|
148
|
+
Path(self.json_path).parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
# Sets aren't JSON-serialisable — flatten to lists.
|
|
150
|
+
data = {
|
|
151
|
+
"adj": {
|
|
152
|
+
k: sorted([list(t) for t in v]) for k, v in self._adj.items()
|
|
153
|
+
},
|
|
154
|
+
"node_text": self._node_text,
|
|
155
|
+
"node_meta": self._node_meta,
|
|
156
|
+
}
|
|
157
|
+
Path(self.json_path).write_text(json.dumps(data, indent=2))
|
|
158
|
+
|
|
159
|
+
def _load(self) -> None:
|
|
160
|
+
assert self.json_path is not None
|
|
161
|
+
data = json.loads(Path(self.json_path).read_text())
|
|
162
|
+
self._adj = defaultdict(set, {
|
|
163
|
+
k: {tuple(t) for t in v} for k, v in data.get("adj", {}).items()
|
|
164
|
+
})
|
|
165
|
+
self._node_text = data.get("node_text", {})
|
|
166
|
+
self._node_meta = data.get("node_meta", {})
|
|
167
|
+
|
|
168
|
+
def nodes(self) -> Iterable[str]:
|
|
169
|
+
return self._adj.keys()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Hybrid retriever — sparse FTS5 (BM25) blended with dense cosine.
|
|
2
|
+
|
|
3
|
+
SQLite FTS5 gives a real BM25 score via its `bm25()` ranking function.
|
|
4
|
+
For the dense leg, we use a *hashing trick* — every token is hashed into one
|
|
5
|
+
of `dim` buckets, weighted by sublinear TF, then ℓ2-normalised. Cosine
|
|
6
|
+
similarity is the dot product of two normalised vectors. Pure stdlib.
|
|
7
|
+
|
|
8
|
+
The combined score is `alpha * dense + (1 - alpha) * sparse`, both legs
|
|
9
|
+
normalised to [0, 1] across the candidate pool.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import re
|
|
17
|
+
import sqlite3
|
|
18
|
+
from collections import Counter
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from omega_engine.rag.base import Document, RetrievalResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_TOKEN_RE = re.compile(r"[A-Za-z0-9]+")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _tokens(text: str) -> list[str]:
|
|
28
|
+
return [t.lower() for t in _TOKEN_RE.findall(text)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _hash_to_bucket(token: str, dim: int) -> int:
|
|
32
|
+
# md5 is fast and stable — we only need a uniform-ish bucket index, not
|
|
33
|
+
# cryptographic security.
|
|
34
|
+
h = hashlib.md5(token.encode("utf-8")).digest()
|
|
35
|
+
return int.from_bytes(h[:4], "big") % dim
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _vectorise(text: str, dim: int) -> list[float]:
|
|
39
|
+
"""Hashed-feature vector with sublinear TF, ℓ2-normalised."""
|
|
40
|
+
counts: Counter[int] = Counter()
|
|
41
|
+
for tok in _tokens(text):
|
|
42
|
+
counts[_hash_to_bucket(tok, dim)] += 1
|
|
43
|
+
vec = [0.0] * dim
|
|
44
|
+
for bucket, c in counts.items():
|
|
45
|
+
# 1 + log(tf) damps very common tokens — the classic tf-idf weighting.
|
|
46
|
+
vec[bucket] = 1.0 + math.log(c)
|
|
47
|
+
norm = math.sqrt(sum(v * v for v in vec))
|
|
48
|
+
if norm == 0.0:
|
|
49
|
+
return vec
|
|
50
|
+
return [v / norm for v in vec]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cosine(a: list[float], b: list[float]) -> float:
|
|
54
|
+
# Both vectors are already ℓ2-normalised → cosine == dot product.
|
|
55
|
+
return sum(x * y for x, y in zip(a, b))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _normalise(values: list[float]) -> list[float]:
|
|
59
|
+
"""Min-max scale a list into [0, 1]. Empty / flat → all zeros."""
|
|
60
|
+
if not values:
|
|
61
|
+
return values
|
|
62
|
+
lo, hi = min(values), max(values)
|
|
63
|
+
if hi - lo < 1e-12:
|
|
64
|
+
return [0.0 for _ in values]
|
|
65
|
+
return [(v - lo) / (hi - lo) for v in values]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_SCHEMA = """
|
|
69
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS docs_fts USING fts5(
|
|
70
|
+
id UNINDEXED, text, tokenize = 'unicode61'
|
|
71
|
+
);
|
|
72
|
+
CREATE TABLE IF NOT EXISTS docs_meta (
|
|
73
|
+
id TEXT PRIMARY KEY,
|
|
74
|
+
text TEXT NOT NULL,
|
|
75
|
+
meta TEXT NOT NULL DEFAULT '{}',
|
|
76
|
+
vec TEXT NOT NULL
|
|
77
|
+
);
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class HybridRetriever:
|
|
82
|
+
"""Sparse FTS5 + dense hashed-cosine, combined per `alpha`.
|
|
83
|
+
|
|
84
|
+
`alpha = 1` → pure dense. `alpha = 0` → pure sparse. Default 0.5 blends.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
strategy = "hybrid"
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
db_path: str | Path,
|
|
92
|
+
*,
|
|
93
|
+
dim: int = 256,
|
|
94
|
+
alpha: float = 0.5,
|
|
95
|
+
) -> None:
|
|
96
|
+
self.db_path = str(db_path)
|
|
97
|
+
self.dim = dim
|
|
98
|
+
self.alpha = alpha
|
|
99
|
+
Path(self.db_path).parent.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
self._conn = sqlite3.connect(self.db_path, isolation_level=None)
|
|
101
|
+
self._conn.row_factory = sqlite3.Row
|
|
102
|
+
self._conn.execute("PRAGMA journal_mode=WAL;")
|
|
103
|
+
self._conn.execute("PRAGMA synchronous=NORMAL;")
|
|
104
|
+
self._conn.executescript(_SCHEMA)
|
|
105
|
+
|
|
106
|
+
# ----- ingest -----
|
|
107
|
+
|
|
108
|
+
def index(self, documents: list[Document]) -> int:
|
|
109
|
+
"""Index a batch. Returns the number of docs written. Idempotent on id."""
|
|
110
|
+
n = 0
|
|
111
|
+
with self._conn:
|
|
112
|
+
for d in documents:
|
|
113
|
+
vec = _vectorise(d.text, self.dim)
|
|
114
|
+
self._conn.execute(
|
|
115
|
+
"INSERT OR REPLACE INTO docs_meta (id, text, meta, vec) "
|
|
116
|
+
"VALUES (?, ?, ?, ?)",
|
|
117
|
+
(d.id, d.text, json.dumps(d.metadata), json.dumps(vec)),
|
|
118
|
+
)
|
|
119
|
+
# FTS5 has no native upsert — delete-then-insert is the idiom.
|
|
120
|
+
self._conn.execute("DELETE FROM docs_fts WHERE id = ?", (d.id,))
|
|
121
|
+
self._conn.execute(
|
|
122
|
+
"INSERT INTO docs_fts (id, text) VALUES (?, ?)", (d.id, d.text)
|
|
123
|
+
)
|
|
124
|
+
n += 1
|
|
125
|
+
return n
|
|
126
|
+
|
|
127
|
+
def count(self) -> int:
|
|
128
|
+
cur = self._conn.execute("SELECT COUNT(*) AS c FROM docs_meta")
|
|
129
|
+
return int(cur.fetchone()["c"])
|
|
130
|
+
|
|
131
|
+
# ----- retrieval -----
|
|
132
|
+
|
|
133
|
+
def retrieve(self, query: str, k: int = 5) -> RetrievalResult:
|
|
134
|
+
if k <= 0 or self.count() == 0:
|
|
135
|
+
return RetrievalResult(query=query, documents=[], score=0.0,
|
|
136
|
+
strategy=self.strategy)
|
|
137
|
+
|
|
138
|
+
# --- sparse leg: FTS5 + bm25. bm25() returns a NEGATIVE score (lower
|
|
139
|
+
# is better), so we negate it before normalising.
|
|
140
|
+
sparse_rows: dict[str, float] = {}
|
|
141
|
+
fts_query = _fts5_safe(query)
|
|
142
|
+
if fts_query:
|
|
143
|
+
try:
|
|
144
|
+
cur = self._conn.execute(
|
|
145
|
+
"SELECT id, bm25(docs_fts) AS score FROM docs_fts "
|
|
146
|
+
"WHERE docs_fts MATCH ? ORDER BY score LIMIT ?",
|
|
147
|
+
(fts_query, k * 4),
|
|
148
|
+
)
|
|
149
|
+
for row in cur.fetchall():
|
|
150
|
+
sparse_rows[row["id"]] = -float(row["score"])
|
|
151
|
+
except sqlite3.OperationalError:
|
|
152
|
+
# malformed FTS5 query — fall back to dense-only
|
|
153
|
+
sparse_rows = {}
|
|
154
|
+
|
|
155
|
+
# --- dense leg: cosine over every indexed doc (small corpora; for
|
|
156
|
+
# larger corpora a clustered ANN goes here, contract identical).
|
|
157
|
+
qvec = _vectorise(query, self.dim)
|
|
158
|
+
dense_rows: dict[str, float] = {}
|
|
159
|
+
cur = self._conn.execute("SELECT id, vec FROM docs_meta")
|
|
160
|
+
for row in cur.fetchall():
|
|
161
|
+
v = json.loads(row["vec"])
|
|
162
|
+
dense_rows[row["id"]] = _cosine(qvec, v)
|
|
163
|
+
|
|
164
|
+
# --- combine: union of candidates, min-max each leg, alpha blend.
|
|
165
|
+
candidates = set(sparse_rows) | set(dense_rows)
|
|
166
|
+
if not candidates:
|
|
167
|
+
return RetrievalResult(query=query, documents=[], score=0.0,
|
|
168
|
+
strategy=self.strategy)
|
|
169
|
+
|
|
170
|
+
ids = list(candidates)
|
|
171
|
+
sparse_vec = [sparse_rows.get(i, 0.0) for i in ids]
|
|
172
|
+
dense_vec = [dense_rows.get(i, 0.0) for i in ids]
|
|
173
|
+
sparse_n = _normalise(sparse_vec)
|
|
174
|
+
dense_n = _normalise(dense_vec)
|
|
175
|
+
combined = [
|
|
176
|
+
(self.alpha * d) + ((1 - self.alpha) * s)
|
|
177
|
+
for d, s in zip(dense_n, sparse_n)
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
ranked = sorted(zip(ids, combined), key=lambda p: p[1], reverse=True)[:k]
|
|
181
|
+
docs: list[Document] = []
|
|
182
|
+
for doc_id, score in ranked:
|
|
183
|
+
cur = self._conn.execute(
|
|
184
|
+
"SELECT text, meta FROM docs_meta WHERE id = ?", (doc_id,)
|
|
185
|
+
)
|
|
186
|
+
row = cur.fetchone()
|
|
187
|
+
if not row:
|
|
188
|
+
continue
|
|
189
|
+
meta = json.loads(row["meta"])
|
|
190
|
+
meta["score"] = score
|
|
191
|
+
docs.append(Document(id=doc_id, text=row["text"], metadata=meta))
|
|
192
|
+
|
|
193
|
+
agg = sum(s for _, s in ranked) / len(ranked) if ranked else 0.0
|
|
194
|
+
return RetrievalResult(query=query, documents=docs, score=agg,
|
|
195
|
+
strategy=self.strategy)
|
|
196
|
+
|
|
197
|
+
def close(self) -> None:
|
|
198
|
+
self._conn.close()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _fts5_safe(query: str) -> str:
|
|
202
|
+
"""Coerce a freeform query into something FTS5 can parse — strip syntax
|
|
203
|
+
chars, OR the surviving tokens together. Returns "" if nothing remains."""
|
|
204
|
+
toks = _tokens(query)
|
|
205
|
+
return " OR ".join(toks) if toks else ""
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Multimodal retriever — PDFs and images on top of a text retriever.
|
|
2
|
+
|
|
3
|
+
v1 keeps the surface honest:
|
|
4
|
+
* PDFs are converted to text via `pdftotext` if the binary is on PATH,
|
|
5
|
+
otherwise the doc is registered with its path + an empty body (no fake
|
|
6
|
+
text, no hallucinated extraction).
|
|
7
|
+
* Images get a caption from the provider (role "rag-caption") and are
|
|
8
|
+
indexed by that caption + their path. If the provider returns nothing,
|
|
9
|
+
the image still indexes by filename.
|
|
10
|
+
|
|
11
|
+
The underlying text index is any text-mode Retriever (typically
|
|
12
|
+
HybridRetriever) — so a query "the architecture diagram explaining the
|
|
13
|
+
event log" can surface an image whose caption matches.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from omega_engine.provider import AgentProvider, AgentRequest
|
|
22
|
+
from omega_engine.rag.base import Document, RetrievalResult, Retriever
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _pdftotext_available() -> bool:
|
|
26
|
+
return shutil.which("pdftotext") is not None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _extract_pdf_text(path: Path) -> str:
|
|
30
|
+
"""Best-effort PDF → text. Returns "" when pdftotext is missing or fails.
|
|
31
|
+
NEVER fakes output (Karpathy: think before coding — no hallucination)."""
|
|
32
|
+
if not _pdftotext_available():
|
|
33
|
+
return ""
|
|
34
|
+
try:
|
|
35
|
+
out = subprocess.run(
|
|
36
|
+
["pdftotext", "-q", str(path), "-"],
|
|
37
|
+
capture_output=True, text=True, timeout=15, check=False,
|
|
38
|
+
)
|
|
39
|
+
return out.stdout if out.returncode == 0 else ""
|
|
40
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
41
|
+
return ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class MultimodalRetriever:
|
|
45
|
+
"""Wraps a text retriever; understands `add_pdf` and `add_image`."""
|
|
46
|
+
|
|
47
|
+
strategy = "multimodal"
|
|
48
|
+
|
|
49
|
+
# `Retriever` (Protocol) needs an `index` method when we use HybridRetriever.
|
|
50
|
+
# We type-hint loosely so any object with `retrieve` works AND any
|
|
51
|
+
# object that *also* has `index` accepts new docs.
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
inner: Retriever,
|
|
55
|
+
provider: AgentProvider | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
self.inner = inner
|
|
58
|
+
self.provider = provider
|
|
59
|
+
self._registered: dict[str, dict] = {} # id → {kind, path, caption}
|
|
60
|
+
|
|
61
|
+
# ----- ingest -----
|
|
62
|
+
|
|
63
|
+
def add_pdf(self, doc_id: str, path: str | Path) -> Document:
|
|
64
|
+
"""Extract text via pdftotext if available; index whatever survives."""
|
|
65
|
+
p = Path(path)
|
|
66
|
+
text = _extract_pdf_text(p)
|
|
67
|
+
meta = {
|
|
68
|
+
"modality": "pdf",
|
|
69
|
+
"source": str(p),
|
|
70
|
+
"extracted": bool(text),
|
|
71
|
+
}
|
|
72
|
+
doc = Document(id=doc_id, text=text or f"[pdf:{p.name}]", metadata=meta)
|
|
73
|
+
self._register(doc)
|
|
74
|
+
return doc
|
|
75
|
+
|
|
76
|
+
def add_image(
|
|
77
|
+
self,
|
|
78
|
+
doc_id: str,
|
|
79
|
+
path: str | Path,
|
|
80
|
+
*,
|
|
81
|
+
caption: str | None = None,
|
|
82
|
+
) -> Document:
|
|
83
|
+
"""Index an image by its caption. Caption is provider-generated
|
|
84
|
+
unless one is passed in. With no provider and no caption, we fall
|
|
85
|
+
back to the filename so the image is still discoverable."""
|
|
86
|
+
p = Path(path)
|
|
87
|
+
final_caption = caption
|
|
88
|
+
if final_caption is None and self.provider is not None:
|
|
89
|
+
result = self.provider.run(AgentRequest(
|
|
90
|
+
role="rag-caption",
|
|
91
|
+
prompt=f"Caption the image at {p}",
|
|
92
|
+
context={"path": str(p)},
|
|
93
|
+
))
|
|
94
|
+
final_caption = (result.artifacts or {}).get("caption")
|
|
95
|
+
if not final_caption:
|
|
96
|
+
final_caption = p.stem.replace("_", " ").replace("-", " ")
|
|
97
|
+
meta = {
|
|
98
|
+
"modality": "image",
|
|
99
|
+
"source": str(p),
|
|
100
|
+
"caption": final_caption,
|
|
101
|
+
}
|
|
102
|
+
doc = Document(id=doc_id, text=final_caption, metadata=meta)
|
|
103
|
+
self._register(doc)
|
|
104
|
+
return doc
|
|
105
|
+
|
|
106
|
+
def add_text(self, doc_id: str, text: str, **meta) -> Document:
|
|
107
|
+
"""Plain text passthrough — keeps the retriever uniformly usable."""
|
|
108
|
+
m = {"modality": "text", **meta}
|
|
109
|
+
doc = Document(id=doc_id, text=text, metadata=m)
|
|
110
|
+
self._register(doc)
|
|
111
|
+
return doc
|
|
112
|
+
|
|
113
|
+
def _register(self, doc: Document) -> None:
|
|
114
|
+
self._registered[doc.id] = doc.metadata
|
|
115
|
+
index = getattr(self.inner, "index", None)
|
|
116
|
+
if callable(index):
|
|
117
|
+
index([doc])
|
|
118
|
+
# If the inner retriever has no `index`, the caller is responsible
|
|
119
|
+
# for populating it elsewhere — we still track the modality metadata.
|
|
120
|
+
|
|
121
|
+
# ----- retrieval -----
|
|
122
|
+
|
|
123
|
+
def retrieve(self, query: str, k: int = 5) -> RetrievalResult:
|
|
124
|
+
result = self.inner.retrieve(query, k=k)
|
|
125
|
+
# Inject modality metadata for any doc we registered.
|
|
126
|
+
for d in result.documents:
|
|
127
|
+
if d.id in self._registered:
|
|
128
|
+
d.metadata.setdefault(
|
|
129
|
+
"modality", self._registered[d.id].get("modality", "text"),
|
|
130
|
+
)
|
|
131
|
+
return RetrievalResult(
|
|
132
|
+
query=query,
|
|
133
|
+
documents=result.documents,
|
|
134
|
+
score=result.score,
|
|
135
|
+
strategy=self.strategy,
|
|
136
|
+
)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""RAGRouter — classifies a query and picks the right inner strategy, then
|
|
2
|
+
wraps the result in the Corrective envelope.
|
|
3
|
+
|
|
4
|
+
Routing happens in two steps:
|
|
5
|
+
1. A cheap heuristic ("does the query look relational? mention an image
|
|
6
|
+
or PDF? have a 'why/how' shape?") gives a default.
|
|
7
|
+
2. The provider (role "rag-route") gets a chance to override; its choice
|
|
8
|
+
wins when it picks one of the registered strategies.
|
|
9
|
+
|
|
10
|
+
The Corrective envelope wraps every dispatch — that's the project's stance:
|
|
11
|
+
"check / recheck until 100%" applied to retrieval (ARCHITECTURE.md §7).
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from typing import Iterable
|
|
17
|
+
|
|
18
|
+
from omega_engine.provider import AgentProvider, AgentRequest
|
|
19
|
+
from omega_engine.rag.base import RetrievalResult, Retriever
|
|
20
|
+
from omega_engine.rag.corrective import CorrectiveRetriever
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_GRAPH_HINTS = re.compile(
|
|
24
|
+
r"\b(depend|related|connection|who|graph|owner|"
|
|
25
|
+
r"between|link|relation|adjacent|connected)\b",
|
|
26
|
+
re.IGNORECASE,
|
|
27
|
+
)
|
|
28
|
+
_MULTIMODAL_HINTS = re.compile(
|
|
29
|
+
r"\b(image|picture|screenshot|diagram|pdf|figure|chart|photo)\b",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
_AGENTIC_HINTS = re.compile(
|
|
33
|
+
r"\b(why|how|explain|compare|trace|step by step|deep dive)\b",
|
|
34
|
+
re.IGNORECASE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RAGRouter:
|
|
39
|
+
"""Routes a query to one of the registered strategies, then wraps in CRAG."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
strategies: dict[str, Retriever],
|
|
44
|
+
provider: AgentProvider,
|
|
45
|
+
*,
|
|
46
|
+
default: str = "hybrid",
|
|
47
|
+
corrective: bool = True,
|
|
48
|
+
threshold: float = 70.0,
|
|
49
|
+
max_retries: int = 2,
|
|
50
|
+
) -> None:
|
|
51
|
+
if not strategies:
|
|
52
|
+
raise ValueError("RAGRouter needs at least one strategy")
|
|
53
|
+
self.strategies = dict(strategies)
|
|
54
|
+
self.provider = provider
|
|
55
|
+
self.default = default if default in self.strategies else next(iter(self.strategies))
|
|
56
|
+
self.corrective = corrective
|
|
57
|
+
self.threshold = threshold
|
|
58
|
+
self.max_retries = max_retries
|
|
59
|
+
|
|
60
|
+
def classify(self, query: str) -> str:
|
|
61
|
+
"""Pick a strategy name. Heuristic first, provider may override."""
|
|
62
|
+
# 1. Cheap heuristic — works with no provider.
|
|
63
|
+
heuristic = self.default
|
|
64
|
+
if "multimodal" in self.strategies and _MULTIMODAL_HINTS.search(query):
|
|
65
|
+
heuristic = "multimodal"
|
|
66
|
+
elif "graph" in self.strategies and _GRAPH_HINTS.search(query):
|
|
67
|
+
heuristic = "graph"
|
|
68
|
+
elif "agentic" in self.strategies and _AGENTIC_HINTS.search(query):
|
|
69
|
+
heuristic = "agentic"
|
|
70
|
+
|
|
71
|
+
# 2. Provider override. If it returns a non-registered strategy, we
|
|
72
|
+
# ignore it (never trust a free-form provider blindly).
|
|
73
|
+
try:
|
|
74
|
+
result = self.provider.run(AgentRequest(
|
|
75
|
+
role="rag-route",
|
|
76
|
+
prompt=f"Classify retrieval strategy for: {query}",
|
|
77
|
+
context={
|
|
78
|
+
"query": query,
|
|
79
|
+
"available": sorted(self.strategies.keys()),
|
|
80
|
+
"heuristic": heuristic,
|
|
81
|
+
},
|
|
82
|
+
))
|
|
83
|
+
chosen = (result.artifacts or {}).get("strategy")
|
|
84
|
+
if isinstance(chosen, str) and chosen in self.strategies:
|
|
85
|
+
return chosen
|
|
86
|
+
except Exception:
|
|
87
|
+
# Provider misbehaved — keep the heuristic, never crash retrieval.
|
|
88
|
+
pass
|
|
89
|
+
return heuristic
|
|
90
|
+
|
|
91
|
+
def retrieve(self, query: str, k: int = 5) -> RetrievalResult:
|
|
92
|
+
name = self.classify(query)
|
|
93
|
+
inner = self.strategies[name]
|
|
94
|
+
if self.corrective:
|
|
95
|
+
wrapped = CorrectiveRetriever(
|
|
96
|
+
inner,
|
|
97
|
+
self.provider,
|
|
98
|
+
threshold=self.threshold,
|
|
99
|
+
max_retries=self.max_retries,
|
|
100
|
+
)
|
|
101
|
+
result = wrapped.retrieve(query, k=k)
|
|
102
|
+
# Surface the chosen inner strategy in the result for observability.
|
|
103
|
+
result.strategy = f"router({name})->{result.strategy}"
|
|
104
|
+
return result
|
|
105
|
+
result = inner.retrieve(query, k=k)
|
|
106
|
+
result.strategy = f"router({name})->{result.strategy}"
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
def names(self) -> Iterable[str]:
|
|
110
|
+
return self.strategies.keys()
|
|
@@ -63,14 +63,32 @@ def reduce(state: Optional[TaskState], event: Event) -> TaskState:
|
|
|
63
63
|
return _TRANSITIONS[key]
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def reduce_task(events: Iterable[Event]
|
|
66
|
+
def reduce_task(events: Iterable[Event],
|
|
67
|
+
initial: Optional[TaskState] = None) -> TaskState:
|
|
67
68
|
"""Fold an ordered event stream into the current task state.
|
|
68
69
|
|
|
69
|
-
The events MUST be ordered (the Event Store returns them by
|
|
70
|
+
The events MUST be ordered (the Event Store returns them by rowid).
|
|
71
|
+
`initial` lets a snapshot-aware caller start the fold from a known state
|
|
72
|
+
(`store.events_since_snapshot` + `snap.state`) instead of from genesis.
|
|
70
73
|
"""
|
|
71
|
-
state: Optional[TaskState] =
|
|
74
|
+
state: Optional[TaskState] = initial
|
|
72
75
|
for ev in events:
|
|
73
76
|
state = reduce(state, ev)
|
|
74
77
|
if state is None:
|
|
75
78
|
raise IllegalTransition("empty event stream — task never created")
|
|
76
79
|
return state
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def reduce_task_fast(store, task_id: str) -> TaskState:
|
|
83
|
+
"""Reduce a task using its latest snapshot if available, else full replay.
|
|
84
|
+
|
|
85
|
+
Bounded reduction time on a long event log. Correctness is identical to
|
|
86
|
+
`reduce_task(store.events_for(task_id))`.
|
|
87
|
+
"""
|
|
88
|
+
snap = store.latest_snapshot(task_id)
|
|
89
|
+
if snap is None:
|
|
90
|
+
return reduce_task(store.events_for(task_id))
|
|
91
|
+
events_after = store.events_since_snapshot(task_id)
|
|
92
|
+
if not events_after:
|
|
93
|
+
return snap["state"]
|
|
94
|
+
return reduce_task(events_after, initial=snap["state"])
|
|
@@ -32,3 +32,31 @@ class ModelRouter:
|
|
|
32
32
|
def single(cls, provider: AgentProvider) -> "ModelRouter":
|
|
33
33
|
"""A router that sends every role to one provider — used for tests."""
|
|
34
34
|
return cls({provider.id: provider}, {}, provider.id)
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def auto(cls) -> "ModelRouter":
|
|
38
|
+
"""Build the best router for the host.
|
|
39
|
+
|
|
40
|
+
Preference order:
|
|
41
|
+
|
|
42
|
+
1. ``claude`` CLI on PATH + logged in → ClaudeMaxProvider
|
|
43
|
+
(Max subscription, no API key).
|
|
44
|
+
2. ``ANTHROPIC_API_KEY`` env var set → ClaudeProvider (API).
|
|
45
|
+
3. Otherwise → MockProvider (deterministic offline).
|
|
46
|
+
|
|
47
|
+
Use this everywhere the engine needs a "sensible default" — the
|
|
48
|
+
mission runner, smoke, pursue, audit pipeline. Tests still use
|
|
49
|
+
``single(MockProvider())`` for determinism.
|
|
50
|
+
"""
|
|
51
|
+
import os
|
|
52
|
+
from omega_engine.provider import (
|
|
53
|
+
ClaudeMaxProvider,
|
|
54
|
+
ClaudeProvider,
|
|
55
|
+
MockProvider,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if ClaudeMaxProvider.is_available():
|
|
59
|
+
return cls.single(ClaudeMaxProvider())
|
|
60
|
+
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
61
|
+
return cls.single(ClaudeProvider())
|
|
62
|
+
return cls.single(MockProvider())
|