apsimo 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (614) hide show
  1. apsimo/__init__.py +38 -0
  2. apsimo/__main__.py +6 -0
  3. apsimo/agent/__init__.py +6 -0
  4. apsimo/agent/client.py +276 -0
  5. apsimo/agent/models.py +46 -0
  6. apsimo/agents/__init__.py +20 -0
  7. apsimo/agents/models.py +264 -0
  8. apsimo/agents/store.py +861 -0
  9. apsimo/agents/websocket.py +522 -0
  10. apsimo/api/__init__.py +1 -0
  11. apsimo/api/auth_telemetry.py +287 -0
  12. apsimo/api/authority.py +1203 -0
  13. apsimo/api/contact_grants.py +347 -0
  14. apsimo/api/middleware.py +483 -0
  15. apsimo/api/routers/__init__.py +1 -0
  16. apsimo/api/routers/commitment_work.py +265 -0
  17. apsimo/api/routers/context_gate.py +123 -0
  18. apsimo/api/routers/executions.py +140 -0
  19. apsimo/api/routers/followup_plans.py +147 -0
  20. apsimo/api/routers/governed_actions.py +162 -0
  21. apsimo/api/routers/host.py +14473 -0
  22. apsimo/api/routers/initiative_work.py +115 -0
  23. apsimo/api/routers/mining.py +104 -0
  24. apsimo/api/routers/observations.py +110 -0
  25. apsimo/api/routers/social_state.py +225 -0
  26. apsimo/api/routers/task_queue.py +2715 -0
  27. apsimo/api/routers/temporal_followups.py +251 -0
  28. apsimo/api/routers/transport.py +110 -0
  29. apsimo/api/routers/transport_ingress_api.py +240 -0
  30. apsimo/api/schemas/__init__.py +1 -0
  31. apsimo/api/schemas/host.py +1949 -0
  32. apsimo/autonomy/cli.py +110 -0
  33. apsimo/autonomy/condition_worker.py +437 -0
  34. apsimo/autonomy/config.py +424 -0
  35. apsimo/autonomy/loop.py +4316 -0
  36. apsimo/autonomy/registry.py +339 -0
  37. apsimo/autonomy/scheduler.py +1822 -0
  38. apsimo/autonomy/synthesis.py +449 -0
  39. apsimo/backup.py +962 -0
  40. apsimo/beliefs/__init__.py +23 -0
  41. apsimo/beliefs/contradictions.py +109 -0
  42. apsimo/beliefs/decay.py +61 -0
  43. apsimo/beliefs/engine.py +479 -0
  44. apsimo/beliefs/models.py +67 -0
  45. apsimo/beliefs/promotion.py +41 -0
  46. apsimo/beliefs/resolve.py +58 -0
  47. apsimo/beliefs/source_claims.py +690 -0
  48. apsimo/beliefs/source_projection.py +883 -0
  49. apsimo/beliefs/source_time.py +208 -0
  50. apsimo/beliefs/store.py +133 -0
  51. apsimo/briefings/aggregators.py +824 -0
  52. apsimo/briefings/composer.py +420 -0
  53. apsimo/briefings/config.py +55 -0
  54. apsimo/briefings/delivery.py +439 -0
  55. apsimo/briefings/engagement.py +97 -0
  56. apsimo/briefings/engine.py +274 -0
  57. apsimo/briefings/enhancer.py +99 -0
  58. apsimo/briefings/models.py +183 -0
  59. apsimo/briefings/scheduler.py +382 -0
  60. apsimo/briefings/store.py +435 -0
  61. apsimo/chain/__init__.py +48 -0
  62. apsimo/chain/block.py +100 -0
  63. apsimo/chain/cli.py +704 -0
  64. apsimo/chain/genesis.py +443 -0
  65. apsimo/chain/identity.py +416 -0
  66. apsimo/chain/keys.py +1025 -0
  67. apsimo/chain/local_keys.py +187 -0
  68. apsimo/chain/manager.py +290 -0
  69. apsimo/chain/node.py +163 -0
  70. apsimo/chain/plugin_transactions.py +371 -0
  71. apsimo/chain/protocol.py +220 -0
  72. apsimo/chain/state_machine.py +676 -0
  73. apsimo/chain/storage.py +503 -0
  74. apsimo/chain/transactions.py +250 -0
  75. apsimo/chain/validation.py +397 -0
  76. apsimo/channels/__init__.py +1 -0
  77. apsimo/channels/manifest.py +31 -0
  78. apsimo/channels/migrations/001_channels_schema.sql +12 -0
  79. apsimo/channels/phone_gateways.py +42 -0
  80. apsimo/channels/presence.py +188 -0
  81. apsimo/channels/router.py +235 -0
  82. apsimo/channels/store.py +231 -0
  83. apsimo/cli.py +2688 -0
  84. apsimo/cognition/__init__.py +11 -0
  85. apsimo/cognition/charter.py +398 -0
  86. apsimo/cognition/drive_governance.py +3530 -0
  87. apsimo/cognition/evidence_pipeline.py +1627 -0
  88. apsimo/cognition/external_events.py +932 -0
  89. apsimo/cognition/goal_spine.py +3488 -0
  90. apsimo/cognition/introspection.py +214 -0
  91. apsimo/cognition/prompt.py +150 -0
  92. apsimo/cognition/runtime.py +108 -0
  93. apsimo/cognition/trigger.py +154 -0
  94. apsimo/commitments/__init__.py +18 -0
  95. apsimo/commitments/local_work.py +355 -0
  96. apsimo/commitments/store.py +1052 -0
  97. apsimo/commitments/work.py +91 -0
  98. apsimo/compat.py +53 -0
  99. apsimo/compression/__init__.py +467 -0
  100. apsimo/connectors/__init__.py +21 -0
  101. apsimo/connectors/base.py +152 -0
  102. apsimo/connectors/caldav_calendar.py +125 -0
  103. apsimo/connectors/fs_documents.py +85 -0
  104. apsimo/connectors/imap_email.py +138 -0
  105. apsimo/connectors/manager.py +218 -0
  106. apsimo/connectors/webhook_pull.py +88 -0
  107. apsimo/contacts/__init__.py +33 -0
  108. apsimo/contacts/comms.py +357 -0
  109. apsimo/contacts/config.py +79 -0
  110. apsimo/contacts/exporters/__init__.py +1 -0
  111. apsimo/contacts/exporters/vcard.py +71 -0
  112. apsimo/contacts/identity_links.py +251 -0
  113. apsimo/contacts/importer.py +280 -0
  114. apsimo/contacts/importers/__init__.py +1 -0
  115. apsimo/contacts/importers/batch.py +43 -0
  116. apsimo/contacts/importers/macos_contacts.py +101 -0
  117. apsimo/contacts/migrations/001_contacts_schema.sql +141 -0
  118. apsimo/contacts/migrations/002_trust_scopes.sql +36 -0
  119. apsimo/contacts/migrations/003_open_gateway_enum.sql +32 -0
  120. apsimo/contacts/migrations/004_contact_provision_operations.sql +18 -0
  121. apsimo/contacts/migrations/005_identity_links.sql +27 -0
  122. apsimo/contacts/models.py +308 -0
  123. apsimo/contacts/scoring.py +16 -0
  124. apsimo/contacts/store.py +1623 -0
  125. apsimo/contacts/transport_ingress.py +252 -0
  126. apsimo/contacts/world_bridge.py +314 -0
  127. apsimo/contextgate/__init__.py +69 -0
  128. apsimo/contextgate/chunker.py +169 -0
  129. apsimo/contextgate/estimate.py +54 -0
  130. apsimo/contextgate/gate.py +313 -0
  131. apsimo/contextgate/retrieve.py +115 -0
  132. apsimo/delivery/__init__.py +16 -0
  133. apsimo/delivery/bridge.py +1260 -0
  134. apsimo/delivery/channels.py +526 -0
  135. apsimo/delivery/classification.py +50 -0
  136. apsimo/delivery/rate_limiter.py +268 -0
  137. apsimo/delivery/reachout_policy.py +206 -0
  138. apsimo/directed/__init__.py +22 -0
  139. apsimo/directed/audit.py +167 -0
  140. apsimo/directed/intake.py +95 -0
  141. apsimo/directed/models.py +191 -0
  142. apsimo/directed/service.py +509 -0
  143. apsimo/directives/__init__.py +25 -0
  144. apsimo/directives/evidence.py +87 -0
  145. apsimo/directives/extractor.py +188 -0
  146. apsimo/directives/guard.py +364 -0
  147. apsimo/directives/models.py +206 -0
  148. apsimo/directives/service.py +372 -0
  149. apsimo/directives/store.py +167 -0
  150. apsimo/doctor.py +2173 -0
  151. apsimo/environment.py +43 -0
  152. apsimo/events/__init__.py +33 -0
  153. apsimo/events/broadcaster.py +98 -0
  154. apsimo/events/bus.py +217 -0
  155. apsimo/events/journal.py +863 -0
  156. apsimo/events/stream.py +131 -0
  157. apsimo/events/types.py +150 -0
  158. apsimo/execution_results.py +357 -0
  159. apsimo/feedback/__init__.py +5 -0
  160. apsimo/feedback/store.py +76 -0
  161. apsimo/feeds/__init__.py +19 -0
  162. apsimo/feeds/cli.py +84 -0
  163. apsimo/feeds/engine.py +437 -0
  164. apsimo/feeds/example-feed.yaml +77 -0
  165. apsimo/feeds/hermes_cron.py +126 -0
  166. apsimo/feeds/manager.py +235 -0
  167. apsimo/feeds/spec.py +250 -0
  168. apsimo/feeds/template.py +202 -0
  169. apsimo/gate/__init__.py +18 -0
  170. apsimo/gate/audit.py +61 -0
  171. apsimo/gate/communication_policy.py +166 -0
  172. apsimo/gate/config.py +72 -0
  173. apsimo/gate/context_provenance.py +170 -0
  174. apsimo/gate/env_risk.py +226 -0
  175. apsimo/gate/guard_audit.py +353 -0
  176. apsimo/gate/layers/__init__.py +1 -0
  177. apsimo/gate/layers/base.py +15 -0
  178. apsimo/gate/layers/l1_recipient.py +66 -0
  179. apsimo/gate/layers/l2_pii.py +134 -0
  180. apsimo/gate/layers/l3_cross_context.py +50 -0
  181. apsimo/gate/layers/l4_trust_tier.py +78 -0
  182. apsimo/gate/layers/l5_injection.py +199 -0
  183. apsimo/gate/layers/l6_review.py +86 -0
  184. apsimo/gate/layers/l7_delay.py +100 -0
  185. apsimo/gate/layers/tom2_epistemic.py +185 -0
  186. apsimo/gate/models.py +64 -0
  187. apsimo/gate/pending_dispatch.py +5 -0
  188. apsimo/gate/pipeline.py +206 -0
  189. apsimo/gate/rejection.py +259 -0
  190. apsimo/gate/response_guard.py +700 -0
  191. apsimo/gate/rulesets/injection_v1.yaml +51 -0
  192. apsimo/gate/surface_policy.py +189 -0
  193. apsimo/gate/taint.py +226 -0
  194. apsimo/genesis.json +9 -0
  195. apsimo/goals/__init__.py +100 -0
  196. apsimo/goals/config.py +38 -0
  197. apsimo/goals/decomposer.py +421 -0
  198. apsimo/goals/engine.py +617 -0
  199. apsimo/goals/inference.py +354 -0
  200. apsimo/goals/models.py +302 -0
  201. apsimo/goals/priority.py +270 -0
  202. apsimo/goals/queue_bridge.py +149 -0
  203. apsimo/goals/replan.py +450 -0
  204. apsimo/goals/schema.sql +89 -0
  205. apsimo/goals/store.py +692 -0
  206. apsimo/governed_actions.py +1708 -0
  207. apsimo/harness_integration/__init__.py +45 -0
  208. apsimo/harness_integration/context.py +41 -0
  209. apsimo/harness_integration/skills.py +231 -0
  210. apsimo/identity/__init__.py +26 -0
  211. apsimo/identity/participants.py +181 -0
  212. apsimo/identity/resolver.py +329 -0
  213. apsimo/identity_bootstrap/__init__.py +5 -0
  214. apsimo/identity_bootstrap/builder.py +208 -0
  215. apsimo/identity_bootstrap/corpus.py +443 -0
  216. apsimo/identity_bootstrap/models.py +54 -0
  217. apsimo/identity_bootstrap/runner.py +353 -0
  218. apsimo/identity_bootstrap/seeders/__init__.py +25 -0
  219. apsimo/identity_bootstrap/seeders/briefings.py +109 -0
  220. apsimo/identity_bootstrap/seeders/chain.py +57 -0
  221. apsimo/identity_bootstrap/seeders/goals.py +128 -0
  222. apsimo/identity_bootstrap/seeders/memory.py +191 -0
  223. apsimo/identity_bootstrap/seeders/neo4j_cognition.py +79 -0
  224. apsimo/identity_bootstrap/seeders/relationship.py +152 -0
  225. apsimo/identity_bootstrap/seeders/sessions.py +67 -0
  226. apsimo/identity_bootstrap/seeders/skills.py +92 -0
  227. apsimo/identity_bootstrap/seeders/task_queue.py +72 -0
  228. apsimo/identity_bootstrap/seeders/world_model.py +143 -0
  229. apsimo/identity_bootstrap/self_query.py +92 -0
  230. apsimo/identity_bootstrap/self_reflection.py +155 -0
  231. apsimo/identity_bootstrap/skill.py +37 -0
  232. apsimo/identity_bootstrap/verifier.py +436 -0
  233. apsimo/initiatives/__init__.py +20 -0
  234. apsimo/initiatives/action_registry.py +454 -0
  235. apsimo/initiatives/approval_authority.py +2105 -0
  236. apsimo/initiatives/approval_policy.py +123 -0
  237. apsimo/initiatives/assignment.py +263 -0
  238. apsimo/initiatives/backup_evidence.py +100 -0
  239. apsimo/initiatives/context_freshness.py +103 -0
  240. apsimo/initiatives/models.py +318 -0
  241. apsimo/initiatives/native_work.py +270 -0
  242. apsimo/initiatives/standing_approvals.py +232 -0
  243. apsimo/initiatives/store.py +1081 -0
  244. apsimo/initiatives/temporal_followup.py +410 -0
  245. apsimo/intelligence/__init__.py +1 -0
  246. apsimo/intelligence/cognition/__init__.py +24 -0
  247. apsimo/intelligence/cognition/gap_detector.py +148 -0
  248. apsimo/intelligence/cognition/metalearner.py +547 -0
  249. apsimo/intelligence/cognition/metrics_collector.py +217 -0
  250. apsimo/intelligence/cognition/performance_index.py +299 -0
  251. apsimo/intelligence/cognition/registry.py +192 -0
  252. apsimo/intelligence/cognition/strategy_adjuster.py +222 -0
  253. apsimo/intelligence/cognition/types.py +16 -0
  254. apsimo/intelligence/components/__init__.py +66 -0
  255. apsimo/intelligence/components/anomaly_detector.py +413 -0
  256. apsimo/intelligence/components/initiative_engine.py +2643 -0
  257. apsimo/intelligence/components/preference_learner.py +521 -0
  258. apsimo/intelligence/components/research_orchestrator.py +358 -0
  259. apsimo/intelligence/components/self_directed_thinker.py +221 -0
  260. apsimo/intelligence/components/self_reflector.py +252 -0
  261. apsimo/intelligence/components/session_continuity.py +154 -0
  262. apsimo/intelligence/components/task_planner.py +320 -0
  263. apsimo/intelligence/components/tool_learner.py +217 -0
  264. apsimo/intelligence/graph/__init__.py +79 -0
  265. apsimo/intelligence/graph/client.py +2483 -0
  266. apsimo/intelligence/graph/consolidator.py +405 -0
  267. apsimo/intelligence/graph/distiller.py +312 -0
  268. apsimo/intelligence/graph/migrations.py +129 -0
  269. apsimo/intelligence/graph/queries.py +248 -0
  270. apsimo/intelligence/graph/recall.py +281 -0
  271. apsimo/intelligence/graph/reconciler.py +144 -0
  272. apsimo/intelligence/graph/schema.py +337 -0
  273. apsimo/intelligence/graph/selection.py +252 -0
  274. apsimo/intelligence/learning/__init__.py +17 -0
  275. apsimo/intelligence/learning/continuous_learner.py +245 -0
  276. apsimo/intelligence/learning/feedback_store.py +321 -0
  277. apsimo/intelligence/mind_model/__init__.py +1 -0
  278. apsimo/intelligence/mind_model/graph_baseline.py +136 -0
  279. apsimo/intelligence/mind_model/signal_collector.py +361 -0
  280. apsimo/intelligence/relationships/__init__.py +11 -0
  281. apsimo/intelligence/relationships/profiler.py +389 -0
  282. apsimo/intelligence/relationships/scorer.py +560 -0
  283. apsimo/intelligence/relationships/signal_floor.py +66 -0
  284. apsimo/intelligence/relationships/trust_tiers.py +300 -0
  285. apsimo/intelligence/synthesis/__init__.py +40 -0
  286. apsimo/intelligence/synthesis/connection_discoverer.py +379 -0
  287. apsimo/intelligence/synthesis/cross_domain_analyzer.py +287 -0
  288. apsimo/intelligence/synthesis/insight_deliverer.py +171 -0
  289. apsimo/intelligence/synthesis/insight_store.py +79 -0
  290. apsimo/intelligence/synthesis/insight_validator.py +183 -0
  291. apsimo/intelligence/synthesis/novelty_scorer.py +267 -0
  292. apsimo/intelligence/turn_middleware/__init__.py +15 -0
  293. apsimo/intelligence/turn_middleware/memory_sync.py +119 -0
  294. apsimo/mcp/__init__.py +41 -0
  295. apsimo/mcp/__main__.py +6 -0
  296. apsimo/mcp/config.py +287 -0
  297. apsimo/mcp/server.py +501 -0
  298. apsimo/migrations.py +187 -0
  299. apsimo/mining/__init__.py +27 -0
  300. apsimo/mining/corpus.py +239 -0
  301. apsimo/mining/escalations.py +289 -0
  302. apsimo/mining/models.py +169 -0
  303. apsimo/mining/store.py +210 -0
  304. apsimo/models/__init__.py +30 -0
  305. apsimo/models/memory.py +80 -0
  306. apsimo/models/mesh.py +72 -0
  307. apsimo/models/person.py +104 -0
  308. apsimo/models/signal.py +108 -0
  309. apsimo/observations/__init__.py +15 -0
  310. apsimo/observations/store.py +277 -0
  311. apsimo/patterns/__init__.py +6 -0
  312. apsimo/patterns/extract.py +187 -0
  313. apsimo/patterns/store.py +227 -0
  314. apsimo/persona/__init__.py +1 -0
  315. apsimo/persona/engine.py +611 -0
  316. apsimo/persona/manifest.py +140 -0
  317. apsimo/projects/__init__.py +28 -0
  318. apsimo/projects/engine.py +1681 -0
  319. apsimo/projects/event_outbox.py +188 -0
  320. apsimo/projects/models.py +216 -0
  321. apsimo/projects/planner.py +181 -0
  322. apsimo/projects/store.py +1446 -0
  323. apsimo/proposals/__init__.py +12 -0
  324. apsimo/proposals/engine.py +114 -0
  325. apsimo/proposals/models.py +207 -0
  326. apsimo/qualification/__init__.py +1 -0
  327. apsimo/qualification/cases.py +75 -0
  328. apsimo/qualification/cli.py +51 -0
  329. apsimo/qualification/memory_cases.py +209 -0
  330. apsimo/qualification/records.py +92 -0
  331. apsimo/qualification/report.py +87 -0
  332. apsimo/qualification/runner.py +311 -0
  333. apsimo/qualification/structured_cases.py +131 -0
  334. apsimo/reasoning/__init__.py +13 -0
  335. apsimo/reasoning/executor.py +506 -0
  336. apsimo/reasoning/loop.py +373 -0
  337. apsimo/reasoning/native_tools/__init__.py +16 -0
  338. apsimo/reasoning/native_tools/calculate.py +141 -0
  339. apsimo/reasoning/native_tools/file_ops.py +150 -0
  340. apsimo/reasoning/native_tools/web_search.py +49 -0
  341. apsimo/reasoning/tool_policy.py +182 -0
  342. apsimo/redact/__init__.py +176 -0
  343. apsimo/repos/__init__.py +5 -0
  344. apsimo/repos/mirrors.py +204 -0
  345. apsimo/research/__init__.py +41 -0
  346. apsimo/research/artifact.py +482 -0
  347. apsimo/research/gatherer.py +387 -0
  348. apsimo/research/pipeline.py +513 -0
  349. apsimo/research/search/__init__.py +7 -0
  350. apsimo/research/search/base.py +41 -0
  351. apsimo/research/search/brave.py +59 -0
  352. apsimo/research/search/cache.py +51 -0
  353. apsimo/research/search/duckduckgo.py +103 -0
  354. apsimo/research/search/orchestrator.py +119 -0
  355. apsimo/research/search/serpapi.py +59 -0
  356. apsimo/research/search/tavily.py +59 -0
  357. apsimo/research/synthesizer.py +309 -0
  358. apsimo/router/__init__.py +30 -0
  359. apsimo/router/complexity_scorer.py +148 -0
  360. apsimo/router/endpoints.py +153 -0
  361. apsimo/router/fallback.py +58 -0
  362. apsimo/router/functions.py +243 -0
  363. apsimo/router/native_policy.py +52 -0
  364. apsimo/router/router.py +762 -0
  365. apsimo/router/self_learning.py +174 -0
  366. apsimo/router/tiers.py +677 -0
  367. apsimo/sandbox/__init__.py +21 -0
  368. apsimo/sandbox/backend.py +195 -0
  369. apsimo/sandbox/manager.py +173 -0
  370. apsimo/scope_bounds.py +7 -0
  371. apsimo/secrets/__init__.py +6 -0
  372. apsimo/secrets/backends/__init__.py +8 -0
  373. apsimo/secrets/backends/base.py +42 -0
  374. apsimo/secrets/backends/env.py +110 -0
  375. apsimo/secrets/backends/keyring.py +72 -0
  376. apsimo/secrets/backends/onepassword.py +232 -0
  377. apsimo/secrets/cli.py +191 -0
  378. apsimo/secrets/manager.py +160 -0
  379. apsimo/secrets/migration.py +101 -0
  380. apsimo/secrets/types.py +98 -0
  381. apsimo/seed.py +41 -0
  382. apsimo/self_model/__init__.py +37 -0
  383. apsimo/self_model/appraisals.py +673 -0
  384. apsimo/self_model/benchmark.py +1314 -0
  385. apsimo/self_model/brief.py +40 -0
  386. apsimo/self_model/event_concerns.py +1128 -0
  387. apsimo/self_model/execution_forecasts.py +353 -0
  388. apsimo/self_model/expectations.py +1595 -0
  389. apsimo/self_model/experiments.py +1150 -0
  390. apsimo/self_model/journal.py +148 -0
  391. apsimo/self_model/judgments.py +705 -0
  392. apsimo/self_model/native_outcomes.py +55 -0
  393. apsimo/self_model/params.py +220 -0
  394. apsimo/self_model/perspective.py +246 -0
  395. apsimo/self_model/reconcile.py +183 -0
  396. apsimo/self_model/reply_forecasts.py +381 -0
  397. apsimo/self_model/runtime_forecasts.py +296 -0
  398. apsimo/self_model/runtime_models.py +67 -0
  399. apsimo/self_model/settlement.py +207 -0
  400. apsimo/self_model/situation.py +1731 -0
  401. apsimo/self_model/store.py +883 -0
  402. apsimo/self_model/supervised.py +137 -0
  403. apsimo/self_model/thinker.py +99 -0
  404. apsimo/self_model/trust.py +388 -0
  405. apsimo/self_model/workspace.py +2388 -0
  406. apsimo/server.py +4197 -0
  407. apsimo/services/__init__.py +1 -0
  408. apsimo/services/agent_bridge.py +474 -0
  409. apsimo/services/initiative_executor.py +914 -0
  410. apsimo/services/instance.py +297 -0
  411. apsimo/sessions/__init__.py +22 -0
  412. apsimo/sessions/config.py +13 -0
  413. apsimo/sessions/context_loader.py +88 -0
  414. apsimo/sessions/federation_session.py +75 -0
  415. apsimo/sessions/isolated_session.py +98 -0
  416. apsimo/sessions/reports.py +84 -0
  417. apsimo/sessions/store.py +148 -0
  418. apsimo/setup.py +2818 -0
  419. apsimo/setup_hermes.py +879 -0
  420. apsimo/setup_local_work.py +218 -0
  421. apsimo/setup_native_goals.py +134 -0
  422. apsimo/setup_native_reviews.py +115 -0
  423. apsimo/skills/__init__.py +10 -0
  424. apsimo/skills/base.py +108 -0
  425. apsimo/skills/budget.py +28 -0
  426. apsimo/skills/executor.py +493 -0
  427. apsimo/skills/executors/__init__.py +1 -0
  428. apsimo/skills/executors/behavioral_correction.py +75 -0
  429. apsimo/skills/executors/capability_gap.py +38 -0
  430. apsimo/skills/executors/data_quality.py +163 -0
  431. apsimo/skills/executors/knowledge_acquisition.py +41 -0
  432. apsimo/skills/executors/operational_hygiene.py +185 -0
  433. apsimo/skills/executors/subsystem_health.py +169 -0
  434. apsimo/skills/hermes_export.py +431 -0
  435. apsimo/skills/index.py +123 -0
  436. apsimo/skills/learning/__init__.py +21 -0
  437. apsimo/skills/learning/novelty_detector.py +206 -0
  438. apsimo/skills/learning/pattern_extractor.py +199 -0
  439. apsimo/skills/learning/triggers.py +159 -0
  440. apsimo/skills/loader.py +246 -0
  441. apsimo/skills/migrations/002_progressive_loading.sql +6 -0
  442. apsimo/skills/migrations/backfill_triggers.py +20 -0
  443. apsimo/skills/models.py +202 -0
  444. apsimo/skills/packager.py +128 -0
  445. apsimo/skills/protocols.py +70 -0
  446. apsimo/skills/registry.py +191 -0
  447. apsimo/skills/runtime.py +58 -0
  448. apsimo/skills/sandbox_runner.py +229 -0
  449. apsimo/skills/scheduler.py +129 -0
  450. apsimo/skills/schema.py +79 -0
  451. apsimo/skills/security/__init__.py +12 -0
  452. apsimo/skills/security/guards.py +53 -0
  453. apsimo/skills/security/scanner.py +223 -0
  454. apsimo/skills_memory/__init__.py +26 -0
  455. apsimo/skills_memory/distill.py +159 -0
  456. apsimo/skills_memory/models.py +85 -0
  457. apsimo/skills_memory/retrieve.py +62 -0
  458. apsimo/skills_memory/store.py +172 -0
  459. apsimo/surprise/__init__.py +6 -0
  460. apsimo/surprise/accumulation.py +57 -0
  461. apsimo/surprise/scorer.py +102 -0
  462. apsimo/surprise/store.py +203 -0
  463. apsimo/task_queue/__init__.py +69 -0
  464. apsimo/task_queue/action_receipts.py +148 -0
  465. apsimo/task_queue/approval_relay_canary.py +108 -0
  466. apsimo/task_queue/config.py +85 -0
  467. apsimo/task_queue/contract.py +361 -0
  468. apsimo/task_queue/events.py +130 -0
  469. apsimo/task_queue/governor.py +1031 -0
  470. apsimo/task_queue/handlers/__init__.py +16 -0
  471. apsimo/task_queue/handlers/base.py +37 -0
  472. apsimo/task_queue/handlers/inference.py +640 -0
  473. apsimo/task_queue/handlers/monitoring.py +116 -0
  474. apsimo/task_queue/handlers/registry.py +75 -0
  475. apsimo/task_queue/handlers/subtask_handler.py +173 -0
  476. apsimo/task_queue/handlers/system_maintenance.py +147 -0
  477. apsimo/task_queue/mesh_integration.py +111 -0
  478. apsimo/task_queue/models.py +317 -0
  479. apsimo/task_queue/queue_manager.py +8286 -0
  480. apsimo/task_queue/routing.py +287 -0
  481. apsimo/task_queue/scheduler.py +252 -0
  482. apsimo/task_queue/schema.sql +197 -0
  483. apsimo/task_queue/work_control.py +342 -0
  484. apsimo/task_queue/worker.py +993 -0
  485. apsimo/telemetry.py +145 -0
  486. apsimo/tom/__init__.py +6 -0
  487. apsimo/tom/affect.py +387 -0
  488. apsimo/tom/approvals.py +171 -0
  489. apsimo/tom/arcs.py +896 -0
  490. apsimo/tom/asymmetry.py +131 -0
  491. apsimo/tom/eligibility.py +248 -0
  492. apsimo/tom/engagement.py +214 -0
  493. apsimo/tom/exposure.py +214 -0
  494. apsimo/tom/extractor.py +306 -0
  495. apsimo/tom/fact_adapters.py +144 -0
  496. apsimo/tom/facts.py +326 -0
  497. apsimo/tom/integration.py +592 -0
  498. apsimo/tom/leveled.py +118 -0
  499. apsimo/tom/levels.py +247 -0
  500. apsimo/tom/recipient_audit.py +995 -0
  501. apsimo/tom/recipient_simulator.py +593 -0
  502. apsimo/tom/source_lineage.py +93 -0
  503. apsimo/tom/tom2.py +277 -0
  504. apsimo/tom/visibility.py +559 -0
  505. apsimo/tom/visibility_store.py +414 -0
  506. apsimo/tools/__init__.py +0 -0
  507. apsimo/tools/definitions.py +740 -0
  508. apsimo/tools/handlers.py +943 -0
  509. apsimo/toolsmith/__init__.py +26 -0
  510. apsimo/toolsmith/authority.py +166 -0
  511. apsimo/toolsmith/engine.py +559 -0
  512. apsimo/toolsmith/integrity.py +100 -0
  513. apsimo/toolsmith/miner.py +145 -0
  514. apsimo/toolsmith/policy.py +110 -0
  515. apsimo/toolsmith/registry.py +635 -0
  516. apsimo/turns/__init__.py +17 -0
  517. apsimo/turns/audio.py +134 -0
  518. apsimo/turns/documents.py +235 -0
  519. apsimo/turns/executions.py +486 -0
  520. apsimo/turns/hermes_history.py +245 -0
  521. apsimo/turns/hermes_kanban.py +268 -0
  522. apsimo/turns/hermes_work.py +96 -0
  523. apsimo/turns/idempotency.py +752 -0
  524. apsimo/turns/local_work.py +115 -0
  525. apsimo/turns/media.py +581 -0
  526. apsimo/turns/reported_workers.py +196 -0
  527. apsimo/turns/source_annotations.py +283 -0
  528. apsimo/turns/source_attribution.py +154 -0
  529. apsimo/turns/source_read.py +351 -0
  530. apsimo/turns/source_vectors.py +263 -0
  531. apsimo/turns/video.py +210 -0
  532. apsimo/util/autonomy_preset.py +220 -0
  533. apsimo/util/instance.py +92 -0
  534. apsimo/util/model_output.py +25 -0
  535. apsimo/util/quiet_hours.py +27 -0
  536. apsimo/util/session_safety.py +37 -0
  537. apsimo/util/temporal.py +343 -0
  538. apsimo/vector/__init__.py +75 -0
  539. apsimo/vector/backfill.py +171 -0
  540. apsimo/vector/caption.py +114 -0
  541. apsimo/vector/collections.py +51 -0
  542. apsimo/vector/config.py +102 -0
  543. apsimo/vector/embedder.py +670 -0
  544. apsimo/vector/image_preprocess.py +406 -0
  545. apsimo/vector/image_store.py +296 -0
  546. apsimo/vector/indexes.py +162 -0
  547. apsimo/vector/migrate.py +334 -0
  548. apsimo/vector/multimodal_provider.py +417 -0
  549. apsimo/vector/multimodal_types.py +87 -0
  550. apsimo/vector/openai_provider.py +119 -0
  551. apsimo/vector/query.py +49 -0
  552. apsimo/vector/reranker.py +565 -0
  553. apsimo/vector/safety_image.py +159 -0
  554. apsimo/vector/scanner.py +197 -0
  555. apsimo/vector/setup.py +289 -0
  556. apsimo/vector/store.py +533 -0
  557. apsimo/vector/tiers.py +263 -0
  558. apsimo/work_orders.py +925 -0
  559. apsimo/workers/__init__.py +21 -0
  560. apsimo/workers/agent_bridge.py +640 -0
  561. apsimo/workers/colony_worker.py +382 -0
  562. apsimo/workers/queue_worker.py +441 -0
  563. apsimo/workers/skills_sync.py +152 -0
  564. apsimo/world_model/__init__.py +71 -0
  565. apsimo/world_model/causal_maintenance.py +131 -0
  566. apsimo/world_model/causal_policy.py +43 -0
  567. apsimo/world_model/causal_query.py +125 -0
  568. apsimo/world_model/confidence.py +54 -0
  569. apsimo/world_model/config.py +64 -0
  570. apsimo/world_model/constants.py +97 -0
  571. apsimo/world_model/entities.py +145 -0
  572. apsimo/world_model/expectation_resolvers.py +177 -0
  573. apsimo/world_model/extraction/__init__.py +7 -0
  574. apsimo/world_model/extraction/base.py +62 -0
  575. apsimo/world_model/extraction/conversation_extractor.py +262 -0
  576. apsimo/world_model/extraction/detector.py +74 -0
  577. apsimo/world_model/extraction/document_extractor.py +78 -0
  578. apsimo/world_model/extraction/formats/__init__.py +24 -0
  579. apsimo/world_model/extraction/formats/csv_fmt.py +68 -0
  580. apsimo/world_model/extraction/formats/html_fmt.py +72 -0
  581. apsimo/world_model/extraction/formats/json_fmt.py +68 -0
  582. apsimo/world_model/extraction/formats/pdf.py +43 -0
  583. apsimo/world_model/extraction/formats/text.py +27 -0
  584. apsimo/world_model/extraction/llm_extractor.py +164 -0
  585. apsimo/world_model/extraction/pipeline.py +73 -0
  586. apsimo/world_model/integrations/__init__.py +5 -0
  587. apsimo/world_model/integrations/mind_model_bridge.py +115 -0
  588. apsimo/world_model/integrations/social_intel_bridge.py +120 -0
  589. apsimo/world_model/jobs/__init__.py +4 -0
  590. apsimo/world_model/jobs/extraction_job.py +168 -0
  591. apsimo/world_model/llm_extract.py +572 -0
  592. apsimo/world_model/neo4j/__init__.py +5 -0
  593. apsimo/world_model/neo4j/backend.py +654 -0
  594. apsimo/world_model/observations.py +155 -0
  595. apsimo/world_model/populator.py +307 -0
  596. apsimo/world_model/postgres/__init__.py +1 -0
  597. apsimo/world_model/postgres/backend.py +683 -0
  598. apsimo/world_model/relationships.py +25 -0
  599. apsimo/world_model/resolution/__init__.py +13 -0
  600. apsimo/world_model/resolution/entity_resolver.py +232 -0
  601. apsimo/world_model/resolution/merge_audit.py +16 -0
  602. apsimo/world_model/resolution/merge_workflow.py +117 -0
  603. apsimo/world_model/source_reports.py +121 -0
  604. apsimo/world_model/sqlite/__init__.py +4 -0
  605. apsimo/world_model/sqlite/backend.py +855 -0
  606. apsimo/world_model/sqlite/schema.sql +132 -0
  607. apsimo/world_model/store.py +545 -0
  608. apsimo-1.3.0.dist-info/METADATA +78 -0
  609. apsimo-1.3.0.dist-info/RECORD +614 -0
  610. apsimo-1.3.0.dist-info/WHEEL +5 -0
  611. apsimo-1.3.0.dist-info/entry_points.txt +11 -0
  612. apsimo-1.3.0.dist-info/licenses/LICENSE +21 -0
  613. apsimo-1.3.0.dist-info/top_level.txt +2 -0
  614. colony_sidecar/__init__.py +4 -0
