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,351 @@
1
+ """Bounded opening of current canonical evidence through the memory read API."""
2
+ from contextlib import closing
3
+ import hashlib
4
+ import json
5
+ import re
6
+
7
+ from .idempotency import canonical_turn_digest, source_message_hash
8
+ from .source_annotations import expand, current_candidates, inputs_unannotated
9
+
10
+
11
+ def input_excerpt(ledger, *, contact_id, session_id, refs, max_chars=240):
12
+ """A short exact admitted input, resolved afresh rather than copied to work.
13
+
14
+ This is what the participant requested, not a generated task summary or
15
+ evidence that the execution is fulfilling it. Multiple inputs and long
16
+ messages remain explicitly partial. Annotated inputs require the normal
17
+ source reader so their conditions cannot be clipped off a work label.
18
+ Exact input membership travels to the existing native freshness check so
19
+ a later annotation can invalidate the excerpt without changing its bytes.
20
+ """
21
+ from .audio import source_text
22
+ watermark = ledger.erasure_watermark(contact_id)
23
+ versions = ledger.resolve_input_dependencies(contact_id=contact_id,
24
+ session_id=session_id, refs=refs)
25
+ available = ledger.source_references([ref['source_id'] for ref in versions],
26
+ contact_id=contact_id, session_id=session_id)
27
+ if not refs or any(ref not in available for ref in versions):
28
+ raise ValueError('source_input_unavailable')
29
+ selected = refs[0]
30
+ with closing(ledger._connect()) as conn:
31
+ source = conn.execute('''SELECT session_id,messages_json FROM turn_sources
32
+ WHERE turn_id=? AND contact_id=? AND (scope='person' OR session_id=?)''',
33
+ (selected['source_id'], contact_id, session_id)).fetchone()
34
+ if source is None:
35
+ raise ValueError('source_input_unavailable')
36
+ matches = [message for message in json.loads(source['messages_json'])
37
+ if message.get('role') == 'user' and source_message_hash(source['session_id'], message)
38
+ == selected['input_message_hash']]
39
+ if len(matches) != 1:
40
+ raise ValueError('source_input_unavailable')
41
+ text = source_text(matches[0].get('content'))
42
+ membership = {}
43
+ for ref in refs:
44
+ membership.setdefault(ref['source_id'], []).append(ref['input_message_hash'])
45
+ candidate = {'id': 'execution-input:' + selected['source_id'], 'kind': 'source_quote',
46
+ 'source_turn_ids': list(membership), 'content': text,
47
+ '_source_message_hashes': membership}
48
+ current = current_candidates(ledger, expand(ledger, [candidate],
49
+ contact_id=contact_id, session_id=session_id), contact_id=contact_id, session_id=session_id)
50
+ if len(current) != 1 or ledger.erasure_watermark(contact_id) != watermark:
51
+ raise ValueError('source_input_unavailable')
52
+ if current[0].get('_annotation_ids') or not inputs_unannotated(ledger, refs):
53
+ return {'status': 'annotated_input_requires_source_read'}
54
+ if not text:
55
+ return {'status': 'input_has_no_text'}
56
+ return {'status': 'admitted_input_excerpt', 'excerpt': text[:max_chars],
57
+ 'partial': len(text) > max_chars or len(refs) > 1,
58
+ 'input_count': len(refs), 'source_id': selected['source_id'],
59
+ 'input_message_hash': selected['input_message_hash'],
60
+ '_provenance': {'contact_id': contact_id, 'watermark': watermark, 'source_refs': versions,
61
+ 'unannotated_input_refs': [dict(ref) for ref in refs]}}
62
+
63
+
64
+ def _document_media(ledger, conn, *, scope, expected, asset_hash, hashes):
65
+ """Serialize exact ownership and bounded original integrity with erasure."""
66
+ from .media import SourceMedia
67
+ from .documents import MAX_DOCUMENT_BYTES
68
+ original = SourceMedia(ledger)
69
+ with conn:
70
+ conn.execute('BEGIN IMMEDIATE')
71
+ owners = original._owned(conn, asset_hash, **scope)
72
+ if not any(owner['turn_id'] == expected['source_id']
73
+ and owner['message_hash'] in hashes
74
+ and canonical_turn_digest(json.loads(owner['messages_json'])) == expected['source_version']
75
+ for owner in owners):
76
+ raise ValueError('source_document_unavailable')
77
+ media = conn.execute('SELECT mime_type,status,size_bytes,media_metadata_json FROM source_media WHERE asset_hash=?',
78
+ (asset_hash,)).fetchone()
79
+ if (media is None or media['mime_type'] != 'application/pdf' or media['status'] == 'orphan'
80
+ or not 0 < media['size_bytes'] <= MAX_DOCUMENT_BYTES):
81
+ raise ValueError('source_document_unavailable')
82
+ try:
83
+ with original.store._original_path(asset_hash, 'application/pdf').open('rb') as stream:
84
+ data = stream.read(MAX_DOCUMENT_BYTES + 1)
85
+ except OSError as exc:
86
+ raise ValueError('source_document_original_unavailable') from exc
87
+ if (len(data) != media['size_bytes'] or len(data) > MAX_DOCUMENT_BYTES
88
+ or hashlib.sha256(data).hexdigest() != asset_hash):
89
+ raise ValueError('source_document_original_integrity_mismatch')
90
+ return media
91
+
92
+
93
+ def _document_page(ledger, conn, *, scope, expected, asset_hash, page, hashes):
94
+ """Open a stored derivative without parsing or returning original bytes."""
95
+ media = _document_media(ledger, conn, scope=scope, expected=expected, asset_hash=asset_hash, hashes=hashes)
96
+ try:
97
+ document = json.loads(media['media_metadata_json'] or '{}').get('document', {})
98
+ if not isinstance(document, dict):
99
+ raise ValueError('invalid document metadata')
100
+ status = {'document_pending': 'pending', 'document_running': 'pending',
101
+ 'document_unsupported': 'unsupported', 'document_failed': 'failed'}.get(media['status'])
102
+ if status is None:
103
+ status = document.get('status')
104
+ if media['status'] != 'complete' or status not in {'complete', 'partial'}:
105
+ raise ValueError('invalid document disposition')
106
+ page_count = document.get('page_count')
107
+ if page_count is not None and (type(page_count) is not int or page_count < 0):
108
+ raise ValueError('invalid document page count')
109
+ if page_count is not None and page > page_count and (page_count > 0 or status in {'complete', 'partial'}):
110
+ raise ValueError('source_document_page_unavailable')
111
+ selected = None
112
+ if status in {'complete', 'partial'}:
113
+ if document.get('version') != 'source-pdf-text-v1' or document.get('ocr_performed') is not False:
114
+ raise ValueError('invalid document derivative')
115
+ pages = document.get('pages')
116
+ if not isinstance(pages, list) or any(not isinstance(item, dict) for item in pages):
117
+ raise ValueError('invalid document pages')
118
+ selected = next((item for item in pages if type(item.get('page')) is int and item['page'] == page), None)
119
+ if selected is not None and (not isinstance(selected.get('text'), str)
120
+ or selected.get('status') not in {'text', 'no_extractable_text'}):
121
+ raise ValueError('invalid document page')
122
+ if selected is None and status == 'complete':
123
+ raise ValueError('source_document_page_unavailable')
124
+ elif status == 'unsupported' and isinstance(document.get('pages'), list):
125
+ # Image-only PDFs can retain honest per-page blank dispositions.
126
+ # Never expose page text from an unsupported derivative.
127
+ selected = next((item for item in document['pages'] if isinstance(item, dict)
128
+ and type(item.get('page')) is int and item['page'] == page
129
+ and item.get('status') == 'no_extractable_text'), None)
130
+ evidence = {'asset_hash': asset_hash, 'asset_id': 'sha256:' + asset_hash,
131
+ 'mime_type': 'application/pdf', 'page': page, 'page_count': page_count,
132
+ 'status': status, 'reason': document.get('reason'),
133
+ 'version': document.get('version'), 'parser': document.get('parser'),
134
+ 'parser_version': document.get('parser_version'), 'ocr_performed': False,
135
+ 'epistemic_state': 'derived_unverified',
136
+ 'page_status': selected['status'] if selected else 'not_extracted'}
137
+ evidence.update({key: document[key] for key in ('memory_control', 'memory_limit_bytes',
138
+ 'memory_sample_interval_ms', 'hard_limit') if key in document})
139
+ text = selected['text'] if selected and selected['status'] == 'text' else ''
140
+ fingerprint = hashlib.sha256(json.dumps([media['status'], document], sort_keys=True).encode()).hexdigest()
141
+ return evidence, text, fingerprint
142
+ except (TypeError, AttributeError, json.JSONDecodeError) as exc:
143
+ raise ValueError('source_document_unavailable') from exc
144
+
145
+
146
+ def read(ledger, *, contact_id, session_id, source_id, source_version,
147
+ view='source', claim_id=None, offset=0, read_revision=None, asset_hash=None, page=None, requested_ms=None):
148
+ if ((view == 'video' and (not isinstance(asset_hash, str) or not re.fullmatch('[0-9a-f]{64}', asset_hash)
149
+ or type(requested_ms) is not int or not 0 <= requested_ms <= 30000 or claim_id or offset))
150
+ or view != 'video' and requested_ms is not None):
151
+ raise ValueError('source_video_requires_asset_hash_and_requested_ms')
152
+ if view == 'document' and (not isinstance(asset_hash, str) or not re.fullmatch('[0-9a-f]{64}', asset_hash)
153
+ or type(page) is not int or page < 1 or claim_id is not None
154
+ or type(offset) is not int or not 0 <= offset <= 10000000 or offset and read_revision is None):
155
+ raise ValueError('source_document_requires_asset_hash_and_page')
156
+ if view != 'document' and page is not None:
157
+ raise ValueError('page_requires_document_view')
158
+ scope = {'contact_id': contact_id, 'session_id': session_id}
159
+ # Stamp before any content read. A concurrent erase then invalidates this
160
+ # result at the existing native request boundary, even after HTTP returns.
161
+ watermark = ledger.erasure_watermark(contact_id)
162
+ expected = {'source_id': source_id, 'source_version': source_version}
163
+ if expected not in ledger.source_references([source_id], **scope):
164
+ raise ValueError('source_unavailable_or_changed')
165
+ with closing(ledger._connect()) as conn:
166
+ source = conn.execute('SELECT * FROM turn_sources WHERE turn_id=?', (source_id,)).fetchone()
167
+ if source is None:
168
+ raise ValueError('source_unavailable_or_changed')
169
+ messages = json.loads(source['messages_json'])
170
+ if canonical_turn_digest(messages) != source_version:
171
+ raise ValueError('source_unavailable_or_changed')
172
+ if view == 'assertions':
173
+ from apsimo.beliefs.source_projection import SourceClaimProjection
174
+ projection = SourceClaimProjection(ledger)
175
+ anchor = projection._rows(conn, **scope, ids=[claim_id])
176
+ if not anchor or anchor[0]['turn_id'] != source_id:
177
+ raise ValueError('source_claim_unavailable')
178
+ # Includes superseded/retracted assertions with their exact status,
179
+ # not a new selection of a winning fact. Output pages are bounded.
180
+ rows = projection._rows(conn, **scope, key=(anchor[0]['subject_key'], anchor[0]['predicate']), limit=10001)
181
+ if len(rows) > 10000:
182
+ raise ValueError('source_history_exceeds_read_limit')
183
+ rows.sort(key=lambda c: (c['recorded_at'], c['id']))
184
+ notes = [tuple(r) for r in conn.execute('''SELECT a.annotation_source_id,a.request_sha256
185
+ FROM source_annotations a JOIN turn_sources s ON s.turn_id=a.annotation_source_id
186
+ WHERE s.contact_id=? AND (s.scope='person' OR s.session_id=?)
187
+ ORDER BY a.annotation_source_id''', (contact_id, session_id))]
188
+ revision = hashlib.sha256(json.dumps([rows, notes, watermark], sort_keys=True).encode()).hexdigest()
189
+ selected = rows[offset:offset + 8]
190
+ total, next_offset = len(rows), offset + len(selected)
191
+ ids = list(dict.fromkeys([source_id, *(c['turn_id'] for c in selected)]))
192
+ retained_ids = {c['id'] for c in rows}
193
+ episode_gap = any(c.get('representation') == 'episode'
194
+ and any(c.get(link) and c[link] not in retained_ids
195
+ for link in ('prior_claim_id', 'retracted_by', 'superseded_by'))
196
+ for c in rows)
197
+ content = json.dumps({'status': 'attributed_assertion_history',
198
+ **({'episode_history': 'incomplete_revision_chain',
199
+ 'guidance': 'An episode revision is unavailable. Do not reconstruct current '
200
+ 'details from older reports. Complete pagination does not close this gap.'}
201
+ if episode_gap else {}), 'assertions': [
202
+ {key: c.get(key) for key in ('id', 'turn_id', 'role', 'subject', 'predicate', 'value', 'evidence',
203
+ 'observed_at', 'recorded_at', 'valid_from', 'valid_to', 'event_at', 'event_time',
204
+ 'validity_basis', 'operation', 'prior_claim_id', 'superseded_by', 'retracted_by')}
205
+ | {key: c[key] for key in ('representation', 'epistemic_state', 'source_modality', 'evidence_basis') if key in c}
206
+ for c in selected]}, ensure_ascii=False)
207
+ hashes = {identifier: [c['message_hash'] for c in selected if c['turn_id'] == identifier]
208
+ for identifier in ids}
209
+ hashes[source_id] = list({*hashes.get(source_id, []), anchor[0]['message_hash']})
210
+ elif view == 'document':
211
+ selected = [message for message in messages if isinstance(message.get('content'), list)
212
+ and any(isinstance(block, dict) and block.get('type') == 'document'
213
+ and block.get('asset_id') == 'sha256:' + asset_hash
214
+ and block.get('mime_type') == 'application/pdf'
215
+ for block in message['content'])]
216
+ if not selected:
217
+ raise ValueError('source_document_unavailable')
218
+ ids = [source_id]
219
+ hashes = {source_id: [source_message_hash(source['session_id'], m) for m in selected]}
220
+ document, page_text, derivative = _document_page(ledger, conn, scope=scope, expected=expected,
221
+ asset_hash=asset_hash, page=page, hashes=hashes[source_id])
222
+ content = json.dumps({'document': {**document, 'text': page_text},
223
+ 'source_messages': [{'message_hash': source_message_hash(source['session_id'], m),
224
+ 'role': m['role']} for m in selected],
225
+ 'reported_at': source['occurred_at'], 'recorded_at': source['ingested_at'],
226
+ 'event_time': 'unknown unless supported by the source'}, ensure_ascii=False)
227
+ elif view in {'image', 'video'}:
228
+ selected = [message for message in messages if isinstance(message.get('content'), list)
229
+ and any(isinstance(block, dict) and block.get('type') == view
230
+ and block.get('asset_id') == 'sha256:' + str(asset_hash)
231
+ for block in message['content'])]
232
+ if not selected or offset or claim_id:
233
+ raise ValueError('source_' + view + '_unavailable')
234
+ ids = [source_id]
235
+ hashes = {source_id: [source_message_hash(source['session_id'], m) for m in selected]}
236
+ content = json.dumps({'asset_id': 'sha256:' + asset_hash,
237
+ **({'requested_ms': requested_ms, 'timestamp_origin': 'first_decoded_frame'} if view == 'video' else {}),
238
+ 'source_messages': [{'message_hash': source_message_hash(source['session_id'], m),
239
+ 'role': m['role']} for m in selected],
240
+ 'reported_at': source['occurred_at'], 'recorded_at': source['ingested_at'],
241
+ 'event_time': 'unknown unless supported by the source'}, ensure_ascii=False)
242
+ else:
243
+ ids = [source_id]
244
+ hashes = {source_id: [source_message_hash(source['session_id'], m) for m in messages]}
245
+ content = json.dumps({'messages': messages, 'reported_at': source['occurred_at'],
246
+ 'recorded_at': source['ingested_at'],
247
+ 'event_time': 'unknown unless explicitly stated in each source'}, ensure_ascii=False)
248
+ candidate = {'id': 'opened:' + source_id, 'kind': 'source_quote', 'source_uri': 'turn:' + source_id,
249
+ 'source_turn_ids': ids, '_source_message_hashes': hashes, 'content': content}
250
+ expanded = current_candidates(ledger, expand(ledger, [candidate], **scope), **scope)
251
+ if len(expanded) != 1:
252
+ raise ValueError('source_correction_unavailable_or_changed')
253
+ row = expanded[0]
254
+ refs = row.get('_annotation_source_refs', [])
255
+ if expected not in refs or any(ref not in ledger.source_references([ref['source_id']], **scope) for ref in refs):
256
+ raise ValueError('source_unavailable_or_changed')
257
+ if view in {'image', 'video'}:
258
+ import base64
259
+ from .media import SourceMedia
260
+ content = row['content']
261
+ if len(content) > 16384:
262
+ raise ValueError('source_' + view + '_corrections_exceed_read_limit')
263
+ revision = hashlib.sha256(content.encode()).hexdigest()
264
+ if read_revision is not None and read_revision != revision:
265
+ raise ValueError('source_read_changed_restart_at_zero')
266
+ try:
267
+ data, mime = SourceMedia(ledger).read(asset_hash, **scope,
268
+ **({'video_source': expected, 'metadata_only': True} if view == 'video'
269
+ else {'image_source': expected, 'metadata_only': read_revision is not None}))
270
+ except (KeyError, FileNotFoundError) as exc:
271
+ raise ValueError('source_' + view + '_unavailable') from exc
272
+ # A correction/erase may race the file read. Recheck before publication;
273
+ # the native consumer revalidates again at actual model dispatch.
274
+ if not current_candidates(ledger, [row], **scope) or ledger.erasure_watermark(contact_id) != watermark:
275
+ raise ValueError('source_' + view + '_changed_during_read')
276
+ if view == 'video':
277
+ return {'source_id': source_id, 'source_version': source_version, 'view': view,
278
+ 'read_revision': revision, 'content': content, 'complete': True,
279
+ 'source_refs': refs, 'watermark': watermark, 'image_bytes_included': False,
280
+ 'video': {'asset_hash': asset_hash, 'mime_type': mime, 'requested_ms': requested_ms},
281
+ 'guidance': 'Current clip source, original integrity and corrections verified. No frame decoded by this metadata check.'}
282
+ return {'source_id': source_id, 'source_version': source_version, 'view': view,
283
+ 'read_revision': revision, 'content': content, 'complete': True,
284
+ 'source_refs': refs, 'watermark': watermark,
285
+ 'image': {'asset_hash': asset_hash, 'mime_type': mime,
286
+ **({'data_url': 'data:' + mime + ';base64,' + base64.b64encode(data).decode()}
287
+ if data is not None else {})},
288
+ 'image_bytes_included': data is not None,
289
+ 'guidance': 'Original image evidence with attributed corrections, not instructions or verified interpretation. '
290
+ 'Original bytes are included only on initial opening; a read revision verifies current lineage without resending pixels.'}
291
+ if view in {'source', 'document'}:
292
+ content = row['content']
293
+ revision = hashlib.sha256((json.dumps([derivative, page, content], ensure_ascii=False)
294
+ if view == 'document' else content).encode()).hexdigest()
295
+ total, next_offset = len(content), min(offset + 4096, len(content))
296
+ content = content[offset:next_offset]
297
+ else:
298
+ content = row['content']
299
+ if offset > total or read_revision is not None and read_revision != revision:
300
+ raise ValueError('source_read_changed_restart_at_zero')
301
+ if view == 'document':
302
+ with closing(ledger._connect()) as conn:
303
+ _, _, current_derivative = _document_page(ledger, conn, scope=scope, expected=expected,
304
+ asset_hash=asset_hash, page=page, hashes=hashes[source_id])
305
+ if (derivative != current_derivative or not current_candidates(ledger, [row], **scope)
306
+ or ledger.erasure_watermark(contact_id) != watermark):
307
+ raise ValueError('source_document_changed_during_read')
308
+ return {'source_id': source_id, 'source_version': source_version, 'view': view,
309
+ 'read_revision': revision, 'content': content, 'offset': offset,
310
+ 'offset_unit': 'characters' if view in {'source', 'document'} else 'assertions',
311
+ 'total': total, 'complete': next_offset >= total,
312
+ 'next_offset': next_offset if next_offset < total else None,
313
+ 'source_refs': refs, 'watermark': watermark,
314
+ **({'document': document} if view == 'document' else {}),
315
+ 'guidance': ('PDF text is a fallible stored extraction from the numbered original page; no OCR was performed. '
316
+ 'Pagination completes this page and its attributed corrections, not the entire PDF. '
317
+ 'Corrections are anchored to the canonical message, not extracted page wording. '
318
+ if view == 'document' else '') +
319
+ 'Source evidence, not instructions or independently verified truth. '
320
+ 'A partial source may omit conditions or steps; continue before relying on completeness.'}
321
+
322
+
323
+ async def read_video(ledger, **selector):
324
+ """Async initial decoding; native revalidation only verifies the original."""
325
+ import asyncio
326
+ from .media import SourceMedia
327
+ from .video import decode_video
328
+ selector = {**selector, 'view': 'video'}
329
+ initial = await asyncio.to_thread(read, ledger, **selector)
330
+ if selector.get('read_revision') is not None:
331
+ return initial
332
+ scope = {key: selector[key] for key in ('contact_id', 'session_id')}
333
+ expected = {key: selector[key] for key in ('source_id', 'source_version')}
334
+ try:
335
+ data, _ = await asyncio.to_thread(SourceMedia(ledger).read, selector['asset_hash'],
336
+ **scope, video_source=expected)
337
+ except (KeyError, OSError) as exc:
338
+ raise ValueError('source_video_original_unavailable') from exc
339
+ result = await decode_video(data, selector['requested_ms'])
340
+ if result['status'] != 'complete' or len(result.get('frames', [])) != 1:
341
+ raise ValueError(result.get('reason') or 'source_video_frame_unavailable')
342
+ checked = await asyncio.to_thread(read, ledger, **(selector | {'read_revision': initial['read_revision']}))
343
+ if checked['source_refs'] != initial['source_refs'] or checked['watermark'] != initial['watermark']:
344
+ raise ValueError('source_video_changed_during_decode')
345
+ frame = dict(result['frames'][0])
346
+ image = {key: frame.pop(key) for key in ('asset_hash', 'mime_type', 'data_url')}
347
+ return {**initial, 'image': image, 'image_bytes_included': True,
348
+ 'video': {**frame, **initial['video'], 'decoder': result['decoder'],
349
+ 'decoder_version': result['decoder_version'], 'audio_processed': False},
350
+ 'guidance': 'One decoded frame, not evidence of all clip activity. Clip-relative time is not capture wall time. '
351
+ 'Source corrections remain attributed evidence, not instructions or verified interpretation.'}
@@ -0,0 +1,263 @@
1
+ """Replaceable semantic projections of canonical quotations and image descriptions.
2
+
3
+ Jobs share the source ledger. Search is scoped before nearest-neighbor selection;
4
+ every result is re-read from its current canonical message or asset description.
5
+ Vector text is never an authoritative source, and retrieval never resolves truth.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from contextlib import closing
11
+ import hashlib
12
+ import json
13
+ import time
14
+ import uuid
15
+
16
+ FORMAT = 'canonical-source-chunks-v1'
17
+ CHUNK = 2000
18
+ STRIDE = 1800
19
+
20
+
21
+ def initialize(conn):
22
+ conn.execute('''CREATE TABLE IF NOT EXISTS source_vector_jobs (
23
+ turn_id TEXT PRIMARY KEY,generation_id TEXT NOT NULL DEFAULT '',
24
+ cursor INTEGER NOT NULL DEFAULT 0,status TEXT NOT NULL DEFAULT 'pending',
25
+ lease_token TEXT NOT NULL DEFAULT '',lease_until REAL NOT NULL DEFAULT 0,
26
+ next_attempt REAL NOT NULL DEFAULT 0,error TEXT)''')
27
+
28
+
29
+ def enqueue(conn, turn_id):
30
+ conn.execute('''INSERT INTO source_vector_jobs(turn_id) VALUES (?)
31
+ ON CONFLICT(turn_id) DO UPDATE SET generation_id='',cursor=0,status='pending',
32
+ lease_token='',lease_until=0,next_attempt=0,error=NULL''', (turn_id,))
33
+
34
+
35
+ def _hash(value):
36
+ return hashlib.sha256(value.encode()).hexdigest()
37
+
38
+
39
+ def _scope_key(scope, contact_id, session_id):
40
+ return _hash(json.dumps([scope, contact_id, session_id if scope == 'session' else ''], separators=(',', ':')))
41
+
42
+
43
+ def _text(message):
44
+ from apsimo.turns.audio import source_text
45
+ return source_text(message.get('content'))
46
+
47
+
48
+ def _metadata(source, message_hash, *, kind, text, **extra):
49
+ meta = {'source_projection': FORMAT, 'source_uri': 'turn:' + source['turn_id'],
50
+ 'source_turn_id': source['turn_id'], 'source_message_hash': message_hash,
51
+ 'contact_id': source['contact_id'], 'session_id': source['session_id'],
52
+ 'scope_key': _scope_key(source['scope'], source['contact_id'], source['session_id']),
53
+ 'kind': kind, 'content_hash': _hash(text), **extra}
54
+ meta['projection_id'] = 'source-vector:' + _hash(json.dumps(meta, sort_keys=True, separators=(',', ':')))
55
+ return meta
56
+
57
+
58
+ def chunks(conn, source):
59
+ from apsimo.turns.idempotency import source_message_hash
60
+ for message in json.loads(source['messages_json']):
61
+ message_hash = source_message_hash(source['session_id'], message)
62
+ text = _text(message)
63
+ for start in range(0, len(text), STRIDE):
64
+ chunk = text[start:start + CHUNK]
65
+ if chunk.strip():
66
+ yield chunk, _metadata(source, message_hash, kind='source_quote', text=chunk, start=start)
67
+ for asset in conn.execute('''SELECT DISTINCT m.asset_hash,m.description FROM source_media m
68
+ JOIN source_media_links l ON l.asset_hash=m.asset_hash
69
+ WHERE l.turn_id=? AND l.message_hash=? AND m.status='complete' AND m.description IS NOT NULL''',
70
+ (source['turn_id'], message_hash)):
71
+ yield asset['description'], _metadata(source, message_hash, kind='media_description',
72
+ text=asset['description'], asset_hash=asset['asset_hash'])
73
+
74
+
75
+ def hydrate(ledger, meta, *, contact_id=None, session_id=None):
76
+ """Validate exact lineage and return current source bytes, never vector text."""
77
+ if meta.get('source_projection') != FORMAT:
78
+ return None
79
+ from apsimo.turns.idempotency import source_message_hash
80
+ with closing(ledger._connect()) as conn:
81
+ from apsimo.turns.source_attribution import is_invalidated
82
+ if is_invalidated(conn, meta.get('source_turn_id')):
83
+ return None
84
+ source = conn.execute('SELECT * FROM turn_sources WHERE turn_id=?', (meta.get('source_turn_id'),)).fetchone()
85
+ if source is None or meta.get('contact_id') != source['contact_id'] or meta.get('session_id') != source['session_id']:
86
+ return None
87
+ if meta.get('source_uri') != 'turn:' + source['turn_id']:
88
+ return None
89
+ if contact_id is not None and (source['contact_id'] != contact_id or
90
+ source['scope'] != 'person' and source['session_id'] != session_id):
91
+ return None
92
+ if meta.get('scope_key') != _scope_key(source['scope'], source['contact_id'], source['session_id']):
93
+ return None
94
+ message = next((m for m in json.loads(source['messages_json'])
95
+ if source_message_hash(source['session_id'], m) == meta.get('source_message_hash')), None)
96
+ if message is None:
97
+ return None
98
+ common = {'turn_id': source['turn_id'], 'role': message['role'], 'session_id': source['session_id'],
99
+ 'scope': source['scope'], 'occurred_at': source['occurred_at'], 'ingested_at': source['ingested_at'],
100
+ 'source_message_hash': meta['source_message_hash'], 'retrieval_method': 'semantic'}
101
+ from apsimo.turns.audio import evidence_metadata
102
+ common.update(evidence_metadata(message))
103
+ if meta.get('kind') == 'source_quote':
104
+ start = meta.get('start')
105
+ if not isinstance(start, int) or start < 0 or start % STRIDE:
106
+ return None
107
+ message_text = _text(message)
108
+ text = message_text[start:start + CHUNK]
109
+ if not text.strip() or _hash(text) != meta.get('content_hash'):
110
+ return None
111
+ return {**common, 'content': text,
112
+ **({'excerpt_truncated': True} if text != message_text else {})}
113
+ if meta.get('kind') == 'media_description':
114
+ asset = conn.execute('''SELECT m.* FROM source_media m JOIN source_media_links l ON l.asset_hash=m.asset_hash
115
+ WHERE l.turn_id=? AND l.message_hash=? AND m.asset_hash=? AND m.status='complete' LIMIT 1''',
116
+ (source['turn_id'], meta['source_message_hash'], meta.get('asset_hash'))).fetchone()
117
+ if asset is None or not asset['description'] or _hash(asset['description']) != meta.get('content_hash'):
118
+ return None
119
+ return {**common, 'id': 'media:' + asset['asset_hash'], 'kind': 'media_description',
120
+ 'source_uri': 'turn:' + source['turn_id'], 'source_turn_id': source['turn_id'],
121
+ 'asset_id': 'sha256:' + asset['asset_hash'], 'epistemic_state': 'derived_unverified',
122
+ 'description_model': asset['model'], 'description_version': asset['description_version'],
123
+ 'content': asset['description'], 'relevance': 1 / 61}
124
+ return None
125
+
126
+
127
+ class SourceVectors:
128
+ def __init__(self, ledger, store, pipeline):
129
+ self.ledger, self.store, self.pipeline = ledger, store, pipeline
130
+
131
+ def backfill(self):
132
+ # Startup only. Source-only projection never replays actions or inference
133
+ # that creates beliefs, affect, authority or commitments.
134
+ with closing(self.ledger._connect()) as conn, conn:
135
+ conn.execute('INSERT OR IGNORE INTO source_vector_jobs(turn_id) SELECT turn_id FROM turn_sources')
136
+
137
+ def status(self, contact_id):
138
+ catalog = getattr(self.store, 'catalog', None)
139
+ active = catalog.active() if catalog is not None else None
140
+ generation = active['id'] if active else ''
141
+ compatible = bool(active and self.pipeline is not None
142
+ and self.store.identity == self.pipeline.index_identity
143
+ and active['fingerprint'] == self.store.identity.fingerprint)
144
+ with closing(self.ledger._connect()) as conn:
145
+ rows = conn.execute('''SELECT j.status,j.generation_id,j.error FROM turn_sources s
146
+ LEFT JOIN source_vector_jobs j ON j.turn_id=s.turn_id WHERE s.contact_id=?''', (contact_id,)).fetchall()
147
+ complete = sum(row['status'] == 'complete' and row['generation_id'] == generation for row in rows)
148
+ return {'index_state': 'compatible' if compatible else 'unverified_or_unavailable',
149
+ 'active_generation': generation or None, 'source_turns': len(rows),
150
+ 'projected_turns': complete, 'pending_turns': len(rows) - complete,
151
+ 'failed_turns': sum(bool(row['error']) for row in rows)}
152
+
153
+ def _claim(self, generation):
154
+ now = time.time()
155
+ with closing(self.ledger._connect()) as conn, conn:
156
+ conn.execute('BEGIN IMMEDIATE')
157
+ row = conn.execute('''SELECT j.* FROM source_vector_jobs j JOIN turn_sources s ON s.turn_id=j.turn_id
158
+ WHERE (j.status<>'complete' OR j.generation_id<>?) AND j.lease_until<=? AND j.next_attempt<=?
159
+ ORDER BY j.next_attempt,s.ingested_at,j.turn_id LIMIT 1''', (generation['id'], now, now)).fetchone()
160
+ if row is None:
161
+ return None
162
+ cursor = row['cursor'] if row['generation_id'] == generation['id'] else 0
163
+ token = uuid.uuid4().hex
164
+ conn.execute("UPDATE source_vector_jobs SET generation_id=?,cursor=?,status='running',lease_token=?,lease_until=? WHERE turn_id=?",
165
+ (generation['id'], cursor, token, now + 60, row['turn_id']))
166
+ return dict(row, cursor=cursor, generation_id=generation['id'], lease_token=token)
167
+
168
+ def _finish(self, job, *, cursor, complete=False, error=None):
169
+ with closing(self.ledger._connect()) as conn, conn:
170
+ conn.execute('''UPDATE source_vector_jobs SET cursor=?,status=?,lease_until=0,next_attempt=?,error=?
171
+ WHERE turn_id=? AND lease_token=?''', (cursor, 'complete' if complete else 'pending',
172
+ time.time() + 30 if error else (0 if complete else time.time()),
173
+ error, job['turn_id'], job['lease_token']))
174
+
175
+ async def _table(self, generation):
176
+ from apsimo.vector.collections import Collection
177
+ table = await self.store._table(Collection.CONVERSATIONS, write=True, generation=generation)
178
+ if 'scope_key' not in (await table.schema()).names:
179
+ await table.add_columns({'scope_key': 'CAST(NULL AS STRING)'})
180
+ return table
181
+
182
+ async def process_one(self, batch_size=16):
183
+ from apsimo.vector.collections import Collection
184
+ from apsimo.vector.indexes import IncompatibleIndex
185
+ if self.store is None or self.pipeline is None or getattr(self.store, 'catalog', None) is None:
186
+ return False
187
+ if self.store.identity != self.pipeline.index_identity:
188
+ return False
189
+ try:
190
+ generation = self.store.catalog.write_generation(self.store.identity)
191
+ except IncompatibleIndex:
192
+ return False # Unknown legacy stays lexical until an explicit rebuild.
193
+ job = self._claim(generation)
194
+ if job is None:
195
+ return False
196
+ try:
197
+ with closing(self.ledger._connect()) as conn:
198
+ source = conn.execute('SELECT * FROM turn_sources WHERE turn_id=?', (job['turn_id'],)).fetchone()
199
+ if source is None:
200
+ return True
201
+ import itertools
202
+ batch = list(itertools.islice(chunks(conn, source), job['cursor'], job['cursor'] + min(16, max(1, batch_size)) + 1))
203
+ more = len(batch) > min(16, max(1, batch_size))
204
+ batch = batch[:min(16, max(1, batch_size))]
205
+ if batch:
206
+ vectors = await asyncio.wait_for(self.pipeline.embed_batch([text for text, _ in batch]), 40)
207
+ if len(vectors) != len(batch):
208
+ raise ValueError('source embedding cardinality mismatch')
209
+ for vector in vectors:
210
+ self.store._validate_vector(vector)
211
+ table = await self._table(generation)
212
+ for (text, meta), vector in zip(batch, vectors):
213
+ if not self.store._eligible(Collection.CONVERSATIONS, meta['projection_id'], meta):
214
+ continue
215
+ now = time.time()
216
+ meta['embedding_fingerprint'] = self.store.identity.fingerprint
217
+ row = {'id': meta['projection_id'], 'text': text, 'vector': vector, 'metadata': json.dumps(meta),
218
+ 'scope_key': meta['scope_key'], 'modality': 'text', 'image_hash': '', 'image_ref': '',
219
+ 'thumbnail_ref': '', 'caption': '', 'created_at': now, 'updated_at': now}
220
+ await table.merge_insert('id').when_matched_update_all().when_not_matched_insert_all().execute([row])
221
+ if not self.store._eligible(Collection.CONVERSATIONS, meta['projection_id'], meta):
222
+ await self.store.delete(Collection.CONVERSATIONS, meta['projection_id'])
223
+ self._finish(job, cursor=job['cursor'] + len(batch), complete=not more)
224
+ except asyncio.CancelledError:
225
+ raise
226
+ except Exception as exc:
227
+ self._finish(job, cursor=job['cursor'], error=type(exc).__name__)
228
+ return True
229
+
230
+ async def search(self, query, *, contact_id, session_id, limit=10):
231
+ from apsimo.vector.collections import Collection
232
+ from apsimo.vector.indexes import IncompatibleIndex
233
+ if not contact_id or self.store is None or self.pipeline is None or getattr(self.store, 'catalog', None) is None:
234
+ return [], []
235
+ if self.store.identity != self.pipeline.index_identity:
236
+ raise IncompatibleIndex('The source query pipeline and index identity do not match')
237
+ # Validate generation before spending inference, and never read an
238
+ # unscoped top-K followed by filtering away another person's results.
239
+ generation = self.store.catalog.read_generation(self.store.identity)
240
+ table = await self.store._table(Collection.CONVERSATIONS, generation=generation)
241
+ if 'scope_key' not in (await table.schema()).names:
242
+ return [], []
243
+ keys = [_scope_key('person', contact_id, session_id), _scope_key('session', contact_id, session_id)]
244
+ vector = await asyncio.wait_for(self.pipeline.embed_query(query), 5)
245
+ hits = await self.store.search(Collection.CONVERSATIONS, vector, limit=min(25, max(1, limit)),
246
+ filter='scope_key IN (' + ','.join(self.store._quoted(key) for key in keys) + ')')
247
+ sources, media = [], []
248
+ for hit in hits:
249
+ current = hydrate(self.ledger, hit.metadata, contact_id=contact_id, session_id=session_id)
250
+ if current is not None:
251
+ (media if current.get('kind') == 'media_description' else sources).append(current)
252
+ return sources, media
253
+
254
+
255
+ def merge_source_hits(lexical, semantic):
256
+ """Fuse ranks from both candidate producers, keeping exact source lineage."""
257
+ scores, rows = {}, {}
258
+ for candidates in (lexical, semantic):
259
+ for rank, row in enumerate(candidates, 1):
260
+ key = (row['turn_id'], row.get('source_message_hash'), row['role'], row['content'])
261
+ scores[key] = scores.get(key, 0) + 1 / (60 + rank)
262
+ rows[key] = {**rows.get(key, {}), **row}
263
+ return [rows[key] for key in sorted(scores, key=scores.get, reverse=True)]