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,654 @@
1
+ """Neo4j storage backend for the Colony World Model.
2
+
3
+ Implements the same interface as SQLiteBackend but backed by Neo4j,
4
+ which provides native graph traversal (Cypher) for relationship queries,
5
+ neighborhood traversal, and path finding.
6
+
7
+ Requires: ``neo4j`` driver (pip install neo4j)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import re
15
+ import secrets
16
+ import time
17
+ from datetime import datetime, timezone
18
+ from typing import Any, Dict, List, Optional, Tuple
19
+
20
+ from ..constants import (
21
+ CAUSAL_RELATIONSHIP_TYPES,
22
+ ENTITY_ID_PREFIX,
23
+ RELATIONSHIP_ID_PREFIX,
24
+ OBSERVATION_ID_PREFIX,
25
+ MERGE_PROPOSAL_ID_PREFIX,
26
+ MERGE_AUDIT_ID_PREFIX,
27
+ )
28
+ from ..entities import BaseEntity, ENTITY_CLASS_MAP, entity_from_dict
29
+ from ..relationships import WorldRelationship
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ # Query-only causal guard (H2.5): untyped (generic) reads NEVER return
34
+ # causal edges; only /world/causal/* and explicitly typed queries do.
35
+ _CAUSAL_EXCLUSION = sorted(CAUSAL_RELATIONSHIP_TYPES)
36
+
37
+
38
+ def _generate_id(prefix: str) -> str:
39
+ ts = int(time.time() * 1000)
40
+ rand = secrets.token_hex(6)
41
+ return f"{prefix}-{ts}-{rand}"
42
+
43
+
44
+ def _entity_to_props(entity: BaseEntity) -> Dict[str, Any]:
45
+ """Convert an entity dataclass to a flat dict for Neo4j node properties."""
46
+ import dataclasses
47
+ props = {}
48
+ for f in dataclasses.fields(entity):
49
+ val = getattr(entity, f.name)
50
+ if val is None:
51
+ continue
52
+ if isinstance(val, (list, dict)):
53
+ props[f.name] = json.dumps(val)
54
+ elif isinstance(val, datetime):
55
+ props[f.name] = val.isoformat()
56
+ else:
57
+ props[f.name] = val
58
+ return props
59
+
60
+
61
+ def _props_to_entity(props: Dict[str, Any]) -> BaseEntity:
62
+ """Convert Neo4j node properties back to an entity dataclass."""
63
+ data = dict(props)
64
+ for key in ("aliases", "external_ids", "properties"):
65
+ if key in data and isinstance(data[key], str):
66
+ try:
67
+ data[key] = json.loads(data[key])
68
+ except (json.JSONDecodeError, TypeError):
69
+ data[key] = [] if key == "aliases" else {}
70
+ for key in ("first_seen", "last_seen", "created_at", "updated_at"):
71
+ if key in data and isinstance(data[key], str):
72
+ try:
73
+ data[key] = datetime.fromisoformat(data[key])
74
+ except (ValueError, TypeError):
75
+ data[key] = None
76
+ return entity_from_dict(data)
77
+
78
+
79
+ def _rel_to_props(rel: WorldRelationship) -> Dict[str, Any]:
80
+ """Convert a WorldRelationship to a flat dict for Neo4j edge properties."""
81
+ import dataclasses
82
+ props = {}
83
+ for f in dataclasses.fields(rel):
84
+ val = getattr(rel, f.name)
85
+ if val is None:
86
+ continue
87
+ if isinstance(val, dict):
88
+ props[f.name] = json.dumps(val)
89
+ else:
90
+ props[f.name] = val
91
+ return props
92
+
93
+
94
+ def _props_to_rel(props: Dict[str, Any]) -> WorldRelationship:
95
+ """Convert Neo4j edge properties back to a WorldRelationship."""
96
+ import dataclasses
97
+ data = dict(props)
98
+ if "properties" in data and isinstance(data["properties"], str):
99
+ try:
100
+ data["properties"] = json.loads(data["properties"])
101
+ except (json.JSONDecodeError, TypeError):
102
+ data["properties"] = {}
103
+ valid = {f.name for f in dataclasses.fields(WorldRelationship)}
104
+ return WorldRelationship(**{k: v for k, v in data.items() if k in valid})
105
+
106
+
107
+ def _sanitize_rel_type(rel_type: str) -> str:
108
+ """Sanitize a relationship type for use in Cypher (alphanumeric + underscore)."""
109
+ safe = re.sub(r'[^A-Z_0-9]', '', rel_type.upper())
110
+ return safe or "WM_RELATED_TO"
111
+
112
+
113
+ def _node_to_dict(node) -> Dict[str, Any]:
114
+ """Convert a Neo4j Node to a plain dict. Handles both Node objects and pre-converted dicts."""
115
+ if isinstance(node, dict):
116
+ return node
117
+ try:
118
+ return dict(node.items())
119
+ except (TypeError, AttributeError):
120
+ return dict(node)
121
+
122
+
123
+ def _rel_to_dict(rel) -> Dict[str, Any]:
124
+ """Convert a Neo4j Relationship to a plain dict.
125
+
126
+ When using result.data(), relationships come as tuples:
127
+ (start_node_dict, rel_type_str, end_node_dict)
128
+ When using result.single(), they come as Relationship objects.
129
+ """
130
+ if isinstance(rel, tuple):
131
+ # result.data() format: (start_node_dict, rel_type_str, end_node_dict)
132
+ # This is unreliable — callers should use properties(r) instead
133
+ return {}
134
+ if isinstance(rel, dict):
135
+ return rel
136
+ try:
137
+ return dict(rel.items())
138
+ except (TypeError, AttributeError):
139
+ return dict(rel)
140
+
141
+
142
+ class Neo4jBackend:
143
+ """Neo4j-backed storage for the Colony World Model.
144
+
145
+ Node labels: ``Entity`` (all entity types, with ``entity_type`` as property)
146
+ Relationship types: prefixed ``WM_`` types from constants
147
+ """
148
+
149
+ def __init__(self, uri: str, database: str = "neo4j",
150
+ username: str = "neo4j", password: str = "") -> None:
151
+ self._uri = uri
152
+ self._database = database
153
+ self._username = username
154
+ self._password = password
155
+ self._driver = None
156
+
157
+ async def connect(self) -> None:
158
+ try:
159
+ from neo4j import AsyncGraphDatabase
160
+ except ImportError:
161
+ raise ImportError("neo4j driver not installed. Run: pip install neo4j")
162
+
163
+ self._driver = AsyncGraphDatabase.driver(
164
+ self._uri,
165
+ auth=(self._username, self._password),
166
+ )
167
+ async with self._driver.session(database=self._database) as session:
168
+ result = await session.run("RETURN 1")
169
+ await result.consume()
170
+ logger.info("Neo4j backend connected: %s (db=%s)", self._uri, self._database)
171
+ await self._apply_schema()
172
+
173
+ async def close(self) -> None:
174
+ if self._driver:
175
+ await self._driver.close()
176
+ self._driver = None
177
+
178
+ async def _apply_schema(self) -> None:
179
+ """Create indexes and constraints for optimal query performance."""
180
+ async with self._driver.session(database=self._database) as session:
181
+ for cypher in [
182
+ "CREATE CONSTRAINT entity_id_unique IF NOT EXISTS FOR (e:Entity) REQUIRE e.id IS UNIQUE",
183
+ "CREATE INDEX entity_type_index IF NOT EXISTS FOR (e:Entity) ON (e.entity_type)",
184
+ "CREATE INDEX entity_name_index IF NOT EXISTS FOR (e:Entity) ON (e.name)",
185
+ "CREATE FULLTEXT INDEX entity_search IF NOT EXISTS FOR (e:Entity) ON EACH [e.name, e.aliases]",
186
+ ]:
187
+ try:
188
+ result = await session.run(cypher)
189
+ await result.consume()
190
+ except Exception:
191
+ pass # May already exist
192
+
193
+ # ── Entity reads ──────────────────────────────────────────────────────
194
+
195
+ async def get_entity(self, entity_id: str) -> Optional[BaseEntity]:
196
+ async with self._driver.session(database=self._database) as session:
197
+ result = await session.run(
198
+ "MATCH (e:Entity {id: $id}) RETURN e", id=entity_id,
199
+ )
200
+ record = await result.single()
201
+ if record is None:
202
+ return None
203
+ return _props_to_entity(_node_to_dict(record["e"]))
204
+
205
+ async def get_entity_by_external_id(
206
+ self, key: str, value: str
207
+ ) -> Optional[BaseEntity]:
208
+ async with self._driver.session(database=self._database) as session:
209
+ result = await session.run(
210
+ "MATCH (e:Entity) WHERE e.external_ids CONTAINS $kv RETURN e",
211
+ kv=json.dumps({key: value}),
212
+ )
213
+ record = await result.single()
214
+ if record is None:
215
+ return None
216
+ return _props_to_entity(_node_to_dict(record["e"]))
217
+
218
+ async def find_entities(
219
+ self,
220
+ query: str,
221
+ entity_type: Optional[str] = None,
222
+ min_confidence: float = 0.30,
223
+ limit: int = 20,
224
+ ) -> List[BaseEntity]:
225
+ safe_query = re.sub(r'[^\w\s]', '', query[:200]).strip()
226
+ if not safe_query:
227
+ return []
228
+
229
+ async with self._driver.session(database=self._database) as session:
230
+ try:
231
+ cypher = (
232
+ "CALL db.index.fulltext.queryNodes('entity_search', $query) "
233
+ "YIELD node, score "
234
+ "WHERE node.confidence >= $min_conf "
235
+ )
236
+ params: Dict[str, Any] = {"query": safe_query, "min_conf": min_confidence, "limit": limit}
237
+ if entity_type:
238
+ cypher += "AND node.entity_type = $etype "
239
+ params["etype"] = entity_type
240
+ cypher += "RETURN node LIMIT $limit"
241
+ result = await session.run(cypher, params)
242
+ records = await result.data()
243
+ return [_props_to_entity(_node_to_dict(r["node"])) for r in records]
244
+ except Exception:
245
+ cypher = (
246
+ "MATCH (e:Entity) "
247
+ "WHERE e.name CONTAINS $query AND e.confidence >= $min_conf "
248
+ )
249
+ params = {"query": safe_query, "min_conf": min_confidence, "limit": limit}
250
+ if entity_type:
251
+ cypher += "AND e.entity_type = $etype "
252
+ params["etype"] = entity_type
253
+ cypher += "RETURN e LIMIT $limit"
254
+ result = await session.run(cypher, params)
255
+ records = await result.data()
256
+ return [_props_to_entity(_node_to_dict(r["e"])) for r in records]
257
+
258
+ # ── Entity writes ─────────────────────────────────────────────────────
259
+
260
+ async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
261
+ props = _entity_to_props(entity)
262
+ now = datetime.now(timezone.utc).isoformat()
263
+ if "created_at" not in props:
264
+ props["created_at"] = now
265
+ props["updated_at"] = now
266
+
267
+ async with self._driver.session(database=self._database) as session:
268
+ result = await session.run(
269
+ "MERGE (e:Entity {id: $id}) SET e += $props RETURN e",
270
+ id=entity.id, props=props,
271
+ )
272
+ record = await result.single()
273
+ return _props_to_entity(_node_to_dict(record["e"]))
274
+
275
+ async def update_entity_property(
276
+ self, entity_id: str, property_key: str, property_value: Any, confidence: float,
277
+ ) -> None:
278
+ async with self._driver.session(database=self._database) as session:
279
+ result = await session.run(
280
+ "MATCH (e:Entity {id: $id}) "
281
+ "SET e.confidence = $conf, e.updated_at = $now",
282
+ id=entity_id, conf=confidence,
283
+ now=datetime.now(timezone.utc).isoformat(),
284
+ )
285
+ await result.consume()
286
+
287
+ async def add_entity_alias(self, entity_id: str, alias: str) -> None:
288
+ async with self._driver.session(database=self._database) as session:
289
+ result = await session.run(
290
+ "MATCH (e:Entity {id: $id}) "
291
+ "SET e.aliases = CASE WHEN e.aliases IS NULL THEN [$alias] "
292
+ " WHEN NOT $alias IN e.aliases THEN e.aliases + [$alias] "
293
+ " ELSE e.aliases END, "
294
+ " e.updated_at = $now",
295
+ id=entity_id, alias=alias,
296
+ now=datetime.now(timezone.utc).isoformat(),
297
+ )
298
+ await result.consume()
299
+
300
+ async def delete_entity(self, entity_id: str) -> None:
301
+ async with self._driver.session(database=self._database) as session:
302
+ result = await session.run(
303
+ "MATCH (e:Entity {id: $id}) DETACH DELETE e", id=entity_id,
304
+ )
305
+ await result.consume()
306
+
307
+ # ── Relationship reads ────────────────────────────────────────────────
308
+
309
+ async def get_relationship(self, rel_id: str) -> Optional[WorldRelationship]:
310
+ async with self._driver.session(database=self._database) as session:
311
+ result = await session.run(
312
+ "MATCH ()-[r]->() WHERE r.id = $id RETURN r", id=rel_id,
313
+ )
314
+ record = await result.single()
315
+ if record is None:
316
+ return None
317
+ return _props_to_rel(_rel_to_dict(record["r"]))
318
+
319
+ async def query_relationships(
320
+ self,
321
+ source_id: Optional[str] = None,
322
+ target_id: Optional[str] = None,
323
+ relationship_type: Optional[str] = None,
324
+ target_types: Optional[List[str]] = None,
325
+ active_only: bool = False,
326
+ min_confidence: float = 0.30,
327
+ limit: int = 100,
328
+ ) -> List[WorldRelationship]:
329
+ clauses = []
330
+ params: Dict[str, Any] = {"min_conf": min_confidence, "limit": limit}
331
+
332
+ if source_id:
333
+ clauses.append("(s:Entity {id: $src})-[r]->(t:Entity)")
334
+ params["src"] = source_id
335
+ elif target_id:
336
+ clauses.append("(s:Entity)-[r]->(t:Entity {id: $tgt})")
337
+ params["tgt"] = target_id
338
+ else:
339
+ clauses.append("(s:Entity)-[r]->(t:Entity)")
340
+
341
+ where_parts = ["r.confidence >= $min_conf"]
342
+ if active_only:
343
+ where_parts.append("r.valid_to IS NULL")
344
+ if target_types:
345
+ where_parts.append("t.entity_type IN $tgt_types")
346
+ params["tgt_types"] = target_types
347
+ if not relationship_type:
348
+ # Untyped read: causal edges are query-only, exclude them.
349
+ where_parts.append("NOT type(r) IN $causal_excl")
350
+ params["causal_excl"] = _CAUSAL_EXCLUSION
351
+
352
+ match_clause = clauses[0] if not relationship_type else \
353
+ clauses[0].replace("-[r]->", f"-[r:{_sanitize_rel_type(relationship_type)}]->")
354
+
355
+ cypher = f"MATCH {match_clause} WHERE {' AND '.join(where_parts)} RETURN s.id AS source_id, t.id AS target_id, type(r) AS rel_type, properties(r) AS rel_props LIMIT $limit"
356
+
357
+ async with self._driver.session(database=self._database) as session:
358
+ result = await session.run(cypher, params)
359
+ records = await result.data()
360
+ rels = []
361
+ for r in records:
362
+ props = r["rel_props"] or {}
363
+ props["source_id"] = r["source_id"]
364
+ props["target_id"] = r["target_id"]
365
+ props.setdefault("relationship_type", r["rel_type"])
366
+ rels.append(_props_to_rel(props))
367
+ return rels
368
+
369
+ async def query_at_time(
370
+ self, entity_id: str, as_of: str,
371
+ relationship_types: Optional[List[str]] = None,
372
+ ) -> List[WorldRelationship]:
373
+ clauses = [
374
+ "r.confidence >= 0.30",
375
+ "(r.valid_from IS NULL OR r.valid_from <= $as_of)",
376
+ "(r.valid_to IS NULL OR r.valid_to > $as_of)",
377
+ ]
378
+ params = {"id": entity_id, "as_of": as_of}
379
+ if not relationship_types:
380
+ # Untyped read: causal edges are query-only, exclude them.
381
+ clauses.append("NOT type(r) IN $causal_excl")
382
+ params["causal_excl"] = _CAUSAL_EXCLUSION
383
+
384
+ rel_match = "-[r]->" if not relationship_types else \
385
+ "-[r:" + "|".join(_sanitize_rel_type(t) for t in relationship_types) + "]->"
386
+
387
+ cypher = (
388
+ f"MATCH (s:Entity {{id: $id}}){rel_match}(t:Entity) "
389
+ f"WHERE {' AND '.join(clauses)} RETURN s.id AS source_id, t.id AS target_id, type(r) AS rel_type, properties(r) AS rel_props"
390
+ )
391
+
392
+ async with self._driver.session(database=self._database) as session:
393
+ result = await session.run(cypher, params)
394
+ records = await result.data()
395
+ rels = []
396
+ for r in records:
397
+ props = r["rel_props"] or {}
398
+ props["source_id"] = r["source_id"]
399
+ props["target_id"] = r["target_id"]
400
+ props.setdefault("relationship_type", r["rel_type"])
401
+ rels.append(_props_to_rel(props))
402
+ return rels
403
+
404
+ async def get_neighbors(
405
+ self, entity_id: str, min_confidence: float = 0.30,
406
+ relationship_types: Optional[List[str]] = None,
407
+ ) -> List[Tuple[BaseEntity, WorldRelationship]]:
408
+ # Bidirectional: both outgoing and incoming relationships
409
+ causal_where = ""
410
+ if not relationship_types:
411
+ rel_type_filter = ""
412
+ # Untyped read: causal edges are query-only, exclude them.
413
+ causal_where = " AND NOT type(r) IN $causal_excl"
414
+ else:
415
+ rel_type_filter = ":" + "|".join(_sanitize_rel_type(t) for t in relationship_types)
416
+
417
+ # Outgoing: (center)-[r]->(other)
418
+ cypher_out = (
419
+ f"MATCH (s:Entity {{id: $id}})-[r{rel_type_filter}]->(t:Entity) "
420
+ f"WHERE r.confidence >= $min_conf{causal_where} "
421
+ "RETURN s.id AS source_id, t.id AS target_id, "
422
+ "type(r) AS rel_type, properties(r) AS rel_props, t"
423
+ )
424
+ # Incoming: (other)-[r]->(center)
425
+ cypher_in = (
426
+ f"MATCH (t:Entity)-[r{rel_type_filter}]->(s:Entity {{id: $id}}) "
427
+ f"WHERE r.confidence >= $min_conf{causal_where} "
428
+ "RETURN t.id AS source_id, s.id AS target_id, "
429
+ "type(r) AS rel_type, properties(r) AS rel_props, t"
430
+ )
431
+
432
+ neighbors = []
433
+ seen_ids = set()
434
+ async with self._driver.session(database=self._database) as session:
435
+ for cypher in [cypher_out, cypher_in]:
436
+ result = await session.run(cypher, id=entity_id, min_conf=min_confidence,
437
+ causal_excl=_CAUSAL_EXCLUSION)
438
+ records = await result.data()
439
+ for rec in records:
440
+ nid = rec["target_id"] if rec["source_id"] == entity_id else rec["source_id"]
441
+ if nid in seen_ids or nid == entity_id:
442
+ continue
443
+ seen_ids.add(nid)
444
+ entity = _props_to_entity(_node_to_dict(rec["t"]))
445
+ props = rec["rel_props"] or {}
446
+ props["source_id"] = rec["source_id"]
447
+ props["target_id"] = rec["target_id"]
448
+ props.setdefault("relationship_type", rec["rel_type"])
449
+ rel = _props_to_rel(props)
450
+ neighbors.append((entity, rel))
451
+ return neighbors
452
+
453
+ # ── Relationship writes ───────────────────────────────────────────────
454
+
455
+ async def upsert_relationship(self, rel: WorldRelationship) -> WorldRelationship:
456
+ props = _rel_to_props(rel)
457
+ now = datetime.now(timezone.utc).isoformat()
458
+ if "created_at" not in props:
459
+ props["created_at"] = now
460
+ props["updated_at"] = now
461
+
462
+ safe_type = _sanitize_rel_type(rel.relationship_type)
463
+
464
+ async with self._driver.session(database=self._database) as session:
465
+ existing_result = await session.run(
466
+ "MATCH ()-[r]->() WHERE r.id = $id RETURN r", id=rel.id,
467
+ )
468
+ existing_record = await existing_result.single()
469
+
470
+ if existing_record:
471
+ result = await session.run(
472
+ "MATCH ()-[r]->() WHERE r.id = $id SET r += $props RETURN r",
473
+ id=rel.id, props=props,
474
+ )
475
+ await result.consume()
476
+ else:
477
+ result = await session.run(
478
+ f"MATCH (s:Entity {{id: $src}}), (t:Entity {{id: $tgt}}) "
479
+ f"CREATE (s)-[r:{safe_type}]->(t) SET r += $props",
480
+ src=rel.source_id, tgt=rel.target_id, props=props,
481
+ )
482
+ await result.consume()
483
+
484
+ return rel
485
+
486
+ async def close_relationship(self, rel_id: str, valid_to: str) -> None:
487
+ async with self._driver.session(database=self._database) as session:
488
+ result = await session.run(
489
+ "MATCH ()-[r]->() WHERE r.id = $id "
490
+ "SET r.valid_to = $valid_to, r.updated_at = $now",
491
+ id=rel_id, valid_to=valid_to,
492
+ now=datetime.now(timezone.utc).isoformat(),
493
+ )
494
+ await result.consume()
495
+
496
+ # ── Observations ──────────────────────────────────────────────────────
497
+
498
+ async def add_observation(
499
+ self, entity_id: Optional[str], relationship_id: Optional[str],
500
+ observation: str, source: str,
501
+ ) -> str:
502
+ obs_id = _generate_id(OBSERVATION_ID_PREFIX)
503
+ now = datetime.now(timezone.utc).isoformat()
504
+
505
+ async with self._driver.session(database=self._database) as session:
506
+ if entity_id:
507
+ result = await session.run(
508
+ "MATCH (e:Entity {id: $eid}) "
509
+ "CREATE (o:Observation {id: $id, text: $text, source: $source, "
510
+ " entity_id: $eid, created_at: $now}) "
511
+ "CREATE (e)-[:HAS_OBSERVATION]->(o)",
512
+ eid=entity_id, id=obs_id, text=observation,
513
+ source=source, now=now,
514
+ )
515
+ elif relationship_id:
516
+ result = await session.run(
517
+ "MATCH ()-[r]->() WHERE r.id = $rid "
518
+ "CREATE (o:Observation {id: $id, text: $text, source: $source, "
519
+ " relationship_id: $rid, created_at: $now})",
520
+ rid=relationship_id, id=obs_id, text=observation,
521
+ source=source, now=now,
522
+ )
523
+ else:
524
+ result = await session.run(
525
+ "CREATE (o:Observation {id: $id, text: $text, source: $source, "
526
+ " created_at: $now})",
527
+ id=obs_id, text=observation, source=source, now=now,
528
+ )
529
+ await result.consume()
530
+ return obs_id
531
+
532
+ # ── Merge proposals ───────────────────────────────────────────────────
533
+
534
+ async def create_merge_proposal(
535
+ self, winner_id: str, loser_id: str, reason: str, confidence: float,
536
+ ) -> str:
537
+ proposal_id = _generate_id(MERGE_PROPOSAL_ID_PREFIX)
538
+ now = datetime.now(timezone.utc).isoformat()
539
+
540
+ async with self._driver.session(database=self._database) as session:
541
+ result = await session.run(
542
+ "CREATE (mp:MergeProposal {id: $id, winner_id: $winner, loser_id: $loser, "
543
+ " reason: $reason, confidence: $conf, status: 'pending', created_at: $now})",
544
+ id=proposal_id, winner=winner_id, loser=loser_id,
545
+ reason=reason, conf=confidence, now=now,
546
+ )
547
+ await result.consume()
548
+ return proposal_id
549
+
550
+ async def get_merge_proposal(self, proposal_id: str) -> Optional[Dict[str, Any]]:
551
+ async with self._driver.session(database=self._database) as session:
552
+ result = await session.run(
553
+ "MATCH (mp:MergeProposal {id: $id}) RETURN mp", id=proposal_id,
554
+ )
555
+ record = await result.single()
556
+ if record is None:
557
+ return None
558
+ return _node_to_dict(record["mp"])
559
+
560
+ async def update_merge_proposal_status(self, proposal_id: str, status: str) -> None:
561
+ async with self._driver.session(database=self._database) as session:
562
+ result = await session.run(
563
+ "MATCH (mp:MergeProposal {id: $id}) SET mp.status = $status",
564
+ id=proposal_id, status=status,
565
+ )
566
+ await result.consume()
567
+
568
+ async def get_pending_merge_proposals(self) -> List[Dict[str, Any]]:
569
+ async with self._driver.session(database=self._database) as session:
570
+ result = await session.run(
571
+ "MATCH (mp:MergeProposal {status: 'pending'}) RETURN mp"
572
+ )
573
+ records = await result.data()
574
+ return [_node_to_dict(r["mp"]) for r in records]
575
+
576
+ async def execute_merge(
577
+ self, winner_id: str, loser_id: str, merge_properties: bool = True,
578
+ ) -> None:
579
+ async with self._driver.session(database=self._database) as session:
580
+ if merge_properties:
581
+ result = await session.run(
582
+ "MATCH (w:Entity {id: $winner}), (l:Entity {id: $loser}) "
583
+ "WITH w, l "
584
+ "SET w.aliases = CASE WHEN w.aliases IS NULL THEN l.aliases "
585
+ " ELSE apoc.coll.union(w.aliases, l.aliases) END",
586
+ winner=winner_id, loser=loser_id,
587
+ )
588
+ await result.consume()
589
+
590
+ # Re-point incoming relationships
591
+ result = await session.run(
592
+ "MATCH (other)-[r]->(loser:Entity {id: $loser}) "
593
+ "WITH other, r, loser, type(r) AS rtype, properties(r) AS rprops "
594
+ "MATCH (winner:Entity {id: $winner}) "
595
+ "CALL apoc.create.relationship(other, rtype, rprops, winner) YIELD rel "
596
+ "DELETE r",
597
+ loser=loser_id, winner=winner_id,
598
+ )
599
+ await result.consume()
600
+
601
+ # Re-point outgoing relationships
602
+ result = await session.run(
603
+ "MATCH (loser:Entity {id: $loser})-[r]->(other) "
604
+ "WITH loser, r, other, type(r) AS rtype, properties(r) AS rprops "
605
+ "MATCH (winner:Entity {id: $winner}) "
606
+ "CALL apoc.create.relationship(winner, rtype, rprops, other) YIELD rel "
607
+ "DELETE r",
608
+ loser=loser_id, winner=winner_id,
609
+ )
610
+ await result.consume()
611
+
612
+ # Delete loser
613
+ result = await session.run(
614
+ "MATCH (l:Entity {id: $loser}) DETACH DELETE l", loser=loser_id,
615
+ )
616
+ await result.consume()
617
+
618
+ # ── Stats ─────────────────────────────────────────────────────────────
619
+
620
+ async def get_stats(self) -> Dict[str, Any]:
621
+ async with self._driver.session(database=self._database) as session:
622
+ type_result = await session.run(
623
+ "MATCH (e:Entity) WHERE e.entity_type IS NOT NULL RETURN e.entity_type AS type, count(e) AS cnt"
624
+ )
625
+ type_records = await type_result.data()
626
+ entities_by_type = {r["type"]: r["cnt"] for r in type_records}
627
+
628
+ total_result = await session.run("MATCH (e:Entity) RETURN count(e) AS total")
629
+ total_record = await total_result.single()
630
+
631
+ rel_result = await session.run("MATCH ()-[r]->() RETURN count(r) AS total")
632
+ rel_record = await rel_result.single()
633
+
634
+ active_result = await session.run(
635
+ "MATCH ()-[r]->() WHERE r.valid_to IS NULL RETURN count(r) AS total"
636
+ )
637
+ active_record = await active_result.single()
638
+
639
+ obs_result = await session.run("MATCH (o:Observation) RETURN count(o) AS total")
640
+ obs_record = await obs_result.single()
641
+
642
+ merge_result = await session.run(
643
+ "MATCH (mp:MergeProposal {status: 'pending'}) RETURN count(mp) AS total"
644
+ )
645
+ merge_record = await merge_result.single()
646
+
647
+ return {
648
+ "total_entities": total_record["total"] if total_record else 0,
649
+ "entities_by_type": entities_by_type,
650
+ "total_relationships": rel_record["total"] if rel_record else 0,
651
+ "active_relationships": active_record["total"] if active_record else 0,
652
+ "total_observations": obs_record["total"] if obs_record else 0,
653
+ "merge_proposals_pending": merge_record["total"] if merge_record else 0,
654
+ }