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,171 @@
1
+ """Insight Deliverer — route insights to appropriate delivery channels.
2
+
3
+ Routes based on:
4
+ - Urgency (high → push notification, low → digest)
5
+ - User preferences (push enabled/disabled, preferred channels)
6
+ - Channel availability
7
+ - Time of day (avoid late-night pushes for non-critical insights)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ import uuid
14
+ from dataclasses import dataclass
15
+ from enum import Enum
16
+ from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class DeliveryChannel(str, Enum):
22
+ """Available delivery channels for insights.
23
+
24
+ PUSH: Immediate notification (WhatsApp, push notification)
25
+ DIGEST: Batched into daily/weekly summary
26
+ IN_APP: Show in next conversation naturally
27
+ EMAIL: Email notification for lower-urgency items
28
+ """
29
+
30
+ PUSH = "push"
31
+ DIGEST = "digest"
32
+ IN_APP = "in_app"
33
+ EMAIL = "email"
34
+
35
+
36
+ @dataclass
37
+ class DeliveryDecision:
38
+ """Routing decision for an insight.
39
+
40
+ Attributes:
41
+ insight_id: ID of the insight being routed
42
+ channel: Selected delivery channel
43
+ reason: Why this channel was chosen
44
+ scheduled_for: ISO timestamp for digest delivery (None for immediate)
45
+ """
46
+
47
+ insight_id: str
48
+ channel: DeliveryChannel
49
+ reason: str
50
+ scheduled_for: Optional[str] = None
51
+
52
+
53
+ @runtime_checkable
54
+ class DelivererEventBus(Protocol):
55
+ """Protocol for event emission from the deliverer."""
56
+
57
+ async def emit_async(self, event: Any) -> None: ...
58
+
59
+
60
+ @runtime_checkable
61
+ class Deliverable(Protocol):
62
+ """Protocol for objects that can be delivered as insights."""
63
+
64
+ @property
65
+ def id(self) -> str: ...
66
+
67
+ @property
68
+ def description(self) -> str: ...
69
+
70
+
71
+ class InsightDeliverer:
72
+ """Route insights to appropriate delivery channels.
73
+
74
+ Makes routing decisions based on urgency level and user preferences.
75
+ High-urgency insights get pushed immediately; low-urgency insights
76
+ queue for the next digest.
77
+
78
+ Args:
79
+ event_bus: Event bus for emitting delivery events
80
+ user_preferences: Dict of user delivery preferences
81
+ """
82
+
83
+ def __init__(
84
+ self,
85
+ event_bus: DelivererEventBus,
86
+ user_preferences: Optional[Dict[str, Any]] = None,
87
+ ) -> None:
88
+ self.events = event_bus
89
+ self.preferences = user_preferences or {}
90
+
91
+ async def deliver(
92
+ self,
93
+ insight: Deliverable,
94
+ urgency: float = 0.5,
95
+ ) -> DeliveryDecision:
96
+ """Determine delivery channel and route an insight.
97
+
98
+ Routing logic:
99
+ - urgency >= 0.8 and push enabled → PUSH
100
+ - urgency >= 0.5 → IN_APP (show in conversation)
101
+ - urgency < 0.5 → DIGEST (queue for summary)
102
+
103
+ Args:
104
+ insight: The insight to deliver
105
+ urgency: Urgency level between 0 and 1
106
+
107
+ Returns:
108
+ DeliveryDecision describing channel and reasoning.
109
+ """
110
+ if urgency >= 0.8 and self.preferences.get("push_enabled", True):
111
+ channel = DeliveryChannel.PUSH
112
+ reason = "High urgency, push enabled"
113
+ await self._send_push(insight)
114
+ elif urgency >= 0.5:
115
+ channel = DeliveryChannel.IN_APP
116
+ reason = "Moderate urgency, show in conversation"
117
+ else:
118
+ channel = DeliveryChannel.DIGEST
119
+ reason = "Low urgency, queue for digest"
120
+
121
+ decision = DeliveryDecision(
122
+ insight_id=insight.id,
123
+ channel=channel,
124
+ reason=reason,
125
+ )
126
+
127
+ logger.info(
128
+ "Insight %s routed to %s: %s",
129
+ insight.id,
130
+ channel.value,
131
+ reason,
132
+ )
133
+ return decision
134
+
135
+ async def deliver_batch(
136
+ self,
137
+ insights: List[tuple[Deliverable, float]],
138
+ ) -> List[DeliveryDecision]:
139
+ """Route multiple insights with their urgency levels.
140
+
141
+ Args:
142
+ insights: List of (insight, urgency) tuples
143
+
144
+ Returns:
145
+ List of DeliveryDecision objects.
146
+ """
147
+ decisions = []
148
+ for insight, urgency in insights:
149
+ decision = await self.deliver(insight, urgency)
150
+ decisions.append(decision)
151
+ return decisions
152
+
153
+ async def _send_push(self, insight: Deliverable) -> None:
154
+ """Send an immediate push notification for a high-urgency insight."""
155
+ if self.events is None:
156
+ logger.warning("No event bus configured; insight %s not delivered via push", insight.id)
157
+ return
158
+ try:
159
+ from apsimo.events.types import InsightPushEvent
160
+ event = InsightPushEvent(
161
+ id=str(uuid.uuid4()),
162
+ source="insight_deliverer",
163
+ insight_id=insight.id,
164
+ description=insight.description,
165
+ urgency=1.0,
166
+ delivery_channel="push",
167
+ )
168
+ await self.events.emit_async(event)
169
+ logger.debug("Push event emitted for insight %s", insight.id)
170
+ except Exception as e:
171
+ logger.warning("Failed to emit push event for insight %s: %s", insight.id, e)
@@ -0,0 +1,79 @@
1
+ """SQLite-backed store for dismissed-insight overlays.
2
+
3
+ Insights themselves are re-computed each call by ConnectionDiscoverer —
4
+ we don't persist the insights. This store tracks which insight IDs the
5
+ user has dismissed so ``list_insights`` can filter them out.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from contextlib import contextmanager
11
+ import logging
12
+ import sqlite3
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Iterable, Iterator, Set
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class InsightStore:
21
+ """Persistent record of dismissed insight IDs."""
22
+
23
+ def __init__(self, db_path: Path | str) -> None:
24
+ self._db_path = str(db_path)
25
+ Path(self._db_path).parent.mkdir(parents=True, exist_ok=True)
26
+ with self._connect() as conn:
27
+ conn.execute(
28
+ """
29
+ CREATE TABLE IF NOT EXISTS dismissed_insights (
30
+ insight_id TEXT PRIMARY KEY,
31
+ dismissed_at TEXT NOT NULL
32
+ )
33
+ """
34
+ )
35
+
36
+ @contextmanager
37
+ def _connect(self) -> Iterator[sqlite3.Connection]:
38
+ connection = sqlite3.connect(self._db_path)
39
+ try:
40
+ with connection:
41
+ yield connection
42
+ finally:
43
+ connection.close()
44
+
45
+ def dismiss(self, insight_id: str) -> None:
46
+ """Mark the given insight as dismissed. Idempotent."""
47
+ with self._connect() as conn:
48
+ conn.execute(
49
+ """
50
+ INSERT INTO dismissed_insights (insight_id, dismissed_at)
51
+ VALUES (?, ?)
52
+ ON CONFLICT(insight_id) DO NOTHING
53
+ """,
54
+ (insight_id, datetime.now(timezone.utc).isoformat()),
55
+ )
56
+
57
+ def is_dismissed(self, insight_id: str) -> bool:
58
+ with self._connect() as conn:
59
+ row = conn.execute(
60
+ "SELECT 1 FROM dismissed_insights WHERE insight_id=? LIMIT 1",
61
+ (insight_id,),
62
+ ).fetchone()
63
+ return row is not None
64
+
65
+ def list_dismissed(self) -> Set[str]:
66
+ with self._connect() as conn:
67
+ rows = conn.execute(
68
+ "SELECT insight_id FROM dismissed_insights"
69
+ ).fetchall()
70
+ return {r[0] for r in rows}
71
+
72
+ def undismiss(self, insight_id: str) -> bool:
73
+ """Remove a dismissal. Returns True if a row was removed."""
74
+ with self._connect() as conn:
75
+ cur = conn.execute(
76
+ "DELETE FROM dismissed_insights WHERE insight_id=?",
77
+ (insight_id,),
78
+ )
79
+ return cur.rowcount > 0
@@ -0,0 +1,183 @@
1
+ """Insight Validator — ensure insights meet quality thresholds before surfacing.
2
+
3
+ Validates insights against configurable criteria:
4
+ - Minimum evidence count (don't surface guesses)
5
+ - Confidence threshold (minimum certainty)
6
+ - Actionability check (prefer actionable insights)
7
+ - Data recency (stale data degrades trust)
8
+
9
+ Only insights passing all checks should be delivered to the user.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from datetime import datetime, timedelta, timezone
17
+ from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @dataclass
23
+ class ValidationResult:
24
+ """Result of insight validation.
25
+
26
+ Attributes:
27
+ valid: Whether the insight passed all checks
28
+ reasons: List of failure reasons (empty if valid)
29
+ evidence_count: Number of supporting evidence items
30
+ confidence: The insight's confidence score
31
+ data_age_hours: Age of the oldest supporting data in hours
32
+ """
33
+
34
+ valid: bool
35
+ reasons: List[str]
36
+ evidence_count: int
37
+ confidence: float
38
+ data_age_hours: float
39
+
40
+
41
+ @runtime_checkable
42
+ class Validatable(Protocol):
43
+ """Protocol for objects that can be validated as insights.
44
+
45
+ Insight objects must expose confidence, supporting evidence,
46
+ and optionally actionability flags.
47
+ """
48
+
49
+ @property
50
+ def confidence(self) -> float: ...
51
+
52
+ @property
53
+ def supporting_evidence(self) -> List[str]: ...
54
+
55
+
56
+ @runtime_checkable
57
+ class ValidatorGraphClient(Protocol):
58
+ """Protocol for graph access needed by insight validation."""
59
+
60
+ async def recall(
61
+ self,
62
+ query: str,
63
+ limit: int = 10,
64
+ min_strength: float = 0.1,
65
+ ) -> List[Dict[str, Any]]: ...
66
+
67
+
68
+ class InsightValidator:
69
+ """Validate insights before surfacing to the user.
70
+
71
+ Applies configurable quality gates. An insight must pass all
72
+ checks to be considered valid for delivery.
73
+
74
+ Args:
75
+ min_evidence: Minimum number of supporting evidence items
76
+ min_confidence: Minimum confidence score (0-1)
77
+ max_data_age_hours: Maximum age of supporting data in hours
78
+ graph: Optional graph client for evidence timestamp lookups
79
+ """
80
+
81
+ def __init__(
82
+ self,
83
+ min_evidence: int = 2,
84
+ min_confidence: float = 0.6,
85
+ max_data_age_hours: float = 168.0, # 1 week
86
+ graph: Optional[ValidatorGraphClient] = None,
87
+ ) -> None:
88
+ self.min_evidence = min_evidence
89
+ self.min_confidence = min_confidence
90
+ self.max_data_age_hours = max_data_age_hours
91
+ self._graph = graph
92
+
93
+ async def validate(self, insight: Validatable) -> ValidationResult:
94
+ """Check if an insight meets all quality thresholds.
95
+
96
+ Evaluates evidence count, confidence, and data recency.
97
+ Returns a ValidationResult with pass/fail and detailed reasons.
98
+
99
+ Args:
100
+ insight: The insight to validate
101
+
102
+ Returns:
103
+ ValidationResult indicating whether the insight is fit
104
+ for delivery, with failure reasons if not.
105
+ """
106
+ reasons: List[str] = []
107
+
108
+ # Evidence count
109
+ evidence_count = len(insight.supporting_evidence)
110
+ if evidence_count < self.min_evidence:
111
+ reasons.append(
112
+ f"Insufficient evidence ({evidence_count} < {self.min_evidence})"
113
+ )
114
+
115
+ # Confidence check
116
+ confidence = insight.confidence
117
+ if confidence < self.min_confidence:
118
+ reasons.append(
119
+ f"Low confidence ({confidence:.2f} < {self.min_confidence})"
120
+ )
121
+
122
+ # Data recency
123
+ data_age = await self._compute_data_age(insight)
124
+ if data_age > self.max_data_age_hours:
125
+ reasons.append(
126
+ f"Stale data ({data_age:.0f}h > {self.max_data_age_hours:.0f}h)"
127
+ )
128
+
129
+ valid = len(reasons) == 0
130
+
131
+ return ValidationResult(
132
+ valid=valid,
133
+ reasons=reasons,
134
+ evidence_count=evidence_count,
135
+ confidence=confidence,
136
+ data_age_hours=data_age,
137
+ )
138
+
139
+ async def validate_batch(
140
+ self,
141
+ insights: List[Validatable],
142
+ ) -> List[tuple[Any, ValidationResult]]:
143
+ """Validate multiple insights, returning paired results.
144
+
145
+ Args:
146
+ insights: List of insights to validate
147
+
148
+ Returns:
149
+ List of (insight, ValidationResult) tuples.
150
+ """
151
+ results = []
152
+ for insight in insights:
153
+ result = await self.validate(insight)
154
+ results.append((insight, result))
155
+ return results
156
+
157
+ async def _compute_data_age(self, insight: Validatable) -> float:
158
+ """Calculate the age of the oldest supporting data in hours."""
159
+ if not insight.supporting_evidence or self._graph is None:
160
+ return 0.0 # no evidence or no graph = treat as fresh
161
+
162
+ oldest_hours = 0.0
163
+ now = datetime.now(timezone.utc)
164
+ for evidence_id in insight.supporting_evidence:
165
+ try:
166
+ records = await self._graph.recall(
167
+ evidence_id, limit=1, min_strength=0.0
168
+ )
169
+ if records:
170
+ created_at = records[0].get("created_at")
171
+ if created_at is not None:
172
+ if hasattr(created_at, 'to_native'):
173
+ created_at = created_at.to_native()
174
+ elif isinstance(created_at, str):
175
+ created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
176
+ if isinstance(created_at, datetime):
177
+ if created_at.tzinfo is None:
178
+ created_at = created_at.replace(tzinfo=timezone.utc)
179
+ age = (now - created_at).total_seconds() / 3600
180
+ oldest_hours = max(oldest_hours, age)
181
+ except Exception as e:
182
+ logger.debug("Could not fetch data age for evidence %s: %s", evidence_id, e)
183
+ return oldest_hours
@@ -0,0 +1,267 @@
1
+ """Novelty Scorer — rate how surprising or useful an insight is.
2
+
3
+ Scores connections and insights based on:
4
+ - Historical frequency (has this been observed before?)
5
+ - Domain distance (cross-domain insights score higher)
6
+ - Confidence delta (unexpected findings score higher)
7
+
8
+ Higher novelty scores indicate insights the user is less likely to
9
+ already know, making them more valuable to surface.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from dataclasses import dataclass
16
+ from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class NoveltyScore:
23
+ """Novelty assessment for an insight or connection.
24
+
25
+ Attributes:
26
+ score: Overall novelty score between 0 and 1
27
+ reasons: Human-readable explanations for the score
28
+ historical_frequency: Number of prior observations of similar connections
29
+ domain_distance: How far apart the involved domains are (0-1)
30
+ confidence_delta: How much the confidence deviates from expectation
31
+ """
32
+
33
+ score: float
34
+ reasons: List[str]
35
+ historical_frequency: int
36
+ domain_distance: float
37
+ confidence_delta: float
38
+
39
+
40
+ @runtime_checkable
41
+ class ScoringGraphClient(Protocol):
42
+ """Protocol for graph access needed by novelty scoring."""
43
+
44
+ async def recall(
45
+ self,
46
+ query: str,
47
+ limit: int = 10,
48
+ min_strength: float = 0.1,
49
+ ) -> List[Dict[str, Any]]: ...
50
+
51
+ async def run_query(
52
+ self,
53
+ cypher: str,
54
+ params: dict,
55
+ ) -> List[Dict[str, Any]]: ...
56
+
57
+
58
+ @runtime_checkable
59
+ class Connectable(Protocol):
60
+ """Protocol for objects that have source/target domain attributes."""
61
+
62
+ @property
63
+ def source_domain(self) -> str: ...
64
+
65
+ @property
66
+ def target_domain(self) -> str: ...
67
+
68
+ @property
69
+ def confidence(self) -> float: ...
70
+
71
+ @property
72
+ def description(self) -> str: ...
73
+
74
+
75
+ # Semantic distance matrix between domains.
76
+ # Higher values mean domains are more distant (cross-domain insights
77
+ # spanning distant domains are scored as more novel).
78
+ DOMAIN_DISTANCES: Dict[tuple[str, str], float] = {
79
+ ("work", "relationships"): 0.6,
80
+ ("health", "work"): 0.8,
81
+ ("health", "finance"): 0.9,
82
+ ("work", "finance"): 0.4,
83
+ }
84
+
85
+
86
+ class NoveltyScorer:
87
+ """Score how novel or useful a connection or insight is.
88
+
89
+ Combines historical frequency, domain distance, and confidence
90
+ deviation into a single novelty score. Lower frequency + higher
91
+ distance + higher confidence delta = more novel.
92
+
93
+ Args:
94
+ graph_client: Graph database client for historical lookups
95
+ """
96
+
97
+ def __init__(self, graph_client: ScoringGraphClient) -> None:
98
+ self.graph = graph_client
99
+
100
+ async def score_connection(self, connection: Connectable) -> NoveltyScore:
101
+ """Score novelty of a discovered connection.
102
+
103
+ Computes a weighted composite of three factors:
104
+ - Frequency factor (40%): inverse of historical frequency
105
+ - Domain distance (30%): semantic distance between domains
106
+ - Confidence delta (30%): deviation from expected confidence
107
+
108
+ Args:
109
+ connection: A connection or insight with domain attributes
110
+
111
+ Returns:
112
+ NoveltyScore with composite score and explanations.
113
+ """
114
+ freq = await self._get_historical_frequency(connection)
115
+ distance = await self._compute_domain_distance(
116
+ connection.source_domain,
117
+ connection.target_domain,
118
+ )
119
+ delta = await self._calculate_confidence_delta(connection)
120
+
121
+ # Composite score: lower frequency + higher distance + higher delta = more novel
122
+ frequency_factor = 1.0 / (1.0 + freq)
123
+ score = frequency_factor * 0.4 + distance * 0.3 + delta * 0.3
124
+
125
+ reasons: List[str] = []
126
+ if freq == 0:
127
+ reasons.append("Never observed before")
128
+ if distance > 0.5:
129
+ reasons.append("Cross-domain connection")
130
+ if delta > 0.3:
131
+ reasons.append("Unexpected confidence level")
132
+
133
+ return NoveltyScore(
134
+ score=min(1.0, score),
135
+ reasons=reasons,
136
+ historical_frequency=freq,
137
+ domain_distance=distance,
138
+ confidence_delta=delta,
139
+ )
140
+
141
+ async def _get_historical_frequency(self, connection: Connectable) -> int:
142
+ """Count how many similar connections exist in the graph."""
143
+ source_domain = connection.source_domain
144
+ target_domain = connection.target_domain
145
+
146
+ # Try run_query first (direct Cypher)
147
+ if hasattr(self.graph, 'run_query'):
148
+ try:
149
+ cypher = """
150
+ MATCH (m:Memory {type: "connection"})
151
+ WHERE (m.metadata.source_domain = $source_domain
152
+ AND m.metadata.target_domain = $target_domain)
153
+ OR (m.metadata.source_domain = $target_domain
154
+ AND m.metadata.target_domain = $source_domain)
155
+ RETURN count(m) AS frequency
156
+ """
157
+ records = await self.graph.run_query(cypher, {
158
+ "source_domain": source_domain,
159
+ "target_domain": target_domain,
160
+ })
161
+ if records:
162
+ return int(records[0].get("frequency", 0))
163
+ except Exception as e:
164
+ logger.debug("_get_historical_frequency query failed: %s", e)
165
+
166
+ # Fallback: use recall with domain pair query
167
+ try:
168
+ query = f"{source_domain} {target_domain} connection pattern"
169
+ results = await self.graph.recall(query, limit=50, min_strength=0.1)
170
+ return len(results)
171
+ except Exception as e:
172
+ logger.debug("_get_historical_frequency recall fallback failed: %s", e)
173
+
174
+ return 0
175
+
176
+ async def _compute_domain_distance(self, source: str, target: str) -> float:
177
+ """Compute domain distance from graph confidence data.
178
+
179
+ Queries the graph for mean confidence of historical connections between
180
+ the domain pair. Distance is derived as ``1 - mean_confidence``: high-
181
+ confidence connections indicate domains are closely related (low
182
+ distance); low-confidence connections indicate distant domains (high
183
+ distance). Falls back to ``_calculate_domain_distance`` when the graph
184
+ returns no data.
185
+
186
+ Args:
187
+ source: Source domain name.
188
+ target: Target domain name.
189
+
190
+ Returns:
191
+ Distance between 0 (same / closely related) and 1 (maximally distant).
192
+ """
193
+ if source == target:
194
+ return 0.0
195
+
196
+ if hasattr(self.graph, "run_query"):
197
+ try:
198
+ cypher = """
199
+ MATCH (m:Memory {type: "connection"})
200
+ WHERE ((m.metadata.source_domain = $source_domain
201
+ AND m.metadata.target_domain = $target_domain)
202
+ OR (m.metadata.source_domain = $target_domain
203
+ AND m.metadata.target_domain = $source_domain))
204
+ AND m.metadata.confidence IS NOT NULL
205
+ RETURN avg(toFloat(m.metadata.confidence)) AS mean_confidence
206
+ """
207
+ records = await self.graph.run_query(
208
+ cypher,
209
+ {"source_domain": source, "target_domain": target},
210
+ )
211
+ if records and records[0].get("mean_confidence") is not None:
212
+ mean_conf = float(records[0]["mean_confidence"])
213
+ return max(0.0, min(1.0, 1.0 - mean_conf))
214
+ except Exception as e:
215
+ logger.debug("_compute_domain_distance query failed: %s", e)
216
+
217
+ return self._calculate_domain_distance(source, target)
218
+
219
+ @staticmethod
220
+ def _calculate_domain_distance(source: str, target: str) -> float:
221
+ """Calculate semantic distance between two domains.
222
+
223
+ Uses a precomputed distance matrix. Unknown pairs default to 0.5.
224
+ Same-domain connections have distance 0.
225
+
226
+ Args:
227
+ source: Source domain name
228
+ target: Target domain name
229
+
230
+ Returns:
231
+ Distance between 0 (same domain) and 1 (maximally distant).
232
+ """
233
+ if source == target:
234
+ return 0.0
235
+ return DOMAIN_DISTANCES.get(
236
+ (source, target),
237
+ DOMAIN_DISTANCES.get((target, source), 0.5),
238
+ )
239
+
240
+ async def _calculate_confidence_delta(self, connection: Connectable) -> float:
241
+ """Measure how unexpected the connection's confidence level is."""
242
+ source_domain = connection.source_domain
243
+ target_domain = connection.target_domain
244
+
245
+ # Try run_query first
246
+ if hasattr(self.graph, 'run_query'):
247
+ try:
248
+ cypher = """
249
+ MATCH (m:Memory {type: "connection"})
250
+ WHERE ((m.metadata.source_domain = $source_domain
251
+ AND m.metadata.target_domain = $target_domain)
252
+ OR (m.metadata.source_domain = $target_domain
253
+ AND m.metadata.target_domain = $source_domain))
254
+ AND m.metadata.confidence IS NOT NULL
255
+ RETURN avg(toFloat(m.metadata.confidence)) AS mean_confidence
256
+ """
257
+ records = await self.graph.run_query(cypher, {
258
+ "source_domain": source_domain,
259
+ "target_domain": target_domain,
260
+ })
261
+ if records and records[0].get("mean_confidence") is not None:
262
+ mean_conf = float(records[0]["mean_confidence"])
263
+ return abs(connection.confidence - mean_conf)
264
+ except Exception as e:
265
+ logger.debug("_calculate_confidence_delta query failed: %s", e)
266
+
267
+ return 0.0