apsimo 1.3.0__py3-none-any.whl
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.
- apsimo/__init__.py +38 -0
- apsimo/__main__.py +6 -0
- apsimo/agent/__init__.py +6 -0
- apsimo/agent/client.py +276 -0
- apsimo/agent/models.py +46 -0
- apsimo/agents/__init__.py +20 -0
- apsimo/agents/models.py +264 -0
- apsimo/agents/store.py +861 -0
- apsimo/agents/websocket.py +522 -0
- apsimo/api/__init__.py +1 -0
- apsimo/api/auth_telemetry.py +287 -0
- apsimo/api/authority.py +1203 -0
- apsimo/api/contact_grants.py +347 -0
- apsimo/api/middleware.py +483 -0
- apsimo/api/routers/__init__.py +1 -0
- apsimo/api/routers/commitment_work.py +265 -0
- apsimo/api/routers/context_gate.py +123 -0
- apsimo/api/routers/executions.py +140 -0
- apsimo/api/routers/followup_plans.py +147 -0
- apsimo/api/routers/governed_actions.py +162 -0
- apsimo/api/routers/host.py +14473 -0
- apsimo/api/routers/initiative_work.py +115 -0
- apsimo/api/routers/mining.py +104 -0
- apsimo/api/routers/observations.py +110 -0
- apsimo/api/routers/social_state.py +225 -0
- apsimo/api/routers/task_queue.py +2715 -0
- apsimo/api/routers/temporal_followups.py +251 -0
- apsimo/api/routers/transport.py +110 -0
- apsimo/api/routers/transport_ingress_api.py +240 -0
- apsimo/api/schemas/__init__.py +1 -0
- apsimo/api/schemas/host.py +1949 -0
- apsimo/autonomy/cli.py +110 -0
- apsimo/autonomy/condition_worker.py +437 -0
- apsimo/autonomy/config.py +424 -0
- apsimo/autonomy/loop.py +4316 -0
- apsimo/autonomy/registry.py +339 -0
- apsimo/autonomy/scheduler.py +1822 -0
- apsimo/autonomy/synthesis.py +449 -0
- apsimo/backup.py +962 -0
- apsimo/beliefs/__init__.py +23 -0
- apsimo/beliefs/contradictions.py +109 -0
- apsimo/beliefs/decay.py +61 -0
- apsimo/beliefs/engine.py +479 -0
- apsimo/beliefs/models.py +67 -0
- apsimo/beliefs/promotion.py +41 -0
- apsimo/beliefs/resolve.py +58 -0
- apsimo/beliefs/source_claims.py +690 -0
- apsimo/beliefs/source_projection.py +883 -0
- apsimo/beliefs/source_time.py +208 -0
- apsimo/beliefs/store.py +133 -0
- apsimo/briefings/aggregators.py +824 -0
- apsimo/briefings/composer.py +420 -0
- apsimo/briefings/config.py +55 -0
- apsimo/briefings/delivery.py +439 -0
- apsimo/briefings/engagement.py +97 -0
- apsimo/briefings/engine.py +274 -0
- apsimo/briefings/enhancer.py +99 -0
- apsimo/briefings/models.py +183 -0
- apsimo/briefings/scheduler.py +382 -0
- apsimo/briefings/store.py +435 -0
- apsimo/chain/__init__.py +48 -0
- apsimo/chain/block.py +100 -0
- apsimo/chain/cli.py +704 -0
- apsimo/chain/genesis.py +443 -0
- apsimo/chain/identity.py +416 -0
- apsimo/chain/keys.py +1025 -0
- apsimo/chain/local_keys.py +187 -0
- apsimo/chain/manager.py +290 -0
- apsimo/chain/node.py +163 -0
- apsimo/chain/plugin_transactions.py +371 -0
- apsimo/chain/protocol.py +220 -0
- apsimo/chain/state_machine.py +676 -0
- apsimo/chain/storage.py +503 -0
- apsimo/chain/transactions.py +250 -0
- apsimo/chain/validation.py +397 -0
- apsimo/channels/__init__.py +1 -0
- apsimo/channels/manifest.py +31 -0
- apsimo/channels/migrations/001_channels_schema.sql +12 -0
- apsimo/channels/phone_gateways.py +42 -0
- apsimo/channels/presence.py +188 -0
- apsimo/channels/router.py +235 -0
- apsimo/channels/store.py +231 -0
- apsimo/cli.py +2688 -0
- apsimo/cognition/__init__.py +11 -0
- apsimo/cognition/charter.py +398 -0
- apsimo/cognition/drive_governance.py +3530 -0
- apsimo/cognition/evidence_pipeline.py +1627 -0
- apsimo/cognition/external_events.py +932 -0
- apsimo/cognition/goal_spine.py +3488 -0
- apsimo/cognition/introspection.py +214 -0
- apsimo/cognition/prompt.py +150 -0
- apsimo/cognition/runtime.py +108 -0
- apsimo/cognition/trigger.py +154 -0
- apsimo/commitments/__init__.py +18 -0
- apsimo/commitments/local_work.py +355 -0
- apsimo/commitments/store.py +1052 -0
- apsimo/commitments/work.py +91 -0
- apsimo/compat.py +53 -0
- apsimo/compression/__init__.py +467 -0
- apsimo/connectors/__init__.py +21 -0
- apsimo/connectors/base.py +152 -0
- apsimo/connectors/caldav_calendar.py +125 -0
- apsimo/connectors/fs_documents.py +85 -0
- apsimo/connectors/imap_email.py +138 -0
- apsimo/connectors/manager.py +218 -0
- apsimo/connectors/webhook_pull.py +88 -0
- apsimo/contacts/__init__.py +33 -0
- apsimo/contacts/comms.py +357 -0
- apsimo/contacts/config.py +79 -0
- apsimo/contacts/exporters/__init__.py +1 -0
- apsimo/contacts/exporters/vcard.py +71 -0
- apsimo/contacts/identity_links.py +251 -0
- apsimo/contacts/importer.py +280 -0
- apsimo/contacts/importers/__init__.py +1 -0
- apsimo/contacts/importers/batch.py +43 -0
- apsimo/contacts/importers/macos_contacts.py +101 -0
- apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
- apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
- apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
- apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
- apsimo/contacts/migrations/005_identity_links.sql +27 -0
- apsimo/contacts/models.py +308 -0
- apsimo/contacts/scoring.py +16 -0
- apsimo/contacts/store.py +1623 -0
- apsimo/contacts/transport_ingress.py +252 -0
- apsimo/contacts/world_bridge.py +314 -0
- apsimo/contextgate/__init__.py +69 -0
- apsimo/contextgate/chunker.py +169 -0
- apsimo/contextgate/estimate.py +54 -0
- apsimo/contextgate/gate.py +313 -0
- apsimo/contextgate/retrieve.py +115 -0
- apsimo/delivery/__init__.py +16 -0
- apsimo/delivery/bridge.py +1260 -0
- apsimo/delivery/channels.py +526 -0
- apsimo/delivery/classification.py +50 -0
- apsimo/delivery/rate_limiter.py +268 -0
- apsimo/delivery/reachout_policy.py +206 -0
- apsimo/directed/__init__.py +22 -0
- apsimo/directed/audit.py +167 -0
- apsimo/directed/intake.py +95 -0
- apsimo/directed/models.py +191 -0
- apsimo/directed/service.py +509 -0
- apsimo/directives/__init__.py +25 -0
- apsimo/directives/evidence.py +87 -0
- apsimo/directives/extractor.py +188 -0
- apsimo/directives/guard.py +364 -0
- apsimo/directives/models.py +206 -0
- apsimo/directives/service.py +372 -0
- apsimo/directives/store.py +167 -0
- apsimo/doctor.py +2173 -0
- apsimo/environment.py +43 -0
- apsimo/events/__init__.py +33 -0
- apsimo/events/broadcaster.py +98 -0
- apsimo/events/bus.py +217 -0
- apsimo/events/journal.py +863 -0
- apsimo/events/stream.py +131 -0
- apsimo/events/types.py +150 -0
- apsimo/execution_results.py +357 -0
- apsimo/feedback/__init__.py +5 -0
- apsimo/feedback/store.py +76 -0
- apsimo/feeds/__init__.py +19 -0
- apsimo/feeds/cli.py +84 -0
- apsimo/feeds/engine.py +437 -0
- apsimo/feeds/example-feed.yaml +77 -0
- apsimo/feeds/hermes_cron.py +126 -0
- apsimo/feeds/manager.py +235 -0
- apsimo/feeds/spec.py +250 -0
- apsimo/feeds/template.py +202 -0
- apsimo/gate/__init__.py +18 -0
- apsimo/gate/audit.py +61 -0
- apsimo/gate/communication_policy.py +166 -0
- apsimo/gate/config.py +72 -0
- apsimo/gate/context_provenance.py +170 -0
- apsimo/gate/env_risk.py +226 -0
- apsimo/gate/guard_audit.py +353 -0
- apsimo/gate/layers/__init__.py +1 -0
- apsimo/gate/layers/base.py +15 -0
- apsimo/gate/layers/l1_recipient.py +66 -0
- apsimo/gate/layers/l2_pii.py +134 -0
- apsimo/gate/layers/l3_cross_context.py +50 -0
- apsimo/gate/layers/l4_trust_tier.py +78 -0
- apsimo/gate/layers/l5_injection.py +199 -0
- apsimo/gate/layers/l6_review.py +86 -0
- apsimo/gate/layers/l7_delay.py +100 -0
- apsimo/gate/layers/tom2_epistemic.py +185 -0
- apsimo/gate/models.py +64 -0
- apsimo/gate/pending_dispatch.py +5 -0
- apsimo/gate/pipeline.py +206 -0
- apsimo/gate/rejection.py +259 -0
- apsimo/gate/response_guard.py +700 -0
- apsimo/gate/rulesets/injection_v1.yaml +51 -0
- apsimo/gate/surface_policy.py +189 -0
- apsimo/gate/taint.py +226 -0
- apsimo/genesis.json +9 -0
- apsimo/goals/__init__.py +100 -0
- apsimo/goals/config.py +38 -0
- apsimo/goals/decomposer.py +421 -0
- apsimo/goals/engine.py +617 -0
- apsimo/goals/inference.py +354 -0
- apsimo/goals/models.py +302 -0
- apsimo/goals/priority.py +270 -0
- apsimo/goals/queue_bridge.py +149 -0
- apsimo/goals/replan.py +450 -0
- apsimo/goals/schema.sql +89 -0
- apsimo/goals/store.py +692 -0
- apsimo/governed_actions.py +1708 -0
- apsimo/harness_integration/__init__.py +45 -0
- apsimo/harness_integration/context.py +41 -0
- apsimo/harness_integration/skills.py +231 -0
- apsimo/identity/__init__.py +26 -0
- apsimo/identity/participants.py +181 -0
- apsimo/identity/resolver.py +329 -0
- apsimo/identity_bootstrap/__init__.py +5 -0
- apsimo/identity_bootstrap/builder.py +208 -0
- apsimo/identity_bootstrap/corpus.py +443 -0
- apsimo/identity_bootstrap/models.py +54 -0
- apsimo/identity_bootstrap/runner.py +353 -0
- apsimo/identity_bootstrap/seeders/__init__.py +25 -0
- apsimo/identity_bootstrap/seeders/briefings.py +109 -0
- apsimo/identity_bootstrap/seeders/chain.py +57 -0
- apsimo/identity_bootstrap/seeders/goals.py +128 -0
- apsimo/identity_bootstrap/seeders/memory.py +191 -0
- apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
- apsimo/identity_bootstrap/seeders/relationship.py +152 -0
- apsimo/identity_bootstrap/seeders/sessions.py +67 -0
- apsimo/identity_bootstrap/seeders/skills.py +92 -0
- apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
- apsimo/identity_bootstrap/seeders/world_model.py +143 -0
- apsimo/identity_bootstrap/self_query.py +92 -0
- apsimo/identity_bootstrap/self_reflection.py +155 -0
- apsimo/identity_bootstrap/skill.py +37 -0
- apsimo/identity_bootstrap/verifier.py +436 -0
- apsimo/initiatives/__init__.py +20 -0
- apsimo/initiatives/action_registry.py +454 -0
- apsimo/initiatives/approval_authority.py +2105 -0
- apsimo/initiatives/approval_policy.py +123 -0
- apsimo/initiatives/assignment.py +263 -0
- apsimo/initiatives/backup_evidence.py +100 -0
- apsimo/initiatives/context_freshness.py +103 -0
- apsimo/initiatives/models.py +318 -0
- apsimo/initiatives/native_work.py +270 -0
- apsimo/initiatives/standing_approvals.py +232 -0
- apsimo/initiatives/store.py +1081 -0
- apsimo/initiatives/temporal_followup.py +410 -0
- apsimo/intelligence/__init__.py +1 -0
- apsimo/intelligence/cognition/__init__.py +24 -0
- apsimo/intelligence/cognition/gap_detector.py +148 -0
- apsimo/intelligence/cognition/metalearner.py +547 -0
- apsimo/intelligence/cognition/metrics_collector.py +217 -0
- apsimo/intelligence/cognition/performance_index.py +299 -0
- apsimo/intelligence/cognition/registry.py +192 -0
- apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
- apsimo/intelligence/cognition/types.py +16 -0
- apsimo/intelligence/components/__init__.py +66 -0
- apsimo/intelligence/components/anomaly_detector.py +413 -0
- apsimo/intelligence/components/initiative_engine.py +2643 -0
- apsimo/intelligence/components/preference_learner.py +521 -0
- apsimo/intelligence/components/research_orchestrator.py +358 -0
- apsimo/intelligence/components/self_directed_thinker.py +221 -0
- apsimo/intelligence/components/self_reflector.py +252 -0
- apsimo/intelligence/components/session_continuity.py +154 -0
- apsimo/intelligence/components/task_planner.py +320 -0
- apsimo/intelligence/components/tool_learner.py +217 -0
- apsimo/intelligence/graph/__init__.py +79 -0
- apsimo/intelligence/graph/client.py +2483 -0
- apsimo/intelligence/graph/consolidator.py +405 -0
- apsimo/intelligence/graph/distiller.py +312 -0
- apsimo/intelligence/graph/migrations.py +129 -0
- apsimo/intelligence/graph/queries.py +248 -0
- apsimo/intelligence/graph/recall.py +281 -0
- apsimo/intelligence/graph/reconciler.py +144 -0
- apsimo/intelligence/graph/schema.py +337 -0
- apsimo/intelligence/graph/selection.py +252 -0
- apsimo/intelligence/learning/__init__.py +17 -0
- apsimo/intelligence/learning/continuous_learner.py +245 -0
- apsimo/intelligence/learning/feedback_store.py +321 -0
- apsimo/intelligence/mind_model/__init__.py +1 -0
- apsimo/intelligence/mind_model/graph_baseline.py +136 -0
- apsimo/intelligence/mind_model/signal_collector.py +361 -0
- apsimo/intelligence/relationships/__init__.py +11 -0
- apsimo/intelligence/relationships/profiler.py +389 -0
- apsimo/intelligence/relationships/scorer.py +560 -0
- apsimo/intelligence/relationships/signal_floor.py +66 -0
- apsimo/intelligence/relationships/trust_tiers.py +300 -0
- apsimo/intelligence/synthesis/__init__.py +40 -0
- apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
- apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
- apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
- apsimo/intelligence/synthesis/insight_store.py +79 -0
- apsimo/intelligence/synthesis/insight_validator.py +183 -0
- apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
- apsimo/intelligence/turn_middleware/__init__.py +15 -0
- apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
- apsimo/mcp/__init__.py +41 -0
- apsimo/mcp/__main__.py +6 -0
- apsimo/mcp/config.py +287 -0
- apsimo/mcp/server.py +501 -0
- apsimo/migrations.py +187 -0
- apsimo/mining/__init__.py +27 -0
- apsimo/mining/corpus.py +239 -0
- apsimo/mining/escalations.py +289 -0
- apsimo/mining/models.py +169 -0
- apsimo/mining/store.py +210 -0
- apsimo/models/__init__.py +30 -0
- apsimo/models/memory.py +80 -0
- apsimo/models/mesh.py +72 -0
- apsimo/models/person.py +104 -0
- apsimo/models/signal.py +108 -0
- apsimo/observations/__init__.py +15 -0
- apsimo/observations/store.py +277 -0
- apsimo/patterns/__init__.py +6 -0
- apsimo/patterns/extract.py +187 -0
- apsimo/patterns/store.py +227 -0
- apsimo/persona/__init__.py +1 -0
- apsimo/persona/engine.py +611 -0
- apsimo/persona/manifest.py +140 -0
- apsimo/projects/__init__.py +28 -0
- apsimo/projects/engine.py +1681 -0
- apsimo/projects/event_outbox.py +188 -0
- apsimo/projects/models.py +216 -0
- apsimo/projects/planner.py +181 -0
- apsimo/projects/store.py +1446 -0
- apsimo/proposals/__init__.py +12 -0
- apsimo/proposals/engine.py +114 -0
- apsimo/proposals/models.py +207 -0
- apsimo/qualification/__init__.py +1 -0
- apsimo/qualification/cases.py +75 -0
- apsimo/qualification/cli.py +51 -0
- apsimo/qualification/memory_cases.py +209 -0
- apsimo/qualification/records.py +92 -0
- apsimo/qualification/report.py +87 -0
- apsimo/qualification/runner.py +311 -0
- apsimo/qualification/structured_cases.py +131 -0
- apsimo/reasoning/__init__.py +13 -0
- apsimo/reasoning/executor.py +506 -0
- apsimo/reasoning/loop.py +373 -0
- apsimo/reasoning/native_tools/__init__.py +16 -0
- apsimo/reasoning/native_tools/calculate.py +141 -0
- apsimo/reasoning/native_tools/file_ops.py +150 -0
- apsimo/reasoning/native_tools/web_search.py +49 -0
- apsimo/reasoning/tool_policy.py +182 -0
- apsimo/redact/__init__.py +176 -0
- apsimo/repos/__init__.py +5 -0
- apsimo/repos/mirrors.py +204 -0
- apsimo/research/__init__.py +41 -0
- apsimo/research/artifact.py +482 -0
- apsimo/research/gatherer.py +387 -0
- apsimo/research/pipeline.py +513 -0
- apsimo/research/search/__init__.py +7 -0
- apsimo/research/search/base.py +41 -0
- apsimo/research/search/brave.py +59 -0
- apsimo/research/search/cache.py +51 -0
- apsimo/research/search/duckduckgo.py +103 -0
- apsimo/research/search/orchestrator.py +119 -0
- apsimo/research/search/serpapi.py +59 -0
- apsimo/research/search/tavily.py +59 -0
- apsimo/research/synthesizer.py +309 -0
- apsimo/router/__init__.py +30 -0
- apsimo/router/complexity_scorer.py +148 -0
- apsimo/router/endpoints.py +153 -0
- apsimo/router/fallback.py +58 -0
- apsimo/router/functions.py +243 -0
- apsimo/router/native_policy.py +52 -0
- apsimo/router/router.py +762 -0
- apsimo/router/self_learning.py +174 -0
- apsimo/router/tiers.py +677 -0
- apsimo/sandbox/__init__.py +21 -0
- apsimo/sandbox/backend.py +195 -0
- apsimo/sandbox/manager.py +173 -0
- apsimo/scope_bounds.py +7 -0
- apsimo/secrets/__init__.py +6 -0
- apsimo/secrets/backends/__init__.py +8 -0
- apsimo/secrets/backends/base.py +42 -0
- apsimo/secrets/backends/env.py +110 -0
- apsimo/secrets/backends/keyring.py +72 -0
- apsimo/secrets/backends/onepassword.py +232 -0
- apsimo/secrets/cli.py +191 -0
- apsimo/secrets/manager.py +160 -0
- apsimo/secrets/migration.py +101 -0
- apsimo/secrets/types.py +98 -0
- apsimo/seed.py +41 -0
- apsimo/self_model/__init__.py +37 -0
- apsimo/self_model/appraisals.py +673 -0
- apsimo/self_model/benchmark.py +1314 -0
- apsimo/self_model/brief.py +40 -0
- apsimo/self_model/event_concerns.py +1128 -0
- apsimo/self_model/execution_forecasts.py +353 -0
- apsimo/self_model/expectations.py +1595 -0
- apsimo/self_model/experiments.py +1150 -0
- apsimo/self_model/journal.py +148 -0
- apsimo/self_model/judgments.py +705 -0
- apsimo/self_model/native_outcomes.py +55 -0
- apsimo/self_model/params.py +220 -0
- apsimo/self_model/perspective.py +246 -0
- apsimo/self_model/reconcile.py +183 -0
- apsimo/self_model/reply_forecasts.py +381 -0
- apsimo/self_model/runtime_forecasts.py +296 -0
- apsimo/self_model/runtime_models.py +67 -0
- apsimo/self_model/settlement.py +207 -0
- apsimo/self_model/situation.py +1731 -0
- apsimo/self_model/store.py +883 -0
- apsimo/self_model/supervised.py +137 -0
- apsimo/self_model/thinker.py +99 -0
- apsimo/self_model/trust.py +388 -0
- apsimo/self_model/workspace.py +2388 -0
- apsimo/server.py +4197 -0
- apsimo/services/__init__.py +1 -0
- apsimo/services/agent_bridge.py +474 -0
- apsimo/services/initiative_executor.py +914 -0
- apsimo/services/instance.py +297 -0
- apsimo/sessions/__init__.py +22 -0
- apsimo/sessions/config.py +13 -0
- apsimo/sessions/context_loader.py +88 -0
- apsimo/sessions/federation_session.py +75 -0
- apsimo/sessions/isolated_session.py +98 -0
- apsimo/sessions/reports.py +84 -0
- apsimo/sessions/store.py +148 -0
- apsimo/setup.py +2818 -0
- apsimo/setup_hermes.py +879 -0
- apsimo/setup_local_work.py +218 -0
- apsimo/setup_native_goals.py +134 -0
- apsimo/setup_native_reviews.py +115 -0
- apsimo/skills/__init__.py +10 -0
- apsimo/skills/base.py +108 -0
- apsimo/skills/budget.py +28 -0
- apsimo/skills/executor.py +493 -0
- apsimo/skills/executors/__init__.py +1 -0
- apsimo/skills/executors/behavioral_correction.py +75 -0
- apsimo/skills/executors/capability_gap.py +38 -0
- apsimo/skills/executors/data_quality.py +163 -0
- apsimo/skills/executors/knowledge_acquisition.py +41 -0
- apsimo/skills/executors/operational_hygiene.py +185 -0
- apsimo/skills/executors/subsystem_health.py +169 -0
- apsimo/skills/hermes_export.py +431 -0
- apsimo/skills/index.py +123 -0
- apsimo/skills/learning/__init__.py +21 -0
- apsimo/skills/learning/novelty_detector.py +206 -0
- apsimo/skills/learning/pattern_extractor.py +199 -0
- apsimo/skills/learning/triggers.py +159 -0
- apsimo/skills/loader.py +246 -0
- apsimo/skills/migrations/002_progressive_loading.sql +6 -0
- apsimo/skills/migrations/backfill_triggers.py +20 -0
- apsimo/skills/models.py +202 -0
- apsimo/skills/packager.py +128 -0
- apsimo/skills/protocols.py +70 -0
- apsimo/skills/registry.py +191 -0
- apsimo/skills/runtime.py +58 -0
- apsimo/skills/sandbox_runner.py +229 -0
- apsimo/skills/scheduler.py +129 -0
- apsimo/skills/schema.py +79 -0
- apsimo/skills/security/__init__.py +12 -0
- apsimo/skills/security/guards.py +53 -0
- apsimo/skills/security/scanner.py +223 -0
- apsimo/skills_memory/__init__.py +26 -0
- apsimo/skills_memory/distill.py +159 -0
- apsimo/skills_memory/models.py +85 -0
- apsimo/skills_memory/retrieve.py +62 -0
- apsimo/skills_memory/store.py +172 -0
- apsimo/surprise/__init__.py +6 -0
- apsimo/surprise/accumulation.py +57 -0
- apsimo/surprise/scorer.py +102 -0
- apsimo/surprise/store.py +203 -0
- apsimo/task_queue/__init__.py +69 -0
- apsimo/task_queue/action_receipts.py +148 -0
- apsimo/task_queue/approval_relay_canary.py +108 -0
- apsimo/task_queue/config.py +85 -0
- apsimo/task_queue/contract.py +361 -0
- apsimo/task_queue/events.py +130 -0
- apsimo/task_queue/governor.py +1031 -0
- apsimo/task_queue/handlers/__init__.py +16 -0
- apsimo/task_queue/handlers/base.py +37 -0
- apsimo/task_queue/handlers/inference.py +640 -0
- apsimo/task_queue/handlers/monitoring.py +116 -0
- apsimo/task_queue/handlers/registry.py +75 -0
- apsimo/task_queue/handlers/subtask_handler.py +173 -0
- apsimo/task_queue/handlers/system_maintenance.py +147 -0
- apsimo/task_queue/mesh_integration.py +111 -0
- apsimo/task_queue/models.py +317 -0
- apsimo/task_queue/queue_manager.py +8286 -0
- apsimo/task_queue/routing.py +287 -0
- apsimo/task_queue/scheduler.py +252 -0
- apsimo/task_queue/schema.sql +197 -0
- apsimo/task_queue/work_control.py +342 -0
- apsimo/task_queue/worker.py +993 -0
- apsimo/telemetry.py +145 -0
- apsimo/tom/__init__.py +6 -0
- apsimo/tom/affect.py +387 -0
- apsimo/tom/approvals.py +171 -0
- apsimo/tom/arcs.py +896 -0
- apsimo/tom/asymmetry.py +131 -0
- apsimo/tom/eligibility.py +248 -0
- apsimo/tom/engagement.py +214 -0
- apsimo/tom/exposure.py +214 -0
- apsimo/tom/extractor.py +306 -0
- apsimo/tom/fact_adapters.py +144 -0
- apsimo/tom/facts.py +326 -0
- apsimo/tom/integration.py +592 -0
- apsimo/tom/leveled.py +118 -0
- apsimo/tom/levels.py +247 -0
- apsimo/tom/recipient_audit.py +995 -0
- apsimo/tom/recipient_simulator.py +593 -0
- apsimo/tom/source_lineage.py +93 -0
- apsimo/tom/tom2.py +277 -0
- apsimo/tom/visibility.py +559 -0
- apsimo/tom/visibility_store.py +414 -0
- apsimo/tools/__init__.py +0 -0
- apsimo/tools/definitions.py +740 -0
- apsimo/tools/handlers.py +943 -0
- apsimo/toolsmith/__init__.py +26 -0
- apsimo/toolsmith/authority.py +166 -0
- apsimo/toolsmith/engine.py +559 -0
- apsimo/toolsmith/integrity.py +100 -0
- apsimo/toolsmith/miner.py +145 -0
- apsimo/toolsmith/policy.py +110 -0
- apsimo/toolsmith/registry.py +635 -0
- apsimo/turns/__init__.py +17 -0
- apsimo/turns/audio.py +134 -0
- apsimo/turns/documents.py +235 -0
- apsimo/turns/executions.py +486 -0
- apsimo/turns/hermes_history.py +245 -0
- apsimo/turns/hermes_kanban.py +268 -0
- apsimo/turns/hermes_work.py +96 -0
- apsimo/turns/idempotency.py +752 -0
- apsimo/turns/local_work.py +115 -0
- apsimo/turns/media.py +581 -0
- apsimo/turns/reported_workers.py +196 -0
- apsimo/turns/source_annotations.py +283 -0
- apsimo/turns/source_attribution.py +154 -0
- apsimo/turns/source_read.py +351 -0
- apsimo/turns/source_vectors.py +263 -0
- apsimo/turns/video.py +210 -0
- apsimo/util/autonomy_preset.py +220 -0
- apsimo/util/instance.py +92 -0
- apsimo/util/model_output.py +25 -0
- apsimo/util/quiet_hours.py +27 -0
- apsimo/util/session_safety.py +37 -0
- apsimo/util/temporal.py +343 -0
- apsimo/vector/__init__.py +75 -0
- apsimo/vector/backfill.py +171 -0
- apsimo/vector/caption.py +114 -0
- apsimo/vector/collections.py +51 -0
- apsimo/vector/config.py +102 -0
- apsimo/vector/embedder.py +670 -0
- apsimo/vector/image_preprocess.py +406 -0
- apsimo/vector/image_store.py +296 -0
- apsimo/vector/indexes.py +162 -0
- apsimo/vector/migrate.py +334 -0
- apsimo/vector/multimodal_provider.py +417 -0
- apsimo/vector/multimodal_types.py +87 -0
- apsimo/vector/openai_provider.py +119 -0
- apsimo/vector/query.py +49 -0
- apsimo/vector/reranker.py +565 -0
- apsimo/vector/safety_image.py +159 -0
- apsimo/vector/scanner.py +197 -0
- apsimo/vector/setup.py +289 -0
- apsimo/vector/store.py +533 -0
- apsimo/vector/tiers.py +263 -0
- apsimo/work_orders.py +925 -0
- apsimo/workers/__init__.py +21 -0
- apsimo/workers/agent_bridge.py +640 -0
- apsimo/workers/colony_worker.py +382 -0
- apsimo/workers/queue_worker.py +441 -0
- apsimo/workers/skills_sync.py +152 -0
- apsimo/world_model/__init__.py +71 -0
- apsimo/world_model/causal_maintenance.py +131 -0
- apsimo/world_model/causal_policy.py +43 -0
- apsimo/world_model/causal_query.py +125 -0
- apsimo/world_model/confidence.py +54 -0
- apsimo/world_model/config.py +64 -0
- apsimo/world_model/constants.py +97 -0
- apsimo/world_model/entities.py +145 -0
- apsimo/world_model/expectation_resolvers.py +177 -0
- apsimo/world_model/extraction/__init__.py +7 -0
- apsimo/world_model/extraction/base.py +62 -0
- apsimo/world_model/extraction/conversation_extractor.py +262 -0
- apsimo/world_model/extraction/detector.py +74 -0
- apsimo/world_model/extraction/document_extractor.py +78 -0
- apsimo/world_model/extraction/formats/__init__.py +24 -0
- apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
- apsimo/world_model/extraction/formats/html_fmt.py +72 -0
- apsimo/world_model/extraction/formats/json_fmt.py +68 -0
- apsimo/world_model/extraction/formats/pdf.py +43 -0
- apsimo/world_model/extraction/formats/text.py +27 -0
- apsimo/world_model/extraction/llm_extractor.py +164 -0
- apsimo/world_model/extraction/pipeline.py +73 -0
- apsimo/world_model/integrations/__init__.py +5 -0
- apsimo/world_model/integrations/mind_model_bridge.py +115 -0
- apsimo/world_model/integrations/social_intel_bridge.py +120 -0
- apsimo/world_model/jobs/__init__.py +4 -0
- apsimo/world_model/jobs/extraction_job.py +168 -0
- apsimo/world_model/llm_extract.py +572 -0
- apsimo/world_model/neo4j/__init__.py +5 -0
- apsimo/world_model/neo4j/backend.py +654 -0
- apsimo/world_model/observations.py +155 -0
- apsimo/world_model/populator.py +307 -0
- apsimo/world_model/postgres/__init__.py +1 -0
- apsimo/world_model/postgres/backend.py +683 -0
- apsimo/world_model/relationships.py +25 -0
- apsimo/world_model/resolution/__init__.py +13 -0
- apsimo/world_model/resolution/entity_resolver.py +232 -0
- apsimo/world_model/resolution/merge_audit.py +16 -0
- apsimo/world_model/resolution/merge_workflow.py +117 -0
- apsimo/world_model/source_reports.py +121 -0
- apsimo/world_model/sqlite/__init__.py +4 -0
- apsimo/world_model/sqlite/backend.py +855 -0
- apsimo/world_model/sqlite/schema.sql +132 -0
- apsimo/world_model/store.py +545 -0
- apsimo-1.3.0.dist-info/METADATA +78 -0
- apsimo-1.3.0.dist-info/RECORD +614 -0
- apsimo-1.3.0.dist-info/WHEEL +5 -0
- apsimo-1.3.0.dist-info/entry_points.txt +11 -0
- apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
- apsimo-1.3.0.dist-info/top_level.txt +2 -0
- colony_sidecar/__init__.py +4 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Complexity scorer — heuristic prompt analysis for tier selection.
|
|
2
|
+
|
|
3
|
+
Scores range 0.0–1.0:
|
|
4
|
+
0.00–0.30 → SMALL (simple Q&A, factual lookups, short summaries)
|
|
5
|
+
0.30–0.65 → MEDIUM (multi-step reasoning, code generation, analysis)
|
|
6
|
+
0.65–1.00 → LARGE (complex architecture, deep research, critical tasks)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from apsimo.router.tiers import ModelTier
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class ComplexitySignals:
|
|
19
|
+
token_count: int
|
|
20
|
+
has_code: bool
|
|
21
|
+
has_math: bool
|
|
22
|
+
has_multi_step: bool # "first... then... finally..."
|
|
23
|
+
has_reasoning_required: bool # "why", "explain", "analyze"
|
|
24
|
+
has_tool_use: bool
|
|
25
|
+
conversation_depth: int # number of prior turns
|
|
26
|
+
user_tier: str # "standard" | "power" | "developer"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Keyword sets for signal extraction
|
|
30
|
+
_MULTI_STEP_PATTERNS = re.compile(
|
|
31
|
+
r"\b(first|step \d|then|after that|finally|next|followed by|subsequently)\b",
|
|
32
|
+
re.IGNORECASE,
|
|
33
|
+
)
|
|
34
|
+
_REASONING_PATTERNS = re.compile(
|
|
35
|
+
r"\b(why|explain|analyze|analyse|compare|contrast|evaluate|assess|critique|"
|
|
36
|
+
r"debate|argue|justify|prove|derive|infer|reason|think through|walk me through)\b",
|
|
37
|
+
re.IGNORECASE,
|
|
38
|
+
)
|
|
39
|
+
_CODE_PATTERNS = re.compile(
|
|
40
|
+
r"(```|\bdef \b|\bclass \b|\bimport \b|\bfunction\b|\bvoid \b|\bpublic \b|"
|
|
41
|
+
r"\bprivate \b|<code>|<script)",
|
|
42
|
+
re.IGNORECASE,
|
|
43
|
+
)
|
|
44
|
+
_MATH_PATTERNS = re.compile(
|
|
45
|
+
r"(\$\$?|\\\(|\bintegral\b|\bderivative\b|\bmatrix\b|\bequation\b|"
|
|
46
|
+
r"\balgebra\b|\bcalculus\b|\bstatistics\b|\bprobability\b)",
|
|
47
|
+
re.IGNORECASE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Signal weights for the final score.
|
|
51
|
+
# Weights are intentionally skewed toward reasoning/multi-step signals
|
|
52
|
+
# because those are the strongest predictors of tasks that genuinely need
|
|
53
|
+
# a larger model (architecture analysis, complex debugging, etc.).
|
|
54
|
+
_WEIGHTS = {
|
|
55
|
+
"token_count": 0.12,
|
|
56
|
+
"has_code": 0.12,
|
|
57
|
+
"has_math": 0.08,
|
|
58
|
+
"has_multi_step": 0.18,
|
|
59
|
+
"has_reasoning_required": 0.22,
|
|
60
|
+
"has_tool_use": 0.08,
|
|
61
|
+
"conversation_depth": 0.06,
|
|
62
|
+
"user_tier": 0.05,
|
|
63
|
+
# Bonus applied when BOTH multi-step AND reasoning are detected.
|
|
64
|
+
# A prompt that requires step-by-step thinking AND analytical reasoning
|
|
65
|
+
# is reliably in the LARGE tier regardless of token count.
|
|
66
|
+
"combined_reasoning_bonus": 0.20,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_TOKEN_SCALE = 500 # tokens above which token_count score saturates at 1.0
|
|
70
|
+
_DEPTH_SCALE = 10 # turns above which depth score saturates at 1.0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class ComplexityScorer:
|
|
74
|
+
"""Score prompt complexity to select an appropriate model tier."""
|
|
75
|
+
|
|
76
|
+
def score(self, prompt: str, context: dict | None = None) -> float:
|
|
77
|
+
"""Return a complexity score in [0.0, 1.0]."""
|
|
78
|
+
signals = self._extract_signals(prompt, context or {})
|
|
79
|
+
return self._compute_score(signals)
|
|
80
|
+
|
|
81
|
+
def select_tier(self, prompt: str, context: dict | None = None) -> ModelTier:
|
|
82
|
+
"""Return the cheapest model tier appropriate for this prompt."""
|
|
83
|
+
score = self.score(prompt, context)
|
|
84
|
+
if score < 0.3:
|
|
85
|
+
return ModelTier.SMALL
|
|
86
|
+
elif score < 0.65:
|
|
87
|
+
return ModelTier.MEDIUM
|
|
88
|
+
else:
|
|
89
|
+
return ModelTier.LARGE
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------
|
|
92
|
+
# Internal helpers
|
|
93
|
+
# ------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def _extract_signals(self, prompt: str, context: dict) -> ComplexitySignals:
|
|
96
|
+
# Rough token estimate: ~4 chars per token
|
|
97
|
+
token_count = max(1, len(prompt) // 4)
|
|
98
|
+
|
|
99
|
+
messages = context.get("messages", [])
|
|
100
|
+
depth = len(messages) if isinstance(messages, list) else 0
|
|
101
|
+
|
|
102
|
+
tools = context.get("tools", [])
|
|
103
|
+
has_tool_use = bool(tools) or "tool" in prompt.lower() or "function" in prompt.lower()
|
|
104
|
+
|
|
105
|
+
user_tier = context.get("user_tier", "standard")
|
|
106
|
+
|
|
107
|
+
return ComplexitySignals(
|
|
108
|
+
token_count=token_count,
|
|
109
|
+
has_code=bool(_CODE_PATTERNS.search(prompt)),
|
|
110
|
+
has_math=bool(_MATH_PATTERNS.search(prompt)),
|
|
111
|
+
has_multi_step=bool(_MULTI_STEP_PATTERNS.search(prompt)),
|
|
112
|
+
has_reasoning_required=bool(_REASONING_PATTERNS.search(prompt)),
|
|
113
|
+
has_tool_use=has_tool_use,
|
|
114
|
+
conversation_depth=depth,
|
|
115
|
+
user_tier=user_tier,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def _compute_score(self, signals: ComplexitySignals) -> float:
|
|
119
|
+
w = _WEIGHTS
|
|
120
|
+
|
|
121
|
+
# Normalise continuous signals to [0, 1]
|
|
122
|
+
token_score = min(1.0, signals.token_count / _TOKEN_SCALE)
|
|
123
|
+
depth_score = min(1.0, signals.conversation_depth / _DEPTH_SCALE)
|
|
124
|
+
tier_score = {"standard": 0.0, "power": 0.5, "developer": 1.0}.get(
|
|
125
|
+
signals.user_tier, 0.0
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Bonus: a prompt that requires BOTH step-by-step structure AND analytical
|
|
129
|
+
# reasoning is reliably complex enough to warrant the LARGE tier.
|
|
130
|
+
# 0.25 ensures reasoning + multi_step clears the 0.65 LARGE threshold.
|
|
131
|
+
combined_bonus = (
|
|
132
|
+
0.25
|
|
133
|
+
if signals.has_multi_step and signals.has_reasoning_required
|
|
134
|
+
else 0.0
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
raw = (
|
|
138
|
+
token_score * w["token_count"]
|
|
139
|
+
+ float(signals.has_code) * w["has_code"]
|
|
140
|
+
+ float(signals.has_math) * w["has_math"]
|
|
141
|
+
+ float(signals.has_multi_step) * w["has_multi_step"]
|
|
142
|
+
+ float(signals.has_reasoning_required) * w["has_reasoning_required"]
|
|
143
|
+
+ float(signals.has_tool_use) * w["has_tool_use"]
|
|
144
|
+
+ depth_score * w["conversation_depth"]
|
|
145
|
+
+ tier_score * w["user_tier"]
|
|
146
|
+
+ combined_bonus
|
|
147
|
+
)
|
|
148
|
+
return min(1.0, max(0.0, raw))
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Bounded observations of configured endpoints, separate from capabilities.
|
|
2
|
+
|
|
3
|
+
A model listing can omit a working request alias. Only completion outcomes
|
|
4
|
+
change routing availability; advertised metadata never grants tools or vision.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
from collections import OrderedDict
|
|
8
|
+
import hashlib
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def models_url(base_url):
|
|
14
|
+
base = base_url.rstrip('/')
|
|
15
|
+
return base + ('/models' if base.endswith('/v1') else '/v1/models')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class EndpointRuntime:
|
|
19
|
+
def __init__(self, *, cooldown=15, ttl=30, clock=time.monotonic, wall=time.time):
|
|
20
|
+
self.cooldown, self.ttl, self.clock, self.wall = cooldown, ttl, clock, wall
|
|
21
|
+
self._calls, self._listings = OrderedDict(), OrderedDict()
|
|
22
|
+
self._probing = set()
|
|
23
|
+
self._lock = threading.RLock()
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def call_key(snapshot, binding):
|
|
27
|
+
return (snapshot.revision, binding.config.base_url, binding.config.model_id, binding.weight_revision)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def listing_key(snapshot, binding):
|
|
31
|
+
credential = hashlib.sha256(binding.config.api_key.encode()).hexdigest()
|
|
32
|
+
return (snapshot.revision, binding.config.base_url, credential)
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _put(cache, key, value):
|
|
36
|
+
cache[key] = value
|
|
37
|
+
cache.move_to_end(key)
|
|
38
|
+
while len(cache) > 256:
|
|
39
|
+
cache.popitem(last=False)
|
|
40
|
+
|
|
41
|
+
def acquire(self, snapshot, binding, request_id):
|
|
42
|
+
"""Healthy calls are unrestricted; one call tests an expired cooldown."""
|
|
43
|
+
key = self.call_key(snapshot, binding)
|
|
44
|
+
with self._lock:
|
|
45
|
+
state = self._calls.get(key)
|
|
46
|
+
if not state or not state.get('failed'):
|
|
47
|
+
return True
|
|
48
|
+
if state['retry_at'] > self.clock() or state.get('recovering'):
|
|
49
|
+
return False
|
|
50
|
+
state['recovering'] = request_id
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
def release(self, snapshot, binding, request_id):
|
|
54
|
+
# Cancellation must not leave the single recovery attempt occupied.
|
|
55
|
+
with self._lock:
|
|
56
|
+
state = self._calls.get(self.call_key(snapshot, binding))
|
|
57
|
+
if state and state.get('recovering') == request_id:
|
|
58
|
+
state['recovering'] = False
|
|
59
|
+
|
|
60
|
+
def failure(self, snapshot, binding, error):
|
|
61
|
+
with self._lock:
|
|
62
|
+
self._put(self._calls, self.call_key(snapshot, binding), {
|
|
63
|
+
'failed': True, 'retry_at': self.clock() + self.cooldown,
|
|
64
|
+
'checked': self.clock(),
|
|
65
|
+
'observed_at': self.wall(), 'error_type': type(error).__name__,
|
|
66
|
+
'status_code': getattr(error, 'status_code', None), 'recovering': False})
|
|
67
|
+
|
|
68
|
+
def success(self, snapshot, binding, response):
|
|
69
|
+
served = getattr(response.raw, 'model', None)
|
|
70
|
+
with self._lock:
|
|
71
|
+
self._put(self._calls, self.call_key(snapshot, binding), {
|
|
72
|
+
'failed': False, 'observed_at': self.wall(),
|
|
73
|
+
'checked': self.clock(),
|
|
74
|
+
'served_model': served[:256] if isinstance(served, str) else None,
|
|
75
|
+
'latency_ms': response.latency_ms})
|
|
76
|
+
|
|
77
|
+
async def refresh(self, snapshot, probe):
|
|
78
|
+
"""At most four concurrent reads, only when /models is requested.
|
|
79
|
+
|
|
80
|
+
Later reads cover remaining endpoints. No queue, scan or background task
|
|
81
|
+
survives the request. Cached entries retain their actual observation age.
|
|
82
|
+
"""
|
|
83
|
+
selected = []
|
|
84
|
+
with self._lock:
|
|
85
|
+
# Unknown and oldest observations go first, so infrequent reads
|
|
86
|
+
# still cover a pool larger than the four concurrent probe slots.
|
|
87
|
+
ordered = sorted(snapshot.bindings.values(), key=lambda binding:
|
|
88
|
+
self._listings.get(self.listing_key(snapshot, binding), {}).get('checked', float('-inf')))
|
|
89
|
+
for binding in ordered:
|
|
90
|
+
key = self.listing_key(snapshot, binding)
|
|
91
|
+
old = self._listings.get(key)
|
|
92
|
+
if key in self._probing or (old and self.clock() - old['checked'] < self.ttl):
|
|
93
|
+
continue
|
|
94
|
+
if len(self._probing) >= 4:
|
|
95
|
+
break
|
|
96
|
+
self._probing.add(key)
|
|
97
|
+
selected.append((key, binding))
|
|
98
|
+
|
|
99
|
+
async def observe(key, binding):
|
|
100
|
+
try:
|
|
101
|
+
models = await asyncio.wait_for(probe(snapshot, binding), timeout=2)
|
|
102
|
+
value = {'available': True, 'models': models, 'observed_at': self.wall()}
|
|
103
|
+
except Exception as error:
|
|
104
|
+
value = {'available': False, 'models': [], 'observed_at': self.wall(),
|
|
105
|
+
'error_type': type(error).__name__}
|
|
106
|
+
else:
|
|
107
|
+
value['error_type'] = None
|
|
108
|
+
finally:
|
|
109
|
+
with self._lock:
|
|
110
|
+
self._probing.discard(key)
|
|
111
|
+
value['checked'] = self.clock()
|
|
112
|
+
with self._lock:
|
|
113
|
+
self._put(self._listings, key, value)
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
await asyncio.gather(*(observe(key, binding) for key, binding in selected))
|
|
117
|
+
finally:
|
|
118
|
+
# Cancellation can precede a child's first instruction and its
|
|
119
|
+
# own finally block. Release every slot reserved by this request.
|
|
120
|
+
with self._lock:
|
|
121
|
+
self._probing.difference_update(key for key, _ in selected)
|
|
122
|
+
|
|
123
|
+
def status(self, snapshot):
|
|
124
|
+
now, wall = self.clock(), self.wall()
|
|
125
|
+
bindings, inventories = {}, {}
|
|
126
|
+
with self._lock:
|
|
127
|
+
for name, binding in snapshot.bindings.items():
|
|
128
|
+
state = self._calls.get(self.call_key(snapshot, binding))
|
|
129
|
+
if state:
|
|
130
|
+
failed = state.get('failed')
|
|
131
|
+
phase = ('recovering' if state.get('recovering') else
|
|
132
|
+
'cooldown' if state.get('retry_at', 0) > now else 'retry_due') if failed else 'available'
|
|
133
|
+
bindings[name] = {k: v for k, v in state.items() if k not in {'failed', 'retry_at', 'recovering', 'checked'}}
|
|
134
|
+
bindings[name].update(state=phase,
|
|
135
|
+
stale=now-state['checked'] >= self.ttl,
|
|
136
|
+
age_seconds=round(max(0, wall-state['observed_at']), 1),
|
|
137
|
+
retry_after_seconds=round(max(0, state.get('retry_at', 0)-now), 2))
|
|
138
|
+
else:
|
|
139
|
+
bindings[name] = {'state': 'unknown', 'stale': True}
|
|
140
|
+
key = self.listing_key(snapshot, binding)
|
|
141
|
+
if key not in inventories:
|
|
142
|
+
listing = self._listings.get(key)
|
|
143
|
+
item = {'bindings': [], 'available': False, 'models': [], 'stale': True}
|
|
144
|
+
if listing:
|
|
145
|
+
item.update({k: v for k, v in listing.items() if k != 'checked'})
|
|
146
|
+
item['age_seconds'] = round(max(0, wall-listing['observed_at']), 1)
|
|
147
|
+
item['stale'] = now-listing['checked'] >= self.ttl
|
|
148
|
+
inventories[key] = item
|
|
149
|
+
inventories[key]['bindings'].append(name)
|
|
150
|
+
return {'completion_observations': bindings, 'model_inventory': list(inventories.values()),
|
|
151
|
+
'inventory_complete': all(not row['stale'] for row in inventories.values()),
|
|
152
|
+
'observation_ttl_seconds': self.ttl, 'retry_cooldown_seconds': self.cooldown,
|
|
153
|
+
'observation_basis': 'endpoint advertisements and actual completion outcomes; declarations remain authoritative'}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""FallbackHandler — escalate to a higher tier when a lower tier fails.
|
|
2
|
+
|
|
3
|
+
When a model call raises a rate-limit, context-length, or capability error,
|
|
4
|
+
the handler promotes the request to the next tier and retries once.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from apsimo.router.tiers import ModelTier
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Errors that warrant a tier upgrade (substrings matched in error messages)
|
|
20
|
+
_UPGRADE_TRIGGERS = (
|
|
21
|
+
"context_length_exceeded",
|
|
22
|
+
"context window",
|
|
23
|
+
"maximum context",
|
|
24
|
+
"rate limit",
|
|
25
|
+
"rate_limit_exceeded",
|
|
26
|
+
"overloaded",
|
|
27
|
+
"too many tokens",
|
|
28
|
+
"insufficient capability",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_TIER_PROMOTION: dict[ModelTier, ModelTier] = {
|
|
32
|
+
ModelTier.SMALL: ModelTier.MEDIUM,
|
|
33
|
+
ModelTier.MEDIUM: ModelTier.LARGE,
|
|
34
|
+
# LARGE has no higher tier — callers must handle this
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FallbackHandler:
|
|
39
|
+
"""Decide whether to escalate a failed request to the next model tier."""
|
|
40
|
+
|
|
41
|
+
def should_escalate(self, error: Exception, current_tier: ModelTier) -> bool:
|
|
42
|
+
"""Return True if the error warrants a tier upgrade."""
|
|
43
|
+
if current_tier not in _TIER_PROMOTION:
|
|
44
|
+
return False # No escalation for the top tier or explicit roles.
|
|
45
|
+
|
|
46
|
+
msg = str(error).lower()
|
|
47
|
+
triggered = any(trigger in msg for trigger in _UPGRADE_TRIGGERS)
|
|
48
|
+
if triggered:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"FallbackHandler: escalating from %s due to: %s",
|
|
51
|
+
current_tier.value,
|
|
52
|
+
type(error).__name__,
|
|
53
|
+
)
|
|
54
|
+
return triggered
|
|
55
|
+
|
|
56
|
+
def next_tier(self, current_tier: ModelTier) -> ModelTier | None:
|
|
57
|
+
"""Return the next tier to try, or None if already at LARGE."""
|
|
58
|
+
return _TIER_PROMOTION.get(current_tier)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Declared function bindings and immutable per-call routing snapshots.
|
|
2
|
+
|
|
3
|
+
No machine discovery or model-name capability inference. Hosts update the same
|
|
4
|
+
configuration when endpoints move; measurements remain labelled declarations.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
import hashlib
|
|
11
|
+
import ipaddress
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from urllib.parse import urlsplit
|
|
15
|
+
|
|
16
|
+
from .tiers import ModelTier, TierConfig, _has_litellm_prefix
|
|
17
|
+
|
|
18
|
+
FUNCTIONS = {'chat', 'reasoning', 'planning', 'extraction', 'judging', 'vision', 'coding'}
|
|
19
|
+
DEFAULT_ROLES = {
|
|
20
|
+
'chat': ['small'], 'reasoning': ['large', 'medium', 'small'],
|
|
21
|
+
'planning': ['large', 'medium', 'small'], 'extraction': ['small', 'medium'],
|
|
22
|
+
'judging': ['large', 'medium'], 'vision': ['vision'], 'coding': ['large', 'medium'],
|
|
23
|
+
}
|
|
24
|
+
DEFAULT_NETWORKS = ('127.0.0.0/8', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '::1/128', 'fc00::/7')
|
|
25
|
+
TASK_ROLES = {
|
|
26
|
+
'source_claim_extraction': 'extraction', 'source_image_description': 'vision',
|
|
27
|
+
'source_appraisal': 'extraction', 'self_judgment': 'reasoning',
|
|
28
|
+
'project_planning': 'planning', 'thought_job': 'reasoning',
|
|
29
|
+
'tom_affect_extraction': 'extraction', 'tom_belief_extraction': 'extraction',
|
|
30
|
+
'tom_intention_extraction': 'extraction',
|
|
31
|
+
'tom_fact_extraction': 'extraction', 'tom_engagement_extraction': 'extraction',
|
|
32
|
+
'context_compression': 'extraction',
|
|
33
|
+
'workspace_thinking': 'reasoning', 'internal_thinking': 'reasoning',
|
|
34
|
+
'toolsmith_draft': 'coding', 'skill_distillation': 'judging',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class Binding:
|
|
40
|
+
name: str
|
|
41
|
+
config: TierConfig
|
|
42
|
+
context_tokens: int = 0
|
|
43
|
+
supports_tools: bool | None = None
|
|
44
|
+
latency_ms: int = 0
|
|
45
|
+
tokens_per_second: float = 0
|
|
46
|
+
concurrency: int = 0
|
|
47
|
+
weight_revision: str = 'unknown'
|
|
48
|
+
legacy: bool = False
|
|
49
|
+
supports_json_schema: bool = False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(frozen=True)
|
|
53
|
+
class FunctionRole:
|
|
54
|
+
candidates: tuple[str, ...]
|
|
55
|
+
timeout_seconds: float = 20
|
|
56
|
+
deadline_seconds: float = 40
|
|
57
|
+
min_context_tokens: int = 0
|
|
58
|
+
max_latency_ms: int = 0
|
|
59
|
+
min_tokens_per_second: float = 0
|
|
60
|
+
min_concurrency: int = 0
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class RoutingSnapshot:
|
|
65
|
+
revision: str
|
|
66
|
+
tiers: dict
|
|
67
|
+
bindings: dict[str, Binding]
|
|
68
|
+
roles: dict[str, FunctionRole]
|
|
69
|
+
networks: tuple
|
|
70
|
+
declared_hosts: frozenset[str]
|
|
71
|
+
task_roles: dict[str, str]
|
|
72
|
+
provider: str = ''
|
|
73
|
+
base_url: str = ''
|
|
74
|
+
|
|
75
|
+
def status(self):
|
|
76
|
+
return {'config_revision': self.revision, 'capability_basis': 'deployment declarations, not measured by this router',
|
|
77
|
+
'roles': {key: list(value.candidates) for key, value in self.roles.items()},
|
|
78
|
+
'task_roles': dict(self.task_roles),
|
|
79
|
+
'models': {name: {'model_id': b.config.model_id, 'weight_revision': b.weight_revision,
|
|
80
|
+
'context_tokens': b.context_tokens, 'supports_vision': b.config.supports_vision,
|
|
81
|
+
'supports_tools': b.supports_tools, 'latency_ms': b.latency_ms,
|
|
82
|
+
'supports_json_schema': b.supports_json_schema,
|
|
83
|
+
'tokens_per_second': b.tokens_per_second, 'concurrency': b.concurrency,
|
|
84
|
+
'legacy_unknown_tools_allowed': b.legacy and b.supports_tools is None}
|
|
85
|
+
for name, b in self.bindings.items()}}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def number(value, *, minimum=0, maximum=10**9):
|
|
89
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or not minimum <= value <= maximum:
|
|
90
|
+
raise ValueError('Invalid routing capability or limit')
|
|
91
|
+
return value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _binding(name, config, spec, *, legacy=False):
|
|
95
|
+
tools = spec.get('supportsTools')
|
|
96
|
+
if tools is not None and type(tools) is not bool:
|
|
97
|
+
raise ValueError('supportsTools must be an explicit boolean')
|
|
98
|
+
structured = spec.get('supportsJsonSchema', False)
|
|
99
|
+
if type(structured) is not bool:
|
|
100
|
+
raise ValueError('supportsJsonSchema must be an explicit boolean')
|
|
101
|
+
return Binding(name, config,
|
|
102
|
+
int(number(spec.get('contextTokens', config.useful_context_tokens))), tools,
|
|
103
|
+
int(number(spec.get('latencyMs', 0))), float(number(spec.get('tokensPerSecond', 0))),
|
|
104
|
+
int(number(spec.get('concurrency', 0))), str(spec.get('weightRevision') or 'unknown')[:160], legacy, structured)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _transport(config, host, spec):
|
|
108
|
+
protocol = spec.get('protocol', host.get('protocol'))
|
|
109
|
+
if protocol not in {None, 'openai-chat'}:
|
|
110
|
+
raise ValueError('Function routing requires the openai-chat protocol')
|
|
111
|
+
if config.model_id.startswith('ollama/') and protocol == 'openai-chat':
|
|
112
|
+
if not config.base_url.rstrip('/').endswith('/v1'):
|
|
113
|
+
raise ValueError('Declared Ollama OpenAI compatibility requires an explicit /v1 base URL')
|
|
114
|
+
config = replace(config, model_id='openai/' + config.model_id.removeprefix('ollama/'))
|
|
115
|
+
if not config.model_id.startswith('openai/'):
|
|
116
|
+
raise ValueError('Function routing requires an explicit OpenAI-compatible endpoint; declare protocol openai-chat for compatible Ollama /v1 endpoints')
|
|
117
|
+
return config
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def build_snapshot(host: dict, tiers: dict) -> RoutingSnapshot:
|
|
121
|
+
"""Build privately, then replace one router reference after validation."""
|
|
122
|
+
bindings, materialized = {}, {}
|
|
123
|
+
for tier, original in tiers.items():
|
|
124
|
+
# Pin inherited endpoint/key into this snapshot. Later provider env
|
|
125
|
+
# changes cannot redirect a request that already selected this config.
|
|
126
|
+
spec = host.get('models', {}).get(tier.value, {})
|
|
127
|
+
spec = spec if isinstance(spec, dict) else {}
|
|
128
|
+
cfg = replace(deepcopy(original),
|
|
129
|
+
base_url=original.base_url or host.get('baseUrl', ''),
|
|
130
|
+
api_key=original.api_key or host.get('apiKey', ''))
|
|
131
|
+
cfg = _transport(cfg, host, spec)
|
|
132
|
+
materialized[tier] = cfg
|
|
133
|
+
bindings[tier.value] = _binding(tier.value, cfg, spec, legacy=True)
|
|
134
|
+
pool = host.get('modelPool', {})
|
|
135
|
+
if not isinstance(pool, dict) or len(pool) > 64:
|
|
136
|
+
raise ValueError('modelPool must contain at most 64 explicit bindings')
|
|
137
|
+
for name, spec in pool.items():
|
|
138
|
+
if not isinstance(name, str) or not re.fullmatch(r'[a-zA-Z0-9_.-]{1,80}', name) or name in bindings:
|
|
139
|
+
raise ValueError('Invalid or duplicate modelPool binding')
|
|
140
|
+
if not isinstance(spec, dict) or not isinstance(spec.get('model'), str) or not spec['model']:
|
|
141
|
+
raise ValueError('Every modelPool binding requires a model')
|
|
142
|
+
model = spec['model']
|
|
143
|
+
provider = str(spec.get('provider', host.get('provider', 'vllm')))
|
|
144
|
+
if not _has_litellm_prefix(model):
|
|
145
|
+
if provider == 'ollama': model = 'ollama/' + model
|
|
146
|
+
elif provider in {'local', 'custom', 'lmstudio', 'vllm', 'openai'}: model = 'openai/' + model
|
|
147
|
+
cfg = TierConfig(tier=ModelTier(spec.get('tier', 'medium')), model_id=model,
|
|
148
|
+
max_tokens=int(number(spec.get('maxTokens', 8192), minimum=1)),
|
|
149
|
+
cost_per_1k_input=0, cost_per_1k_output=0, latency_p50_ms=0,
|
|
150
|
+
base_url=spec.get('baseUrl', host.get('baseUrl', '')),
|
|
151
|
+
api_key=spec.get('apiKey', host.get('apiKey', '')),
|
|
152
|
+
extra_body=deepcopy(spec.get('extraBody')),
|
|
153
|
+
useful_context_tokens=int(number(spec.get('contextTokens', 0))),
|
|
154
|
+
supports_vision=spec.get('supportsVision') is True)
|
|
155
|
+
if not isinstance(cfg.base_url, str) or not isinstance(cfg.api_key, str):
|
|
156
|
+
raise ValueError('Invalid model endpoint configuration')
|
|
157
|
+
cfg = _transport(cfg, host, spec)
|
|
158
|
+
bindings[name] = _binding(name, cfg, spec)
|
|
159
|
+
if not bindings:
|
|
160
|
+
raise ValueError('At least one deployed model must be declared explicitly')
|
|
161
|
+
role_config = host.get('functionRoles', {})
|
|
162
|
+
if not isinstance(role_config, dict) or set(role_config) - FUNCTIONS:
|
|
163
|
+
raise ValueError('Unknown function role; speech/embedding/rerank use their own transports')
|
|
164
|
+
task_roles = host.get('taskRoles', {})
|
|
165
|
+
if (not isinstance(task_roles, dict) or set(task_roles) - TASK_ROLES.keys()
|
|
166
|
+
or any(not isinstance(role, str) or role not in FUNCTIONS for role in task_roles.values())):
|
|
167
|
+
raise ValueError('taskRoles must map supported task names to existing function roles')
|
|
168
|
+
roles = {}
|
|
169
|
+
for name, defaults in DEFAULT_ROLES.items():
|
|
170
|
+
raw = role_config.get(name, [key for key in defaults if key in bindings])
|
|
171
|
+
raw = {'candidates': raw} if isinstance(raw, list) else raw
|
|
172
|
+
if not isinstance(raw, dict): raise ValueError('Invalid function role')
|
|
173
|
+
if set(raw) - {'candidates', 'timeoutSeconds', 'deadlineSeconds', 'minContextTokens', 'maxLatencyMs', 'minTokensPerSecond', 'minConcurrency'}:
|
|
174
|
+
raise ValueError('Unknown function role constraint')
|
|
175
|
+
candidates = raw.get('candidates', [])
|
|
176
|
+
if not isinstance(candidates, list) or len(candidates) > 8 or any(not isinstance(key, str) or key not in bindings for key in candidates):
|
|
177
|
+
raise ValueError('Role candidates must name at most eight configured bindings')
|
|
178
|
+
slow = name in {'reasoning', 'planning', 'judging', 'coding'}
|
|
179
|
+
roles[name] = FunctionRole(tuple(dict.fromkeys(candidates)),
|
|
180
|
+
float(number(raw.get('timeoutSeconds', 120 if slow else 20), minimum=.05, maximum=300)),
|
|
181
|
+
float(number(raw.get('deadlineSeconds', 180 if slow else 40), minimum=.05, maximum=600)),
|
|
182
|
+
int(number(raw.get('minContextTokens', 0))), int(number(raw.get('maxLatencyMs', 0))),
|
|
183
|
+
float(number(raw.get('minTokensPerSecond', 0))), int(number(raw.get('minConcurrency', 0))))
|
|
184
|
+
raw_networks = host.get('localNetworks', DEFAULT_NETWORKS)
|
|
185
|
+
if not isinstance(raw_networks, (list, tuple)) or len(raw_networks) > 64:
|
|
186
|
+
raise ValueError('localNetworks must be a bounded list of CIDRs')
|
|
187
|
+
networks = tuple(ipaddress.ip_network(value, strict=False) for value in raw_networks)
|
|
188
|
+
if any(n.prefixlen == 0 for n in networks): raise ValueError('A default route cannot declare the whole internet local')
|
|
189
|
+
hosts = host.get('localHosts', ['localhost'])
|
|
190
|
+
if not isinstance(hosts, list) or any(not isinstance(h, str) or len(h) > 253 for h in hosts):
|
|
191
|
+
raise ValueError('localHosts must list deployment hostnames')
|
|
192
|
+
revision = hashlib.sha256(json.dumps(host, sort_keys=True, separators=(',', ':')).encode()).hexdigest()[:20]
|
|
193
|
+
return RoutingSnapshot(revision, materialized, bindings, roles, networks, frozenset(h.casefold() for h in hosts),
|
|
194
|
+
task_roles=dict(task_roles),
|
|
195
|
+
provider=str(host.get('provider') or ''), base_url=str(host.get('baseUrl') or ''))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def select_role(snapshot, context):
|
|
199
|
+
"""The same selection governs capability hints, deadlines and dispatch."""
|
|
200
|
+
task = context.get('task')
|
|
201
|
+
return (context.get('function_role')
|
|
202
|
+
or (snapshot.task_roles.get(task) if snapshot else None)
|
|
203
|
+
or TASK_ROLES.get(task, 'reasoning'))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def endpoint_host(binding, snapshot):
|
|
207
|
+
"""Return literal eligible IP or declared hostname needing DNS verification.
|
|
208
|
+
|
|
209
|
+
No suffix such as .local is trusted. Every resolved address must be inside
|
|
210
|
+
deployment networks; endpoints with URL credentials or query strings fail.
|
|
211
|
+
"""
|
|
212
|
+
parsed = urlsplit(binding.config.base_url)
|
|
213
|
+
if parsed.scheme not in {'http', 'https'} or parsed.username or parsed.password or parsed.query or parsed.fragment:
|
|
214
|
+
return None
|
|
215
|
+
host = (parsed.hostname or '').casefold()
|
|
216
|
+
try:
|
|
217
|
+
address = ipaddress.ip_address(host)
|
|
218
|
+
return host if any(address in net for net in snapshot.networks) else None
|
|
219
|
+
except ValueError:
|
|
220
|
+
return host if host in snapshot.declared_hosts else None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def candidates(snapshot, role_name, context, *, has_images, has_tools):
|
|
224
|
+
role = snapshot.roles[role_name]
|
|
225
|
+
minimum = max(role.min_context_tokens, int(context.get('required_context_tokens', 0)))
|
|
226
|
+
output = []
|
|
227
|
+
for name in role.candidates:
|
|
228
|
+
b = snapshot.bindings[name]
|
|
229
|
+
if (has_images or role_name == 'vision') and not b.config.supports_vision: continue
|
|
230
|
+
if has_tools and b.supports_tools is not True and not (b.legacy and b.supports_tools is None): continue
|
|
231
|
+
if minimum and b.context_tokens < minimum: continue
|
|
232
|
+
# Reuse the existing text estimator to avoid an obviously undersized
|
|
233
|
+
# fallback. Image token accounting is model-specific and remains unknown.
|
|
234
|
+
requested_output = context.get('max_output_tokens', context.get('max_tokens', b.config.max_tokens))
|
|
235
|
+
input_tokens = context.get('estimated_input_tokens', 0)
|
|
236
|
+
if b.supports_json_schema:
|
|
237
|
+
input_tokens += context.get('response_schema_tokens', 0)
|
|
238
|
+
if b.context_tokens and input_tokens + min(b.config.max_tokens, int(requested_output)) > b.context_tokens: continue
|
|
239
|
+
if role.max_latency_ms and (not b.latency_ms or b.latency_ms > role.max_latency_ms): continue
|
|
240
|
+
if b.tokens_per_second < role.min_tokens_per_second or b.concurrency < role.min_concurrency: continue
|
|
241
|
+
if endpoint_host(b, snapshot) is None: continue
|
|
242
|
+
output.append(b)
|
|
243
|
+
return output
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Resolve a local function binding for a native Hermes caller.
|
|
2
|
+
|
|
3
|
+
This runs in the sidecar interpreter. The caller captures the private JSON pipe;
|
|
4
|
+
Hermes retains its own dependencies, agent loop, fallback and execution identity.
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .functions import candidates
|
|
12
|
+
from .router import LLMRouter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def planning(configuration):
|
|
16
|
+
if 'planning' not in configuration.get('functionRoles', {}):
|
|
17
|
+
raise ValueError('An explicit planning role is required')
|
|
18
|
+
router = LLMRouter(tiers={}, self_learner=object())
|
|
19
|
+
router.configure(configuration)
|
|
20
|
+
snapshot = router._snapshot
|
|
21
|
+
selected = candidates(snapshot, 'planning', {}, has_images=False, has_tools=True)
|
|
22
|
+
if not selected:
|
|
23
|
+
raise ValueError('No tool-capable local planning binding is available')
|
|
24
|
+
for binding in selected:
|
|
25
|
+
if not await router._local_addresses(snapshot, binding):
|
|
26
|
+
raise ValueError('Planning endpoint has no eligible local address')
|
|
27
|
+
if any(binding.config.extra_body != selected[0].config.extra_body for binding in selected):
|
|
28
|
+
raise ValueError('Planning candidates require different native request overrides')
|
|
29
|
+
role = snapshot.roles['planning']
|
|
30
|
+
entries = [dict(provider='openai', model=b.config.model_id.removeprefix('openai/'),
|
|
31
|
+
base_url=b.config.base_url, api_key=b.config.api_key or 'local-no-key',
|
|
32
|
+
api_mode='chat_completions') for b in selected]
|
|
33
|
+
maximum = min(binding.config.max_tokens for binding in selected)
|
|
34
|
+
options = {**entries[0], 'requested_provider':'openai', 'fallback_model':entries[1:] or None,
|
|
35
|
+
'max_tokens':maximum, 'request_overrides':{'extra_body':selected[0].config.extra_body or {}}}
|
|
36
|
+
policy = {'role':'planning', 'configuration_revision':snapshot.revision,
|
|
37
|
+
'candidates':[{'binding':b.name, 'model':b.config.model_id, 'weight_revision':b.weight_revision} for b in selected],
|
|
38
|
+
'max_output_tokens':maximum, 'request_timeout_seconds':role.timeout_seconds,
|
|
39
|
+
'run_deadline_seconds':role.deadline_seconds,
|
|
40
|
+
'fallback_owner':'Hermes native runtime'}
|
|
41
|
+
return options, policy
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
46
|
+
parser.add_argument('--config', type=Path, required=True)
|
|
47
|
+
args = parser.parse_args()
|
|
48
|
+
print(json.dumps(asyncio.run(planning(json.loads(args.config.read_text())))))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == '__main__':
|
|
52
|
+
main()
|