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,27 @@
|
|
|
1
|
+
"""Mining: escalation detection + training-corpus export.
|
|
2
|
+
|
|
3
|
+
The model-agnostic mining half of a continuous self-improvement loop:
|
|
4
|
+
escalation events (build-agent consultations, heavy-model / cloud-failover
|
|
5
|
+
turns) become skills-memory distillation inputs and golden eval cases, and
|
|
6
|
+
the verbatim turn capture becomes fine-tune-ready JSONL for an external
|
|
7
|
+
training pipeline. Everything stays under COLONY_STATE_DIR.
|
|
8
|
+
"""
|
|
9
|
+
from apsimo.mining.corpus import export_corpus
|
|
10
|
+
from apsimo.mining.escalations import EscalationMiner
|
|
11
|
+
from apsimo.mining.models import (
|
|
12
|
+
EscalationRecord,
|
|
13
|
+
MinedTurn,
|
|
14
|
+
corpus_export_enabled,
|
|
15
|
+
mining_mode,
|
|
16
|
+
)
|
|
17
|
+
from apsimo.mining.store import MiningStore
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"EscalationMiner",
|
|
21
|
+
"EscalationRecord",
|
|
22
|
+
"MinedTurn",
|
|
23
|
+
"MiningStore",
|
|
24
|
+
"corpus_export_enabled",
|
|
25
|
+
"export_corpus",
|
|
26
|
+
"mining_mode",
|
|
27
|
+
]
|
apsimo/mining/corpus.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Training-corpus exporter: turn store -> fine-tune-ready JSONL.
|
|
2
|
+
|
|
3
|
+
Row shape (the DeepSpec / standard fine-tune contract, one JSON object per
|
|
4
|
+
line): {"id": ..., "conversations": [{"role": "user"|"assistant", "content":
|
|
5
|
+
...}, ...], "meta": {...}}. Conversations always start with a user message
|
|
6
|
+
and alternate user/assistant, so they parse under GeneralParser-style
|
|
7
|
+
tooling without warnings.
|
|
8
|
+
|
|
9
|
+
PII stance: everything stays local. Source rows never leave the sidecar's
|
|
10
|
+
SQLite; the export file is always written under COLONY_STATE_DIR/exports/
|
|
11
|
+
(path traversal is not accepted). Optional redact=true masks credential /
|
|
12
|
+
token shapes via the sidecar's redact machinery. Consumers (fine-tune
|
|
13
|
+
pipelines) read the file from the state dir; nothing is uploaded by this
|
|
14
|
+
code.
|
|
15
|
+
|
|
16
|
+
Quality gate: system/skill-origin turns are excluded using the same
|
|
17
|
+
sanitize/origin machinery the reachout policy uses (is_system_origin +
|
|
18
|
+
sanitize_text), machine markers (e.g. reply-context [[rc ...]] suffixes)
|
|
19
|
+
are stripped, cron/self sessions are excluded by default, and exact
|
|
20
|
+
duplicate exchanges are deduplicated.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import hashlib
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import re
|
|
28
|
+
import time
|
|
29
|
+
from datetime import datetime, timezone
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Dict, List, Optional
|
|
32
|
+
|
|
33
|
+
from apsimo.mining.store import MiningStore
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
_RC_MARKER = re.compile(r"\s*\[\[rc [^\]]*\]\]")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parse_when(value: Optional[str]) -> Optional[float]:
|
|
41
|
+
"""ISO timestamp or relative '<n>d'/'<n>h' -> epoch seconds."""
|
|
42
|
+
if not value:
|
|
43
|
+
return None
|
|
44
|
+
v = value.strip()
|
|
45
|
+
m = re.fullmatch(r"(\d+)([dh])", v)
|
|
46
|
+
if m:
|
|
47
|
+
n, unit = int(m.group(1)), m.group(2)
|
|
48
|
+
return time.time() - n * (86400 if unit == "d" else 3600)
|
|
49
|
+
try:
|
|
50
|
+
return datetime.fromisoformat(v.replace("Z", "+00:00")).timestamp()
|
|
51
|
+
except ValueError:
|
|
52
|
+
raise ValueError(f"unparseable time filter: {value!r}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _clean(text: str, redact: bool) -> str:
|
|
56
|
+
out = _RC_MARKER.sub("", text or "").strip()
|
|
57
|
+
try:
|
|
58
|
+
from apsimo.delivery.reachout_policy import sanitize_text
|
|
59
|
+
|
|
60
|
+
out = sanitize_text(out)
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
63
|
+
if redact:
|
|
64
|
+
try:
|
|
65
|
+
from apsimo.redact import redact_sensitive_text
|
|
66
|
+
|
|
67
|
+
out = redact_sensitive_text(out)
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
return out.strip()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _system_origin(text: str) -> bool:
|
|
74
|
+
try:
|
|
75
|
+
from apsimo.delivery.reachout_policy import is_system_origin
|
|
76
|
+
|
|
77
|
+
return is_system_origin(text or "")
|
|
78
|
+
except Exception:
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def export_corpus(
|
|
83
|
+
store: MiningStore,
|
|
84
|
+
*,
|
|
85
|
+
state_dir: Path,
|
|
86
|
+
contact_id: Optional[str] = None,
|
|
87
|
+
channels: Optional[List[str]] = None,
|
|
88
|
+
since: Optional[str] = None,
|
|
89
|
+
until: Optional[str] = None,
|
|
90
|
+
group: str = "turn",
|
|
91
|
+
min_chars: int = 2,
|
|
92
|
+
include_cron: bool = False,
|
|
93
|
+
include_escalations: bool = False,
|
|
94
|
+
dedup: bool = True,
|
|
95
|
+
redact: bool = False,
|
|
96
|
+
limit: int = 100000,
|
|
97
|
+
filename: Optional[str] = None,
|
|
98
|
+
) -> Dict[str, Any]:
|
|
99
|
+
"""Export filtered conversations as JSONL; returns stats + path."""
|
|
100
|
+
import os
|
|
101
|
+
|
|
102
|
+
if group not in ("turn", "session"):
|
|
103
|
+
raise ValueError("group must be 'turn' or 'session'")
|
|
104
|
+
if contact_id is None:
|
|
105
|
+
contact_id = os.environ.get("COLONY_OWNER_CONTACT_ID", "owner")
|
|
106
|
+
|
|
107
|
+
turns = store.list_turns(
|
|
108
|
+
contact_id=contact_id,
|
|
109
|
+
channels=channels,
|
|
110
|
+
since_ts=_parse_when(since),
|
|
111
|
+
until_ts=_parse_when(until),
|
|
112
|
+
limit=limit,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
skipped = {"quality": 0, "cron": 0, "dedup": 0}
|
|
116
|
+
seen: set = set()
|
|
117
|
+
exchanges: List[Dict[str, Any]] = []
|
|
118
|
+
for t in turns:
|
|
119
|
+
if not include_cron and t.session_id.startswith("cron_"):
|
|
120
|
+
skipped["cron"] += 1
|
|
121
|
+
continue
|
|
122
|
+
user = _clean(t.user_text, redact)
|
|
123
|
+
asst = _clean(t.assistant_text, redact)
|
|
124
|
+
if (
|
|
125
|
+
len(user) < min_chars
|
|
126
|
+
or len(asst) < min_chars
|
|
127
|
+
or _system_origin(t.user_text)
|
|
128
|
+
or _system_origin(t.assistant_text)
|
|
129
|
+
):
|
|
130
|
+
skipped["quality"] += 1
|
|
131
|
+
continue
|
|
132
|
+
if dedup:
|
|
133
|
+
h = hashlib.sha256(f"{user}\x00{asst}".encode()).hexdigest()
|
|
134
|
+
if h in seen:
|
|
135
|
+
skipped["dedup"] += 1
|
|
136
|
+
continue
|
|
137
|
+
seen.add(h)
|
|
138
|
+
exchanges.append(
|
|
139
|
+
{
|
|
140
|
+
"session_id": t.session_id,
|
|
141
|
+
"user": user,
|
|
142
|
+
"assistant": asst,
|
|
143
|
+
"meta": {
|
|
144
|
+
"channel": t.channel_id,
|
|
145
|
+
"contact": t.contact_id,
|
|
146
|
+
"session": t.session_id,
|
|
147
|
+
"ts": t.ts,
|
|
148
|
+
"model": t.model,
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
rows: List[Dict[str, Any]] = []
|
|
154
|
+
if group == "turn":
|
|
155
|
+
for i, ex in enumerate(exchanges):
|
|
156
|
+
rows.append(
|
|
157
|
+
{
|
|
158
|
+
"id": f"turn-{i}",
|
|
159
|
+
"conversations": [
|
|
160
|
+
{"role": "user", "content": ex["user"]},
|
|
161
|
+
{"role": "assistant", "content": ex["assistant"]},
|
|
162
|
+
],
|
|
163
|
+
"meta": ex["meta"],
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
by_session: Dict[str, List[Dict[str, Any]]] = {}
|
|
168
|
+
for ex in exchanges:
|
|
169
|
+
by_session.setdefault(ex["session_id"], []).append(ex)
|
|
170
|
+
for i, (sid, exs) in enumerate(by_session.items()):
|
|
171
|
+
conv: List[Dict[str, str]] = []
|
|
172
|
+
for ex in exs:
|
|
173
|
+
conv.append({"role": "user", "content": ex["user"]})
|
|
174
|
+
conv.append({"role": "assistant", "content": ex["assistant"]})
|
|
175
|
+
rows.append(
|
|
176
|
+
{
|
|
177
|
+
"id": f"session-{i}",
|
|
178
|
+
"conversations": conv,
|
|
179
|
+
"meta": {**exs[0]["meta"], "exchanges": len(exs)},
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
escalations_included = 0
|
|
184
|
+
if include_escalations:
|
|
185
|
+
for e in store.list_escalations(limit=1000):
|
|
186
|
+
user = _clean(e.task_context, redact)
|
|
187
|
+
asst = _clean(e.escalated_answer, redact)
|
|
188
|
+
if len(user) < min_chars or len(asst) < min_chars:
|
|
189
|
+
continue
|
|
190
|
+
rows.append(
|
|
191
|
+
{
|
|
192
|
+
"id": f"escalation-{e.id[:8]}",
|
|
193
|
+
"conversations": [
|
|
194
|
+
{"role": "user", "content": user},
|
|
195
|
+
{"role": "assistant", "content": asst},
|
|
196
|
+
],
|
|
197
|
+
"meta": {
|
|
198
|
+
"kind": e.kind,
|
|
199
|
+
"channel": e.channel_id,
|
|
200
|
+
"matched": e.matched,
|
|
201
|
+
"outcome": e.outcome,
|
|
202
|
+
"model": e.model,
|
|
203
|
+
"ts": e.ts,
|
|
204
|
+
},
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
escalations_included += 1
|
|
208
|
+
|
|
209
|
+
exports_dir = Path(state_dir) / "exports"
|
|
210
|
+
exports_dir.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
name = filename or f"corpus-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.jsonl"
|
|
212
|
+
name = Path(name).name # no traversal: basename only, always under exports/
|
|
213
|
+
out_path = exports_dir / name
|
|
214
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
215
|
+
for row in rows:
|
|
216
|
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
217
|
+
|
|
218
|
+
stats = {
|
|
219
|
+
"path": str(out_path),
|
|
220
|
+
"rows": len(rows),
|
|
221
|
+
"exchanges": len(exchanges),
|
|
222
|
+
"sessions": len({ex["session_id"] for ex in exchanges}),
|
|
223
|
+
"turns_scanned": len(turns),
|
|
224
|
+
"skipped": skipped,
|
|
225
|
+
"escalations_included": escalations_included,
|
|
226
|
+
"group": group,
|
|
227
|
+
"contact_id": contact_id,
|
|
228
|
+
"redacted": redact,
|
|
229
|
+
}
|
|
230
|
+
try:
|
|
231
|
+
from apsimo.events.journal import append_event
|
|
232
|
+
|
|
233
|
+
append_event("mining.corpus_export", stats)
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.debug("corpus export journal failed", exc_info=True)
|
|
236
|
+
logger.info(
|
|
237
|
+
"corpus export: %d rows (%d sessions) -> %s", len(rows), stats["sessions"], out_path
|
|
238
|
+
)
|
|
239
|
+
return stats
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""EscalationMiner: detect + bank escalation events from the live turn stream.
|
|
2
|
+
|
|
3
|
+
Two detectors run on every synced turn:
|
|
4
|
+
|
|
5
|
+
1. CONSULTATION: the agent shelled out to a build/coding agent during the
|
|
6
|
+
turn (a terminal-class tool ran AND the turn text matches
|
|
7
|
+
COLONY_ESCALATION_CONSULT_REGEX). The situation was hard enough that the
|
|
8
|
+
agent escalated to a stronger coding agent: exactly the material worth
|
|
9
|
+
distilling into a skill and worth banking as a golden case.
|
|
10
|
+
2. PROVIDER ESCALATION: the turn's model (optional per-turn metadata from the
|
|
11
|
+
host) matches COLONY_ESCALATION_HEAVY_RE: a heavy-model or cloud-failover
|
|
12
|
+
turn.
|
|
13
|
+
|
|
14
|
+
Each record banks: task/prompt context, the prior local attempt in the same
|
|
15
|
+
session (when present), the escalated answer, channel, model, ts. Outcome is
|
|
16
|
+
tracked lightweight: the NEXT turn in the same session marks the record
|
|
17
|
+
followed_up with the user's reaction excerpt.
|
|
18
|
+
|
|
19
|
+
Modes (COLONY_ESCALATION_MINING): shadow banks + journals only; live also
|
|
20
|
+
feeds records into skills-memory distillation (domain "escalation"), which
|
|
21
|
+
itself honors COLONY_SKILLS_DISTILL. Everything is best-effort: a miner
|
|
22
|
+
failure must never affect the turn pipeline.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import logging
|
|
28
|
+
import re
|
|
29
|
+
from typing import Any, Callable, List, Optional
|
|
30
|
+
|
|
31
|
+
from apsimo.mining.models import (
|
|
32
|
+
EscalationRecord,
|
|
33
|
+
MinedTurn,
|
|
34
|
+
consult_regex,
|
|
35
|
+
heavy_model_regex,
|
|
36
|
+
mining_mode,
|
|
37
|
+
turn_cap,
|
|
38
|
+
)
|
|
39
|
+
from apsimo.mining.store import MiningStore
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
_TERMINAL_TOOLS = {"terminal", "shell", "bash", "execute_code", "shell_exec"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _session_tool_activity(session_id: str, max_lines: int = 400):
|
|
47
|
+
"""Recent host tool-activity for a session: (summaries_text, tool_names).
|
|
48
|
+
|
|
49
|
+
Consultations often show only in the tool COMMANDS (e.g. `claude -p ...`
|
|
50
|
+
run via the terminal tool), not in the reply text. When the host exposes
|
|
51
|
+
its tool-activity stream (COLONY_TOOL_ACTIVITY_FILE, a JSONL of
|
|
52
|
+
{ts, session, tool, summary}), include those summaries in the detection
|
|
53
|
+
text and use the tool names as a terminal-use fallback signal.
|
|
54
|
+
Best-effort: any failure returns ("", empty set).
|
|
55
|
+
"""
|
|
56
|
+
import json as _json
|
|
57
|
+
import os as _os
|
|
58
|
+
|
|
59
|
+
path = _os.environ.get("COLONY_TOOL_ACTIVITY_FILE", "")
|
|
60
|
+
if not path or not session_id:
|
|
61
|
+
return "", set()
|
|
62
|
+
try:
|
|
63
|
+
with open(_os.path.expanduser(path), "rb") as f:
|
|
64
|
+
try:
|
|
65
|
+
f.seek(-200_000, 2)
|
|
66
|
+
except OSError:
|
|
67
|
+
f.seek(0)
|
|
68
|
+
lines = f.read().decode("utf-8", "replace").splitlines()[-max_lines:]
|
|
69
|
+
out = []
|
|
70
|
+
tools = set()
|
|
71
|
+
for ln in lines:
|
|
72
|
+
try:
|
|
73
|
+
r = _json.loads(ln)
|
|
74
|
+
except Exception:
|
|
75
|
+
continue
|
|
76
|
+
if r.get("session") == session_id:
|
|
77
|
+
if r.get("tool"):
|
|
78
|
+
tools.add(str(r["tool"]))
|
|
79
|
+
if r.get("summary"):
|
|
80
|
+
out.append(str(r["summary"]))
|
|
81
|
+
return "\n".join(out[-50:]), tools
|
|
82
|
+
except Exception:
|
|
83
|
+
return "", set()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class EscalationMiner:
|
|
87
|
+
def __init__(
|
|
88
|
+
self,
|
|
89
|
+
store: MiningStore,
|
|
90
|
+
*,
|
|
91
|
+
skill_store: Any = None,
|
|
92
|
+
router_getter: Optional[Callable[[], Any]] = None,
|
|
93
|
+
) -> None:
|
|
94
|
+
self._store = store
|
|
95
|
+
self._skill_store = skill_store
|
|
96
|
+
self._router_getter = router_getter
|
|
97
|
+
|
|
98
|
+
# -- turn intake -----------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def observe_turn(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
session_id: str,
|
|
104
|
+
contact_id: str,
|
|
105
|
+
channel_id: str,
|
|
106
|
+
user_text: str,
|
|
107
|
+
assistant_text: str,
|
|
108
|
+
summary: str = "",
|
|
109
|
+
tools_used: Optional[List[str]] = None,
|
|
110
|
+
model: str = "",
|
|
111
|
+
) -> Optional[EscalationRecord]:
|
|
112
|
+
"""Bank the verbatim turn, close out a pending escalation's outcome,
|
|
113
|
+
and detect a new escalation. Returns the new record, if any."""
|
|
114
|
+
mode = mining_mode()
|
|
115
|
+
if mode == "off":
|
|
116
|
+
return None
|
|
117
|
+
try:
|
|
118
|
+
cap = turn_cap()
|
|
119
|
+
tools = list(tools_used or [])
|
|
120
|
+
prior = self._store.last_turn_in_session(session_id)
|
|
121
|
+
|
|
122
|
+
# Outcome follow-up: the next turn's user message is the reaction
|
|
123
|
+
# to a previously banked escalation in this session.
|
|
124
|
+
try:
|
|
125
|
+
open_esc = self._store.latest_open_escalation(session_id)
|
|
126
|
+
if open_esc is not None and user_text:
|
|
127
|
+
open_esc.outcome = "followed_up"
|
|
128
|
+
open_esc.outcome_note = user_text[:300]
|
|
129
|
+
self._store.update_escalation(open_esc)
|
|
130
|
+
except Exception:
|
|
131
|
+
logger.debug("escalation outcome follow-up failed", exc_info=True)
|
|
132
|
+
|
|
133
|
+
turn = MinedTurn(
|
|
134
|
+
session_id=session_id,
|
|
135
|
+
contact_id=contact_id,
|
|
136
|
+
channel_id=channel_id,
|
|
137
|
+
user_text=(user_text or "")[:cap],
|
|
138
|
+
assistant_text=(assistant_text or "")[:cap],
|
|
139
|
+
summary=(summary or "")[:1000],
|
|
140
|
+
tools_used=tools[:20],
|
|
141
|
+
model=model or "",
|
|
142
|
+
)
|
|
143
|
+
self._store.add_turn(turn)
|
|
144
|
+
|
|
145
|
+
record = self._detect(turn, prior)
|
|
146
|
+
if record is None:
|
|
147
|
+
return None
|
|
148
|
+
self._store.add_escalation(record)
|
|
149
|
+
self._journal(record, mode)
|
|
150
|
+
if mode == "live":
|
|
151
|
+
self._feed_distiller(record)
|
|
152
|
+
return record
|
|
153
|
+
except Exception:
|
|
154
|
+
logger.debug("escalation miner observe_turn failed", exc_info=True)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
# -- detection --------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def _detect(
|
|
160
|
+
self, turn: MinedTurn, prior: Optional[MinedTurn]
|
|
161
|
+
) -> Optional[EscalationRecord]:
|
|
162
|
+
text = f"{turn.user_text}\n{turn.assistant_text}\n{turn.summary}"
|
|
163
|
+
tool_text, activity_tools = _session_tool_activity(turn.session_id)
|
|
164
|
+
if tool_text:
|
|
165
|
+
text = f"{text}\n{tool_text}"
|
|
166
|
+
|
|
167
|
+
# (a) build-agent consultation. Terminal-use comes from the synced
|
|
168
|
+
# tools_used when the host sends it, with the host tool-activity
|
|
169
|
+
# stream as the fallback source.
|
|
170
|
+
all_tools = set(turn.tools_used) | activity_tools
|
|
171
|
+
ran_terminal = any(t in _TERMINAL_TOOLS for t in all_tools)
|
|
172
|
+
pattern = consult_regex()
|
|
173
|
+
if ran_terminal and pattern:
|
|
174
|
+
try:
|
|
175
|
+
m = re.search(pattern, text, re.IGNORECASE)
|
|
176
|
+
except re.error:
|
|
177
|
+
m = None
|
|
178
|
+
logger.warning("invalid COLONY_ESCALATION_CONSULT_REGEX; detector idle")
|
|
179
|
+
if m:
|
|
180
|
+
return EscalationRecord(
|
|
181
|
+
kind="consultation",
|
|
182
|
+
session_id=turn.session_id,
|
|
183
|
+
contact_id=turn.contact_id,
|
|
184
|
+
channel_id=turn.channel_id,
|
|
185
|
+
task_context=turn.user_text[:2000],
|
|
186
|
+
local_attempt=(prior.assistant_text[:2000] if prior else ""),
|
|
187
|
+
escalated_answer=turn.assistant_text[:4000],
|
|
188
|
+
model=turn.model,
|
|
189
|
+
matched=m.group(0)[:120],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# (b) heavy-model / cloud-failover turn
|
|
193
|
+
heavy = heavy_model_regex()
|
|
194
|
+
if heavy and turn.model:
|
|
195
|
+
try:
|
|
196
|
+
hm = re.search(heavy, turn.model, re.IGNORECASE)
|
|
197
|
+
except re.error:
|
|
198
|
+
hm = None
|
|
199
|
+
logger.warning("invalid COLONY_ESCALATION_HEAVY_RE; detector idle")
|
|
200
|
+
if hm:
|
|
201
|
+
return EscalationRecord(
|
|
202
|
+
kind="provider_escalation",
|
|
203
|
+
session_id=turn.session_id,
|
|
204
|
+
contact_id=turn.contact_id,
|
|
205
|
+
channel_id=turn.channel_id,
|
|
206
|
+
task_context=turn.user_text[:2000],
|
|
207
|
+
local_attempt=(prior.assistant_text[:2000] if prior else ""),
|
|
208
|
+
escalated_answer=turn.assistant_text[:4000],
|
|
209
|
+
model=turn.model,
|
|
210
|
+
matched=hm.group(0)[:120],
|
|
211
|
+
)
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
# -- consumers ---------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
def _journal(self, record: EscalationRecord, mode: str) -> None:
|
|
217
|
+
try:
|
|
218
|
+
from apsimo.events.journal import append_event
|
|
219
|
+
|
|
220
|
+
append_event(
|
|
221
|
+
"mining.escalation",
|
|
222
|
+
{
|
|
223
|
+
"escalation_id": record.id,
|
|
224
|
+
"kind": record.kind,
|
|
225
|
+
"channel_id": record.channel_id,
|
|
226
|
+
"session_id": record.session_id,
|
|
227
|
+
"contact_id": record.contact_id,
|
|
228
|
+
"model": record.model,
|
|
229
|
+
"matched": record.matched,
|
|
230
|
+
"mode": mode,
|
|
231
|
+
"summary": record.task_context[:200],
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
except Exception:
|
|
235
|
+
logger.debug("mining.escalation journal failed", exc_info=True)
|
|
236
|
+
|
|
237
|
+
def _feed_distiller(self, record: EscalationRecord) -> None:
|
|
238
|
+
"""Live mode: escalations are high-value distillation inputs."""
|
|
239
|
+
try:
|
|
240
|
+
from apsimo.skills_memory import (
|
|
241
|
+
distill_from_completion,
|
|
242
|
+
skills_distill_mode,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if skills_distill_mode() == "off" or self._skill_store is None:
|
|
246
|
+
return
|
|
247
|
+
router = self._router_getter() if self._router_getter else None
|
|
248
|
+
if router is None:
|
|
249
|
+
return
|
|
250
|
+
result_text = record.escalated_answer
|
|
251
|
+
if record.local_attempt:
|
|
252
|
+
result_text = (
|
|
253
|
+
f"Local attempt (before escalating):\n{record.local_attempt}\n\n"
|
|
254
|
+
f"Escalated resolution:\n{record.escalated_answer}"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
async def _run() -> None:
|
|
258
|
+
try:
|
|
259
|
+
skill = await distill_from_completion(
|
|
260
|
+
router,
|
|
261
|
+
self._skill_store,
|
|
262
|
+
domain="escalation",
|
|
263
|
+
task_text=record.task_context,
|
|
264
|
+
result_text=result_text,
|
|
265
|
+
source_ref=record.id,
|
|
266
|
+
)
|
|
267
|
+
if skill is not None:
|
|
268
|
+
record.distilled = 1
|
|
269
|
+
self._store.update_escalation(record)
|
|
270
|
+
except Exception:
|
|
271
|
+
logger.debug("escalation distillation failed", exc_info=True)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
loop = asyncio.get_running_loop()
|
|
275
|
+
loop.create_task(_run())
|
|
276
|
+
except RuntimeError:
|
|
277
|
+
asyncio.run(_run())
|
|
278
|
+
except Exception:
|
|
279
|
+
logger.debug("escalation distiller feed failed", exc_info=True)
|
|
280
|
+
|
|
281
|
+
# -- reads -------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def recent(self, *, kind: Optional[str] = None, limit: int = 50) -> List[dict]:
|
|
284
|
+
return [e.to_row() for e in self._store.list_escalations(kind=kind, limit=limit)]
|
|
285
|
+
|
|
286
|
+
def stats(self) -> dict:
|
|
287
|
+
s = self._store.escalation_stats()
|
|
288
|
+
s["mode"] = mining_mode()
|
|
289
|
+
return s
|