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
apsimo/turns/audio.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Bounded supplied audio and fallible transcript segments, never remote fetches."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import hashlib
|
|
7
|
+
import io
|
|
8
|
+
import math
|
|
9
|
+
import wave
|
|
10
|
+
|
|
11
|
+
MAX_AUDIO_BYTES = 4 * 1024 * 1024
|
|
12
|
+
MAX_AUDIO_SECONDS = 60
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def decode_audio(block):
|
|
16
|
+
item = block.get('input_audio')
|
|
17
|
+
if (not isinstance(item, dict) or set(item) != {'data', 'format'}
|
|
18
|
+
or item['format'] != 'wav' or not isinstance(item['data'], str)
|
|
19
|
+
or len(item['data']) > MAX_AUDIO_BYTES * 4 // 3 + 16):
|
|
20
|
+
raise ValueError('unsupported_or_oversized_audio')
|
|
21
|
+
data = base64.b64decode(item['data'], validate=True)
|
|
22
|
+
if len(data) > MAX_AUDIO_BYTES:
|
|
23
|
+
raise ValueError('oversized_audio')
|
|
24
|
+
with wave.open(io.BytesIO(data), 'rb') as clip:
|
|
25
|
+
rate, frames = clip.getframerate(), clip.getnframes()
|
|
26
|
+
if (clip.getnchannels() != 1 or clip.getsampwidth() != 2 or clip.getcomptype() != 'NONE'
|
|
27
|
+
or not 8000 <= rate <= 48000 or not 0 < frames <= rate * MAX_AUDIO_SECONDS
|
|
28
|
+
or len(clip.readframes(frames + 1)) != frames * 2):
|
|
29
|
+
raise ValueError('unsupported_audio_encoding_or_duration')
|
|
30
|
+
return data, {'sample_rate': rate, 'channels': 1, 'sample_width_bytes': 2,
|
|
31
|
+
'duration_ms': frames * 1000 / rate, 'frame_count': frames}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def transcript(block, data, duration_ms):
|
|
35
|
+
"""Validate supplied recognition lineage; clocks are not inferred from text."""
|
|
36
|
+
if (not isinstance(block, dict) or set(block) != {'type', 'audio_sha256', 'segments',
|
|
37
|
+
'recognizer', 'received_at', 'captured_at'} or block['type'] != 'audio_transcript'
|
|
38
|
+
or block['audio_sha256'] != hashlib.sha256(data).hexdigest()):
|
|
39
|
+
raise ValueError('audio_transcript_parent_mismatch')
|
|
40
|
+
segments = block['segments']
|
|
41
|
+
if not isinstance(segments, list) or not 1 <= len(segments) <= 32:
|
|
42
|
+
raise ValueError('unsupported_transcript_segments')
|
|
43
|
+
previous = 0
|
|
44
|
+
for segment in segments:
|
|
45
|
+
if (not isinstance(segment, dict) or set(segment) != {'start_ms', 'end_ms', 'text'}
|
|
46
|
+
or not isinstance(segment['text'], str) or not 1 <= len(segment['text']) <= 12000
|
|
47
|
+
or any(type(segment[key]) not in (int, float) or not math.isfinite(segment[key])
|
|
48
|
+
for key in ('start_ms', 'end_ms'))
|
|
49
|
+
or not previous <= segment['start_ms'] < segment['end_ms'] <= duration_ms + .001):
|
|
50
|
+
raise ValueError('invalid_transcript_segment')
|
|
51
|
+
previous = segment['end_ms']
|
|
52
|
+
for key in ('received_at', 'captured_at'):
|
|
53
|
+
value = block[key]
|
|
54
|
+
if value is None and key == 'captured_at':
|
|
55
|
+
continue
|
|
56
|
+
if not isinstance(value, str) or len(value) > 64 or datetime.fromisoformat(value.replace('Z', '+00:00')).tzinfo is None:
|
|
57
|
+
raise ValueError('invalid_audio_clock')
|
|
58
|
+
model = block['recognizer']
|
|
59
|
+
if (not isinstance(model, dict) or set(model) != {'model_id', 'model_revision'}
|
|
60
|
+
or any(not isinstance(value, str) or not 1 <= len(value) <= 256 for value in model.values())):
|
|
61
|
+
raise ValueError('invalid_recognizer_provenance')
|
|
62
|
+
return {**block, 'asset_id': 'sha256:' + block['audio_sha256'],
|
|
63
|
+
'epistemic_state': 'derived_unverified', 'timing_basis': 'supplied_clip_offsets',
|
|
64
|
+
'confidence': None}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _render(content):
|
|
68
|
+
"""One rendering for source indexing, exact claim spans and recall."""
|
|
69
|
+
if isinstance(content, str):
|
|
70
|
+
return content, []
|
|
71
|
+
parts, segments, length = [], [], 0
|
|
72
|
+
|
|
73
|
+
def append(text):
|
|
74
|
+
nonlocal length
|
|
75
|
+
start = length + bool(parts)
|
|
76
|
+
parts.append(text)
|
|
77
|
+
length = start + len(text)
|
|
78
|
+
return start
|
|
79
|
+
|
|
80
|
+
blocks = content if isinstance(content, list) else []
|
|
81
|
+
for index, block in enumerate(blocks):
|
|
82
|
+
if not isinstance(block, dict):
|
|
83
|
+
continue
|
|
84
|
+
if block.get('type') in {'text', 'input_text', 'output_text'} and isinstance(block.get('text'), str):
|
|
85
|
+
append(block['text'])
|
|
86
|
+
elif block.get('type') == 'audio_transcript' and block.get('epistemic_state') == 'derived_unverified':
|
|
87
|
+
parent = blocks[index - 1] if index else {}
|
|
88
|
+
owned = (isinstance(parent, dict) and parent.get('type') == 'audio'
|
|
89
|
+
and parent.get('asset_id') == block['asset_id'])
|
|
90
|
+
for segment_index, segment in enumerate(block.get('segments', [])):
|
|
91
|
+
prefix = (f"Unverified machine transcript [{segment['start_ms'] / 1000:.3f}.."
|
|
92
|
+
f"{segment['end_ms'] / 1000:.3f}s of {block['asset_id']}]: ")
|
|
93
|
+
start = append(prefix + segment['text']) + len(prefix)
|
|
94
|
+
if owned:
|
|
95
|
+
segments.append({'block_index': index, 'segment_index': segment_index,
|
|
96
|
+
'asset_id': block['asset_id'], 'start_ms': segment['start_ms'], 'end_ms': segment['end_ms'],
|
|
97
|
+
'source_start': start, 'source_end': start + len(segment['text']),
|
|
98
|
+
**{key: block[key] for key in ('recognizer', 'received_at', 'captured_at',
|
|
99
|
+
'timing_basis', 'confidence')}})
|
|
100
|
+
return '\n'.join(parts), segments
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def source_text(content):
|
|
104
|
+
"""Text-only recall of retained evidence; raw audio never enters prompts."""
|
|
105
|
+
return _render(content)[0]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def claim_message(message):
|
|
109
|
+
"""An internal extraction view, never a second canonical user message.
|
|
110
|
+
|
|
111
|
+
Complete surrounding text remains available to interpret narrative scope.
|
|
112
|
+
Only retained, paired ASR word ranges become eligible derived evidence.
|
|
113
|
+
"""
|
|
114
|
+
if isinstance(message.get('content'), str):
|
|
115
|
+
return message
|
|
116
|
+
text, segments = _render(message.get('content'))
|
|
117
|
+
return {**message, 'content': text, '_audio_segments': segments} if segments else None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def claim_basis(message, start, end):
|
|
121
|
+
for segment in message.get('_audio_segments', []):
|
|
122
|
+
if segment['source_start'] <= start < end <= segment['source_end']:
|
|
123
|
+
return {'epistemic_state': 'derived_unverified', 'source_modality': 'audio_transcript',
|
|
124
|
+
'segment': {**segment, 'evidence_start': start - segment['source_start'],
|
|
125
|
+
'evidence_end': end - segment['source_start']}}
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def evidence_metadata(message):
|
|
130
|
+
content = message.get('content')
|
|
131
|
+
if isinstance(content, list) and any(isinstance(block, dict) and block.get('type') == 'audio_transcript'
|
|
132
|
+
and block.get('epistemic_state') == 'derived_unverified' for block in content):
|
|
133
|
+
return {'epistemic_state': 'derived_unverified', 'source_modality': 'audio_transcript'}
|
|
134
|
+
return {}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""Bounded PDF extraction from owned bytes, never paths supplied by a caller.
|
|
2
|
+
|
|
3
|
+
This file also runs as a standalone isolated child. Keep imports at module load
|
|
4
|
+
in the standard library so resource limits precede loading the PDF parser.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import base64
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
MAX_DOCUMENT_BYTES = 4 * 1024 * 1024
|
|
17
|
+
MAX_PAGES = 64
|
|
18
|
+
MAX_PAGE_STREAM_BYTES = 2 * 1024 * 1024
|
|
19
|
+
MAX_PAGE_CHARS = 32000
|
|
20
|
+
MAX_TEXT_CHARS = 200000
|
|
21
|
+
MAX_RESULT_BYTES = 6 * MAX_TEXT_CHARS + 65536
|
|
22
|
+
MAX_PARSE_SECONDS = 15
|
|
23
|
+
MAX_MEMORY_BYTES = 384 * 1024 * 1024
|
|
24
|
+
MEMORY_SAMPLE_INTERVAL_MS = 25
|
|
25
|
+
VERSION = 'source-pdf-text-v1'
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def decode_document(block):
|
|
29
|
+
"""Only explicit inline PDF bytes are supported by this ingestion contract."""
|
|
30
|
+
item = block.get('input_document')
|
|
31
|
+
if (set(block) != {'type', 'input_document'} or not isinstance(item, dict)
|
|
32
|
+
or set(item) != {'mime_type', 'data'} or item.get('mime_type') != 'application/pdf'
|
|
33
|
+
or not isinstance(item.get('data'), str)):
|
|
34
|
+
raise ValueError('unsupported_document_input')
|
|
35
|
+
if len(item['data']) > ((MAX_DOCUMENT_BYTES + 2) // 3) * 4:
|
|
36
|
+
raise ValueError('document_bytes_exceed_limit')
|
|
37
|
+
try:
|
|
38
|
+
data = base64.b64decode(item['data'], validate=True)
|
|
39
|
+
except (ValueError, TypeError):
|
|
40
|
+
raise ValueError('invalid_document_encoding') from None
|
|
41
|
+
if len(data) > MAX_DOCUMENT_BYTES:
|
|
42
|
+
raise ValueError('document_bytes_exceed_limit')
|
|
43
|
+
# This is a bounded admission signature check, not a PDF parser. Malformed
|
|
44
|
+
# PDF-looking originals remain retained with an honest extraction failure.
|
|
45
|
+
if not data.startswith(b'%PDF-'):
|
|
46
|
+
raise ValueError('unsupported_document_format')
|
|
47
|
+
return data
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def disposition(status, reason=None, *, parser_version=None, page_count=None, pages=None):
|
|
51
|
+
return {'version': VERSION, 'parser': 'pypdf', 'parser_version': parser_version,
|
|
52
|
+
'status': status, 'reason': reason, 'page_count': page_count,
|
|
53
|
+
'pages': pages or [], 'ocr_performed': False,
|
|
54
|
+
'epistemic_state': 'derived_unverified'}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract(data):
|
|
58
|
+
"""Called only inside the resource-limited child, including in tests."""
|
|
59
|
+
try:
|
|
60
|
+
import pypdf
|
|
61
|
+
except ImportError:
|
|
62
|
+
return disposition('failed', 'parser_unavailable')
|
|
63
|
+
version = pypdf.__version__
|
|
64
|
+
if not data.startswith(b'%PDF-') or len(data) > MAX_DOCUMENT_BYTES:
|
|
65
|
+
return disposition('failed', 'invalid_retained_document', parser_version=version)
|
|
66
|
+
try:
|
|
67
|
+
reader = pypdf.PdfReader(io.BytesIO(data), strict=True)
|
|
68
|
+
if reader.is_encrypted:
|
|
69
|
+
return disposition('unsupported', 'encrypted_pdf', parser_version=version)
|
|
70
|
+
count = len(reader.pages)
|
|
71
|
+
if not 0 < count <= MAX_PAGES:
|
|
72
|
+
return disposition('unsupported', 'page_count_exceeds_limit' if count else 'empty_pdf',
|
|
73
|
+
parser_version=version, page_count=count)
|
|
74
|
+
pages, total = [], 0
|
|
75
|
+
for number, page in enumerate(reader.pages, 1):
|
|
76
|
+
contents = page.get_contents()
|
|
77
|
+
# Decompression itself is inside the memory/CPU fence.
|
|
78
|
+
if contents is not None and len(contents.get_data()) > MAX_PAGE_STREAM_BYTES:
|
|
79
|
+
return disposition('unsupported', 'page_stream_exceeds_limit', parser_version=version,
|
|
80
|
+
page_count=count)
|
|
81
|
+
text = page.extract_text() or ''
|
|
82
|
+
total += len(text)
|
|
83
|
+
if len(text) > MAX_PAGE_CHARS or total > MAX_TEXT_CHARS:
|
|
84
|
+
return disposition('unsupported', 'extracted_text_exceeds_limit', parser_version=version,
|
|
85
|
+
page_count=count)
|
|
86
|
+
pages.append({'page': number, 'text': text,
|
|
87
|
+
'status': 'text' if text.strip() else 'no_extractable_text'})
|
|
88
|
+
text_pages = sum(page['status'] == 'text' for page in pages)
|
|
89
|
+
return disposition('complete' if text_pages == count else 'partial' if text_pages else 'unsupported',
|
|
90
|
+
None if text_pages == count else 'no_extractable_text_on_some_pages' if text_pages
|
|
91
|
+
else 'no_extractable_text', parser_version=version, page_count=count, pages=pages)
|
|
92
|
+
except MemoryError:
|
|
93
|
+
return disposition('unsupported', 'parser_memory_limit', parser_version=version)
|
|
94
|
+
except Exception:
|
|
95
|
+
# Parser errors can contain document text. Persist a code, not that text.
|
|
96
|
+
return disposition('failed', 'invalid_or_unreadable_pdf', parser_version=version)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _child():
|
|
100
|
+
logging.disable(logging.CRITICAL)
|
|
101
|
+
memory_control = 'address_space'
|
|
102
|
+
try:
|
|
103
|
+
import resource
|
|
104
|
+
resource.setrlimit(resource.RLIMIT_CPU, (10, 10))
|
|
105
|
+
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
|
|
106
|
+
try:
|
|
107
|
+
resource.setrlimit(resource.RLIMIT_AS, (MAX_MEMORY_BYTES, MAX_MEMORY_BYTES))
|
|
108
|
+
except (ValueError, OSError):
|
|
109
|
+
if sys.platform != 'darwin' or sys.argv[1:] != ['--rss-guarded']:
|
|
110
|
+
raise
|
|
111
|
+
memory_control = 'sampled_rss'
|
|
112
|
+
except (ImportError, ValueError, OSError):
|
|
113
|
+
result = disposition('unsupported', 'parser_resource_limits_unavailable')
|
|
114
|
+
else:
|
|
115
|
+
# The guarded parent samples this PID before sending stdin. No parser
|
|
116
|
+
# import or PDF decoding starts until that parent closes the pipe.
|
|
117
|
+
result = _extract(sys.stdin.buffer.read(MAX_DOCUMENT_BYTES + 1))
|
|
118
|
+
result.update(_memory_metadata(memory_control))
|
|
119
|
+
sys.stdout.buffer.write(json.dumps(result, ensure_ascii=True).encode())
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _memory_metadata(control):
|
|
123
|
+
return {'memory_control': control, 'memory_limit_bytes': MAX_MEMORY_BYTES,
|
|
124
|
+
'memory_sample_interval_ms': MEMORY_SAMPLE_INTERVAL_MS if control == 'sampled_rss' else None,
|
|
125
|
+
'hard_limit': control == 'address_space'}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class _MemoryGuardError(RuntimeError):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _sample_rss(target):
|
|
133
|
+
import psutil
|
|
134
|
+
try:
|
|
135
|
+
if not target.is_running():
|
|
136
|
+
raise psutil.NoSuchProcess(target.pid)
|
|
137
|
+
rss = target.memory_info().rss
|
|
138
|
+
if type(rss) is not int or rss < 0:
|
|
139
|
+
raise ValueError('invalid RSS')
|
|
140
|
+
except psutil.NoSuchProcess as exc:
|
|
141
|
+
raise _MemoryGuardError('parser_memory_monitor_exited') from exc
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
raise _MemoryGuardError('parser_memory_monitor_failed') from exc
|
|
144
|
+
if rss > MAX_MEMORY_BYTES:
|
|
145
|
+
raise _MemoryGuardError('parser_memory_limit')
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
async def _watch_rss(process, target):
|
|
149
|
+
# Best-effort resident memory samples; scheduling and allocations can
|
|
150
|
+
# exceed this interval/threshold. This is not an OS memory allocation cap.
|
|
151
|
+
while process.returncode is None:
|
|
152
|
+
try:
|
|
153
|
+
_sample_rss(target)
|
|
154
|
+
except _MemoryGuardError as exc:
|
|
155
|
+
if str(exc) == 'parser_memory_monitor_exited':
|
|
156
|
+
await asyncio.sleep(0) # Let a genuine exit notification settle.
|
|
157
|
+
if process.returncode is not None:
|
|
158
|
+
return
|
|
159
|
+
raise
|
|
160
|
+
await asyncio.sleep(MEMORY_SAMPLE_INTERVAL_MS / 1000)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
async def extract_document(data):
|
|
164
|
+
"""No model, OCR, fetch, embedded action, or file reference is executed."""
|
|
165
|
+
guarded = sys.platform == 'darwin'
|
|
166
|
+
if guarded:
|
|
167
|
+
try:
|
|
168
|
+
import psutil
|
|
169
|
+
except ImportError:
|
|
170
|
+
return disposition('unsupported', 'parser_memory_monitor_unavailable')
|
|
171
|
+
process = await asyncio.create_subprocess_exec(
|
|
172
|
+
sys.executable, '-I', str(Path(__file__).resolve()),
|
|
173
|
+
*(['--rss-guarded'] if guarded else []),
|
|
174
|
+
stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
|
|
175
|
+
stderr=asyncio.subprocess.DEVNULL)
|
|
176
|
+
communication = watcher = target = None
|
|
177
|
+
guard_metadata = {}
|
|
178
|
+
try:
|
|
179
|
+
async with asyncio.timeout(MAX_PARSE_SECONDS):
|
|
180
|
+
if guarded:
|
|
181
|
+
try:
|
|
182
|
+
target = psutil.Process(process.pid)
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
raise _MemoryGuardError('parser_memory_monitor_unavailable') from exc
|
|
185
|
+
guard_metadata = _memory_metadata('sampled_rss')
|
|
186
|
+
_sample_rss(target) # Must succeed before any PDF bytes go in.
|
|
187
|
+
watcher = asyncio.create_task(_watch_rss(process, target))
|
|
188
|
+
communication = asyncio.create_task(process.communicate(data))
|
|
189
|
+
if watcher is not None:
|
|
190
|
+
await asyncio.wait((communication, watcher), return_when=asyncio.FIRST_COMPLETED)
|
|
191
|
+
if watcher.done():
|
|
192
|
+
watcher.result() # A breach takes precedence over output.
|
|
193
|
+
output, _ = await asyncio.shield(communication)
|
|
194
|
+
if process.returncode:
|
|
195
|
+
return disposition('failed', 'parser_process_failed_or_resource_limit') | guard_metadata
|
|
196
|
+
if len(output) > MAX_RESULT_BYTES:
|
|
197
|
+
return disposition('failed', 'parser_output_exceeds_limit') | guard_metadata
|
|
198
|
+
result = json.loads(output)
|
|
199
|
+
if result.get('version') != VERSION or result.get('status') not in {'complete', 'partial', 'unsupported', 'failed'}:
|
|
200
|
+
raise ValueError('invalid parser result')
|
|
201
|
+
return result
|
|
202
|
+
except asyncio.TimeoutError:
|
|
203
|
+
return disposition('failed', 'parser_time_limit') | guard_metadata
|
|
204
|
+
except _MemoryGuardError as exc:
|
|
205
|
+
reason = 'parser_memory_monitor_failed' if str(exc) == 'parser_memory_monitor_exited' else str(exc)
|
|
206
|
+
return disposition('unsupported', reason) | guard_metadata
|
|
207
|
+
except (ValueError, TypeError):
|
|
208
|
+
return disposition('failed', 'invalid_parser_result') | guard_metadata
|
|
209
|
+
finally:
|
|
210
|
+
try:
|
|
211
|
+
if process.returncode is None:
|
|
212
|
+
try:
|
|
213
|
+
if target is None:
|
|
214
|
+
process.kill()
|
|
215
|
+
else:
|
|
216
|
+
try:
|
|
217
|
+
target.kill() # Checks this bound PID's identity.
|
|
218
|
+
except psutil.NoSuchProcess:
|
|
219
|
+
pass
|
|
220
|
+
except psutil.Error:
|
|
221
|
+
process.kill() # Owned-child Popen also polls first.
|
|
222
|
+
except ProcessLookupError:
|
|
223
|
+
pass
|
|
224
|
+
finally:
|
|
225
|
+
if watcher is not None:
|
|
226
|
+
watcher.cancel()
|
|
227
|
+
if communication is None:
|
|
228
|
+
communication = asyncio.create_task(process.communicate())
|
|
229
|
+
# Drain pipes while reaping, including cancellation during output.
|
|
230
|
+
await asyncio.gather(communication, *([watcher] if watcher is not None else []), return_exceptions=True)
|
|
231
|
+
await process.wait()
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
if __name__ == '__main__':
|
|
235
|
+
_child()
|