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,351 @@
|
|
|
1
|
+
"""Bounded opening of current canonical evidence through the memory read API."""
|
|
2
|
+
from contextlib import closing
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from .idempotency import canonical_turn_digest, source_message_hash
|
|
8
|
+
from .source_annotations import expand, current_candidates, inputs_unannotated
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def input_excerpt(ledger, *, contact_id, session_id, refs, max_chars=240):
|
|
12
|
+
"""A short exact admitted input, resolved afresh rather than copied to work.
|
|
13
|
+
|
|
14
|
+
This is what the participant requested, not a generated task summary or
|
|
15
|
+
evidence that the execution is fulfilling it. Multiple inputs and long
|
|
16
|
+
messages remain explicitly partial. Annotated inputs require the normal
|
|
17
|
+
source reader so their conditions cannot be clipped off a work label.
|
|
18
|
+
Exact input membership travels to the existing native freshness check so
|
|
19
|
+
a later annotation can invalidate the excerpt without changing its bytes.
|
|
20
|
+
"""
|
|
21
|
+
from .audio import source_text
|
|
22
|
+
watermark = ledger.erasure_watermark(contact_id)
|
|
23
|
+
versions = ledger.resolve_input_dependencies(contact_id=contact_id,
|
|
24
|
+
session_id=session_id, refs=refs)
|
|
25
|
+
available = ledger.source_references([ref['source_id'] for ref in versions],
|
|
26
|
+
contact_id=contact_id, session_id=session_id)
|
|
27
|
+
if not refs or any(ref not in available for ref in versions):
|
|
28
|
+
raise ValueError('source_input_unavailable')
|
|
29
|
+
selected = refs[0]
|
|
30
|
+
with closing(ledger._connect()) as conn:
|
|
31
|
+
source = conn.execute('''SELECT session_id,messages_json FROM turn_sources
|
|
32
|
+
WHERE turn_id=? AND contact_id=? AND (scope='person' OR session_id=?)''',
|
|
33
|
+
(selected['source_id'], contact_id, session_id)).fetchone()
|
|
34
|
+
if source is None:
|
|
35
|
+
raise ValueError('source_input_unavailable')
|
|
36
|
+
matches = [message for message in json.loads(source['messages_json'])
|
|
37
|
+
if message.get('role') == 'user' and source_message_hash(source['session_id'], message)
|
|
38
|
+
== selected['input_message_hash']]
|
|
39
|
+
if len(matches) != 1:
|
|
40
|
+
raise ValueError('source_input_unavailable')
|
|
41
|
+
text = source_text(matches[0].get('content'))
|
|
42
|
+
membership = {}
|
|
43
|
+
for ref in refs:
|
|
44
|
+
membership.setdefault(ref['source_id'], []).append(ref['input_message_hash'])
|
|
45
|
+
candidate = {'id': 'execution-input:' + selected['source_id'], 'kind': 'source_quote',
|
|
46
|
+
'source_turn_ids': list(membership), 'content': text,
|
|
47
|
+
'_source_message_hashes': membership}
|
|
48
|
+
current = current_candidates(ledger, expand(ledger, [candidate],
|
|
49
|
+
contact_id=contact_id, session_id=session_id), contact_id=contact_id, session_id=session_id)
|
|
50
|
+
if len(current) != 1 or ledger.erasure_watermark(contact_id) != watermark:
|
|
51
|
+
raise ValueError('source_input_unavailable')
|
|
52
|
+
if current[0].get('_annotation_ids') or not inputs_unannotated(ledger, refs):
|
|
53
|
+
return {'status': 'annotated_input_requires_source_read'}
|
|
54
|
+
if not text:
|
|
55
|
+
return {'status': 'input_has_no_text'}
|
|
56
|
+
return {'status': 'admitted_input_excerpt', 'excerpt': text[:max_chars],
|
|
57
|
+
'partial': len(text) > max_chars or len(refs) > 1,
|
|
58
|
+
'input_count': len(refs), 'source_id': selected['source_id'],
|
|
59
|
+
'input_message_hash': selected['input_message_hash'],
|
|
60
|
+
'_provenance': {'contact_id': contact_id, 'watermark': watermark, 'source_refs': versions,
|
|
61
|
+
'unannotated_input_refs': [dict(ref) for ref in refs]}}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _document_media(ledger, conn, *, scope, expected, asset_hash, hashes):
|
|
65
|
+
"""Serialize exact ownership and bounded original integrity with erasure."""
|
|
66
|
+
from .media import SourceMedia
|
|
67
|
+
from .documents import MAX_DOCUMENT_BYTES
|
|
68
|
+
original = SourceMedia(ledger)
|
|
69
|
+
with conn:
|
|
70
|
+
conn.execute('BEGIN IMMEDIATE')
|
|
71
|
+
owners = original._owned(conn, asset_hash, **scope)
|
|
72
|
+
if not any(owner['turn_id'] == expected['source_id']
|
|
73
|
+
and owner['message_hash'] in hashes
|
|
74
|
+
and canonical_turn_digest(json.loads(owner['messages_json'])) == expected['source_version']
|
|
75
|
+
for owner in owners):
|
|
76
|
+
raise ValueError('source_document_unavailable')
|
|
77
|
+
media = conn.execute('SELECT mime_type,status,size_bytes,media_metadata_json FROM source_media WHERE asset_hash=?',
|
|
78
|
+
(asset_hash,)).fetchone()
|
|
79
|
+
if (media is None or media['mime_type'] != 'application/pdf' or media['status'] == 'orphan'
|
|
80
|
+
or not 0 < media['size_bytes'] <= MAX_DOCUMENT_BYTES):
|
|
81
|
+
raise ValueError('source_document_unavailable')
|
|
82
|
+
try:
|
|
83
|
+
with original.store._original_path(asset_hash, 'application/pdf').open('rb') as stream:
|
|
84
|
+
data = stream.read(MAX_DOCUMENT_BYTES + 1)
|
|
85
|
+
except OSError as exc:
|
|
86
|
+
raise ValueError('source_document_original_unavailable') from exc
|
|
87
|
+
if (len(data) != media['size_bytes'] or len(data) > MAX_DOCUMENT_BYTES
|
|
88
|
+
or hashlib.sha256(data).hexdigest() != asset_hash):
|
|
89
|
+
raise ValueError('source_document_original_integrity_mismatch')
|
|
90
|
+
return media
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _document_page(ledger, conn, *, scope, expected, asset_hash, page, hashes):
|
|
94
|
+
"""Open a stored derivative without parsing or returning original bytes."""
|
|
95
|
+
media = _document_media(ledger, conn, scope=scope, expected=expected, asset_hash=asset_hash, hashes=hashes)
|
|
96
|
+
try:
|
|
97
|
+
document = json.loads(media['media_metadata_json'] or '{}').get('document', {})
|
|
98
|
+
if not isinstance(document, dict):
|
|
99
|
+
raise ValueError('invalid document metadata')
|
|
100
|
+
status = {'document_pending': 'pending', 'document_running': 'pending',
|
|
101
|
+
'document_unsupported': 'unsupported', 'document_failed': 'failed'}.get(media['status'])
|
|
102
|
+
if status is None:
|
|
103
|
+
status = document.get('status')
|
|
104
|
+
if media['status'] != 'complete' or status not in {'complete', 'partial'}:
|
|
105
|
+
raise ValueError('invalid document disposition')
|
|
106
|
+
page_count = document.get('page_count')
|
|
107
|
+
if page_count is not None and (type(page_count) is not int or page_count < 0):
|
|
108
|
+
raise ValueError('invalid document page count')
|
|
109
|
+
if page_count is not None and page > page_count and (page_count > 0 or status in {'complete', 'partial'}):
|
|
110
|
+
raise ValueError('source_document_page_unavailable')
|
|
111
|
+
selected = None
|
|
112
|
+
if status in {'complete', 'partial'}:
|
|
113
|
+
if document.get('version') != 'source-pdf-text-v1' or document.get('ocr_performed') is not False:
|
|
114
|
+
raise ValueError('invalid document derivative')
|
|
115
|
+
pages = document.get('pages')
|
|
116
|
+
if not isinstance(pages, list) or any(not isinstance(item, dict) for item in pages):
|
|
117
|
+
raise ValueError('invalid document pages')
|
|
118
|
+
selected = next((item for item in pages if type(item.get('page')) is int and item['page'] == page), None)
|
|
119
|
+
if selected is not None and (not isinstance(selected.get('text'), str)
|
|
120
|
+
or selected.get('status') not in {'text', 'no_extractable_text'}):
|
|
121
|
+
raise ValueError('invalid document page')
|
|
122
|
+
if selected is None and status == 'complete':
|
|
123
|
+
raise ValueError('source_document_page_unavailable')
|
|
124
|
+
elif status == 'unsupported' and isinstance(document.get('pages'), list):
|
|
125
|
+
# Image-only PDFs can retain honest per-page blank dispositions.
|
|
126
|
+
# Never expose page text from an unsupported derivative.
|
|
127
|
+
selected = next((item for item in document['pages'] if isinstance(item, dict)
|
|
128
|
+
and type(item.get('page')) is int and item['page'] == page
|
|
129
|
+
and item.get('status') == 'no_extractable_text'), None)
|
|
130
|
+
evidence = {'asset_hash': asset_hash, 'asset_id': 'sha256:' + asset_hash,
|
|
131
|
+
'mime_type': 'application/pdf', 'page': page, 'page_count': page_count,
|
|
132
|
+
'status': status, 'reason': document.get('reason'),
|
|
133
|
+
'version': document.get('version'), 'parser': document.get('parser'),
|
|
134
|
+
'parser_version': document.get('parser_version'), 'ocr_performed': False,
|
|
135
|
+
'epistemic_state': 'derived_unverified',
|
|
136
|
+
'page_status': selected['status'] if selected else 'not_extracted'}
|
|
137
|
+
evidence.update({key: document[key] for key in ('memory_control', 'memory_limit_bytes',
|
|
138
|
+
'memory_sample_interval_ms', 'hard_limit') if key in document})
|
|
139
|
+
text = selected['text'] if selected and selected['status'] == 'text' else ''
|
|
140
|
+
fingerprint = hashlib.sha256(json.dumps([media['status'], document], sort_keys=True).encode()).hexdigest()
|
|
141
|
+
return evidence, text, fingerprint
|
|
142
|
+
except (TypeError, AttributeError, json.JSONDecodeError) as exc:
|
|
143
|
+
raise ValueError('source_document_unavailable') from exc
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def read(ledger, *, contact_id, session_id, source_id, source_version,
|
|
147
|
+
view='source', claim_id=None, offset=0, read_revision=None, asset_hash=None, page=None, requested_ms=None):
|
|
148
|
+
if ((view == 'video' and (not isinstance(asset_hash, str) or not re.fullmatch('[0-9a-f]{64}', asset_hash)
|
|
149
|
+
or type(requested_ms) is not int or not 0 <= requested_ms <= 30000 or claim_id or offset))
|
|
150
|
+
or view != 'video' and requested_ms is not None):
|
|
151
|
+
raise ValueError('source_video_requires_asset_hash_and_requested_ms')
|
|
152
|
+
if view == 'document' and (not isinstance(asset_hash, str) or not re.fullmatch('[0-9a-f]{64}', asset_hash)
|
|
153
|
+
or type(page) is not int or page < 1 or claim_id is not None
|
|
154
|
+
or type(offset) is not int or not 0 <= offset <= 10000000 or offset and read_revision is None):
|
|
155
|
+
raise ValueError('source_document_requires_asset_hash_and_page')
|
|
156
|
+
if view != 'document' and page is not None:
|
|
157
|
+
raise ValueError('page_requires_document_view')
|
|
158
|
+
scope = {'contact_id': contact_id, 'session_id': session_id}
|
|
159
|
+
# Stamp before any content read. A concurrent erase then invalidates this
|
|
160
|
+
# result at the existing native request boundary, even after HTTP returns.
|
|
161
|
+
watermark = ledger.erasure_watermark(contact_id)
|
|
162
|
+
expected = {'source_id': source_id, 'source_version': source_version}
|
|
163
|
+
if expected not in ledger.source_references([source_id], **scope):
|
|
164
|
+
raise ValueError('source_unavailable_or_changed')
|
|
165
|
+
with closing(ledger._connect()) as conn:
|
|
166
|
+
source = conn.execute('SELECT * FROM turn_sources WHERE turn_id=?', (source_id,)).fetchone()
|
|
167
|
+
if source is None:
|
|
168
|
+
raise ValueError('source_unavailable_or_changed')
|
|
169
|
+
messages = json.loads(source['messages_json'])
|
|
170
|
+
if canonical_turn_digest(messages) != source_version:
|
|
171
|
+
raise ValueError('source_unavailable_or_changed')
|
|
172
|
+
if view == 'assertions':
|
|
173
|
+
from apsimo.beliefs.source_projection import SourceClaimProjection
|
|
174
|
+
projection = SourceClaimProjection(ledger)
|
|
175
|
+
anchor = projection._rows(conn, **scope, ids=[claim_id])
|
|
176
|
+
if not anchor or anchor[0]['turn_id'] != source_id:
|
|
177
|
+
raise ValueError('source_claim_unavailable')
|
|
178
|
+
# Includes superseded/retracted assertions with their exact status,
|
|
179
|
+
# not a new selection of a winning fact. Output pages are bounded.
|
|
180
|
+
rows = projection._rows(conn, **scope, key=(anchor[0]['subject_key'], anchor[0]['predicate']), limit=10001)
|
|
181
|
+
if len(rows) > 10000:
|
|
182
|
+
raise ValueError('source_history_exceeds_read_limit')
|
|
183
|
+
rows.sort(key=lambda c: (c['recorded_at'], c['id']))
|
|
184
|
+
notes = [tuple(r) for r in conn.execute('''SELECT a.annotation_source_id,a.request_sha256
|
|
185
|
+
FROM source_annotations a JOIN turn_sources s ON s.turn_id=a.annotation_source_id
|
|
186
|
+
WHERE s.contact_id=? AND (s.scope='person' OR s.session_id=?)
|
|
187
|
+
ORDER BY a.annotation_source_id''', (contact_id, session_id))]
|
|
188
|
+
revision = hashlib.sha256(json.dumps([rows, notes, watermark], sort_keys=True).encode()).hexdigest()
|
|
189
|
+
selected = rows[offset:offset + 8]
|
|
190
|
+
total, next_offset = len(rows), offset + len(selected)
|
|
191
|
+
ids = list(dict.fromkeys([source_id, *(c['turn_id'] for c in selected)]))
|
|
192
|
+
retained_ids = {c['id'] for c in rows}
|
|
193
|
+
episode_gap = any(c.get('representation') == 'episode'
|
|
194
|
+
and any(c.get(link) and c[link] not in retained_ids
|
|
195
|
+
for link in ('prior_claim_id', 'retracted_by', 'superseded_by'))
|
|
196
|
+
for c in rows)
|
|
197
|
+
content = json.dumps({'status': 'attributed_assertion_history',
|
|
198
|
+
**({'episode_history': 'incomplete_revision_chain',
|
|
199
|
+
'guidance': 'An episode revision is unavailable. Do not reconstruct current '
|
|
200
|
+
'details from older reports. Complete pagination does not close this gap.'}
|
|
201
|
+
if episode_gap else {}), 'assertions': [
|
|
202
|
+
{key: c.get(key) for key in ('id', 'turn_id', 'role', 'subject', 'predicate', 'value', 'evidence',
|
|
203
|
+
'observed_at', 'recorded_at', 'valid_from', 'valid_to', 'event_at', 'event_time',
|
|
204
|
+
'validity_basis', 'operation', 'prior_claim_id', 'superseded_by', 'retracted_by')}
|
|
205
|
+
| {key: c[key] for key in ('representation', 'epistemic_state', 'source_modality', 'evidence_basis') if key in c}
|
|
206
|
+
for c in selected]}, ensure_ascii=False)
|
|
207
|
+
hashes = {identifier: [c['message_hash'] for c in selected if c['turn_id'] == identifier]
|
|
208
|
+
for identifier in ids}
|
|
209
|
+
hashes[source_id] = list({*hashes.get(source_id, []), anchor[0]['message_hash']})
|
|
210
|
+
elif view == 'document':
|
|
211
|
+
selected = [message for message in messages if isinstance(message.get('content'), list)
|
|
212
|
+
and any(isinstance(block, dict) and block.get('type') == 'document'
|
|
213
|
+
and block.get('asset_id') == 'sha256:' + asset_hash
|
|
214
|
+
and block.get('mime_type') == 'application/pdf'
|
|
215
|
+
for block in message['content'])]
|
|
216
|
+
if not selected:
|
|
217
|
+
raise ValueError('source_document_unavailable')
|
|
218
|
+
ids = [source_id]
|
|
219
|
+
hashes = {source_id: [source_message_hash(source['session_id'], m) for m in selected]}
|
|
220
|
+
document, page_text, derivative = _document_page(ledger, conn, scope=scope, expected=expected,
|
|
221
|
+
asset_hash=asset_hash, page=page, hashes=hashes[source_id])
|
|
222
|
+
content = json.dumps({'document': {**document, 'text': page_text},
|
|
223
|
+
'source_messages': [{'message_hash': source_message_hash(source['session_id'], m),
|
|
224
|
+
'role': m['role']} for m in selected],
|
|
225
|
+
'reported_at': source['occurred_at'], 'recorded_at': source['ingested_at'],
|
|
226
|
+
'event_time': 'unknown unless supported by the source'}, ensure_ascii=False)
|
|
227
|
+
elif view in {'image', 'video'}:
|
|
228
|
+
selected = [message for message in messages if isinstance(message.get('content'), list)
|
|
229
|
+
and any(isinstance(block, dict) and block.get('type') == view
|
|
230
|
+
and block.get('asset_id') == 'sha256:' + str(asset_hash)
|
|
231
|
+
for block in message['content'])]
|
|
232
|
+
if not selected or offset or claim_id:
|
|
233
|
+
raise ValueError('source_' + view + '_unavailable')
|
|
234
|
+
ids = [source_id]
|
|
235
|
+
hashes = {source_id: [source_message_hash(source['session_id'], m) for m in selected]}
|
|
236
|
+
content = json.dumps({'asset_id': 'sha256:' + asset_hash,
|
|
237
|
+
**({'requested_ms': requested_ms, 'timestamp_origin': 'first_decoded_frame'} if view == 'video' else {}),
|
|
238
|
+
'source_messages': [{'message_hash': source_message_hash(source['session_id'], m),
|
|
239
|
+
'role': m['role']} for m in selected],
|
|
240
|
+
'reported_at': source['occurred_at'], 'recorded_at': source['ingested_at'],
|
|
241
|
+
'event_time': 'unknown unless supported by the source'}, ensure_ascii=False)
|
|
242
|
+
else:
|
|
243
|
+
ids = [source_id]
|
|
244
|
+
hashes = {source_id: [source_message_hash(source['session_id'], m) for m in messages]}
|
|
245
|
+
content = json.dumps({'messages': messages, 'reported_at': source['occurred_at'],
|
|
246
|
+
'recorded_at': source['ingested_at'],
|
|
247
|
+
'event_time': 'unknown unless explicitly stated in each source'}, ensure_ascii=False)
|
|
248
|
+
candidate = {'id': 'opened:' + source_id, 'kind': 'source_quote', 'source_uri': 'turn:' + source_id,
|
|
249
|
+
'source_turn_ids': ids, '_source_message_hashes': hashes, 'content': content}
|
|
250
|
+
expanded = current_candidates(ledger, expand(ledger, [candidate], **scope), **scope)
|
|
251
|
+
if len(expanded) != 1:
|
|
252
|
+
raise ValueError('source_correction_unavailable_or_changed')
|
|
253
|
+
row = expanded[0]
|
|
254
|
+
refs = row.get('_annotation_source_refs', [])
|
|
255
|
+
if expected not in refs or any(ref not in ledger.source_references([ref['source_id']], **scope) for ref in refs):
|
|
256
|
+
raise ValueError('source_unavailable_or_changed')
|
|
257
|
+
if view in {'image', 'video'}:
|
|
258
|
+
import base64
|
|
259
|
+
from .media import SourceMedia
|
|
260
|
+
content = row['content']
|
|
261
|
+
if len(content) > 16384:
|
|
262
|
+
raise ValueError('source_' + view + '_corrections_exceed_read_limit')
|
|
263
|
+
revision = hashlib.sha256(content.encode()).hexdigest()
|
|
264
|
+
if read_revision is not None and read_revision != revision:
|
|
265
|
+
raise ValueError('source_read_changed_restart_at_zero')
|
|
266
|
+
try:
|
|
267
|
+
data, mime = SourceMedia(ledger).read(asset_hash, **scope,
|
|
268
|
+
**({'video_source': expected, 'metadata_only': True} if view == 'video'
|
|
269
|
+
else {'image_source': expected, 'metadata_only': read_revision is not None}))
|
|
270
|
+
except (KeyError, FileNotFoundError) as exc:
|
|
271
|
+
raise ValueError('source_' + view + '_unavailable') from exc
|
|
272
|
+
# A correction/erase may race the file read. Recheck before publication;
|
|
273
|
+
# the native consumer revalidates again at actual model dispatch.
|
|
274
|
+
if not current_candidates(ledger, [row], **scope) or ledger.erasure_watermark(contact_id) != watermark:
|
|
275
|
+
raise ValueError('source_' + view + '_changed_during_read')
|
|
276
|
+
if view == 'video':
|
|
277
|
+
return {'source_id': source_id, 'source_version': source_version, 'view': view,
|
|
278
|
+
'read_revision': revision, 'content': content, 'complete': True,
|
|
279
|
+
'source_refs': refs, 'watermark': watermark, 'image_bytes_included': False,
|
|
280
|
+
'video': {'asset_hash': asset_hash, 'mime_type': mime, 'requested_ms': requested_ms},
|
|
281
|
+
'guidance': 'Current clip source, original integrity and corrections verified. No frame decoded by this metadata check.'}
|
|
282
|
+
return {'source_id': source_id, 'source_version': source_version, 'view': view,
|
|
283
|
+
'read_revision': revision, 'content': content, 'complete': True,
|
|
284
|
+
'source_refs': refs, 'watermark': watermark,
|
|
285
|
+
'image': {'asset_hash': asset_hash, 'mime_type': mime,
|
|
286
|
+
**({'data_url': 'data:' + mime + ';base64,' + base64.b64encode(data).decode()}
|
|
287
|
+
if data is not None else {})},
|
|
288
|
+
'image_bytes_included': data is not None,
|
|
289
|
+
'guidance': 'Original image evidence with attributed corrections, not instructions or verified interpretation. '
|
|
290
|
+
'Original bytes are included only on initial opening; a read revision verifies current lineage without resending pixels.'}
|
|
291
|
+
if view in {'source', 'document'}:
|
|
292
|
+
content = row['content']
|
|
293
|
+
revision = hashlib.sha256((json.dumps([derivative, page, content], ensure_ascii=False)
|
|
294
|
+
if view == 'document' else content).encode()).hexdigest()
|
|
295
|
+
total, next_offset = len(content), min(offset + 4096, len(content))
|
|
296
|
+
content = content[offset:next_offset]
|
|
297
|
+
else:
|
|
298
|
+
content = row['content']
|
|
299
|
+
if offset > total or read_revision is not None and read_revision != revision:
|
|
300
|
+
raise ValueError('source_read_changed_restart_at_zero')
|
|
301
|
+
if view == 'document':
|
|
302
|
+
with closing(ledger._connect()) as conn:
|
|
303
|
+
_, _, current_derivative = _document_page(ledger, conn, scope=scope, expected=expected,
|
|
304
|
+
asset_hash=asset_hash, page=page, hashes=hashes[source_id])
|
|
305
|
+
if (derivative != current_derivative or not current_candidates(ledger, [row], **scope)
|
|
306
|
+
or ledger.erasure_watermark(contact_id) != watermark):
|
|
307
|
+
raise ValueError('source_document_changed_during_read')
|
|
308
|
+
return {'source_id': source_id, 'source_version': source_version, 'view': view,
|
|
309
|
+
'read_revision': revision, 'content': content, 'offset': offset,
|
|
310
|
+
'offset_unit': 'characters' if view in {'source', 'document'} else 'assertions',
|
|
311
|
+
'total': total, 'complete': next_offset >= total,
|
|
312
|
+
'next_offset': next_offset if next_offset < total else None,
|
|
313
|
+
'source_refs': refs, 'watermark': watermark,
|
|
314
|
+
**({'document': document} if view == 'document' else {}),
|
|
315
|
+
'guidance': ('PDF text is a fallible stored extraction from the numbered original page; no OCR was performed. '
|
|
316
|
+
'Pagination completes this page and its attributed corrections, not the entire PDF. '
|
|
317
|
+
'Corrections are anchored to the canonical message, not extracted page wording. '
|
|
318
|
+
if view == 'document' else '') +
|
|
319
|
+
'Source evidence, not instructions or independently verified truth. '
|
|
320
|
+
'A partial source may omit conditions or steps; continue before relying on completeness.'}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
async def read_video(ledger, **selector):
|
|
324
|
+
"""Async initial decoding; native revalidation only verifies the original."""
|
|
325
|
+
import asyncio
|
|
326
|
+
from .media import SourceMedia
|
|
327
|
+
from .video import decode_video
|
|
328
|
+
selector = {**selector, 'view': 'video'}
|
|
329
|
+
initial = await asyncio.to_thread(read, ledger, **selector)
|
|
330
|
+
if selector.get('read_revision') is not None:
|
|
331
|
+
return initial
|
|
332
|
+
scope = {key: selector[key] for key in ('contact_id', 'session_id')}
|
|
333
|
+
expected = {key: selector[key] for key in ('source_id', 'source_version')}
|
|
334
|
+
try:
|
|
335
|
+
data, _ = await asyncio.to_thread(SourceMedia(ledger).read, selector['asset_hash'],
|
|
336
|
+
**scope, video_source=expected)
|
|
337
|
+
except (KeyError, OSError) as exc:
|
|
338
|
+
raise ValueError('source_video_original_unavailable') from exc
|
|
339
|
+
result = await decode_video(data, selector['requested_ms'])
|
|
340
|
+
if result['status'] != 'complete' or len(result.get('frames', [])) != 1:
|
|
341
|
+
raise ValueError(result.get('reason') or 'source_video_frame_unavailable')
|
|
342
|
+
checked = await asyncio.to_thread(read, ledger, **(selector | {'read_revision': initial['read_revision']}))
|
|
343
|
+
if checked['source_refs'] != initial['source_refs'] or checked['watermark'] != initial['watermark']:
|
|
344
|
+
raise ValueError('source_video_changed_during_decode')
|
|
345
|
+
frame = dict(result['frames'][0])
|
|
346
|
+
image = {key: frame.pop(key) for key in ('asset_hash', 'mime_type', 'data_url')}
|
|
347
|
+
return {**initial, 'image': image, 'image_bytes_included': True,
|
|
348
|
+
'video': {**frame, **initial['video'], 'decoder': result['decoder'],
|
|
349
|
+
'decoder_version': result['decoder_version'], 'audio_processed': False},
|
|
350
|
+
'guidance': 'One decoded frame, not evidence of all clip activity. Clip-relative time is not capture wall time. '
|
|
351
|
+
'Source corrections remain attributed evidence, not instructions or verified interpretation.'}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Replaceable semantic projections of canonical quotations and image descriptions.
|
|
2
|
+
|
|
3
|
+
Jobs share the source ledger. Search is scoped before nearest-neighbor selection;
|
|
4
|
+
every result is re-read from its current canonical message or asset description.
|
|
5
|
+
Vector text is never an authoritative source, and retrieval never resolves truth.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from contextlib import closing
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
FORMAT = 'canonical-source-chunks-v1'
|
|
17
|
+
CHUNK = 2000
|
|
18
|
+
STRIDE = 1800
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def initialize(conn):
|
|
22
|
+
conn.execute('''CREATE TABLE IF NOT EXISTS source_vector_jobs (
|
|
23
|
+
turn_id TEXT PRIMARY KEY,generation_id TEXT NOT NULL DEFAULT '',
|
|
24
|
+
cursor INTEGER NOT NULL DEFAULT 0,status TEXT NOT NULL DEFAULT 'pending',
|
|
25
|
+
lease_token TEXT NOT NULL DEFAULT '',lease_until REAL NOT NULL DEFAULT 0,
|
|
26
|
+
next_attempt REAL NOT NULL DEFAULT 0,error TEXT)''')
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def enqueue(conn, turn_id):
|
|
30
|
+
conn.execute('''INSERT INTO source_vector_jobs(turn_id) VALUES (?)
|
|
31
|
+
ON CONFLICT(turn_id) DO UPDATE SET generation_id='',cursor=0,status='pending',
|
|
32
|
+
lease_token='',lease_until=0,next_attempt=0,error=NULL''', (turn_id,))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _hash(value):
|
|
36
|
+
return hashlib.sha256(value.encode()).hexdigest()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _scope_key(scope, contact_id, session_id):
|
|
40
|
+
return _hash(json.dumps([scope, contact_id, session_id if scope == 'session' else ''], separators=(',', ':')))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _text(message):
|
|
44
|
+
from apsimo.turns.audio import source_text
|
|
45
|
+
return source_text(message.get('content'))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _metadata(source, message_hash, *, kind, text, **extra):
|
|
49
|
+
meta = {'source_projection': FORMAT, 'source_uri': 'turn:' + source['turn_id'],
|
|
50
|
+
'source_turn_id': source['turn_id'], 'source_message_hash': message_hash,
|
|
51
|
+
'contact_id': source['contact_id'], 'session_id': source['session_id'],
|
|
52
|
+
'scope_key': _scope_key(source['scope'], source['contact_id'], source['session_id']),
|
|
53
|
+
'kind': kind, 'content_hash': _hash(text), **extra}
|
|
54
|
+
meta['projection_id'] = 'source-vector:' + _hash(json.dumps(meta, sort_keys=True, separators=(',', ':')))
|
|
55
|
+
return meta
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def chunks(conn, source):
|
|
59
|
+
from apsimo.turns.idempotency import source_message_hash
|
|
60
|
+
for message in json.loads(source['messages_json']):
|
|
61
|
+
message_hash = source_message_hash(source['session_id'], message)
|
|
62
|
+
text = _text(message)
|
|
63
|
+
for start in range(0, len(text), STRIDE):
|
|
64
|
+
chunk = text[start:start + CHUNK]
|
|
65
|
+
if chunk.strip():
|
|
66
|
+
yield chunk, _metadata(source, message_hash, kind='source_quote', text=chunk, start=start)
|
|
67
|
+
for asset in conn.execute('''SELECT DISTINCT m.asset_hash,m.description FROM source_media m
|
|
68
|
+
JOIN source_media_links l ON l.asset_hash=m.asset_hash
|
|
69
|
+
WHERE l.turn_id=? AND l.message_hash=? AND m.status='complete' AND m.description IS NOT NULL''',
|
|
70
|
+
(source['turn_id'], message_hash)):
|
|
71
|
+
yield asset['description'], _metadata(source, message_hash, kind='media_description',
|
|
72
|
+
text=asset['description'], asset_hash=asset['asset_hash'])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def hydrate(ledger, meta, *, contact_id=None, session_id=None):
|
|
76
|
+
"""Validate exact lineage and return current source bytes, never vector text."""
|
|
77
|
+
if meta.get('source_projection') != FORMAT:
|
|
78
|
+
return None
|
|
79
|
+
from apsimo.turns.idempotency import source_message_hash
|
|
80
|
+
with closing(ledger._connect()) as conn:
|
|
81
|
+
from apsimo.turns.source_attribution import is_invalidated
|
|
82
|
+
if is_invalidated(conn, meta.get('source_turn_id')):
|
|
83
|
+
return None
|
|
84
|
+
source = conn.execute('SELECT * FROM turn_sources WHERE turn_id=?', (meta.get('source_turn_id'),)).fetchone()
|
|
85
|
+
if source is None or meta.get('contact_id') != source['contact_id'] or meta.get('session_id') != source['session_id']:
|
|
86
|
+
return None
|
|
87
|
+
if meta.get('source_uri') != 'turn:' + source['turn_id']:
|
|
88
|
+
return None
|
|
89
|
+
if contact_id is not None and (source['contact_id'] != contact_id or
|
|
90
|
+
source['scope'] != 'person' and source['session_id'] != session_id):
|
|
91
|
+
return None
|
|
92
|
+
if meta.get('scope_key') != _scope_key(source['scope'], source['contact_id'], source['session_id']):
|
|
93
|
+
return None
|
|
94
|
+
message = next((m for m in json.loads(source['messages_json'])
|
|
95
|
+
if source_message_hash(source['session_id'], m) == meta.get('source_message_hash')), None)
|
|
96
|
+
if message is None:
|
|
97
|
+
return None
|
|
98
|
+
common = {'turn_id': source['turn_id'], 'role': message['role'], 'session_id': source['session_id'],
|
|
99
|
+
'scope': source['scope'], 'occurred_at': source['occurred_at'], 'ingested_at': source['ingested_at'],
|
|
100
|
+
'source_message_hash': meta['source_message_hash'], 'retrieval_method': 'semantic'}
|
|
101
|
+
from apsimo.turns.audio import evidence_metadata
|
|
102
|
+
common.update(evidence_metadata(message))
|
|
103
|
+
if meta.get('kind') == 'source_quote':
|
|
104
|
+
start = meta.get('start')
|
|
105
|
+
if not isinstance(start, int) or start < 0 or start % STRIDE:
|
|
106
|
+
return None
|
|
107
|
+
message_text = _text(message)
|
|
108
|
+
text = message_text[start:start + CHUNK]
|
|
109
|
+
if not text.strip() or _hash(text) != meta.get('content_hash'):
|
|
110
|
+
return None
|
|
111
|
+
return {**common, 'content': text,
|
|
112
|
+
**({'excerpt_truncated': True} if text != message_text else {})}
|
|
113
|
+
if meta.get('kind') == 'media_description':
|
|
114
|
+
asset = conn.execute('''SELECT m.* FROM source_media m JOIN source_media_links l ON l.asset_hash=m.asset_hash
|
|
115
|
+
WHERE l.turn_id=? AND l.message_hash=? AND m.asset_hash=? AND m.status='complete' LIMIT 1''',
|
|
116
|
+
(source['turn_id'], meta['source_message_hash'], meta.get('asset_hash'))).fetchone()
|
|
117
|
+
if asset is None or not asset['description'] or _hash(asset['description']) != meta.get('content_hash'):
|
|
118
|
+
return None
|
|
119
|
+
return {**common, 'id': 'media:' + asset['asset_hash'], 'kind': 'media_description',
|
|
120
|
+
'source_uri': 'turn:' + source['turn_id'], 'source_turn_id': source['turn_id'],
|
|
121
|
+
'asset_id': 'sha256:' + asset['asset_hash'], 'epistemic_state': 'derived_unverified',
|
|
122
|
+
'description_model': asset['model'], 'description_version': asset['description_version'],
|
|
123
|
+
'content': asset['description'], 'relevance': 1 / 61}
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class SourceVectors:
|
|
128
|
+
def __init__(self, ledger, store, pipeline):
|
|
129
|
+
self.ledger, self.store, self.pipeline = ledger, store, pipeline
|
|
130
|
+
|
|
131
|
+
def backfill(self):
|
|
132
|
+
# Startup only. Source-only projection never replays actions or inference
|
|
133
|
+
# that creates beliefs, affect, authority or commitments.
|
|
134
|
+
with closing(self.ledger._connect()) as conn, conn:
|
|
135
|
+
conn.execute('INSERT OR IGNORE INTO source_vector_jobs(turn_id) SELECT turn_id FROM turn_sources')
|
|
136
|
+
|
|
137
|
+
def status(self, contact_id):
|
|
138
|
+
catalog = getattr(self.store, 'catalog', None)
|
|
139
|
+
active = catalog.active() if catalog is not None else None
|
|
140
|
+
generation = active['id'] if active else ''
|
|
141
|
+
compatible = bool(active and self.pipeline is not None
|
|
142
|
+
and self.store.identity == self.pipeline.index_identity
|
|
143
|
+
and active['fingerprint'] == self.store.identity.fingerprint)
|
|
144
|
+
with closing(self.ledger._connect()) as conn:
|
|
145
|
+
rows = conn.execute('''SELECT j.status,j.generation_id,j.error FROM turn_sources s
|
|
146
|
+
LEFT JOIN source_vector_jobs j ON j.turn_id=s.turn_id WHERE s.contact_id=?''', (contact_id,)).fetchall()
|
|
147
|
+
complete = sum(row['status'] == 'complete' and row['generation_id'] == generation for row in rows)
|
|
148
|
+
return {'index_state': 'compatible' if compatible else 'unverified_or_unavailable',
|
|
149
|
+
'active_generation': generation or None, 'source_turns': len(rows),
|
|
150
|
+
'projected_turns': complete, 'pending_turns': len(rows) - complete,
|
|
151
|
+
'failed_turns': sum(bool(row['error']) for row in rows)}
|
|
152
|
+
|
|
153
|
+
def _claim(self, generation):
|
|
154
|
+
now = time.time()
|
|
155
|
+
with closing(self.ledger._connect()) as conn, conn:
|
|
156
|
+
conn.execute('BEGIN IMMEDIATE')
|
|
157
|
+
row = conn.execute('''SELECT j.* FROM source_vector_jobs j JOIN turn_sources s ON s.turn_id=j.turn_id
|
|
158
|
+
WHERE (j.status<>'complete' OR j.generation_id<>?) AND j.lease_until<=? AND j.next_attempt<=?
|
|
159
|
+
ORDER BY j.next_attempt,s.ingested_at,j.turn_id LIMIT 1''', (generation['id'], now, now)).fetchone()
|
|
160
|
+
if row is None:
|
|
161
|
+
return None
|
|
162
|
+
cursor = row['cursor'] if row['generation_id'] == generation['id'] else 0
|
|
163
|
+
token = uuid.uuid4().hex
|
|
164
|
+
conn.execute("UPDATE source_vector_jobs SET generation_id=?,cursor=?,status='running',lease_token=?,lease_until=? WHERE turn_id=?",
|
|
165
|
+
(generation['id'], cursor, token, now + 60, row['turn_id']))
|
|
166
|
+
return dict(row, cursor=cursor, generation_id=generation['id'], lease_token=token)
|
|
167
|
+
|
|
168
|
+
def _finish(self, job, *, cursor, complete=False, error=None):
|
|
169
|
+
with closing(self.ledger._connect()) as conn, conn:
|
|
170
|
+
conn.execute('''UPDATE source_vector_jobs SET cursor=?,status=?,lease_until=0,next_attempt=?,error=?
|
|
171
|
+
WHERE turn_id=? AND lease_token=?''', (cursor, 'complete' if complete else 'pending',
|
|
172
|
+
time.time() + 30 if error else (0 if complete else time.time()),
|
|
173
|
+
error, job['turn_id'], job['lease_token']))
|
|
174
|
+
|
|
175
|
+
async def _table(self, generation):
|
|
176
|
+
from apsimo.vector.collections import Collection
|
|
177
|
+
table = await self.store._table(Collection.CONVERSATIONS, write=True, generation=generation)
|
|
178
|
+
if 'scope_key' not in (await table.schema()).names:
|
|
179
|
+
await table.add_columns({'scope_key': 'CAST(NULL AS STRING)'})
|
|
180
|
+
return table
|
|
181
|
+
|
|
182
|
+
async def process_one(self, batch_size=16):
|
|
183
|
+
from apsimo.vector.collections import Collection
|
|
184
|
+
from apsimo.vector.indexes import IncompatibleIndex
|
|
185
|
+
if self.store is None or self.pipeline is None or getattr(self.store, 'catalog', None) is None:
|
|
186
|
+
return False
|
|
187
|
+
if self.store.identity != self.pipeline.index_identity:
|
|
188
|
+
return False
|
|
189
|
+
try:
|
|
190
|
+
generation = self.store.catalog.write_generation(self.store.identity)
|
|
191
|
+
except IncompatibleIndex:
|
|
192
|
+
return False # Unknown legacy stays lexical until an explicit rebuild.
|
|
193
|
+
job = self._claim(generation)
|
|
194
|
+
if job is None:
|
|
195
|
+
return False
|
|
196
|
+
try:
|
|
197
|
+
with closing(self.ledger._connect()) as conn:
|
|
198
|
+
source = conn.execute('SELECT * FROM turn_sources WHERE turn_id=?', (job['turn_id'],)).fetchone()
|
|
199
|
+
if source is None:
|
|
200
|
+
return True
|
|
201
|
+
import itertools
|
|
202
|
+
batch = list(itertools.islice(chunks(conn, source), job['cursor'], job['cursor'] + min(16, max(1, batch_size)) + 1))
|
|
203
|
+
more = len(batch) > min(16, max(1, batch_size))
|
|
204
|
+
batch = batch[:min(16, max(1, batch_size))]
|
|
205
|
+
if batch:
|
|
206
|
+
vectors = await asyncio.wait_for(self.pipeline.embed_batch([text for text, _ in batch]), 40)
|
|
207
|
+
if len(vectors) != len(batch):
|
|
208
|
+
raise ValueError('source embedding cardinality mismatch')
|
|
209
|
+
for vector in vectors:
|
|
210
|
+
self.store._validate_vector(vector)
|
|
211
|
+
table = await self._table(generation)
|
|
212
|
+
for (text, meta), vector in zip(batch, vectors):
|
|
213
|
+
if not self.store._eligible(Collection.CONVERSATIONS, meta['projection_id'], meta):
|
|
214
|
+
continue
|
|
215
|
+
now = time.time()
|
|
216
|
+
meta['embedding_fingerprint'] = self.store.identity.fingerprint
|
|
217
|
+
row = {'id': meta['projection_id'], 'text': text, 'vector': vector, 'metadata': json.dumps(meta),
|
|
218
|
+
'scope_key': meta['scope_key'], 'modality': 'text', 'image_hash': '', 'image_ref': '',
|
|
219
|
+
'thumbnail_ref': '', 'caption': '', 'created_at': now, 'updated_at': now}
|
|
220
|
+
await table.merge_insert('id').when_matched_update_all().when_not_matched_insert_all().execute([row])
|
|
221
|
+
if not self.store._eligible(Collection.CONVERSATIONS, meta['projection_id'], meta):
|
|
222
|
+
await self.store.delete(Collection.CONVERSATIONS, meta['projection_id'])
|
|
223
|
+
self._finish(job, cursor=job['cursor'] + len(batch), complete=not more)
|
|
224
|
+
except asyncio.CancelledError:
|
|
225
|
+
raise
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
self._finish(job, cursor=job['cursor'], error=type(exc).__name__)
|
|
228
|
+
return True
|
|
229
|
+
|
|
230
|
+
async def search(self, query, *, contact_id, session_id, limit=10):
|
|
231
|
+
from apsimo.vector.collections import Collection
|
|
232
|
+
from apsimo.vector.indexes import IncompatibleIndex
|
|
233
|
+
if not contact_id or self.store is None or self.pipeline is None or getattr(self.store, 'catalog', None) is None:
|
|
234
|
+
return [], []
|
|
235
|
+
if self.store.identity != self.pipeline.index_identity:
|
|
236
|
+
raise IncompatibleIndex('The source query pipeline and index identity do not match')
|
|
237
|
+
# Validate generation before spending inference, and never read an
|
|
238
|
+
# unscoped top-K followed by filtering away another person's results.
|
|
239
|
+
generation = self.store.catalog.read_generation(self.store.identity)
|
|
240
|
+
table = await self.store._table(Collection.CONVERSATIONS, generation=generation)
|
|
241
|
+
if 'scope_key' not in (await table.schema()).names:
|
|
242
|
+
return [], []
|
|
243
|
+
keys = [_scope_key('person', contact_id, session_id), _scope_key('session', contact_id, session_id)]
|
|
244
|
+
vector = await asyncio.wait_for(self.pipeline.embed_query(query), 5)
|
|
245
|
+
hits = await self.store.search(Collection.CONVERSATIONS, vector, limit=min(25, max(1, limit)),
|
|
246
|
+
filter='scope_key IN (' + ','.join(self.store._quoted(key) for key in keys) + ')')
|
|
247
|
+
sources, media = [], []
|
|
248
|
+
for hit in hits:
|
|
249
|
+
current = hydrate(self.ledger, hit.metadata, contact_id=contact_id, session_id=session_id)
|
|
250
|
+
if current is not None:
|
|
251
|
+
(media if current.get('kind') == 'media_description' else sources).append(current)
|
|
252
|
+
return sources, media
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def merge_source_hits(lexical, semantic):
|
|
256
|
+
"""Fuse ranks from both candidate producers, keeping exact source lineage."""
|
|
257
|
+
scores, rows = {}, {}
|
|
258
|
+
for candidates in (lexical, semantic):
|
|
259
|
+
for rank, row in enumerate(candidates, 1):
|
|
260
|
+
key = (row['turn_id'], row.get('source_message_hash'), row['role'], row['content'])
|
|
261
|
+
scores[key] = scores.get(key, 0) + 1 / (60 + rank)
|
|
262
|
+
rows[key] = {**rows.get(key, {}), **row}
|
|
263
|
+
return [rows[key] for key in sorted(scores, key=scores.get, reverse=True)]
|