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,521 @@
|
|
|
1
|
+
"""Preference Learner — capture and apply the owner's stated preferences.
|
|
2
|
+
|
|
3
|
+
This is the *explicit-directive* lane of preference learning: when the owner
|
|
4
|
+
says how they want their assistant to communicate ("be concise", "use bullet
|
|
5
|
+
points", "stop with the emoji"), it is captured deterministically at high
|
|
6
|
+
confidence and surfaced back every turn. It is deliberately distinct from the
|
|
7
|
+
EngagementStore, which passively *infers* a contact's OCEAN/style over many
|
|
8
|
+
observations — here we record what the owner directly told us.
|
|
9
|
+
|
|
10
|
+
Tracks:
|
|
11
|
+
- Communication style preferences (length / format / tone / emoji)
|
|
12
|
+
- Topic interests
|
|
13
|
+
- Scheduling preferences
|
|
14
|
+
|
|
15
|
+
Preferences persist to SQLite when a ``db_path`` is supplied, so an explicit
|
|
16
|
+
directive survives a sidecar restart instead of resetting to nothing.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import re
|
|
21
|
+
import sqlite3
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Keyword maps for feedback parsing
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
_LENGTH_SHORT = frozenset({"short", "brief", "concise", "terse", "quick", "compact", "succinct"})
|
|
35
|
+
_LENGTH_LONG = frozenset({"long", "detailed", "comprehensive", "verbose", "thorough", "elaborate", "in-depth"})
|
|
36
|
+
_LENGTH_MEDIUM = frozenset({"medium", "moderate", "balanced", "average"})
|
|
37
|
+
|
|
38
|
+
_FORMAT_BULLETS = frozenset({"bullet", "bullets", "list", "points", "numbered", "itemize"})
|
|
39
|
+
_FORMAT_PROSE = frozenset({"paragraph", "prose", "narrative", "flowing", "text"})
|
|
40
|
+
_FORMAT_CODE = frozenset({"code", "snippet", "example", "examples", "implementation"})
|
|
41
|
+
_FORMAT_TABLE = frozenset({"table", "grid", "tabular", "spreadsheet"})
|
|
42
|
+
|
|
43
|
+
_STYLE_FORMAL = frozenset({"formal", "professional", "business", "official", "academic"})
|
|
44
|
+
_STYLE_CASUAL = frozenset({"casual", "informal", "friendly", "conversational", "relaxed"})
|
|
45
|
+
_STYLE_TECHNICAL = frozenset({"technical", "expert", "advanced", "detailed", "deep"})
|
|
46
|
+
|
|
47
|
+
_TIMING_MORNING = frozenset({"morning", "early", "am", "dawn", "breakfast"})
|
|
48
|
+
_TIMING_EVENING = frozenset({"evening", "pm", "afternoon", "night", "after work"})
|
|
49
|
+
|
|
50
|
+
_EMOJI_WORDS = frozenset({"emoji", "emojis", "emoticon", "emoticons"})
|
|
51
|
+
_NEGATION = frozenset({"no", "not", "dont", "don", "stop", "without", "never", "drop", "avoid", "less", "skip"})
|
|
52
|
+
|
|
53
|
+
# Tokens that signal the owner is *directing* how to communicate (vs. merely
|
|
54
|
+
# using a style word in passing). A directive needs both a style keyword AND
|
|
55
|
+
# one of these cues, which keeps false positives off ordinary messages.
|
|
56
|
+
_DIRECTIVE_CUES = frozenset({
|
|
57
|
+
"be", "keep", "make", "use", "using", "give", "show", "stop", "do", "dont", "don",
|
|
58
|
+
"prefer", "want", "always", "never", "instead", "please", "less", "more", "shorter",
|
|
59
|
+
"longer", "reply", "replies", "respond", "response", "responses", "answer", "answers",
|
|
60
|
+
"you", "your", "from", "now", "on", "going", "forward",
|
|
61
|
+
})
|
|
62
|
+
_DIRECTIVE_MAX_TOKENS = 30 # directives are short; ignore long messages that merely contain a style word
|
|
63
|
+
|
|
64
|
+
# All style keywords that can anchor a directive.
|
|
65
|
+
_STYLE_KEYWORDS = (
|
|
66
|
+
_LENGTH_SHORT | _LENGTH_LONG | _LENGTH_MEDIUM
|
|
67
|
+
| _FORMAT_BULLETS | _FORMAT_PROSE | _FORMAT_CODE | _FORMAT_TABLE
|
|
68
|
+
| _STYLE_FORMAL | _STYLE_CASUAL | _STYLE_TECHNICAL
|
|
69
|
+
| _EMOJI_WORDS
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Action keyword → (category, key, value)
|
|
73
|
+
_ACTION_RULES: List[Tuple[Set[str], str, str, Any]] = [
|
|
74
|
+
({"short", "brief", "concise"}, "communication_style", "length", "short"),
|
|
75
|
+
({"long", "detailed", "verbose"}, "communication_style", "length", "long"),
|
|
76
|
+
({"dark"}, "display", "theme", "dark"),
|
|
77
|
+
({"light"}, "display", "theme", "light"),
|
|
78
|
+
({"bullet", "list"}, "communication_style", "format", "bullet_points"),
|
|
79
|
+
({"prose", "paragraph"}, "communication_style", "format", "prose"),
|
|
80
|
+
({"code", "snippet"}, "communication_style", "format", "code_examples"),
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@dataclass
|
|
85
|
+
class Preference:
|
|
86
|
+
"""A learned user preference.
|
|
87
|
+
|
|
88
|
+
Attributes:
|
|
89
|
+
category: Preference domain ("communication_style", "response_length", etc.)
|
|
90
|
+
key: Specific preference key within the category
|
|
91
|
+
value: The preference value
|
|
92
|
+
confidence: How confident we are (0-1)
|
|
93
|
+
learned_from: How the preference was learned ("explicit", "implicit", "inferred")
|
|
94
|
+
last_updated: Most recent update timestamp
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
category: str
|
|
98
|
+
key: str
|
|
99
|
+
value: Any
|
|
100
|
+
confidence: float
|
|
101
|
+
learned_from: str
|
|
102
|
+
last_updated: datetime = field(default_factory=datetime.now)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class PreferenceLearner:
|
|
106
|
+
"""Learn and apply user preferences.
|
|
107
|
+
|
|
108
|
+
Tracks preferences from two sources:
|
|
109
|
+
- Explicit feedback (high confidence, 0.9)
|
|
110
|
+
- Observed behavior (lower confidence, 0.5, accumulates with repetition)
|
|
111
|
+
|
|
112
|
+
Feedback parsing uses keyword matching to extract structured preferences
|
|
113
|
+
from natural-language feedback strings.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
graph_client: Optional Colony graph client (reserved for graph mirroring).
|
|
117
|
+
db_path: Optional SQLite path. When given, preferences are loaded on
|
|
118
|
+
construction and persisted on every update, so explicit owner
|
|
119
|
+
directives survive a restart.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, graph_client: Any = None, db_path: Optional[str] = None, *, perspective: Any = None) -> None:
|
|
123
|
+
self.graph = graph_client
|
|
124
|
+
self.perspective = perspective
|
|
125
|
+
self._preferences: Dict[str, Preference] = {}
|
|
126
|
+
self._conn: Optional[sqlite3.Connection] = None
|
|
127
|
+
if db_path:
|
|
128
|
+
try:
|
|
129
|
+
self._conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
|
130
|
+
self._conn.row_factory = sqlite3.Row
|
|
131
|
+
self._conn.execute("PRAGMA journal_mode=WAL")
|
|
132
|
+
self._conn.execute(
|
|
133
|
+
"""CREATE TABLE IF NOT EXISTS owner_preferences (
|
|
134
|
+
pref_key TEXT PRIMARY KEY,
|
|
135
|
+
category TEXT NOT NULL,
|
|
136
|
+
key TEXT NOT NULL,
|
|
137
|
+
value TEXT NOT NULL,
|
|
138
|
+
confidence REAL NOT NULL,
|
|
139
|
+
learned_from TEXT NOT NULL,
|
|
140
|
+
last_updated TEXT NOT NULL
|
|
141
|
+
)"""
|
|
142
|
+
)
|
|
143
|
+
self._conn.commit()
|
|
144
|
+
self._load()
|
|
145
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
146
|
+
logger.warning("PreferenceLearner persistence disabled (%s)", exc)
|
|
147
|
+
self._conn = None
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
# Persistence
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def _load(self) -> None:
|
|
154
|
+
if self._conn is None:
|
|
155
|
+
return
|
|
156
|
+
for row in self._conn.execute("SELECT * FROM owner_preferences"):
|
|
157
|
+
try:
|
|
158
|
+
value = json.loads(row["value"])
|
|
159
|
+
except (TypeError, ValueError):
|
|
160
|
+
value = row["value"]
|
|
161
|
+
self._preferences[row["pref_key"]] = Preference(
|
|
162
|
+
category=row["category"],
|
|
163
|
+
key=row["key"],
|
|
164
|
+
value=value,
|
|
165
|
+
confidence=row["confidence"],
|
|
166
|
+
learned_from=row["learned_from"],
|
|
167
|
+
last_updated=datetime.fromisoformat(row["last_updated"]),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _persist(self, pref_key: str, pref: "Preference") -> None:
|
|
171
|
+
if self._conn is None:
|
|
172
|
+
return
|
|
173
|
+
try:
|
|
174
|
+
self._conn.execute(
|
|
175
|
+
"""INSERT INTO owner_preferences
|
|
176
|
+
(pref_key, category, key, value, confidence, learned_from, last_updated)
|
|
177
|
+
VALUES (?,?,?,?,?,?,?)
|
|
178
|
+
ON CONFLICT(pref_key) DO UPDATE SET
|
|
179
|
+
category=excluded.category, key=excluded.key, value=excluded.value,
|
|
180
|
+
confidence=excluded.confidence, learned_from=excluded.learned_from,
|
|
181
|
+
last_updated=excluded.last_updated""",
|
|
182
|
+
(
|
|
183
|
+
pref_key, pref.category, pref.key, json.dumps(pref.value),
|
|
184
|
+
float(pref.confidence), pref.learned_from,
|
|
185
|
+
pref.last_updated.isoformat(),
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
self._conn.commit()
|
|
189
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
190
|
+
logger.debug("PreferenceLearner persist failed: %s", exc)
|
|
191
|
+
|
|
192
|
+
async def learn_from_feedback(
|
|
193
|
+
self,
|
|
194
|
+
category: str,
|
|
195
|
+
feedback: str,
|
|
196
|
+
context: Optional[Dict[str, Any]] = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Learn preference from explicit user feedback.
|
|
199
|
+
|
|
200
|
+
Explicit feedback gets high confidence (0.9) since the user
|
|
201
|
+
directly stated a preference. The raw feedback is parsed into
|
|
202
|
+
a (key, value) pair using keyword matching.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
category: Preference domain
|
|
206
|
+
feedback: The user's stated preference
|
|
207
|
+
context: Optional additional context
|
|
208
|
+
"""
|
|
209
|
+
key, value = self._parse_feedback(category, feedback)
|
|
210
|
+
|
|
211
|
+
pref_key = f"{category}.{key}"
|
|
212
|
+
self._preferences[pref_key] = Preference(
|
|
213
|
+
category=category,
|
|
214
|
+
key=key,
|
|
215
|
+
value=value,
|
|
216
|
+
confidence=0.9,
|
|
217
|
+
learned_from="explicit",
|
|
218
|
+
)
|
|
219
|
+
self._persist(pref_key, self._preferences[pref_key])
|
|
220
|
+
|
|
221
|
+
logger.debug("Learned explicit preference: %s = %s", pref_key, value)
|
|
222
|
+
|
|
223
|
+
async def learn_from_behavior(
|
|
224
|
+
self,
|
|
225
|
+
action: str,
|
|
226
|
+
context: Optional[Dict[str, Any]] = None,
|
|
227
|
+
) -> None:
|
|
228
|
+
"""Learn preference from observed user behavior.
|
|
229
|
+
|
|
230
|
+
Implicit preferences start at 0.5 confidence and accumulate
|
|
231
|
+
with repeated observation (capped at 1.0).
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
action: The observed user action (e.g., "clicked_short_response")
|
|
235
|
+
context: Optional additional context
|
|
236
|
+
"""
|
|
237
|
+
category, key, value = self._infer_from_action(action, context)
|
|
238
|
+
|
|
239
|
+
if not category:
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
pref_key = f"{category}.{key}"
|
|
243
|
+
existing = self._preferences.get(pref_key)
|
|
244
|
+
|
|
245
|
+
if existing and existing.learned_from == "explicit":
|
|
246
|
+
return # Observations cannot overwrite a correction from the owner.
|
|
247
|
+
if self.perspective is not None and pref_key in self.perspective.tracked_keys():
|
|
248
|
+
return
|
|
249
|
+
if existing:
|
|
250
|
+
existing.value = value
|
|
251
|
+
existing.confidence = min(1.0, existing.confidence + 0.1)
|
|
252
|
+
existing.learned_from = "implicit"
|
|
253
|
+
existing.last_updated = datetime.now()
|
|
254
|
+
else:
|
|
255
|
+
self._preferences[pref_key] = Preference(
|
|
256
|
+
category=category,
|
|
257
|
+
key=key,
|
|
258
|
+
value=value,
|
|
259
|
+
confidence=0.5,
|
|
260
|
+
learned_from="implicit",
|
|
261
|
+
)
|
|
262
|
+
self._persist(pref_key, self._preferences[pref_key])
|
|
263
|
+
|
|
264
|
+
logger.debug("Learned implicit preference: %s.%s = %s", category, key, value)
|
|
265
|
+
|
|
266
|
+
async def get_preference(
|
|
267
|
+
self,
|
|
268
|
+
category: str,
|
|
269
|
+
key: str,
|
|
270
|
+
default: Any = None,
|
|
271
|
+
) -> Any:
|
|
272
|
+
"""Get a learned preference value.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
category: Preference domain
|
|
276
|
+
key: Specific preference key
|
|
277
|
+
default: Value to return if preference not found
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
The preference value, or default if not found
|
|
281
|
+
"""
|
|
282
|
+
pref_key = f"{category}.{key}"
|
|
283
|
+
if self.perspective is not None:
|
|
284
|
+
for source in self.perspective.preferences():
|
|
285
|
+
if source['pref_key'] == pref_key:
|
|
286
|
+
return source['value']
|
|
287
|
+
if pref_key in self.perspective.tracked_keys():
|
|
288
|
+
return default
|
|
289
|
+
pref = self._preferences.get(pref_key)
|
|
290
|
+
return pref.value if pref else default
|
|
291
|
+
|
|
292
|
+
async def get_all_preferences(self, category: Optional[str] = None) -> List[Preference]:
|
|
293
|
+
"""Get all preferences, optionally filtered by category.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
category: If provided, only return preferences in this category
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
List of matching preferences
|
|
300
|
+
"""
|
|
301
|
+
tracked = self.perspective.tracked_keys() if self.perspective is not None else set()
|
|
302
|
+
prefs = [pref for key, pref in self._preferences.items() if key not in tracked]
|
|
303
|
+
if self.perspective is not None:
|
|
304
|
+
for source in self.perspective.preferences():
|
|
305
|
+
source_category, key = source['pref_key'].split('.', 1)
|
|
306
|
+
prefs.append(Preference(source_category, key, source['value'], 0.9,
|
|
307
|
+
'explicit_source', datetime.fromisoformat(source['source_at'])))
|
|
308
|
+
if category:
|
|
309
|
+
prefs = [p for p in prefs if p.category == category]
|
|
310
|
+
return prefs
|
|
311
|
+
|
|
312
|
+
def detect_directive(self, message: str) -> Optional[Tuple[str, str, Any]]:
|
|
313
|
+
"""Detect an explicit communication directive in a raw owner message.
|
|
314
|
+
|
|
315
|
+
Returns ``(category, key, value)`` only when the message both contains a
|
|
316
|
+
style keyword and reads like an instruction (a directive cue token, and
|
|
317
|
+
not an overly long message). Returns ``None`` otherwise, so ordinary
|
|
318
|
+
conversation never trips preference learning.
|
|
319
|
+
|
|
320
|
+
Examples that match::
|
|
321
|
+
|
|
322
|
+
"be more concise" -> ("communication_style", "length", "short")
|
|
323
|
+
"use bullet points please" -> ("communication_style", "format", "bullet_points")
|
|
324
|
+
"stop using emoji" -> ("communication_style", "emoji", "off")
|
|
325
|
+
"keep your replies formal" -> ("communication_style", "style", "formal")
|
|
326
|
+
"""
|
|
327
|
+
if not message:
|
|
328
|
+
return None
|
|
329
|
+
words = set(re.findall(r"\b\w+\b", message.lower()))
|
|
330
|
+
if len(words) > _DIRECTIVE_MAX_TOKENS:
|
|
331
|
+
return None
|
|
332
|
+
if not (words & _STYLE_KEYWORDS) or not (words & _DIRECTIVE_CUES):
|
|
333
|
+
return None
|
|
334
|
+
key, value = self._parse_feedback("communication_style", message)
|
|
335
|
+
if key == "general":
|
|
336
|
+
return None
|
|
337
|
+
return ("communication_style", key, value)
|
|
338
|
+
|
|
339
|
+
def _parse_all(self, message: str) -> List[Tuple[str, Any]]:
|
|
340
|
+
"""Extract every communication-style ``(key, value)`` present in *message*.
|
|
341
|
+
|
|
342
|
+
Unlike ``_parse_feedback`` (first match only), this captures multiple
|
|
343
|
+
directives in one sentence, e.g. "be concise and use bullet points".
|
|
344
|
+
Length wins ties over the technical tone bucket on shared words like
|
|
345
|
+
"detailed", so a word is never double-counted across dimensions.
|
|
346
|
+
"""
|
|
347
|
+
words = set(re.findall(r"\b\w+\b", message.lower()))
|
|
348
|
+
out: List[Tuple[str, Any]] = []
|
|
349
|
+
|
|
350
|
+
if words & _EMOJI_WORDS:
|
|
351
|
+
out.append(("emoji", "off" if (words & _NEGATION) else "on"))
|
|
352
|
+
|
|
353
|
+
if words & _LENGTH_SHORT:
|
|
354
|
+
out.append(("length", "short"))
|
|
355
|
+
elif words & _LENGTH_LONG:
|
|
356
|
+
out.append(("length", "long"))
|
|
357
|
+
elif words & _LENGTH_MEDIUM:
|
|
358
|
+
out.append(("length", "medium"))
|
|
359
|
+
|
|
360
|
+
if words & _FORMAT_BULLETS:
|
|
361
|
+
out.append(("format", "bullet_points"))
|
|
362
|
+
elif words & _FORMAT_PROSE:
|
|
363
|
+
out.append(("format", "prose"))
|
|
364
|
+
elif words & _FORMAT_CODE:
|
|
365
|
+
out.append(("format", "code_examples"))
|
|
366
|
+
elif words & _FORMAT_TABLE:
|
|
367
|
+
out.append(("format", "table"))
|
|
368
|
+
|
|
369
|
+
style_words = words - _LENGTH_LONG # don't let 'detailed' also claim 'technical'
|
|
370
|
+
if style_words & _STYLE_FORMAL:
|
|
371
|
+
out.append(("style", "formal"))
|
|
372
|
+
elif style_words & _STYLE_CASUAL:
|
|
373
|
+
out.append(("style", "casual"))
|
|
374
|
+
elif style_words & _STYLE_TECHNICAL:
|
|
375
|
+
out.append(("style", "technical"))
|
|
376
|
+
|
|
377
|
+
return out
|
|
378
|
+
|
|
379
|
+
async def learn_directive(self, message: str) -> Optional[Tuple[str, str, Any]]:
|
|
380
|
+
"""If *message* is an explicit communication directive, learn every style
|
|
381
|
+
preference it contains (explicit confidence) and return the primary
|
|
382
|
+
``(category, key, value)``; else ``None``.
|
|
383
|
+
|
|
384
|
+
The gate (``detect_directive``) keeps ordinary conversation from being
|
|
385
|
+
learned; once gated in, all directives in the message are captured.
|
|
386
|
+
"""
|
|
387
|
+
primary = self.detect_directive(message)
|
|
388
|
+
if primary is None:
|
|
389
|
+
return None
|
|
390
|
+
for key, value in self._parse_all(message):
|
|
391
|
+
pref_key = f"communication_style.{key}"
|
|
392
|
+
self._preferences[pref_key] = Preference(
|
|
393
|
+
category="communication_style", key=key, value=value,
|
|
394
|
+
confidence=0.9, learned_from="explicit",
|
|
395
|
+
)
|
|
396
|
+
self._persist(pref_key, self._preferences[pref_key])
|
|
397
|
+
logger.info("Learned owner directive(s) from: %r", message[:80])
|
|
398
|
+
return primary
|
|
399
|
+
|
|
400
|
+
def learn_source(self, turn_id: str) -> list:
|
|
401
|
+
"""Project attributed owner corrections from retained canonical words."""
|
|
402
|
+
return self.perspective.observe_source(turn_id, self) if self.perspective is not None else []
|
|
403
|
+
|
|
404
|
+
# Human-readable rendering of each communication-style preference.
|
|
405
|
+
_BRIEF_LINES: Dict[Tuple[str, Any], str] = {
|
|
406
|
+
("length", "short"): "Keep replies short and to the point.",
|
|
407
|
+
("length", "long"): "Give thorough, detailed replies.",
|
|
408
|
+
("length", "medium"): "Aim for medium-length replies.",
|
|
409
|
+
("format", "bullet_points"): "Prefer bullet points over prose.",
|
|
410
|
+
("format", "prose"): "Prefer flowing prose over bullet lists.",
|
|
411
|
+
("format", "code_examples"): "Include code examples when relevant.",
|
|
412
|
+
("format", "table"): "Use tables where they help.",
|
|
413
|
+
("style", "formal"): "Keep a formal, professional tone.",
|
|
414
|
+
("style", "casual"): "Keep a casual, relaxed tone.",
|
|
415
|
+
("style", "technical"): "Be technical and precise.",
|
|
416
|
+
("emoji", "off"): "Don't use emoji.",
|
|
417
|
+
("emoji", "on"): "Emoji are fine.",
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
def build_brief(self, min_confidence: float = 0.5, *, source_ids=None) -> str:
|
|
421
|
+
"""Render the owner's stated communication preferences as a short brief.
|
|
422
|
+
|
|
423
|
+
Only ``communication_style`` preferences at or above *min_confidence* are
|
|
424
|
+
included (explicit directives are 0.9). Returns '' when there is nothing
|
|
425
|
+
confident to assert.
|
|
426
|
+
"""
|
|
427
|
+
lines: List[str] = []
|
|
428
|
+
tracked = self.perspective.tracked_keys() if self.perspective is not None else set()
|
|
429
|
+
for pref_key, pref in self._preferences.items():
|
|
430
|
+
if pref_key in tracked:
|
|
431
|
+
continue
|
|
432
|
+
if pref.category != "communication_style" or pref.confidence < min_confidence:
|
|
433
|
+
continue
|
|
434
|
+
line = self._BRIEF_LINES.get((pref.key, pref.value))
|
|
435
|
+
if line and line not in lines:
|
|
436
|
+
lines.append(line)
|
|
437
|
+
if self.perspective is not None:
|
|
438
|
+
for pref in self.perspective.preferences():
|
|
439
|
+
if not pref['pref_key'].startswith('communication_style.'):
|
|
440
|
+
continue
|
|
441
|
+
line = self._BRIEF_LINES.get((pref['pref_key'].split('.', 1)[1], pref['value']))
|
|
442
|
+
if line:
|
|
443
|
+
lines.append(f"{line} [Owner correction {pref['id']}, source turn:{pref['source_turn_id']}]")
|
|
444
|
+
if source_ids is not None:
|
|
445
|
+
source_ids.append(pref['source_turn_id'])
|
|
446
|
+
return "\n".join(f"- {ln}" for ln in lines)
|
|
447
|
+
|
|
448
|
+
def _parse_feedback(self, category: str, feedback: str) -> Tuple[str, Any]:
|
|
449
|
+
"""Parse explicit feedback into a (key, value) preference pair.
|
|
450
|
+
|
|
451
|
+
Uses keyword matching to identify the most likely preference
|
|
452
|
+
being expressed. Falls back to storing the raw feedback under
|
|
453
|
+
the 'general' key.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
category: The preference category (used for context)
|
|
457
|
+
feedback: Natural-language feedback string
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Tuple of (preference_key, preference_value)
|
|
461
|
+
"""
|
|
462
|
+
words = set(re.findall(r"\b\w+\b", feedback.lower()))
|
|
463
|
+
|
|
464
|
+
if words & _EMOJI_WORDS:
|
|
465
|
+
return ("emoji", "off" if (words & _NEGATION) else "on")
|
|
466
|
+
|
|
467
|
+
if words & _LENGTH_SHORT:
|
|
468
|
+
return ("length", "short")
|
|
469
|
+
if words & _LENGTH_LONG:
|
|
470
|
+
return ("length", "long")
|
|
471
|
+
if words & _LENGTH_MEDIUM:
|
|
472
|
+
return ("length", "medium")
|
|
473
|
+
|
|
474
|
+
if words & _FORMAT_BULLETS:
|
|
475
|
+
return ("format", "bullet_points")
|
|
476
|
+
if words & _FORMAT_PROSE:
|
|
477
|
+
return ("format", "prose")
|
|
478
|
+
if words & _FORMAT_CODE:
|
|
479
|
+
return ("format", "code_examples")
|
|
480
|
+
if words & _FORMAT_TABLE:
|
|
481
|
+
return ("format", "table")
|
|
482
|
+
|
|
483
|
+
if words & _STYLE_FORMAL:
|
|
484
|
+
return ("style", "formal")
|
|
485
|
+
if words & _STYLE_CASUAL:
|
|
486
|
+
return ("style", "casual")
|
|
487
|
+
if words & _STYLE_TECHNICAL:
|
|
488
|
+
return ("style", "technical")
|
|
489
|
+
|
|
490
|
+
if words & _TIMING_MORNING:
|
|
491
|
+
return ("timing", "morning")
|
|
492
|
+
if words & _TIMING_EVENING:
|
|
493
|
+
return ("timing", "evening")
|
|
494
|
+
|
|
495
|
+
return ("general", feedback)
|
|
496
|
+
|
|
497
|
+
def _infer_from_action(
|
|
498
|
+
self,
|
|
499
|
+
action: str,
|
|
500
|
+
context: Optional[Dict[str, Any]],
|
|
501
|
+
) -> Tuple[Optional[str], Optional[str], Any]:
|
|
502
|
+
"""Infer preference from an observed action pattern.
|
|
503
|
+
|
|
504
|
+
Tokenises the action string on underscores/spaces and matches
|
|
505
|
+
against known keyword rules.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
action: Action identifier (e.g., "clicked_short_response")
|
|
509
|
+
context: Optional additional context
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Tuple of (category, key, value) or (None, None, None) if not recognised
|
|
513
|
+
"""
|
|
514
|
+
# Split on underscores and spaces (not \b which treats _ as word char)
|
|
515
|
+
parts = set(re.findall(r"[a-z]+", action.lower()))
|
|
516
|
+
|
|
517
|
+
for keywords, category, key, value in _ACTION_RULES:
|
|
518
|
+
if parts & keywords:
|
|
519
|
+
return (category, key, value)
|
|
520
|
+
|
|
521
|
+
return (None, None, None)
|