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
apsimo/reasoning/loop.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""ReasoningLoop — orchestrates model calls and tool execution.
|
|
2
|
+
|
|
3
|
+
This is the core of Colony's reasoning capability. It replaces the
|
|
4
|
+
tightly-coupled reasoning loop in ``run_agent.py`` with a clean,
|
|
5
|
+
host-agnostic interface that can be driven via the ``/v1/host/reasoning/turn``
|
|
6
|
+
HTTP endpoint.
|
|
7
|
+
|
|
8
|
+
The loop works like this:
|
|
9
|
+
|
|
10
|
+
1. Receive messages + optional system prompt from the host
|
|
11
|
+
2. Call the LLM via LLMRouter
|
|
12
|
+
3. If the LLM returns tool calls, execute them and append results
|
|
13
|
+
4. Repeat until the LLM responds without tool calls (or budget exhausted)
|
|
14
|
+
5. Return the final response + any pending tool calls
|
|
15
|
+
|
|
16
|
+
Key differences from run_agent.py:
|
|
17
|
+
|
|
18
|
+
- No session persistence (hosts manage their own sessions)
|
|
19
|
+
- No streaming callbacks (hosts handle streaming themselves)
|
|
20
|
+
- No display/print logic (hosts handle display)
|
|
21
|
+
- No memory auto-recall (that's the context engine's job)
|
|
22
|
+
- Tool execution is delegated to ToolExecutor (pluggable)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
import json
|
|
29
|
+
import logging
|
|
30
|
+
import uuid
|
|
31
|
+
from dataclasses import dataclass, field
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from apsimo.reasoning.executor import ToolExecutor, ToolRegistryError
|
|
35
|
+
from apsimo.reasoning.tool_policy import ToolActorPolicy
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ReasoningConfig:
|
|
42
|
+
"""Tunable parameters for the reasoning loop."""
|
|
43
|
+
|
|
44
|
+
max_iterations: int = 10
|
|
45
|
+
tool_delay_seconds: float = 0.0 # Delay between tool call batches
|
|
46
|
+
force_tier: str | None = None # Force a specific LLM tier
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ReasoningResult:
|
|
51
|
+
"""Result of a single reasoning turn."""
|
|
52
|
+
|
|
53
|
+
status: str # "completed" | "needs_tool" | "error"
|
|
54
|
+
message: dict[str, Any] | None = None # HostMessage-shaped
|
|
55
|
+
tool_calls: list[dict[str, Any]] = field(default_factory=list)
|
|
56
|
+
usage: dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
error: str | None = None
|
|
58
|
+
model_provenance: dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _messages_to_dicts(messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
62
|
+
"""Normalise messages for LiteLLM consumption.
|
|
63
|
+
|
|
64
|
+
Ensures every message has at least ``role`` and ``content`` keys.
|
|
65
|
+
Strips None content, replacing with empty string.
|
|
66
|
+
"""
|
|
67
|
+
result = []
|
|
68
|
+
for m in messages:
|
|
69
|
+
d = dict(m)
|
|
70
|
+
if d.get("content") is None:
|
|
71
|
+
d["content"] = ""
|
|
72
|
+
result.append(d)
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ReasoningLoop:
|
|
77
|
+
"""Orchestrate model calls and tool execution for a reasoning turn.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
model :
|
|
82
|
+
An object with an ``async complete(messages, *, tools, force_tier, context)``
|
|
83
|
+
method — typically an :class:`~apsimo.router.router.LLMRouter`.
|
|
84
|
+
tools :
|
|
85
|
+
A :class:`ToolExecutor` that can dispatch tool calls.
|
|
86
|
+
config :
|
|
87
|
+
Optional tuning parameters.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(
|
|
91
|
+
self,
|
|
92
|
+
model: Any,
|
|
93
|
+
tools: ToolExecutor | None = None,
|
|
94
|
+
config: ReasoningConfig | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
self._model = model
|
|
97
|
+
self._tools = tools or ToolExecutor()
|
|
98
|
+
self._config = config or ReasoningConfig()
|
|
99
|
+
|
|
100
|
+
async def run_turn(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
session_id: str,
|
|
104
|
+
messages: list[dict[str, Any]],
|
|
105
|
+
available_tools: list[str] | None = None,
|
|
106
|
+
model_override: str | None = None,
|
|
107
|
+
system_prompt: str | None = None,
|
|
108
|
+
context: dict[str, Any] | None = None,
|
|
109
|
+
actor_policy: ToolActorPolicy | None = None,
|
|
110
|
+
) -> ReasoningResult:
|
|
111
|
+
"""Run a single reasoning turn with tool iteration.
|
|
112
|
+
|
|
113
|
+
This is the main entry point called by the host router.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
session_id :
|
|
118
|
+
Identifier for the reasoning session (for logging / tracing).
|
|
119
|
+
messages :
|
|
120
|
+
OpenAI-format message list (role + content).
|
|
121
|
+
available_tools :
|
|
122
|
+
Tool names the host has made available.
|
|
123
|
+
model_override :
|
|
124
|
+
If set, passed as ``force_tier`` hint to the LLMRouter.
|
|
125
|
+
system_prompt :
|
|
126
|
+
Optional system prompt prepended to messages.
|
|
127
|
+
context :
|
|
128
|
+
Optional routing hints for the LLMRouter.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
ReasoningResult
|
|
133
|
+
The final response, or a needs_tool signal if the budget
|
|
134
|
+
ran out mid-iteration.
|
|
135
|
+
"""
|
|
136
|
+
turn_id = str(uuid.uuid4())[:8]
|
|
137
|
+
log_prefix = f"[reasoning:{turn_id}]"
|
|
138
|
+
|
|
139
|
+
# Build the working message list
|
|
140
|
+
working = []
|
|
141
|
+
if system_prompt:
|
|
142
|
+
working.append({"role": "system", "content": system_prompt})
|
|
143
|
+
working.extend(_messages_to_dicts(messages))
|
|
144
|
+
|
|
145
|
+
# Build tool definitions for the LLM call
|
|
146
|
+
try:
|
|
147
|
+
tool_defs = self._tools.get_definitions(available_tools)
|
|
148
|
+
except ToolRegistryError as exc:
|
|
149
|
+
logger.error(
|
|
150
|
+
"%s tool registry failed closed: %s: %s",
|
|
151
|
+
log_prefix,
|
|
152
|
+
exc.code,
|
|
153
|
+
exc,
|
|
154
|
+
)
|
|
155
|
+
return ReasoningResult(
|
|
156
|
+
status="error",
|
|
157
|
+
error=f"{exc.code}: {exc}",
|
|
158
|
+
)
|
|
159
|
+
allowed_tool_names = (
|
|
160
|
+
None if available_tools is None else frozenset(available_tools)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
iterations = 0
|
|
164
|
+
last_usage: dict[str, Any] = {}
|
|
165
|
+
cumulative_usage: dict[str, int] = {
|
|
166
|
+
"prompt_tokens": 0,
|
|
167
|
+
"completion_tokens": 0,
|
|
168
|
+
"total_tokens": 0,
|
|
169
|
+
}
|
|
170
|
+
model_provenance: dict[str, Any] = {}
|
|
171
|
+
|
|
172
|
+
while iterations < self._config.max_iterations:
|
|
173
|
+
iterations += 1
|
|
174
|
+
logger.debug(
|
|
175
|
+
"%s iteration %d/%d — %d messages, %d tools",
|
|
176
|
+
log_prefix,
|
|
177
|
+
iterations,
|
|
178
|
+
self._config.max_iterations,
|
|
179
|
+
len(working),
|
|
180
|
+
len(tool_defs),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Call the LLM
|
|
184
|
+
try:
|
|
185
|
+
tier_hint = model_override or self._config.force_tier
|
|
186
|
+
if isinstance(tier_hint, str):
|
|
187
|
+
from apsimo.router.tiers import ModelTier
|
|
188
|
+
try:
|
|
189
|
+
tier_hint = ModelTier(tier_hint)
|
|
190
|
+
except ValueError:
|
|
191
|
+
tier_hint = None
|
|
192
|
+
response = await self._model.complete(
|
|
193
|
+
working,
|
|
194
|
+
tools=tool_defs if tool_defs else None,
|
|
195
|
+
force_tier=tier_hint,
|
|
196
|
+
context=context or {},
|
|
197
|
+
)
|
|
198
|
+
except Exception as exc:
|
|
199
|
+
logger.error("%s LLM call failed: %s", log_prefix, exc)
|
|
200
|
+
return ReasoningResult(
|
|
201
|
+
status="error",
|
|
202
|
+
error=f"LLM call failed: {exc}",
|
|
203
|
+
model_provenance=model_provenance,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Accumulate usage
|
|
207
|
+
used = getattr(response, 'tier_used', None)
|
|
208
|
+
model_provenance = {'model_role': getattr(response, 'function_role', '') or getattr(used, 'value', str(used or 'unknown')),
|
|
209
|
+
'model_id': getattr(response, 'model_id', 'unknown'),
|
|
210
|
+
'model_revision': getattr(response, 'model_revision', 'unknown'),
|
|
211
|
+
'config_revision': getattr(response, 'config_revision', '') or 'unknown',
|
|
212
|
+
'basis': 'last_successful_model_response'}
|
|
213
|
+
last_usage = dict(response.usage)
|
|
214
|
+
for k in cumulative_usage:
|
|
215
|
+
cumulative_usage[k] += last_usage.get(k, 0)
|
|
216
|
+
|
|
217
|
+
# Extract the assistant's response
|
|
218
|
+
raw = response.raw
|
|
219
|
+
assistant_content = response.content or ""
|
|
220
|
+
tool_calls_raw = self._extract_tool_calls(raw)
|
|
221
|
+
|
|
222
|
+
if not tool_calls_raw:
|
|
223
|
+
# No tool calls — the LLM is done
|
|
224
|
+
return ReasoningResult(
|
|
225
|
+
status="completed",
|
|
226
|
+
message={
|
|
227
|
+
"role": "assistant",
|
|
228
|
+
"content": assistant_content,
|
|
229
|
+
},
|
|
230
|
+
usage=cumulative_usage,
|
|
231
|
+
model_provenance=model_provenance,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Tool calls — execute them
|
|
235
|
+
logger.debug(
|
|
236
|
+
"%s LLM requested %d tool calls",
|
|
237
|
+
log_prefix,
|
|
238
|
+
len(tool_calls_raw),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Add assistant message with tool calls to working history
|
|
242
|
+
working.append(self._build_assistant_message(raw, assistant_content, tool_calls_raw))
|
|
243
|
+
|
|
244
|
+
# Execute tool calls
|
|
245
|
+
tool_delay = self._config.tool_delay_seconds
|
|
246
|
+
if tool_delay > 0:
|
|
247
|
+
await asyncio.sleep(tool_delay)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
tool_results = await self._tools.execute_batch(
|
|
251
|
+
tool_calls_raw,
|
|
252
|
+
session_id=session_id,
|
|
253
|
+
allowed_tools=allowed_tool_names,
|
|
254
|
+
actor_policy=actor_policy,
|
|
255
|
+
)
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
logger.error("%s tool execution failed: %s", log_prefix, exc)
|
|
258
|
+
# Return what we have — the host can decide what to do
|
|
259
|
+
return ReasoningResult(
|
|
260
|
+
status="error",
|
|
261
|
+
message={
|
|
262
|
+
"role": "assistant",
|
|
263
|
+
"content": assistant_content or f"Tool execution error: {exc}",
|
|
264
|
+
},
|
|
265
|
+
tool_calls=tool_calls_raw,
|
|
266
|
+
usage=cumulative_usage,
|
|
267
|
+
error=f"Tool execution failed: {exc}",
|
|
268
|
+
model_provenance=model_provenance,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Append tool results to working history
|
|
272
|
+
for result in tool_results:
|
|
273
|
+
working.append({
|
|
274
|
+
"role": "tool",
|
|
275
|
+
"tool_call_id": result["tool_call_id"],
|
|
276
|
+
"content": result["content"],
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
# Budget exhausted — return needs_tool with pending calls
|
|
280
|
+
logger.warning(
|
|
281
|
+
"%s budget exhausted after %d iterations",
|
|
282
|
+
log_prefix,
|
|
283
|
+
iterations,
|
|
284
|
+
)
|
|
285
|
+
return ReasoningResult(
|
|
286
|
+
status="needs_tool",
|
|
287
|
+
message={
|
|
288
|
+
"role": "assistant",
|
|
289
|
+
"content": assistant_content,
|
|
290
|
+
},
|
|
291
|
+
tool_calls=tool_calls_raw,
|
|
292
|
+
usage=cumulative_usage,
|
|
293
|
+
model_provenance=model_provenance,
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _extract_tool_calls(raw_response: Any) -> list[dict[str, Any]]:
|
|
298
|
+
"""Extract tool calls from a LiteLLM response object.
|
|
299
|
+
|
|
300
|
+
Returns a list of dicts with keys: id, name, arguments.
|
|
301
|
+
"""
|
|
302
|
+
if raw_response is None:
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
# LiteLLM / OpenAI response format
|
|
306
|
+
choices = getattr(raw_response, "choices", None)
|
|
307
|
+
if not choices:
|
|
308
|
+
return []
|
|
309
|
+
|
|
310
|
+
message = getattr(choices[0], "message", None)
|
|
311
|
+
if message is None:
|
|
312
|
+
return []
|
|
313
|
+
|
|
314
|
+
raw_calls = getattr(message, "tool_calls", None)
|
|
315
|
+
if not raw_calls:
|
|
316
|
+
return []
|
|
317
|
+
|
|
318
|
+
result = []
|
|
319
|
+
for tc in raw_calls:
|
|
320
|
+
tc_id = getattr(tc, "id", None) or str(uuid.uuid4())
|
|
321
|
+
func = getattr(tc, "function", None)
|
|
322
|
+
if func is None:
|
|
323
|
+
continue
|
|
324
|
+
name = getattr(func, "name", "unknown")
|
|
325
|
+
try:
|
|
326
|
+
arguments = json.loads(getattr(func, "arguments", "{}"))
|
|
327
|
+
except (json.JSONDecodeError, TypeError):
|
|
328
|
+
arguments = {}
|
|
329
|
+
|
|
330
|
+
result.append({
|
|
331
|
+
"id": tc_id,
|
|
332
|
+
"name": name,
|
|
333
|
+
"arguments": arguments,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _build_assistant_message(
|
|
340
|
+
raw_response: Any,
|
|
341
|
+
content: str,
|
|
342
|
+
tool_calls: list[dict[str, Any]],
|
|
343
|
+
) -> dict[str, Any]:
|
|
344
|
+
"""Build an assistant message dict that includes tool calls.
|
|
345
|
+
|
|
346
|
+
This preserves the tool call structure in the conversation
|
|
347
|
+
history so the LLM can see its own prior tool calls.
|
|
348
|
+
"""
|
|
349
|
+
msg: dict[str, Any] = {"role": "assistant"}
|
|
350
|
+
|
|
351
|
+
if content:
|
|
352
|
+
msg["content"] = content
|
|
353
|
+
else:
|
|
354
|
+
msg["content"] = None
|
|
355
|
+
|
|
356
|
+
if tool_calls:
|
|
357
|
+
msg["tool_calls"] = [
|
|
358
|
+
{
|
|
359
|
+
"id": tc["id"],
|
|
360
|
+
"type": "function",
|
|
361
|
+
"function": {
|
|
362
|
+
"name": tc["name"],
|
|
363
|
+
"arguments": (
|
|
364
|
+
tc["arguments"]
|
|
365
|
+
if isinstance(tc["arguments"], str)
|
|
366
|
+
else json.dumps(tc["arguments"])
|
|
367
|
+
),
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
for tc in tool_calls
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
return msg
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Native tool handlers for Colony's reasoning loop.
|
|
2
|
+
|
|
3
|
+
These tools run inside Colony without requiring a host harness roundtrip.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from apsimo.reasoning.native_tools.calculate import CalculateTool
|
|
7
|
+
from apsimo.reasoning.native_tools.web_search import WebSearchTool
|
|
8
|
+
from apsimo.reasoning.native_tools.file_ops import ReadFileTool, WriteFileTool, ListDirectoryTool
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CalculateTool",
|
|
12
|
+
"WebSearchTool",
|
|
13
|
+
"ReadFileTool",
|
|
14
|
+
"WriteFileTool",
|
|
15
|
+
"ListDirectoryTool",
|
|
16
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Safe math evaluation tool — no eval(), no exec()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import logging
|
|
7
|
+
import operator
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
ALLOWED_NAMES = {
|
|
13
|
+
"abs": abs, "round": round, "min": min, "max": max,
|
|
14
|
+
"sum": sum, "len": len, "int": int, "float": float,
|
|
15
|
+
"pow": pow, "divmod": divmod, "sorted": sorted,
|
|
16
|
+
"True": True, "False": False, "None": None,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
BIN_OPS = {
|
|
20
|
+
ast.Add: operator.add, ast.Sub: operator.sub,
|
|
21
|
+
ast.Mult: operator.mul, ast.Div: operator.truediv,
|
|
22
|
+
ast.Mod: operator.mod, ast.Pow: operator.pow,
|
|
23
|
+
ast.FloorDiv: operator.floordiv,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
COMPARE_OPS = {
|
|
27
|
+
ast.Eq: operator.eq, ast.NotEq: operator.ne,
|
|
28
|
+
ast.Lt: operator.lt, ast.LtE: operator.le,
|
|
29
|
+
ast.Gt: operator.gt, ast.GtE: operator.ge,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
UNARY_OPS = {
|
|
33
|
+
ast.USub: operator.neg,
|
|
34
|
+
ast.UAdd: operator.pos,
|
|
35
|
+
ast.Not: operator.not_,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CalculateTool:
|
|
40
|
+
"""Safe math expression evaluator using AST parsing.
|
|
41
|
+
|
|
42
|
+
No eval(), no exec(). Only numeric operations allowed.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def name(self) -> str:
|
|
47
|
+
return "calculate"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def schema(self) -> dict:
|
|
51
|
+
return {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"properties": {
|
|
54
|
+
"expression": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"description": "Math expression to evaluate (e.g., '2 + 2', 'abs(-5) * 3')",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
"required": ["expression"],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
63
|
+
expression = args.get("expression", "")
|
|
64
|
+
if not expression:
|
|
65
|
+
return {"error": True, "message": "No expression provided"}
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
tree = ast.parse(expression, mode="eval")
|
|
69
|
+
result = self._eval_node(tree.body)
|
|
70
|
+
return {"result": result, "expression": expression}
|
|
71
|
+
except (ValueError, TypeError, SyntaxError, ZeroDivisionError) as e:
|
|
72
|
+
return {"error": True, "message": str(e), "expression": expression}
|
|
73
|
+
except Exception as e:
|
|
74
|
+
return {"error": True, "message": f"Cannot evaluate: {e}", "expression": expression}
|
|
75
|
+
|
|
76
|
+
def _eval_node(self, node: ast.AST) -> Any:
|
|
77
|
+
# Constants (numbers, strings)
|
|
78
|
+
if isinstance(node, ast.Constant):
|
|
79
|
+
return node.value
|
|
80
|
+
|
|
81
|
+
# Names (functions, constants)
|
|
82
|
+
if isinstance(node, ast.Name):
|
|
83
|
+
if node.id in ALLOWED_NAMES:
|
|
84
|
+
return ALLOWED_NAMES[node.id]
|
|
85
|
+
raise ValueError(f"Name not allowed: {node.id}")
|
|
86
|
+
|
|
87
|
+
# Binary operations
|
|
88
|
+
if isinstance(node, ast.BinOp):
|
|
89
|
+
left = self._eval_node(node.left)
|
|
90
|
+
right = self._eval_node(node.right)
|
|
91
|
+
op_type = type(node.op)
|
|
92
|
+
if op_type in BIN_OPS:
|
|
93
|
+
return BIN_OPS[op_type](left, right)
|
|
94
|
+
raise ValueError(f"Binary operation not allowed: {op_type.__name__}")
|
|
95
|
+
|
|
96
|
+
# Unary operations
|
|
97
|
+
if isinstance(node, ast.UnaryOp):
|
|
98
|
+
operand = self._eval_node(node.operand)
|
|
99
|
+
op_type = type(node.op)
|
|
100
|
+
if op_type in UNARY_OPS:
|
|
101
|
+
return UNARY_OPS[op_type](operand)
|
|
102
|
+
raise ValueError(f"Unary operation not allowed: {op_type.__name__}")
|
|
103
|
+
|
|
104
|
+
# Comparisons
|
|
105
|
+
if isinstance(node, ast.Compare):
|
|
106
|
+
left = self._eval_node(node.left)
|
|
107
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
108
|
+
right = self._eval_node(comparator)
|
|
109
|
+
op_type = type(op)
|
|
110
|
+
if op_type in COMPARE_OPS:
|
|
111
|
+
if not COMPARE_OPS[op_type](left, right):
|
|
112
|
+
return False
|
|
113
|
+
left = right
|
|
114
|
+
else:
|
|
115
|
+
raise ValueError(f"Comparison not allowed: {op_type.__name__}")
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
# Function calls (abs, round, min, max, etc.)
|
|
119
|
+
if isinstance(node, ast.Call):
|
|
120
|
+
func = self._eval_node(node.func)
|
|
121
|
+
call_args = [self._eval_node(a) for a in node.args]
|
|
122
|
+
# Reject keyword arguments for safety
|
|
123
|
+
if node.keywords:
|
|
124
|
+
raise ValueError("Keyword arguments not allowed")
|
|
125
|
+
return func(*call_args)
|
|
126
|
+
|
|
127
|
+
# Subscripts (list[0])
|
|
128
|
+
if isinstance(node, ast.Subscript):
|
|
129
|
+
value = self._eval_node(node.value)
|
|
130
|
+
slice_val = self._eval_node(node.slice) if isinstance(node.slice, ast.AST) else node.slice
|
|
131
|
+
return value[slice_val]
|
|
132
|
+
|
|
133
|
+
# Lists
|
|
134
|
+
if isinstance(node, ast.List):
|
|
135
|
+
return [self._eval_node(e) for e in node.elts]
|
|
136
|
+
|
|
137
|
+
# Tuples
|
|
138
|
+
if isinstance(node, ast.Tuple):
|
|
139
|
+
return tuple(self._eval_node(e) for e in node.elts)
|
|
140
|
+
|
|
141
|
+
raise ValueError(f"Operation not allowed: {type(node).__name__}")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Sandboxed file operation tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReadFileTool:
|
|
13
|
+
"""Read files from a sandboxed directory."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, sandbox_dir: str = ""):
|
|
16
|
+
self._sandbox = Path(sandbox_dir).resolve() if sandbox_dir else None
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def name(self) -> str:
|
|
20
|
+
return "read_file"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def schema(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {
|
|
27
|
+
"path": {"type": "string", "description": "File path relative to sandbox"},
|
|
28
|
+
},
|
|
29
|
+
"required": ["path"],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
33
|
+
if not self._sandbox:
|
|
34
|
+
return {"error": True, "message": "File sandbox not configured"}
|
|
35
|
+
|
|
36
|
+
path = args.get("path", "")
|
|
37
|
+
try:
|
|
38
|
+
resolved = self._resolve_safe(path)
|
|
39
|
+
if not resolved.exists():
|
|
40
|
+
return {"error": True, "message": f"File not found: {path}"}
|
|
41
|
+
if resolved.is_dir():
|
|
42
|
+
return {"error": True, "message": f"Path is a directory: {path}"}
|
|
43
|
+
content = resolved.read_text(errors="replace")
|
|
44
|
+
return {"content": content, "path": str(resolved.relative_to(self._sandbox)), "size": len(content)}
|
|
45
|
+
except ValueError as e:
|
|
46
|
+
return {"error": True, "message": str(e)}
|
|
47
|
+
|
|
48
|
+
def _resolve_safe(self, path: str) -> Path:
|
|
49
|
+
"""Resolve path within sandbox — reject path traversal."""
|
|
50
|
+
if not self._sandbox:
|
|
51
|
+
raise ValueError("Sandbox not configured")
|
|
52
|
+
resolved = (self._sandbox / path).resolve()
|
|
53
|
+
if not str(resolved).startswith(str(self._sandbox)):
|
|
54
|
+
raise ValueError("Path traversal detected")
|
|
55
|
+
return resolved
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class WriteFileTool:
|
|
59
|
+
"""Write files to a sandboxed directory."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, sandbox_dir: str = ""):
|
|
62
|
+
self._sandbox = Path(sandbox_dir).resolve() if sandbox_dir else None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def name(self) -> str:
|
|
66
|
+
return "write_file"
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def schema(self) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"path": {"type": "string", "description": "File path relative to sandbox"},
|
|
74
|
+
"content": {"type": "string", "description": "Content to write"},
|
|
75
|
+
},
|
|
76
|
+
"required": ["path", "content"],
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
80
|
+
if not self._sandbox:
|
|
81
|
+
return {"error": True, "message": "File sandbox not configured"}
|
|
82
|
+
|
|
83
|
+
path = args.get("path", "")
|
|
84
|
+
content = args.get("content", "")
|
|
85
|
+
try:
|
|
86
|
+
resolved = self._resolve_safe(path)
|
|
87
|
+
resolved.parent.mkdir(parents=True, exist_ok=True)
|
|
88
|
+
resolved.write_text(content)
|
|
89
|
+
return {"path": str(resolved.relative_to(self._sandbox)), "size": len(content), "written": True}
|
|
90
|
+
except ValueError as e:
|
|
91
|
+
return {"error": True, "message": str(e)}
|
|
92
|
+
|
|
93
|
+
def _resolve_safe(self, path: str) -> Path:
|
|
94
|
+
if not self._sandbox:
|
|
95
|
+
raise ValueError("Sandbox not configured")
|
|
96
|
+
resolved = (self._sandbox / path).resolve()
|
|
97
|
+
if not str(resolved).startswith(str(self._sandbox)):
|
|
98
|
+
raise ValueError("Path traversal detected")
|
|
99
|
+
return resolved
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ListDirectoryTool:
|
|
103
|
+
"""List files in a sandboxed directory."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, sandbox_dir: str = ""):
|
|
106
|
+
self._sandbox = Path(sandbox_dir).resolve() if sandbox_dir else None
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def name(self) -> str:
|
|
110
|
+
return "list_directory"
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def schema(self) -> dict:
|
|
114
|
+
return {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"properties": {
|
|
117
|
+
"path": {"type": "string", "description": "Directory path relative to sandbox (default: root)", "default": ""},
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
122
|
+
if not self._sandbox:
|
|
123
|
+
return {"error": True, "message": "File sandbox not configured"}
|
|
124
|
+
|
|
125
|
+
path = args.get("path", "")
|
|
126
|
+
try:
|
|
127
|
+
resolved = self._resolve_safe(path)
|
|
128
|
+
if not resolved.exists():
|
|
129
|
+
return {"error": True, "message": f"Directory not found: {path}"}
|
|
130
|
+
if not resolved.is_dir():
|
|
131
|
+
return {"error": True, "message": f"Not a directory: {path}"}
|
|
132
|
+
|
|
133
|
+
entries = []
|
|
134
|
+
for p in sorted(resolved.iterdir()):
|
|
135
|
+
entries.append({
|
|
136
|
+
"name": p.name,
|
|
137
|
+
"type": "directory" if p.is_dir() else "file",
|
|
138
|
+
"size": p.stat().st_size if p.is_file() else None,
|
|
139
|
+
})
|
|
140
|
+
return {"path": path or "/", "entries": entries, "count": len(entries)}
|
|
141
|
+
except ValueError as e:
|
|
142
|
+
return {"error": True, "message": str(e)}
|
|
143
|
+
|
|
144
|
+
def _resolve_safe(self, path: str) -> Path:
|
|
145
|
+
if not self._sandbox:
|
|
146
|
+
raise ValueError("Sandbox not configured")
|
|
147
|
+
resolved = (self._sandbox / path).resolve() if path else self._sandbox
|
|
148
|
+
if not str(resolved).startswith(str(self._sandbox)):
|
|
149
|
+
raise ValueError("Path traversal detected")
|
|
150
|
+
return resolved
|