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,824 @@
1
+ """Colony Briefing System — data aggregators.
2
+
3
+ Thin adapter layer that pulls structured data from Colony intelligence
4
+ modules and formats it for the BriefingComposer. Each aggregator uses
5
+ stub/default behaviour when the underlying subsystem is unavailable,
6
+ so briefings always generate even in partially-configured environments.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import concurrent.futures
13
+ import logging
14
+ import threading
15
+ import zoneinfo
16
+ from dataclasses import dataclass, field
17
+ from datetime import date as _date, datetime, timedelta as _timedelta, timezone
18
+ from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
19
+
20
+ from .models import CalendarEvent
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ # ---------------------------------------------------------------------------
26
+ # Domain value objects
27
+ # ---------------------------------------------------------------------------
28
+
29
+
30
+ @dataclass
31
+ class RelationshipChange:
32
+ contact_name: str
33
+ change_type: str # "new" | "dormant" | "sentiment_shift" | "tier_change"
34
+ description: str
35
+ trust_tier: str # "info" | "casual" | "close" | "inner_circle"
36
+
37
+
38
+ @dataclass
39
+ class GoalSummary:
40
+ goal_id: str
41
+ title: str
42
+ status: str # "overdue" | "blocked" | "completing_soon"
43
+ due_at: Optional[datetime] = None
44
+
45
+
46
+ @dataclass
47
+ class GoalCompletionStats:
48
+ total_initiated: int
49
+ total_completed: int
50
+ completion_rate: float # 0.0–1.0
51
+
52
+
53
+ @dataclass
54
+ class AnomalySummary:
55
+ anomaly_id: str
56
+ severity: str # "critical" | "warning" | "info"
57
+ description: str
58
+ detected_at: datetime
59
+ source: str
60
+
61
+
62
+ @dataclass
63
+ class HealthSnapshot:
64
+ sleep_score: Optional[float] = None # 0.0–1.0
65
+ readiness: Optional[float] = None
66
+ notable: Optional[str] = None
67
+
68
+
69
+ @dataclass
70
+ class CrossDomainInsight:
71
+ insight_id: str
72
+ description: str
73
+ confidence: float
74
+ domains: List[str] = field(default_factory=list)
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Aggregator protocols (allow injection of real or fake implementations)
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ @runtime_checkable
83
+ class RelationshipAggregatorProtocol(Protocol):
84
+ def get_notable_changes(
85
+ self, since: datetime, min_delta: float = 0.15
86
+ ) -> List[RelationshipChange]: ...
87
+
88
+ def get_neglected_contacts(
89
+ self, days_since_contact: int = 14, limit: int = 5
90
+ ) -> List[str]: ...
91
+
92
+
93
+ @runtime_checkable
94
+ class CalendarAggregatorProtocol(Protocol):
95
+ def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]: ...
96
+
97
+ def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]: ...
98
+
99
+ def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]: ...
100
+
101
+
102
+ @runtime_checkable
103
+ class GoalAggregatorProtocol(Protocol):
104
+ def get_overdue_goals(self) -> List[GoalSummary]: ...
105
+
106
+ def get_blocked_goals(self) -> List[GoalSummary]: ...
107
+
108
+ def get_completing_soon(self, hours: float = 4.0) -> List[GoalSummary]: ...
109
+
110
+ def get_week_completion_stats(
111
+ self, period_start: datetime, period_end: datetime
112
+ ) -> GoalCompletionStats: ...
113
+
114
+
115
+ @runtime_checkable
116
+ class AnomalyAggregatorProtocol(Protocol):
117
+ def get_active_anomalies(self, min_severity: str = "warning") -> List[AnomalySummary]: ...
118
+
119
+ def get_new_since(
120
+ self, since: datetime, min_severity: str = "warning"
121
+ ) -> List[AnomalySummary]: ...
122
+
123
+
124
+ @runtime_checkable
125
+ class MindModelAggregatorProtocol(Protocol):
126
+ def get_health_snapshot(self) -> Optional[HealthSnapshot]: ...
127
+
128
+ def get_predicted_load(self, date: str) -> Optional[float]: ...
129
+
130
+
131
+ @runtime_checkable
132
+ class SynthesisAggregatorProtocol(Protocol):
133
+ def get_high_confidence_insights(
134
+ self,
135
+ min_confidence: float = 0.80,
136
+ since: Optional[datetime] = None,
137
+ limit: int = 3,
138
+ ) -> List[CrossDomainInsight]: ...
139
+
140
+ def get_weekly_patterns(
141
+ self, period_start: datetime, period_end: datetime
142
+ ) -> List[str]: ...
143
+
144
+
145
+ # ---------------------------------------------------------------------------
146
+ # Default stub implementations (used when subsystems are not configured)
147
+ # ---------------------------------------------------------------------------
148
+
149
+
150
+ class StubRelationshipAggregator:
151
+ def get_notable_changes(
152
+ self, since: datetime, min_delta: float = 0.15
153
+ ) -> List[RelationshipChange]:
154
+ return []
155
+
156
+ def get_neglected_contacts(
157
+ self, days_since_contact: int = 14, limit: int = 5
158
+ ) -> List[str]:
159
+ return []
160
+
161
+
162
+ class StubCalendarAggregator:
163
+ def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]:
164
+ return []
165
+
166
+ def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]:
167
+ return []
168
+
169
+ def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]:
170
+ return []
171
+
172
+
173
+ class StubGoalAggregator:
174
+ def get_overdue_goals(self) -> List[GoalSummary]:
175
+ return []
176
+
177
+ def get_blocked_goals(self) -> List[GoalSummary]:
178
+ return []
179
+
180
+ def get_completing_soon(self, hours: float = 4.0) -> List[GoalSummary]:
181
+ return []
182
+
183
+ def get_week_completion_stats(
184
+ self, period_start: datetime, period_end: datetime
185
+ ) -> GoalCompletionStats:
186
+ return GoalCompletionStats(total_initiated=0, total_completed=0, completion_rate=0.0)
187
+
188
+
189
+ class StubAnomalyAggregator:
190
+ def get_active_anomalies(self, min_severity: str = "warning") -> List[AnomalySummary]:
191
+ return []
192
+
193
+ def get_new_since(
194
+ self, since: datetime, min_severity: str = "warning"
195
+ ) -> List[AnomalySummary]:
196
+ return []
197
+
198
+
199
+ class StubMindModelAggregator:
200
+ def get_health_snapshot(self) -> Optional[HealthSnapshot]:
201
+ return None
202
+
203
+ def get_predicted_load(self, date: str) -> Optional[float]:
204
+ return None
205
+
206
+
207
+ class StubSynthesisAggregator:
208
+ def get_high_confidence_insights(
209
+ self,
210
+ min_confidence: float = 0.80,
211
+ since: Optional[datetime] = None,
212
+ limit: int = 3,
213
+ ) -> List[CrossDomainInsight]:
214
+ return []
215
+
216
+ def get_weekly_patterns(
217
+ self, period_start: datetime, period_end: datetime
218
+ ) -> List[str]:
219
+ return []
220
+
221
+
222
+ # ---------------------------------------------------------------------------
223
+ # Async → sync bridge
224
+ # ---------------------------------------------------------------------------
225
+
226
+
227
+ _ASYNC_BRIDGE_POOL: Optional[concurrent.futures.ThreadPoolExecutor] = None
228
+ _ASYNC_BRIDGE_LOCK = threading.Lock()
229
+
230
+
231
+ def _async_bridge_pool() -> concurrent.futures.ThreadPoolExecutor:
232
+ """Lazily create one shared bridge pool, instead of a new executor per call.
233
+
234
+ A fresh ``ThreadPoolExecutor`` per ``_run_async`` thrashed thread+loop
235
+ create/destroy under tactical-briefing bursts (a composer makes 5–8 of these
236
+ calls per briefing). One small bounded pool is reused for the whole process.
237
+ """
238
+ global _ASYNC_BRIDGE_POOL
239
+ if _ASYNC_BRIDGE_POOL is None:
240
+ with _ASYNC_BRIDGE_LOCK:
241
+ if _ASYNC_BRIDGE_POOL is None:
242
+ _ASYNC_BRIDGE_POOL = concurrent.futures.ThreadPoolExecutor(
243
+ max_workers=2, thread_name_prefix="briefing-async"
244
+ )
245
+ return _ASYNC_BRIDGE_POOL
246
+
247
+
248
+ def _run_async(coro: Any) -> Any:
249
+ """Run an async coroutine from a synchronous call-site.
250
+
251
+ Uses ``asyncio.run()`` when no event loop is active (the normal production
252
+ path where briefing generation is synchronous). Falls back to a shared
253
+ worker pool when a loop is already running (e.g. inside pytest-asyncio
254
+ tests), so we never deadlock by trying to nest ``asyncio.run()``.
255
+ """
256
+ try:
257
+ asyncio.get_running_loop()
258
+ # Already inside a running loop — offload to the shared bridge pool.
259
+ return _async_bridge_pool().submit(asyncio.run, coro).result()
260
+ except RuntimeError:
261
+ # No running loop — safe to call asyncio.run directly.
262
+ return asyncio.run(coro)
263
+
264
+
265
+ # ---------------------------------------------------------------------------
266
+ # Real implementations
267
+ # ---------------------------------------------------------------------------
268
+
269
+
270
+ class RelationshipAggregator:
271
+ """Real implementation: wraps RelationshipScorer and queries Neo4j graph data.
272
+
273
+ Score-change history is read from ``ScoreEvent`` nodes written by
274
+ ``RelationshipScorer.record_score_change()``. Neglected contacts are
275
+ found via a direct graph query so that the aggregator is usable without
276
+ running a full scorer refresh cycle.
277
+ """
278
+
279
+ # Scores live on a 0-100 scale; min_delta is expressed as a 0-1 fraction,
280
+ # so we multiply by 100 to get the point-delta threshold.
281
+ _SCORE_CHANGES_CYPHER = """
282
+ MATCH (p:Person)-[:SCORE_CHANGED]->(se:ScoreEvent)
283
+ WHERE se.createdAt >= datetime($since_iso)
284
+ AND abs(se.delta) >= $delta_threshold
285
+ RETURN p.name AS name, p.tier AS current_tier,
286
+ se.delta AS delta, se.reason AS reason, se.tier AS new_tier
287
+ ORDER BY abs(se.delta) DESC
288
+ """
289
+
290
+ _NEGLECTED_CYPHER = """
291
+ MATCH (p:Person)
292
+ WHERE p.tier IN ['inner_circle', 'trusted', 'regular']
293
+ AND (
294
+ p.lastInteraction IS NULL
295
+ OR p.lastInteraction < datetime() - duration({days: $days_threshold})
296
+ )
297
+ RETURN p.name AS name
298
+ ORDER BY p.lastInteraction ASC
299
+ LIMIT $limit
300
+ """
301
+
302
+ def __init__(self, scorer: Any, graph: Any) -> None:
303
+ self._scorer = scorer
304
+ self._graph = graph
305
+
306
+ # ------------------------------------------------------------------
307
+ # Protocol methods
308
+ # ------------------------------------------------------------------
309
+
310
+ def get_notable_changes(
311
+ self, since: datetime, min_delta: float = 0.15
312
+ ) -> List[RelationshipChange]:
313
+ try:
314
+ return _run_async(self._notable_changes_async(since, min_delta))
315
+ except Exception:
316
+ logger.exception("RelationshipAggregator.get_notable_changes failed")
317
+ return []
318
+
319
+ def get_neglected_contacts(
320
+ self, days_since_contact: int = 14, limit: int = 5
321
+ ) -> List[str]:
322
+ try:
323
+ return _run_async(self._neglected_contacts_async(days_since_contact, limit))
324
+ except Exception:
325
+ logger.exception("RelationshipAggregator.get_neglected_contacts failed")
326
+ return []
327
+
328
+ # ------------------------------------------------------------------
329
+ # Async helpers (isolated for testability)
330
+ # ------------------------------------------------------------------
331
+
332
+ async def _notable_changes_async(
333
+ self, since: datetime, min_delta: float
334
+ ) -> List[RelationshipChange]:
335
+ delta_threshold = min_delta * 100.0
336
+ rows = await self._query_score_changes(since.isoformat(), delta_threshold)
337
+ result: List[RelationshipChange] = []
338
+ for row in rows:
339
+ name: str = row.get("name") or "Unknown"
340
+ tier: str = row.get("new_tier") or row.get("current_tier") or "peripheral"
341
+ delta: float = float(row.get("delta") or 0.0)
342
+ reason: str = row.get("reason") or ""
343
+
344
+ if reason == "new_contact":
345
+ change_type = "new"
346
+ elif delta < 0:
347
+ change_type = "dormant"
348
+ else:
349
+ change_type = "tier_change"
350
+
351
+ description = (
352
+ f"Score increased by {delta:.1f} points"
353
+ if delta >= 0
354
+ else f"Score decreased by {abs(delta):.1f} points"
355
+ )
356
+ result.append(RelationshipChange(
357
+ contact_name=name,
358
+ change_type=change_type,
359
+ description=description,
360
+ trust_tier=tier,
361
+ ))
362
+ return result
363
+
364
+ async def _neglected_contacts_async(
365
+ self, days_since_contact: int, limit: int
366
+ ) -> List[str]:
367
+ rows = await self._query_neglected(days_since_contact, limit)
368
+ return [r["name"] for r in rows if r.get("name")]
369
+
370
+ async def _query_score_changes(
371
+ self, since_iso: str, delta_threshold: float
372
+ ) -> List[Dict[str, Any]]:
373
+ async with self._graph.driver.session(database=self._graph.database) as session:
374
+ result = await session.run(
375
+ self._SCORE_CHANGES_CYPHER,
376
+ since_iso=since_iso,
377
+ delta_threshold=delta_threshold,
378
+ )
379
+ return [dict(r) async for r in result]
380
+
381
+ async def _query_neglected(
382
+ self, days_threshold: int, limit: int
383
+ ) -> List[Dict[str, Any]]:
384
+ async with self._graph.driver.session(database=self._graph.database) as session:
385
+ result = await session.run(
386
+ self._NEGLECTED_CYPHER,
387
+ days_threshold=days_threshold,
388
+ limit=limit,
389
+ )
390
+ return [dict(r) async for r in result]
391
+
392
+
393
+ class AnomalyDetectorAggregator:
394
+ """Real implementation over the AnomalyDetector the autonomy registry
395
+ holds (its observation state is in-memory on that ONE instance, so the
396
+ detector is resolved lazily at call time — never constructed fresh here,
397
+ which would always be empty)."""
398
+
399
+ # Severity labels over the detector's 0-1 float scale.
400
+ _SEVERITY_FLOOR = {"critical": 0.85, "warning": 0.6, "info": 0.3}
401
+
402
+ def __init__(self, detector_provider: Optional[Any] = None) -> None:
403
+ self._provider = detector_provider or self._registry_detector
404
+
405
+ @staticmethod
406
+ def _registry_detector():
407
+ try:
408
+ from apsimo.api.routers import host as _h
409
+ loop = getattr(_h, "_autonomy_loop", None)
410
+ registry = getattr(loop, "_registry", None)
411
+ return getattr(registry, "anomalies", None)
412
+ except Exception:
413
+ return None
414
+
415
+ @classmethod
416
+ def _label(cls, severity: float) -> str:
417
+ if severity >= cls._SEVERITY_FLOOR["critical"]:
418
+ return "critical"
419
+ if severity >= cls._SEVERITY_FLOOR["warning"]:
420
+ return "warning"
421
+ return "info"
422
+
423
+ @staticmethod
424
+ def _aware(dt: Optional[datetime]) -> datetime:
425
+ # the detector stamps naive LOCAL datetimes
426
+ if dt is None:
427
+ return datetime.now(timezone.utc)
428
+ if dt.tzinfo is None:
429
+ return dt.astimezone()
430
+ return dt
431
+
432
+ def _summaries(self, min_severity: str) -> List[AnomalySummary]:
433
+ detector = self._provider() if callable(self._provider) else self._provider
434
+ if detector is None:
435
+ return []
436
+ floor = self._SEVERITY_FLOOR.get(min_severity, 0.6)
437
+ try:
438
+ recent = detector.get_recent(min_severity=floor, limit=20) or []
439
+ except Exception:
440
+ logger.exception("AnomalyDetectorAggregator get_recent failed")
441
+ return []
442
+ out = []
443
+ for a in recent:
444
+ out.append(AnomalySummary(
445
+ anomaly_id=getattr(a, "id", ""),
446
+ severity=self._label(float(getattr(a, "severity", 0.0))),
447
+ description=getattr(a, "description", ""),
448
+ detected_at=self._aware(getattr(a, "detected_at", None)),
449
+ source=getattr(getattr(a, "type", None), "value",
450
+ str(getattr(a, "type", "anomaly"))),
451
+ ))
452
+ return out
453
+
454
+ def get_active_anomalies(self, min_severity: str = "warning") -> List[AnomalySummary]:
455
+ return self._summaries(min_severity)
456
+
457
+ def get_new_since(self, since: datetime,
458
+ min_severity: str = "warning") -> List[AnomalySummary]:
459
+ if since is not None and since.tzinfo is None:
460
+ since = since.replace(tzinfo=timezone.utc)
461
+ return [a for a in self._summaries(min_severity)
462
+ if since is None or a.detected_at >= since]
463
+
464
+
465
+ class DiscovererSynthesisAggregator:
466
+ """Real implementation over the ConnectionDiscoverer + InsightStore.
467
+
468
+ Connections are recomputed on demand (the discoverer persists nothing);
469
+ dismissed insight ids are filtered out. Connections carry no reliable
470
+ timestamps, so `since` cannot be honored and is ignored — the confidence
471
+ floor and dismissal filter are the honest selectors."""
472
+
473
+ def __init__(self, discoverer_provider: Optional[Any] = None,
474
+ insight_store_provider: Optional[Any] = None) -> None:
475
+ self._discoverer = discoverer_provider or self._host_discoverer
476
+ self._insights = insight_store_provider or self._host_insight_store
477
+
478
+ @staticmethod
479
+ def _host_discoverer():
480
+ try:
481
+ from apsimo.api.routers import host as _h
482
+ return getattr(_h, "_connection_discoverer", None)
483
+ except Exception:
484
+ return None
485
+
486
+ @staticmethod
487
+ def _host_insight_store():
488
+ try:
489
+ from apsimo.api.routers import host as _h
490
+ return getattr(_h, "_insight_store", None)
491
+ except Exception:
492
+ return None
493
+
494
+ def _connections(self, min_confidence: float) -> List[Any]:
495
+ disc = self._discoverer() if callable(self._discoverer) else self._discoverer
496
+ if disc is None:
497
+ return []
498
+ try:
499
+ conns = _run_async(disc.discover_connections(
500
+ min_confidence=min_confidence)) or []
501
+ except Exception:
502
+ logger.exception("DiscovererSynthesisAggregator discovery failed")
503
+ return []
504
+ store = self._insights() if callable(self._insights) else self._insights
505
+ if store is not None:
506
+ try:
507
+ dismissed = store.list_dismissed()
508
+ conns = [c for c in conns
509
+ if getattr(c, "id", None) not in dismissed]
510
+ except Exception:
511
+ logger.debug("dismissed-insight filter failed", exc_info=True)
512
+ return conns
513
+
514
+ def get_high_confidence_insights(
515
+ self,
516
+ min_confidence: float = 0.80,
517
+ since: Optional[datetime] = None,
518
+ limit: int = 3,
519
+ ) -> List[CrossDomainInsight]:
520
+ out = []
521
+ for c in self._connections(min_confidence)[:limit]:
522
+ out.append(CrossDomainInsight(
523
+ insight_id=getattr(c, "id", ""),
524
+ description=getattr(c, "description", ""),
525
+ confidence=float(getattr(c, "confidence", 0.0)),
526
+ domains=[d for d in (getattr(c, "source_domain", None),
527
+ getattr(c, "target_domain", None)) if d],
528
+ ))
529
+ return out
530
+
531
+ def get_weekly_patterns(self, period_start: datetime,
532
+ period_end: datetime) -> List[str]:
533
+ # Recurring connections (seen more than once) read as patterns.
534
+ return [getattr(c, "description", "")
535
+ for c in self._connections(min_confidence=0.6)
536
+ if getattr(c, "observation_count", 1) > 1][:5]
537
+
538
+
539
+ class GoalEngineAggregator:
540
+ """Real implementation backed by the GoalEngine (goals are owner-scoped;
541
+ no person filter). Overdue/blocked/completing-soon read live goal state;
542
+ weekly stats come from created/completed timestamps in the window."""
543
+
544
+ def __init__(self, engine: Any) -> None:
545
+ self._engine = engine
546
+
547
+ def _goals(self, status: Optional[str] = None, limit: int = 200) -> List[Any]:
548
+ try:
549
+ return self._engine.list_goals(status=status, limit=limit) or []
550
+ except Exception:
551
+ logger.exception("GoalEngineAggregator list failed")
552
+ return []
553
+
554
+ @staticmethod
555
+ def _aware(dt: Optional[datetime]) -> Optional[datetime]:
556
+ if dt is not None and dt.tzinfo is None:
557
+ return dt.replace(tzinfo=timezone.utc)
558
+ return dt
559
+
560
+ @staticmethod
561
+ def _summary(g: Any, status: str) -> GoalSummary:
562
+ return GoalSummary(goal_id=getattr(g, "goal_id", ""),
563
+ title=getattr(g, "title", ""),
564
+ status=status,
565
+ due_at=getattr(g, "deadline", None))
566
+
567
+ def get_overdue_goals(self) -> List[GoalSummary]:
568
+ out = []
569
+ for g in self._goals(status="active"):
570
+ try:
571
+ if getattr(g, "deadline", None) is not None and g.is_overdue():
572
+ out.append(self._summary(g, "overdue"))
573
+ except Exception:
574
+ continue
575
+ return out
576
+
577
+ def get_blocked_goals(self) -> List[GoalSummary]:
578
+ return [self._summary(g, "blocked") for g in self._goals(status="blocked")]
579
+
580
+ def get_completing_soon(self, hours: float = 4.0) -> List[GoalSummary]:
581
+ now = datetime.now(timezone.utc)
582
+ out = []
583
+ for g in self._goals(status="active"):
584
+ dl = self._aware(getattr(g, "deadline", None))
585
+ if dl is None:
586
+ continue
587
+ delta = (dl - now).total_seconds()
588
+ if 0 <= delta <= hours * 3600:
589
+ out.append(self._summary(g, "completing_soon"))
590
+ return out
591
+
592
+ def get_week_completion_stats(
593
+ self, period_start: datetime, period_end: datetime
594
+ ) -> GoalCompletionStats:
595
+ period_start = self._aware(period_start)
596
+ period_end = self._aware(period_end)
597
+ initiated = completed = 0
598
+ for g in self._goals(status=None, limit=500):
599
+ ca = self._aware(getattr(g, "created_at", None))
600
+ if ca is not None and period_start <= ca <= period_end:
601
+ initiated += 1
602
+ done = self._aware(getattr(g, "completed_at", None))
603
+ if done is not None and period_start <= done <= period_end:
604
+ completed += 1
605
+ # A goal completed in-window but created before it can push the raw
606
+ # ratio past 1.0; the field's contract is 0.0-1.0, so clamp.
607
+ rate = min(1.0, completed / initiated) if initiated else 0.0
608
+ return GoalCompletionStats(total_initiated=initiated,
609
+ total_completed=completed,
610
+ completion_rate=rate)
611
+
612
+
613
+ class ConnectorCalendarAggregator:
614
+ """Real calendar sections from the ICS calendar CONNECTOR (the only
615
+ calendar source that exists in-repo). Fetches the configured feed on
616
+ demand — briefings run once or twice a day, so a fresh fetch per compose
617
+ is fine and never serves stale events. Gated on the connector's own
618
+ COLONY_CONNECTOR_CALENDAR_ENABLED so it can't surprise-fetch.
619
+
620
+ Prep-needed heuristic: two or more attendees (the ICS payload carries no
621
+ video-link field)."""
622
+
623
+ _PREP_ATTENDEE_THRESHOLD = 2
624
+
625
+ def __init__(self, connector: Optional[Any] = None) -> None:
626
+ # accepts one connector, a list of connectors, or None (resolve all
627
+ # enabled calendar instances — base + per-account — at call time)
628
+ if connector is None:
629
+ self._connectors = None
630
+ elif isinstance(connector, (list, tuple)):
631
+ self._connectors = list(connector)
632
+ else:
633
+ self._connectors = [connector]
634
+
635
+ def _get_connectors(self):
636
+ if self._connectors is not None:
637
+ return [c for c in self._connectors if getattr(c, "enabled", True)]
638
+ try:
639
+ from apsimo.connectors.base import ConnectorConfig
640
+ from apsimo.connectors.caldav_calendar import CalendarConnector
641
+ accounts = [a.strip() for a in
642
+ ConnectorConfig("calendar").get("ACCOUNTS", "")
643
+ .replace(";", ",").split(",") if a.strip()]
644
+ if accounts:
645
+ out = []
646
+ for a in accounts:
647
+ c = CalendarConnector(account=a)
648
+ c._default_enabled = True
649
+ out.append(c)
650
+ return [c for c in out if c.enabled]
651
+ c = CalendarConnector()
652
+ return [c] if c.enabled else []
653
+ except Exception:
654
+ return []
655
+
656
+ def _events_between(self, start_utc: datetime,
657
+ end_utc: datetime, tz: str) -> List[CalendarEvent]:
658
+ observations = []
659
+ for conn in self._get_connectors():
660
+ try:
661
+ observations.extend(conn.poll() or [])
662
+ except Exception:
663
+ logger.exception("ConnectorCalendarAggregator poll failed (%s)",
664
+ getattr(conn, "name", "?"))
665
+ try:
666
+ zone = zoneinfo.ZoneInfo(tz)
667
+ except Exception:
668
+ zone = timezone.utc
669
+ from apsimo.connectors.caldav_calendar import _ics_ts
670
+ dated: List[Any] = []
671
+ for obs in observations:
672
+ when = datetime.fromtimestamp(obs.ts, tz=timezone.utc)
673
+ if not (start_utc <= when < end_utc):
674
+ continue
675
+ payload = obs.payload or {}
676
+ attendees = list(payload.get("attendees") or [])
677
+ end_raw = payload.get("end") or ""
678
+ duration = 30
679
+ if end_raw:
680
+ dur = (_ics_ts(end_raw) - obs.ts) / 60.0
681
+ if 0 < dur <= 24 * 60:
682
+ duration = int(dur)
683
+ dated.append((when, CalendarEvent(
684
+ time=when.astimezone(zone).strftime("%H:%M"),
685
+ title=payload.get("summary") or "(untitled)",
686
+ participants=attendees,
687
+ prep_needed=len(attendees) >= self._PREP_ATTENDEE_THRESHOLD,
688
+ location=payload.get("location") or None,
689
+ duration_minutes=duration,
690
+ )))
691
+ # sort by the REAL datetime — an HH:MM string sort scrambles any
692
+ # window longer than one day (Tue 08:00 before Mon 09:00)
693
+ dated.sort(key=lambda pair: pair[0])
694
+ return [e for _, e in dated]
695
+
696
+ @staticmethod
697
+ def _day_bounds(date: str, tz: str, days: int = 1):
698
+ try:
699
+ zone = zoneinfo.ZoneInfo(tz)
700
+ except Exception:
701
+ zone = timezone.utc
702
+ day = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=zone)
703
+ start = day.astimezone(timezone.utc)
704
+ return start, start + _timedelta(days=days)
705
+
706
+ def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]:
707
+ try:
708
+ start, end = self._day_bounds(date, timezone)
709
+ return self._events_between(start, end, timezone)
710
+ except Exception:
711
+ logger.exception("ConnectorCalendarAggregator.get_today_events failed")
712
+ return []
713
+
714
+ def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]:
715
+ return [e for e in events if e.prep_needed]
716
+
717
+ def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]:
718
+ try:
719
+ start, end = self._day_bounds(start_date, timezone, days=7)
720
+ return self._events_between(start, end, timezone)
721
+ except Exception:
722
+ logger.exception("ConnectorCalendarAggregator.get_upcoming_week failed")
723
+ return []
724
+
725
+
726
+ class CalendarAggregator:
727
+ """Real implementation: wraps CalendarIntegration for the briefing pipeline.
728
+
729
+ ``CalendarIntegration`` is async; this class bridges the sync protocol
730
+ interface using ``_run_async()``. Events are converted from the
731
+ ``EventData`` representation used by the integration layer to the
732
+ ``CalendarEvent`` model used by briefings.
733
+
734
+ Prep-needed heuristic: an event needs prep when it has a video link *or*
735
+ has two or more attendees.
736
+ """
737
+
738
+ _PREP_ATTENDEE_THRESHOLD = 2
739
+
740
+ def __init__(self, calendar: Any) -> None:
741
+ self._calendar = calendar
742
+
743
+ # ------------------------------------------------------------------
744
+ # Protocol methods
745
+ # ------------------------------------------------------------------
746
+
747
+ def get_today_events(self, date: str, timezone: str) -> List[CalendarEvent]:
748
+ try:
749
+ return _run_async(self._today_events_async(date, timezone))
750
+ except Exception:
751
+ logger.exception("CalendarAggregator.get_today_events failed")
752
+ return []
753
+
754
+ def get_prep_needed(self, events: List[CalendarEvent]) -> List[CalendarEvent]:
755
+ return [e for e in events if e.prep_needed]
756
+
757
+ def get_upcoming_week(self, start_date: str, timezone: str) -> List[CalendarEvent]:
758
+ try:
759
+ return _run_async(self._upcoming_week_async(start_date, timezone))
760
+ except Exception:
761
+ logger.exception("CalendarAggregator.get_upcoming_week failed")
762
+ return []
763
+
764
+ # ------------------------------------------------------------------
765
+ # Async helpers (isolated for testability)
766
+ # ------------------------------------------------------------------
767
+
768
+ async def _today_events_async(
769
+ self, date_str: str, tz_name: str
770
+ ) -> List[CalendarEvent]:
771
+ target_date = _date.fromisoformat(date_str)
772
+ tz_info = _resolve_tz(tz_name)
773
+ # Fetch 2 days to safely cover timezone-offset edge cases.
774
+ raw_events = await self._calendar.list_events(days=2, max_results=100)
775
+ items: List[tuple] = []
776
+ for ev in raw_events:
777
+ ev_local = ev.start.astimezone(tz_info)
778
+ if ev_local.date() != target_date:
779
+ continue
780
+ items.append((ev_local, _to_calendar_event(ev, tz_info)))
781
+ items.sort(key=lambda x: x[0])
782
+ return [item[1] for item in items]
783
+
784
+ async def _upcoming_week_async(
785
+ self, start_date: str, tz_name: str
786
+ ) -> List[CalendarEvent]:
787
+ target_start = _date.fromisoformat(start_date)
788
+ target_end = target_start + _timedelta(days=7)
789
+ tz_info = _resolve_tz(tz_name)
790
+ raw_events = await self._calendar.list_events(days=8, max_results=100)
791
+ items: List[tuple] = []
792
+ for ev in raw_events:
793
+ ev_local = ev.start.astimezone(tz_info)
794
+ if not (target_start <= ev_local.date() < target_end):
795
+ continue
796
+ items.append((ev_local, _to_calendar_event(ev, tz_info)))
797
+ items.sort(key=lambda x: x[0])
798
+ return [item[1] for item in items]
799
+
800
+
801
+ # ---------------------------------------------------------------------------
802
+ # Module-level helpers shared by CalendarAggregator
803
+ # ---------------------------------------------------------------------------
804
+
805
+
806
+ def _resolve_tz(tz_name: str) -> Any:
807
+ try:
808
+ return zoneinfo.ZoneInfo(tz_name)
809
+ except Exception:
810
+ logger.warning("Unknown timezone %r, falling back to UTC", tz_name)
811
+ return timezone.utc
812
+
813
+
814
+ def _to_calendar_event(ev: Any, tz_info: Any) -> CalendarEvent:
815
+ prep_needed = bool(ev.video_link or len(ev.attendees) >= 2)
816
+ ev_local = ev.start.astimezone(tz_info)
817
+ return CalendarEvent(
818
+ time=ev_local.strftime("%H:%M"),
819
+ title=ev.title,
820
+ participants=list(ev.attendees),
821
+ prep_needed=prep_needed,
822
+ location=ev.location,
823
+ duration_minutes=int(ev.duration_minutes),
824
+ )