insika 0.2.0 → 0.7.0

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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +325 -17
  3. data/README.md +52 -11
  4. data/bin/insika +895 -8
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +229 -163
  7. data/docs/API.md +56 -0
  8. data/docs/ARCHITECTURE.md +3 -2
  9. data/docs/ARTIFACTS.md +95 -0
  10. data/docs/BENCHMARK.md +2 -2
  11. data/docs/CHANNELS.md +111 -21
  12. data/docs/CONTEXT.md +132 -19
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +75 -4
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +146 -2
  17. data/docs/FACTS.md +135 -0
  18. data/docs/HARVEST.md +116 -0
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +16 -10
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +65 -6
  23. data/docs/OUTCOMES.md +137 -0
  24. data/docs/PLUGINS.md +51 -6
  25. data/docs/POLICY.md +216 -0
  26. data/docs/REFINEMENT.md +17 -12
  27. data/docs/RELEASING.md +27 -0
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +6 -6
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +79 -2
  32. data/docs/SKILLS.md +13 -4
  33. data/docs/SOAK.md +127 -0
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +220 -27
  36. data/docs/WHY.md +1 -1
  37. data/docs/WORKFLOWS.md +2 -2
  38. data/docs/_includes/head_custom.html +5 -0
  39. data/docs/_includes/title.html +13 -0
  40. data/docs/_sass/color_schemes/insika.scss +32 -0
  41. data/docs/_sass/custom/custom.scss +199 -0
  42. data/docs/_sass/custom/setup.scss +26 -0
  43. data/docs/assets/img/favicon.svg +7 -0
  44. data/docs/assets/img/insika-mark.svg +7 -0
  45. data/docs/core-concepts.md +21 -0
  46. data/docs/domain.md +115 -0
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +9 -6
  49. data/docs/integrate.md +20 -0
  50. data/docs/operate.md +13 -6
  51. data/docs/prompts/ADD-TOOL.md +118 -0
  52. data/docs/prompts/DIAGNOSE-TURN.md +65 -0
  53. data/docs/prompts/GO-LIVE.md +138 -0
  54. data/docs/prompts/RUN-EXAMPLES.md +70 -0
  55. data/docs/reference.md +19 -0
  56. data/docs/ship.md +10 -2
  57. data/docs/start-here.md +18 -0
  58. data/lib/insika/agent_profile.rb +202 -10
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/balloon_splitter.rb +102 -0
  62. data/lib/insika/budget_ledger.rb +34 -3
  63. data/lib/insika/cache_series_store.rb +49 -0
  64. data/lib/insika/channel_delivery.rb +119 -14
  65. data/lib/insika/channels/relay.rb +77 -3
  66. data/lib/insika/channels/web.rb +2 -2
  67. data/lib/insika/chat_builder.rb +105 -2
  68. data/lib/insika/checkpoint_store.rb +16 -0
  69. data/lib/insika/commands/agent_payload.rb +3 -3
  70. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  71. data/lib/insika/commands/cancel_followup.rb +49 -0
  72. data/lib/insika/commands/delete_artifact.rb +35 -0
  73. data/lib/insika/commands/delete_concept.rb +34 -0
  74. data/lib/insika/commands/delete_mcp.rb +6 -2
  75. data/lib/insika/commands/delete_tenant_data.rb +107 -0
  76. data/lib/insika/commands/export_customer_memory.rb +48 -0
  77. data/lib/insika/commands/forget_customer.rb +117 -0
  78. data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
  79. data/lib/insika/commands/gate_harvest.rb +138 -0
  80. data/lib/insika/commands/gate_refinement.rb +1 -1
  81. data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
  82. data/lib/insika/commands/memory_forget_fact.rb +20 -4
  83. data/lib/insika/commands/memory_put_fact.rb +23 -4
  84. data/lib/insika/commands/promote_harvest.rb +130 -0
  85. data/lib/insika/commands/record_outcome.rb +46 -0
  86. data/lib/insika/commands/record_shadow_reply.rb +68 -0
  87. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  88. data/lib/insika/commands/reject_harvest.rb +38 -0
  89. data/lib/insika/commands/resolve_proposal.rb +108 -0
  90. data/lib/insika/commands/restore_concept.rb +34 -0
  91. data/lib/insika/commands/revoke_contact.rb +49 -0
  92. data/lib/insika/commands/rollback_harvest.rb +86 -0
  93. data/lib/insika/commands/run_distillation.rb +186 -0
  94. data/lib/insika/commands/run_harvest.rb +393 -0
  95. data/lib/insika/commands/seed_demo_data.rb +31 -0
  96. data/lib/insika/commands/send_message.rb +103 -6
  97. data/lib/insika/commands/session_purge.rb +67 -0
  98. data/lib/insika/commands/upsert_mcp.rb +6 -3
  99. data/lib/insika/commands/write_concept.rb +57 -0
  100. data/lib/insika/contact_store.rb +183 -0
  101. data/lib/insika/context/builder.rb +21 -3
  102. data/lib/insika/context/fragment.rb +7 -3
  103. data/lib/insika/context/priority.rb +5 -0
  104. data/lib/insika/context/provider.rb +17 -3
  105. data/lib/insika/context/providers/briefing.rb +96 -0
  106. data/lib/insika/context/providers/knowledge.rb +108 -0
  107. data/lib/insika/context/providers/memory.rb +16 -7
  108. data/lib/insika/context/providers/prompt.rb +57 -23
  109. data/lib/insika/context/providers/skill.rb +2 -0
  110. data/lib/insika/context/providers/tool_search.rb +2 -0
  111. data/lib/insika/context_trace_store.rb +38 -2
  112. data/lib/insika/cron.rb +189 -0
  113. data/lib/insika/demo/agent_attrs.rb +43 -0
  114. data/lib/insika/demo/golden_cases.rb +81 -0
  115. data/lib/insika/demo/seeder.rb +336 -0
  116. data/lib/insika/distill.rb +224 -0
  117. data/lib/insika/distill_engine.rb +169 -0
  118. data/lib/insika/doctor.rb +889 -11
  119. data/lib/insika/dsl/definition.rb +3 -2
  120. data/lib/insika/dsl/runtime.rb +69 -82
  121. data/lib/insika/dsl/server_boot.rb +92 -1
  122. data/lib/insika/dsl/system.rb +10 -2
  123. data/lib/insika/dsl.rb +186 -5
  124. data/lib/insika/edge_limiter.rb +28 -7
  125. data/lib/insika/env_schema.rb +25 -3
  126. data/lib/insika/errors.rb +11 -0
  127. data/lib/insika/evals/assertions.rb +3 -2
  128. data/lib/insika/evals/golden.rb +41 -4
  129. data/lib/insika/evals/judge.rb +47 -2
  130. data/lib/insika/evals/pairwise.rb +29 -6
  131. data/lib/insika/evals/persona.rb +98 -0
  132. data/lib/insika/evals/runner.rb +9 -0
  133. data/lib/insika/evals/simulator.rb +225 -0
  134. data/lib/insika/evals/transport.rb +83 -1
  135. data/lib/insika/event_stream.rb +10 -0
  136. data/lib/insika/evidence.rb +183 -0
  137. data/lib/insika/executor.rb +869 -76
  138. data/lib/insika/followup_engine.rb +207 -0
  139. data/lib/insika/followup_policy.rb +198 -0
  140. data/lib/insika/followup_store.rb +306 -0
  141. data/lib/insika/funnel_declaration.rb +106 -0
  142. data/lib/insika/funnel_fold.rb +179 -0
  143. data/lib/insika/funnel_store.rb +163 -0
  144. data/lib/insika/golden_store.rb +17 -2
  145. data/lib/insika/grounding/matcher.rb +69 -0
  146. data/lib/insika/grounding.rb +44 -0
  147. data/lib/insika/harvest/conversion_gate.rb +159 -0
  148. data/lib/insika/harvest/criterion.rb +98 -0
  149. data/lib/insika/harvest/gate.rb +194 -0
  150. data/lib/insika/harvest/negative_list.rb +199 -0
  151. data/lib/insika/harvest.rb +241 -0
  152. data/lib/insika/harvest_engine.rb +193 -0
  153. data/lib/insika/harvest_store.rb +548 -0
  154. data/lib/insika/knowledge.rb +680 -0
  155. data/lib/insika/knowledge_store.rb +140 -0
  156. data/lib/insika/mcp_client.rb +94 -0
  157. data/lib/insika/mcp_json.rb +74 -0
  158. data/lib/insika/mcp_live_tool.rb +43 -0
  159. data/lib/insika/mcp_store.rb +98 -26
  160. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  161. data/lib/insika/mcp_tool_registry.rb +100 -0
  162. data/lib/insika/media.rb +382 -0
  163. data/lib/insika/memory_audit_store.rb +85 -0
  164. data/lib/insika/memory_store.rb +264 -23
  165. data/lib/insika/message_origin.rb +7 -2
  166. data/lib/insika/middleware.rb +9 -0
  167. data/lib/insika/model_visible.rb +87 -0
  168. data/lib/insika/model_visible_trace_store.rb +66 -0
  169. data/lib/insika/onboarding.rb +22 -1
  170. data/lib/insika/outbox_store.rb +42 -4
  171. data/lib/insika/outcome_store.rb +147 -0
  172. data/lib/insika/overlay_tool_registry.rb +37 -17
  173. data/lib/insika/packaging.rb +163 -0
  174. data/lib/insika/parity/criterion.rb +79 -0
  175. data/lib/insika/parity/verdict.rb +318 -0
  176. data/lib/insika/prefix_fingerprint.rb +58 -0
  177. data/lib/insika/profile_source.rb +29 -1
  178. data/lib/insika/prompt_catalog.rb +10 -0
  179. data/lib/insika/proposal_store.rb +271 -0
  180. data/lib/insika/queue_policy.rb +4 -1
  181. data/lib/insika/refinement/proposer.rb +1 -1
  182. data/lib/insika/reliability.rb +32 -6
  183. data/lib/insika/retention.rb +316 -0
  184. data/lib/insika/router/app.rb +157 -0
  185. data/lib/insika/router/backend_pool.rb +98 -0
  186. data/lib/insika/router/hash_ring.rb +55 -0
  187. data/lib/insika/router/proxy_body.rb +34 -0
  188. data/lib/insika/router/session_key.rb +54 -0
  189. data/lib/insika/router.rb +18 -0
  190. data/lib/insika/routing.rb +101 -0
  191. data/lib/insika/safety/config.rb +44 -4
  192. data/lib/insika/safety/corpus.rb +255 -0
  193. data/lib/insika/safety/detectors.rb +32 -113
  194. data/lib/insika/safety/factory.rb +15 -2
  195. data/lib/insika/safety/grounding_enforcer.rb +59 -0
  196. data/lib/insika/safety/grounding_validator.rb +49 -0
  197. data/lib/insika/safety/input_guardrail.rb +1 -1
  198. data/lib/insika/safety/moderator.rb +1 -1
  199. data/lib/insika/safety/output_filter.rb +9 -5
  200. data/lib/insika/safety/output_validator.rb +11 -5
  201. data/lib/insika/schedule.rb +177 -0
  202. data/lib/insika/schedule_engine.rb +314 -0
  203. data/lib/insika/schedule_store.rb +208 -0
  204. data/lib/insika/schema_guard.rb +35 -0
  205. data/lib/insika/server/app.rb +247 -17
  206. data/lib/insika/server/rack_app.rb +21 -1
  207. data/lib/insika/server/responses.rb +40 -1
  208. data/lib/insika/session_actor.rb +8 -4
  209. data/lib/insika/session_store.rb +65 -2
  210. data/lib/insika/settings_store.rb +10 -0
  211. data/lib/insika/shadow_pair_store.rb +258 -0
  212. data/lib/insika/skill_catalog.rb +16 -0
  213. data/lib/insika/soak/envelope.rb +140 -0
  214. data/lib/insika/soak/report.rb +392 -0
  215. data/lib/insika/soak/runner.rb +554 -0
  216. data/lib/insika/steer_injector.rb +21 -10
  217. data/lib/insika/store.rb +10 -1
  218. data/lib/insika/stores/memory.rb +6 -0
  219. data/lib/insika/stores/sqlite.rb +8 -0
  220. data/lib/insika/studio/app.rb +1437 -54
  221. data/lib/insika/studio/assets/dist/application.css +1 -1
  222. data/lib/insika/studio/assets/dist/application.js +23 -23
  223. data/lib/insika/studio/assets/dist/favicon.svg +6 -0
  224. data/lib/insika/studio/forms.rb +308 -10
  225. data/lib/insika/studio/nav_icons.rb +19 -1
  226. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  227. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  228. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  229. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  230. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  231. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  232. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  233. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  234. data/lib/insika/studio/views/_agents_master.erb +44 -0
  235. data/lib/insika/studio/views/_message.erb +49 -32
  236. data/lib/insika/studio/views/agent_detail.erb +62 -278
  237. data/lib/insika/studio/views/agents.erb +71 -54
  238. data/lib/insika/studio/views/approvals.erb +4 -1
  239. data/lib/insika/studio/views/artifact.erb +23 -0
  240. data/lib/insika/studio/views/artifacts.erb +59 -0
  241. data/lib/insika/studio/views/chats.erb +4 -1
  242. data/lib/insika/studio/views/customer.erb +94 -0
  243. data/lib/insika/studio/views/customers.erb +32 -0
  244. data/lib/insika/studio/views/evals.erb +6 -3
  245. data/lib/insika/studio/views/facts.erb +133 -0
  246. data/lib/insika/studio/views/followups.erb +125 -0
  247. data/lib/insika/studio/views/funnel.erb +106 -0
  248. data/lib/insika/studio/views/harvest.erb +234 -0
  249. data/lib/insika/studio/views/home.erb +106 -66
  250. data/lib/insika/studio/views/knowledge.erb +123 -0
  251. data/lib/insika/studio/views/layout.erb +15 -11
  252. data/lib/insika/studio/views/mcp.erb +174 -80
  253. data/lib/insika/studio/views/parity.erb +147 -0
  254. data/lib/insika/studio/views/playground.erb +7 -1
  255. data/lib/insika/studio/views/session.erb +233 -124
  256. data/lib/insika/studio/views/settings.erb +41 -1
  257. data/lib/insika/studio/views/skills.erb +1 -2
  258. data/lib/insika/studio/views/system_files.erb +1 -1
  259. data/lib/insika/studio/views/task.erb +13 -0
  260. data/lib/insika/studio/views/tasks.erb +4 -1
  261. data/lib/insika/studio/views/tools.erb +24 -10
  262. data/lib/insika/task_store.rb +21 -1
  263. data/lib/insika/templates/browser-agent/README.md +36 -0
  264. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  265. data/lib/insika/templates/daily-digest/README.md +38 -0
  266. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  267. data/lib/insika/templates/repo-explorer/README.md +36 -0
  268. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  269. data/lib/insika/templates/research-analyst/README.md +26 -0
  270. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  271. data/lib/insika/templates/review-panel/README.md +20 -0
  272. data/lib/insika/templates/review-panel/agent.rb +50 -0
  273. data/lib/insika/templates/travel-planner/README.md +35 -0
  274. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  275. data/lib/insika/templates.rb +112 -0
  276. data/lib/insika/testing/store_contract.rb +27 -6
  277. data/lib/insika/tick.rb +47 -11
  278. data/lib/insika/timezone.rb +45 -0
  279. data/lib/insika/tool_definition.rb +17 -7
  280. data/lib/insika/tool_envelope.rb +69 -0
  281. data/lib/insika/tool_manifest.rb +5 -1
  282. data/lib/insika/tools/data_defined_tool.rb +10 -0
  283. data/lib/insika/tools/generate_image.rb +89 -0
  284. data/lib/insika/tools/load_knowledge.rb +74 -0
  285. data/lib/insika/tools/run_persona_eval.rb +328 -0
  286. data/lib/insika/tools/save_artifact.rb +95 -0
  287. data/lib/insika/tools/schedule_followup.rb +164 -0
  288. data/lib/insika/tools/tts.rb +47 -0
  289. data/lib/insika/tools/update_briefing.rb +126 -0
  290. data/lib/insika/turn_output.rb +1 -1
  291. data/lib/insika/turn_state.rb +49 -1
  292. data/lib/insika/turn_timing.rb +22 -2
  293. data/lib/insika/version.rb +1 -1
  294. data/lib/insika/vitals.rb +84 -0
  295. data/lib/insika/wiring/graph.rb +472 -13
  296. data/lib/insika/wiring/graph_chat.rb +102 -0
  297. data/lib/insika.rb +149 -3
  298. metadata +181 -5
  299. data/docs/build.md +0 -14
  300. data/docs/understand.md +0 -10
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ module Demo
5
+ # The one agent the demo seed provisions: a fictional e-commerce
6
+ # support agent that declares BOTH a funnel and a follow-up policy, which
7
+ # nothing shipped in examples/ does (Funnel/Followups render an empty state
8
+ # without a declaring profile — see AgentProfile#funnel/#followup). Model
9
+ # is deliberately nil: a demo about seeded DATA needs no live LLM
10
+ # credentials to be useful, and AgentProfile.build resolves a missing
11
+ # model against the platform default at turn time (never at seed time).
12
+ AGENT_ID = "demo-store"
13
+
14
+ AGENT_ATTRS = {
15
+ id: AGENT_ID,
16
+ base_prompt: <<~PROMPT.strip,
17
+ You are the support agent for Demo Shop, a fictional online store used
18
+ to explore the Insika Studio. Help customers find products, track
19
+ orders and answer questions about shipping and returns.
20
+ PROMPT
21
+ memory: true,
22
+ metadata: { "demo" => true },
23
+ funnel: {
24
+ "stages" => %w[greeted browsing cart_started checkout_started purchased],
25
+ "advance_on" => {
26
+ "greeted" => "greeted", "browsing" => "browsing",
27
+ "cart_started" => "cart_started", "checkout_started" => "checkout_started",
28
+ "purchased" => "purchased"
29
+ },
30
+ "primary" => "purchased",
31
+ "attribution_window" => "72h"
32
+ },
33
+ followup: {
34
+ "arm" => "nudge",
35
+ "policy" => {
36
+ "max_frequency" => "2/24h",
37
+ "cancel_keywords" => ["stop contacting me"],
38
+ "silence_after_sends" => 3
39
+ }
40
+ }
41
+ }.freeze
42
+ end
43
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Insika
4
+ module Demo
5
+ # Golden cases for the demo agent (AGENT_ID), shaped exactly like the
6
+ # evals/golden/**/*.yml corpus (GoldenStore#write validates through the
7
+ # same Evals::GoldenLoader) — bundled here, not on disk, because
8
+ # evals/golden/ never ships in the gem (Packaging.payload_path?).
9
+ GOLDEN_CASES = [
10
+ {
11
+ id: "demo-store-greeting",
12
+ agent: AGENT_ID,
13
+ turns: [{ user: "hi, are you open?" }],
14
+ expect: {
15
+ must_not: ["pii_leak"],
16
+ rubric: "Greets the customer and asks what they're looking for, " \
17
+ "without inventing store hours it wasn't given.",
18
+ min_score: 0.7
19
+ }
20
+ },
21
+ {
22
+ id: "demo-store-find-product",
23
+ agent: AGENT_ID,
24
+ turns: [{ user: "I'm looking for a pair of running shoes" }],
25
+ expect: {
26
+ must_not: %w[pii_leak tool_error],
27
+ rubric: "Asks a qualifying question (size, budget) or offers to " \
28
+ "search the catalog — never invents a specific product " \
29
+ "or price.",
30
+ min_score: 0.7
31
+ }
32
+ },
33
+ {
34
+ id: "demo-store-order-status",
35
+ agent: AGENT_ID,
36
+ turns: [{ user: "where is my order #10234?" }],
37
+ expect: {
38
+ must_not: ["pii_leak"],
39
+ rubric: "Acknowledges the order number and asks for or offers to " \
40
+ "check shipping status, without fabricating a delivery date.",
41
+ min_score: 0.65
42
+ }
43
+ },
44
+ {
45
+ id: "demo-store-return-policy",
46
+ agent: AGENT_ID,
47
+ turns: [{ user: "can I return an item I bought last week?" }],
48
+ expect: {
49
+ must_not: ["pii_leak"],
50
+ rubric: "Explains that returns are handled and asks for the order " \
51
+ "number, without stating a specific return window unless " \
52
+ "one was provided.",
53
+ min_score: 0.65
54
+ }
55
+ },
56
+ {
57
+ id: "demo-store-angry-customer",
58
+ agent: AGENT_ID,
59
+ turns: [{ user: "this is the second time my package is late, this is ridiculous" }],
60
+ expect: {
61
+ must_not: %w[pii_leak safe_reply],
62
+ rubric: "Acknowledges the frustration, apologizes for the delay " \
63
+ "and offers a concrete next step, without being defensive.",
64
+ min_score: 0.7
65
+ }
66
+ },
67
+ {
68
+ id: "demo-store-off-topic",
69
+ agent: AGENT_ID,
70
+ turns: [{ user: "what's the weather like today?" }],
71
+ expect: {
72
+ must_not: ["pii_leak"],
73
+ rubric: "Redirects politely to what it can help with (orders, " \
74
+ "products, shipping) instead of answering the weather " \
75
+ "question.",
76
+ min_score: 0.6
77
+ }
78
+ }
79
+ ].freeze
80
+ end
81
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module Insika
7
+ module Demo
8
+ # Populates one instance with enough realistic-looking data to see every
9
+ # loop working at once: a funnel with a frozen baseline, follow-ups in all
10
+ # four states, refinement runs across the lifecycle, pending + resolved
11
+ # approvals, distillation proposals/facts, and a golden set with a
12
+ # baseline. Writes ONLY through the same domain-store APIs a real turn
13
+ # would use (OutcomeStore#create + FunnelFold, FollowupStore#create +
14
+ # transitions, ...) — there is no bulk/bypass path, by design (D1: the
15
+ # engine never hard-codes a shortcut for its own demo).
16
+ #
17
+ # `force: false` (default) is a no-op once the demo agent exists — safe
18
+ # to run more than once. `force: true` re-seeds on top: it re-recomputes
19
+ # the funnel cleanly (FunnelFold#recompute always wipes its own pair
20
+ # first) but APPENDS a fresh batch of followups/refinement runs/
21
+ # approvals/proposals/goldens, because none of those stores expose a
22
+ # scoped bulk-delete that a shared "platform" tenant could safely call
23
+ # without risking another agent's data (FollowupStore#purge/
24
+ # ProposalStore#purge are TENANT-wide, not per-agent).
25
+ class Seeder
26
+ FUNNEL_DAYS = 40
27
+
28
+ def initialize(profiles:, store:, session_store:, task_store:, outcome_store:,
29
+ funnel_store:, followup_store:, refinement_store:,
30
+ pending_action_store:, proposal_store:, memory_store:,
31
+ golden_store:, baseline_store:, event_stream:)
32
+ @profiles = profiles
33
+ @store = store
34
+ @session_store = session_store
35
+ @task_store = task_store
36
+ @outcome_store = outcome_store
37
+ @funnel_store = funnel_store
38
+ @followup_store = followup_store
39
+ @refinement_store = refinement_store
40
+ @pending_action_store = pending_action_store
41
+ @proposal_store = proposal_store
42
+ @memory_store = memory_store
43
+ @golden_store = golden_store
44
+ @baseline_store = baseline_store
45
+ @event_stream = event_stream
46
+ end
47
+
48
+ # -> { seeded: false, reason:, agent: } | { seeded: true, agent:, counts: {…} }
49
+ def seed!(force: false)
50
+ existing = @profiles.fetch(Demo::AGENT_ID)
51
+ return { seeded: false, reason: "already_seeded", agent: Demo::AGENT_ID } if existing && !force
52
+
53
+ # A distinct suffix per call: `force: true` on top of an existing
54
+ # batch would otherwise collide on FollowupStore's own dedup rule (one
55
+ # pending record per tenant+agent+customer+reason) and raise.
56
+ @batch = SecureRandom.hex(3)
57
+ seed_agent!(existing)
58
+ # `funnel_outcomes` runs LAST: `seed_followups!` writes one extra
59
+ # "purchased" outcome (the nudge's conversion, for the A/B card) —
60
+ # the fold has to see it, or it sits un-folded until some later pass.
61
+ counts = {
62
+ followups: seed_followups!,
63
+ refinement_runs: seed_refinement!,
64
+ approvals: seed_approvals!,
65
+ distillation_proposals: seed_distillation!,
66
+ golden_cases: seed_evals!,
67
+ funnel_outcomes: seed_sessions_and_funnel!
68
+ }
69
+ { seeded: true, agent: Demo::AGENT_ID, counts: counts }
70
+ end
71
+
72
+ private
73
+
74
+ # --- agent -----------------------------------------------------------
75
+
76
+ def seed_agent!(existing)
77
+ handler = existing ? Insika::Commands::UpdateAgent.new(profile_source: @profiles, event_stream: @event_stream)
78
+ : Insika::Commands::CreateAgent.new(profile_source: @profiles, event_stream: @event_stream)
79
+ type = existing ? :update_agent : :create_agent
80
+ handler.call(Insika::Command.build(type, Demo::AGENT_ATTRS))
81
+ end
82
+
83
+ # --- funnel: FUNNEL_DAYS of outcomes, folded, then frozen -------------
84
+
85
+ def seed_sessions_and_funnel!
86
+ declaration = Insika::FunnelDeclaration.parse!(Demo::AGENT_ATTRS[:funnel])
87
+ created = 0
88
+ FUNNEL_DAYS.downto(1) do |days_ago|
89
+ at = Time.now.utc - (days_ago * 86_400)
90
+ day_counts(at).each { |stage, n| created += record_stage(stage, n, at) }
91
+ end
92
+
93
+ Insika::FunnelFold.new(outcome_store: @outcome_store, funnel_store: @funnel_store,
94
+ profiles: @profiles, store: @store)
95
+ .recompute(tenant: nil, agent: Demo::AGENT_ID, declaration: declaration)
96
+ Insika::Commands::FreezeFunnelBaseline.new(funnel_store: @funnel_store, profiles: @profiles,
97
+ event_stream: @event_stream)
98
+ .call(Insika::Command.build(:freeze_funnel_baseline, { agent: Demo::AGENT_ID }))
99
+ created
100
+ end
101
+
102
+ # A believable e-commerce dropout: each stage keeps a random fraction
103
+ # of the one before it.
104
+ def day_counts(at)
105
+ greeted = rand(4..9)
106
+ browsing = (greeted * rand(0.5..0.7)).round
107
+ cart = (browsing * rand(0.3..0.45)).round
108
+ checkout = (cart * rand(0.45..0.65)).round
109
+ purchased = (checkout * rand(0.55..0.8)).round
110
+ { "greeted" => greeted, "browsing" => browsing, "cart_started" => cart,
111
+ "checkout_started" => checkout, "purchased" => purchased }
112
+ end
113
+
114
+ def record_stage(stage, count, at)
115
+ count.times do
116
+ value = stage == "purchased" ? rand(60.0..320.0).round(2) : 0.0
117
+ @outcome_store.create(tenant: nil, agent: Demo::AGENT_ID, outcome: stage, value: value, at: at)
118
+ end
119
+ count
120
+ end
121
+
122
+ # --- follow-ups: one per state ----------------------------------------
123
+
124
+ def seed_followups!
125
+ pending_followup!(customer: batch_customer(1), reason: "cart_abandoned", arm: "control")
126
+
127
+ nudge_session = new_task_session!("I'll take the blue sneakers after all")
128
+ fire_followup!(customer: batch_customer(2), reason: "cart_abandoned", arm: "nudge",
129
+ session: nudge_session[:session], task: nudge_session[:task])
130
+ @outcome_store.create(tenant: nil, agent: Demo::AGENT_ID, session_id: nudge_session[:session].id,
131
+ outcome: "purchased", value: 89.9)
132
+
133
+ control_session = new_task_session!("thinking about it, maybe later")
134
+ fire_followup!(customer: batch_customer(3), reason: "cart_abandoned", arm: "control",
135
+ session: control_session[:session], task: control_session[:task])
136
+
137
+ cancelled = pending_followup!(customer: batch_customer(4), reason: "no_response", arm: "nudge")
138
+ @followup_store.cancel(id: cancelled.id)
139
+
140
+ blocked = pending_followup!(customer: batch_customer(5), reason: "reminder", arm: "nudge")
141
+ @followup_store.block(id: blocked.id, reason: "quiet_hours")
142
+
143
+ 5
144
+ end
145
+
146
+ # Distinct per seed! call (see @batch) — the store refuses two pending
147
+ # records for the same (tenant, agent, customer, reason) tuple, and a
148
+ # force reseed must not collide with the previous batch's still-pending
149
+ # one.
150
+ def batch_customer(n) = "demo-customer-#{n}-#{@batch}"
151
+
152
+ def pending_followup!(customer:, reason:, arm:)
153
+ @followup_store.create(tenant: nil, agent: Demo::AGENT_ID, customer: customer,
154
+ session_id: @session_store.create.id, at: Time.now.utc + 3600,
155
+ reason: reason, arm: arm, transport: "web")
156
+ end
157
+
158
+ def fire_followup!(customer:, reason:, arm:, session:, task:)
159
+ record = @followup_store.create(tenant: nil, agent: Demo::AGENT_ID, customer: customer,
160
+ session_id: session.id, at: Time.now.utc + 3600,
161
+ reason: reason, arm: arm, transport: "web")
162
+ @followup_store.transition_fired(id: record.id, task_id: task.id)
163
+ finish_task!(task)
164
+ end
165
+
166
+ # --- refinement: one run per stage of the lifecycle -------------------
167
+
168
+ def seed_refinement!
169
+ awaiting_approval_run!
170
+ applied_run!
171
+ rejected_run!
172
+ no_findings_run!
173
+ 4
174
+ end
175
+
176
+ def awaiting_approval_run!
177
+ run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 50 })
178
+ @refinement_store.complete(run.id, findings: [repeated_price_finding])
179
+ candidate = discipline_candidate
180
+ @refinement_store.gating(run.id, candidate: candidate)
181
+ @refinement_store.gated(run.id, report: passing_gate_report(candidate))
182
+ end
183
+
184
+ def applied_run!
185
+ run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 50 })
186
+ @refinement_store.complete(run.id, findings: [repeated_price_finding])
187
+ candidate = discipline_candidate
188
+ @refinement_store.gating(run.id, candidate: candidate)
189
+ @refinement_store.gated(run.id, report: passing_gate_report(candidate))
190
+ @refinement_store.resolve(run.id, decision: :applied, operator: "demo-seed",
191
+ note: "Looks safe — applying to AGENTS.md.")
192
+ end
193
+
194
+ def rejected_run!
195
+ run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 30 })
196
+ @refinement_store.complete(run.id, findings: [off_topic_finding])
197
+ candidate = off_topic_candidate
198
+ @refinement_store.gating(run.id, candidate: candidate)
199
+ @refinement_store.gated(run.id, report: failing_gate_report(candidate))
200
+ end
201
+
202
+ def no_findings_run!
203
+ run = @refinement_store.create(agent_id: Demo::AGENT_ID, window: { "last_sessions" => 20 })
204
+ @refinement_store.complete(run.id, findings: [])
205
+ end
206
+
207
+ def repeated_price_finding
208
+ { "kind" => "repeated_price_without_stock_check", "count" => 4,
209
+ "title" => "Repeats a price without checking stock",
210
+ "detail" => "asked the price of the same item twice; the agent repeated it verbatim " \
211
+ "without a stock check" }
212
+ end
213
+
214
+ def off_topic_finding
215
+ { "kind" => "answers_off_topic_questions", "count" => 2,
216
+ "title" => "Answers questions unrelated to the store",
217
+ "detail" => "answered a question about the weather instead of redirecting to store topics" }
218
+ end
219
+
220
+ def discipline_candidate
221
+ { "id" => SecureRandom.uuid, "proposer" => "demo-seed",
222
+ "rationale" => "Customers who ask about the same product twice get the same price restated " \
223
+ "without a stock check — a stale price can be quoted after a sellout.",
224
+ "edits" => [{ "file" => "AGENTS.md", "op" => "append", "anchor" => nil, "before" => nil,
225
+ "after" => "Always confirm current stock before repeating a price.",
226
+ "addresses" => ["repeated_price_without_stock_check"] }],
227
+ "dropped" => [] }
228
+ end
229
+
230
+ def off_topic_candidate
231
+ { "id" => SecureRandom.uuid, "proposer" => "demo-seed",
232
+ "rationale" => "The agent occasionally answers off-topic questions instead of redirecting.",
233
+ "edits" => [{ "file" => "AGENTS.md", "op" => "append", "anchor" => nil, "before" => nil,
234
+ "after" => "Never answer questions unrelated to the store; redirect politely.",
235
+ "addresses" => ["answers_off_topic_questions"] }],
236
+ "dropped" => [] }
237
+ end
238
+
239
+ def passing_gate_report(candidate)
240
+ { "passed" => true, "candidate_id" => candidate["id"], "cases" => 12, "passed_cases" => 12,
241
+ "regressions" => [] }
242
+ end
243
+
244
+ def failing_gate_report(candidate)
245
+ { "passed" => false, "candidate_id" => candidate["id"], "cases" => 10, "passed_cases" => 7,
246
+ "regressions" => ["demo-store-off-topic"], "reason" => "3 case(s) regressed" }
247
+ end
248
+
249
+ # --- approvals: 2 pending + 1 resolved --------------------------------
250
+
251
+ def seed_approvals!
252
+ [
253
+ ["I want a refund for order #10234, the shoes don't fit", "issue_refund",
254
+ { "order_id" => "10234", "amount" => 89.9 }],
255
+ ["can you apply the WELCOME10 discount code retroactively?", "apply_discount_code",
256
+ { "order_id" => "10391", "code" => "WELCOME10" }]
257
+ ].each do |message, tool, args|
258
+ task = new_task_session!(message)[:task]
259
+ @pending_action_store.create(task_id: task.id, turn: 3, tool: tool, args: args)
260
+ finish_task!(task)
261
+ end
262
+
263
+ task = new_task_session!("please cancel order #10500 and refund me")[:task]
264
+ pending = @pending_action_store.create(task_id: task.id, turn: 2, tool: "cancel_order",
265
+ args: { "order_id" => "10500" })
266
+ @pending_action_store.resolve(pending.id, decision: :approved, operator: "demo-seed")
267
+ finish_task!(task)
268
+
269
+ 3
270
+ end
271
+
272
+ # --- distillation: proposals + one approved fact ----------------------
273
+
274
+ def seed_distillation!
275
+ session6 = distillation_session!("demo-customer-6", "I usually pay with Pix, is that ok here too?")
276
+ @proposal_store.create(tenant: nil, customer: "demo-customer-6", session_ref: session6.id,
277
+ key: "preferred_payment_method", value: "pix", confidence: 0.82, evidence: [0])
278
+
279
+ session7 = distillation_session!("demo-customer-7", "I'm always browsing the electronics section")
280
+ approved = @proposal_store.create(tenant: nil, customer: "demo-customer-7", session_ref: session7.id,
281
+ key: "favorite_category", value: "electronics", confidence: 0.9,
282
+ evidence: [0])
283
+ @proposal_store.approve(id: approved.id, operator: "demo-seed", note: "Confirmed across 3 conversations.")
284
+ @memory_store.put_fact(tenant: nil, customer: "demo-customer-7", key: "favorite_category",
285
+ value: "electronics", origin: "distilled:#{approved.id}")
286
+
287
+ session8 = distillation_session!("demo-customer-8", "I wear a size 42, I think")
288
+ rejected = @proposal_store.create(tenant: nil, customer: "demo-customer-8", session_ref: session8.id,
289
+ key: "shoe_size", value: "42", confidence: 0.4, evidence: [0])
290
+ @proposal_store.reject(id: rejected.id, operator: "demo-seed", note: "Confidence too low, likely a typo.")
291
+
292
+ 3
293
+ end
294
+
295
+ # A real Session (not a bare string) so the Facts page's evidence link
296
+ # resolves — a plain id would 404 on /studio/sessions/:id.
297
+ def distillation_session!(customer, message)
298
+ session = @session_store.create(vars: { "customer" => customer, "agent" => Demo::AGENT_ID })
299
+ @session_store.append_messages(session.id, { "role" => "user", "content" => message })
300
+ session
301
+ end
302
+
303
+ # --- evals: the golden set + a mixed pass/fail baseline ---------------
304
+
305
+ def seed_evals!
306
+ ids = Demo::GOLDEN_CASES.map { |raw| @golden_store.write(raw).id }
307
+ last = ids.size - 1
308
+ cases = ids.each_with_index.to_h do |id, i|
309
+ [id, i == last ? { "pass" => false, "score" => 0.42 } : { "pass" => true, "score" => (0.75 + i * 0.03).round(2) }]
310
+ end
311
+ @baseline_store.put(Demo::AGENT_ID, { "cases" => cases })
312
+ ids.size
313
+ end
314
+
315
+ # --- shared: a Session + one user Task, like a real turn would leave --
316
+
317
+ def new_task_session!(message)
318
+ session = @session_store.create(vars: { "customer" => SecureRandom.uuid, "agent" => Demo::AGENT_ID })
319
+ command = Insika::Command.build(:send_message, { agent: Demo::AGENT_ID, message: message })
320
+ task = @task_store.create(command: command.to_h, session_id: session.id)
321
+ { session: session, task: task }
322
+ end
323
+
324
+ # Lands a fresh task in a TERMINAL status (queued -> running ->
325
+ # completed). A seeded task must never sit at :running/:waiting/:paused
326
+ # (or even :queued) once seeding is done — Recovery's boot sweep reads
327
+ # those as an interrupted turn and either dispatches a real resume (no
328
+ # LLM configured -> a pointless failure) or fails the task outright for
329
+ # want of a checkpoint that was never real. Terminal is inert either way.
330
+ def finish_task!(task)
331
+ @task_store.transition(task.id, to: :running)
332
+ @task_store.transition(task.id, to: :completed)
333
+ end
334
+ end
335
+ end
336
+ end