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,245 @@
|
|
|
1
|
+
"""ContinuousLearner — fuse live signals into intelligence component weights.
|
|
2
|
+
|
|
3
|
+
Unlike MetaLearner (which operates on historical windows), this component
|
|
4
|
+
processes signals in near-real-time and updates component weights without
|
|
5
|
+
waiting for a training window to close.
|
|
6
|
+
|
|
7
|
+
Signal sources:
|
|
8
|
+
- User corrections (explicit: "that's wrong", "don't do that again")
|
|
9
|
+
- Implicit feedback (task re-done immediately → low quality)
|
|
10
|
+
- Engagement signals (briefing sections read vs skipped)
|
|
11
|
+
- Goal success/failure outcomes
|
|
12
|
+
|
|
13
|
+
Weight update rule: exponential moving average with per-signal-type learning
|
|
14
|
+
rates. Weights are clamped to [0.1, 2.0] so no component is permanently
|
|
15
|
+
silenced or runs unrestrained.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing import Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
# Signal types
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class BriefingEngagement:
|
|
33
|
+
"""Engagement data from a delivered briefing."""
|
|
34
|
+
|
|
35
|
+
briefing_id: str
|
|
36
|
+
section: str # e.g. "goals", "relationships", "insights"
|
|
37
|
+
read: bool # True = read, False = skipped
|
|
38
|
+
dwell_seconds: float = 0.0
|
|
39
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class GoalOutcome:
|
|
44
|
+
"""Outcome of a goal managed by Colony."""
|
|
45
|
+
|
|
46
|
+
goal_id: str
|
|
47
|
+
success: bool
|
|
48
|
+
component: str # Which component's output influenced this goal
|
|
49
|
+
confidence_at_time: float = 1.0
|
|
50
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
# Learning rates by signal type
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
_LEARNING_RATES: Dict[str, float] = {
|
|
58
|
+
# correction_type → lr (how much a single correction moves the weight)
|
|
59
|
+
"factual": 0.10,
|
|
60
|
+
"tone": 0.05,
|
|
61
|
+
"action": 0.08,
|
|
62
|
+
"preference": 0.06,
|
|
63
|
+
# engagement
|
|
64
|
+
"engagement_read": 0.02,
|
|
65
|
+
"engagement_skip": 0.03,
|
|
66
|
+
# outcomes
|
|
67
|
+
"outcome_success": 0.04,
|
|
68
|
+
"outcome_failure": 0.06,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
_DEFAULT_WEIGHT = 1.0
|
|
72
|
+
_WEIGHT_MIN = 0.1
|
|
73
|
+
_WEIGHT_MAX = 2.0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Main class
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
class ContinuousLearner:
|
|
81
|
+
"""Fuse live signals into intelligence component weights.
|
|
82
|
+
|
|
83
|
+
Weights are maintained in-memory and optionally persisted to a graph
|
|
84
|
+
if a ``graph_client`` is provided.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
graph_client: Optional ColonyGraph instance for weight persistence.
|
|
88
|
+
initial_weights: Optional seed weights (component_name → float).
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(
|
|
92
|
+
self,
|
|
93
|
+
graph_client: Optional[object] = None,
|
|
94
|
+
initial_weights: Optional[Dict[str, float]] = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
self._graph = graph_client
|
|
97
|
+
# Component weights: maps component name to a multiplier [0.1, 2.0]
|
|
98
|
+
self._weights: Dict[str, float] = dict(initial_weights or {})
|
|
99
|
+
self._correction_count = 0
|
|
100
|
+
self._engagement_count = 0
|
|
101
|
+
self._outcome_count = 0
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# Signal ingestion
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
async def ingest_correction(self, correction: "UserCorrection") -> None: # noqa: F821
|
|
108
|
+
"""Update component weights based on an explicit user correction.
|
|
109
|
+
|
|
110
|
+
The component inferred from correction_type has its weight reduced
|
|
111
|
+
(we were wrong); complementary components may be boosted slightly.
|
|
112
|
+
"""
|
|
113
|
+
lr = _LEARNING_RATES.get(correction.correction_type, 0.05)
|
|
114
|
+
component = self._correction_type_to_component(correction.correction_type)
|
|
115
|
+
|
|
116
|
+
# Penalise the component that produced the wrong output
|
|
117
|
+
self._adjust_weight(component, -lr)
|
|
118
|
+
|
|
119
|
+
# Slight boost to components that weren't responsible
|
|
120
|
+
if correction.correction_type == "factual":
|
|
121
|
+
self._adjust_weight("retrieval", -lr * 0.5)
|
|
122
|
+
elif correction.correction_type == "action":
|
|
123
|
+
self._adjust_weight("task_planner", -lr * 0.5)
|
|
124
|
+
|
|
125
|
+
self._correction_count += 1
|
|
126
|
+
logger.debug(
|
|
127
|
+
"ingest_correction: type=%s component=%s lr=%.3f new_weight=%.3f",
|
|
128
|
+
correction.correction_type,
|
|
129
|
+
component,
|
|
130
|
+
lr,
|
|
131
|
+
self._weights.get(component, _DEFAULT_WEIGHT),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
await self._maybe_persist_weights()
|
|
135
|
+
|
|
136
|
+
async def ingest_engagement(self, engagement: BriefingEngagement) -> None:
|
|
137
|
+
"""Update weights from briefing section engagement signal."""
|
|
138
|
+
signal_type = "engagement_read" if engagement.read else "engagement_skip"
|
|
139
|
+
lr = _LEARNING_RATES[signal_type]
|
|
140
|
+
component = self._section_to_component(engagement.section)
|
|
141
|
+
|
|
142
|
+
# Read → boost, skip → penalise
|
|
143
|
+
delta = lr if engagement.read else -lr
|
|
144
|
+
self._adjust_weight(component, delta)
|
|
145
|
+
|
|
146
|
+
self._engagement_count += 1
|
|
147
|
+
logger.debug(
|
|
148
|
+
"ingest_engagement: section=%s read=%s component=%s delta=%.3f",
|
|
149
|
+
engagement.section,
|
|
150
|
+
engagement.read,
|
|
151
|
+
component,
|
|
152
|
+
delta,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
await self._maybe_persist_weights()
|
|
156
|
+
|
|
157
|
+
async def ingest_outcome(self, outcome: GoalOutcome) -> None:
|
|
158
|
+
"""Update weights from goal success/failure."""
|
|
159
|
+
lr = _LEARNING_RATES["outcome_success" if outcome.success else "outcome_failure"]
|
|
160
|
+
# Scale by confidence: high-confidence failures are penalised more
|
|
161
|
+
effective_lr = lr * outcome.confidence_at_time
|
|
162
|
+
delta = effective_lr if outcome.success else -effective_lr
|
|
163
|
+
self._adjust_weight(outcome.component, delta)
|
|
164
|
+
|
|
165
|
+
self._outcome_count += 1
|
|
166
|
+
logger.debug(
|
|
167
|
+
"ingest_outcome: goal=%s success=%s component=%s delta=%.3f",
|
|
168
|
+
outcome.goal_id,
|
|
169
|
+
outcome.success,
|
|
170
|
+
outcome.component,
|
|
171
|
+
delta,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
await self._maybe_persist_weights()
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Weight access
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
async def get_component_weights(self) -> Dict[str, float]:
|
|
181
|
+
"""Return a snapshot of current component weights."""
|
|
182
|
+
return dict(self._weights)
|
|
183
|
+
|
|
184
|
+
def get_weight(self, component: str) -> float:
|
|
185
|
+
"""Return current weight for *component* (1.0 if not yet set)."""
|
|
186
|
+
return self._weights.get(component, _DEFAULT_WEIGHT)
|
|
187
|
+
|
|
188
|
+
def set_weight(self, component: str, value: float) -> None:
|
|
189
|
+
"""Manually set a component weight (e.g., MetaLearner override)."""
|
|
190
|
+
self._weights[component] = max(_WEIGHT_MIN, min(_WEIGHT_MAX, value))
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def stats(self) -> Dict[str, int]:
|
|
194
|
+
"""Return ingestion counters."""
|
|
195
|
+
return {
|
|
196
|
+
"corrections": self._correction_count,
|
|
197
|
+
"engagements": self._engagement_count,
|
|
198
|
+
"outcomes": self._outcome_count,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# ------------------------------------------------------------------
|
|
202
|
+
# Internal helpers
|
|
203
|
+
# ------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def _adjust_weight(self, component: str, delta: float) -> None:
|
|
206
|
+
"""Apply *delta* to *component* weight, clamping to [min, max]."""
|
|
207
|
+
current = self._weights.get(component, _DEFAULT_WEIGHT)
|
|
208
|
+
updated = max(_WEIGHT_MIN, min(_WEIGHT_MAX, current + delta))
|
|
209
|
+
self._weights[component] = updated
|
|
210
|
+
|
|
211
|
+
async def _maybe_persist_weights(self) -> None:
|
|
212
|
+
"""Persist current weights to the graph if a client is available."""
|
|
213
|
+
if self._graph is None:
|
|
214
|
+
return
|
|
215
|
+
try:
|
|
216
|
+
weights_json = str(self._weights) # simple repr; real impl uses JSON
|
|
217
|
+
if hasattr(self._graph, "execute"):
|
|
218
|
+
await self._graph.execute(
|
|
219
|
+
"MERGE (w:LearnerWeights {id: 'continuous_learner'}) "
|
|
220
|
+
"SET w.weights = $weights, w.updated_at = datetime()",
|
|
221
|
+
weights=weights_json,
|
|
222
|
+
)
|
|
223
|
+
except Exception as exc:
|
|
224
|
+
logger.debug("Weight persistence skipped: %s", exc)
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _correction_type_to_component(correction_type: str) -> str:
|
|
228
|
+
mapping = {
|
|
229
|
+
"factual": "knowledge",
|
|
230
|
+
"tone": "response_quality",
|
|
231
|
+
"action": "task_planner",
|
|
232
|
+
"preference": "preference_learner",
|
|
233
|
+
}
|
|
234
|
+
return mapping.get(correction_type, "general")
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _section_to_component(section: str) -> str:
|
|
238
|
+
mapping = {
|
|
239
|
+
"goals": "goal_engine",
|
|
240
|
+
"relationships": "engagement_profile",
|
|
241
|
+
"insights": "research_orchestrator",
|
|
242
|
+
"anomalies": "anomaly_detector",
|
|
243
|
+
"predictions": "mind_predictor",
|
|
244
|
+
}
|
|
245
|
+
return mapping.get(section, "briefing")
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""FeedbackStore — persist and retrieve user corrections for learning.
|
|
2
|
+
|
|
3
|
+
Corrections feed into ContinuousLearner and are periodically summarized
|
|
4
|
+
by MetaLearner into durable preference updates.
|
|
5
|
+
|
|
6
|
+
Storage: SQLite (default ~/.colony/feedback.db) so corrections survive
|
|
7
|
+
process restarts without requiring Neo4j to be reachable.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import sqlite3
|
|
16
|
+
import uuid
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from dataclasses import asdict, dataclass, field
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from typing import Any, Dict, Generator, List, Optional
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
_DEFAULT_DB_PATH = os.path.join(
|
|
25
|
+
os.path.expanduser("~"), ".colony", "feedback.db"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
_SCHEMA = """
|
|
29
|
+
CREATE TABLE IF NOT EXISTS corrections (
|
|
30
|
+
correction_id TEXT PRIMARY KEY,
|
|
31
|
+
timestamp TEXT NOT NULL,
|
|
32
|
+
original_response TEXT NOT NULL,
|
|
33
|
+
correction_text TEXT NOT NULL,
|
|
34
|
+
correction_type TEXT NOT NULL,
|
|
35
|
+
context_hash TEXT NOT NULL,
|
|
36
|
+
applied INTEGER NOT NULL DEFAULT 0,
|
|
37
|
+
person_id TEXT NOT NULL DEFAULT '',
|
|
38
|
+
processed_at TEXT
|
|
39
|
+
);
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Migrations for existing databases that pre-date new columns.
|
|
43
|
+
_MIGRATIONS = [
|
|
44
|
+
"ALTER TABLE corrections ADD COLUMN person_id TEXT NOT NULL DEFAULT ''",
|
|
45
|
+
"ALTER TABLE corrections ADD COLUMN processed_at TEXT",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# Data types
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class UserCorrection:
|
|
55
|
+
"""A single user correction event."""
|
|
56
|
+
|
|
57
|
+
correction_id: str
|
|
58
|
+
timestamp: datetime
|
|
59
|
+
original_response: str
|
|
60
|
+
correction_text: str
|
|
61
|
+
correction_type: str # "factual" | "tone" | "action" | "preference"
|
|
62
|
+
context_hash: str
|
|
63
|
+
applied: bool = False
|
|
64
|
+
person_id: str = ""
|
|
65
|
+
processed_at: Optional[datetime] = None
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def create(
|
|
69
|
+
cls,
|
|
70
|
+
original_response: str,
|
|
71
|
+
correction_text: str,
|
|
72
|
+
correction_type: str,
|
|
73
|
+
context_hash: str,
|
|
74
|
+
person_id: str = "",
|
|
75
|
+
) -> "UserCorrection":
|
|
76
|
+
"""Factory that generates a UUID and timestamps the correction."""
|
|
77
|
+
return cls(
|
|
78
|
+
correction_id=str(uuid.uuid4()),
|
|
79
|
+
timestamp=datetime.now(timezone.utc),
|
|
80
|
+
original_response=original_response,
|
|
81
|
+
correction_text=correction_text,
|
|
82
|
+
correction_type=correction_type,
|
|
83
|
+
context_hash=context_hash,
|
|
84
|
+
person_id=person_id,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Store
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
class FeedbackStore:
|
|
93
|
+
"""Persist and retrieve user corrections for learning.
|
|
94
|
+
|
|
95
|
+
Thread-safe via SQLite's WAL mode. All public methods are synchronous
|
|
96
|
+
so they can be called from both sync and async contexts without a
|
|
97
|
+
dedicated event loop.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
db_path: Path to the SQLite database file. Defaults to
|
|
101
|
+
``~/.colony/feedback.db``. Pass ``:memory:`` for tests.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, db_path: str = _DEFAULT_DB_PATH) -> None:
|
|
105
|
+
self._db_path = db_path
|
|
106
|
+
# For :memory: DBs we keep a single persistent connection so schema survives
|
|
107
|
+
self._mem_conn: sqlite3.Connection | None = None
|
|
108
|
+
if db_path == ":memory:":
|
|
109
|
+
self._mem_conn = sqlite3.connect(":memory:", check_same_thread=False)
|
|
110
|
+
self._mem_conn.row_factory = sqlite3.Row
|
|
111
|
+
self._mem_conn.execute("PRAGMA journal_mode=WAL")
|
|
112
|
+
self._mem_conn.executescript(_SCHEMA)
|
|
113
|
+
self._mem_conn.commit()
|
|
114
|
+
else:
|
|
115
|
+
self._ensure_db()
|
|
116
|
+
|
|
117
|
+
# ------------------------------------------------------------------
|
|
118
|
+
# Public API
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
def record_correction(self, correction: UserCorrection) -> None:
|
|
122
|
+
"""Persist *correction* to the store (idempotent by correction_id)."""
|
|
123
|
+
with self._conn() as conn:
|
|
124
|
+
conn.execute(
|
|
125
|
+
"INSERT OR REPLACE INTO corrections "
|
|
126
|
+
"(correction_id, timestamp, original_response, correction_text, "
|
|
127
|
+
" correction_type, context_hash, applied, person_id, processed_at) "
|
|
128
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
129
|
+
(
|
|
130
|
+
correction.correction_id,
|
|
131
|
+
correction.timestamp.isoformat(),
|
|
132
|
+
correction.original_response,
|
|
133
|
+
correction.correction_text,
|
|
134
|
+
correction.correction_type,
|
|
135
|
+
correction.context_hash,
|
|
136
|
+
int(correction.applied),
|
|
137
|
+
correction.person_id,
|
|
138
|
+
correction.processed_at.isoformat() if correction.processed_at else None,
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
logger.debug("Recorded correction %s (%s)", correction.correction_id, correction.correction_type)
|
|
142
|
+
|
|
143
|
+
def get_unapplied(self, limit: int = 50) -> List[UserCorrection]:
|
|
144
|
+
"""Return up to *limit* corrections that have not yet been applied."""
|
|
145
|
+
with self._conn() as conn:
|
|
146
|
+
rows = conn.execute(
|
|
147
|
+
"SELECT * FROM corrections WHERE applied = 0 "
|
|
148
|
+
"ORDER BY timestamp ASC LIMIT ?",
|
|
149
|
+
(limit,),
|
|
150
|
+
).fetchall()
|
|
151
|
+
return [self._row_to_correction(r) for r in rows]
|
|
152
|
+
|
|
153
|
+
def mark_processed(self, correction_ids: List[str]) -> None:
|
|
154
|
+
"""Stamp *processed_at* on each correction so consume_unprocessed skips it."""
|
|
155
|
+
if not correction_ids:
|
|
156
|
+
return
|
|
157
|
+
now_iso = datetime.now(timezone.utc).isoformat()
|
|
158
|
+
placeholders = ",".join("?" * len(correction_ids))
|
|
159
|
+
with self._conn() as conn:
|
|
160
|
+
conn.execute(
|
|
161
|
+
f"UPDATE corrections SET processed_at = ? "
|
|
162
|
+
f"WHERE correction_id IN ({placeholders})",
|
|
163
|
+
[now_iso] + list(correction_ids),
|
|
164
|
+
)
|
|
165
|
+
logger.debug("Marked %d corrections as processed", len(correction_ids))
|
|
166
|
+
|
|
167
|
+
def consume_unprocessed(self) -> List[UserCorrection]:
|
|
168
|
+
"""Fetch all unprocessed corrections, mark them processed, and return them."""
|
|
169
|
+
with self._conn() as conn:
|
|
170
|
+
cols = {row[1] for row in conn.execute("PRAGMA table_info(corrections)").fetchall()}
|
|
171
|
+
if "processed_at" not in cols:
|
|
172
|
+
conn.execute("ALTER TABLE corrections ADD COLUMN processed_at TEXT")
|
|
173
|
+
|
|
174
|
+
rows = conn.execute(
|
|
175
|
+
"SELECT * FROM corrections WHERE processed_at IS NULL ORDER BY timestamp ASC"
|
|
176
|
+
).fetchall()
|
|
177
|
+
|
|
178
|
+
if not rows:
|
|
179
|
+
return []
|
|
180
|
+
|
|
181
|
+
corrections = [self._row_to_correction(r) for r in rows]
|
|
182
|
+
self.mark_processed([c.correction_id for c in corrections])
|
|
183
|
+
logger.info("consume_unprocessed: returned %d corrections", len(corrections))
|
|
184
|
+
return corrections
|
|
185
|
+
|
|
186
|
+
def mark_applied(self, correction_ids: List[str]) -> None:
|
|
187
|
+
"""Mark the given corrections as applied (won't be returned by get_unapplied)."""
|
|
188
|
+
if not correction_ids:
|
|
189
|
+
return
|
|
190
|
+
placeholders = ",".join("?" * len(correction_ids))
|
|
191
|
+
with self._conn() as conn:
|
|
192
|
+
conn.execute(
|
|
193
|
+
f"UPDATE corrections SET applied = 1 WHERE correction_id IN ({placeholders})",
|
|
194
|
+
correction_ids,
|
|
195
|
+
)
|
|
196
|
+
logger.debug("Marked %d corrections as applied", len(correction_ids))
|
|
197
|
+
|
|
198
|
+
def get_correction_summary(self, days: int = 7) -> Dict[str, object]:
|
|
199
|
+
"""Return summary statistics for corrections in the last *days* days."""
|
|
200
|
+
cutoff = (
|
|
201
|
+
datetime.now(timezone.utc)
|
|
202
|
+
.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
203
|
+
)
|
|
204
|
+
# Subtract days manually (no timedelta import needed above)
|
|
205
|
+
import datetime as _dt
|
|
206
|
+
cutoff = cutoff - _dt.timedelta(days=days)
|
|
207
|
+
cutoff_iso = cutoff.isoformat()
|
|
208
|
+
|
|
209
|
+
with self._conn() as conn:
|
|
210
|
+
total = conn.execute(
|
|
211
|
+
"SELECT COUNT(*) FROM corrections WHERE timestamp >= ?", (cutoff_iso,)
|
|
212
|
+
).fetchone()[0]
|
|
213
|
+
applied = conn.execute(
|
|
214
|
+
"SELECT COUNT(*) FROM corrections WHERE timestamp >= ? AND applied = 1",
|
|
215
|
+
(cutoff_iso,),
|
|
216
|
+
).fetchone()[0]
|
|
217
|
+
by_type_rows = conn.execute(
|
|
218
|
+
"SELECT correction_type, COUNT(*) FROM corrections "
|
|
219
|
+
"WHERE timestamp >= ? GROUP BY correction_type",
|
|
220
|
+
(cutoff_iso,),
|
|
221
|
+
).fetchall()
|
|
222
|
+
|
|
223
|
+
by_type: Dict[str, int] = {row[0]: row[1] for row in by_type_rows}
|
|
224
|
+
return {
|
|
225
|
+
"days": days,
|
|
226
|
+
"total": total,
|
|
227
|
+
"applied": applied,
|
|
228
|
+
"unapplied": total - applied,
|
|
229
|
+
"by_type": by_type,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def between(
|
|
233
|
+
self,
|
|
234
|
+
since_iso: str,
|
|
235
|
+
until_iso: str,
|
|
236
|
+
*,
|
|
237
|
+
person_id: str = "",
|
|
238
|
+
limit: int = 10000,
|
|
239
|
+
) -> List[Dict[str, Any]]:
|
|
240
|
+
"""Read a stable correction cohort without consuming its rows."""
|
|
241
|
+
|
|
242
|
+
query = (
|
|
243
|
+
"SELECT correction_id,timestamp,correction_type,context_hash,"
|
|
244
|
+
"applied,person_id,processed_at FROM corrections "
|
|
245
|
+
"WHERE timestamp>=? AND timestamp<?")
|
|
246
|
+
params: List[Any] = [since_iso, until_iso]
|
|
247
|
+
if person_id:
|
|
248
|
+
query += " AND person_id=?"
|
|
249
|
+
params.append(person_id)
|
|
250
|
+
query += " ORDER BY timestamp,correction_id LIMIT ?"
|
|
251
|
+
params.append(max(1, min(int(limit), 100000)))
|
|
252
|
+
with self._conn() as conn:
|
|
253
|
+
rows = conn.execute(query, params).fetchall()
|
|
254
|
+
return [dict(row) for row in rows]
|
|
255
|
+
|
|
256
|
+
def count(self) -> int:
|
|
257
|
+
"""Return total number of stored corrections."""
|
|
258
|
+
with self._conn() as conn:
|
|
259
|
+
return conn.execute("SELECT COUNT(*) FROM corrections").fetchone()[0]
|
|
260
|
+
|
|
261
|
+
def clear(self) -> None:
|
|
262
|
+
"""Delete all records (useful for tests / onboarding reset)."""
|
|
263
|
+
with self._conn() as conn:
|
|
264
|
+
conn.execute("DELETE FROM corrections")
|
|
265
|
+
|
|
266
|
+
# ------------------------------------------------------------------
|
|
267
|
+
# Private helpers
|
|
268
|
+
# ------------------------------------------------------------------
|
|
269
|
+
|
|
270
|
+
def _ensure_db(self) -> None:
|
|
271
|
+
"""Create the database file and schema if they don't exist, then migrate."""
|
|
272
|
+
if self._db_path != ":memory:":
|
|
273
|
+
os.makedirs(os.path.dirname(self._db_path), exist_ok=True)
|
|
274
|
+
with self._conn() as conn:
|
|
275
|
+
conn.executescript(_SCHEMA)
|
|
276
|
+
# Best-effort migrations for pre-existing databases.
|
|
277
|
+
for stmt in _MIGRATIONS:
|
|
278
|
+
try:
|
|
279
|
+
conn.execute(stmt)
|
|
280
|
+
except sqlite3.OperationalError:
|
|
281
|
+
pass # Column already exists — that's fine.
|
|
282
|
+
|
|
283
|
+
@contextmanager
|
|
284
|
+
def _conn(self) -> Generator[sqlite3.Connection, None, None]:
|
|
285
|
+
if self._mem_conn is not None:
|
|
286
|
+
# In-memory: reuse the persistent connection; caller manages commit
|
|
287
|
+
try:
|
|
288
|
+
yield self._mem_conn
|
|
289
|
+
self._mem_conn.commit()
|
|
290
|
+
except Exception:
|
|
291
|
+
self._mem_conn.rollback()
|
|
292
|
+
raise
|
|
293
|
+
return
|
|
294
|
+
conn = sqlite3.connect(self._db_path, check_same_thread=False)
|
|
295
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
296
|
+
conn.row_factory = sqlite3.Row
|
|
297
|
+
try:
|
|
298
|
+
yield conn
|
|
299
|
+
conn.commit()
|
|
300
|
+
except Exception:
|
|
301
|
+
conn.rollback()
|
|
302
|
+
raise
|
|
303
|
+
finally:
|
|
304
|
+
conn.close()
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def _row_to_correction(row: sqlite3.Row) -> UserCorrection:
|
|
308
|
+
ts_raw = row["timestamp"]
|
|
309
|
+
try:
|
|
310
|
+
ts = datetime.fromisoformat(ts_raw)
|
|
311
|
+
except ValueError:
|
|
312
|
+
ts = datetime.now(timezone.utc)
|
|
313
|
+
return UserCorrection(
|
|
314
|
+
correction_id=row["correction_id"],
|
|
315
|
+
timestamp=ts,
|
|
316
|
+
original_response=row["original_response"],
|
|
317
|
+
correction_text=row["correction_text"],
|
|
318
|
+
correction_type=row["correction_type"],
|
|
319
|
+
context_hash=row["context_hash"],
|
|
320
|
+
applied=bool(row["applied"]),
|
|
321
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# mind_model
|