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,439 @@
1
+ """Colony Briefing System — delivery gateways and delivery engine.
2
+
3
+ Implements the gateway abstraction and concrete iMessage, Telegram,
4
+ and API gateways with retry logic and failure handling.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ import os
12
+ import time
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass
15
+ from datetime import datetime, timezone
16
+ from typing import Any, Dict, List, Optional
17
+
18
+ from .models import Briefing, BriefingPriority, BriefingStatus
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ _IMESSAGE_MAX_CHARS = 3000
23
+ _TELEGRAM_MAX_CHARS = 4096
24
+
25
+ _PRIORITY_EMOJI = {
26
+ BriefingPriority.LOW: "🔵",
27
+ BriefingPriority.NORMAL: "⚪",
28
+ BriefingPriority.HIGH: "🟡",
29
+ BriefingPriority.URGENT: "🔴",
30
+ }
31
+
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # Result
35
+ # ---------------------------------------------------------------------------
36
+
37
+
38
+ @dataclass
39
+ class DeliveryResult:
40
+ success: bool
41
+ gateway: str
42
+ briefing_id: str
43
+ error: Optional[str] = None
44
+ delivered_at: Optional[datetime] = None
45
+
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Abstract gateway
49
+ # ---------------------------------------------------------------------------
50
+
51
+
52
+ class BriefingGateway(ABC):
53
+ """Abstract delivery gateway for briefings."""
54
+
55
+ @abstractmethod
56
+ def name(self) -> str: ...
57
+
58
+ @abstractmethod
59
+ def is_available(self) -> bool: ...
60
+
61
+ @abstractmethod
62
+ def send(self, briefing: Briefing, formatted: str) -> DeliveryResult: ...
63
+
64
+ def format(self, briefing: Briefing) -> str:
65
+ """Plain-text fallback formatter."""
66
+ lines = [
67
+ f"[{briefing.briefing_type.value.upper()} BRIEFING] {briefing.briefing_id[:8]}",
68
+ f"Priority: {briefing.priority.value}",
69
+ "",
70
+ ]
71
+ for section in briefing.active_sections():
72
+ lines.append(f"--- {section.name.upper()} ---")
73
+ if section.narrative:
74
+ lines.append(section.narrative)
75
+ else:
76
+ lines.append(str(section.content))
77
+ lines.append("")
78
+ return "\n".join(lines)
79
+
80
+
81
+ # ---------------------------------------------------------------------------
82
+ # iMessage gateway
83
+ # ---------------------------------------------------------------------------
84
+
85
+
86
+ class IMessageBriefingGateway(BriefingGateway):
87
+ """Deliver briefings via iMessage."""
88
+
89
+ def __init__(
90
+ self,
91
+ imessage_client: Optional[Any] = None,
92
+ recipient_handle: str = "",
93
+ rich_formatting: bool = True,
94
+ quiet_hours: Optional[tuple[int, int]] = None, # (start_hour, end_hour)
95
+ ) -> None:
96
+ self._client = imessage_client
97
+ self._recipient = recipient_handle
98
+ self._rich = rich_formatting
99
+ self._quiet_hours = quiet_hours
100
+
101
+ def name(self) -> str:
102
+ return "imessage"
103
+
104
+ def is_available(self) -> bool:
105
+ return self._client is not None and bool(self._recipient)
106
+
107
+ def _in_quiet_hours(self) -> bool:
108
+ if not self._quiet_hours:
109
+ return False
110
+ now_hour = datetime.now().hour
111
+ start, end = self._quiet_hours
112
+ if start <= end:
113
+ return start <= now_hour < end
114
+ return now_hour >= start or now_hour < end
115
+
116
+ def format(self, briefing: Briefing) -> str:
117
+ emoji = _PRIORITY_EMOJI.get(briefing.priority, "⚪")
118
+ lines = [f"{emoji} *Colony {briefing.briefing_type.value.title()} Briefing*", ""]
119
+ for section in briefing.active_sections():
120
+ header = f"**{section.name.replace('_', ' ').title()}**" if self._rich else section.name.upper()
121
+ lines.append(header)
122
+ if section.narrative:
123
+ lines.append(section.narrative)
124
+ else:
125
+ lines.append(_dict_to_text(section.content))
126
+ lines.append("")
127
+ text = "\n".join(lines)
128
+ if len(text) > _IMESSAGE_MAX_CHARS:
129
+ trailer = "\n... [see Colony for full briefing]"
130
+ text = text[: _IMESSAGE_MAX_CHARS - len(trailer)] + trailer
131
+ return text
132
+
133
+ def send(self, briefing: Briefing, formatted: str) -> DeliveryResult:
134
+ if self._in_quiet_hours():
135
+ return DeliveryResult(
136
+ success=False,
137
+ gateway=self.name(),
138
+ briefing_id=briefing.briefing_id,
139
+ error="quiet hours",
140
+ )
141
+ try:
142
+ self._client.send(self._recipient, formatted)
143
+ return DeliveryResult(
144
+ success=True,
145
+ gateway=self.name(),
146
+ briefing_id=briefing.briefing_id,
147
+ delivered_at=datetime.now(timezone.utc),
148
+ )
149
+ except Exception as exc:
150
+ return DeliveryResult(
151
+ success=False,
152
+ gateway=self.name(),
153
+ briefing_id=briefing.briefing_id,
154
+ error=str(exc),
155
+ )
156
+
157
+
158
+ # ---------------------------------------------------------------------------
159
+ # Telegram gateway
160
+ # ---------------------------------------------------------------------------
161
+
162
+
163
+ class TelegramBriefingGateway(BriefingGateway):
164
+ """Deliver briefings via Telegram bot."""
165
+
166
+ def __init__(
167
+ self,
168
+ telegram_client: Optional[Any] = None,
169
+ chat_id: Optional[str] = None,
170
+ ) -> None:
171
+ self._client = telegram_client
172
+ self._chat_id = chat_id
173
+
174
+ def name(self) -> str:
175
+ return "telegram"
176
+
177
+ def is_available(self) -> bool:
178
+ return self._client is not None and bool(self._chat_id)
179
+
180
+ def format(self, briefing: Briefing) -> str:
181
+ emoji = _PRIORITY_EMOJI.get(briefing.priority, "⚪")
182
+ lines = [f"{emoji} *Colony {briefing.briefing_type.value.title()} Briefing*", ""]
183
+ for section in briefing.active_sections():
184
+ lines.append(f"*{section.name.replace('_', ' ').title()}*")
185
+ if section.narrative:
186
+ lines.append(section.narrative)
187
+ else:
188
+ lines.append(_dict_to_text(section.content))
189
+ lines.append("")
190
+ return "\n".join(lines)
191
+
192
+ def send(self, briefing: Briefing, formatted: str) -> DeliveryResult:
193
+ # Split long messages
194
+ chunks = _split_message(formatted, _TELEGRAM_MAX_CHARS)
195
+ try:
196
+ for chunk in chunks:
197
+ self._client.send_message(self._chat_id, chunk, parse_mode="Markdown")
198
+ return DeliveryResult(
199
+ success=True,
200
+ gateway=self.name(),
201
+ briefing_id=briefing.briefing_id,
202
+ delivered_at=datetime.now(timezone.utc),
203
+ )
204
+ except Exception as exc:
205
+ return DeliveryResult(
206
+ success=False,
207
+ gateway=self.name(),
208
+ briefing_id=briefing.briefing_id,
209
+ error=str(exc),
210
+ )
211
+
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # API gateway
215
+ # ---------------------------------------------------------------------------
216
+
217
+
218
+ class APIBriefingGateway(BriefingGateway):
219
+ """Store briefings for API retrieval — no push delivery."""
220
+
221
+ def __init__(self, store: Optional[Any] = None) -> None:
222
+ self._store = store
223
+
224
+ def name(self) -> str:
225
+ return "api"
226
+
227
+ def is_available(self) -> bool:
228
+ return True # Always available
229
+
230
+ def send(self, briefing: Briefing, formatted: str) -> DeliveryResult:
231
+ # Mark briefing as delivered; API clients poll to retrieve it
232
+ if self._store:
233
+ self._store.mark_delivered(briefing.briefing_id, self.name())
234
+ return DeliveryResult(
235
+ success=True,
236
+ gateway=self.name(),
237
+ briefing_id=briefing.briefing_id,
238
+ delivered_at=datetime.now(timezone.utc),
239
+ )
240
+
241
+
242
+ # ---------------------------------------------------------------------------
243
+ # WhatsApp gateway
244
+ # ---------------------------------------------------------------------------
245
+
246
+
247
+ class WhatsAppBriefingGateway(BriefingGateway):
248
+ """Deliver briefings via WhatsApp using ProactiveDeliveryBridge.push_to_gateway()."""
249
+
250
+ def __init__(
251
+ self,
252
+ delivery_bridge: Optional[Any] = None, # ProactiveDeliveryBridge
253
+ chat_id: Optional[str] = None,
254
+ ) -> None:
255
+ self._bridge = delivery_bridge
256
+ self._chat_id = chat_id or os.environ.get("WHATSAPP_HOME_CHANNEL", "")
257
+
258
+ def name(self) -> str:
259
+ return "whatsapp"
260
+
261
+ def is_available(self) -> bool:
262
+ return self._bridge is not None and bool(self._chat_id)
263
+
264
+ def format(self, briefing: Briefing) -> str:
265
+ emoji = _PRIORITY_EMOJI.get(briefing.priority, "⚪")
266
+ lines = [f"{emoji} *Colony {briefing.briefing_type.value.title()} Briefing*", ""]
267
+ for section in briefing.active_sections():
268
+ lines.append(f"*{section.name.replace('_', ' ').title()}*")
269
+ if section.narrative:
270
+ lines.append(section.narrative)
271
+ else:
272
+ lines.append(_dict_to_text(section.content))
273
+ lines.append("")
274
+ return "\n".join(lines)
275
+
276
+ def send(self, briefing: Briefing, formatted: str) -> DeliveryResult:
277
+ try:
278
+ success = self._push(formatted, briefing.briefing_id)
279
+ if success:
280
+ return DeliveryResult(
281
+ success=True,
282
+ gateway=self.name(),
283
+ briefing_id=briefing.briefing_id,
284
+ delivered_at=datetime.now(timezone.utc),
285
+ )
286
+ return DeliveryResult(
287
+ success=False,
288
+ gateway=self.name(),
289
+ briefing_id=briefing.briefing_id,
290
+ error="gateway rejected delivery",
291
+ )
292
+ except Exception as exc:
293
+ return DeliveryResult(
294
+ success=False,
295
+ gateway=self.name(),
296
+ briefing_id=briefing.briefing_id,
297
+ error=str(exc),
298
+ )
299
+
300
+ def _push(self, message: str, briefing_id: str) -> bool:
301
+ """Synchronous wrapper around the async push_to_gateway call."""
302
+ coro = self._bridge.push_to_gateway(
303
+ platform="whatsapp",
304
+ chat_id=self._chat_id,
305
+ message=message,
306
+ source="briefing",
307
+ delivery_id="briefing:" + briefing_id,
308
+ source_id="briefing:" + briefing_id,
309
+ )
310
+ try:
311
+ asyncio.get_running_loop()
312
+ except RuntimeError:
313
+ # No running loop (the scheduler's worker thread — the normal
314
+ # path) — safe to call asyncio.run() directly.
315
+ return asyncio.run(coro)
316
+ # We ARE on the event-loop thread. The old run_coroutine_threadsafe +
317
+ # blocking result() here scheduled onto THIS loop and then blocked it:
318
+ # guaranteed deadlock until the 10s timeout, then a failed delivery.
319
+ # Run the coroutine on its own loop in a worker thread instead —
320
+ # still a synchronous wait (this method's contract), but it completes.
321
+ import concurrent.futures
322
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as ex:
323
+ return ex.submit(asyncio.run, coro).result(timeout=15.0)
324
+
325
+
326
+ # ---------------------------------------------------------------------------
327
+ # Delivery engine
328
+ # ---------------------------------------------------------------------------
329
+
330
+ _RETRY_DELAYS = [1.0, 4.0, 16.0]
331
+
332
+
333
+ class BriefingDeliveryEngine:
334
+ """Route briefings to the appropriate gateway with retry logic."""
335
+
336
+ def __init__(
337
+ self,
338
+ gateways: Optional[List[BriefingGateway]] = None,
339
+ default_gateway: str = "api",
340
+ ) -> None:
341
+ self._gateways: Dict[str, BriefingGateway] = {}
342
+ for gw in gateways or []:
343
+ self._gateways[gw.name()] = gw
344
+ self._default = default_gateway
345
+
346
+ def register(self, gateway: BriefingGateway) -> None:
347
+ self._gateways[gateway.name()] = gateway
348
+
349
+ def deliver(
350
+ self,
351
+ briefing: Briefing,
352
+ gateway_name: Optional[str] = None,
353
+ ) -> DeliveryResult:
354
+ """Deliver a briefing, retrying up to 3 times with exponential backoff.
355
+
356
+ For URGENT briefings that fail on the primary gateway, attempts all
357
+ remaining available gateways.
358
+ """
359
+ name = gateway_name or briefing.gateway or self._default
360
+ gw = self._gateways.get(name)
361
+
362
+ if gw is None or not gw.is_available():
363
+ # Try fallbacks
364
+ gw = self._find_available_gateway(exclude=name)
365
+
366
+ if gw is None:
367
+ return DeliveryResult(
368
+ success=False,
369
+ gateway=name,
370
+ briefing_id=briefing.briefing_id,
371
+ error="no available gateway",
372
+ )
373
+
374
+ formatted = gw.format(briefing)
375
+ result = self._deliver_with_retry(gw, briefing, formatted)
376
+
377
+ # For critical briefings: try all gateways on failure
378
+ if not result.success and briefing.priority == BriefingPriority.URGENT:
379
+ for fallback in self._gateways.values():
380
+ if fallback.name() == gw.name() or not fallback.is_available():
381
+ continue
382
+ fb_formatted = fallback.format(briefing)
383
+ fb_result = self._deliver_with_retry(fallback, briefing, fb_formatted)
384
+ if fb_result.success:
385
+ return fb_result
386
+
387
+ return result
388
+
389
+ def _deliver_with_retry(
390
+ self,
391
+ gw: BriefingGateway,
392
+ briefing: Briefing,
393
+ formatted: str,
394
+ ) -> DeliveryResult:
395
+ last_result: Optional[DeliveryResult] = None
396
+ for attempt, delay in enumerate([0.0] + _RETRY_DELAYS):
397
+ if delay > 0:
398
+ time.sleep(delay)
399
+ last_result = gw.send(briefing, formatted)
400
+ if last_result.success:
401
+ return last_result
402
+ logger.warning(
403
+ "Delivery attempt %d failed for briefing %s via %s: %s",
404
+ attempt + 1,
405
+ briefing.briefing_id,
406
+ gw.name(),
407
+ last_result.error,
408
+ )
409
+ return last_result # type: ignore[return-value]
410
+
411
+ def _find_available_gateway(self, exclude: str = "") -> Optional[BriefingGateway]:
412
+ for gw in self._gateways.values():
413
+ if gw.name() != exclude and gw.is_available():
414
+ return gw
415
+ return None
416
+
417
+
418
+ # ---------------------------------------------------------------------------
419
+ # Helpers
420
+ # ---------------------------------------------------------------------------
421
+
422
+
423
+ def _dict_to_text(d: Any) -> str:
424
+ if isinstance(d, dict):
425
+ parts = []
426
+ for k, v in d.items():
427
+ parts.append(f"{k}: {v}")
428
+ return " | ".join(parts)
429
+ return str(d)
430
+
431
+
432
+ def _split_message(text: str, max_len: int) -> List[str]:
433
+ if len(text) <= max_len:
434
+ return [text]
435
+ chunks = []
436
+ while text:
437
+ chunks.append(text[:max_len])
438
+ text = text[max_len:]
439
+ return chunks
@@ -0,0 +1,97 @@
1
+ """Section engagement tracking with EMA scoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from dataclasses import dataclass, field
7
+ from typing import Dict, List, Optional
8
+
9
+ from apsimo.briefings.models import SectionEngagementRecord
10
+
11
+ # Signal weights for EMA scoring
12
+ _SIGNAL_WEIGHTS: Dict[str, float] = {
13
+ "read": 0.7,
14
+ "acted": 1.0,
15
+ "shared": 0.9,
16
+ "dismissed": 0.0,
17
+ }
18
+
19
+ _EMA_ALPHA = 0.3
20
+ _BASELINE = 0.5
21
+
22
+
23
+ @dataclass
24
+ class _SectionStats:
25
+ score: float = _BASELINE
26
+ total: int = 0
27
+ dismissals: int = 0
28
+
29
+
30
+ class SectionEngagementTracker:
31
+ """Tracks per-section engagement scores using exponential moving average."""
32
+
33
+ def __init__(self, store=None) -> None:
34
+ self._store = store
35
+ self._stats: Dict[str, _SectionStats] = defaultdict(_SectionStats)
36
+
37
+ if store is not None:
38
+ for r in store.get_engagement_records():
39
+ self._apply(r, persist=False)
40
+
41
+ def _apply(self, record: SectionEngagementRecord, persist: bool = True) -> None:
42
+ stats = self._stats[record.section_name]
43
+ weight = _SIGNAL_WEIGHTS.get(record.signal, 0.0)
44
+ stats.score = _EMA_ALPHA * weight + (1 - _EMA_ALPHA) * stats.score
45
+ stats.total += 1
46
+ if record.signal == "dismissed":
47
+ stats.dismissals += 1
48
+ if persist and self._store is not None:
49
+ self._store.record_engagement(record)
50
+
51
+ def record(self, record: SectionEngagementRecord) -> None:
52
+ self._apply(record)
53
+
54
+ def get_section_scores(self) -> Dict[str, float]:
55
+ return {name: s.score for name, s in self._stats.items()}
56
+
57
+ def get_suppression_candidates(
58
+ self,
59
+ min_dismissals: int = 5,
60
+ dismissal_rate_threshold: float = 0.80,
61
+ ) -> List[str]:
62
+ candidates = []
63
+ for name, stats in self._stats.items():
64
+ if stats.dismissals >= min_dismissals and stats.total > 0:
65
+ rate = stats.dismissals / stats.total
66
+ if rate >= dismissal_rate_threshold:
67
+ candidates.append(name)
68
+ return candidates
69
+
70
+ def record_briefing_read(self, briefing_id: str, section_names: List[str]) -> None:
71
+ from datetime import datetime, timezone
72
+ now = datetime.now(timezone.utc)
73
+ for name in section_names:
74
+ self.record(SectionEngagementRecord(
75
+ section_name=name,
76
+ briefing_id=briefing_id,
77
+ signal="read",
78
+ recorded_at=now,
79
+ ))
80
+
81
+ def record_dismissal(self, briefing_id: str, section_name: str) -> None:
82
+ from datetime import datetime, timezone
83
+ self.record(SectionEngagementRecord(
84
+ section_name=section_name,
85
+ briefing_id=briefing_id,
86
+ signal="dismissed",
87
+ recorded_at=datetime.now(timezone.utc),
88
+ ))
89
+
90
+ def record_action(self, briefing_id: str, section_name: str) -> None:
91
+ from datetime import datetime, timezone
92
+ self.record(SectionEngagementRecord(
93
+ section_name=section_name,
94
+ briefing_id=briefing_id,
95
+ signal="acted",
96
+ recorded_at=datetime.now(timezone.utc),
97
+ ))