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,152 @@
|
|
|
1
|
+
"""Connector framework base types (cognition item 2, Phase C).
|
|
2
|
+
|
|
3
|
+
A connector is a READ-ONLY sense: it polls an external source on its own
|
|
4
|
+
cadence and normalizes what it sees into Observations. Everything downstream
|
|
5
|
+
(the observation store that feeds the initiative engine, the world-model
|
|
6
|
+
populator, belief maintenance) consumes the same normalized shape, so a new
|
|
7
|
+
source is just a new poll+normalize -- no bespoke ingest path.
|
|
8
|
+
|
|
9
|
+
All configuration comes from the environment (per-connector
|
|
10
|
+
COLONY_CONNECTOR_<NAME>_<KEY>); no credentials ever live in code or tests.
|
|
11
|
+
Push-style ingress is intentionally NOT built here: the host framework's
|
|
12
|
+
webhook adapter already handles HMAC-authenticated inbound POSTs. These are the
|
|
13
|
+
pull-style connectors (IMAP, calendar, filesystem, metrics pull).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
import time
|
|
20
|
+
from abc import ABC, abstractmethod
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any, Dict, List, Optional
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class EntityHint:
|
|
27
|
+
"""A structured entity a connector recognized in an observation."""
|
|
28
|
+
kind: str # person | company | event | document | project | ...
|
|
29
|
+
name: str
|
|
30
|
+
external_ids: Dict[str, str] = field(default_factory=dict)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class Observation:
|
|
35
|
+
"""The normalized unit every connector emits."""
|
|
36
|
+
domain: str # email | calendar | document | metrics | ...
|
|
37
|
+
external_id: str # stable id within the domain (dedup key)
|
|
38
|
+
ts: float # source event time (epoch seconds)
|
|
39
|
+
payload: Dict[str, Any] = field(default_factory=dict)
|
|
40
|
+
entities: List[EntityHint] = field(default_factory=list)
|
|
41
|
+
text: str = "" # natural-language render for world extraction
|
|
42
|
+
|
|
43
|
+
def to_store_row(self) -> Dict[str, Any]:
|
|
44
|
+
"""Shape for ObservationStore.record_batch (one snapshot per entity)."""
|
|
45
|
+
return {"entity_id": self.external_id,
|
|
46
|
+
"payload": {**self.payload, "_entities": [
|
|
47
|
+
{"kind": e.kind, "name": e.name, "external_ids": e.external_ids}
|
|
48
|
+
for e in self.entities], "_text": self.text},
|
|
49
|
+
"observed_at": _iso(self.ts)}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _iso(ts: float):
|
|
53
|
+
from datetime import datetime, timezone
|
|
54
|
+
try:
|
|
55
|
+
return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat()
|
|
56
|
+
except (ValueError, OverflowError, OSError):
|
|
57
|
+
return datetime.now(timezone.utc).isoformat()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _slug(value: str) -> str:
|
|
61
|
+
"""Lowercase [a-z0-9_] account slug — it becomes part of env var names
|
|
62
|
+
and secrets paths, so anything else is dropped."""
|
|
63
|
+
return "".join(c for c in (value or "").lower() if c.isalnum() or c == "_")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ConnectorConfig:
|
|
67
|
+
"""Config for one connector, resolved env-first then secrets-store.
|
|
68
|
+
|
|
69
|
+
Precedence: COLONY_CONNECTOR_<NAME>_<KEY> env (an explicit deployment
|
|
70
|
+
override always wins) → the Colony secrets store at
|
|
71
|
+
``connector/<name>/<key>`` → default. The secrets fallback means a
|
|
72
|
+
credential (IMAP password, ICS URL) is entered ONCE via the secrets
|
|
73
|
+
API/CLI, lives encrypted in the store, and survives service redeploys
|
|
74
|
+
without ever touching a plist or unit file."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, name: str) -> None:
|
|
77
|
+
self._name = name.lower()
|
|
78
|
+
self._prefix = f"COLONY_CONNECTOR_{name.upper()}_"
|
|
79
|
+
|
|
80
|
+
def get(self, key: str, default: str = "") -> str:
|
|
81
|
+
env = os.environ.get(self._prefix + key.upper())
|
|
82
|
+
if env:
|
|
83
|
+
return env
|
|
84
|
+
try:
|
|
85
|
+
from apsimo.api.routers import host as _h
|
|
86
|
+
mgr = getattr(_h, "_secrets_manager", None)
|
|
87
|
+
if mgr is not None:
|
|
88
|
+
v = mgr.get(f"connector/{self._name}/{key.lower()}")
|
|
89
|
+
if v is not None:
|
|
90
|
+
return v
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
return default
|
|
94
|
+
|
|
95
|
+
def get_int(self, key: str, default: int) -> int:
|
|
96
|
+
try:
|
|
97
|
+
return int(self.get(key, str(default)))
|
|
98
|
+
except (TypeError, ValueError):
|
|
99
|
+
return default
|
|
100
|
+
|
|
101
|
+
def get_bool(self, key: str, default: bool = False) -> bool:
|
|
102
|
+
v = self.get(key, "1" if default else "0").strip().lower()
|
|
103
|
+
return v in ("1", "true", "yes", "on")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Connector(ABC):
|
|
107
|
+
"""Read-only pull connector. Subclasses implement fetch + normalize."""
|
|
108
|
+
|
|
109
|
+
name: str = "connector"
|
|
110
|
+
domain: str = "generic"
|
|
111
|
+
default_poll_secs: int = 900
|
|
112
|
+
|
|
113
|
+
def __init__(self, account: str = "") -> None:
|
|
114
|
+
# An account-scoped instance ("imap" + account "aevonix") reads its
|
|
115
|
+
# own namespace: secrets connector/imap_aevonix/* and env
|
|
116
|
+
# COLONY_CONNECTOR_IMAP_AEVONIX_*. Several instances of one connector
|
|
117
|
+
# class run side by side, one per account.
|
|
118
|
+
self.account = _slug(account)
|
|
119
|
+
if self.account:
|
|
120
|
+
self.name = f"{self.name}_{self.account}"
|
|
121
|
+
self.config = ConnectorConfig(self.name)
|
|
122
|
+
self._last_poll: float = 0.0
|
|
123
|
+
|
|
124
|
+
# -- lifecycle knobs (env/secrets-driven) -----------------------------
|
|
125
|
+
@property
|
|
126
|
+
def enabled(self) -> bool:
|
|
127
|
+
# An account listed in the connector's ACCOUNTS key is intended ON;
|
|
128
|
+
# it can still be turned off explicitly with its own enabled=false.
|
|
129
|
+
return self.config.get_bool(
|
|
130
|
+
"ENABLED", getattr(self, "_default_enabled", False))
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def poll_secs(self) -> int:
|
|
134
|
+
return self.config.get_int("POLL_SECS", self.default_poll_secs)
|
|
135
|
+
|
|
136
|
+
def due(self, now: Optional[float] = None) -> bool:
|
|
137
|
+
now = now if now is not None else time.time()
|
|
138
|
+
return (now - self._last_poll) >= self.poll_secs
|
|
139
|
+
|
|
140
|
+
def mark_polled(self, now: Optional[float] = None) -> None:
|
|
141
|
+
self._last_poll = now if now is not None else time.time()
|
|
142
|
+
|
|
143
|
+
# -- the contract -----------------------------------------------------
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def poll(self) -> List[Observation]:
|
|
146
|
+
"""Fetch from the source and return normalized Observations.
|
|
147
|
+
|
|
148
|
+
Must be side-effect free beyond reading. Never raises past its own
|
|
149
|
+
try/except; returns [] on any failure so one bad source cannot stall
|
|
150
|
+
the manager.
|
|
151
|
+
"""
|
|
152
|
+
...
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Calendar connector (read-only): pulls an iCalendar (.ics) feed and
|
|
2
|
+
normalizes VEVENTs into "calendar" observations + Event/person entities.
|
|
3
|
+
|
|
4
|
+
An ICS feed URL (Google/CalDAV/Exchange all expose one) is the
|
|
5
|
+
dependency-light pull path; a full CalDAV REPORT or an OAuth API token is the
|
|
6
|
+
deployment option, documented in the handoff. Credentials env-only
|
|
7
|
+
(COLONY_CONNECTOR_CALENDAR_*). Read-only: never creates or edits events.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
import time
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from typing import Any, Dict, List
|
|
17
|
+
|
|
18
|
+
from apsimo.connectors.base import Connector, EntityHint, Observation
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
_UNFOLD = re.compile(r"\r?\n[ \t]")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse_ics(ics_text: str) -> List[Dict[str, Any]]:
|
|
26
|
+
"""Minimal VEVENT parser (stdlib): returns per-event property dicts."""
|
|
27
|
+
text = _UNFOLD.sub("", ics_text or "") # unfold continued lines
|
|
28
|
+
events: List[Dict[str, Any]] = []
|
|
29
|
+
cur: Dict[str, Any] = {}
|
|
30
|
+
in_event = False
|
|
31
|
+
for line in text.splitlines():
|
|
32
|
+
if line.startswith("BEGIN:VEVENT"):
|
|
33
|
+
in_event, cur = True, {"attendees": []}
|
|
34
|
+
continue
|
|
35
|
+
if line.startswith("END:VEVENT"):
|
|
36
|
+
if in_event:
|
|
37
|
+
events.append(cur)
|
|
38
|
+
in_event = False
|
|
39
|
+
continue
|
|
40
|
+
if not in_event or ":" not in line:
|
|
41
|
+
continue
|
|
42
|
+
name, value = line.split(":", 1)
|
|
43
|
+
key = name.split(";", 1)[0].upper()
|
|
44
|
+
if key == "ATTENDEE":
|
|
45
|
+
cn = re.search(r"CN=([^;:]+)", name)
|
|
46
|
+
cur["attendees"].append(cn.group(1) if cn else value.replace("mailto:", ""))
|
|
47
|
+
elif key in ("SUMMARY", "UID", "LOCATION", "DTSTART", "DTEND",
|
|
48
|
+
"ORGANIZER", "DESCRIPTION"):
|
|
49
|
+
cur[key.lower()] = value
|
|
50
|
+
if key == "ORGANIZER":
|
|
51
|
+
cn = re.search(r"CN=([^;:]+)", name)
|
|
52
|
+
if cn:
|
|
53
|
+
cur["organizer_name"] = cn.group(1)
|
|
54
|
+
return events
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _ics_ts(value: str) -> float:
|
|
58
|
+
if not value:
|
|
59
|
+
return time.time()
|
|
60
|
+
v = value.strip().rstrip("Z")
|
|
61
|
+
for fmt in ("%Y%m%dT%H%M%S", "%Y%m%d"):
|
|
62
|
+
try:
|
|
63
|
+
return datetime.strptime(v, fmt).replace(tzinfo=timezone.utc).timestamp()
|
|
64
|
+
except ValueError:
|
|
65
|
+
continue
|
|
66
|
+
return time.time()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class CalendarConnector(Connector):
|
|
70
|
+
name = "calendar"
|
|
71
|
+
domain = "calendar"
|
|
72
|
+
default_poll_secs = 1800
|
|
73
|
+
|
|
74
|
+
def normalize(self, events: List[Dict[str, Any]]) -> List[Observation]:
|
|
75
|
+
out: List[Observation] = []
|
|
76
|
+
for ev in events:
|
|
77
|
+
summary = ev.get("summary", "").strip()
|
|
78
|
+
uid = ev.get("uid", "") or summary
|
|
79
|
+
location = ev.get("location", "").strip()
|
|
80
|
+
attendees = [a.strip() for a in ev.get("attendees", []) if a.strip()]
|
|
81
|
+
organizer = ev.get("organizer_name", "").strip()
|
|
82
|
+
ts = _ics_ts(ev.get("dtstart", ""))
|
|
83
|
+
entities: List[EntityHint] = []
|
|
84
|
+
if summary:
|
|
85
|
+
entities.append(EntityHint(kind="event", name=summary,
|
|
86
|
+
external_ids={"uid": uid}))
|
|
87
|
+
for person in ([organizer] if organizer else []) + attendees:
|
|
88
|
+
if person and "@" not in person and len(person.split()) <= 4:
|
|
89
|
+
entities.append(EntityHint(kind="person", name=person))
|
|
90
|
+
if location and not location.startswith("http"):
|
|
91
|
+
entities.append(EntityHint(kind="location", name=location))
|
|
92
|
+
who = ", ".join([organizer] + attendees).strip(", ")
|
|
93
|
+
text = (f"Calendar event '{summary}'"
|
|
94
|
+
+ (f" at {location}" if location else "")
|
|
95
|
+
+ (f" with {who}" if who else "")).strip()
|
|
96
|
+
out.append(Observation(
|
|
97
|
+
domain=self.domain, external_id=str(uid)[:200], ts=ts,
|
|
98
|
+
payload={"summary": summary, "location": location,
|
|
99
|
+
"attendees": attendees, "organizer": organizer,
|
|
100
|
+
"start": ev.get("dtstart", ""), "end": ev.get("dtend", "")},
|
|
101
|
+
entities=entities, text=text))
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
def _fetch(self) -> List[Dict[str, Any]]:
|
|
105
|
+
import urllib.request
|
|
106
|
+
url = self.config.get("ICS_URL")
|
|
107
|
+
if not url:
|
|
108
|
+
return []
|
|
109
|
+
req = urllib.request.Request(url, method="GET")
|
|
110
|
+
user = self.config.get("USER")
|
|
111
|
+
password = self.config.get("PASSWORD")
|
|
112
|
+
if user and password:
|
|
113
|
+
import base64
|
|
114
|
+
token = base64.b64encode(f"{user}:{password}".encode()).decode()
|
|
115
|
+
req.add_header("Authorization", f"Basic {token}")
|
|
116
|
+
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
117
|
+
return _parse_ics(resp.read().decode("utf-8", "replace"))
|
|
118
|
+
|
|
119
|
+
def poll(self) -> List[Observation]:
|
|
120
|
+
try:
|
|
121
|
+
limit = self.config.get_int("MAX", 50)
|
|
122
|
+
return self.normalize(self._fetch()[:limit])
|
|
123
|
+
except Exception:
|
|
124
|
+
logger.debug("calendar poll failed", exc_info=True)
|
|
125
|
+
return []
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Filesystem documents connector (read-only): watches a folder for new or
|
|
2
|
+
changed files and normalizes them into "document" observations + entity hints.
|
|
3
|
+
|
|
4
|
+
Config env-only (COLONY_CONNECTOR_FS_*): PATH (folder), EXTENSIONS (csv),
|
|
5
|
+
MAX. Only reads file metadata + a short text snippet (for the world-model
|
|
6
|
+
extractor); never writes or deletes.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import hashlib
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any, Dict, List
|
|
15
|
+
|
|
16
|
+
from apsimo.connectors.base import Connector, EntityHint, Observation
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
_TEXT_EXTS = {".txt", ".md", ".rst", ".csv", ".json", ".log"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FSDocumentsConnector(Connector):
|
|
24
|
+
name = "fs"
|
|
25
|
+
domain = "document"
|
|
26
|
+
default_poll_secs = 600
|
|
27
|
+
|
|
28
|
+
def normalize(self, files: List[Dict[str, Any]]) -> List[Observation]:
|
|
29
|
+
out: List[Observation] = []
|
|
30
|
+
for f in files:
|
|
31
|
+
path = f.get("path", "")
|
|
32
|
+
fname = f.get("name", "") or os.path.basename(path)
|
|
33
|
+
snippet = (f.get("snippet", "") or "")[:400]
|
|
34
|
+
eid = hashlib.sha1(path.encode("utf-8")).hexdigest()[:16] if path else fname
|
|
35
|
+
text = f"Document '{fname}'" + (f": {snippet}" if snippet else "")
|
|
36
|
+
out.append(Observation(
|
|
37
|
+
domain=self.domain, external_id=eid,
|
|
38
|
+
ts=float(f.get("mtime", 0.0)),
|
|
39
|
+
payload={"path": path, "name": fname, "size": f.get("size", 0),
|
|
40
|
+
"mtime": f.get("mtime", 0.0), "snippet": snippet},
|
|
41
|
+
entities=[EntityHint(kind="document", name=fname,
|
|
42
|
+
external_ids={"path": path})],
|
|
43
|
+
text=text))
|
|
44
|
+
return out
|
|
45
|
+
|
|
46
|
+
def _fetch(self) -> List[Dict[str, Any]]:
|
|
47
|
+
folder = self.config.get("PATH")
|
|
48
|
+
if not folder or not os.path.isdir(folder):
|
|
49
|
+
return []
|
|
50
|
+
exts = {e.strip().lower() if e.strip().startswith(".") else "." + e.strip().lower()
|
|
51
|
+
for e in self.config.get("EXTENSIONS", "txt,md,pdf,docx").split(",")
|
|
52
|
+
if e.strip()}
|
|
53
|
+
limit = self.config.get_int("MAX", 50)
|
|
54
|
+
since = self._last_poll
|
|
55
|
+
found: List[Dict[str, Any]] = []
|
|
56
|
+
for root, _dirs, names in os.walk(folder):
|
|
57
|
+
for n in names:
|
|
58
|
+
ext = os.path.splitext(n)[1].lower()
|
|
59
|
+
if exts and ext not in exts:
|
|
60
|
+
continue
|
|
61
|
+
p = os.path.join(root, n)
|
|
62
|
+
try:
|
|
63
|
+
st = os.stat(p)
|
|
64
|
+
except OSError:
|
|
65
|
+
continue
|
|
66
|
+
if st.st_mtime <= since:
|
|
67
|
+
continue
|
|
68
|
+
snippet = ""
|
|
69
|
+
if ext in _TEXT_EXTS:
|
|
70
|
+
try:
|
|
71
|
+
with open(p, "r", errors="replace") as fh:
|
|
72
|
+
snippet = fh.read(600)
|
|
73
|
+
except Exception:
|
|
74
|
+
pass
|
|
75
|
+
found.append({"path": p, "name": n, "size": st.st_size,
|
|
76
|
+
"mtime": st.st_mtime, "snippet": snippet})
|
|
77
|
+
found.sort(key=lambda d: d["mtime"], reverse=True)
|
|
78
|
+
return found[:limit]
|
|
79
|
+
|
|
80
|
+
def poll(self) -> List[Observation]:
|
|
81
|
+
try:
|
|
82
|
+
return self.normalize(self._fetch())
|
|
83
|
+
except Exception:
|
|
84
|
+
logger.debug("fs poll failed", exc_info=True)
|
|
85
|
+
return []
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""IMAP email connector (read-only). Reference connector for item 2.
|
|
2
|
+
|
|
3
|
+
Reads recent message envelopes (headers + a short snippet) over IMAP and
|
|
4
|
+
normalizes them into "email" observations plus person/company entity hints.
|
|
5
|
+
Credentials are env-only (COLONY_CONNECTOR_IMAP_*). Never sends, never
|
|
6
|
+
deletes, never marks read (uses BODY.PEEK).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import email
|
|
12
|
+
import logging
|
|
13
|
+
import re
|
|
14
|
+
from email.header import decode_header
|
|
15
|
+
from email.utils import parseaddr, parsedate_to_datetime
|
|
16
|
+
from typing import Any, Dict, List
|
|
17
|
+
|
|
18
|
+
from apsimo.connectors.base import Connector, EntityHint, Observation
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Consumer providers whose domain is NOT a company signal.
|
|
23
|
+
_GENERIC_DOMAINS = frozenset({
|
|
24
|
+
"gmail.com", "googlemail.com", "yahoo.com", "outlook.com", "hotmail.com",
|
|
25
|
+
"icloud.com", "me.com", "aol.com", "proton.me", "protonmail.com", "gmx.com",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _decode(raw: Any) -> str:
|
|
30
|
+
if raw is None:
|
|
31
|
+
return ""
|
|
32
|
+
if isinstance(raw, bytes):
|
|
33
|
+
raw = raw.decode("utf-8", "replace")
|
|
34
|
+
try:
|
|
35
|
+
parts = decode_header(raw)
|
|
36
|
+
return "".join(
|
|
37
|
+
(p.decode(enc or "utf-8", "replace") if isinstance(p, bytes) else p)
|
|
38
|
+
for p, enc in parts)
|
|
39
|
+
except Exception:
|
|
40
|
+
return str(raw)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _company_from_email(addr: str) -> str:
|
|
44
|
+
dom = (addr.rsplit("@", 1)[-1] if "@" in addr else "").lower()
|
|
45
|
+
if not dom or dom in _GENERIC_DOMAINS:
|
|
46
|
+
return ""
|
|
47
|
+
base = dom.rsplit(".", 1)[0].split(".")[-1] # acme from mail.acme.co.uk-ish
|
|
48
|
+
return base.replace("-", " ").title() if base else ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class IMAPEmailConnector(Connector):
|
|
52
|
+
name = "imap"
|
|
53
|
+
domain = "email"
|
|
54
|
+
default_poll_secs = 900
|
|
55
|
+
|
|
56
|
+
def normalize(self, messages: List[Dict[str, Any]]) -> List[Observation]:
|
|
57
|
+
"""Pure: [{message_id, from, to, subject, date, snippet}] -> Observations."""
|
|
58
|
+
out: List[Observation] = []
|
|
59
|
+
for m in messages:
|
|
60
|
+
from_name, from_addr = parseaddr(m.get("from", ""))
|
|
61
|
+
from_name = _decode(from_name) or from_addr
|
|
62
|
+
subject = _decode(m.get("subject", ""))
|
|
63
|
+
snippet = (m.get("snippet", "") or "")[:400]
|
|
64
|
+
ts = self._ts(m.get("date"))
|
|
65
|
+
entities: List[EntityHint] = []
|
|
66
|
+
if from_name and "@" not in from_name and len(from_name.split()) <= 4:
|
|
67
|
+
entities.append(EntityHint(kind="person", name=from_name,
|
|
68
|
+
external_ids={"email": from_addr}))
|
|
69
|
+
company = _company_from_email(from_addr)
|
|
70
|
+
if company:
|
|
71
|
+
entities.append(EntityHint(kind="company", name=company,
|
|
72
|
+
external_ids={"domain": from_addr.rsplit('@', 1)[-1]}))
|
|
73
|
+
text = (f"Email from {from_name} <{from_addr}> "
|
|
74
|
+
f"re: {subject}. {snippet}").strip()
|
|
75
|
+
out.append(Observation(
|
|
76
|
+
domain=self.domain,
|
|
77
|
+
external_id=str(m.get("message_id") or f"{from_addr}:{subject}")[:200],
|
|
78
|
+
ts=ts,
|
|
79
|
+
payload={"from": from_addr, "from_name": from_name,
|
|
80
|
+
"to": m.get("to", ""), "subject": subject,
|
|
81
|
+
"snippet": snippet,
|
|
82
|
+
"account": self.config.get("USER", self.account)},
|
|
83
|
+
entities=entities, text=text))
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def _ts(date_str: Any) -> float:
|
|
88
|
+
import time
|
|
89
|
+
if not date_str:
|
|
90
|
+
return time.time()
|
|
91
|
+
try:
|
|
92
|
+
return parsedate_to_datetime(str(date_str)).timestamp()
|
|
93
|
+
except Exception:
|
|
94
|
+
return time.time()
|
|
95
|
+
|
|
96
|
+
def _fetch(self) -> List[Dict[str, Any]]:
|
|
97
|
+
import imaplib
|
|
98
|
+
host = self.config.get("HOST")
|
|
99
|
+
user = self.config.get("USER")
|
|
100
|
+
password = self.config.get("PASSWORD")
|
|
101
|
+
if not (host and user and password):
|
|
102
|
+
return []
|
|
103
|
+
port = self.config.get_int("PORT", 993)
|
|
104
|
+
mailbox = self.config.get("MAILBOX", "INBOX")
|
|
105
|
+
limit = self.config.get_int("MAX", 20)
|
|
106
|
+
msgs: List[Dict[str, Any]] = []
|
|
107
|
+
imap = imaplib.IMAP4_SSL(host, port)
|
|
108
|
+
try:
|
|
109
|
+
imap.login(user, password)
|
|
110
|
+
imap.select(mailbox, readonly=True)
|
|
111
|
+
typ, data = imap.search(None, "ALL")
|
|
112
|
+
if typ != "OK":
|
|
113
|
+
return []
|
|
114
|
+
ids = data[0].split()[-limit:]
|
|
115
|
+
for mid in reversed(ids):
|
|
116
|
+
typ, msg_data = imap.fetch(
|
|
117
|
+
mid, "(BODY.PEEK[HEADER.FIELDS (MESSAGE-ID FROM TO SUBJECT DATE)])")
|
|
118
|
+
if typ != "OK" or not msg_data or not msg_data[0]:
|
|
119
|
+
continue
|
|
120
|
+
hdr = email.message_from_bytes(msg_data[0][1])
|
|
121
|
+
msgs.append({
|
|
122
|
+
"message_id": hdr.get("Message-ID", ""),
|
|
123
|
+
"from": hdr.get("From", ""), "to": hdr.get("To", ""),
|
|
124
|
+
"subject": hdr.get("Subject", ""), "date": hdr.get("Date", ""),
|
|
125
|
+
"snippet": ""})
|
|
126
|
+
finally:
|
|
127
|
+
try:
|
|
128
|
+
imap.logout()
|
|
129
|
+
except Exception:
|
|
130
|
+
pass
|
|
131
|
+
return msgs
|
|
132
|
+
|
|
133
|
+
def poll(self) -> List[Observation]:
|
|
134
|
+
try:
|
|
135
|
+
return self.normalize(self._fetch())
|
|
136
|
+
except Exception:
|
|
137
|
+
logger.debug("imap poll failed", exc_info=True)
|
|
138
|
+
return []
|