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,449 @@
|
|
|
1
|
+
"""ConversationSynthesisTask — periodically scan stored conversation memories
|
|
2
|
+
for implicit goals, commitments, and action items.
|
|
3
|
+
|
|
4
|
+
Architecture:
|
|
5
|
+
1. Query recent episodic Memory nodes from Neo4j
|
|
6
|
+
2. Parse turn summaries back into ConversationMessage objects
|
|
7
|
+
3. Run GoalInferencePipeline.process_history() on each batch
|
|
8
|
+
4. Feed candidates to GoalEngine for deduplication and creation
|
|
9
|
+
5. Persist last-processed watermark to avoid re-processing
|
|
10
|
+
|
|
11
|
+
This complements live goal inference (GoalEngine.on_message) by catching
|
|
12
|
+
items that were missed in real-time or that only become clear in retrospect
|
|
13
|
+
across multiple turns.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
from apsimo.goals.inference import (
|
|
27
|
+
ConversationMessage,
|
|
28
|
+
GoalDeduplicator,
|
|
29
|
+
GoalInferencePipeline,
|
|
30
|
+
InferenceCandidate,
|
|
31
|
+
)
|
|
32
|
+
from apsimo.goals.models import GoalPriority, GoalSource, GoalStatus
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── State persistence ───────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class SynthesisState:
|
|
41
|
+
"""Persistent watermark for the synthesis task."""
|
|
42
|
+
|
|
43
|
+
last_processed_at: Optional[str] = None # ISO-8601
|
|
44
|
+
memories_processed: int = 0
|
|
45
|
+
goals_created: int = 0
|
|
46
|
+
last_run_at: Optional[str] = None
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def load(cls, path: Path) -> "SynthesisState":
|
|
50
|
+
if path.exists():
|
|
51
|
+
try:
|
|
52
|
+
data = json.loads(path.read_text())
|
|
53
|
+
return cls(
|
|
54
|
+
last_processed_at=data.get("last_processed_at"),
|
|
55
|
+
memories_processed=data.get("memories_processed", 0),
|
|
56
|
+
goals_created=data.get("goals_created", 0),
|
|
57
|
+
last_run_at=data.get("last_run_at"),
|
|
58
|
+
)
|
|
59
|
+
except Exception:
|
|
60
|
+
logger.warning("Failed to load synthesis state from %s", path)
|
|
61
|
+
return cls()
|
|
62
|
+
|
|
63
|
+
def save(self, path: Path) -> None:
|
|
64
|
+
try:
|
|
65
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
path.write_text(json.dumps(self.__dict__, indent=2))
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logger.warning("Failed to save synthesis state: %s", exc)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── Memory parsing ────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
def _parse_turn_content(content: str) -> List[ConversationMessage]:
|
|
74
|
+
"""Parse a turn summary back into ConversationMessage objects.
|
|
75
|
+
|
|
76
|
+
WARNING: This is tightly coupled to the format produced by
|
|
77
|
+
``turns_sync`` in ``api/routers/host.py``. If that format changes
|
|
78
|
+
(e.g. prefix wording, line breaks), this parser will silently fall
|
|
79
|
+
back to treating the entire content as a single user message.
|
|
80
|
+
|
|
81
|
+
turns_sync currently stores combined summaries as:
|
|
82
|
+
User: {text}
|
|
83
|
+
Assistant: {text}
|
|
84
|
+
|
|
85
|
+
Returns a list of ConversationMessage objects (user first, then assistant).
|
|
86
|
+
"""
|
|
87
|
+
messages: List[ConversationMessage] = []
|
|
88
|
+
if not content:
|
|
89
|
+
return messages
|
|
90
|
+
|
|
91
|
+
# Try to split on "User:" and "Assistant:" prefixes
|
|
92
|
+
lines = content.split("\n")
|
|
93
|
+
current_role: Optional[str] = None
|
|
94
|
+
current_parts: List[str] = []
|
|
95
|
+
|
|
96
|
+
for line in lines:
|
|
97
|
+
stripped = line.strip()
|
|
98
|
+
if stripped.startswith("User:"):
|
|
99
|
+
# Flush previous
|
|
100
|
+
if current_role and current_parts:
|
|
101
|
+
messages.append(ConversationMessage(
|
|
102
|
+
message_id=f"turn_{len(messages)}",
|
|
103
|
+
role=current_role,
|
|
104
|
+
content=" ".join(current_parts).strip(),
|
|
105
|
+
))
|
|
106
|
+
current_role = "user"
|
|
107
|
+
current_parts = [stripped[5:].strip()]
|
|
108
|
+
elif stripped.startswith("Assistant:"):
|
|
109
|
+
if current_role and current_parts:
|
|
110
|
+
messages.append(ConversationMessage(
|
|
111
|
+
message_id=f"turn_{len(messages)}",
|
|
112
|
+
role=current_role,
|
|
113
|
+
content=" ".join(current_parts).strip(),
|
|
114
|
+
))
|
|
115
|
+
current_role = "assistant"
|
|
116
|
+
current_parts = [stripped[10:].strip()]
|
|
117
|
+
else:
|
|
118
|
+
if current_role:
|
|
119
|
+
current_parts.append(stripped)
|
|
120
|
+
|
|
121
|
+
# Flush final
|
|
122
|
+
if current_role and current_parts:
|
|
123
|
+
messages.append(ConversationMessage(
|
|
124
|
+
message_id=f"turn_{len(messages)}",
|
|
125
|
+
role=current_role,
|
|
126
|
+
content=" ".join(current_parts).strip(),
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
# Fallback: if no prefixes found, treat entire content as user message
|
|
130
|
+
if not messages and content.strip():
|
|
131
|
+
messages.append(ConversationMessage(
|
|
132
|
+
message_id="turn_0",
|
|
133
|
+
role="user",
|
|
134
|
+
content=content.strip(),
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
return messages
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# ── Main task ───────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
class ConversationSynthesisTask:
|
|
143
|
+
"""Scheduler task that scans conversation memories for implicit goals.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
registry: The AutonomyLoop's subsystem registry (provides graph, goals)
|
|
147
|
+
lookback_hours: How far back to look for memories (default 2.0)
|
|
148
|
+
min_confidence: Minimum inference confidence to create a goal (default 0.35)
|
|
149
|
+
max_memories_per_run: Safety cap on memories to process per tick (default 50)
|
|
150
|
+
max_goals_per_run: Safety cap on goals to create per tick (default 5)
|
|
151
|
+
state_file: Path to persist watermark state
|
|
152
|
+
telemetry: Optional TelemetryStore for health monitoring
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(
|
|
156
|
+
self,
|
|
157
|
+
registry: Any,
|
|
158
|
+
lookback_hours: float = 2.0,
|
|
159
|
+
min_confidence: float = 0.35,
|
|
160
|
+
max_memories_per_run: int = 50,
|
|
161
|
+
max_goals_per_run: int = 5,
|
|
162
|
+
state_file: Optional[Path] = None,
|
|
163
|
+
telemetry: Any = None,
|
|
164
|
+
) -> None:
|
|
165
|
+
self.registry = registry
|
|
166
|
+
self.lookback_hours = lookback_hours
|
|
167
|
+
self.min_confidence = min_confidence
|
|
168
|
+
self.max_memories_per_run = max_memories_per_run
|
|
169
|
+
self.max_goals_per_run = max_goals_per_run
|
|
170
|
+
self.state_file = state_file or (Path.home() / ".colony" / "data" / "autonomy_synthesis.json")
|
|
171
|
+
self.telemetry = telemetry
|
|
172
|
+
self.state = SynthesisState.load(self.state_file)
|
|
173
|
+
self.pipeline = GoalInferencePipeline(min_confidence=min_confidence)
|
|
174
|
+
self._deduplicator = GoalDeduplicator()
|
|
175
|
+
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
# Scheduler interface
|
|
178
|
+
# ------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
async def run(self) -> Dict[str, Any]:
|
|
181
|
+
"""Execute one synthesis pass. Called by the scheduler.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Dict with ``memories_scanned``, ``candidates_found``, ``goals_created``.
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
from apsimo.cognition.goal_spine import cognition_spine_exclusive
|
|
188
|
+
if cognition_spine_exclusive():
|
|
189
|
+
return {
|
|
190
|
+
"memories_scanned": 0,
|
|
191
|
+
"candidates_found": 0,
|
|
192
|
+
"goals_created": 0,
|
|
193
|
+
"status": "legacy_goal_writer_read_only",
|
|
194
|
+
}
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
start = time.monotonic()
|
|
198
|
+
graph = getattr(self.registry, "graph", None)
|
|
199
|
+
goals = getattr(self.registry, "goals", None)
|
|
200
|
+
|
|
201
|
+
if graph is None:
|
|
202
|
+
logger.warning("ConversationSynthesis: no graph available")
|
|
203
|
+
return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0, "error": "no_graph"}
|
|
204
|
+
|
|
205
|
+
if goals is None:
|
|
206
|
+
logger.warning("ConversationSynthesis: no goals engine available")
|
|
207
|
+
return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0, "error": "no_goals"}
|
|
208
|
+
|
|
209
|
+
# 1. Fetch recent episodic memories
|
|
210
|
+
memories = await self._fetch_recent_memories(graph)
|
|
211
|
+
if not memories:
|
|
212
|
+
logger.debug("ConversationSynthesis: no new memories in last %.1f hours", self.lookback_hours)
|
|
213
|
+
return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0}
|
|
214
|
+
|
|
215
|
+
# 2. Filter out already-processed memories (by parsed timestamp)
|
|
216
|
+
watermark = self.state.last_processed_at
|
|
217
|
+
watermark_dt = self._parse_dt(watermark) if watermark else None
|
|
218
|
+
new_memories = []
|
|
219
|
+
for mem in memories:
|
|
220
|
+
mem_time = mem.get("created_at")
|
|
221
|
+
mem_dt = self._parse_dt(mem_time) if mem_time else None
|
|
222
|
+
if watermark_dt and mem_dt and mem_dt <= watermark_dt:
|
|
223
|
+
continue
|
|
224
|
+
new_memories.append(mem)
|
|
225
|
+
|
|
226
|
+
if not new_memories:
|
|
227
|
+
logger.debug("ConversationSynthesis: all %d memories already processed", len(memories))
|
|
228
|
+
return {"memories_scanned": 0, "candidates_found": 0, "goals_created": 0}
|
|
229
|
+
|
|
230
|
+
# Safety cap
|
|
231
|
+
capped = False
|
|
232
|
+
if len(new_memories) > self.max_memories_per_run:
|
|
233
|
+
logger.info(
|
|
234
|
+
"ConversationSynthesis: capping %d memories to %d",
|
|
235
|
+
len(new_memories),
|
|
236
|
+
self.max_memories_per_run,
|
|
237
|
+
)
|
|
238
|
+
new_memories = new_memories[: self.max_memories_per_run]
|
|
239
|
+
capped = True
|
|
240
|
+
|
|
241
|
+
# 3. Process each memory
|
|
242
|
+
all_candidates: List[InferenceCandidate] = []
|
|
243
|
+
for mem in new_memories:
|
|
244
|
+
content = mem.get("content", "")
|
|
245
|
+
messages = _parse_turn_content(content)
|
|
246
|
+
if not messages:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
# Run inference pipeline on parsed messages.
|
|
250
|
+
# process_message already returns None for non-user roles,
|
|
251
|
+
# so candidates only come from user messages.
|
|
252
|
+
candidates = self.pipeline.process_history(messages)
|
|
253
|
+
all_candidates.extend(candidates)
|
|
254
|
+
|
|
255
|
+
# 4. Deduplicate candidates against each other (same tick)
|
|
256
|
+
seen_titles: set[str] = set()
|
|
257
|
+
unique_candidates: List[InferenceCandidate] = []
|
|
258
|
+
for cand in all_candidates:
|
|
259
|
+
normalized = cand.title.lower().strip()
|
|
260
|
+
if normalized not in seen_titles:
|
|
261
|
+
seen_titles.add(normalized)
|
|
262
|
+
unique_candidates.append(cand)
|
|
263
|
+
|
|
264
|
+
# 5. Load existing non-terminal goals for cross-run deduplication
|
|
265
|
+
existing_goals: List[Any] = []
|
|
266
|
+
try:
|
|
267
|
+
proposed = goals.list_goals(status=GoalStatus.PROPOSED, limit=100)
|
|
268
|
+
active = goals.list_goals(status=GoalStatus.ACTIVE, limit=100)
|
|
269
|
+
existing_goals = proposed + active
|
|
270
|
+
except Exception as exc:
|
|
271
|
+
logger.warning("ConversationSynthesis: failed to load existing goals: %s", exc)
|
|
272
|
+
|
|
273
|
+
# 6. Create goals via GoalEngine (with dedup, rate limit, and merge)
|
|
274
|
+
goals_created = 0
|
|
275
|
+
for cand in unique_candidates:
|
|
276
|
+
# Stop if we've hit the per-run cap
|
|
277
|
+
if goals_created >= self.max_goals_per_run:
|
|
278
|
+
logger.info(
|
|
279
|
+
"ConversationSynthesis: hit max_goals_per_run (%d), skipping remaining candidates",
|
|
280
|
+
self.max_goals_per_run,
|
|
281
|
+
)
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
# Check against existing goals
|
|
285
|
+
if existing_goals:
|
|
286
|
+
similarity = self._deduplicator.check(cand, existing_goals)
|
|
287
|
+
if similarity:
|
|
288
|
+
if similarity.recommendation == "duplicate":
|
|
289
|
+
logger.debug(
|
|
290
|
+
"ConversationSynthesis: skipping duplicate '%s' (sim=%.2f, goal=%s)",
|
|
291
|
+
cand.title,
|
|
292
|
+
similarity.similarity_score,
|
|
293
|
+
similarity.goal_id_b,
|
|
294
|
+
)
|
|
295
|
+
continue
|
|
296
|
+
elif similarity.recommendation == "merge":
|
|
297
|
+
# Find the existing goal, merge context, and persist
|
|
298
|
+
for goal in existing_goals:
|
|
299
|
+
if goal.goal_id == similarity.goal_id_b:
|
|
300
|
+
self._deduplicator.merge(goal, cand)
|
|
301
|
+
# Persist merged changes back to the store
|
|
302
|
+
try:
|
|
303
|
+
goals._store.save_goal(goal)
|
|
304
|
+
except Exception as save_exc:
|
|
305
|
+
logger.warning(
|
|
306
|
+
"ConversationSynthesis: failed to persist merge for goal %s: %s",
|
|
307
|
+
goal.goal_id,
|
|
308
|
+
save_exc,
|
|
309
|
+
)
|
|
310
|
+
logger.info(
|
|
311
|
+
"ConversationSynthesis: merged '%s' into existing goal %s (sim=%.2f)",
|
|
312
|
+
cand.title,
|
|
313
|
+
goal.goal_id,
|
|
314
|
+
similarity.similarity_score,
|
|
315
|
+
)
|
|
316
|
+
break
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Root-cause guard: conversation memory content can carry raw
|
|
320
|
+
# messaging/skill markup (e.g. <<RCSCTX ...>> injected by the RCS
|
|
321
|
+
# bridge, or [IMPORTANT: ...skill...] system turns). Do not mint
|
|
322
|
+
# goals from system/skill-origin turns, and store only clean titles
|
|
323
|
+
# so downstream follow-ups are meaningful rather than junk.
|
|
324
|
+
from apsimo.delivery.reachout_policy import (
|
|
325
|
+
is_system_origin, sanitize_text,
|
|
326
|
+
)
|
|
327
|
+
if is_system_origin(cand.title) or is_system_origin(cand.description):
|
|
328
|
+
logger.debug(
|
|
329
|
+
"ConversationSynthesis: skipping system/skill-origin candidate %r",
|
|
330
|
+
str(cand.title)[:80],
|
|
331
|
+
)
|
|
332
|
+
continue
|
|
333
|
+
clean_title = sanitize_text(cand.title)
|
|
334
|
+
if not clean_title:
|
|
335
|
+
logger.debug("ConversationSynthesis: skipping empty-after-sanitise candidate")
|
|
336
|
+
continue
|
|
337
|
+
clean_description = sanitize_text(cand.description) or clean_title
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
goal = goals.propose_goal(
|
|
341
|
+
title=clean_title,
|
|
342
|
+
description=clean_description,
|
|
343
|
+
priority=cand.priority,
|
|
344
|
+
source=GoalSource.INFERRED,
|
|
345
|
+
context={
|
|
346
|
+
"inferred_confidence": cand.confidence,
|
|
347
|
+
"inferred_signals": [s.value for s in cand.signals],
|
|
348
|
+
"source_messages": cand.source_messages,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
if goal is not None:
|
|
352
|
+
goals_created += 1
|
|
353
|
+
existing_goals.append(goal) # prevent duplicates within same run
|
|
354
|
+
logger.info(
|
|
355
|
+
"ConversationSynthesis: created goal '%s' (confidence=%.2f, signals=%s)",
|
|
356
|
+
cand.title,
|
|
357
|
+
cand.confidence,
|
|
358
|
+
[s.value for s in cand.signals],
|
|
359
|
+
)
|
|
360
|
+
except Exception as exc:
|
|
361
|
+
logger.warning("ConversationSynthesis: failed to create goal '%s': %s", cand.title, exc)
|
|
362
|
+
|
|
363
|
+
# 7. Update watermark and state
|
|
364
|
+
newest_time = max(
|
|
365
|
+
(m.get("created_at") for m in new_memories if m.get("created_at")),
|
|
366
|
+
default=None,
|
|
367
|
+
)
|
|
368
|
+
if newest_time and not capped:
|
|
369
|
+
self.state.last_processed_at = newest_time
|
|
370
|
+
self.state.memories_processed += len(new_memories)
|
|
371
|
+
self.state.goals_created += goals_created
|
|
372
|
+
self.state.last_run_at = datetime.now(timezone.utc).isoformat()
|
|
373
|
+
self.state.save(self.state_file)
|
|
374
|
+
|
|
375
|
+
# 8. Telemetry
|
|
376
|
+
if self.telemetry is not None and hasattr(self.telemetry, "touch"):
|
|
377
|
+
try:
|
|
378
|
+
await self.telemetry.touch("last_synthesis_at")
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
|
|
382
|
+
duration_ms = (time.monotonic() - start) * 1000
|
|
383
|
+
logger.info(
|
|
384
|
+
"ConversationSynthesis: scanned=%d candidates=%d goals_created=%d (%.1fms)",
|
|
385
|
+
len(new_memories),
|
|
386
|
+
len(unique_candidates),
|
|
387
|
+
goals_created,
|
|
388
|
+
duration_ms,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
"memories_scanned": len(new_memories),
|
|
393
|
+
"candidates_found": len(unique_candidates),
|
|
394
|
+
"goals_created": goals_created,
|
|
395
|
+
"duration_ms": round(duration_ms, 1),
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
# Private helpers
|
|
400
|
+
# ------------------------------------------------------------------
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def _parse_dt(iso_str: str) -> Optional[datetime]:
|
|
404
|
+
"""Parse an ISO-8601 string to a timezone-aware datetime.
|
|
405
|
+
|
|
406
|
+
Handles both 'Z' suffix and '+00:00' offset.
|
|
407
|
+
"""
|
|
408
|
+
if not iso_str:
|
|
409
|
+
return None
|
|
410
|
+
try:
|
|
411
|
+
normalized = iso_str.replace("Z", "+00:00")
|
|
412
|
+
return datetime.fromisoformat(normalized)
|
|
413
|
+
except (ValueError, TypeError):
|
|
414
|
+
logger.debug("ConversationSynthesis: failed to parse datetime '%s'", iso_str)
|
|
415
|
+
return None
|
|
416
|
+
|
|
417
|
+
async def _fetch_recent_memories(
|
|
418
|
+
self,
|
|
419
|
+
graph: Any,
|
|
420
|
+
) -> List[Dict[str, Any]]:
|
|
421
|
+
"""Return recent episodic Memory nodes from Neo4j."""
|
|
422
|
+
query = (
|
|
423
|
+
"MATCH (m:Memory) "
|
|
424
|
+
"WHERE m.type = 'episodic' "
|
|
425
|
+
" AND m.created_at >= datetime() - duration({hours: $hours}) "
|
|
426
|
+
"RETURN m.id AS id, m.content AS content, "
|
|
427
|
+
" toString(m.created_at) AS created_at, m.strength AS strength "
|
|
428
|
+
"ORDER BY m.created_at DESC "
|
|
429
|
+
"LIMIT $limit"
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Support both ColonyGraph (driver.session) and mock with execute()
|
|
433
|
+
if hasattr(graph, "driver"):
|
|
434
|
+
async with graph.driver.session(database=graph.database) as session:
|
|
435
|
+
result = await session.run(
|
|
436
|
+
query,
|
|
437
|
+
hours=self.lookback_hours,
|
|
438
|
+
limit=self.max_memories_per_run * 2, # fetch a bit more for filtering
|
|
439
|
+
)
|
|
440
|
+
rows = [dict(record) async for record in result]
|
|
441
|
+
return rows if rows else []
|
|
442
|
+
|
|
443
|
+
# Mock / test path
|
|
444
|
+
if hasattr(graph, "execute"):
|
|
445
|
+
rows = await graph.execute(query, hours=self.lookback_hours, limit=self.max_memories_per_run * 2)
|
|
446
|
+
return rows if rows else []
|
|
447
|
+
|
|
448
|
+
logger.warning("ConversationSynthesis: graph has no driver or execute method")
|
|
449
|
+
return []
|