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
apsimo/vector/store.py ADDED
@@ -0,0 +1,533 @@
1
+ """Colony Vector Store — LanceDB wrapper.
2
+
3
+ Provides CRUD + ANN search across typed collections. Each
4
+ ``Collection`` maps to a separate LanceDB table. All vector
5
+ operations degrade gracefully when the store is not initialized.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import math
13
+ import os
14
+ from pathlib import Path
15
+ import re
16
+ import time
17
+ from typing import Any, Optional
18
+
19
+ import pyarrow as pa
20
+
21
+ from apsimo.vector.collections import Collection
22
+ from apsimo.vector.query import VectorItem, VectorResult
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # LanceDB's json_extract() requires LargeBinary but our metadata column is Utf8.
27
+ # Detect filters that use json_extract(metadata, ...) so we can apply them in Python.
28
+ _METADATA_JSON_FILTER_RE = re.compile(
29
+ r"\bjson_extract\s*\(\s*metadata\b", re.IGNORECASE
30
+ )
31
+
32
+ # Parse individual json_extract(metadata, '$.key') <op> <value> conditions.
33
+ _JSON_COND_RE = re.compile(
34
+ r"json_extract\s*\(\s*metadata\s*,\s*['\"]?\$\.(\w+)['\"]?\s*\)"
35
+ r"\s*(>=|<=|!=|<>|>|<|=|LIKE)"
36
+ r"\s*(%?'[^']*'%?|%?\"[^\"]*\"%?|[-\d.]+)",
37
+ re.IGNORECASE,
38
+ )
39
+
40
+
41
+ def _eval_metadata_filter(meta_str: str, filter_expr: str) -> bool:
42
+ """Evaluate a filter expression containing json_extract(metadata,...) in Python.
43
+
44
+ Handles the patterns actually used in the codebase:
45
+ - ``json_extract(metadata, '$.key') >= <number>``
46
+ - ``json_extract(metadata, '$.key') LIKE '%value%'``
47
+ Multiple conditions are treated as AND (all must pass).
48
+ """
49
+ try:
50
+ meta = json.loads(meta_str) if meta_str else {}
51
+ except (json.JSONDecodeError, TypeError):
52
+ meta = {}
53
+
54
+ for match in _JSON_COND_RE.finditer(filter_expr):
55
+ key = match.group(1)
56
+ op = match.group(2).upper()
57
+ raw_val = match.group(3).strip()
58
+
59
+ meta_val = meta.get(key)
60
+
61
+ if op == "LIKE":
62
+ # Strip surrounding % and quotes for a substring match
63
+ pattern = raw_val.strip("'\"").replace("%", "")
64
+ if pattern not in str(meta_val if meta_val is not None else ""):
65
+ return False
66
+ continue
67
+
68
+ # Numeric or string comparison — strip quotes first
69
+ val_str = raw_val.strip("'\"")
70
+ try:
71
+ num_val = float(val_str)
72
+ num_meta = float(meta_val) if meta_val is not None else 0.0
73
+ if op == ">=" and not (num_meta >= num_val):
74
+ return False
75
+ elif op == "<=" and not (num_meta <= num_val):
76
+ return False
77
+ elif op == ">" and not (num_meta > num_val):
78
+ return False
79
+ elif op == "<" and not (num_meta < num_val):
80
+ return False
81
+ elif op == "=" and not (num_meta == num_val):
82
+ return False
83
+ elif op in ("!=", "<>") and not (num_meta != num_val):
84
+ return False
85
+ except (ValueError, TypeError):
86
+ str_meta = str(meta_val if meta_val is not None else "")
87
+ if op == "=" and str_meta != val_str:
88
+ return False
89
+ elif op in ("!=", "<>") and str_meta == val_str:
90
+ return False
91
+
92
+ return True
93
+
94
+
95
+ def _base_schema(dims: int) -> pa.Schema:
96
+ """Build the Arrow schema shared by all collection tables."""
97
+ return pa.schema([
98
+ pa.field("id", pa.utf8()),
99
+ pa.field("text", pa.utf8()),
100
+ pa.field("vector", pa.list_(pa.float32(), dims)),
101
+ pa.field("metadata", pa.utf8()),
102
+ pa.field("scope_key", pa.utf8()),
103
+ pa.field("modality", pa.utf8()),
104
+ pa.field("image_hash", pa.utf8()),
105
+ pa.field("image_ref", pa.utf8()),
106
+ pa.field("thumbnail_ref", pa.utf8()),
107
+ pa.field("caption", pa.utf8()),
108
+ pa.field("created_at", pa.float64()),
109
+ pa.field("updated_at", pa.float64()),
110
+ ])
111
+
112
+
113
+ class VectorStore:
114
+ """LanceDB-backed vector store. One table per Collection."""
115
+
116
+ def __init__(self, data_dir: str, *, identity=None, catalog=None) -> None:
117
+ self._data_dir = data_dir
118
+ self._db = None
119
+ self._dims: int | None = None
120
+ self.identity = identity
121
+ self.catalog = catalog
122
+ if (identity is None) != (catalog is None):
123
+ raise ValueError('Managed indexes require both embedding identity and source-ledger catalog')
124
+ self._generation_dbs = {}
125
+
126
+ async def connect(self, dimensions: int) -> None:
127
+ """Open (or create) the LanceDB database directory."""
128
+ import lancedb
129
+
130
+ os.makedirs(self._data_dir, exist_ok=True)
131
+ self._db = await lancedb.connect_async(self._data_dir)
132
+ self._dims = dimensions
133
+ if self.identity is not None and self.identity.dimensions != dimensions:
134
+ raise ValueError('Embedding identity dimension disagrees with selected pipeline')
135
+ if self.catalog is not None:
136
+ self.catalog.initialize(self.identity, legacy_exists=bool(await self._db.table_names()))
137
+ self._generation_dbs['legacy'] = self._db
138
+ logger.info("VectorStore connected (path=%s, dims=%d)", self._data_dir, dimensions)
139
+
140
+ async def ensure_collections(self, dimensions: int) -> None:
141
+ """Create any missing collection tables."""
142
+ if self._db is None:
143
+ await self.connect(dimensions)
144
+
145
+ db = self._db
146
+ if self.catalog is not None:
147
+ active = self.catalog.active()
148
+ if active['fingerprint'] != self.identity.fingerprint:
149
+ return # Never relabel or overwrite an existing unknown index.
150
+ db = await self._generation_db(active)
151
+ existing = set(await db.table_names())
152
+ schema = _base_schema(dimensions)
153
+ for col in Collection:
154
+ if col.value not in existing:
155
+ await db.create_table(col.value, schema=schema)
156
+ logger.info("Created vector collection: %s", col.value)
157
+
158
+ async def _generation_db(self, generation):
159
+ key = generation['id']
160
+ if key not in self._generation_dbs:
161
+ import lancedb
162
+ if not re.fullmatch(r'[0-9a-f]{32}', key):
163
+ raise ValueError('Invalid vector generation identifier')
164
+ path = Path(self._data_dir) / 'generations' / key
165
+ path.mkdir(parents=True, exist_ok=True)
166
+ self._generation_dbs[key] = await lancedb.connect_async(str(path))
167
+ return self._generation_dbs[key]
168
+
169
+ async def _table(self, collection, *, write=False, semantic=False, generation=None):
170
+ if self.catalog is None:
171
+ return await self._db.open_table(collection.value)
172
+ if generation is None:
173
+ generation = (self.catalog.write_generation(self.identity) if write else
174
+ self.catalog.read_generation(self.identity) if semantic else self.catalog.active())
175
+ db = await self._generation_db(generation)
176
+ if write and collection.value not in await db.table_names():
177
+ await db.create_table(collection.value, schema=_base_schema(generation['identity']['dimensions']), exist_ok=True)
178
+ return await db.open_table(collection.value)
179
+
180
+ def _eligible(self, collection, entry_id, metadata):
181
+ return self.catalog is None or not (
182
+ self.catalog.deleted(collection.value, str(entry_id)) or self.catalog.source_erased(metadata))
183
+
184
+ def _validate_vector(self, vector):
185
+ if self._dims and len(vector) != self._dims:
186
+ raise ValueError(f'Vector dimension mismatch: expected {self._dims}, got {len(vector)}; rebuild a separate index generation')
187
+ if not vector or not all(math.isfinite(value) for value in vector) or not any(vector):
188
+ raise ValueError('Vector must contain finite nonzero values')
189
+
190
+ @staticmethod
191
+ def _quoted(value):
192
+ return "'" + str(value).replace("'", "''") + "'"
193
+
194
+ # ------------------------------------------------------------------
195
+ # CRUD
196
+ # ------------------------------------------------------------------
197
+
198
+ async def add(
199
+ self,
200
+ collection: Collection,
201
+ id: str,
202
+ text: str,
203
+ vector: list[float],
204
+ metadata: dict[str, Any] | None = None,
205
+ ) -> None:
206
+ """Add a single vector entry."""
207
+ now = time.time()
208
+ meta = metadata or {}
209
+ meta.setdefault("embedded_at", now)
210
+ meta.setdefault("modality", "text")
211
+
212
+ # Dimension validation
213
+ self._validate_vector(vector)
214
+ if not self._eligible(collection, id, meta):
215
+ return
216
+ if self.identity is not None:
217
+ meta = {**meta, 'embedding_fingerprint': self.identity.fingerprint,
218
+ 'requested_model': self.identity.requested_model,
219
+ 'served_model': self.identity.served_model,
220
+ 'declared_revision': self.identity.declared_revision}
221
+
222
+ table = await self._table(collection, write=True)
223
+ await table.add([{
224
+ "id": id,
225
+ "text": text,
226
+ "vector": vector,
227
+ "metadata": json.dumps(meta),
228
+ "modality": meta.get("modality", "text"),
229
+ "image_hash": meta.get("image_hash", ""),
230
+ "image_ref": meta.get("image_ref", ""),
231
+ "thumbnail_ref": meta.get("thumbnail_ref", ""),
232
+ "caption": meta.get("caption", ""),
233
+ "created_at": now,
234
+ "updated_at": now,
235
+ }])
236
+ if not self._eligible(collection, id, meta):
237
+ await table.delete('id = ' + self._quoted(id))
238
+
239
+ async def add_batch(
240
+ self,
241
+ collection: Collection,
242
+ items: list[VectorItem],
243
+ ) -> None:
244
+ """Add a batch of vector entries."""
245
+ if not items:
246
+ return
247
+ now = time.time()
248
+
249
+ # Dimension validation on first item
250
+ for item in items:
251
+ self._validate_vector(item.vector)
252
+
253
+ table = await self._table(collection, write=True)
254
+ rows = []
255
+ for item in items:
256
+ meta = item.metadata or {}
257
+ if not self._eligible(collection, item.id, meta):
258
+ continue
259
+ if self.identity is not None:
260
+ meta = {**meta, 'embedding_fingerprint': self.identity.fingerprint}
261
+ meta.setdefault("embedded_at", now)
262
+ meta.setdefault("modality", "text")
263
+ rows.append({
264
+ "id": item.id,
265
+ "text": item.text,
266
+ "vector": item.vector,
267
+ "metadata": json.dumps(meta),
268
+ "modality": meta.get("modality", "text"),
269
+ "image_hash": meta.get("image_hash", ""),
270
+ "image_ref": meta.get("image_ref", ""),
271
+ "thumbnail_ref": meta.get("thumbnail_ref", ""),
272
+ "caption": meta.get("caption", ""),
273
+ "created_at": now,
274
+ "updated_at": now,
275
+ })
276
+ if rows:
277
+ await table.add(rows)
278
+ for item in items:
279
+ if not self._eligible(collection, item.id, item.metadata):
280
+ await table.delete('id = ' + self._quoted(item.id))
281
+
282
+ async def search(
283
+ self,
284
+ collection: Collection,
285
+ query_vector: list[float],
286
+ limit: int = 10,
287
+ filter: Optional[str] = None,
288
+ min_score: float = 0.0,
289
+ ) -> list[VectorResult]:
290
+ """ANN search on a collection. Returns results sorted by score descending."""
291
+ self._validate_vector(query_vector)
292
+ table = await self._table(collection, semantic=True)
293
+ query = table.vector_search(query_vector).distance_type("cosine")
294
+
295
+ if filter and _METADATA_JSON_FILTER_RE.search(filter):
296
+ # json_extract(metadata, ...) requires LargeBinary but the column is
297
+ # Utf8 — the SQL planner rejects it. Fetch a larger candidate set
298
+ # without the metadata filter and apply it in Python instead.
299
+ query = query.limit(max(limit * 20, 200))
300
+ raw = await query.to_pandas()
301
+ mask = raw["metadata"].apply(
302
+ lambda m: _eval_metadata_filter(str(m) if m is not None else "{}", filter)
303
+ )
304
+ results = raw[mask].head(limit)
305
+ else:
306
+ query = query.limit(limit)
307
+ if filter:
308
+ query = query.where(filter)
309
+ results = await query.to_pandas()
310
+
311
+ out: list[VectorResult] = []
312
+ for _, row in results.iterrows():
313
+ # LanceDB returns _distance (cosine distance); convert to similarity
314
+ score = 1.0 - float(row.get("_distance", 0.0))
315
+ if score < min_score:
316
+ continue
317
+ meta_str = row.get("metadata", "{}")
318
+ try:
319
+ meta = json.loads(meta_str) if isinstance(meta_str, str) else {}
320
+ except (json.JSONDecodeError, TypeError):
321
+ meta = {}
322
+ if not self._eligible(collection, row['id'], meta):
323
+ continue
324
+ out.append(VectorResult(
325
+ id=str(row["id"]),
326
+ score=score,
327
+ text=str(row.get("text", "")),
328
+ metadata=meta,
329
+ ))
330
+
331
+ return out
332
+
333
+ async def search_cross_modal(
334
+ self,
335
+ collection: Collection,
336
+ query_vector: list[float],
337
+ limit: int = 10,
338
+ filter_modality: Optional[str] = None,
339
+ min_score: float = 0.0,
340
+ ) -> list[dict[str, Any]]:
341
+ """Cross-modal search — text query finds images, image query finds text.
342
+
343
+ Works because multimodal models produce vectors in the same
344
+ embedding space regardless of input type.
345
+
346
+ Parameters
347
+ ----------
348
+ filter_modality : str, optional
349
+ Only return results of this modality ("text" or "image").
350
+ None = return all modalities.
351
+ """
352
+ filter_clause = None
353
+ if filter_modality:
354
+ filter_clause = f"modality = '{filter_modality}'"
355
+
356
+ results = await self.search(
357
+ collection, query_vector, limit=limit,
358
+ filter=filter_clause, min_score=min_score,
359
+ )
360
+
361
+ # Enrich with modality-specific fields
362
+ enriched = []
363
+ for r in results:
364
+ meta = r.metadata or {}
365
+ entry = {
366
+ "id": r.id,
367
+ "score": r.score,
368
+ "text": r.text,
369
+ "modality": meta.get("modality", "text"),
370
+ "image_ref": meta.get("image_ref", ""),
371
+ "image_hash": meta.get("image_hash", ""),
372
+ "thumbnail_ref": meta.get("thumbnail_ref", ""),
373
+ "caption": meta.get("caption", ""),
374
+ "metadata": meta,
375
+ }
376
+ enriched.append(entry)
377
+ return enriched
378
+
379
+ async def search_by_image_hash(self, collection: Collection, image_hash: str) -> Optional[VectorResult]:
380
+ """Find an existing vector by image hash (for dedup)."""
381
+ try:
382
+ table = await self._table(collection, semantic=True)
383
+ results = await table.query().where('image_hash = ' + self._quoted(image_hash)).limit(1).to_pandas()
384
+ if results.empty:
385
+ return None
386
+ row = results.iloc[0]
387
+ meta_str = row.get("metadata", "{}")
388
+ try:
389
+ meta = json.loads(meta_str) if isinstance(meta_str, str) else {}
390
+ except (json.JSONDecodeError, TypeError):
391
+ meta = {}
392
+ if not self._eligible(collection, row['id'], meta):
393
+ return None
394
+ return VectorResult(
395
+ id=str(row["id"]),
396
+ score=1.0,
397
+ text=str(row.get("text", "")),
398
+ metadata=meta,
399
+ )
400
+ except Exception:
401
+ return None
402
+
403
+ async def delete(self, collection: Collection, id: str) -> None:
404
+ """Delete a single entry by ID."""
405
+ if self.catalog is None:
406
+ table = await self._table(collection)
407
+ await table.delete('id = ' + self._quoted(id))
408
+ return
409
+ self.catalog.delete(collection.value, id)
410
+ # Exact ID removal covers active, staged, unknown legacy and retained generations.
411
+ for generation in self.catalog.generations():
412
+ db = await self._generation_db(generation)
413
+ if collection.value in await db.table_names():
414
+ await (await db.open_table(collection.value)).delete('id = ' + self._quoted(id))
415
+
416
+ async def update(
417
+ self,
418
+ collection: Collection,
419
+ id: str,
420
+ text: str,
421
+ vector: list[float],
422
+ metadata: dict[str, Any] | None = None,
423
+ ) -> None:
424
+ """Validate first, then atomically replace an entry in one generation."""
425
+ self._validate_vector(vector)
426
+ meta = dict(metadata or {})
427
+ if not self._eligible(collection, id, meta):
428
+ return
429
+ now = time.time()
430
+ meta.setdefault('embedded_at', now)
431
+ if self.identity is not None:
432
+ meta['embedding_fingerprint'] = self.identity.fingerprint
433
+ row = {'id': id, 'text': text, 'vector': vector, 'metadata': json.dumps(meta),
434
+ 'modality': meta.get('modality', 'text'), 'image_hash': meta.get('image_hash', ''),
435
+ 'image_ref': meta.get('image_ref', ''), 'thumbnail_ref': meta.get('thumbnail_ref', ''),
436
+ 'caption': meta.get('caption', ''), 'created_at': now, 'updated_at': now}
437
+ table = await self._table(collection, write=True)
438
+ await table.merge_insert('id').when_matched_update_all().when_not_matched_insert_all().execute([row])
439
+ if not self._eligible(collection, id, meta):
440
+ await table.delete('id = ' + self._quoted(id))
441
+
442
+ async def get(self, collection: Collection, id: str) -> Optional[VectorResult]:
443
+ """Fetch a single entry by ID."""
444
+ table = await self._table(collection)
445
+ results = await table.query().where('id = ' + self._quoted(id)).limit(1).to_pandas()
446
+ if results.empty:
447
+ return None
448
+ row = results.iloc[0]
449
+ meta_str = row.get("metadata", "{}")
450
+ try:
451
+ meta = json.loads(meta_str) if isinstance(meta_str, str) else {}
452
+ except (json.JSONDecodeError, TypeError):
453
+ meta = {}
454
+ if not self._eligible(collection, row['id'], meta):
455
+ return None
456
+ return VectorResult(
457
+ id=str(row["id"]),
458
+ score=1.0,
459
+ text=str(row.get("text", "")),
460
+ metadata=meta,
461
+ )
462
+
463
+ async def count(self, collection: Collection) -> int:
464
+ """Return the number of entries in a collection."""
465
+ table = await self._table(collection)
466
+ return await table.count_rows()
467
+
468
+ async def list_ids(self, collection: Collection) -> list[str]:
469
+ """Return all row ids in a collection via a projected query.
470
+
471
+ Unlike ``scan_all`` this never materializes the vector column — an
472
+ id-only projection, cheap enough to run against a large store (used
473
+ by the orphan-vector vacuum to diff against graph node ids).
474
+ """
475
+ table = await self._table(collection)
476
+ df = await table.query().select(["id"]).to_pandas()
477
+ if df.empty:
478
+ return []
479
+ return [str(x) for x in df["id"].tolist()]
480
+
481
+ async def scan_all(self, collection: Collection) -> list[dict[str, Any]]:
482
+ """Return all rows from a collection as raw dicts."""
483
+ table = await self._table(collection)
484
+ df = await table.to_pandas()
485
+ if df.empty:
486
+ return []
487
+ return df.to_dict(orient="records")
488
+
489
+ async def get_stored_models(self) -> list[str]:
490
+ """Return unique model_id values across all collections."""
491
+ models: set[str] = set()
492
+ for col in Collection:
493
+ try:
494
+ rows = await self.scan_all(col)
495
+ for row in rows:
496
+ meta_str = row.get("metadata", "{}")
497
+ try:
498
+ meta = json.loads(meta_str) if isinstance(meta_str, str) else (meta_str or {})
499
+ except (json.JSONDecodeError, TypeError):
500
+ meta = {}
501
+ model_id = meta.get("model_id", "")
502
+ if model_id:
503
+ models.add(model_id)
504
+ except Exception:
505
+ pass
506
+ return sorted(models)
507
+
508
+ async def close(self) -> None:
509
+ """Release database resources."""
510
+ self._db = None
511
+ self._generation_dbs.clear()
512
+
513
+ async def erase_source_projections(self, turn_ids):
514
+ """Remove exact linked rows even when the old graph row is already gone."""
515
+ if self.catalog is None:
516
+ return 0
517
+ selected = {'turn:' + value for value in turn_ids}
518
+ deleted = set()
519
+ for generation in self.catalog.generations():
520
+ db = await self._generation_db(generation)
521
+ names = await db.table_names()
522
+ for collection in Collection:
523
+ if collection.value not in names:
524
+ continue
525
+ table = await db.open_table(collection.value)
526
+ rows = await table.query().select(['id', 'metadata']).to_list()
527
+ for row in rows:
528
+ meta = json.loads(row['metadata'] or '{}')
529
+ if meta.get('source_uri') in selected and self.catalog.source_erased(meta):
530
+ self.catalog.delete(collection.value, row['id'])
531
+ await table.delete('id = ' + self._quoted(row['id']))
532
+ deleted.add((collection.value, row['id']))
533
+ return len(deleted)