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,132 @@
1
+ -- Colony World Model SQLite schema
2
+ -- All tables prefixed with wm_ to avoid collisions with existing schema.
3
+
4
+ -- Base entity table (all types)
5
+ CREATE TABLE IF NOT EXISTS wm_entities (
6
+ id TEXT PRIMARY KEY, -- we-<timestamp>-<random7>
7
+ name TEXT NOT NULL,
8
+ entity_type TEXT NOT NULL
9
+ CHECK(entity_type IN (
10
+ 'person', 'company', 'project', 'product',
11
+ 'location', 'event', 'concept'
12
+ )),
13
+ aliases TEXT NOT NULL DEFAULT '[]', -- JSON array
14
+ external_ids TEXT NOT NULL DEFAULT '{}', -- JSON object
15
+ properties TEXT NOT NULL DEFAULT '{}', -- JSON object
16
+ confidence REAL NOT NULL DEFAULT 0.5,
17
+ first_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
18
+ last_seen TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
19
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
20
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_wm_entities_type
24
+ ON wm_entities(entity_type);
25
+ CREATE INDEX IF NOT EXISTS idx_wm_entities_name
26
+ ON wm_entities(name COLLATE NOCASE);
27
+ CREATE INDEX IF NOT EXISTS idx_wm_entities_confidence
28
+ ON wm_entities(confidence DESC);
29
+
30
+ -- Full-text search index on name + aliases
31
+ CREATE VIRTUAL TABLE IF NOT EXISTS wm_entities_fts
32
+ USING fts5(
33
+ id UNINDEXED,
34
+ name,
35
+ aliases,
36
+ content='wm_entities',
37
+ content_rowid='rowid'
38
+ );
39
+
40
+ -- Triggers to keep FTS in sync
41
+ CREATE TRIGGER IF NOT EXISTS wm_entities_fts_insert
42
+ AFTER INSERT ON wm_entities BEGIN
43
+ INSERT INTO wm_entities_fts(rowid, id, name, aliases)
44
+ VALUES (new.rowid, new.id, new.name, new.aliases);
45
+ END;
46
+
47
+ CREATE TRIGGER IF NOT EXISTS wm_entities_fts_update
48
+ AFTER UPDATE ON wm_entities BEGIN
49
+ INSERT INTO wm_entities_fts(wm_entities_fts, rowid, id, name, aliases)
50
+ VALUES ('delete', old.rowid, old.id, old.name, old.aliases);
51
+ INSERT INTO wm_entities_fts(rowid, id, name, aliases)
52
+ VALUES (new.rowid, new.id, new.name, new.aliases);
53
+ END;
54
+
55
+ CREATE TRIGGER IF NOT EXISTS wm_entities_fts_delete
56
+ AFTER DELETE ON wm_entities BEGIN
57
+ INSERT INTO wm_entities_fts(wm_entities_fts, rowid, id, name, aliases)
58
+ VALUES ('delete', old.rowid, old.id, old.name, old.aliases);
59
+ END;
60
+
61
+ -- Relationships
62
+ CREATE TABLE IF NOT EXISTS wm_relationships (
63
+ id TEXT PRIMARY KEY, -- wr-<timestamp>-<random7>
64
+ source_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
65
+ target_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
66
+ relationship_type TEXT NOT NULL,
67
+ confidence REAL NOT NULL DEFAULT 0.5,
68
+ valid_from TEXT, -- ISO8601; NULL = unknown start
69
+ valid_to TEXT, -- ISO8601; NULL = still active
70
+ properties TEXT NOT NULL DEFAULT '{}', -- JSON
71
+ source_observation_id TEXT,
72
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
73
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
74
+ );
75
+
76
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_source
77
+ ON wm_relationships(source_id);
78
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_target
79
+ ON wm_relationships(target_id);
80
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_type
81
+ ON wm_relationships(relationship_type);
82
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_active
83
+ ON wm_relationships(valid_to)
84
+ WHERE valid_to IS NULL;
85
+ CREATE INDEX IF NOT EXISTS idx_wm_rels_temporal
86
+ ON wm_relationships(valid_from, valid_to);
87
+
88
+ -- Observations (provenance trail)
89
+ CREATE TABLE IF NOT EXISTS wm_observations (
90
+ id TEXT PRIMARY KEY, -- wo-<timestamp>-<random7>
91
+ entity_id TEXT REFERENCES wm_entities(id) ON DELETE CASCADE,
92
+ relationship_id TEXT REFERENCES wm_relationships(id) ON DELETE CASCADE,
93
+ observation TEXT NOT NULL,
94
+ source TEXT,
95
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
96
+ );
97
+
98
+ CREATE INDEX IF NOT EXISTS idx_wm_obs_entity
99
+ ON wm_observations(entity_id, created_at DESC);
100
+
101
+ -- Merge proposals
102
+ CREATE TABLE IF NOT EXISTS wm_merge_proposals (
103
+ id TEXT PRIMARY KEY, -- mp-<timestamp>-<random7>
104
+ candidate_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
105
+ existing_id TEXT NOT NULL REFERENCES wm_entities(id) ON DELETE CASCADE,
106
+ match_confidence REAL NOT NULL,
107
+ match_reason TEXT NOT NULL,
108
+ evidence TEXT NOT NULL DEFAULT '{}', -- JSON
109
+ status TEXT NOT NULL DEFAULT 'pending'
110
+ CHECK(status IN ('pending', 'approved', 'rejected', 'expired')),
111
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
112
+ resolved_at TEXT
113
+ );
114
+
115
+ -- Merge audit log
116
+ CREATE TABLE IF NOT EXISTS wm_merge_log (
117
+ id TEXT PRIMARY KEY, -- ma-<timestamp>-<random7>
118
+ surviving_id TEXT NOT NULL,
119
+ retired_id TEXT NOT NULL,
120
+ relationships_repointed INTEGER NOT NULL DEFAULT 0,
121
+ properties_updated INTEGER NOT NULL DEFAULT 0,
122
+ executed_by TEXT NOT NULL, -- "auto" | "owner_approved"
123
+ merge_proposal_id TEXT,
124
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
125
+ );
126
+
127
+ -- Schema version tracking
128
+ CREATE TABLE IF NOT EXISTS wm_schema_migrations (
129
+ version TEXT PRIMARY KEY,
130
+ applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
131
+ description TEXT
132
+ );
@@ -0,0 +1,545 @@
1
+ """World Model primary store interface.
2
+
3
+ SQLite is the canonical typed-observation backend. Neo4j and PostgreSQL are
4
+ legacy entity/relationship adapters without typed-observation support.
5
+ All callers must use this interface and never access the backend directly.
6
+ """
7
+
8
+ import logging
9
+ import os
10
+ from datetime import datetime, timedelta, timezone
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional, Union
13
+ from collections import deque
14
+
15
+ from .config import WorldModelConfig
16
+ from .constants import CAUSAL_RELATIONSHIP_TYPES
17
+ from .entities import BaseEntity
18
+ from .relationships import WorldRelationship
19
+ from .sqlite.backend import SQLiteBackend
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _is_causal(rel_type: str) -> bool:
25
+ return str(rel_type or "").strip().upper() in CAUSAL_RELATIONSHIP_TYPES
26
+
27
+ # Optional inline belief-maintenance hook (item 7): called as
28
+ # cb(entity_id, key, old_value, new_value, old_conf, new_conf) when a
29
+ # property UPDATE changes an existing value, so the superseded value gets an
30
+ # audit row. Set at boot by the server; never raises into the write path.
31
+ _property_audit_hook = None
32
+
33
+
34
+ def set_property_audit_hook(cb) -> None:
35
+ global _property_audit_hook
36
+ _property_audit_hook = cb
37
+
38
+
39
+ @dataclass
40
+ class GraphNeighborhoodResult:
41
+ center: BaseEntity
42
+ reachable: List[BaseEntity]
43
+ edges: List[WorldRelationship]
44
+ hop_counts: Dict[str, int] # entity_id → min hops from center
45
+ truncated: bool = False # True if max_nodes reached
46
+
47
+
48
+ @dataclass
49
+ class WorldModelStats:
50
+ total_entities: int
51
+ entities_by_type: Dict[str, int]
52
+ total_relationships: int
53
+ active_relationships: int
54
+ total_observations: int
55
+ merge_proposals_pending: int
56
+
57
+
58
+ class WorldModelStore:
59
+ """Primary interface to the Colony World Model entity graph.
60
+
61
+ SQLite is selected by default. Explicit legacy Neo4j/Postgres configurations
62
+ may fall back to SQLite when credentials or drivers are missing. Typed
63
+ observation methods reject an actual legacy backend rather than claiming
64
+ equivalent provenance, correction, temporal-view or erasure behavior.
65
+ All methods are async. Callers MUST NOT access the backing store directly.
66
+ """
67
+
68
+ def __init__(self, config: Optional[WorldModelConfig] = None) -> None:
69
+ self._config = config or WorldModelConfig()
70
+ self._backend: Optional[SQLiteBackend] = None
71
+
72
+ async def connect(self) -> None:
73
+ """Initialize and connect to the storage backend."""
74
+ if self._config.backend == "neo4j":
75
+ try:
76
+ from apsimo.world_model.neo4j.backend import Neo4jBackend
77
+ neo4j_uri = self._config.neo4j_uri
78
+ neo4j_db = self._config.neo4j_database
79
+ neo4j_user = os.environ.get("NEO4J_USER", "neo4j")
80
+ neo4j_pass = os.environ.get("NEO4J_PASSWORD", "")
81
+ if neo4j_uri and neo4j_pass:
82
+ self._backend = Neo4jBackend(
83
+ uri=neo4j_uri,
84
+ database=neo4j_db,
85
+ username=neo4j_user,
86
+ password=neo4j_pass,
87
+ )
88
+ else:
89
+ logger.warning("NEO4J_URI or NEO4J_PASSWORD not set — falling back to sqlite")
90
+ self._backend = SQLiteBackend(self._config.sqlite_path)
91
+ except ImportError:
92
+ logger.warning("neo4j driver not installed — falling back to sqlite")
93
+ self._backend = SQLiteBackend(self._config.sqlite_path)
94
+ elif self._config.backend == "postgres":
95
+ try:
96
+ from apsimo.world_model.postgres.backend import PostgresBackend
97
+ pg_conn = os.environ.get("WORLD_MODEL_PG_CONNECTION", "")
98
+ if pg_conn:
99
+ self._backend = PostgresBackend(pg_conn)
100
+ else:
101
+ logger.warning("WORLD_MODEL_PG_CONNECTION not set — falling back to sqlite")
102
+ self._backend = SQLiteBackend(self._config.sqlite_path)
103
+ except ImportError:
104
+ logger.warning("asyncpg not installed — falling back to sqlite")
105
+ self._backend = SQLiteBackend(self._config.sqlite_path)
106
+ elif self._config.backend == "sqlite":
107
+ self._backend = SQLiteBackend(self._config.sqlite_path)
108
+ else:
109
+ logger.warning("WorldModel backend '%s' not supported — defaulting to sqlite", self._config.backend)
110
+ self._backend = SQLiteBackend(self._config.sqlite_path)
111
+ await self._backend.connect()
112
+
113
+ async def close(self) -> None:
114
+ if self._backend:
115
+ await self._backend.close()
116
+
117
+ async def __aenter__(self) -> "WorldModelStore":
118
+ await self.connect()
119
+ return self
120
+
121
+ async def __aexit__(self, *args) -> None:
122
+ await self.close()
123
+
124
+ # ── Entity reads ──────────────────────────────────────────────────────────
125
+
126
+ async def get_entity(
127
+ self,
128
+ entity_id: str,
129
+ min_confidence: float = 0.30,
130
+ ) -> Optional[BaseEntity]:
131
+ """Fetch a single entity by ID.
132
+
133
+ Returns None if the entity does not exist or falls below min_confidence.
134
+ """
135
+ entity = await self._backend.get_entity(entity_id)
136
+ if entity and entity.confidence >= min_confidence:
137
+ return entity
138
+ return None
139
+
140
+ async def find_entities(
141
+ self,
142
+ query: str,
143
+ entity_type: Optional[str] = None,
144
+ min_confidence: float = 0.30,
145
+ limit: int = 20,
146
+ ) -> List[BaseEntity]:
147
+ """Full-text search across entity names and aliases."""
148
+ return await self._backend.find_entities(
149
+ query=query,
150
+ entity_type=entity_type,
151
+ min_confidence=min_confidence,
152
+ limit=limit,
153
+ )
154
+
155
+ async def get_entity_by_external_id(
156
+ self,
157
+ key: str,
158
+ value: str,
159
+ ) -> Optional[BaseEntity]:
160
+ """Look up an entity by external ID key/value pair."""
161
+ return await self._backend.get_entity_by_external_id(key, value)
162
+
163
+ # ── Graph traversal ───────────────────────────────────────────────────────
164
+
165
+ async def get_neighborhood(
166
+ self,
167
+ entity_id: str,
168
+ max_hops: int = 2,
169
+ relationship_types: Optional[List[str]] = None,
170
+ min_confidence: float = 0.30,
171
+ max_nodes: int = 200,
172
+ ) -> GraphNeighborhoodResult:
173
+ """BFS neighborhood traversal up to max_hops from entity_id.
174
+
175
+ MUST NOT exceed max_hops = 5 regardless of caller input.
176
+ """
177
+ max_hops = min(max_hops, self._config.max_graph_hops)
178
+ center = await self._backend.get_entity(entity_id)
179
+ if not center:
180
+ return GraphNeighborhoodResult(
181
+ center=None, reachable=[], edges=[], hop_counts={}, truncated=False
182
+ )
183
+
184
+ visited: Dict[str, int] = {entity_id: 0}
185
+ frontier = deque([entity_id])
186
+ reachable: List[BaseEntity] = []
187
+ edges: List[WorldRelationship] = []
188
+ truncated = False
189
+
190
+ while frontier:
191
+ current_id = frontier.popleft()
192
+ current_hop = visited[current_id]
193
+ if current_hop >= max_hops:
194
+ continue
195
+
196
+ neighbors = await self._backend.get_neighbors(
197
+ current_id, min_confidence=min_confidence,
198
+ relationship_types=relationship_types,
199
+ )
200
+ for neighbor, rel in neighbors:
201
+ # Query-only causal guard (H2.5, belt-and-braces over the
202
+ # backend exclusion): an untyped traversal never returns or
203
+ # walks causal edges; only explicitly typed queries do.
204
+ if not relationship_types and _is_causal(rel.relationship_type):
205
+ continue
206
+ edges.append(rel)
207
+ if neighbor.id not in visited:
208
+ hop = current_hop + 1
209
+ visited[neighbor.id] = hop
210
+ reachable.append(neighbor)
211
+ if len(reachable) >= max_nodes:
212
+ truncated = True
213
+ break
214
+ frontier.append(neighbor.id)
215
+ if truncated:
216
+ break
217
+
218
+ hop_counts = {eid: h for eid, h in visited.items() if eid != entity_id}
219
+ return GraphNeighborhoodResult(
220
+ center=center,
221
+ reachable=reachable,
222
+ edges=edges,
223
+ hop_counts=hop_counts,
224
+ truncated=truncated,
225
+ )
226
+
227
+ async def find_path(
228
+ self,
229
+ source_id: str,
230
+ target_id: str,
231
+ max_hops: int = 5,
232
+ min_confidence: float = 0.30,
233
+ ) -> Optional[List[WorldRelationship]]:
234
+ """Find shortest path between two entities via BFS.
235
+
236
+ Returns ordered list of relationships, or None if no path exists.
237
+ """
238
+ max_hops = min(max_hops, self._config.max_graph_hops)
239
+ if source_id == target_id:
240
+ return []
241
+
242
+ # BFS with path tracking
243
+ visited: Dict[str, Optional[tuple]] = {source_id: None} # id → (prev_id, rel)
244
+ frontier = deque([(source_id, 0)])
245
+
246
+ while frontier:
247
+ current_id, hops = frontier.popleft()
248
+ if hops >= max_hops:
249
+ continue
250
+ neighbors = await self._backend.get_neighbors(
251
+ current_id, min_confidence=min_confidence
252
+ )
253
+ for neighbor, rel in neighbors:
254
+ # Query-only causal guard: untyped path-finding never walks
255
+ # causal edges (see get_neighborhood).
256
+ if _is_causal(rel.relationship_type):
257
+ continue
258
+ if neighbor.id not in visited:
259
+ visited[neighbor.id] = (current_id, rel)
260
+ if neighbor.id == target_id:
261
+ # Reconstruct path
262
+ path = []
263
+ node = target_id
264
+ while visited[node] is not None:
265
+ prev_id, edge = visited[node]
266
+ path.append(edge)
267
+ node = prev_id
268
+ path.reverse()
269
+ return path
270
+ frontier.append((neighbor.id, hops + 1))
271
+ return None
272
+
273
+ async def find_common_neighbors(
274
+ self,
275
+ entity_id_a: str,
276
+ entity_id_b: str,
277
+ max_hops: int = 1,
278
+ ) -> List[BaseEntity]:
279
+ """Return entities reachable from both A and B within max_hops."""
280
+ result_a = await self.get_neighborhood(entity_id_a, max_hops=max_hops)
281
+ result_b = await self.get_neighborhood(entity_id_b, max_hops=max_hops)
282
+ ids_a = {e.id for e in result_a.reachable}
283
+ ids_b = {e.id for e in result_b.reachable}
284
+ shared_ids = ids_a & ids_b
285
+ return [e for e in result_a.reachable if e.id in shared_ids]
286
+
287
+ # ── Relationship reads ────────────────────────────────────────────────────
288
+
289
+ async def query_relationships(
290
+ self,
291
+ source_id: Optional[str] = None,
292
+ target_id: Optional[str] = None,
293
+ relationship_type: Optional[str] = None,
294
+ target_types: Optional[List[str]] = None,
295
+ active_only: bool = False,
296
+ min_confidence: float = 0.30,
297
+ limit: int = 100,
298
+ ) -> List[WorldRelationship]:
299
+ """Query relationships with flexible filtering.
300
+
301
+ Query-only causal guard (H2.5): when ``relationship_type`` is None
302
+ (an untyped, generic read) causal edges are excluded — enforced in
303
+ every backend and re-checked here so no backend drift can leak them.
304
+ Only explicitly typed queries (and /world/causal/*) return them.
305
+ """
306
+ rels = await self._backend.query_relationships(
307
+ source_id=source_id,
308
+ target_id=target_id,
309
+ relationship_type=relationship_type,
310
+ target_types=target_types,
311
+ active_only=active_only,
312
+ min_confidence=min_confidence,
313
+ limit=limit,
314
+ )
315
+ if not relationship_type:
316
+ rels = [r for r in rels if not _is_causal(r.relationship_type)]
317
+ return rels
318
+
319
+ async def query_at_time(
320
+ self,
321
+ entity_id: str,
322
+ as_of: str,
323
+ relationship_types: Optional[List[str]] = None,
324
+ ) -> List[WorldRelationship]:
325
+ """Return relationships active at a specific point in time.
326
+ Untyped calls exclude causal edges (query-only guard, H2.5)."""
327
+ rels = await self._backend.query_at_time(
328
+ entity_id=entity_id, as_of=as_of, relationship_types=relationship_types
329
+ )
330
+ if not relationship_types:
331
+ rels = [r for r in rels if not _is_causal(r.relationship_type)]
332
+ return rels
333
+
334
+ # ── Entity writes ─────────────────────────────────────────────────────────
335
+
336
+ async def upsert_entity(self, entity: BaseEntity) -> BaseEntity:
337
+ """Insert or update an entity. Returns the surviving entity."""
338
+ result = await self._backend.upsert_entity(entity)
339
+ self._emit_change("entity_upsert", entity_id=getattr(result, "id", None))
340
+ return result
341
+
342
+ async def update_entity_property(
343
+ self,
344
+ entity_id: str,
345
+ property_key: str,
346
+ property_value: Any,
347
+ confidence: float,
348
+ ) -> None:
349
+ """Update a legacy descriptive property if its confidence is higher.
350
+
351
+ This is not current operational truth. New operational consumers use
352
+ record_property_observation/property_state with explicit provenance.
353
+ """
354
+ # Capture the pre-update value so a supersession can be audited
355
+ # (item 7 inline hook). Best-effort; never blocks the write.
356
+ old_value, old_conf = None, 0.0
357
+ if _property_audit_hook is not None:
358
+ try:
359
+ prior = await self._backend.get_entity(entity_id)
360
+ if prior is not None:
361
+ props = getattr(prior, "properties", {}) or {}
362
+ old_value = props.get(property_key,
363
+ getattr(prior, property_key, None))
364
+ old_conf = float(props.get(f"_conf_{property_key}",
365
+ prior.confidence) or 0.0)
366
+ except Exception:
367
+ old_value = None
368
+ await self._backend.update_entity_property(
369
+ entity_id, property_key, property_value, confidence
370
+ )
371
+ if (_property_audit_hook is not None and old_value not in (None, "")
372
+ and str(old_value) != str(property_value)
373
+ and confidence >= old_conf):
374
+ try:
375
+ _property_audit_hook(entity_id, property_key, old_value,
376
+ property_value, old_conf, confidence)
377
+ except Exception:
378
+ logger.debug("property audit hook failed", exc_info=True)
379
+ self._emit_change("entity_property_update", entity_id=entity_id, property_key=property_key)
380
+
381
+ async def reinforce_entity(self, entity_id: str) -> None:
382
+ """Record a repeat mention without raising evidential confidence.
383
+
384
+ Familiarity updates last_seen/count. No-op on unsupported backends.
385
+ """
386
+ fn = getattr(self._backend, "reinforce_entity", None)
387
+ if fn is None:
388
+ logger.debug("reinforce_entity unsupported by %s backend",
389
+ type(self._backend).__name__)
390
+ return
391
+ await fn(entity_id)
392
+
393
+ async def add_entity_alias(self, entity_id: str, alias: str) -> None:
394
+ """Add an alias to an entity's alias list if not already present."""
395
+ await self._backend.add_entity_alias(entity_id, alias)
396
+ self._emit_change("entity_alias_added", entity_id=entity_id)
397
+
398
+ # ── Relationship writes ───────────────────────────────────────────────────
399
+
400
+ async def upsert_relationship(
401
+ self, relationship: WorldRelationship
402
+ ) -> WorldRelationship:
403
+ """Insert or update a relationship."""
404
+ result = await self._backend.upsert_relationship(relationship)
405
+ self._emit_change(
406
+ "relationship_upsert",
407
+ relationship_id=getattr(result, "id", None),
408
+ )
409
+ return result
410
+
411
+ async def close_relationship(
412
+ self, relationship_id: str, valid_to: str
413
+ ) -> None:
414
+ """Mark a relationship as ended by setting valid_to."""
415
+ await self._backend.close_relationship(relationship_id, valid_to)
416
+ self._emit_change("relationship_closed", relationship_id=relationship_id)
417
+
418
+ # ── Event emission ────────────────────────────────────────────────────────
419
+
420
+ @staticmethod
421
+ def _emit_change(change_type: str, **payload: Any) -> None:
422
+ """Broadcast a world_model_changed event. Best-effort — never raises."""
423
+ try:
424
+ from apsimo.events.broadcaster import emit as _emit
425
+ _emit(
426
+ "world_model_changed",
427
+ {"change_type": change_type, **{k: v for k, v in payload.items() if v is not None}},
428
+ )
429
+ except Exception:
430
+ pass
431
+
432
+ # ── Observations ─────────────────────────────────────────────────────────
433
+
434
+ async def record_property_observation(self, **kwargs):
435
+ """Append one scoped assertion; producers must supply canonical evidence.
436
+
437
+ SQLite is the qualified observation backend in this release. Other
438
+ adapters remain valid for legacy entities/edges and report unsupported
439
+ rather than substituting old unqualified property values.
440
+ """
441
+ from .observations import observation
442
+ put = getattr(self._backend, 'put_property_observation', None)
443
+ if put is None:
444
+ raise NotImplementedError('typed_world_observations_require_sqlite')
445
+ data = observation(**kwargs)
446
+ if await self.get_entity(data['entity_id'], min_confidence=0) is None:
447
+ raise ValueError('world_observation_entity_not_found')
448
+ result = await put(data)
449
+ self._emit_change('property_observed', entity_id=data['entity_id'], observation_id=data['observation_id'])
450
+ return result
451
+
452
+ async def property_state(self, entity_id, property_key, *, subject_person_id,
453
+ viewer_scope, shareability, as_of=None, source_ledger=None):
454
+ from .observations import project, iso
455
+ reader = getattr(self._backend, 'current_property_observations', None)
456
+ if reader is None:
457
+ raise NotImplementedError('typed_world_observations_require_sqlite')
458
+ moment = iso(as_of if as_of is not None else datetime.now(timezone.utc).isoformat())
459
+ rows = await reader(entity_id, property_key, subject_person_id=subject_person_id,
460
+ viewer_scope=viewer_scope, shareability=shareability, as_of=moment)
461
+ from .source_reports import validate_reports
462
+ rows = validate_reports(rows, source_ledger)
463
+ return project(rows, entity_id=entity_id, property_key=property_key,
464
+ subject_person_id=subject_person_id, viewer_scope=viewer_scope, shareability=shareability,
465
+ as_of=moment, coverage_limited=len(rows) >= 2001)
466
+
467
+ async def erase_property_evidence(self, evidence_refs, *, subject_person_id):
468
+ eraser = getattr(self._backend, 'erase_property_evidence', None)
469
+ if eraser is None:
470
+ raise NotImplementedError('typed_world_observations_require_sqlite')
471
+ removed = await eraser(evidence_refs, subject_person_id=subject_person_id)
472
+ if removed:
473
+ self._emit_change('property_evidence_invalidated', observation_ids=removed)
474
+ return removed
475
+
476
+ async def property_views(self, entity_ids, *, subject_person_id, viewer_scope, shareability,
477
+ as_of=None, source_ledger=None, limit=8):
478
+ """Bounded scoped facts about already-relevant entities for turn context."""
479
+ keys = getattr(self._backend, 'property_keys', None)
480
+ if keys is None:
481
+ raise NotImplementedError('typed_world_observations_require_sqlite')
482
+ limit = max(1, min(int(limit), 32))
483
+ result = []
484
+ for entity_id in list(dict.fromkeys(entity_ids))[:8]:
485
+ properties = await keys(entity_id, subject_person_id=subject_person_id, viewer_scope=viewer_scope,
486
+ shareability=shareability, limit=limit)
487
+ for key in properties:
488
+ result.append(await self.property_state(entity_id, key, subject_person_id=subject_person_id,
489
+ viewer_scope=viewer_scope, shareability=shareability, as_of=as_of, source_ledger=source_ledger))
490
+ if len(result) >= limit:
491
+ return result
492
+ return result
493
+
494
+ async def add_observation(
495
+ self,
496
+ entity_id: Optional[str],
497
+ relationship_id: Optional[str],
498
+ observation: str,
499
+ source: str,
500
+ ) -> str:
501
+ """Record a raw observation string for an entity or relationship."""
502
+ return await self._backend.add_observation(
503
+ entity_id, relationship_id, observation, source
504
+ )
505
+
506
+ # ── Maintenance ───────────────────────────────────────────────────────────
507
+
508
+ async def prune(self, ttl_days: Optional[int] = None,
509
+ max_confidence: Optional[float] = None) -> Dict[str, Any]:
510
+ """Remove low-confidence entities that haven't been seen within the
511
+ TTL (config: low_confidence_entity_ttl_days / min_confidence_for_query
512
+ by default). High-confidence entities are never pruned regardless of
513
+ age. Returns {"status", "pruned", "cutoff", "max_confidence"};
514
+ backends without a prune primitive report skipped instead of lying."""
515
+ backend_prune = getattr(self._backend, "prune_entities", None)
516
+ if backend_prune is None:
517
+ return {"status": "skipped",
518
+ "reason": f"{type(self._backend).__name__} has no prune"}
519
+ ttl = int(ttl_days if ttl_days is not None
520
+ else self._config.low_confidence_entity_ttl_days)
521
+ ceiling = float(max_confidence if max_confidence is not None
522
+ else self._config.min_confidence_for_query)
523
+ cutoff = (datetime.now(timezone.utc) - timedelta(days=ttl)
524
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
525
+ pruned = await backend_prune(cutoff, ceiling)
526
+ if pruned:
527
+ logger.info("world model pruned %d stale low-confidence entities "
528
+ "(last_seen < %s, confidence < %.2f)",
529
+ pruned, cutoff, ceiling)
530
+ return {"status": "ok", "pruned": pruned, "cutoff": cutoff,
531
+ "max_confidence": ceiling}
532
+
533
+ # ── Stats ─────────────────────────────────────────────────────────────────
534
+
535
+ async def get_stats(self) -> WorldModelStats:
536
+ """Return summary statistics for the world model graph."""
537
+ raw = await self._backend.get_stats()
538
+ return WorldModelStats(
539
+ total_entities=raw["total_entities"],
540
+ entities_by_type=raw["entities_by_type"],
541
+ total_relationships=raw["total_relationships"],
542
+ active_relationships=raw["active_relationships"],
543
+ total_observations=raw["total_observations"],
544
+ merge_proposals_pending=raw["merge_proposals_pending"],
545
+ )