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,196 @@
|
|
|
1
|
+
"""Optional local heartbeat reports, separate from canonical executions."""
|
|
2
|
+
import json
|
|
3
|
+
import hashlib
|
|
4
|
+
import math
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _text(value, maximum=128):
|
|
11
|
+
return (isinstance(value, str) and 0 < len(value) <= maximum
|
|
12
|
+
and not any(ord(char) < 32 for char in value))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _number(value):
|
|
16
|
+
try:
|
|
17
|
+
return type(value) in (int, float) and math.isfinite(value) and value >= 0
|
|
18
|
+
except OverflowError:
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def work_snapshot(value, *, now=None):
|
|
23
|
+
"""Bounded metadata from an explicitly enrolled producer, never authority.
|
|
24
|
+
|
|
25
|
+
Producers may attach this snapshot to an existing process heartbeat. The
|
|
26
|
+
public reader knows no private ledger schema, recipients or machine paths.
|
|
27
|
+
"""
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
if not isinstance(value, dict) or value.get('schema') != 'ColonyWorkSnapshotV1':
|
|
31
|
+
return {'schema': 'ColonyWorkSnapshotV1', 'available': False, 'reason': 'unsupported_work_snapshot', 'complete': False}
|
|
32
|
+
if type(value.get('available')) is not bool or not _number(value.get('observed_at')):
|
|
33
|
+
return {'schema': 'ColonyWorkSnapshotV1', 'available': False, 'reason': 'invalid_work_snapshot', 'complete': False}
|
|
34
|
+
now = time.time() if now is None else now
|
|
35
|
+
age = now - value['observed_at']
|
|
36
|
+
result = {'schema': 'ColonyWorkSnapshotV1', 'available': value['available'], 'complete': False, 'observed_at': value['observed_at'],
|
|
37
|
+
'freshness': 'recent' if 0 <= age <= 120 else 'stale' if age > 120 else 'unknown',
|
|
38
|
+
'items': [], 'truncated': bool(value.get('truncated')), 'partial': bool(value.get('partial'))}
|
|
39
|
+
for key in ('total', 'pending_total', 'terminal_total', 'unmatched_provider_total'):
|
|
40
|
+
if type(value.get(key)) is int and value[key] >= 0:
|
|
41
|
+
result[key] = value[key]
|
|
42
|
+
for key in ('reason', 'coverage'):
|
|
43
|
+
if _text(value.get(key), 200):
|
|
44
|
+
result[key] = value[key]
|
|
45
|
+
statuses = value.get('source_status')
|
|
46
|
+
if isinstance(statuses, dict):
|
|
47
|
+
result['source_status'] = {key: state for key, state in list(statuses.items())[:8]
|
|
48
|
+
if _text(key, 32) and state in {'observed', 'unavailable', 'partial', 'not_observed'}}
|
|
49
|
+
states = value.get('state_counts')
|
|
50
|
+
if isinstance(states, dict):
|
|
51
|
+
result['state_counts'] = {key: n for key, n in list(states.items())[:12]
|
|
52
|
+
if _text(key, 48) and type(n) is int and n >= 0}
|
|
53
|
+
rows = value.get('items', [])
|
|
54
|
+
if not isinstance(rows, list):
|
|
55
|
+
return {'schema': 'ColonyWorkSnapshotV1', 'available': False, 'reason': 'invalid_work_snapshot', 'complete': False}
|
|
56
|
+
result['truncated'] |= len(rows) > 4
|
|
57
|
+
for row in rows[:4]:
|
|
58
|
+
if not isinstance(row, dict):
|
|
59
|
+
result['partial'] = True
|
|
60
|
+
continue
|
|
61
|
+
item = {'liveness': 'record_only'}
|
|
62
|
+
for key in ('delivery_id', 'intent_id', 'request_key', 'attempt_id', 'state', 'record_kind',
|
|
63
|
+
'provider_state', 'observation_state'):
|
|
64
|
+
if _text(row.get(key), 256 if key.endswith('_id') or key == 'request_key' else 64):
|
|
65
|
+
item[key] = row[key]
|
|
66
|
+
if not item.get('delivery_id') or not item.get('state'):
|
|
67
|
+
result['partial'] = True
|
|
68
|
+
continue
|
|
69
|
+
for key in ('updated_at', 'provider_updated_at'):
|
|
70
|
+
if _number(row.get(key)):
|
|
71
|
+
item[key] = row[key]
|
|
72
|
+
if type(row.get('transport_observation_pending')) is bool:
|
|
73
|
+
item['transport_observation_pending'] = row['transport_observation_pending']
|
|
74
|
+
digest = row.get('evidence_sha256')
|
|
75
|
+
if isinstance(digest, str) and len(digest) == 64 and all(c in '0123456789abcdef' for c in digest):
|
|
76
|
+
item['evidence_sha256'] = digest
|
|
77
|
+
result['items'].append(item)
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def work_details(value, *, now=None):
|
|
82
|
+
"""Project declared work metadata; never follow references or verify effects.
|
|
83
|
+
|
|
84
|
+
These optional fields extend existing heartbeat producers. Bad optional
|
|
85
|
+
values do not hide the underlying legacy state or make a task terminal.
|
|
86
|
+
The same projection is used for automatic request context.
|
|
87
|
+
"""
|
|
88
|
+
result = {}
|
|
89
|
+
snapshot = work_snapshot(value.get('work_snapshot'), now=now)
|
|
90
|
+
if snapshot is not None:
|
|
91
|
+
result['work_snapshot'] = snapshot
|
|
92
|
+
for key in ('task_id', 'parent_task_id', 'worker_id', 'kind'):
|
|
93
|
+
if _text(value.get(key)):
|
|
94
|
+
result[key] = value[key]
|
|
95
|
+
for key in ('started_at', 'finished_at'):
|
|
96
|
+
if _number(value.get(key)):
|
|
97
|
+
result[key] = value[key]
|
|
98
|
+
if type(value.get('exit_code')) is int and -(2**31) <= value['exit_code'] < 2**31:
|
|
99
|
+
result['exit_code'] = value['exit_code']
|
|
100
|
+
progress = value.get('progress')
|
|
101
|
+
if isinstance(progress, list):
|
|
102
|
+
selected = []
|
|
103
|
+
for row in progress[:4]:
|
|
104
|
+
if (not isinstance(row, dict) or not _number(row.get('completed'))
|
|
105
|
+
or not _number(row.get('observed_at')) or not _text(row.get('unit'), 32)
|
|
106
|
+
or not _text(row.get('source'), 128)):
|
|
107
|
+
continue
|
|
108
|
+
item = {key: row[key] for key in ('completed', 'unit', 'source', 'observed_at')}
|
|
109
|
+
if 'total' in row:
|
|
110
|
+
if not _number(row['total']) or row['total'] < row['completed']:
|
|
111
|
+
continue
|
|
112
|
+
item['total'] = row['total']
|
|
113
|
+
selected.append(item)
|
|
114
|
+
if selected:
|
|
115
|
+
result['progress'] = selected
|
|
116
|
+
references = value.get('result_refs')
|
|
117
|
+
if isinstance(references, list):
|
|
118
|
+
selected = []
|
|
119
|
+
for row in references[:4]:
|
|
120
|
+
if (not isinstance(row, dict) or not _text(row.get('kind'), 64)
|
|
121
|
+
or not _text(row.get('reference'), 512)):
|
|
122
|
+
continue
|
|
123
|
+
item = {key: row[key] for key in ('kind', 'reference')}
|
|
124
|
+
digest = row.get('sha256')
|
|
125
|
+
if isinstance(digest, str) and len(digest) == 64 and all(c in '0123456789abcdef' for c in digest):
|
|
126
|
+
item['sha256'] = digest
|
|
127
|
+
if _number(row.get('observed_at')):
|
|
128
|
+
item['observed_at'] = row['observed_at']
|
|
129
|
+
# The producer describes its check; the reader does not endorse it.
|
|
130
|
+
if _text(row.get('verification'), 160):
|
|
131
|
+
item['verification'] = row['verification']
|
|
132
|
+
selected.append(item)
|
|
133
|
+
if selected:
|
|
134
|
+
result['result_refs'] = selected
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def reported_worker_view(*, limit=8, now=None):
|
|
139
|
+
configured = os.environ.get('COLONY_WORKER_STATUS_PATHS', '').strip()
|
|
140
|
+
if not configured:
|
|
141
|
+
return None
|
|
142
|
+
view = {'source':'configured_local_heartbeats', 'available':False,
|
|
143
|
+
'items':[], 'complete':False,
|
|
144
|
+
'coverage':'reported worker state only; no execution or effect verification'}
|
|
145
|
+
try:
|
|
146
|
+
paths = json.loads(configured)
|
|
147
|
+
if (len(configured)>8192 or not isinstance(paths,dict)
|
|
148
|
+
or not all(isinstance(label,str) and label and isinstance(path,str) and path
|
|
149
|
+
for label,path in paths.items())):
|
|
150
|
+
raise ValueError('Invalid heartbeat mapping')
|
|
151
|
+
except (ValueError,TypeError):
|
|
152
|
+
return {**view,'reason':'invalid_status_configuration'}
|
|
153
|
+
now = time.time() if now is None else now
|
|
154
|
+
bounded = max(1,min(int(limit),8))
|
|
155
|
+
items = []
|
|
156
|
+
for label,filename in list(paths.items())[:bounded]:
|
|
157
|
+
item = {'label':label[:128], 'available':False, 'liveness':'unverified'}
|
|
158
|
+
try:
|
|
159
|
+
path = Path(filename).expanduser()
|
|
160
|
+
if not path.is_file():
|
|
161
|
+
raise ValueError('Heartbeat unavailable')
|
|
162
|
+
with path.open('rb') as stream:
|
|
163
|
+
raw = stream.read(16385)
|
|
164
|
+
value = json.loads(raw)
|
|
165
|
+
if len(raw)>16384 or not isinstance(value,dict):
|
|
166
|
+
raise ValueError('Invalid heartbeat')
|
|
167
|
+
updated = value.get('updated_at')
|
|
168
|
+
state = value.get('state')
|
|
169
|
+
if isinstance(state, dict) and 'work_snapshot' in state:
|
|
170
|
+
value = {**value, 'work_snapshot': state['work_snapshot']}
|
|
171
|
+
state = 'reported_ready' if value.get('ready') is True else 'reported_unready'
|
|
172
|
+
if (not isinstance(state,str) or not state or len(state)>64
|
|
173
|
+
or isinstance(updated,bool) or not isinstance(updated,(int,float))
|
|
174
|
+
or not math.isfinite(updated)):
|
|
175
|
+
raise ValueError('Invalid heartbeat fields')
|
|
176
|
+
age = now-updated
|
|
177
|
+
item.update(available=True,state=state,updated_at=updated,
|
|
178
|
+
age_seconds=round(age,1) if age>=0 else None,
|
|
179
|
+
freshness='recent' if 0<=age<=120 else 'stale' if age>120 else 'unknown',
|
|
180
|
+
status_sha256=hashlib.sha256(raw).hexdigest())
|
|
181
|
+
for key in ('detail_code','release_commit'):
|
|
182
|
+
if isinstance(value.get(key),str):
|
|
183
|
+
item[key] = value[key][:128]
|
|
184
|
+
item.update(work_details(value, now=now))
|
|
185
|
+
if 'task_id' in item:
|
|
186
|
+
finished = item.get('finished_at')
|
|
187
|
+
terminal = (finished is not None and finished <= updated
|
|
188
|
+
and finished >= item.get('started_at', 0)
|
|
189
|
+
and state in {'exited', 'completed', 'failed', 'interrupted', 'cancelled'})
|
|
190
|
+
item['record_kind'] = 'terminal_report' if terminal else 'progress_report'
|
|
191
|
+
except (OSError,ValueError,TypeError):
|
|
192
|
+
item['reason'] = 'heartbeat_unavailable'
|
|
193
|
+
items.append(item)
|
|
194
|
+
partial = any(not item.get('available') or item.get('work_snapshot', {}).get('available') is False
|
|
195
|
+
or item.get('work_snapshot', {}).get('partial') for item in items)
|
|
196
|
+
return {**view,'available':True,'items':items,'truncated':len(paths)>bounded, 'partial': partial}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Attributed source corrections in the existing source ledger and recall packet."""
|
|
2
|
+
from contextlib import closing
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import sqlite3
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def initialize(conn):
|
|
11
|
+
# No correction text is duplicated here. These references survive erasure
|
|
12
|
+
# so deleting an annotation cannot silently revive its unqualified source.
|
|
13
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS source_annotations (
|
|
14
|
+
annotation_source_id TEXT PRIMARY KEY, target_source_id TEXT NOT NULL,
|
|
15
|
+
target_version TEXT NOT NULL, target_message_hashes_json TEXT NOT NULL,
|
|
16
|
+
request_sha256 TEXT NOT NULL)''')
|
|
17
|
+
conn.execute('CREATE INDEX IF NOT EXISTS source_annotation_target ON source_annotations(target_source_id)')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def append(ledger, *, contact_id, session_id, annotation_id, source_id, source_version,
|
|
21
|
+
excerpt, correction, author_principal):
|
|
22
|
+
from .idempotency import canonical_turn_digest, source_message_hash, SourceErased
|
|
23
|
+
from .audio import source_text
|
|
24
|
+
for value, maximum in ((contact_id, 256), (session_id, 256), (annotation_id, 128),
|
|
25
|
+
(source_id, 256), (excerpt, 4096), (correction, 4096),
|
|
26
|
+
(author_principal, 256)):
|
|
27
|
+
if not isinstance(value, str) or not value.strip() or len(value) > maximum:
|
|
28
|
+
raise ValueError('invalid_source_annotation')
|
|
29
|
+
if not isinstance(source_version, str) or not re.fullmatch('[0-9a-f]{64}', source_version):
|
|
30
|
+
raise ValueError('invalid_source_annotation')
|
|
31
|
+
identity = [contact_id, author_principal, annotation_id]
|
|
32
|
+
annotation_source = 'source-annotation:' + canonical_turn_digest(identity)
|
|
33
|
+
target = {'source_id': source_id, 'source_version': source_version}
|
|
34
|
+
request_digest = canonical_turn_digest([identity, session_id, target, excerpt, correction])
|
|
35
|
+
with closing(ledger._connect()) as conn, conn:
|
|
36
|
+
conn.execute('BEGIN IMMEDIATE')
|
|
37
|
+
prior = conn.execute('SELECT * FROM source_annotations WHERE annotation_source_id=?',
|
|
38
|
+
(annotation_source,)).fetchone()
|
|
39
|
+
if prior and prior['request_sha256'] != request_digest:
|
|
40
|
+
raise ValueError('annotation_id_conflict')
|
|
41
|
+
if conn.execute('SELECT 1 FROM source_erasures WHERE turn_id=?', (annotation_source,)).fetchone():
|
|
42
|
+
raise SourceErased('source_erased')
|
|
43
|
+
row = conn.execute('''SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=?
|
|
44
|
+
AND (scope='person' OR session_id=?)''', (source_id, contact_id, session_id)).fetchone()
|
|
45
|
+
if row is None:
|
|
46
|
+
raise ValueError('source_not_found')
|
|
47
|
+
messages = json.loads(row['messages_json'])
|
|
48
|
+
if canonical_turn_digest(messages) != source_version:
|
|
49
|
+
raise ValueError('source_version_mismatch')
|
|
50
|
+
matched = [source_message_hash(row['session_id'], m) for m in messages
|
|
51
|
+
if excerpt in source_text(m.get('content'))]
|
|
52
|
+
if not matched:
|
|
53
|
+
raise ValueError('source_excerpt_mismatch')
|
|
54
|
+
if prior:
|
|
55
|
+
stored = conn.execute('SELECT messages_json FROM turn_sources WHERE turn_id=?',
|
|
56
|
+
(annotation_source,)).fetchone()
|
|
57
|
+
if stored is None:
|
|
58
|
+
raise SourceErased('source_erased')
|
|
59
|
+
annotation_messages = json.loads(stored['messages_json'])
|
|
60
|
+
else:
|
|
61
|
+
at = datetime.now(timezone.utc).isoformat()
|
|
62
|
+
content = {'target': target, 'excerpt': excerpt, 'correction': correction,
|
|
63
|
+
'author_principal': author_principal, 'recorded_at': at,
|
|
64
|
+
'state': 'attributed_correction'}
|
|
65
|
+
annotation_messages = [{'role': 'assistant', 'content': json.dumps(content, ensure_ascii=False),
|
|
66
|
+
'_supplied_sources': [target]}]
|
|
67
|
+
# The dedicated method owns authorship, source and relation in one
|
|
68
|
+
# transaction. Ordinary turn metadata cannot create a correction.
|
|
69
|
+
ledger._validate_dependencies(conn, annotation_source, contact_id, session_id, annotation_messages)
|
|
70
|
+
digest = canonical_turn_digest({'messages': annotation_messages, 'contact_id': contact_id,
|
|
71
|
+
'session_id': session_id, 'scope': row['scope'], 'occurred_at': at})
|
|
72
|
+
try:
|
|
73
|
+
conn.execute('''INSERT INTO turn_sources
|
|
74
|
+
(turn_id,content_sha256,contact_id,session_id,scope,messages_json,occurred_at)
|
|
75
|
+
VALUES (?,?,?,?,?,?,?)''', (annotation_source, digest, contact_id, session_id,
|
|
76
|
+
row['scope'], json.dumps(annotation_messages, ensure_ascii=True, sort_keys=True,
|
|
77
|
+
separators=(',', ':')), at))
|
|
78
|
+
except sqlite3.IntegrityError as exc:
|
|
79
|
+
raise ValueError('annotation_id_conflict') from exc
|
|
80
|
+
ledger._index_messages(conn, annotation_source, annotation_messages)
|
|
81
|
+
from .source_vectors import enqueue
|
|
82
|
+
enqueue(conn, annotation_source)
|
|
83
|
+
conn.execute('INSERT INTO source_annotations VALUES (?,?,?,?,?)',
|
|
84
|
+
(annotation_source, source_id, source_version, json.dumps(matched), request_digest))
|
|
85
|
+
return {'accepted': True, 'created': prior is None, 'source_id': annotation_source,
|
|
86
|
+
'source_version': canonical_turn_digest(annotation_messages), 'target': target}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def source_ids(row):
|
|
90
|
+
ids = list(row.get('source_turn_ids') or [])
|
|
91
|
+
if row.get('source_turn_id'):
|
|
92
|
+
ids.append(row['source_turn_id'])
|
|
93
|
+
uri = str(row.get('source_uri') or '')
|
|
94
|
+
if uri.startswith('turn:'):
|
|
95
|
+
ids.append(uri[5:])
|
|
96
|
+
return list(dict.fromkeys(ids))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def expand(ledger, candidates, *, contact_id, session_id, covered=()):
|
|
100
|
+
"""Bundle exact revision corrections before ranking and character packing.
|
|
101
|
+
|
|
102
|
+
A source-backed belief/conflict bundle and a derived answer are subject to
|
|
103
|
+
the same rule as direct lexical/semantic excerpts. All discovered notes
|
|
104
|
+
remain attributed evidence; the newest note does not automatically win.
|
|
105
|
+
"""
|
|
106
|
+
from .idempotency import canonical_turn_digest, source_message_hash
|
|
107
|
+
from .audio import source_text
|
|
108
|
+
result = []
|
|
109
|
+
with closing(ledger._connect()) as conn:
|
|
110
|
+
cached = {}
|
|
111
|
+
|
|
112
|
+
def source(identifier):
|
|
113
|
+
if identifier not in cached:
|
|
114
|
+
row = conn.execute('''SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=?
|
|
115
|
+
AND (scope='person' OR session_id=?)''', (identifier, contact_id, session_id)).fetchone()
|
|
116
|
+
cached[identifier] = dict(row) if row else None
|
|
117
|
+
if row:
|
|
118
|
+
cached[identifier]['messages'] = json.loads(row['messages_json'])
|
|
119
|
+
return cached[identifier]
|
|
120
|
+
|
|
121
|
+
def candidate_hashes(candidate, identifier):
|
|
122
|
+
# Assertion bundles carry canonical claim membership, not a JSON
|
|
123
|
+
# string to reinterpret. Semantic excerpts already have one hash.
|
|
124
|
+
if '_source_message_hashes' in candidate:
|
|
125
|
+
return set(candidate['_source_message_hashes'].get(identifier, []))
|
|
126
|
+
if candidate.get('source_message_hash'):
|
|
127
|
+
return {candidate['source_message_hash']}
|
|
128
|
+
if candidate.get('kind') == 'source_quote' or candidate.get('source_turn_id'):
|
|
129
|
+
row = source(identifier)
|
|
130
|
+
text, role = candidate.get('content'), candidate.get('role')
|
|
131
|
+
return {source_message_hash(row['session_id'], message) for message in row['messages']
|
|
132
|
+
if (not role or message.get('role') == role)
|
|
133
|
+
and isinstance(text, str) and text
|
|
134
|
+
and text in source_text(message.get('content'))} if row else set()
|
|
135
|
+
# Older graph summaries and supplied parent references attest a
|
|
136
|
+
# whole source version. Do not invent finer ancestry from prose.
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
for original in candidates:
|
|
140
|
+
ids = source_ids(original)
|
|
141
|
+
pending = [(identifier, candidate_hashes(original, identifier)) for identifier in ids]
|
|
142
|
+
visited, notes = {}, {}
|
|
143
|
+
unavailable = False
|
|
144
|
+
while pending:
|
|
145
|
+
identifier, selected_hashes = pending.pop()
|
|
146
|
+
row = source(identifier)
|
|
147
|
+
if row is None:
|
|
148
|
+
continue
|
|
149
|
+
version = canonical_turn_digest(row['messages'])
|
|
150
|
+
hashes = {source_message_hash(row['session_id'], m) for m in row['messages']}
|
|
151
|
+
selected_hashes = hashes if selected_hashes is None else hashes.intersection(selected_hashes)
|
|
152
|
+
selected_hashes -= visited.get(identifier, set())
|
|
153
|
+
if not selected_hashes:
|
|
154
|
+
continue
|
|
155
|
+
visited.setdefault(identifier, set()).update(selected_hashes)
|
|
156
|
+
for relation in conn.execute('SELECT * FROM source_annotations WHERE target_source_id=?', (identifier,)):
|
|
157
|
+
# A partial erase can change the source revision while
|
|
158
|
+
# retaining the annotated message. Its removed correction
|
|
159
|
+
# still fences that message; unrelated survivors remain.
|
|
160
|
+
same_version = relation['target_version'] == version
|
|
161
|
+
retained_message = selected_hashes.intersection(json.loads(relation['target_message_hashes_json']))
|
|
162
|
+
if not retained_message:
|
|
163
|
+
continue
|
|
164
|
+
annotation = source(relation['annotation_source_id'])
|
|
165
|
+
if annotation is None or not same_version:
|
|
166
|
+
unavailable = True
|
|
167
|
+
break
|
|
168
|
+
data = json.loads(annotation['messages'][0]['content'])
|
|
169
|
+
notes[relation['annotation_source_id']] = data
|
|
170
|
+
pending.append((relation['annotation_source_id'], None))
|
|
171
|
+
if unavailable:
|
|
172
|
+
break
|
|
173
|
+
own_annotation = conn.execute('SELECT * FROM source_annotations WHERE annotation_source_id=?',
|
|
174
|
+
(identifier,)).fetchone()
|
|
175
|
+
for message in row['messages']:
|
|
176
|
+
if (message.get('role') == 'assistant'
|
|
177
|
+
and source_message_hash(row['session_id'], message) in selected_hashes):
|
|
178
|
+
for ref in message.get('_supplied_sources', []):
|
|
179
|
+
parent = source(ref['source_id'])
|
|
180
|
+
if parent and canonical_turn_digest(parent['messages']) == ref['source_version']:
|
|
181
|
+
# Dedicated annotations have exact target message
|
|
182
|
+
# membership. Their own source link must not widen
|
|
183
|
+
# a selected excerpt back to all sibling messages.
|
|
184
|
+
target_hashes = (set(json.loads(own_annotation['target_message_hashes_json']))
|
|
185
|
+
if own_annotation and own_annotation['target_source_id'] == ref['source_id']
|
|
186
|
+
and own_annotation['target_version'] == ref['source_version'] else None)
|
|
187
|
+
pending.append((ref['source_id'], target_hashes))
|
|
188
|
+
if unavailable:
|
|
189
|
+
continue
|
|
190
|
+
if not notes:
|
|
191
|
+
# An empty correction set is also a selection-time snapshot.
|
|
192
|
+
# A first note can arrive while ranking awaits; preserve exact
|
|
193
|
+
# message membership so a sibling's note does not hide this row.
|
|
194
|
+
row = dict(original)
|
|
195
|
+
if visited:
|
|
196
|
+
row['_annotation_source_refs'] = [{'source_id': identifier,
|
|
197
|
+
'source_version': canonical_turn_digest(source(identifier)['messages'])}
|
|
198
|
+
for identifier in visited]
|
|
199
|
+
row['_annotation_ids'] = ()
|
|
200
|
+
row['_annotation_message_hashes'] = {identifier: sorted(hashes)
|
|
201
|
+
for identifier, hashes in visited.items()}
|
|
202
|
+
result.append(row)
|
|
203
|
+
continue
|
|
204
|
+
row = dict(original)
|
|
205
|
+
if (original.get('kind') == 'belief'
|
|
206
|
+
or 'kind' not in original and not original.get('source_turn_id')):
|
|
207
|
+
row['_recall_memory_id'] = original.get('_recall_memory_id', original['id'])
|
|
208
|
+
# If the annotation itself matched the query, show its exact
|
|
209
|
+
# target excerpt first instead of presenting an orphan note.
|
|
210
|
+
if len(ids) == 1 and ids[0] in notes:
|
|
211
|
+
note = notes[ids[0]]
|
|
212
|
+
original_evidence = {'source': note['target'], 'quote': note['excerpt']}
|
|
213
|
+
row['_annotation_only'] = True
|
|
214
|
+
else:
|
|
215
|
+
original_evidence = {'source_ids': ids, 'content': str(original.get('content') or '')}
|
|
216
|
+
corrections = [dict(data, source_id=identifier) for identifier, data in sorted(
|
|
217
|
+
notes.items(), key=lambda pair: (pair[1]['recorded_at'], pair[0]))]
|
|
218
|
+
row.update(content=json.dumps({'original': original_evidence, 'corrections': corrections,
|
|
219
|
+
'interpretation': 'Read the original in light of these attributed corrections; conflicting corrections remain unresolved.'},
|
|
220
|
+
ensure_ascii=False), atomic_evidence=True, epistemic_state='correction_evidence',
|
|
221
|
+
source_turn_ids=list(dict.fromkeys(ids + sorted(visited) + list(notes))))
|
|
222
|
+
row['_annotation_source_refs'] = [{'source_id': identifier,
|
|
223
|
+
'source_version': canonical_turn_digest(source(identifier)['messages'])}
|
|
224
|
+
for identifier in row['source_turn_ids'] if source(identifier)]
|
|
225
|
+
row['_annotation_ids'] = tuple(sorted(notes))
|
|
226
|
+
row['_annotation_message_hashes'] = {identifier: sorted(hashes) for identifier, hashes in visited.items()}
|
|
227
|
+
row['ranking_text'] = '\n'.join(
|
|
228
|
+
'Attributed correction (not independently verified):\n' + note['correction']
|
|
229
|
+
for note in corrections) + '\nOriginal evidence:\n' + str(
|
|
230
|
+
original.get('ranking_text') or original.get('content') or '')
|
|
231
|
+
row['id'] = 'annotated:' + hashlib.sha256(json.dumps([original['id'], list(notes)], sort_keys=True).encode()).hexdigest()
|
|
232
|
+
result.append(row)
|
|
233
|
+
# A lexical/semantic annotation hit adds no evidence when an original
|
|
234
|
+
# excerpt already carries the same complete correction set. Preserve all
|
|
235
|
+
# distinct original excerpts; this is not content or semantic merging.
|
|
236
|
+
seen = {row['_annotation_ids'] for row in [*covered, *result]
|
|
237
|
+
if row.get('_annotation_ids') and not row.get('_annotation_only')}
|
|
238
|
+
retained = []
|
|
239
|
+
for row in result:
|
|
240
|
+
if row.get('_annotation_only'):
|
|
241
|
+
if row['_annotation_ids'] in seen:
|
|
242
|
+
continue
|
|
243
|
+
seen.add(row['_annotation_ids'])
|
|
244
|
+
retained.append(row)
|
|
245
|
+
return retained
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def inputs_unannotated(ledger, refs):
|
|
249
|
+
"""Check exact input membership, including retained withdrawn annotations.
|
|
250
|
+
|
|
251
|
+
Callers separately validate source ownership. This small current read also
|
|
252
|
+
serves the existing native freshness round trip; it adds no revision store.
|
|
253
|
+
"""
|
|
254
|
+
membership = {}
|
|
255
|
+
for ref in refs:
|
|
256
|
+
membership.setdefault(ref['source_id'], set()).add(ref['input_message_hash'])
|
|
257
|
+
if not membership:
|
|
258
|
+
return True
|
|
259
|
+
with closing(ledger._connect()) as conn:
|
|
260
|
+
rows = conn.execute('SELECT target_source_id,target_message_hashes_json '
|
|
261
|
+
'FROM source_annotations WHERE target_source_id IN ('
|
|
262
|
+
+ ','.join('?' for _ in membership) + ')', list(membership))
|
|
263
|
+
return not any(membership[row[0]].intersection(json.loads(row[1])) for row in rows)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def current_candidates(ledger, candidates, *, contact_id, session_id):
|
|
267
|
+
"""Do not publish stale evidence after a correction changes during ranking."""
|
|
268
|
+
required = [ref for row in candidates for ref in row.get('_annotation_source_refs', [])]
|
|
269
|
+
if not required:
|
|
270
|
+
return candidates
|
|
271
|
+
current = {r['source_id']: r['source_version'] for r in ledger.source_references(
|
|
272
|
+
[r['source_id'] for r in required], contact_id=contact_id, session_id=session_id)}
|
|
273
|
+
notes = {}
|
|
274
|
+
with closing(ledger._connect()) as conn:
|
|
275
|
+
for identifier, version in current.items():
|
|
276
|
+
notes[identifier] = {row[0]: set(json.loads(row[1])) for row in conn.execute('''SELECT annotation_source_id,target_message_hashes_json
|
|
277
|
+
FROM source_annotations WHERE target_source_id=? AND target_version=?''', (identifier, version))}
|
|
278
|
+
return [row for row in candidates if all(current.get(ref['source_id']) == ref['source_version']
|
|
279
|
+
for ref in row.get('_annotation_source_refs', [])) and (
|
|
280
|
+
not row.get('_annotation_source_refs') or set(row['_annotation_ids']) == set().union(
|
|
281
|
+
*({identifier for identifier, hashes in notes.get(ref['source_id'], {}).items()
|
|
282
|
+
if hashes.intersection(row['_annotation_message_hashes'].get(ref['source_id'], []))}
|
|
283
|
+
for ref in row['_annotation_source_refs'])))]
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Explicit participant corrections without rewriting or erasing source text.
|
|
2
|
+
|
|
3
|
+
The host authorizes the exact old/new people and selected sources. This module
|
|
4
|
+
owns the source transaction; contact handles live in their existing store and
|
|
5
|
+
are reconciled separately using the same operation ID. No source is inferred
|
|
6
|
+
from a name, similarity, session or contact-wide history sweep.
|
|
7
|
+
"""
|
|
8
|
+
from contextlib import closing
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def initialize(conn):
|
|
15
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS source_attribution_operations (
|
|
16
|
+
operation_id TEXT PRIMARY KEY,request_sha256 TEXT NOT NULL,
|
|
17
|
+
result_json TEXT NOT NULL,recorded_at TEXT NOT NULL)''')
|
|
18
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS source_attribution_revisions (
|
|
19
|
+
operation_id TEXT NOT NULL,source_id TEXT NOT NULL,old_contact_id TEXT NOT NULL,
|
|
20
|
+
contact_id TEXT NOT NULL,original_digest TEXT NOT NULL,source_version TEXT NOT NULL,
|
|
21
|
+
PRIMARY KEY(operation_id,source_id))''')
|
|
22
|
+
conn.execute('CREATE INDEX IF NOT EXISTS attribution_source ON source_attribution_revisions(source_id)')
|
|
23
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS source_attribution_invalidations (
|
|
24
|
+
source_id TEXT NOT NULL,corrected_source_id TEXT NOT NULL,operation_id TEXT NOT NULL,
|
|
25
|
+
PRIMARY KEY(source_id,corrected_source_id,operation_id))''')
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def correct(ledger, *, operation_id, performed_by, old_contact_id, contact_id,
|
|
29
|
+
source_ids, evidence_refs):
|
|
30
|
+
"""Correct an explicit source set and invalidate derived interpretations.
|
|
31
|
+
|
|
32
|
+
The original ingest digest, messages, media links, session and timestamps
|
|
33
|
+
remain unchanged. Changing the participant invalidates cached source views
|
|
34
|
+
and their current projections; it does not adopt old judgments for a new
|
|
35
|
+
person. Returned refs let other stores purge their own source projections.
|
|
36
|
+
"""
|
|
37
|
+
from apsimo.contacts.identity_links import _refs, _json
|
|
38
|
+
from .idempotency import canonical_turn_digest, SourceErased
|
|
39
|
+
selected, evidence = _refs(source_ids), _refs(evidence_refs)
|
|
40
|
+
if not selected or not evidence or old_contact_id == contact_id:
|
|
41
|
+
raise ValueError('invalid_source_attribution')
|
|
42
|
+
for value in (operation_id, performed_by, old_contact_id, contact_id):
|
|
43
|
+
if not isinstance(value, str) or not value.strip() or len(value) > 256:
|
|
44
|
+
raise ValueError('invalid_source_attribution')
|
|
45
|
+
request_hash = hashlib.sha256(_json([performed_by, old_contact_id, contact_id, selected, evidence]).encode()).hexdigest()
|
|
46
|
+
with closing(ledger._connect()) as conn, conn:
|
|
47
|
+
conn.execute('BEGIN IMMEDIATE')
|
|
48
|
+
initialize(conn)
|
|
49
|
+
prior = conn.execute('SELECT * FROM source_attribution_operations WHERE operation_id=?', (operation_id,)).fetchone()
|
|
50
|
+
if prior:
|
|
51
|
+
if prior['request_sha256'] != request_hash:
|
|
52
|
+
raise ValueError('source_attribution_operation_conflict')
|
|
53
|
+
return json.loads(prior['result_json'])
|
|
54
|
+
rows = []
|
|
55
|
+
for sid in selected:
|
|
56
|
+
if conn.execute('SELECT 1 FROM source_erasures WHERE turn_id=?', (sid,)).fetchone():
|
|
57
|
+
raise SourceErased('source_erased')
|
|
58
|
+
row = conn.execute("SELECT * FROM turn_sources WHERE turn_id=? AND contact_id=? AND scope='person'",
|
|
59
|
+
(sid, old_contact_id)).fetchone()
|
|
60
|
+
if row is None:
|
|
61
|
+
raise ValueError('source_attribution_preimage_changed')
|
|
62
|
+
rows.append(row)
|
|
63
|
+
# Discover source-linked descendant answers for invalidation, not silent
|
|
64
|
+
# reassignment. Their original prose remains historical source evidence.
|
|
65
|
+
descendants = set()
|
|
66
|
+
pending = set(selected)
|
|
67
|
+
candidates = conn.execute("SELECT turn_id,messages_json FROM turn_sources WHERE contact_id=?", (old_contact_id,)).fetchall()
|
|
68
|
+
while pending:
|
|
69
|
+
found = set()
|
|
70
|
+
for row in candidates:
|
|
71
|
+
if row['turn_id'] in selected or row['turn_id'] in descendants:
|
|
72
|
+
continue
|
|
73
|
+
if any(ref.get('source_id') in pending for message in json.loads(row['messages_json'])
|
|
74
|
+
for ref in message.get('_supplied_sources', [])):
|
|
75
|
+
found.add(row['turn_id'])
|
|
76
|
+
descendants.update(found)
|
|
77
|
+
pending = found
|
|
78
|
+
affected = sorted(set(selected) | descendants)
|
|
79
|
+
for sid in descendants:
|
|
80
|
+
# Existing projection invalidation table owns graph/ToM cleanup.
|
|
81
|
+
for original in selected:
|
|
82
|
+
conn.execute('INSERT OR IGNORE INTO source_projection_erasures VALUES (?,?)', (sid, original))
|
|
83
|
+
conn.execute('INSERT OR IGNORE INTO source_attribution_invalidations VALUES (?,?,?)',
|
|
84
|
+
(sid, original, operation_id))
|
|
85
|
+
from apsimo.beliefs.source_projection import erase_removed as erase_claims, enqueue
|
|
86
|
+
from apsimo.self_model.perspective import erase_removed as erase_preferences
|
|
87
|
+
from apsimo.self_model.judgments import erase_removed as erase_judgments
|
|
88
|
+
from .source_vectors import enqueue as enqueue_vectors
|
|
89
|
+
for row in rows:
|
|
90
|
+
sid = row['turn_id']
|
|
91
|
+
messages = json.loads(row['messages_json'])
|
|
92
|
+
prior_job = conn.execute('SELECT timezone FROM source_claim_jobs WHERE turn_id=?', (sid,)).fetchone()
|
|
93
|
+
conn.execute('INSERT INTO source_attribution_revisions VALUES (?,?,?,?,?,?)',
|
|
94
|
+
(operation_id, sid, old_contact_id, contact_id, row['content_sha256'], canonical_turn_digest(messages)))
|
|
95
|
+
# Revoke claim-worker leases before changing their input attribution.
|
|
96
|
+
erase_claims(conn, sid, row['session_id'], [])
|
|
97
|
+
erase_preferences(conn, sid, row['session_id'], [])
|
|
98
|
+
erase_judgments(conn, sid, row['session_id'], [])
|
|
99
|
+
conn.execute('UPDATE turn_sources SET contact_id=? WHERE turn_id=?', (contact_id, sid))
|
|
100
|
+
conn.execute('UPDATE turn_ingestion SET response_json=NULL,error=NULL WHERE turn_id=?', (sid,))
|
|
101
|
+
# Preserve admission and timezone. Reviewed source-only imports
|
|
102
|
+
# must not start learning merely because their identity was fixed.
|
|
103
|
+
if prior_job:
|
|
104
|
+
enqueue(conn, sid, messages, scope=row['scope'], timezone_name=prior_job['timezone'])
|
|
105
|
+
enqueue_vectors(conn, sid)
|
|
106
|
+
for sid in descendants:
|
|
107
|
+
row = conn.execute('SELECT session_id FROM turn_sources WHERE turn_id=?', (sid,)).fetchone()
|
|
108
|
+
if row:
|
|
109
|
+
conn.execute('UPDATE turn_ingestion SET response_json=NULL,error=NULL WHERE turn_id=?', (sid,))
|
|
110
|
+
erase_claims(conn, sid, row['session_id'], [])
|
|
111
|
+
erase_preferences(conn, sid, row['session_id'], [])
|
|
112
|
+
erase_judgments(conn, sid, row['session_id'], [])
|
|
113
|
+
try:
|
|
114
|
+
from apsimo.self_model.appraisals import invalidate_source_attribution
|
|
115
|
+
except ModuleNotFoundError as exc:
|
|
116
|
+
if exc.name != 'apsimo.self_model.appraisals':
|
|
117
|
+
raise
|
|
118
|
+
else:
|
|
119
|
+
invalidate_source_attribution(conn, affected, old_contact_id, contact_id)
|
|
120
|
+
result = {'schema': 'SourceAttributionCorrectionV1', 'operation_id': operation_id,
|
|
121
|
+
'old_contact_id': old_contact_id, 'contact_id': contact_id,
|
|
122
|
+
'source_ids': selected, 'affected_source_ids': affected,
|
|
123
|
+
'invalidated_source_ids': sorted(descendants), 'evidence_refs': evidence,
|
|
124
|
+
'recorded_at': datetime.now(timezone.utc).isoformat(), 'performed_by': performed_by,
|
|
125
|
+
'authority_granted': False, 'source_text_preserved': True}
|
|
126
|
+
conn.execute('INSERT INTO source_attribution_operations VALUES (?,?,?,?)',
|
|
127
|
+
(operation_id, request_hash, _json(result), result['recorded_at']))
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def history(ledger, *, source_id, contact_id):
|
|
132
|
+
"""Correction provenance only for the source's currently authorized person."""
|
|
133
|
+
with closing(ledger._connect()) as conn:
|
|
134
|
+
if not conn.execute('SELECT 1 FROM sqlite_master WHERE name=?', ('source_attribution_revisions',)).fetchone():
|
|
135
|
+
return []
|
|
136
|
+
if not conn.execute('SELECT 1 FROM turn_sources WHERE turn_id=? AND contact_id=?', (source_id, contact_id)).fetchone():
|
|
137
|
+
return []
|
|
138
|
+
rows = conn.execute('''SELECT r.*,o.recorded_at FROM source_attribution_revisions r
|
|
139
|
+
JOIN source_attribution_operations o USING(operation_id) WHERE r.source_id=?
|
|
140
|
+
ORDER BY o.recorded_at,r.operation_id''', (source_id,)).fetchall()
|
|
141
|
+
return [dict(row) for row in rows]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def is_invalidated(conn, source_id):
|
|
145
|
+
if not conn.execute("SELECT 1 FROM sqlite_master WHERE name='source_attribution_invalidations'").fetchone():
|
|
146
|
+
return False
|
|
147
|
+
return bool(conn.execute('SELECT 1 FROM source_attribution_invalidations WHERE source_id=? LIMIT 1',
|
|
148
|
+
(source_id,)).fetchone())
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def visible_hits(ledger, hits):
|
|
152
|
+
"""Drop stale derived-answer copies, never the corrected direct evidence."""
|
|
153
|
+
with closing(ledger._connect()) as conn:
|
|
154
|
+
return [hit for hit in hits if not is_invalidated(conn, hit.get('turn_id') or hit.get('source_turn_id'))]
|