apsimo/tom/exposure.py ADDED
@@ -0,0 +1,214 @@
1
+ """Tom2 exposure ledger + budgets (L2.3) — refs only, never content.
2
+
3
+ Every level-2 rendering, when the system eventually wires one, must be
4
+ written here FIRST and budgeted against what was already shown. The ledger
5
+ slows aggregation (T6): a reader cannot harvest the epistemic map by asking
6
+ often, because budgets bind per (reader, subject) pair, per reader, and
7
+ globally per rolling 24h window.
8
+
9
+ PRIVACY INVARIANT (same pin as Tom2Store, enforced at write time and
10
+ regression-locked by a raw-DB regex test): a ledger row carries contact
11
+ ids, an opaque fact ref, and an opaque conversation key — cross-contact
12
+ fact TEXT never enters this table. A leak of the ledger reveals that
13
+ topology was rendered, never what it said.
14
+
15
+ Budget semantics are fail-closed: malformed env values read as budget 0
16
+ (nothing renders), and any storage error answers "no budget".
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ import re
24
+ import sqlite3
25
+ import threading
26
+ import uuid
27
+ from datetime import datetime, timedelta, timezone
28
+ from typing import Any, Dict, List, Optional
29
+
30
+ from apsimo.tom.tom2 import _validate_ref
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ #: Opaque conversation key: compact, no whitespace — prose is refused so
35
+ #: message text cannot be smuggled through the key field.
36
+ _CONV_KEY_RE = re.compile(r"^[A-Za-z0-9_.:+@-]{1,128}$")
37
+
38
+ #: Shipped budget defaults — deliberately tight; a deployment widens them
39
+ #: explicitly. Malformed values fail closed to 0.
40
+ DEFAULT_PAIR_DAY = 1
41
+ DEFAULT_READER_DAY = 3
42
+ DEFAULT_GLOBAL_DAY = 10
43
+
44
+
45
+ def _budget(name: str, default: int) -> int:
46
+ raw = os.environ.get(name)
47
+ if raw is None or not str(raw).strip():
48
+ return default
49
+ try:
50
+ return max(0, int(str(raw).strip()))
51
+ except (TypeError, ValueError):
52
+ logger.warning("%s=%r is malformed — failing closed to budget 0",
53
+ name, raw)
54
+ return 0
55
+
56
+
57
+ def budget_pair_day() -> int:
58
+ """COLONY_TOM2_BUDGET_PAIR_DAY (default 1): renderings per (reader,
59
+ subject) pair per rolling 24h."""
60
+ return _budget("COLONY_TOM2_BUDGET_PAIR_DAY", DEFAULT_PAIR_DAY)
61
+
62
+
63
+ def budget_reader_day() -> int:
64
+ """COLONY_TOM2_BUDGET_READER_DAY (default 3): renderings per reader
65
+ per rolling 24h."""
66
+ return _budget("COLONY_TOM2_BUDGET_READER_DAY", DEFAULT_READER_DAY)
67
+
68
+
69
+ def budget_global_day() -> int:
70
+ """COLONY_TOM2_BUDGET_GLOBAL_DAY (default 10): renderings across all
71
+ readers per rolling 24h."""
72
+ return _budget("COLONY_TOM2_BUDGET_GLOBAL_DAY", DEFAULT_GLOBAL_DAY)
73
+
74
+
75
+ def _now() -> datetime:
76
+ return datetime.now(tz=timezone.utc)
77
+
78
+
79
+ def _validate_conv_key(key: Any) -> str:
80
+ key = str(key or "").strip()
81
+ if not _CONV_KEY_RE.match(key):
82
+ raise ValueError(
83
+ f"exposure conversation key {key[:40]!r} is not an opaque id "
84
+ "(refs-not-content invariant)")
85
+ return key
86
+
87
+
88
+ def _validate_cid(cid: Any, what: str) -> str:
89
+ cid = str(cid or "").strip()
90
+ if not _CONV_KEY_RE.match(cid):
91
+ raise ValueError(
92
+ f"exposure {what} {cid[:40]!r} is not an opaque contact id "
93
+ "(refs-not-content invariant)")
94
+ return cid
95
+
96
+
97
+ class Tom2ExposureStore:
98
+ """SQLite-backed ledger of level-2 renderings (refs-not-content)."""
99
+
100
+ def __init__(self, db_path: Optional[str] = None) -> None:
101
+ self._lock = threading.RLock()
102
+ self._conn = sqlite3.connect(db_path or ":memory:",
103
+ check_same_thread=False)
104
+ self._conn.row_factory = sqlite3.Row
105
+ with self._lock:
106
+ self._conn.execute("""
107
+ CREATE TABLE IF NOT EXISTS tom2_exposures (
108
+ id TEXT PRIMARY KEY,
109
+ reader_contact_id TEXT NOT NULL,
110
+ subject_contact_id TEXT NOT NULL,
111
+ fact_ref TEXT NOT NULL,
112
+ conversation_key TEXT NOT NULL,
113
+ created_at TEXT NOT NULL
114
+ )""")
115
+ self._conn.execute(
116
+ "CREATE INDEX IF NOT EXISTS idx_tom2_exp_reader "
117
+ "ON tom2_exposures(reader_contact_id, created_at)")
118
+ self._conn.commit()
119
+
120
+ # -- writes -------------------------------------------------------------
121
+ def record_exposure(self, *, reader_contact_id: str,
122
+ subject_contact_id: str, fact_ref: str,
123
+ conversation_key: str) -> Dict[str, Any]:
124
+ """Append one rendering to the ledger. Raises on any privacy-pin
125
+ violation (prose in any field) — never best-effort."""
126
+ row = {
127
+ "id": str(uuid.uuid4()),
128
+ "reader_contact_id": _validate_cid(reader_contact_id, "reader"),
129
+ "subject_contact_id": _validate_cid(subject_contact_id,
130
+ "subject"),
131
+ "fact_ref": _validate_ref(fact_ref),
132
+ "conversation_key": _validate_conv_key(conversation_key),
133
+ "created_at": _now().isoformat(),
134
+ }
135
+ with self._lock:
136
+ self._conn.execute(
137
+ "INSERT INTO tom2_exposures (id, reader_contact_id, "
138
+ "subject_contact_id, fact_ref, conversation_key, created_at)"
139
+ " VALUES (?,?,?,?,?,?)", list(row.values()))
140
+ self._conn.commit()
141
+ return row
142
+
143
+ # -- budgets --------------------------------------------------------------
144
+ def _count(self, where: str, params: List[Any], hours: float) -> int:
145
+ cutoff = (_now() - timedelta(hours=hours)).isoformat()
146
+ with self._lock:
147
+ r = self._conn.execute(
148
+ f"SELECT COUNT(*) n FROM tom2_exposures WHERE {where} "
149
+ "AND created_at >= ?", params + [cutoff]).fetchone()
150
+ return int(r["n"])
151
+
152
+ def budget_ok(self, reader_contact_id: str, subject_contact_id: str,
153
+ fact_ref: str = "") -> bool:
154
+ """True when rendering one more line for (reader, subject) stays
155
+ inside ALL budgets (pair, reader, global; rolling 24h). Any error
156
+ answers False — a broken ledger cannot vouch for capacity."""
157
+ try:
158
+ reader = str(reader_contact_id or "").strip()
159
+ subject = str(subject_contact_id or "").strip()
160
+ if not reader or not subject:
161
+ return False
162
+ if self._count(
163
+ "reader_contact_id=? AND subject_contact_id=?",
164
+ [reader, subject], 24.0) >= budget_pair_day():
165
+ return False
166
+ if self._count("reader_contact_id=?", [reader],
167
+ 24.0) >= budget_reader_day():
168
+ return False
169
+ if self._count("1=1", [], 24.0) >= budget_global_day():
170
+ return False
171
+ return True
172
+ except Exception:
173
+ logger.debug("exposure budget check failed (=> no budget)",
174
+ exc_info=True)
175
+ return False
176
+
177
+ # -- owner reads ----------------------------------------------------------
178
+ def recent(self, *, reader_contact_id: Optional[str] = None,
179
+ subject_contact_id: Optional[str] = None,
180
+ limit: int = 50) -> List[Dict[str, Any]]:
181
+ clauses, params = ["1=1"], []
182
+ if reader_contact_id:
183
+ clauses.append("reader_contact_id=?")
184
+ params.append(reader_contact_id)
185
+ if subject_contact_id:
186
+ clauses.append("subject_contact_id=?")
187
+ params.append(subject_contact_id)
188
+ params.append(max(1, min(500, int(limit))))
189
+ with self._lock:
190
+ rows = self._conn.execute(
191
+ f"SELECT * FROM tom2_exposures WHERE {' AND '.join(clauses)}"
192
+ " ORDER BY created_at DESC LIMIT ?", params).fetchall()
193
+ return [dict(r) for r in rows]
194
+
195
+ def counts(self) -> Dict[str, Any]:
196
+ """Aggregate observability (numbers only)."""
197
+ with self._lock:
198
+ total = self._conn.execute(
199
+ "SELECT COUNT(*) n FROM tom2_exposures").fetchone()["n"]
200
+ readers = self._conn.execute(
201
+ "SELECT COUNT(DISTINCT reader_contact_id) n "
202
+ "FROM tom2_exposures").fetchone()["n"]
203
+ pairs = self._conn.execute(
204
+ "SELECT COUNT(*) n FROM (SELECT DISTINCT reader_contact_id,"
205
+ " subject_contact_id FROM tom2_exposures)").fetchone()["n"]
206
+ last24 = self._count("1=1", [], 24.0)
207
+ return {"total": int(total), "readers": int(readers),
208
+ "pairs": int(pairs), "last_24h": last24}
209
+
210
+ def close(self) -> None:
211
+ try:
212
+ self._conn.close()
213
+ except Exception:
214
+ pass
@@ -0,0 +1,306 @@
1
+ """LLM-backed extraction for Theory of Mind.
2
+
3
+ Extracts affect signals and shared facts from conversation turns.
4
+ Uses the LLM router (same as world model entity extraction) — Colony
5
+ does NOT build its own LLM client.
6
+
7
+ Throttled per contact and operation: max 1 extraction per
8
+ COLONY_TOM_EXTRACTION_THROTTLE_MINUTES (default 5 minutes).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import os
16
+ import re
17
+ from datetime import datetime, timezone
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from apsimo.beliefs.promotion import PROMOTION_PROMPT, promotion_metadata
21
+ from apsimo.util.model_output import final_text
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ MAX_INPUT_CHARS = 4_000
26
+ MAX_RESPONSE_TOKENS = int(os.environ.get("COLONY_TOM_MAX_TOKENS", "2048")) # reasoning models (mimo) need room; 512 returns empty content
27
+ THROTTLE_MINUTES = int(os.environ.get("COLONY_TOM_EXTRACTION_THROTTLE_MINUTES", "5"))
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Affect extraction prompt
31
+ # ---------------------------------------------------------------------------
32
+
33
+ _AFFECT_SYSTEM_PROMPT = (
34
+ "You are an emotion analysis engine. Read the conversation and return a "
35
+ "JSON object with the speaker's emotional state. Keys: "
36
+ '"valence" (float -1.0 to 1.0, negative=unhappy, positive=happy), '
37
+ '"arousal" (float 0.0 to 1.0, 0=calm, 1=intense), '
38
+ '"trigger" (string, brief excerpt causing the reading), '
39
+ '"confidence" (float 0.0 to 1.0). '
40
+ "Return ONLY the JSON object. If the conversation is neutral, "
41
+ 'return {"valence": 0.0, "arousal": 0.3, "trigger": null, "confidence": 0.5}. '
42
+ "Do not add prose, code fences, or explanation."
43
+ )
44
+
45
+ # ---------------------------------------------------------------------------
46
+ # Fact extraction prompt
47
+ # ---------------------------------------------------------------------------
48
+
49
+ _FACT_SYSTEM_PROMPT = (
50
+ "You are a knowledge extraction engine. Read the conversation and identify "
51
+ "what the contact now KNOWS or BELIEVES as a result of this conversation. "
52
+ "Return a JSON array of objects. Each object has keys: "
53
+ '"evidence" (an exact contiguous quotation of at most 500 characters from the conversation), '
54
+ '"source" (one of: told_by_contact, told_to_contact, shared_context, inferred), '
55
+ '"confidence" (float 0.0 to 1.0). '
56
+ "Keep the speaker's actual words and attribution. This is an estimate of what "
57
+ "was communicated, not proof it is true or that the contact accepted it. "
58
+ "Do not infer that the contact knows something merely because it was in the "
59
+ "assistant's response. Return at most 6 items. "
60
+ "Return ONLY the JSON array. Return [] if no useful new knowledge. "
61
+ "Do not add prose, code fences, or explanation."
62
+ ) + '\n' + PROMOTION_PROMPT
63
+
64
+
65
+
66
+
67
+ class TomExtractor:
68
+ """LLM-backed ToM extraction from conversation turns."""
69
+
70
+ def __init__(self, llm_router: Any) -> None:
71
+ self._router = llm_router
72
+ self._last_extraction: Dict[str, str] = {} # contact_id → ISO timestamp
73
+ self._last_facts: Dict[str, str] = {} # affect must not suppress fact extraction
74
+
75
+ async def extract_affect(
76
+ self,
77
+ conversation_text: str,
78
+ contact_id: str,
79
+ *,
80
+ session_id: Optional[str] = None,
81
+ ) -> Optional[Dict[str, Any]]:
82
+ """Extract affect from a conversation snippet.
83
+
84
+ Returns dict with valence, arousal, trigger, confidence or None.
85
+ Throttled per contact.
86
+ """
87
+ if not self._can_extract(contact_id):
88
+ return None
89
+
90
+ snippet = (conversation_text or "").strip()
91
+ if not snippet:
92
+ return None
93
+ if len(snippet) > MAX_INPUT_CHARS:
94
+ snippet = snippet[:MAX_INPUT_CHARS]
95
+
96
+ user_prompt = f"Conversation:\n---\n{snippet}\n---\n\nAnalyze the speaker's emotional state."
97
+
98
+ try:
99
+ resp = await self._router.complete(
100
+ messages=[
101
+ {"role": "system", "content": _AFFECT_SYSTEM_PROMPT},
102
+ {"role": "user", "content": user_prompt},
103
+ ],
104
+ context={"task": "tom_affect_extraction", "max_tokens": MAX_RESPONSE_TOKENS},
105
+ )
106
+ except Exception as exc:
107
+ logger.warning("ToM affect extraction LLM call failed: %s", exc)
108
+ return None
109
+
110
+ try:
111
+ content = final_text(resp)
112
+ except ValueError:
113
+ return None
114
+ result = _parse_affect_json(content)
115
+ if result is not None:
116
+ self._mark_extracted(contact_id)
117
+ result["source"] = "inferred"
118
+ result["contact_id"] = contact_id
119
+ if session_id:
120
+ result["session_id"] = session_id
121
+ return result
122
+
123
+ async def extract_facts(
124
+ self,
125
+ conversation_text: str,
126
+ contact_id: str,
127
+ *,
128
+ session_id: Optional[str] = None,
129
+ ) -> List[Dict[str, Any]]:
130
+ """Extract shared facts from a conversation snippet.
131
+
132
+ Returns list of dicts with fact, source, confidence.
133
+ Throttled per contact.
134
+ """
135
+ if not self._can_extract(contact_id, operation="facts"):
136
+ return []
137
+
138
+ snippet = (conversation_text or "").strip()
139
+ if not snippet:
140
+ return []
141
+ if len(snippet) > MAX_INPUT_CHARS:
142
+ snippet = snippet[:MAX_INPUT_CHARS]
143
+
144
+ user_prompt = f"Conversation:\n---\n{snippet}\n---\n\nWhat does the contact now know?"
145
+
146
+ try:
147
+ resp = await self._router.complete(
148
+ messages=[
149
+ {"role": "system", "content": _FACT_SYSTEM_PROMPT},
150
+ {"role": "user", "content": user_prompt},
151
+ ],
152
+ context={"task": "tom_fact_extraction", "max_tokens": MAX_RESPONSE_TOKENS},
153
+ )
154
+ except Exception as exc:
155
+ logger.warning("ToM fact extraction LLM call failed: %s", exc)
156
+ return []
157
+
158
+ try:
159
+ content = final_text(resp)
160
+ except ValueError:
161
+ return []
162
+ results = _parse_fact_array(content, conversation_text=snippet)
163
+ self._mark_extracted(contact_id, operation="facts")
164
+ for r in results:
165
+ r["contact_id"] = contact_id
166
+ r['model_provenance'] = {
167
+ 'function_role': getattr(resp, 'function_role', '') or 'extraction',
168
+ 'model_id': getattr(resp, 'model_id', 'unknown'),
169
+ 'config_revision': getattr(resp, 'config_revision', '') or 'unknown',
170
+ 'weight_revision': getattr(resp, 'model_revision', '') or 'unknown'}
171
+ return results
172
+
173
+ async def extract_engagement(
174
+ self,
175
+ conversation_text: str,
176
+ contact_id: str,
177
+ *,
178
+ session_id: Optional[str] = None,
179
+ ) -> Optional[Dict[str, Any]]:
180
+ """Retired numeric personality pass; canonical appraisal worker owns this.
181
+
182
+ Retain the method for older host callers during a paired upgrade. It
183
+ makes no model call and never creates count-derived trait certainty.
184
+ """
185
+ return None
186
+
187
+ def _can_extract(self, contact_id: str, *, operation: str = "affect") -> bool:
188
+ """Check the operation's per-contact throttle."""
189
+ timestamps = self._last_facts if operation == "facts" else self._last_extraction
190
+ last = timestamps.get(contact_id)
191
+ if last is None:
192
+ return True
193
+ try:
194
+ last_dt = datetime.fromisoformat(last)
195
+ elapsed = (datetime.now(timezone.utc) - last_dt).total_seconds() / 60
196
+ return elapsed >= THROTTLE_MINUTES
197
+ except (ValueError, TypeError):
198
+ return True
199
+
200
+ def _mark_extracted(self, contact_id: str, *, operation: str = "affect") -> None:
201
+ timestamps = self._last_facts if operation == "facts" else self._last_extraction
202
+ timestamps[contact_id] = datetime.now(timezone.utc).isoformat()
203
+
204
+
205
+ # ---------------------------------------------------------------------------
206
+ # Parsing helpers
207
+ # ---------------------------------------------------------------------------
208
+
209
+ _CODE_FENCE = re.compile(r"^```(?:json)?\s*|\s*```$", re.MULTILINE)
210
+
211
+
212
+ def _strip_code_fence(text: str) -> str:
213
+ return _CODE_FENCE.sub("", text)
214
+
215
+
216
+ def _parse_affect_json(raw: str) -> Optional[Dict[str, Any]]:
217
+ """Parse LLM response into affect dict."""
218
+ if not raw:
219
+ return None
220
+ candidate = _strip_code_fence(raw).strip()
221
+ try:
222
+ parsed = json.loads(candidate)
223
+ except json.JSONDecodeError:
224
+ # Try to find JSON object in response
225
+ match = re.search(r"\{.*\}", candidate, re.DOTALL)
226
+ if not match:
227
+ return None
228
+ try:
229
+ parsed = json.loads(match.group(0))
230
+ except json.JSONDecodeError:
231
+ return None
232
+
233
+ if not isinstance(parsed, dict):
234
+ return None
235
+
236
+ valence = parsed.get("valence", 0.0)
237
+ arousal = parsed.get("arousal", 0.3)
238
+ confidence = parsed.get("confidence", 0.5)
239
+
240
+ try:
241
+ valence = max(-1.0, min(1.0, float(valence)))
242
+ arousal = max(0.0, min(1.0, float(arousal)))
243
+ confidence = max(0.0, min(1.0, float(confidence)))
244
+ except (TypeError, ValueError):
245
+ return None
246
+
247
+ # Skip neutral readings — not worth storing
248
+ if abs(valence) < 0.1 and abs(arousal - 0.3) < 0.1:
249
+ return None
250
+
251
+ return {
252
+ "valence": valence,
253
+ "arousal": arousal,
254
+ "trigger": parsed.get("trigger"),
255
+ "confidence": confidence,
256
+ }
257
+
258
+
259
+ def _parse_fact_array(raw: str, *, conversation_text: str = "") -> List[Dict[str, Any]]:
260
+ """Accept useful quotations; unsupported output remains conversation history."""
261
+ if not raw:
262
+ return []
263
+ candidate = _strip_code_fence(raw).strip()
264
+ try:
265
+ parsed = json.loads(candidate)
266
+ except json.JSONDecodeError:
267
+ match = re.search(r"\[.*\]", candidate, re.DOTALL)
268
+ if not match:
269
+ return []
270
+ try:
271
+ parsed = json.loads(match.group(0))
272
+ except json.JSONDecodeError:
273
+ return []
274
+
275
+ if isinstance(parsed, dict):
276
+ parsed = parsed.get("facts") or parsed.get("items") or []
277
+ if not isinstance(parsed, list) or len(parsed) > 6:
278
+ return []
279
+
280
+ facts: List[Dict[str, Any]] = []
281
+ for item in parsed:
282
+ if not isinstance(item, dict):
283
+ continue
284
+ fact_text = item.get("evidence")
285
+ quality = promotion_metadata(item)
286
+ if (not isinstance(fact_text, str) or not fact_text.strip()
287
+ or len(fact_text) > 500 or fact_text not in conversation_text or quality is None):
288
+ continue
289
+ source = item.get("source", "inferred")
290
+ if source not in ("told_by_contact", "told_to_contact", "shared_context", "inferred"):
291
+ source = "inferred"
292
+ confidence = item.get("confidence", 0.7)
293
+ try:
294
+ confidence = max(0.0, min(1.0, float(confidence)))
295
+ except (TypeError, ValueError):
296
+ confidence = 0.7
297
+ facts.append({
298
+ "fact": fact_text.strip(),
299
+ "source": source,
300
+ "confidence": confidence,
301
+ "memory_quality": quality,
302
+ })
303
+ unique = {}
304
+ for fact in facts:
305
+ unique.setdefault((fact['source'], fact['fact']), fact)
306
+ return list(unique.values())
@@ -0,0 +1,144 @@
1
+ """Pure P8 fact adapters with an explicit server-authority boundary.
2
+
3
+ Models and request bodies may propose only fact content and confidence. Fact
4
+ identity, source, subject, audience, shareability, evidence, and freshness are
5
+ accepted solely through the typed ``ServerFactAuthorityV1`` input. Unknown
6
+ untrusted fields are rejected rather than silently ignored so a future caller
7
+ cannot accidentally make a body/model field authoritative during refactoring.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from datetime import datetime, timezone
14
+ import math
15
+ from typing import Any, Mapping
16
+
17
+ from apsimo.tom.visibility import (
18
+ FactCandidateV1,
19
+ FactVisibilityV1,
20
+ content_digest,
21
+ )
22
+
23
+
24
+ class FactAuthorityBoundaryError(ValueError):
25
+ """Untrusted input attempted to cross the fact-authority boundary."""
26
+
27
+
28
+ def _iso(value: datetime | str, *, field: str) -> str:
29
+ if isinstance(value, datetime):
30
+ parsed = value
31
+ else:
32
+ try:
33
+ parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
34
+ except (TypeError, ValueError) as exc:
35
+ raise ValueError(f"{field} must be an ISO-8601 timestamp") from exc
36
+ if parsed.tzinfo is None:
37
+ raise ValueError(f"{field} must include a timezone")
38
+ return parsed.astimezone(timezone.utc).isoformat()
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class FactPayloadV1:
43
+ """The complete set of fields an untrusted fact producer may propose."""
44
+
45
+ content: str
46
+ confidence: float
47
+
48
+ def __post_init__(self) -> None:
49
+ if not isinstance(self.content, str) or not self.content.strip():
50
+ raise ValueError("fact content is required")
51
+ if isinstance(self.confidence, bool):
52
+ raise ValueError("fact confidence must be finite and between zero and one")
53
+ try:
54
+ confidence = float(self.confidence)
55
+ except (TypeError, ValueError) as exc:
56
+ raise ValueError(
57
+ "fact confidence must be finite and between zero and one") from exc
58
+ if not math.isfinite(confidence) or not 0.0 <= confidence <= 1.0:
59
+ raise ValueError(
60
+ "fact confidence must be finite and between zero and one")
61
+ object.__setattr__(self, "confidence", confidence)
62
+
63
+ @classmethod
64
+ def from_untrusted(
65
+ cls,
66
+ fields: Mapping[str, Any],
67
+ *,
68
+ origin: str,
69
+ ) -> "FactPayloadV1":
70
+ """Whitelist content/confidence and reject every other input field."""
71
+
72
+ label = str(origin or "untrusted").strip().lower() or "untrusted"
73
+ if not isinstance(fields, Mapping):
74
+ raise FactAuthorityBoundaryError(
75
+ f"{label} fact payload must be a structured mapping")
76
+ allowed = {"content", "confidence"}
77
+ rejected = sorted(str(key) for key in fields if key not in allowed)
78
+ if rejected:
79
+ raise FactAuthorityBoundaryError(
80
+ f"{label} field {rejected[0]} cannot supply fact authority")
81
+ missing = sorted(allowed.difference(fields))
82
+ if missing:
83
+ raise ValueError(f"{label} fact payload is missing {missing[0]}")
84
+ return cls(
85
+ content=fields["content"],
86
+ confidence=fields["confidence"],
87
+ )
88
+
89
+
90
+ @dataclass(frozen=True, slots=True)
91
+ class ServerFactAuthorityV1:
92
+ """Structured authority derived from authenticated server-side state."""
93
+
94
+ fact_ref: str
95
+ source_ref: str
96
+ subject_person_id: str
97
+ viewer_scope: str
98
+ shareability: str
99
+ observed_at: datetime | str
100
+ fresh_until: datetime | str
101
+ evidence_refs: tuple[str, ...]
102
+
103
+ def __post_init__(self) -> None:
104
+ object.__setattr__(
105
+ self, "observed_at", _iso(self.observed_at, field="observed_at"))
106
+ object.__setattr__(
107
+ self, "fresh_until", _iso(self.fresh_until, field="fresh_until"))
108
+ object.__setattr__(self, "evidence_refs", tuple(self.evidence_refs))
109
+
110
+
111
+ def build_fact_candidate(
112
+ *,
113
+ authority: ServerFactAuthorityV1,
114
+ payload: FactPayloadV1,
115
+ ) -> FactCandidateV1:
116
+ """Combine typed server authority with a non-authoritative fact payload."""
117
+
118
+ if not isinstance(authority, ServerFactAuthorityV1):
119
+ raise FactAuthorityBoundaryError(
120
+ "fact visibility authority must be a server-derived structured input")
121
+ if not isinstance(payload, FactPayloadV1):
122
+ raise FactAuthorityBoundaryError(
123
+ "fact content must be a bounded FactPayloadV1 input")
124
+ visibility = FactVisibilityV1(
125
+ fact_ref=authority.fact_ref,
126
+ content_digest=content_digest(payload.content),
127
+ source_ref=authority.source_ref,
128
+ subject_person_id=authority.subject_person_id,
129
+ viewer_scope=authority.viewer_scope,
130
+ shareability=authority.shareability,
131
+ confidence=payload.confidence,
132
+ observed_at=authority.observed_at,
133
+ fresh_until=authority.fresh_until,
134
+ evidence_refs=authority.evidence_refs,
135
+ )
136
+ return FactCandidateV1(content=payload.content, visibility=visibility)
137
+
138
+
139
+ __all__ = [
140
+ "FactAuthorityBoundaryError",
141
+ "FactPayloadV1",
142
+ "ServerFactAuthorityV1",
143
+ "build_fact_candidate",
144
+ ]