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,513 @@
1
+ """Research-to-Artifact Pipeline — 6-stage orchestrator.
2
+
3
+ Stages:
4
+ 1. DECOMPOSE — parse goal into sub-tasks with GoalDecomposer
5
+ 2. GATHER — collect evidence from web, graph, documents, email
6
+ 3. SYNTHESIZE — cross-reference evidence into insights
7
+ 4. OUTLINE — build structured artifact outline
8
+ 5. PRODUCE — render artifact in requested format
9
+ 6. REVIEW — quality and safety gate (ResponseGate)
10
+
11
+ Usage::
12
+
13
+ pipeline = ResearchPipeline()
14
+ run = await pipeline.run(
15
+ goal="Research competitor pricing",
16
+ format=ArtifactFormat.MARKDOWN,
17
+ )
18
+ print(run.artifact.content)
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ import secrets
25
+ import time
26
+ from dataclasses import dataclass, field
27
+ from datetime import datetime, timezone
28
+ from enum import Enum
29
+ from typing import Any, Dict, List, Optional
30
+
31
+ from apsimo.research.gatherer import EvidencePackage, GatherConfig, SourceGatherer
32
+ from apsimo.research.synthesizer import ResearchSynthesizer, SynthesisConfig, SynthesisReport
33
+ from apsimo.research.artifact import (
34
+ Artifact,
35
+ ArtifactFormat,
36
+ ArtifactOutline,
37
+ ArtifactRenderer,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # Pipeline state models
45
+ # ---------------------------------------------------------------------------
46
+
47
+
48
+ class PipelineStage(str, Enum):
49
+ DECOMPOSE = "decompose"
50
+ GATHER = "gather"
51
+ SYNTHESIZE = "synthesize"
52
+ OUTLINE = "outline"
53
+ PRODUCE = "produce"
54
+ REVIEW = "review"
55
+ DONE = "done"
56
+
57
+
58
+ class PipelineStatus(str, Enum):
59
+ PENDING = "pending"
60
+ RUNNING = "running"
61
+ COMPLETED = "completed"
62
+ FAILED = "failed"
63
+ PAUSED = "paused" # awaiting outline approval
64
+
65
+
66
+ @dataclass
67
+ class ReviewResult:
68
+ """Outcome of the Stage 6 review gate."""
69
+
70
+ passed: bool
71
+ pii_clean: bool = True
72
+ injection_clean: bool = True
73
+ hallucination_flags: List[str] = field(default_factory=list)
74
+ rerender_required: bool = False
75
+ resynth_required: bool = False
76
+ gate_notes: str = ""
77
+
78
+
79
+ @dataclass
80
+ class PipelineRun:
81
+ """Live state of a single pipeline execution."""
82
+
83
+ id: str
84
+ goal: str
85
+ format: ArtifactFormat
86
+ status: PipelineStatus
87
+ current_stage: PipelineStage
88
+
89
+ # Intermediate results
90
+ evidence: EvidencePackage = field(default_factory=list)
91
+ synthesis: Optional[SynthesisReport] = None
92
+ outline: Optional[ArtifactOutline] = None
93
+ artifact: Optional[Artifact] = None
94
+ review: Optional[ReviewResult] = None
95
+
96
+ # Timing
97
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
98
+ updated_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
99
+ completed_at: Optional[datetime] = None
100
+
101
+ # Stage durations (seconds)
102
+ stage_durations: Dict[str, float] = field(default_factory=dict)
103
+
104
+ # Error info
105
+ error: Optional[str] = None
106
+
107
+ # Config carried through
108
+ document_ids: List[str] = field(default_factory=list)
109
+ metadata: Dict[str, Any] = field(default_factory=dict)
110
+
111
+ def touch(self, stage: Optional[PipelineStage] = None) -> None:
112
+ self.updated_at = datetime.now(timezone.utc)
113
+ if stage:
114
+ self.current_stage = stage
115
+
116
+ def to_dict(self) -> Dict[str, Any]:
117
+ return {
118
+ "id": self.id,
119
+ "goal": self.goal,
120
+ "format": self.format.value,
121
+ "status": self.status.value,
122
+ "current_stage": self.current_stage.value,
123
+ "evidence_count": len(self.evidence),
124
+ "synthesis_confidence": (
125
+ self.synthesis.synthesis_confidence if self.synthesis else None
126
+ ),
127
+ "insight_count": (
128
+ len(self.synthesis.insights) if self.synthesis else None
129
+ ),
130
+ "artifact_id": self.artifact.id if self.artifact else None,
131
+ "artifact_word_count": self.artifact.word_count if self.artifact else None,
132
+ "review_passed": self.review.passed if self.review else None,
133
+ "created_at": self.created_at.isoformat(),
134
+ "updated_at": self.updated_at.isoformat(),
135
+ "completed_at": self.completed_at.isoformat() if self.completed_at else None,
136
+ "stage_durations": self.stage_durations,
137
+ "error": self.error,
138
+ }
139
+
140
+
141
+ # ---------------------------------------------------------------------------
142
+ # Pipeline configuration
143
+ # ---------------------------------------------------------------------------
144
+
145
+
146
+ @dataclass
147
+ class PipelineConfig:
148
+ """Full pipeline configuration."""
149
+
150
+ gather: GatherConfig = field(default_factory=GatherConfig)
151
+ synthesis: SynthesisConfig = field(default_factory=SynthesisConfig)
152
+
153
+ require_outline_approval: bool = False
154
+ outline_approval_timeout_seconds: float = 300.0
155
+
156
+ # Review settings
157
+ review_l6_enabled: bool = True
158
+ review_hallucination_threshold: float = 0.70
159
+ review_pii_action: str = "redact" # "redact" | "block"
160
+
161
+ # Delivery
162
+ l7_cancel_window_seconds: float = 30.0
163
+ persist_to_graph: bool = True
164
+
165
+ # Stage timeouts (seconds)
166
+ timeout_decompose: float = 30.0
167
+ timeout_gather: float = 120.0
168
+ timeout_synthesize: float = 90.0
169
+ timeout_outline: float = 45.0
170
+ timeout_produce: float = 180.0
171
+ timeout_review: float = 60.0
172
+
173
+
174
+ # ---------------------------------------------------------------------------
175
+ # In-memory run store (used when no persistent backend is available)
176
+ # ---------------------------------------------------------------------------
177
+
178
+ _run_store: Dict[str, PipelineRun] = {}
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # Pipeline
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ class ResearchPipeline:
187
+ """6-stage Research-to-Artifact Pipeline.
188
+
189
+ Each stage is implemented as an async method. ``run()`` chains all
190
+ stages sequentially, recording stage durations and propagating errors.
191
+
192
+ The pipeline is designed to be resilient: failures in optional stages
193
+ (e.g. the review gate) are logged and skipped rather than aborting the
194
+ entire run.
195
+ """
196
+
197
+ def __init__(
198
+ self,
199
+ config: Optional[PipelineConfig] = None,
200
+ *,
201
+ graph: Any = None,
202
+ allow_fallback_graph: bool = True,
203
+ ) -> None:
204
+ self.config = config or PipelineConfig()
205
+ self._gatherer = SourceGatherer(
206
+ self.config.gather,
207
+ graph=graph,
208
+ allow_fallback_graph=allow_fallback_graph,
209
+ )
210
+ self._synthesizer = ResearchSynthesizer(self.config.synthesis)
211
+ self._renderer = ArtifactRenderer()
212
+
213
+ # ------------------------------------------------------------------
214
+ # Public entry point
215
+ # ------------------------------------------------------------------
216
+
217
+ async def run(
218
+ self,
219
+ goal: str,
220
+ format: ArtifactFormat = ArtifactFormat.MARKDOWN,
221
+ document_ids: Optional[List[str]] = None,
222
+ run_id: Optional[str] = None,
223
+ metadata: Optional[Dict[str, Any]] = None,
224
+ ) -> PipelineRun:
225
+ """Execute all 6 pipeline stages and return the completed run.
226
+
227
+ Args:
228
+ goal: Free-text research goal.
229
+ format: Desired output artifact format.
230
+ document_ids: Optional document IDs to ingest in Stage 2.
231
+ run_id: Optional pre-assigned run ID.
232
+ metadata: Optional metadata to attach to the run.
233
+
234
+ Returns:
235
+ A ``PipelineRun`` with ``status=COMPLETED`` (or FAILED on error).
236
+ """
237
+ run = PipelineRun(
238
+ id=run_id or ("rap-" + secrets.token_hex(8)),
239
+ goal=goal,
240
+ format=format,
241
+ status=PipelineStatus.RUNNING,
242
+ current_stage=PipelineStage.DECOMPOSE,
243
+ document_ids=document_ids or [],
244
+ metadata=metadata or {},
245
+ )
246
+ _run_store[run.id] = run
247
+ logger.info("ResearchPipeline: starting run %s goal=%r format=%s", run.id, goal, format.value)
248
+
249
+ try:
250
+ # Stage 1: Decompose
251
+ await self._stage_decompose(run)
252
+
253
+ # Stage 2: Gather
254
+ await self._stage_gather(run)
255
+
256
+ # Stage 3: Synthesize
257
+ await self._stage_synthesize(run)
258
+
259
+ # Stage 4: Outline
260
+ await self._stage_outline(run)
261
+
262
+ # Stage 5: Produce
263
+ await self._stage_produce(run)
264
+
265
+ # Stage 6: Review
266
+ await self._stage_review(run)
267
+
268
+ run.status = PipelineStatus.COMPLETED
269
+ run.current_stage = PipelineStage.DONE
270
+ run.completed_at = datetime.now(timezone.utc)
271
+ logger.info(
272
+ "ResearchPipeline: run %s completed in %.1fs",
273
+ run.id,
274
+ sum(run.stage_durations.values()),
275
+ )
276
+
277
+ except Exception as exc:
278
+ run.status = PipelineStatus.FAILED
279
+ run.error = str(exc)
280
+ logger.error("ResearchPipeline: run %s failed at stage %s: %s", run.id, run.current_stage, exc)
281
+
282
+ run.touch()
283
+ return run
284
+
285
+ # ------------------------------------------------------------------
286
+ # Stage 1: Decompose
287
+ # ------------------------------------------------------------------
288
+
289
+ async def _stage_decompose(self, run: PipelineRun) -> None:
290
+ """Parse goal text and validate format/constraints."""
291
+ t0 = time.monotonic()
292
+ run.touch(PipelineStage.DECOMPOSE)
293
+
294
+ # Validation: goal must be non-empty
295
+ if not run.goal or not run.goal.strip():
296
+ raise ValueError("Research goal cannot be empty")
297
+
298
+ # Optionally use GoalDecomposer to validate / enrich the goal
299
+ try:
300
+ from apsimo.goals.decomposer import GoalDecomposer
301
+ decomposer = GoalDecomposer()
302
+ dag = decomposer.decompose_text(run.goal)
303
+ run.metadata["goal_dag_id"] = getattr(dag, "id", None)
304
+ except Exception as exc:
305
+ logger.debug("GoalDecomposer unavailable (%s), continuing without DAG", exc)
306
+
307
+ run.stage_durations[PipelineStage.DECOMPOSE.value] = time.monotonic() - t0
308
+ logger.debug("Stage DECOMPOSE: %.2fs", run.stage_durations[PipelineStage.DECOMPOSE.value])
309
+
310
+ # ------------------------------------------------------------------
311
+ # Stage 2: Gather
312
+ # ------------------------------------------------------------------
313
+
314
+ async def _stage_gather(self, run: PipelineRun) -> None:
315
+ """Collect evidence from all enabled sources in parallel."""
316
+ t0 = time.monotonic()
317
+ run.touch(PipelineStage.GATHER)
318
+
319
+ evidence = await self._gatherer.gather(
320
+ query=run.goal,
321
+ document_ids=run.document_ids or None,
322
+ )
323
+ run.evidence = evidence
324
+
325
+ run.stage_durations[PipelineStage.GATHER.value] = time.monotonic() - t0
326
+ logger.debug(
327
+ "Stage GATHER: %d items in %.2fs",
328
+ len(evidence),
329
+ run.stage_durations[PipelineStage.GATHER.value],
330
+ )
331
+
332
+ # ------------------------------------------------------------------
333
+ # Stage 3: Synthesize
334
+ # ------------------------------------------------------------------
335
+
336
+ async def _stage_synthesize(self, run: PipelineRun) -> None:
337
+ """Cross-reference evidence and extract insights."""
338
+ t0 = time.monotonic()
339
+ run.touch(PipelineStage.SYNTHESIZE)
340
+
341
+ synthesis = await self._synthesizer.synthesize(
342
+ evidence=run.evidence,
343
+ goal_id=run.id,
344
+ goal_text=run.goal,
345
+ )
346
+ run.synthesis = synthesis
347
+
348
+ run.stage_durations[PipelineStage.SYNTHESIZE.value] = time.monotonic() - t0
349
+ logger.debug(
350
+ "Stage SYNTHESIZE: %d insights, confidence=%.2f in %.2fs",
351
+ len(synthesis.insights),
352
+ synthesis.synthesis_confidence,
353
+ run.stage_durations[PipelineStage.SYNTHESIZE.value],
354
+ )
355
+
356
+ # ------------------------------------------------------------------
357
+ # Stage 4: Outline
358
+ # ------------------------------------------------------------------
359
+
360
+ async def _stage_outline(self, run: PipelineRun) -> None:
361
+ """Build the structured artifact outline."""
362
+ t0 = time.monotonic()
363
+ run.touch(PipelineStage.OUTLINE)
364
+
365
+ assert run.synthesis is not None
366
+ outline = self._renderer.build_outline(
367
+ synthesis=run.synthesis,
368
+ goal_text=run.goal,
369
+ format=run.format,
370
+ )
371
+ run.outline = outline
372
+
373
+ run.stage_durations[PipelineStage.OUTLINE.value] = time.monotonic() - t0
374
+ logger.debug(
375
+ "Stage OUTLINE: %d sections in %.2fs",
376
+ len(outline.sections),
377
+ run.stage_durations[PipelineStage.OUTLINE.value],
378
+ )
379
+
380
+ # ------------------------------------------------------------------
381
+ # Stage 5: Produce
382
+ # ------------------------------------------------------------------
383
+
384
+ async def _stage_produce(self, run: PipelineRun) -> None:
385
+ """Render the artifact in the requested format."""
386
+ t0 = time.monotonic()
387
+ run.touch(PipelineStage.PRODUCE)
388
+
389
+ assert run.outline is not None
390
+ assert run.synthesis is not None
391
+ artifact = self._renderer.render(
392
+ outline=run.outline,
393
+ synthesis=run.synthesis,
394
+ goal_text=run.goal,
395
+ )
396
+ run.artifact = artifact
397
+
398
+ run.stage_durations[PipelineStage.PRODUCE.value] = time.monotonic() - t0
399
+ logger.debug(
400
+ "Stage PRODUCE: format=%s words=%d in %.2fs",
401
+ artifact.format.value,
402
+ artifact.word_count,
403
+ run.stage_durations[PipelineStage.PRODUCE.value],
404
+ )
405
+
406
+ # ------------------------------------------------------------------
407
+ # Stage 6: Review
408
+ # ------------------------------------------------------------------
409
+
410
+ async def _stage_review(self, run: PipelineRun) -> None:
411
+ """Run artifact through the response gate."""
412
+ t0 = time.monotonic()
413
+ run.touch(PipelineStage.REVIEW)
414
+
415
+ assert run.artifact is not None
416
+ review = await self._run_review_gate(run.artifact, run)
417
+ run.review = review
418
+
419
+ if not review.passed:
420
+ if review.resynth_required:
421
+ # Re-synthesize flagged sections (simplified: log and continue)
422
+ logger.warning("Review: resynth required — skipping in this run")
423
+ elif not review.rerender_required:
424
+ raise RuntimeError(
425
+ f"Artifact failed review gate: {review.gate_notes}"
426
+ )
427
+
428
+ run.stage_durations[PipelineStage.REVIEW.value] = time.monotonic() - t0
429
+ logger.debug(
430
+ "Stage REVIEW: passed=%s in %.2fs",
431
+ review.passed,
432
+ run.stage_durations[PipelineStage.REVIEW.value],
433
+ )
434
+
435
+ async def _run_review_gate(
436
+ self,
437
+ artifact: Artifact,
438
+ run: PipelineRun,
439
+ ) -> ReviewResult:
440
+ """Content-safety review of a research artifact.
441
+
442
+ A research artifact has no recipient/trust-tier, so the full
443
+ ResponseGate (recipient verification, cross-context, trust tiering)
444
+ does not apply — but its PII scanner (L2) and injection detector (L5)
445
+ do. Run those two layers directly. (The previous code constructed
446
+ ResponseGate() with the wrong arguments, threw on every call, and
447
+ silently degraded to a 4-string substring scan.)"""
448
+ try:
449
+ from apsimo.gate.layers.l2_pii import PIIScanner
450
+ from apsimo.gate.layers.l5_injection import InjectionDetector
451
+ from apsimo.gate.config import GateConfig
452
+ from apsimo.gate.models import GatePayload, TrustTier
453
+
454
+ cfg = GateConfig()
455
+ payload = GatePayload(
456
+ response_text=artifact.content,
457
+ target_contact_id="", target_gateway="",
458
+ session_id=str(run.metadata.get("session_id", "research")),
459
+ trust_tier=TrustTier.PERIPHERAL,
460
+ mentioned_entities=frozenset(),
461
+ turn_id=str(getattr(run, "run_id", "") or "research"),
462
+ incoming_message_text="",
463
+ )
464
+ pii = await PIIScanner(cfg).check(payload)
465
+ inj = await InjectionDetector(cfg).check(payload)
466
+ pii_clean = not pii.blocked
467
+ injection_clean = not inj.blocked
468
+ notes = []
469
+ if not pii_clean:
470
+ notes.append(f"pii:{pii.code}")
471
+ if not injection_clean:
472
+ notes.append(f"injection:{inj.reason or inj.code}")
473
+ return ReviewResult(
474
+ passed=pii_clean and injection_clean,
475
+ pii_clean=pii_clean,
476
+ injection_clean=injection_clean,
477
+ gate_notes="; ".join(notes) or "clean",
478
+ )
479
+ except Exception as exc:
480
+ logger.warning("research review gate failed (%s); "
481
+ "falling back to injection-marker scan", exc)
482
+
483
+ # Fallback only when the gate layers are genuinely unavailable.
484
+ content = artifact.content
485
+ injection_markers = ["<script", "javascript:", "data:text/html", "eval("]
486
+ injection_found = any(m in content.lower() for m in injection_markers)
487
+ return ReviewResult(
488
+ passed=not injection_found,
489
+ pii_clean=True,
490
+ injection_clean=not injection_found,
491
+ gate_notes="basic-review" if not injection_found else "injection-detected",
492
+ )
493
+
494
+ # ------------------------------------------------------------------
495
+ # Run management helpers
496
+ # ------------------------------------------------------------------
497
+
498
+ def get_run(self, run_id: str) -> Optional[PipelineRun]:
499
+ """Retrieve a run by ID from the in-memory store."""
500
+ return _run_store.get(run_id)
501
+
502
+ def list_runs(
503
+ self,
504
+ status: Optional[str] = None,
505
+ limit: int = 50,
506
+ offset: int = 0,
507
+ ) -> List[PipelineRun]:
508
+ """List runs from the in-memory store with optional status filter."""
509
+ runs = list(_run_store.values())
510
+ if status:
511
+ runs = [r for r in runs if r.status.value == status]
512
+ runs.sort(key=lambda r: r.created_at, reverse=True)
513
+ return runs[offset: offset + limit]
@@ -0,0 +1,7 @@
1
+ """Colony web search integration."""
2
+
3
+ from apsimo.research.search.base import SearchProvider, SearchResult
4
+ from apsimo.research.search.orchestrator import SearchOrchestrator
5
+ from apsimo.research.search.cache import SearchCache
6
+
7
+ __all__ = ["SearchProvider", "SearchResult", "SearchOrchestrator", "SearchCache"]
@@ -0,0 +1,41 @@
1
+ """Base classes for search providers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timezone
8
+ from typing import Optional
9
+
10
+
11
+ @dataclass
12
+ class SearchResult:
13
+ """A single search result."""
14
+
15
+ title: str
16
+ url: str
17
+ snippet: str
18
+ content: Optional[str] = None # Full page content if available
19
+ source: str = ""
20
+ rank: int = 0
21
+ retrieved_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
22
+
23
+
24
+ class SearchProvider(ABC):
25
+ """Base class for search API providers."""
26
+
27
+ @abstractmethod
28
+ async def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
29
+ """Execute a search query and return results."""
30
+ ...
31
+
32
+ @property
33
+ @abstractmethod
34
+ def name(self) -> str:
35
+ """Provider identifier (e.g., 'tavily', 'serpapi')."""
36
+ ...
37
+
38
+ @property
39
+ def rate_limit_per_minute(self) -> int:
40
+ """Maximum requests per minute."""
41
+ return 30
@@ -0,0 +1,59 @@
1
+ """Brave Search API provider."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import Optional
7
+
8
+ from apsimo.research.search.base import SearchProvider, SearchResult
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ try:
13
+ import httpx
14
+ HAS_HTTPX = True
15
+ except ImportError:
16
+ HAS_HTTPX = False
17
+
18
+
19
+ class BraveSearchProvider(SearchProvider):
20
+ """Brave Search API — privacy-focused search."""
21
+
22
+ def __init__(self, api_key: str):
23
+ self._api_key = api_key
24
+ self._base_url = "https://api.search.brave.com/res/v1/web/search"
25
+
26
+ @property
27
+ def name(self) -> str:
28
+ return "brave"
29
+
30
+ async def search(self, query: str, max_results: int = 5) -> list[SearchResult]:
31
+ if not HAS_HTTPX:
32
+ logger.warning("httpx not installed — cannot use Brave Search")
33
+ return []
34
+
35
+ try:
36
+ async with httpx.AsyncClient(timeout=15) as client:
37
+ resp = await client.get(self._base_url, params={
38
+ "q": query,
39
+ "count": max_results,
40
+ }, headers={
41
+ "X-Subscription-Token": self._api_key,
42
+ "Accept": "application/json",
43
+ })
44
+ resp.raise_for_status()
45
+ data = resp.json()
46
+ results = []
47
+ for i, r in enumerate(data.get("web", {}).get("results", [])[:max_results]):
48
+ results.append(SearchResult(
49
+ title=r.get("title", ""),
50
+ url=r.get("url", ""),
51
+ snippet=r.get("description", ""),
52
+ content=None,
53
+ source="brave",
54
+ rank=i,
55
+ ))
56
+ return results
57
+ except Exception as e:
58
+ logger.warning("Brave Search failed: %s", e)
59
+ return []
@@ -0,0 +1,51 @@
1
+ """Search result cache to avoid duplicate API calls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import logging
7
+ import time
8
+ from typing import Dict, List, Optional
9
+
10
+ from apsimo.research.search.base import SearchResult
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class SearchCache:
16
+ """In-memory search result cache with TTL."""
17
+
18
+ def __init__(self, ttl_seconds: int = 3600, max_entries: int = 500):
19
+ self._ttl = ttl_seconds
20
+ self._max_entries = max_entries
21
+ self._cache: Dict[str, tuple[float, List[SearchResult]]] = {}
22
+
23
+ def _key(self, query: str) -> str:
24
+ return hashlib.sha256(query.lower().strip().encode()).hexdigest()
25
+
26
+ def get(self, query: str) -> Optional[List[SearchResult]]:
27
+ key = self._key(query)
28
+ entry = self._cache.get(key)
29
+ if entry is None:
30
+ return None
31
+ timestamp, results = entry
32
+ if time.time() - timestamp > self._ttl:
33
+ del self._cache[key]
34
+ return None
35
+ return results
36
+
37
+ def put(self, query: str, results: List[SearchResult]) -> None:
38
+ # Evict oldest entries if at capacity
39
+ if len(self._cache) >= self._max_entries:
40
+ oldest_key = min(self._cache, key=lambda k: self._cache[k][0])
41
+ del self._cache[oldest_key]
42
+
43
+ key = self._key(query)
44
+ self._cache[key] = (time.time(), results)
45
+
46
+ def clear(self) -> None:
47
+ self._cache.clear()
48
+
49
+ @property
50
+ def size(self) -> int:
51
+ return len(self._cache)