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.
Files changed (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. 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 []