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,683 @@
1
+ """PostgreSQL storage backend for the Colony World Model.
2
+
3
+ Requires asyncpg. Install with: pip install colony-sidecar[postgres]
4
+
5
+ Same interface as SQLiteBackend — drop-in replacement for larger deployments.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import re
13
+ import time
14
+ from datetime import datetime, timezone
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from ..constants import (
18
+ CAUSAL_RELATIONSHIP_TYPES,
19
+ ENTITY_ID_PREFIX,
20
+ RELATIONSHIP_ID_PREFIX,
21
+ OBSERVATION_ID_PREFIX,
22
+ MERGE_PROPOSAL_ID_PREFIX,
23
+ MERGE_AUDIT_ID_PREFIX,
24
+ )
25
+ from ..entities import BaseEntity, ENTITY_CLASS_MAP
26
+ from ..relationships import WorldRelationship
27
+
28
+ # Query-only causal guard (H2.5): untyped (generic) reads NEVER return
29
+ # causal edges; only /world/causal/* and explicitly typed queries do.
30
+ _CAUSAL_EXCLUSION = tuple(sorted(CAUSAL_RELATIONSHIP_TYPES))
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ try:
35
+ import asyncpg
36
+ HAS_ASYNCPG = True
37
+ except ImportError:
38
+ HAS_ASYNCPG = False
39
+
40
+
41
+ def _generate_id(prefix: str) -> str:
42
+ import secrets
43
+ ts = int(time.time() * 1000)
44
+ rand = secrets.token_hex(6)
45
+ return f"{prefix}-{ts}-{rand}"
46
+
47
+
48
+ def _now_iso() -> str:
49
+ return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
50
+
51
+
52
+ def _entity_to_params(entity: BaseEntity) -> dict:
53
+ """Flatten a BaseEntity to a dict suitable for Postgres insertion."""
54
+ import dataclasses
55
+ from ..entities import BaseEntity as BaseEntityClass
56
+
57
+ base_fields = {f.name for f in dataclasses.fields(BaseEntityClass)}
58
+ extra: Dict[str, Any] = dict(entity.properties)
59
+ for f in dataclasses.fields(entity):
60
+ if f.name not in base_fields:
61
+ v = getattr(entity, f.name)
62
+ if v is not None:
63
+ extra[f.name] = v
64
+
65
+ return {
66
+ "id": entity.id,
67
+ "name": entity.name,
68
+ "entity_type": entity.entity_type,
69
+ "aliases": json.dumps(entity.aliases),
70
+ "external_ids": json.dumps(entity.external_ids),
71
+ "confidence": entity.confidence,
72
+ "properties": json.dumps(extra),
73
+ "first_seen": entity.first_seen.strftime("%Y-%m-%dT%H:%M:%SZ") if entity.first_seen else _now_iso(),
74
+ "last_seen": entity.last_seen.strftime("%Y-%m-%dT%H:%M:%SZ") if entity.last_seen else _now_iso(),
75
+ "created_at": entity.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if entity.created_at else _now_iso(),
76
+ "updated_at": _now_iso(),
77
+ }
78
+
79
+
80
+ def _record_to_entity(record) -> BaseEntity:
81
+ """Reconstruct an entity from a Postgres record."""
82
+ d = dict(record)
83
+ # Handle asyncpg Record objects
84
+ if not isinstance(d, dict):
85
+ d = {k: v for k, v in record.items()}
86
+
87
+ d["aliases"] = json.loads(d.get("aliases") or "[]")
88
+ d["external_ids"] = json.loads(d.get("external_ids") or "{}")
89
+ props = json.loads(d.get("properties") or "{}")
90
+ d["properties"] = {}
91
+
92
+ entity_type = d.get("entity_type", "")
93
+ cls = ENTITY_CLASS_MAP.get(entity_type, BaseEntity)
94
+ import dataclasses
95
+ valid_fields = {f.name for f in dataclasses.fields(cls)}
96
+
97
+ for k, v in props.items():
98
+ if k in valid_fields:
99
+ d[k] = v
100
+ else:
101
+ d["properties"][k] = v
102
+
103
+ for dt_field in ("first_seen", "last_seen", "created_at", "updated_at"):
104
+ val = d.get(dt_field)
105
+ if val and isinstance(val, str):
106
+ try:
107
+ d[dt_field] = datetime.fromisoformat(val.replace("Z", "+00:00"))
108
+ except ValueError:
109
+ d[dt_field] = None
110
+ elif val and isinstance(val, datetime):
111
+ pass # Already a datetime
112
+
113
+ filtered = {k: v for k, v in d.items() if k in valid_fields}
114
+ return cls(**filtered)
115
+
116
+
117
+ def _record_to_relationship(record) -> WorldRelationship:
118
+ d = dict(record)
119
+ if not isinstance(d, dict):
120
+ d = {k: v for k, v in record.items()}
121
+ d["properties"] = json.loads(d.get("properties") or "{}")
122
+ return WorldRelationship(
123
+ id=d["id"],
124
+ source_id=d["source_id"],
125
+ target_id=d["target_id"],
126
+ relationship_type=d["relationship_type"],
127
+ confidence=d.get("confidence", 0.5),
128
+ valid_from=d.get("valid_from"),
129
+ valid_to=d.get("valid_to"),
130
+ properties=d["properties"],
131
+ source_observation_id=d.get("source_observation_id"),
132
+ created_at=d.get("created_at"),
133
+ updated_at=d.get("updated_at"),
134
+ )
135
+
136
+
137
+ def _fts_escape(q: str, max_len: int = 200) -> str:
138
+ safe = re.sub(r'[^\w\s]', '', q[:max_len], flags=re.UNICODE)
139
+ return safe.strip()
140
+
141
+
142
+ class PostgresBackend:
143
+ """Async PostgreSQL backend for the World Model.
144
+
145
+ Requires asyncpg and a running Postgres instance.
146
+ Configure via WORLD_MODEL_PG_CONNECTION environment variable.
147
+ """
148
+
149
+ def __init__(self, connection_string: str) -> None:
150
+ if not HAS_ASYNCPG:
151
+ raise ImportError("asyncpg is required for the Postgres backend. Install with: pip install colony-sidecar[postgres]")
152
+ self._conn_string = connection_string
153
+ self._pool: Optional[asyncpg.Pool] = None
154
+
155
+ async def connect(self) -> None:
156
+ self._pool = await asyncpg.create_pool(self._conn_string, min_size=2, max_size=10)
157
+ await self._apply_schema()
158
+
159
+ async def close(self) -> None:
160
+ if self._pool:
161
+ await self._pool.close()
162
+ self._pool = None
163
+
164
+ async def _apply_schema(self) -> None:
165
+ async with self._pool.acquire() as conn:
166
+ await conn.execute("""
167
+ CREATE TABLE IF NOT EXISTS wm_entities (
168
+ id TEXT PRIMARY KEY,
169
+ name TEXT NOT NULL,
170
+ entity_type TEXT NOT NULL,
171
+ aliases JSONB DEFAULT '[]',
172
+ external_ids JSONB DEFAULT '{}',
173
+ confidence REAL DEFAULT 0.5,
174
+ properties JSONB DEFAULT '{}',
175
+ first_seen TIMESTAMPTZ DEFAULT NOW(),
176
+ last_seen TIMESTAMPTZ DEFAULT NOW(),
177
+ created_at TIMESTAMPTZ DEFAULT NOW(),
178
+ updated_at TIMESTAMPTZ DEFAULT NOW()
179
+ );
180
+
181
+ CREATE INDEX IF NOT EXISTS idx_wm_entities_type ON wm_entities(entity_type);
182
+ CREATE INDEX IF NOT EXISTS idx_wm_entities_name ON wm_entities USING gin(to_tsvector('english', name));
183
+
184
+ CREATE TABLE IF NOT EXISTS wm_relationships (
185
+ id TEXT PRIMARY KEY,
186
+ source_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
187
+ target_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
188
+ relationship_type TEXT NOT NULL,
189
+ confidence REAL DEFAULT 0.5,
190
+ valid_from TIMESTAMPTZ,
191
+ valid_to TIMESTAMPTZ,
192
+ properties JSONB DEFAULT '{}',
193
+ source_observation_id TEXT,
194
+ created_at TIMESTAMPTZ DEFAULT NOW(),
195
+ updated_at TIMESTAMPTZ DEFAULT NOW()
196
+ );
197
+
198
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_source ON wm_relationships(source_id);
199
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_target ON wm_relationships(target_id);
200
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_type ON wm_relationships(relationship_type);
201
+
202
+ CREATE TABLE IF NOT EXISTS wm_observations (
203
+ id TEXT PRIMARY KEY,
204
+ entity_id TEXT REFERENCES wm_entities(id) ON DELETE SET NULL,
205
+ relationship_id TEXT REFERENCES wm_relationships(id) ON DELETE SET NULL,
206
+ observation TEXT NOT NULL,
207
+ source TEXT DEFAULT '',
208
+ created_at TIMESTAMPTZ DEFAULT NOW()
209
+ );
210
+
211
+ CREATE TABLE IF NOT EXISTS wm_merge_proposals (
212
+ id TEXT PRIMARY KEY,
213
+ candidate_id TEXT NOT NULL,
214
+ existing_id TEXT NOT NULL,
215
+ match_confidence REAL DEFAULT 0.0,
216
+ match_reason TEXT DEFAULT '',
217
+ evidence JSONB DEFAULT '{}',
218
+ status TEXT DEFAULT 'pending',
219
+ resolved_at TIMESTAMPTZ,
220
+ created_at TIMESTAMPTZ DEFAULT NOW()
221
+ );
222
+
223
+ CREATE TABLE IF NOT EXISTS wm_merge_log (
224
+ id TEXT PRIMARY KEY,
225
+ surviving_id TEXT NOT NULL,
226
+ retired_id TEXT NOT NULL,
227
+ relationships_repointed INTEGER DEFAULT 0,
228
+ properties_updated INTEGER DEFAULT 0,
229
+ executed_by TEXT DEFAULT '',
230
+ merge_proposal_id TEXT,
231
+ created_at TIMESTAMPTZ DEFAULT NOW()
232
+ );
233
+
234
+ CREATE TABLE IF NOT EXISTS wm_schema_migrations (
235
+ version TEXT PRIMARY KEY,
236
+ description TEXT DEFAULT '',
237
+ applied_at TIMESTAMPTZ DEFAULT NOW()
238
+ );
239
+ """)
240
+ # Record migrations
241
+ for version, desc in [
242
+ ("wm-001", "Create base entity tables and GIN index"),
243
+ ("wm-002", "Create relationship table"),
244
+ ("wm-003", "Create observations table"),
245
+ ("wm-004", "Create merge proposals and audit log"),
246
+ ("wm-005", "Create schema migrations table"),
247
+ ]:
248
+ await conn.execute(
249
+ "INSERT INTO wm_schema_migrations (version, description) VALUES ($1, $2) ON CONFLICT DO NOTHING",
250
+ version, desc,
251
+ )
252
+
253
+ # ── Entity CRUD ───────────────────────────────────────────────────────────
254
+
255
+ async def get_entity(self, entity_id: str) -> Optional[BaseEntity]:
256
+ async with self._pool.acquire() as conn:
257
+ return await self._get_entity_in_conn(conn, entity_id)
258
+
259
+ async def _get_entity_in_conn(
260
+ self, conn: Any, entity_id: str
261
+ ) -> Optional[BaseEntity]:
262
+ """Read an entity using an already-acquired connection.
263
+
264
+ Lets callers participate in an outer transaction (see ``execute_merge``)
265
+ instead of opening a second pool connection that would be unable to
266
+ see the in-flight writes.
267
+ """
268
+ row = await conn.fetchrow("SELECT * FROM wm_entities WHERE id = $1", entity_id)
269
+ return _record_to_entity(row) if row else None
270
+
271
+ async def get_entity_by_external_id(self, key: str, value: str) -> Optional[BaseEntity]:
272
+ async with self._pool.acquire() as conn:
273
+ row = await conn.fetchrow(
274
+ "SELECT * FROM wm_entities WHERE external_ids->>$1 = $2 LIMIT 1",
275
+ key, value,
276
+ )
277
+ return _record_to_entity(row) if row else None
278
+
279
+ async def find_entities(
280
+ self,
281
+ query: str,
282
+ entity_type: Optional[str] = None,
283
+ min_confidence: float = 0.30,
284
+ limit: int = 20,
285
+ ) -> List[BaseEntity]:
286
+ async with self._pool.acquire() as conn:
287
+ if query:
288
+ sql = """
289
+ SELECT * FROM wm_entities
290
+ WHERE to_tsvector('english', name) @@ to_tsquery('english', $1)
291
+ AND confidence >= $2
292
+ """
293
+ # Convert to tsquery format
294
+ ts_query = " | ".join(_fts_escape(query).split())
295
+ params = [ts_query, min_confidence]
296
+ param_idx = 3
297
+ if entity_type:
298
+ sql += f" AND entity_type = ${param_idx}"
299
+ params.append(entity_type)
300
+ param_idx += 1
301
+ sql += f" ORDER BY confidence DESC LIMIT ${param_idx}"
302
+ params.append(limit)
303
+ rows = await conn.fetch(sql, *params)
304
+ else:
305
+ sql = "SELECT * FROM wm_entities WHERE confidence >= $1"
306
+ params = [min_confidence]
307
+ param_idx = 2
308
+ if entity_type:
309
+ sql += f" AND entity_type = ${param_idx}"
310
+ params.append(entity_type)
311
+ param_idx += 1
312
+ sql += f" ORDER BY confidence DESC LIMIT ${param_idx}"
313
+ params.append(limit)
314
+ rows = await conn.fetch(sql, *params)
315
+ return [_record_to_entity(r) for r in rows]
316
+
317
+ async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
318
+ if not entity.id:
319
+ entity.id = _generate_id(ENTITY_ID_PREFIX)
320
+ p = _entity_to_params(entity)
321
+ async with self._pool.acquire() as conn:
322
+ await conn.execute("""
323
+ INSERT INTO wm_entities
324
+ (id, name, entity_type, aliases, external_ids, properties,
325
+ confidence, first_seen, last_seen, created_at, updated_at)
326
+ VALUES ($1, $2, $3, $4::jsonb, $5::jsonb, $6::jsonb,
327
+ $7, $8, $9, $10, $11)
328
+ ON CONFLICT(id) DO UPDATE SET
329
+ name = EXCLUDED.name,
330
+ aliases = EXCLUDED.aliases,
331
+ external_ids = EXCLUDED.external_ids,
332
+ properties = EXCLUDED.properties,
333
+ confidence = EXCLUDED.confidence,
334
+ last_seen = EXCLUDED.last_seen,
335
+ updated_at = EXCLUDED.updated_at
336
+ """, p["id"], p["name"], p["entity_type"], p["aliases"], p["external_ids"],
337
+ p["properties"], p["confidence"], p["first_seen"], p["last_seen"],
338
+ p["created_at"], p["updated_at"])
339
+ return entity
340
+
341
+ async def update_entity_property(
342
+ self, entity_id: str, property_key: str, property_value: Any, confidence: float
343
+ ) -> None:
344
+ entity = await self.get_entity(entity_id)
345
+ if not entity:
346
+ return
347
+ import dataclasses
348
+ existing_fields = {f.name for f in dataclasses.fields(type(entity))}
349
+ if property_key in existing_fields:
350
+ setattr(entity, property_key, property_value)
351
+ if confidence > entity.confidence:
352
+ entity.confidence = confidence
353
+ await self.upsert_entity(entity)
354
+ else:
355
+ if confidence >= entity.properties.get(f"_conf_{property_key}", 0.0):
356
+ entity.properties[property_key] = property_value
357
+ entity.properties[f"_conf_{property_key}"] = confidence
358
+ await self.upsert_entity(entity)
359
+
360
+ async def add_entity_alias(self, entity_id: str, alias: str) -> None:
361
+ entity = await self.get_entity(entity_id)
362
+ if entity and alias not in entity.aliases:
363
+ entity.aliases.append(alias)
364
+ async with self._pool.acquire() as conn:
365
+ await conn.execute(
366
+ "UPDATE wm_entities SET aliases = $1::jsonb, updated_at = $2 WHERE id = $3",
367
+ json.dumps(entity.aliases), _now_iso(), entity_id,
368
+ )
369
+
370
+ async def delete_entity(self, entity_id: str) -> None:
371
+ async with self._pool.acquire() as conn:
372
+ await conn.execute("DELETE FROM wm_entities WHERE id = $1", entity_id)
373
+
374
+ # ── Relationship CRUD ─────────────────────────────────────────────────────
375
+
376
+ async def get_relationship(self, rel_id: str) -> Optional[WorldRelationship]:
377
+ async with self._pool.acquire() as conn:
378
+ row = await conn.fetchrow("SELECT * FROM wm_relationships WHERE id = $1", rel_id)
379
+ return _record_to_relationship(row) if row else None
380
+
381
+ async def upsert_relationship(self, rel: WorldRelationship) -> WorldRelationship:
382
+ if not rel.id:
383
+ rel.id = _generate_id(RELATIONSHIP_ID_PREFIX)
384
+ now = _now_iso()
385
+ async with self._pool.acquire() as conn:
386
+ await conn.execute("""
387
+ INSERT INTO wm_relationships
388
+ (id, source_id, target_id, relationship_type, confidence,
389
+ valid_from, valid_to, properties, source_observation_id,
390
+ created_at, updated_at)
391
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9, $10, $11)
392
+ ON CONFLICT(id) DO UPDATE SET
393
+ confidence = EXCLUDED.confidence,
394
+ valid_from = EXCLUDED.valid_from,
395
+ valid_to = EXCLUDED.valid_to,
396
+ properties = EXCLUDED.properties,
397
+ updated_at = EXCLUDED.updated_at
398
+ """, rel.id, rel.source_id, rel.target_id, rel.relationship_type,
399
+ rel.confidence, rel.valid_from, rel.valid_to,
400
+ json.dumps(rel.properties), rel.source_observation_id,
401
+ rel.created_at or now, now)
402
+ return rel
403
+
404
+ async def close_relationship(self, rel_id: str, valid_to: str) -> None:
405
+ async with self._pool.acquire() as conn:
406
+ await conn.execute(
407
+ "UPDATE wm_relationships SET valid_to = $1, updated_at = $2 WHERE id = $3",
408
+ valid_to, _now_iso(), rel_id,
409
+ )
410
+
411
+ async def query_relationships(
412
+ self,
413
+ source_id: Optional[str] = None,
414
+ target_id: Optional[str] = None,
415
+ relationship_type: Optional[str] = None,
416
+ target_types: Optional[List[str]] = None,
417
+ active_only: bool = False,
418
+ min_confidence: float = 0.30,
419
+ limit: int = 100,
420
+ ) -> List[WorldRelationship]:
421
+ conditions = ["r.confidence >= $1"]
422
+ params: list = [min_confidence]
423
+ idx = 2
424
+
425
+ sql = "SELECT r.* FROM wm_relationships r"
426
+
427
+ if target_types:
428
+ sql += " JOIN wm_entities te ON te.id = r.target_id"
429
+ placeholders = ", ".join(f"${idx + i}" for i in range(len(target_types)))
430
+ conditions.append(f"te.entity_type IN ({placeholders})")
431
+ params.extend(target_types)
432
+ idx += len(target_types)
433
+
434
+ if source_id:
435
+ conditions.append(f"r.source_id = ${idx}")
436
+ params.append(source_id)
437
+ idx += 1
438
+ if target_id:
439
+ conditions.append(f"r.target_id = ${idx}")
440
+ params.append(target_id)
441
+ idx += 1
442
+ if relationship_type:
443
+ conditions.append(f"r.relationship_type = ${idx}")
444
+ params.append(relationship_type)
445
+ idx += 1
446
+ else:
447
+ # Untyped read: causal edges are query-only, exclude them.
448
+ placeholders = ", ".join(
449
+ f"${idx + i}" for i in range(len(_CAUSAL_EXCLUSION)))
450
+ conditions.append(f"r.relationship_type NOT IN ({placeholders})")
451
+ params.extend(_CAUSAL_EXCLUSION)
452
+ idx += len(_CAUSAL_EXCLUSION)
453
+ if active_only:
454
+ conditions.append("r.valid_to IS NULL")
455
+
456
+ sql += " WHERE " + " AND ".join(conditions)
457
+ sql += f" ORDER BY r.confidence DESC LIMIT ${idx}"
458
+ params.append(limit)
459
+
460
+ async with self._pool.acquire() as conn:
461
+ rows = await conn.fetch(sql, *params)
462
+ return [_record_to_relationship(r) for r in rows]
463
+
464
+ async def query_at_time(
465
+ self,
466
+ entity_id: str,
467
+ as_of: str,
468
+ relationship_types: Optional[List[str]] = None,
469
+ ) -> List[WorldRelationship]:
470
+ sql = """
471
+ SELECT * FROM wm_relationships
472
+ WHERE (source_id = $1 OR target_id = $2)
473
+ AND (valid_from IS NULL OR valid_from <= $3)
474
+ AND (valid_to IS NULL OR valid_to > $4)
475
+ """
476
+ params: list = [entity_id, entity_id, as_of, as_of]
477
+ if relationship_types:
478
+ placeholders = ", ".join(f"${5 + i}" for i in range(len(relationship_types)))
479
+ sql += f" AND relationship_type IN ({placeholders})"
480
+ params.extend(relationship_types)
481
+ else:
482
+ # Untyped read: causal edges are query-only, exclude them.
483
+ placeholders = ", ".join(
484
+ f"${5 + i}" for i in range(len(_CAUSAL_EXCLUSION)))
485
+ sql += f" AND relationship_type NOT IN ({placeholders})"
486
+ params.extend(_CAUSAL_EXCLUSION)
487
+ async with self._pool.acquire() as conn:
488
+ rows = await conn.fetch(sql, *params)
489
+ return [_record_to_relationship(r) for r in rows]
490
+
491
+ # ── Graph Traversal ───────────────────────────────────────────────────────
492
+
493
+ async def get_neighbors(
494
+ self,
495
+ entity_id: str,
496
+ min_confidence: float = 0.30,
497
+ relationship_types: Optional[List[str]] = None,
498
+ ) -> List[tuple]:
499
+ sql = """
500
+ SELECT e.*, r.id as rel_id, r.source_id, r.target_id,
501
+ r.relationship_type, r.confidence as rel_confidence,
502
+ r.valid_from, r.valid_to, r.properties as rel_props,
503
+ r.source_observation_id, r.created_at as rel_created_at,
504
+ r.updated_at as rel_updated_at
505
+ FROM wm_relationships r
506
+ JOIN wm_entities e ON (
507
+ (r.source_id = $1 AND e.id = r.target_id) OR
508
+ (r.target_id = $2 AND e.id = r.source_id)
509
+ )
510
+ WHERE r.confidence >= $3 AND e.confidence >= $4
511
+ """
512
+ params: list = [entity_id, entity_id, min_confidence, min_confidence]
513
+ if relationship_types:
514
+ placeholders = ", ".join(f"${5 + i}" for i in range(len(relationship_types)))
515
+ sql += f" AND r.relationship_type IN ({placeholders})"
516
+ params.extend(relationship_types)
517
+ else:
518
+ # Untyped read: causal edges are query-only, exclude them.
519
+ placeholders = ", ".join(
520
+ f"${5 + i}" for i in range(len(_CAUSAL_EXCLUSION)))
521
+ sql += f" AND r.relationship_type NOT IN ({placeholders})"
522
+ params.extend(_CAUSAL_EXCLUSION)
523
+
524
+ async with self._pool.acquire() as conn:
525
+ rows = await conn.fetch(sql, *params)
526
+
527
+ results = []
528
+ for row in rows:
529
+ entity = _record_to_entity(row)
530
+ d = dict(row) if isinstance(row, dict) else {k: v for k, v in row.items()}
531
+ rel = WorldRelationship(
532
+ id=d["rel_id"],
533
+ source_id=d["source_id"],
534
+ target_id=d["target_id"],
535
+ relationship_type=d["relationship_type"],
536
+ confidence=d["rel_confidence"],
537
+ valid_from=d.get("valid_from"),
538
+ valid_to=d.get("valid_to"),
539
+ properties=json.loads(d.get("rel_props") or "{}") if isinstance(d.get("rel_props"), str) else (d.get("rel_props") or {}),
540
+ source_observation_id=d.get("source_observation_id"),
541
+ created_at=d.get("rel_created_at"),
542
+ updated_at=d.get("rel_updated_at"),
543
+ )
544
+ results.append((entity, rel))
545
+ return results
546
+
547
+ # ── Observations ──────────────────────────────────────────────────────────
548
+
549
+ async def add_observation(
550
+ self,
551
+ entity_id: Optional[str],
552
+ relationship_id: Optional[str],
553
+ observation: str,
554
+ source: str,
555
+ ) -> str:
556
+ obs_id = _generate_id(OBSERVATION_ID_PREFIX)
557
+ async with self._pool.acquire() as conn:
558
+ await conn.execute(
559
+ "INSERT INTO wm_observations (id, entity_id, relationship_id, observation, source) VALUES ($1, $2, $3, $4, $5)",
560
+ obs_id, entity_id, relationship_id, observation, source,
561
+ )
562
+ return obs_id
563
+
564
+ # ── Merge Proposals ───────────────────────────────────────────────────────
565
+
566
+ async def create_merge_proposal(
567
+ self,
568
+ candidate_id: str,
569
+ existing_id: str,
570
+ confidence: float,
571
+ reason: str,
572
+ evidence: Optional[Dict[str, Any]] = None,
573
+ ) -> str:
574
+ proposal_id = _generate_id(MERGE_PROPOSAL_ID_PREFIX)
575
+ async with self._pool.acquire() as conn:
576
+ await conn.execute(
577
+ "INSERT INTO wm_merge_proposals (id, candidate_id, existing_id, match_confidence, match_reason, evidence) VALUES ($1, $2, $3, $4, $5, $6::jsonb)",
578
+ proposal_id, candidate_id, existing_id, confidence, reason, json.dumps(evidence or {}),
579
+ )
580
+ return proposal_id
581
+
582
+ async def get_merge_proposal(self, proposal_id: str) -> Optional[Dict[str, Any]]:
583
+ async with self._pool.acquire() as conn:
584
+ row = await conn.fetchrow("SELECT * FROM wm_merge_proposals WHERE id = $1", proposal_id)
585
+ if not row:
586
+ return None
587
+ d = {k: v for k, v in row.items()}
588
+ d["evidence"] = json.loads(d.get("evidence") or "{}") if isinstance(d.get("evidence"), str) else (d.get("evidence") or {})
589
+ return d
590
+
591
+ async def update_merge_proposal_status(self, proposal_id: str, status: str) -> None:
592
+ async with self._pool.acquire() as conn:
593
+ await conn.execute(
594
+ "UPDATE wm_merge_proposals SET status = $1, resolved_at = $2 WHERE id = $3",
595
+ status, _now_iso(), proposal_id,
596
+ )
597
+
598
+ async def get_pending_merge_proposals(self) -> List[Dict[str, Any]]:
599
+ async with self._pool.acquire() as conn:
600
+ rows = await conn.fetch("SELECT * FROM wm_merge_proposals WHERE status = 'pending'")
601
+ result = []
602
+ for row in rows:
603
+ d = {k: v for k, v in row.items()}
604
+ d["evidence"] = json.loads(d.get("evidence") or "{}") if isinstance(d.get("evidence"), str) else (d.get("evidence") or {})
605
+ result.append(d)
606
+ return result
607
+
608
+ # ── Merge Execution ───────────────────────────────────────────────────────
609
+
610
+ async def execute_merge(
611
+ self,
612
+ surviving_id: str,
613
+ retired_id: str,
614
+ executed_by: str,
615
+ proposal_id: Optional[str] = None,
616
+ ) -> str:
617
+ # Wrap the whole merge in a single Postgres transaction: asyncpg
618
+ # auto-commits each statement otherwise, so a failure between the
619
+ # relationship repoint and the DELETE would leave dangling references
620
+ # to a half-retired entity with no audit row.
621
+ async with self._pool.acquire() as conn:
622
+ async with conn.transaction():
623
+ # Read both entities up-front so we fail before mutating if
624
+ # either is missing — and so a concurrent merge cannot delete
625
+ # one of them between our reads and writes.
626
+ surviving = await self._get_entity_in_conn(conn, surviving_id)
627
+ retired = await self._get_entity_in_conn(conn, retired_id)
628
+
629
+ # Count relationships to repoint
630
+ rel_count = await conn.fetchval(
631
+ "SELECT COUNT(*) FROM wm_relationships WHERE source_id = $1 OR target_id = $1",
632
+ retired_id,
633
+ )
634
+
635
+ # Repoint
636
+ await conn.execute("UPDATE wm_relationships SET source_id = $1 WHERE source_id = $2", surviving_id, retired_id)
637
+ await conn.execute("UPDATE wm_relationships SET target_id = $1 WHERE target_id = $2", surviving_id, retired_id)
638
+
639
+ # Merge aliases
640
+ props_updated = 0
641
+ if surviving and retired:
642
+ new_aliases = surviving.aliases.copy()
643
+ if retired_id not in new_aliases:
644
+ new_aliases.append(retired_id)
645
+ for alias in retired.aliases:
646
+ if alias not in new_aliases:
647
+ new_aliases.append(alias)
648
+ props_updated = len(new_aliases) - len(surviving.aliases)
649
+ await conn.execute(
650
+ "UPDATE wm_entities SET aliases = $1::jsonb, updated_at = $2 WHERE id = $3",
651
+ json.dumps(new_aliases), _now_iso(), surviving_id,
652
+ )
653
+
654
+ # Delete retired entity
655
+ await conn.execute("DELETE FROM wm_entities WHERE id = $1", retired_id)
656
+
657
+ # Audit record
658
+ audit_id = _generate_id(MERGE_AUDIT_ID_PREFIX)
659
+ await conn.execute(
660
+ "INSERT INTO wm_merge_log (id, surviving_id, retired_id, relationships_repointed, properties_updated, executed_by, merge_proposal_id) VALUES ($1, $2, $3, $4, $5, $6, $7)",
661
+ audit_id, surviving_id, retired_id, rel_count, props_updated, executed_by, proposal_id,
662
+ )
663
+ return audit_id
664
+
665
+ # ── Stats ─────────────────────────────────────────────────────────────────
666
+
667
+ async def get_stats(self) -> Dict[str, Any]:
668
+ async with self._pool.acquire() as conn:
669
+ type_rows = await conn.fetch("SELECT entity_type, COUNT(*) as cnt FROM wm_entities GROUP BY entity_type")
670
+ total_entities = await conn.fetchval("SELECT COUNT(*) FROM wm_entities")
671
+ total_rels = await conn.fetchval("SELECT COUNT(*) FROM wm_relationships")
672
+ active_rels = await conn.fetchval("SELECT COUNT(*) FROM wm_relationships WHERE valid_to IS NULL")
673
+ total_obs = await conn.fetchval("SELECT COUNT(*) FROM wm_observations")
674
+ pending_proposals = await conn.fetchval("SELECT COUNT(*) FROM wm_merge_proposals WHERE status = 'pending'")
675
+
676
+ return {
677
+ "total_entities": total_entities,
678
+ "entities_by_type": {r["entity_type"]: r["cnt"] for r in type_rows},
679
+ "total_relationships": total_rels,
680
+ "active_relationships": active_rels,
681
+ "total_observations": total_obs,
682
+ "merge_proposals_pending": pending_proposals,
683
+ }