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,136 @@
1
+ """GraphBaselineStore — person behavioral baselines stored on Neo4j Person nodes.
2
+
3
+ Uses Welford's online algorithm for O(1) incremental mean/std updates.
4
+ No cache, no TTL — Neo4j is the single source of truth.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import logging
11
+ import math
12
+ from dataclasses import dataclass
13
+ from typing import List, Optional
14
+
15
+ from apsimo.intelligence.mind_model.signal_collector import (
16
+ BaselineStore,
17
+ PersonBaseline,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # Default preferred hours when insufficient data exists
23
+ _DEFAULT_PREFERRED_HOURS = [9, 10, 11, 14, 15, 16]
24
+
25
+
26
+ @dataclass
27
+ class PersonBaselineImpl:
28
+ """Concrete PersonBaseline implementation."""
29
+ length_mean: float
30
+ length_std: float
31
+ preferred_hours: List[int]
32
+
33
+
34
+ class GraphBaselineStore:
35
+ """BaselineStore backed by Person node properties in Neo4j.
36
+
37
+ Baselines live as properties on the :Person node — no new nodes,
38
+ no new infrastructure. Welford's algorithm gives O(1) incremental
39
+ updates for mean and standard deviation without storing raw history.
40
+
41
+ Falls back to neutral defaults when Neo4j is unavailable.
42
+ """
43
+
44
+ def __init__(self, graph) -> None:
45
+ self._graph = graph
46
+
47
+ async def get(self, person_id: str) -> PersonBaselineImpl:
48
+ """Read baseline from Person node. Returns empty baseline on miss."""
49
+ try:
50
+ row = await self._graph._run_get_baseline(person_id)
51
+ if row is None:
52
+ return PersonBaselineImpl(
53
+ length_mean=0.0,
54
+ length_std=0.0,
55
+ preferred_hours=list(_DEFAULT_PREFERRED_HOURS),
56
+ )
57
+ return PersonBaselineImpl(
58
+ length_mean=row.get("length_mean") or 0.0,
59
+ length_std=row.get("length_std") or 0.0,
60
+ preferred_hours=self._preferred_hours(
61
+ row.get("hour_histogram")
62
+ ),
63
+ )
64
+ except Exception as exc:
65
+ logger.debug("GraphBaselineStore.get failed for %s: %s", person_id, exc)
66
+ return PersonBaselineImpl(
67
+ length_mean=0.0,
68
+ length_std=0.0,
69
+ preferred_hours=list(_DEFAULT_PREFERRED_HOURS),
70
+ )
71
+
72
+ async def record_message(self, person_id: str, length: int, hour: int) -> None:
73
+ """Incrementally update baseline on the Person node via Welford."""
74
+ try:
75
+ current = await self._graph._run_get_baseline(person_id)
76
+ count = (current or {}).get("msg_count", 0) or 0
77
+ mean = (current or {}).get("length_mean", 0.0) or 0.0
78
+ m2 = (current or {}).get("length_m2", 0.0) or 0.0
79
+ hist_raw = (current or {}).get("hour_histogram")
80
+
81
+ # Parse histogram
82
+ if hist_raw:
83
+ try:
84
+ histogram = json.loads(hist_raw)
85
+ if len(histogram) != 24:
86
+ histogram = [0] * 24
87
+ except (json.JSONDecodeError, TypeError):
88
+ histogram = [0] * 24
89
+ else:
90
+ histogram = [0] * 24
91
+
92
+ # Welford update
93
+ count += 1
94
+ delta = length - mean
95
+ mean += delta / count
96
+ delta2 = length - mean
97
+ m2 += delta * delta2
98
+ std = math.sqrt(m2 / count) if count > 1 else 0.0
99
+
100
+ # Hour histogram
101
+ histogram[hour] = histogram[hour] + 1
102
+
103
+ # Write back
104
+ await self._graph._run_update_baseline(
105
+ person_id=person_id,
106
+ msg_count=count,
107
+ length_mean=mean,
108
+ length_m2=m2,
109
+ length_std=std,
110
+ hour_histogram=json.dumps(histogram),
111
+ )
112
+ except Exception as exc:
113
+ logger.debug("GraphBaselineStore.record_message failed for %s: %s", person_id, exc)
114
+
115
+ async def store_signal(self, signal: "Signal") -> None:
116
+ """Persist a signal to the graph. Best-effort — logs on failure."""
117
+ try:
118
+ await self._graph.store_signal(signal)
119
+ except Exception as exc:
120
+ logger.debug("GraphBaselineStore.store_signal failed: %s", exc)
121
+
122
+ @staticmethod
123
+ def _preferred_hours(histogram_json: Optional[str]) -> List[int]:
124
+ """Hours above the median activity level."""
125
+ if not histogram_json:
126
+ return list(_DEFAULT_PREFERRED_HOURS)
127
+ try:
128
+ counts = json.loads(histogram_json)
129
+ except (json.JSONDecodeError, TypeError):
130
+ return list(_DEFAULT_PREFERRED_HOURS)
131
+ if len(counts) != 24 or sum(counts) < 5:
132
+ return list(_DEFAULT_PREFERRED_HOURS)
133
+ median = sorted(counts)[12]
134
+ if median == 0:
135
+ return list(_DEFAULT_PREFERRED_HOURS)
136
+ return [h for h, c in enumerate(counts) if c >= median]
@@ -0,0 +1,361 @@
1
+ """Colony's interaction signal collection — zero LLM, pure computation.
2
+
3
+ Colony extracts behavioral signals from every message it exchanges across connected
4
+ gateways. These signals build Colony's own picture of each person it interacts with:
5
+ how they communicate, when they're engaged, their emotional tone. Colony's relational
6
+ intelligence is built from this stream of first-hand observations, not from the
7
+ owner's address book or stated opinions.
8
+ """
9
+ import logging
10
+ import time
11
+ from dataclasses import dataclass
12
+
13
+ logger = logging.getLogger(__name__)
14
+ from datetime import datetime
15
+ from typing import List, Optional, Protocol
16
+ import re
17
+
18
+ @dataclass
19
+ class Signal:
20
+ signal_type: str
21
+ raw_value: float
22
+ normalized_value: float
23
+ timestamp: datetime
24
+ person_id: str
25
+ source: str # "message", "reaction", "call"
26
+ context: Optional[dict] = None
27
+
28
+
29
+ class BaselineStore(Protocol):
30
+ """Protocol for baseline store dependency."""
31
+ async def get(self, person_id: str) -> "PersonBaseline": ...
32
+ async def record_message(self, person_id: str, length: int, hour: int) -> None: ...
33
+
34
+
35
+ class ColonyGraph(Protocol):
36
+ """Protocol for graph dependency."""
37
+ async def store_signal(self, signal: Signal) -> None: ...
38
+ async def get_recent_signals(self, person_id: str, hours: int, signal_type: Optional[str] = None) -> List[Signal]: ...
39
+
40
+
41
+ class PersonBaseline(Protocol):
42
+ """Protocol for person baseline."""
43
+ length_mean: float
44
+ length_std: float
45
+ preferred_hours: List[int]
46
+
47
+
48
+ class Message(Protocol):
49
+ """Protocol for message input."""
50
+ sender_id: str
51
+ content: str
52
+ timestamp: datetime
53
+ reply_to_id: Optional[str] = None
54
+ has_media: bool = False
55
+
56
+
57
+ class SignalCollector:
58
+ """Extract behavioral signals from Colony's own interactions — zero LLM, pure computation.
59
+
60
+ Each message Colony sends or receives is an observation. SignalCollector turns
61
+ those observations into quantified signals (frequency, sentiment, latency, etc.)
62
+ that feed Colony's mind model and relationship scoring. Colony builds its social
63
+ intelligence from this first-hand evidence, independent of the owner's stated views.
64
+ """
65
+
66
+ POSITIVE_WORDS = {"happy", "great", "love", "thanks", "awesome", "excited",
67
+ "good", "wonderful", "amazing", "fantastic", "perfect"}
68
+ NEGATIVE_WORDS = {"sad", "angry", "frustrated", "sorry", "bad", "worried",
69
+ "terrible", "awful", "horrible", "hate", "upset"}
70
+ NEGATION_WORDS = {"not", "no", "never", "don't", "doesn't", "didn't", "wasn't"}
71
+ EMOJI_PATTERN = re.compile(r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F700-\U0001F77F\U0001F780-\U0001F7FF\U0001F800-\U0001F8FF\U0001F900-\U0001F9FF\U0001FA00-\U0001FA6F\U0001FA70-\U0001FAFF\U00002702-\U000027B0]')
72
+
73
+ def __init__(self, baseline_store: BaselineStore, graph: ColonyGraph, metrics=None):
74
+ self.baselines = baseline_store
75
+ self.graph = graph
76
+ self._metrics = metrics # Optional ColonyMetricsCollector
77
+
78
+ async def ingest_raw(self, signal_data: dict) -> None:
79
+ """Ingest a pre-computed signal from an external source."""
80
+ try:
81
+ from apsimo.models.signal import Signal, SignalType, SignalStrength
82
+ sig_type = SignalType.ENGAGEMENT_DEPTH # default
83
+ raw_type = signal_data.get("type", "")
84
+ for st in SignalType:
85
+ if st.value == raw_type or st.name.lower() == raw_type.lower():
86
+ sig_type = st
87
+ break
88
+ sig = Signal(
89
+ id=signal_data.get("id", f"raw-{time.monotonic_ns()}"),
90
+ person_id=signal_data.get("person_id", ""),
91
+ signal_type=sig_type,
92
+ value=float(signal_data.get("value", 0.5)),
93
+ source=signal_data.get("source", "api"),
94
+ context=signal_data.get("data"),
95
+ )
96
+ await self.baselines.store_signal(sig)
97
+ except Exception as e:
98
+ logger.warning("ingest_raw failed: %s", e)
99
+
100
+ async def collect(self, message: Message) -> List[Signal]:
101
+ """Extract all signals from a single message in Colony's interaction stream."""
102
+ t0 = time.monotonic()
103
+ signals = []
104
+ person_id = message.sender_id
105
+
106
+ try:
107
+ baseline = await self.baselines.get(person_id)
108
+ except (LookupError, AttributeError, OSError, RuntimeError):
109
+ baseline = None
110
+
111
+ # 1. Message length (Z-score normalized)
112
+ length = len(message.content)
113
+ if baseline and baseline.length_std > 0:
114
+ z_score = (length - baseline.length_mean) / baseline.length_std
115
+ else:
116
+ z_score = 0.0
117
+ signals.append(Signal(
118
+ signal_type="message_length",
119
+ raw_value=length,
120
+ normalized_value=z_score,
121
+ timestamp=message.timestamp,
122
+ person_id=person_id,
123
+ source="message",
124
+ ))
125
+
126
+ # 2. Sentiment (lexicon-based, no LLM)
127
+ sentiment = self._compute_sentiment(message.content)
128
+ signals.append(Signal(
129
+ signal_type="sentiment",
130
+ raw_value=sentiment,
131
+ normalized_value=sentiment, # Already -1 to 1
132
+ timestamp=message.timestamp,
133
+ person_id=person_id,
134
+ source="message",
135
+ ))
136
+
137
+ # 3. Emoji usage
138
+ emoji_count = len(self.EMOJI_PATTERN.findall(message.content))
139
+ emoji_per_100 = (emoji_count / max(len(message.content), 1)) * 100
140
+ signals.append(Signal(
141
+ signal_type="emoji_usage",
142
+ raw_value=emoji_count,
143
+ normalized_value=emoji_per_100,
144
+ timestamp=message.timestamp,
145
+ person_id=person_id,
146
+ source="message",
147
+ ))
148
+
149
+ # 4. Media usage
150
+ signals.append(Signal(
151
+ signal_type="media_usage",
152
+ raw_value=1.0 if message.has_media else 0.0,
153
+ normalized_value=1.0 if message.has_media else 0.0,
154
+ timestamp=message.timestamp,
155
+ person_id=person_id,
156
+ source="message",
157
+ ))
158
+
159
+ # 5. Time of day
160
+ hour = message.timestamp.hour
161
+ if baseline and hasattr(baseline, 'preferred_hours') and baseline.preferred_hours:
162
+ if hour in baseline.preferred_hours:
163
+ tod_bucket = 0 # preferred
164
+ elif 8 <= hour <= 22:
165
+ tod_bucket = 1 # off-hours but reasonable
166
+ else:
167
+ tod_bucket = 2 # late-night
168
+ else:
169
+ if 8 <= hour <= 22:
170
+ tod_bucket = 0
171
+ elif 6 <= hour < 8 or 22 < hour <= 24:
172
+ tod_bucket = 1
173
+ else:
174
+ tod_bucket = 2
175
+ signals.append(Signal(
176
+ signal_type="time_of_day",
177
+ raw_value=float(hour),
178
+ normalized_value=float(tod_bucket),
179
+ timestamp=message.timestamp,
180
+ person_id=person_id,
181
+ source="message",
182
+ ))
183
+
184
+ # 6. Message frequency (24h rolling)
185
+ recent_count = await self._get_message_frequency(person_id, hours=24)
186
+ signals.append(Signal(
187
+ signal_type="message_frequency",
188
+ raw_value=float(recent_count + 1), # +1 for this message
189
+ normalized_value=min(float(recent_count + 1) / 10.0, 10.0), # Cap at 10
190
+ timestamp=message.timestamp,
191
+ person_id=person_id,
192
+ source="message",
193
+ ))
194
+
195
+ # 7. Response latency (if replying)
196
+ if message.reply_to_id:
197
+ latency = await self._compute_latency(message)
198
+ if latency is not None:
199
+ signals.append(Signal(
200
+ signal_type="response_latency",
201
+ raw_value=latency,
202
+ normalized_value=min(latency / 60.0, 24.0), # Hours, cap at 24
203
+ timestamp=message.timestamp,
204
+ person_id=person_id,
205
+ source="message",
206
+ ))
207
+
208
+ # 8. Initiation ratio (30-day window)
209
+ init_ratio = await self._compute_initiation_ratio(person_id, days=30)
210
+ signals.append(Signal(
211
+ signal_type="initiation_ratio",
212
+ raw_value=init_ratio,
213
+ normalized_value=init_ratio, # Already 0-1
214
+ timestamp=message.timestamp,
215
+ person_id=person_id,
216
+ source="message",
217
+ ))
218
+
219
+ # Persist all signals to graph
220
+ for sig in signals:
221
+ try:
222
+ await self.graph.store_signal(sig)
223
+ except (OSError, RuntimeError) as exc:
224
+ logger.debug("SignalCollector: failed to store signal %s: %s", sig.signal_type, exc)
225
+
226
+ # Update baseline with this message
227
+ try:
228
+ if hasattr(self.baselines, 'record_message'):
229
+ await self.baselines.record_message(person_id, length, hour)
230
+ except (OSError, RuntimeError, AttributeError) as exc:
231
+ logger.debug("SignalCollector: failed to update baseline for %s: %s", person_id, exc)
232
+
233
+ elapsed_ms = (time.monotonic() - t0) * 1000
234
+ logger.info("Collected %d signals for %s in %.1fms", len(signals), person_id, elapsed_ms)
235
+ if self._metrics is not None:
236
+ try:
237
+ self._metrics.record_signal_collection(
238
+ latency_ms=elapsed_ms,
239
+ signal_count=len(signals),
240
+ signal_types=[s.signal_type for s in signals],
241
+ )
242
+ except Exception as exc:
243
+ logger.debug("Metrics recording failed: %s", exc)
244
+
245
+ return signals
246
+
247
+ def _compute_sentiment(self, text: str) -> float:
248
+ """Lexicon-based sentiment with negation handling."""
249
+ raw_words = text.lower().split()
250
+ # Strip punctuation so "great!" matches "great", etc.
251
+ words = [w.strip(".,!?;:\"'()[]{}") for w in raw_words]
252
+ pos_count = 0
253
+ neg_count = 0
254
+
255
+ for i, word in enumerate(words):
256
+ # Check for negation in previous 3 words
257
+ negated = False
258
+ for j in range(max(0, i - 3), i):
259
+ if words[j] in self.NEGATION_WORDS:
260
+ negated = True
261
+ break
262
+
263
+ if word in self.POSITIVE_WORDS:
264
+ if negated:
265
+ neg_count += 0.5 # Negated positive = weak negative
266
+ else:
267
+ pos_count += 1
268
+ elif word in self.NEGATIVE_WORDS:
269
+ if negated:
270
+ pos_count += 0.5 # Negated negative = weak positive
271
+ else:
272
+ neg_count += 1
273
+
274
+ total = pos_count + neg_count
275
+ if total == 0:
276
+ return 0.0
277
+ return (pos_count - neg_count) / total
278
+
279
+ async def _get_message_frequency(self, person_id: str, hours: int = 24) -> int:
280
+ """Count messages in the last N hours."""
281
+ try:
282
+ signals = await self.graph.get_recent_signals(
283
+ person_id, hours=hours, signal_type="message_length"
284
+ )
285
+ return len(signals)
286
+ except (OSError, RuntimeError):
287
+ return 0
288
+
289
+ async def _compute_latency(self, message: Message) -> Optional[float]:
290
+ """Compute response latency in minutes for a reply message.
291
+
292
+ Uses the most recent prior signal as a proxy for the original
293
+ message timestamp, since Signal.context doesn't carry message IDs.
294
+ Returns None if no prior signals exist or on error.
295
+ """
296
+ if not message.reply_to_id:
297
+ return None
298
+ try:
299
+ # Fetch recent signals ordered by timestamp DESC; the most recent
300
+ # signal before this message is the best proxy for what's being
301
+ # replied to.
302
+ prior_signals = await self.graph.get_recent_signals(
303
+ message.sender_id, hours=24 * 7, signal_type="message_length"
304
+ )
305
+ for sig in prior_signals:
306
+ if sig.timestamp < message.timestamp:
307
+ delta = (message.timestamp - sig.timestamp).total_seconds() / 60.0
308
+ return max(0.0, delta)
309
+ except (OSError, RuntimeError):
310
+ pass
311
+ return None
312
+
313
+ async def _compute_initiation_ratio(self, person_id: str, days: int = 30) -> float:
314
+ """Compute conversation initiation ratio over past N days.
315
+
316
+ A new conversation starts when there is a 4-hour gap since the
317
+ previous message from this person. Since we only track signals
318
+ from this person (not Colony's outbound messages), we approximate
319
+ the initiation ratio as (person-initiated conversations) / total.
320
+ A signal with ``source="inbound"`` means the person sent it;
321
+ ``source="outbound"`` means Colony sent first. If direction info
322
+ is unavailable (all ``source="message"``), we fall back to the
323
+ ratio of conversation-starts to total messages as an activity proxy.
324
+
325
+ Returns a value in [0, 1]. Returns 0.5 when there is insufficient data.
326
+ """
327
+ GAP_HOURS = 4
328
+ try:
329
+ signals = await self.graph.get_recent_signals(
330
+ person_id, hours=days * 24, signal_type="message_length"
331
+ )
332
+ if not signals:
333
+ return 0.5
334
+
335
+ signals.sort(key=lambda s: s.timestamp)
336
+ conversation_starts = 0
337
+ person_starts = 0
338
+ prev_ts = None
339
+ has_direction = any(s.source in ("inbound", "outbound") for s in signals)
340
+
341
+ for sig in signals:
342
+ is_new_convo = (
343
+ prev_ts is None
344
+ or (sig.timestamp - prev_ts).total_seconds() > GAP_HOURS * 3600
345
+ )
346
+ if is_new_convo:
347
+ conversation_starts += 1
348
+ if has_direction:
349
+ if sig.source == "inbound":
350
+ person_starts += 1
351
+ else:
352
+ # No direction info — count all conversation starts
353
+ # as person-initiated (best available approximation)
354
+ person_starts += 1
355
+ prev_ts = sig.timestamp
356
+
357
+ if conversation_starts == 0:
358
+ return 0.5
359
+ return person_starts / conversation_starts
360
+ except (OSError, RuntimeError):
361
+ return 0.5
@@ -0,0 +1,11 @@
1
+ """Relationships package — trust tiers and relationship scoring."""
2
+
3
+ from .trust_tiers import TrustTier, TrustTierManager
4
+ from .scorer import ScoreWeights, RelationshipScorer
5
+
6
+ __all__ = [
7
+ "TrustTier",
8
+ "TrustTierManager",
9
+ "ScoreWeights",
10
+ "RelationshipScorer",
11
+ ]