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,521 @@
1
+ """Preference Learner — capture and apply the owner's stated preferences.
2
+
3
+ This is the *explicit-directive* lane of preference learning: when the owner
4
+ says how they want their assistant to communicate ("be concise", "use bullet
5
+ points", "stop with the emoji"), it is captured deterministically at high
6
+ confidence and surfaced back every turn. It is deliberately distinct from the
7
+ EngagementStore, which passively *infers* a contact's OCEAN/style over many
8
+ observations — here we record what the owner directly told us.
9
+
10
+ Tracks:
11
+ - Communication style preferences (length / format / tone / emoji)
12
+ - Topic interests
13
+ - Scheduling preferences
14
+
15
+ Preferences persist to SQLite when a ``db_path`` is supplied, so an explicit
16
+ directive survives a sidecar restart instead of resetting to nothing.
17
+ """
18
+
19
+ import json
20
+ import re
21
+ import sqlite3
22
+ from dataclasses import dataclass, field
23
+ from datetime import datetime
24
+ from typing import Any, Dict, List, Optional, Set, Tuple
25
+
26
+ import logging
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Keyword maps for feedback parsing
32
+ # ---------------------------------------------------------------------------
33
+
34
+ _LENGTH_SHORT = frozenset({"short", "brief", "concise", "terse", "quick", "compact", "succinct"})
35
+ _LENGTH_LONG = frozenset({"long", "detailed", "comprehensive", "verbose", "thorough", "elaborate", "in-depth"})
36
+ _LENGTH_MEDIUM = frozenset({"medium", "moderate", "balanced", "average"})
37
+
38
+ _FORMAT_BULLETS = frozenset({"bullet", "bullets", "list", "points", "numbered", "itemize"})
39
+ _FORMAT_PROSE = frozenset({"paragraph", "prose", "narrative", "flowing", "text"})
40
+ _FORMAT_CODE = frozenset({"code", "snippet", "example", "examples", "implementation"})
41
+ _FORMAT_TABLE = frozenset({"table", "grid", "tabular", "spreadsheet"})
42
+
43
+ _STYLE_FORMAL = frozenset({"formal", "professional", "business", "official", "academic"})
44
+ _STYLE_CASUAL = frozenset({"casual", "informal", "friendly", "conversational", "relaxed"})
45
+ _STYLE_TECHNICAL = frozenset({"technical", "expert", "advanced", "detailed", "deep"})
46
+
47
+ _TIMING_MORNING = frozenset({"morning", "early", "am", "dawn", "breakfast"})
48
+ _TIMING_EVENING = frozenset({"evening", "pm", "afternoon", "night", "after work"})
49
+
50
+ _EMOJI_WORDS = frozenset({"emoji", "emojis", "emoticon", "emoticons"})
51
+ _NEGATION = frozenset({"no", "not", "dont", "don", "stop", "without", "never", "drop", "avoid", "less", "skip"})
52
+
53
+ # Tokens that signal the owner is *directing* how to communicate (vs. merely
54
+ # using a style word in passing). A directive needs both a style keyword AND
55
+ # one of these cues, which keeps false positives off ordinary messages.
56
+ _DIRECTIVE_CUES = frozenset({
57
+ "be", "keep", "make", "use", "using", "give", "show", "stop", "do", "dont", "don",
58
+ "prefer", "want", "always", "never", "instead", "please", "less", "more", "shorter",
59
+ "longer", "reply", "replies", "respond", "response", "responses", "answer", "answers",
60
+ "you", "your", "from", "now", "on", "going", "forward",
61
+ })
62
+ _DIRECTIVE_MAX_TOKENS = 30 # directives are short; ignore long messages that merely contain a style word
63
+
64
+ # All style keywords that can anchor a directive.
65
+ _STYLE_KEYWORDS = (
66
+ _LENGTH_SHORT | _LENGTH_LONG | _LENGTH_MEDIUM
67
+ | _FORMAT_BULLETS | _FORMAT_PROSE | _FORMAT_CODE | _FORMAT_TABLE
68
+ | _STYLE_FORMAL | _STYLE_CASUAL | _STYLE_TECHNICAL
69
+ | _EMOJI_WORDS
70
+ )
71
+
72
+ # Action keyword → (category, key, value)
73
+ _ACTION_RULES: List[Tuple[Set[str], str, str, Any]] = [
74
+ ({"short", "brief", "concise"}, "communication_style", "length", "short"),
75
+ ({"long", "detailed", "verbose"}, "communication_style", "length", "long"),
76
+ ({"dark"}, "display", "theme", "dark"),
77
+ ({"light"}, "display", "theme", "light"),
78
+ ({"bullet", "list"}, "communication_style", "format", "bullet_points"),
79
+ ({"prose", "paragraph"}, "communication_style", "format", "prose"),
80
+ ({"code", "snippet"}, "communication_style", "format", "code_examples"),
81
+ ]
82
+
83
+
84
+ @dataclass
85
+ class Preference:
86
+ """A learned user preference.
87
+
88
+ Attributes:
89
+ category: Preference domain ("communication_style", "response_length", etc.)
90
+ key: Specific preference key within the category
91
+ value: The preference value
92
+ confidence: How confident we are (0-1)
93
+ learned_from: How the preference was learned ("explicit", "implicit", "inferred")
94
+ last_updated: Most recent update timestamp
95
+ """
96
+
97
+ category: str
98
+ key: str
99
+ value: Any
100
+ confidence: float
101
+ learned_from: str
102
+ last_updated: datetime = field(default_factory=datetime.now)
103
+
104
+
105
+ class PreferenceLearner:
106
+ """Learn and apply user preferences.
107
+
108
+ Tracks preferences from two sources:
109
+ - Explicit feedback (high confidence, 0.9)
110
+ - Observed behavior (lower confidence, 0.5, accumulates with repetition)
111
+
112
+ Feedback parsing uses keyword matching to extract structured preferences
113
+ from natural-language feedback strings.
114
+
115
+ Args:
116
+ graph_client: Optional Colony graph client (reserved for graph mirroring).
117
+ db_path: Optional SQLite path. When given, preferences are loaded on
118
+ construction and persisted on every update, so explicit owner
119
+ directives survive a restart.
120
+ """
121
+
122
+ def __init__(self, graph_client: Any = None, db_path: Optional[str] = None, *, perspective: Any = None) -> None:
123
+ self.graph = graph_client
124
+ self.perspective = perspective
125
+ self._preferences: Dict[str, Preference] = {}
126
+ self._conn: Optional[sqlite3.Connection] = None
127
+ if db_path:
128
+ try:
129
+ self._conn = sqlite3.connect(str(db_path), check_same_thread=False)
130
+ self._conn.row_factory = sqlite3.Row
131
+ self._conn.execute("PRAGMA journal_mode=WAL")
132
+ self._conn.execute(
133
+ """CREATE TABLE IF NOT EXISTS owner_preferences (
134
+ pref_key TEXT PRIMARY KEY,
135
+ category TEXT NOT NULL,
136
+ key TEXT NOT NULL,
137
+ value TEXT NOT NULL,
138
+ confidence REAL NOT NULL,
139
+ learned_from TEXT NOT NULL,
140
+ last_updated TEXT NOT NULL
141
+ )"""
142
+ )
143
+ self._conn.commit()
144
+ self._load()
145
+ except Exception as exc: # pragma: no cover - defensive
146
+ logger.warning("PreferenceLearner persistence disabled (%s)", exc)
147
+ self._conn = None
148
+
149
+ # ------------------------------------------------------------------
150
+ # Persistence
151
+ # ------------------------------------------------------------------
152
+
153
+ def _load(self) -> None:
154
+ if self._conn is None:
155
+ return
156
+ for row in self._conn.execute("SELECT * FROM owner_preferences"):
157
+ try:
158
+ value = json.loads(row["value"])
159
+ except (TypeError, ValueError):
160
+ value = row["value"]
161
+ self._preferences[row["pref_key"]] = Preference(
162
+ category=row["category"],
163
+ key=row["key"],
164
+ value=value,
165
+ confidence=row["confidence"],
166
+ learned_from=row["learned_from"],
167
+ last_updated=datetime.fromisoformat(row["last_updated"]),
168
+ )
169
+
170
+ def _persist(self, pref_key: str, pref: "Preference") -> None:
171
+ if self._conn is None:
172
+ return
173
+ try:
174
+ self._conn.execute(
175
+ """INSERT INTO owner_preferences
176
+ (pref_key, category, key, value, confidence, learned_from, last_updated)
177
+ VALUES (?,?,?,?,?,?,?)
178
+ ON CONFLICT(pref_key) DO UPDATE SET
179
+ category=excluded.category, key=excluded.key, value=excluded.value,
180
+ confidence=excluded.confidence, learned_from=excluded.learned_from,
181
+ last_updated=excluded.last_updated""",
182
+ (
183
+ pref_key, pref.category, pref.key, json.dumps(pref.value),
184
+ float(pref.confidence), pref.learned_from,
185
+ pref.last_updated.isoformat(),
186
+ ),
187
+ )
188
+ self._conn.commit()
189
+ except Exception as exc: # pragma: no cover - defensive
190
+ logger.debug("PreferenceLearner persist failed: %s", exc)
191
+
192
+ async def learn_from_feedback(
193
+ self,
194
+ category: str,
195
+ feedback: str,
196
+ context: Optional[Dict[str, Any]] = None,
197
+ ) -> None:
198
+ """Learn preference from explicit user feedback.
199
+
200
+ Explicit feedback gets high confidence (0.9) since the user
201
+ directly stated a preference. The raw feedback is parsed into
202
+ a (key, value) pair using keyword matching.
203
+
204
+ Args:
205
+ category: Preference domain
206
+ feedback: The user's stated preference
207
+ context: Optional additional context
208
+ """
209
+ key, value = self._parse_feedback(category, feedback)
210
+
211
+ pref_key = f"{category}.{key}"
212
+ self._preferences[pref_key] = Preference(
213
+ category=category,
214
+ key=key,
215
+ value=value,
216
+ confidence=0.9,
217
+ learned_from="explicit",
218
+ )
219
+ self._persist(pref_key, self._preferences[pref_key])
220
+
221
+ logger.debug("Learned explicit preference: %s = %s", pref_key, value)
222
+
223
+ async def learn_from_behavior(
224
+ self,
225
+ action: str,
226
+ context: Optional[Dict[str, Any]] = None,
227
+ ) -> None:
228
+ """Learn preference from observed user behavior.
229
+
230
+ Implicit preferences start at 0.5 confidence and accumulate
231
+ with repeated observation (capped at 1.0).
232
+
233
+ Args:
234
+ action: The observed user action (e.g., "clicked_short_response")
235
+ context: Optional additional context
236
+ """
237
+ category, key, value = self._infer_from_action(action, context)
238
+
239
+ if not category:
240
+ return
241
+
242
+ pref_key = f"{category}.{key}"
243
+ existing = self._preferences.get(pref_key)
244
+
245
+ if existing and existing.learned_from == "explicit":
246
+ return # Observations cannot overwrite a correction from the owner.
247
+ if self.perspective is not None and pref_key in self.perspective.tracked_keys():
248
+ return
249
+ if existing:
250
+ existing.value = value
251
+ existing.confidence = min(1.0, existing.confidence + 0.1)
252
+ existing.learned_from = "implicit"
253
+ existing.last_updated = datetime.now()
254
+ else:
255
+ self._preferences[pref_key] = Preference(
256
+ category=category,
257
+ key=key,
258
+ value=value,
259
+ confidence=0.5,
260
+ learned_from="implicit",
261
+ )
262
+ self._persist(pref_key, self._preferences[pref_key])
263
+
264
+ logger.debug("Learned implicit preference: %s.%s = %s", category, key, value)
265
+
266
+ async def get_preference(
267
+ self,
268
+ category: str,
269
+ key: str,
270
+ default: Any = None,
271
+ ) -> Any:
272
+ """Get a learned preference value.
273
+
274
+ Args:
275
+ category: Preference domain
276
+ key: Specific preference key
277
+ default: Value to return if preference not found
278
+
279
+ Returns:
280
+ The preference value, or default if not found
281
+ """
282
+ pref_key = f"{category}.{key}"
283
+ if self.perspective is not None:
284
+ for source in self.perspective.preferences():
285
+ if source['pref_key'] == pref_key:
286
+ return source['value']
287
+ if pref_key in self.perspective.tracked_keys():
288
+ return default
289
+ pref = self._preferences.get(pref_key)
290
+ return pref.value if pref else default
291
+
292
+ async def get_all_preferences(self, category: Optional[str] = None) -> List[Preference]:
293
+ """Get all preferences, optionally filtered by category.
294
+
295
+ Args:
296
+ category: If provided, only return preferences in this category
297
+
298
+ Returns:
299
+ List of matching preferences
300
+ """
301
+ tracked = self.perspective.tracked_keys() if self.perspective is not None else set()
302
+ prefs = [pref for key, pref in self._preferences.items() if key not in tracked]
303
+ if self.perspective is not None:
304
+ for source in self.perspective.preferences():
305
+ source_category, key = source['pref_key'].split('.', 1)
306
+ prefs.append(Preference(source_category, key, source['value'], 0.9,
307
+ 'explicit_source', datetime.fromisoformat(source['source_at'])))
308
+ if category:
309
+ prefs = [p for p in prefs if p.category == category]
310
+ return prefs
311
+
312
+ def detect_directive(self, message: str) -> Optional[Tuple[str, str, Any]]:
313
+ """Detect an explicit communication directive in a raw owner message.
314
+
315
+ Returns ``(category, key, value)`` only when the message both contains a
316
+ style keyword and reads like an instruction (a directive cue token, and
317
+ not an overly long message). Returns ``None`` otherwise, so ordinary
318
+ conversation never trips preference learning.
319
+
320
+ Examples that match::
321
+
322
+ "be more concise" -> ("communication_style", "length", "short")
323
+ "use bullet points please" -> ("communication_style", "format", "bullet_points")
324
+ "stop using emoji" -> ("communication_style", "emoji", "off")
325
+ "keep your replies formal" -> ("communication_style", "style", "formal")
326
+ """
327
+ if not message:
328
+ return None
329
+ words = set(re.findall(r"\b\w+\b", message.lower()))
330
+ if len(words) > _DIRECTIVE_MAX_TOKENS:
331
+ return None
332
+ if not (words & _STYLE_KEYWORDS) or not (words & _DIRECTIVE_CUES):
333
+ return None
334
+ key, value = self._parse_feedback("communication_style", message)
335
+ if key == "general":
336
+ return None
337
+ return ("communication_style", key, value)
338
+
339
+ def _parse_all(self, message: str) -> List[Tuple[str, Any]]:
340
+ """Extract every communication-style ``(key, value)`` present in *message*.
341
+
342
+ Unlike ``_parse_feedback`` (first match only), this captures multiple
343
+ directives in one sentence, e.g. "be concise and use bullet points".
344
+ Length wins ties over the technical tone bucket on shared words like
345
+ "detailed", so a word is never double-counted across dimensions.
346
+ """
347
+ words = set(re.findall(r"\b\w+\b", message.lower()))
348
+ out: List[Tuple[str, Any]] = []
349
+
350
+ if words & _EMOJI_WORDS:
351
+ out.append(("emoji", "off" if (words & _NEGATION) else "on"))
352
+
353
+ if words & _LENGTH_SHORT:
354
+ out.append(("length", "short"))
355
+ elif words & _LENGTH_LONG:
356
+ out.append(("length", "long"))
357
+ elif words & _LENGTH_MEDIUM:
358
+ out.append(("length", "medium"))
359
+
360
+ if words & _FORMAT_BULLETS:
361
+ out.append(("format", "bullet_points"))
362
+ elif words & _FORMAT_PROSE:
363
+ out.append(("format", "prose"))
364
+ elif words & _FORMAT_CODE:
365
+ out.append(("format", "code_examples"))
366
+ elif words & _FORMAT_TABLE:
367
+ out.append(("format", "table"))
368
+
369
+ style_words = words - _LENGTH_LONG # don't let 'detailed' also claim 'technical'
370
+ if style_words & _STYLE_FORMAL:
371
+ out.append(("style", "formal"))
372
+ elif style_words & _STYLE_CASUAL:
373
+ out.append(("style", "casual"))
374
+ elif style_words & _STYLE_TECHNICAL:
375
+ out.append(("style", "technical"))
376
+
377
+ return out
378
+
379
+ async def learn_directive(self, message: str) -> Optional[Tuple[str, str, Any]]:
380
+ """If *message* is an explicit communication directive, learn every style
381
+ preference it contains (explicit confidence) and return the primary
382
+ ``(category, key, value)``; else ``None``.
383
+
384
+ The gate (``detect_directive``) keeps ordinary conversation from being
385
+ learned; once gated in, all directives in the message are captured.
386
+ """
387
+ primary = self.detect_directive(message)
388
+ if primary is None:
389
+ return None
390
+ for key, value in self._parse_all(message):
391
+ pref_key = f"communication_style.{key}"
392
+ self._preferences[pref_key] = Preference(
393
+ category="communication_style", key=key, value=value,
394
+ confidence=0.9, learned_from="explicit",
395
+ )
396
+ self._persist(pref_key, self._preferences[pref_key])
397
+ logger.info("Learned owner directive(s) from: %r", message[:80])
398
+ return primary
399
+
400
+ def learn_source(self, turn_id: str) -> list:
401
+ """Project attributed owner corrections from retained canonical words."""
402
+ return self.perspective.observe_source(turn_id, self) if self.perspective is not None else []
403
+
404
+ # Human-readable rendering of each communication-style preference.
405
+ _BRIEF_LINES: Dict[Tuple[str, Any], str] = {
406
+ ("length", "short"): "Keep replies short and to the point.",
407
+ ("length", "long"): "Give thorough, detailed replies.",
408
+ ("length", "medium"): "Aim for medium-length replies.",
409
+ ("format", "bullet_points"): "Prefer bullet points over prose.",
410
+ ("format", "prose"): "Prefer flowing prose over bullet lists.",
411
+ ("format", "code_examples"): "Include code examples when relevant.",
412
+ ("format", "table"): "Use tables where they help.",
413
+ ("style", "formal"): "Keep a formal, professional tone.",
414
+ ("style", "casual"): "Keep a casual, relaxed tone.",
415
+ ("style", "technical"): "Be technical and precise.",
416
+ ("emoji", "off"): "Don't use emoji.",
417
+ ("emoji", "on"): "Emoji are fine.",
418
+ }
419
+
420
+ def build_brief(self, min_confidence: float = 0.5, *, source_ids=None) -> str:
421
+ """Render the owner's stated communication preferences as a short brief.
422
+
423
+ Only ``communication_style`` preferences at or above *min_confidence* are
424
+ included (explicit directives are 0.9). Returns '' when there is nothing
425
+ confident to assert.
426
+ """
427
+ lines: List[str] = []
428
+ tracked = self.perspective.tracked_keys() if self.perspective is not None else set()
429
+ for pref_key, pref in self._preferences.items():
430
+ if pref_key in tracked:
431
+ continue
432
+ if pref.category != "communication_style" or pref.confidence < min_confidence:
433
+ continue
434
+ line = self._BRIEF_LINES.get((pref.key, pref.value))
435
+ if line and line not in lines:
436
+ lines.append(line)
437
+ if self.perspective is not None:
438
+ for pref in self.perspective.preferences():
439
+ if not pref['pref_key'].startswith('communication_style.'):
440
+ continue
441
+ line = self._BRIEF_LINES.get((pref['pref_key'].split('.', 1)[1], pref['value']))
442
+ if line:
443
+ lines.append(f"{line} [Owner correction {pref['id']}, source turn:{pref['source_turn_id']}]")
444
+ if source_ids is not None:
445
+ source_ids.append(pref['source_turn_id'])
446
+ return "\n".join(f"- {ln}" for ln in lines)
447
+
448
+ def _parse_feedback(self, category: str, feedback: str) -> Tuple[str, Any]:
449
+ """Parse explicit feedback into a (key, value) preference pair.
450
+
451
+ Uses keyword matching to identify the most likely preference
452
+ being expressed. Falls back to storing the raw feedback under
453
+ the 'general' key.
454
+
455
+ Args:
456
+ category: The preference category (used for context)
457
+ feedback: Natural-language feedback string
458
+
459
+ Returns:
460
+ Tuple of (preference_key, preference_value)
461
+ """
462
+ words = set(re.findall(r"\b\w+\b", feedback.lower()))
463
+
464
+ if words & _EMOJI_WORDS:
465
+ return ("emoji", "off" if (words & _NEGATION) else "on")
466
+
467
+ if words & _LENGTH_SHORT:
468
+ return ("length", "short")
469
+ if words & _LENGTH_LONG:
470
+ return ("length", "long")
471
+ if words & _LENGTH_MEDIUM:
472
+ return ("length", "medium")
473
+
474
+ if words & _FORMAT_BULLETS:
475
+ return ("format", "bullet_points")
476
+ if words & _FORMAT_PROSE:
477
+ return ("format", "prose")
478
+ if words & _FORMAT_CODE:
479
+ return ("format", "code_examples")
480
+ if words & _FORMAT_TABLE:
481
+ return ("format", "table")
482
+
483
+ if words & _STYLE_FORMAL:
484
+ return ("style", "formal")
485
+ if words & _STYLE_CASUAL:
486
+ return ("style", "casual")
487
+ if words & _STYLE_TECHNICAL:
488
+ return ("style", "technical")
489
+
490
+ if words & _TIMING_MORNING:
491
+ return ("timing", "morning")
492
+ if words & _TIMING_EVENING:
493
+ return ("timing", "evening")
494
+
495
+ return ("general", feedback)
496
+
497
+ def _infer_from_action(
498
+ self,
499
+ action: str,
500
+ context: Optional[Dict[str, Any]],
501
+ ) -> Tuple[Optional[str], Optional[str], Any]:
502
+ """Infer preference from an observed action pattern.
503
+
504
+ Tokenises the action string on underscores/spaces and matches
505
+ against known keyword rules.
506
+
507
+ Args:
508
+ action: Action identifier (e.g., "clicked_short_response")
509
+ context: Optional additional context
510
+
511
+ Returns:
512
+ Tuple of (category, key, value) or (None, None, None) if not recognised
513
+ """
514
+ # Split on underscores and spaces (not \b which treats _ as word char)
515
+ parts = set(re.findall(r"[a-z]+", action.lower()))
516
+
517
+ for keywords, category, key, value in _ACTION_RULES:
518
+ if parts & keywords:
519
+ return (category, key, value)
520
+
521
+ return (None, None, None)