insika 0.3.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 (190) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -0
  3. data/README.md +45 -10
  4. data/bin/insika +684 -0
  5. data/bin/insika-router +87 -0
  6. data/docs/AGENTS.md +94 -403
  7. data/docs/API.md +5 -5
  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 +14 -14
  12. data/docs/CONTEXT.md +9 -7
  13. data/docs/DEMO.md +80 -0
  14. data/docs/DEPLOY.md +71 -3
  15. data/docs/EMBEDDING.md +1 -1
  16. data/docs/EVALS.md +128 -3
  17. data/docs/FACTS.md +3 -3
  18. data/docs/HARVEST.md +5 -6
  19. data/docs/KNOWLEDGE.md +290 -0
  20. data/docs/LOADTEST.md +2 -2
  21. data/docs/MEDIA.md +128 -0
  22. data/docs/OBSERVABILITY.md +15 -10
  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 +14 -9
  27. data/docs/RELEASING.md +4 -4
  28. data/docs/ROUTER.md +213 -0
  29. data/docs/RUNNING-LOCAL.md +3 -3
  30. data/docs/SCHEDULING.md +121 -0
  31. data/docs/SECURITY.md +22 -6
  32. data/docs/SKILLS.md +11 -2
  33. data/docs/SOAK.md +2 -2
  34. data/docs/TEMPLATES.md +134 -0
  35. data/docs/TOOLS.md +152 -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 +4 -4
  47. data/docs/improve.md +20 -0
  48. data/docs/index.md +8 -5
  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 +73 -16
  59. data/lib/insika/artifact_signing.rb +82 -0
  60. data/lib/insika/artifact_store.rb +160 -0
  61. data/lib/insika/channel_delivery.rb +1 -1
  62. data/lib/insika/chat_builder.rb +22 -2
  63. data/lib/insika/commands/agent_payload.rb +2 -2
  64. data/lib/insika/commands/backfill_knowledge.rb +145 -0
  65. data/lib/insika/commands/delete_artifact.rb +35 -0
  66. data/lib/insika/commands/delete_concept.rb +34 -0
  67. data/lib/insika/commands/delete_mcp.rb +6 -2
  68. data/lib/insika/commands/delete_tenant_data.rb +15 -3
  69. data/lib/insika/commands/gate_refinement.rb +1 -1
  70. data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
  71. data/lib/insika/commands/restore_concept.rb +34 -0
  72. data/lib/insika/commands/seed_demo_data.rb +31 -0
  73. data/lib/insika/commands/upsert_mcp.rb +6 -3
  74. data/lib/insika/commands/write_concept.rb +57 -0
  75. data/lib/insika/context/priority.rb +2 -0
  76. data/lib/insika/context/providers/knowledge.rb +108 -0
  77. data/lib/insika/context/providers/prompt.rb +30 -24
  78. data/lib/insika/cron.rb +189 -0
  79. data/lib/insika/demo/agent_attrs.rb +43 -0
  80. data/lib/insika/demo/golden_cases.rb +81 -0
  81. data/lib/insika/demo/seeder.rb +336 -0
  82. data/lib/insika/doctor.rb +176 -8
  83. data/lib/insika/dsl/definition.rb +3 -2
  84. data/lib/insika/dsl/runtime.rb +60 -79
  85. data/lib/insika/dsl/server_boot.rb +23 -1
  86. data/lib/insika/dsl/system.rb +10 -2
  87. data/lib/insika/dsl.rb +103 -2
  88. data/lib/insika/env_schema.rb +16 -1
  89. data/lib/insika/evals/golden.rb +41 -4
  90. data/lib/insika/evals/judge.rb +47 -2
  91. data/lib/insika/evals/pairwise.rb +11 -0
  92. data/lib/insika/evals/persona.rb +98 -0
  93. data/lib/insika/evals/runner.rb +9 -0
  94. data/lib/insika/evals/simulator.rb +225 -0
  95. data/lib/insika/evals/transport.rb +83 -1
  96. data/lib/insika/event_stream.rb +10 -0
  97. data/lib/insika/executor.rb +231 -55
  98. data/lib/insika/followup_policy.rb +2 -25
  99. data/lib/insika/golden_store.rb +16 -1
  100. data/lib/insika/grounding/matcher.rb +1 -1
  101. data/lib/insika/knowledge.rb +680 -0
  102. data/lib/insika/knowledge_store.rb +140 -0
  103. data/lib/insika/mcp_client.rb +94 -0
  104. data/lib/insika/mcp_json.rb +74 -0
  105. data/lib/insika/mcp_live_tool.rb +43 -0
  106. data/lib/insika/mcp_store.rb +98 -26
  107. data/lib/insika/mcp_tool_ingestor.rb +30 -8
  108. data/lib/insika/mcp_tool_registry.rb +100 -0
  109. data/lib/insika/media.rb +115 -31
  110. data/lib/insika/message_origin.rb +1 -1
  111. data/lib/insika/middleware.rb +9 -0
  112. data/lib/insika/onboarding.rb +17 -1
  113. data/lib/insika/outcome_store.rb +1 -1
  114. data/lib/insika/overlay_tool_registry.rb +37 -17
  115. data/lib/insika/packaging.rb +2 -2
  116. data/lib/insika/profile_source.rb +8 -1
  117. data/lib/insika/prompt_catalog.rb +10 -0
  118. data/lib/insika/retention.rb +36 -1
  119. data/lib/insika/router/app.rb +157 -0
  120. data/lib/insika/router/backend_pool.rb +98 -0
  121. data/lib/insika/router/hash_ring.rb +55 -0
  122. data/lib/insika/router/proxy_body.rb +34 -0
  123. data/lib/insika/router/session_key.rb +54 -0
  124. data/lib/insika/router.rb +18 -0
  125. data/lib/insika/schedule.rb +177 -0
  126. data/lib/insika/schedule_engine.rb +314 -0
  127. data/lib/insika/schedule_store.rb +208 -0
  128. data/lib/insika/server/app.rb +105 -15
  129. data/lib/insika/server/rack_app.rb +5 -1
  130. data/lib/insika/server/responses.rb +1 -1
  131. data/lib/insika/skill_catalog.rb +12 -0
  132. data/lib/insika/steer_injector.rb +21 -10
  133. data/lib/insika/studio/app.rb +567 -45
  134. data/lib/insika/studio/assets/dist/application.css +1 -1
  135. data/lib/insika/studio/assets/dist/application.js +21 -21
  136. data/lib/insika/studio/forms.rb +46 -5
  137. data/lib/insika/studio/nav_icons.rb +14 -1
  138. data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
  139. data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
  140. data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
  141. data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
  142. data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
  143. data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
  144. data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
  145. data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
  146. data/lib/insika/studio/views/_agents_master.erb +44 -0
  147. data/lib/insika/studio/views/_message.erb +49 -32
  148. data/lib/insika/studio/views/agent_detail.erb +61 -820
  149. data/lib/insika/studio/views/agents.erb +70 -57
  150. data/lib/insika/studio/views/artifact.erb +23 -0
  151. data/lib/insika/studio/views/artifacts.erb +59 -0
  152. data/lib/insika/studio/views/evals.erb +2 -2
  153. data/lib/insika/studio/views/facts.erb +1 -1
  154. data/lib/insika/studio/views/funnel.erb +1 -1
  155. data/lib/insika/studio/views/home.erb +106 -67
  156. data/lib/insika/studio/views/knowledge.erb +123 -0
  157. data/lib/insika/studio/views/layout.erb +14 -11
  158. data/lib/insika/studio/views/mcp.erb +174 -80
  159. data/lib/insika/studio/views/session.erb +231 -177
  160. data/lib/insika/studio/views/settings.erb +39 -1
  161. data/lib/insika/studio/views/skills.erb +1 -1
  162. data/lib/insika/studio/views/tools.erb +24 -9
  163. data/lib/insika/templates/browser-agent/README.md +36 -0
  164. data/lib/insika/templates/browser-agent/agent.rb +49 -0
  165. data/lib/insika/templates/daily-digest/README.md +38 -0
  166. data/lib/insika/templates/daily-digest/agent.rb +77 -0
  167. data/lib/insika/templates/repo-explorer/README.md +36 -0
  168. data/lib/insika/templates/repo-explorer/agent.rb +45 -0
  169. data/lib/insika/templates/research-analyst/README.md +26 -0
  170. data/lib/insika/templates/research-analyst/agent.rb +58 -0
  171. data/lib/insika/templates/review-panel/README.md +20 -0
  172. data/lib/insika/templates/review-panel/agent.rb +50 -0
  173. data/lib/insika/templates/travel-planner/README.md +35 -0
  174. data/lib/insika/templates/travel-planner/agent.rb +87 -0
  175. data/lib/insika/templates.rb +112 -0
  176. data/lib/insika/tick.rb +24 -12
  177. data/lib/insika/timezone.rb +45 -0
  178. data/lib/insika/tools/generate_image.rb +52 -7
  179. data/lib/insika/tools/load_knowledge.rb +74 -0
  180. data/lib/insika/tools/run_persona_eval.rb +328 -0
  181. data/lib/insika/tools/save_artifact.rb +95 -0
  182. data/lib/insika/turn_output.rb +1 -1
  183. data/lib/insika/turn_state.rb +15 -4
  184. data/lib/insika/version.rb +1 -1
  185. data/lib/insika/wiring/graph.rb +184 -12
  186. data/lib/insika/wiring/graph_chat.rb +102 -0
  187. data/lib/insika.rb +57 -0
  188. metadata +105 -5
  189. data/docs/build.md +0 -14
  190. data/docs/understand.md +0 -10
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Browser Agent
5
+ # trail: MCP
6
+ # description: Live MCP tool-loop over stdio — navigates and summarizes a real webpage via Playwright's MCP server. Requires Node.js/npm and INSIKA_MCP_STDIO=1.
7
+ # capabilities: mcp, stdio
8
+ # env: INSIKA_MCP_STDIO
9
+ # requires: Node.js and npm (npx spawns the MCP server as a child process)
10
+ # ---
11
+ #
12
+ # browser-agent — a live MCP tool-loop over STDIO: the agent
13
+ # drives a real, sandboxed browser through Playwright's MCP server
14
+ # (@playwright/mcp, no API key). stdio is arbitrary command execution by
15
+ # config, so it needs the operator's explicit opt-in (INSIKA_MCP_STDIO=1)
16
+ # and Node.js/npm on the machine — the two things this template needs
17
+ # beyond the gem and a provider key. Swap MCP_COMMAND/MCP_ARGS for any other
18
+ # MCP server and nothing else in this file changes.
19
+ #
20
+ # INSIKA_MCP_STDIO=1 DEEPSEEK_API_KEY=sk-... ruby browser-agent/agent.rb "go to example.com and summarize the page"
21
+ require "insika"
22
+ require "shellwords"
23
+
24
+ browser = Insika.agent("browser-agent") do
25
+ model "deepseek-v4-flash"
26
+ provider :deepseek
27
+
28
+ instructions <<~PROMPT
29
+ You browse the web using the browser MCP tools. Navigate to the
30
+ requested page, then extract or summarize what was asked. Never invent
31
+ page content — always navigate and read it first.
32
+ PROMPT
33
+
34
+ mcp "browser", transport: :stdio,
35
+ command: ENV.fetch("MCP_COMMAND", "npx"),
36
+ args: Shellwords.split(ENV.fetch("MCP_ARGS", "-y @playwright/mcp@latest"))
37
+ end
38
+
39
+ if __FILE__ == $PROGRAM_NAME
40
+ if ARGV.delete("--serve")
41
+ browser.serve
42
+ else
43
+ message = ARGV.join(" ")
44
+ message = "Go to https://example.com and summarize the page in two sentences." if message.empty?
45
+ puts browser.reply(message)
46
+ end
47
+ end
48
+
49
+ browser
@@ -0,0 +1,38 @@
1
+ # daily-digest
2
+
3
+ **Always-on trail.** The report pipeline in one file: a recurring
4
+ `schedule` (the engine's own tick fires it), a `save_artifact`
5
+ tool (the report destination), and a store-free skill describing
6
+ *how* to build the report. No database, no store content — the day's
7
+ numbers are fake, inline text.
8
+
9
+ ```bash
10
+ DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb
11
+ ```
12
+
13
+ Expected output: a reply ending in `Report: /studio/artifacts/<id>`.
14
+
15
+ ```bash
16
+ DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb --serve
17
+ ```
18
+
19
+ Then open `/studio`, log in with the printed token, and either wait for the
20
+ schedule (22:00 America/Sao_Paulo) or send a message to the "reporter" agent
21
+ in the Playground — the artifact lands on the Artifacts tab either way.
22
+
23
+ ## What's real here
24
+
25
+ - The **schedule** is a live engine feature: the tick actually fires this
26
+ turn at the cron time, in the same process, no external cron needed.
27
+ - The **artifact** is a real signed/authenticated URL the report is saved
28
+ to — open it and the HTML renders with its own strict CSP (`default-src
29
+ 'none'`), so the inline SVG chart has to be self-contained by
30
+ construction, not by convention.
31
+ - The **numbers** are not. Swap the literal string in `agent.rb` for a
32
+ `data_tool` against your own sales API and nothing else changes.
33
+
34
+ ## Edit it
35
+
36
+ The skill's instructions are the actual report spec — change the palette,
37
+ add a second table, or add another chart. The schedule's `cron`/`tz` are
38
+ plain arguments.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Daily Digest
5
+ # trail: Always-on
6
+ # description: A recurring schedule plus save_artifact build and publish a self-contained HTML report — no store, fake in-memory numbers only.
7
+ # capabilities: schedule, save_artifact, skill
8
+ # ---
9
+ #
10
+ # daily-digest — the report pipeline in one file: a recurring schedule (the
11
+ # engine's own tick fires it), a daily-digest skill (the inline-SVG
12
+ # pattern), the save_artifact tool (the report destination) and the
13
+ # per-agent allowlist that gates it. No store content — fake, in-memory
14
+ # "sales" numbers only.
15
+ #
16
+ # DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb
17
+ # DEEPSEEK_API_KEY=sk-... ruby daily-digest/agent.rb --serve
18
+ # # --serve: open /studio, log in with the printed token, then send a
19
+ # # message to the "reporter" agent in the Playground — the artifact
20
+ # # lands on the Artifacts tab. The schedule fires the same run at
21
+ # # 22:00 America/Sao_Paulo on its own.
22
+ require "insika"
23
+
24
+ todays_sales = "Coffee 128 units ($512), Tea 64 units ($192), Pastries 40 units ($160)."
25
+
26
+ reporter = Insika.agent("reporter") do
27
+ model "deepseek-v4-flash"
28
+ provider :deepseek
29
+
30
+ instructions <<~PROMPT
31
+ You produce the daily sales digest from the numbers given to you in the
32
+ message. Follow the daily-digest skill exactly, then save the finished
33
+ page with save_artifact (HTML with inline SVG). End your reply with the
34
+ artifact url, on its own line, prefixed with "Report: ".
35
+ PROMPT
36
+
37
+ # The per-agent allowlist IS the switch for save_artifact — without this
38
+ # line the model never even sees the tool.
39
+ tools %w[save_artifact]
40
+
41
+ # A recurring turn — the engine's tick fires it. session_mode:
42
+ # "new" = a fresh session per run (the report shape); the overrides raise
43
+ # the chat-time ceilings a real report needs.
44
+ schedule "daily_report", cron: "0 22 * * *", tz: "America/Sao_Paulo",
45
+ message: "Run the daily report now. Today's sales: #{todays_sales}",
46
+ session_mode: "new",
47
+ overrides: { turn_timeout: 600, max_tool_calls: 120 }
48
+
49
+ # The generic, store-free skill: how a report is BUILT — the inline-SVG
50
+ # pattern (palette, table, pure-SVG bars, light/dark). No store ids, no
51
+ # queries — that half belongs in a merchant's pack, not here.
52
+ skill "daily-digest",
53
+ description: "How to build the daily sales digest as a self-contained HTML report",
54
+ instructions: <<~MD
55
+ Build the digest as ONE self-contained HTML page:
56
+ - a <style> block with a light palette (e.g. #f8fafc bg, #0f172a text,
57
+ #6366f1 accent), plus a prefers-color-scheme: dark override;
58
+ - one <table> of the numbers (th/tbody, right-aligned amounts);
59
+ - one inline SVG bar chart (no <script>, no <img>, no <iframe>,
60
+ no external fonts or fetches — the report is served with
61
+ default-src 'none', so nothing external can load);
62
+ - a title and the run date.
63
+ Keep it readable on a phone. The report is the deliverable; the
64
+ channel message is just the link to it.
65
+ MD
66
+ end
67
+
68
+ if __FILE__ == $PROGRAM_NAME
69
+ if ARGV.delete("--serve")
70
+ reporter.serve
71
+ else
72
+ puts reporter.reply("Run the daily report now. Today's sales: #{todays_sales}")
73
+ puts "\nThen open the returned /studio/artifacts/<id> page (or --serve and look at the Artifacts tab)."
74
+ end
75
+ end
76
+
77
+ reporter
@@ -0,0 +1,36 @@
1
+ # repo-explorer
2
+
3
+ **MCP trail (http).** A live MCP tool-loop over Streamable HTTP:
4
+ the agent calls a real, running MCP server's tools — not a snapshot, not a
5
+ hand-rolled HTTP wrapper. The default target is
6
+ [DeepWiki's public MCP server](https://mcp.deepwiki.com/mcp), which needs
7
+ no API key for public repos.
8
+
9
+ ```bash
10
+ DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "how does rails/rails route a request?"
11
+ ```
12
+
13
+ This is not a DeepWiki showcase — it's exactly how you plug **any** MCP
14
+ server into an agent:
15
+
16
+ ```bash
17
+ MCP_URL=https://your-mcp-server/mcp DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "..."
18
+ ```
19
+
20
+ Point `MCP_URL` at any Streamable HTTP or SSE MCP server and nothing else
21
+ in `agent.rb` changes — swap the URL, rewrite the instructions for the new
22
+ server's tools, done.
23
+
24
+ ## Under the hood
25
+
26
+ `mcp "repo-docs", transport: :http, url: …` declares the instance; the
27
+ engine connects live, discovers its tools (`read_wiki_structure`,
28
+ `read_wiki_contents`, `ask_question`), and wires them straight into the
29
+ agent's tool-loop with group `mcp:repo-docs`. Declaring an `mcp` inside an
30
+ agent's block auto-grants that agent access to the group — see
31
+ `lib/insika/dsl.rb`'s `mcp` method if you're curious why that matters.
32
+
33
+ A public HTTPS target needs no extra configuration (same egress guard as
34
+ any data-tool). A target on a private network needs the same
35
+ `INSIKA_EGRESS_ALLOW_PRIVATE`/`INSIKA_EGRESS_HOSTS` env vars a data-tool
36
+ would.
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Repo Explorer
5
+ # trail: MCP
6
+ # description: Live MCP tool-loop over http — answers questions about any public GitHub repo via a keyless public MCP server. Point MCP_URL at any other MCP server instead.
7
+ # capabilities: mcp, http
8
+ # ---
9
+ #
10
+ # repo-explorer — a live MCP tool-loop over HTTP: the agent calls
11
+ # a real MCP server's tools (read_wiki_structure, read_wiki_contents,
12
+ # ask_question) to answer questions about a public GitHub repo. The default
13
+ # target is DeepWiki's public, keyless MCP server — swap MCP_URL for any
14
+ # other MCP server and nothing else in this file changes.
15
+ #
16
+ # DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "how does rails/rails route a request?"
17
+ # MCP_URL=https://your-mcp-server/mcp DEEPSEEK_API_KEY=sk-... ruby repo-explorer/agent.rb "..."
18
+ require "insika"
19
+
20
+ repo = Insika.agent("repo-explorer") do
21
+ model "deepseek-v4-flash"
22
+ provider :deepseek
23
+
24
+ instructions <<~PROMPT
25
+ You answer questions about public GitHub repositories using the
26
+ repo-docs MCP tools (read_wiki_structure, read_wiki_contents,
27
+ ask_question). Repos are named "owner/repo" (e.g. "rails/rails"). Never
28
+ answer from your own training data when a tool can check — call
29
+ ask_question first.
30
+ PROMPT
31
+
32
+ mcp "repo-docs", transport: :http, url: ENV.fetch("MCP_URL", "https://mcp.deepwiki.com/mcp")
33
+ end
34
+
35
+ if __FILE__ == $PROGRAM_NAME
36
+ if ARGV.delete("--serve")
37
+ repo.serve
38
+ else
39
+ message = ARGV.join(" ")
40
+ message = "In the rails/rails repo, how does routing work? One paragraph." if message.empty?
41
+ puts repo.reply(message)
42
+ end
43
+ end
44
+
45
+ repo
@@ -0,0 +1,26 @@
1
+ # research-analyst
2
+
3
+ **Advanced trail.** Four agents in one `Insika.system`: three specialists
4
+ (market, technical, risk) and a lead ("analyst") that has no expertise of
5
+ its own and must delegate. The MODEL decides to fan out — nothing in Ruby
6
+ orchestrates the parallel calls.
7
+
8
+ ```bash
9
+ DEEPSEEK_API_KEY=sk-... ruby research-analyst/agent.rb "a subscription box for specialty coffee"
10
+ ```
11
+
12
+ Under the hood: the lead calls `spawn_subagents` once with all three
13
+ specialist ids, they run **in parallel** (each in an isolated context — a
14
+ child never sees the parent's conversation), and the lead synthesizes their
15
+ three answers into one recommendation.
16
+
17
+ Nothing forces the model to delegate — that's the trade of a model-driven
18
+ pattern over a hand-coded workflow. If it answers alone instead, the fix is
19
+ the lead's prompt, not the code: it needs to be told, plainly, that it has
20
+ no expertise of its own.
21
+
22
+ ## Edit it
23
+
24
+ Add a fourth specialist (`agent("competitors") { … }`, then add it to
25
+ `subagents`), or turn any specialist into a `Insika.agent` with its own
26
+ data-tools — a subagent is an ordinary agent, capability included.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Research Analyst
5
+ # trail: Advanced
6
+ # description: Insika.system fan-out — three specialist subagents research different angles of a topic in parallel, the lead delegates and synthesizes.
7
+ # capabilities: subagents, delegation, system
8
+ # ---
9
+ #
10
+ # research-analyst — a lead agent with no expertise of its own: it must
11
+ # delegate. spawn_subagents fans out to three specialists on separate angles
12
+ # of a business idea, IN PARALLEL (each in its own isolated context), then
13
+ # the lead synthesizes one recommendation.
14
+ #
15
+ # DEEPSEEK_API_KEY=sk-... ruby research-analyst/agent.rb "a subscription box for specialty coffee"
16
+ # DEEPSEEK_API_KEY=sk-... ruby research-analyst/agent.rb --serve
17
+ require "insika"
18
+
19
+ team = Insika.system do
20
+ provider :deepseek
21
+
22
+ agent("market") do
23
+ model "deepseek-v4-flash"
24
+ instructions "Research the MARKET angle of a business idea: audience, demand, competitors. Three sentences."
25
+ end
26
+ agent("technical") do
27
+ model "deepseek-v4-flash"
28
+ instructions "Research the TECHNICAL/OPERATIONAL angle of a business idea: what it takes to build and run it. Three sentences."
29
+ end
30
+ agent("risk") do
31
+ model "deepseek-v4-flash"
32
+ instructions "Research the RISK angle of a business idea: what could make it fail. Three sentences."
33
+ end
34
+
35
+ agent "analyst" do
36
+ model "deepseek-v4-flash"
37
+ instructions <<~PROMPT
38
+ You are a research LEAD with no expertise of your own — never answer
39
+ from your own knowledge. Given a business idea, call spawn_subagents
40
+ once with all three specialists (market, technical, risk), then
41
+ synthesize their findings into one short recommendation: go, no-go, or
42
+ go-with-changes, and why.
43
+ PROMPT
44
+ subagents "market", "technical", "risk"
45
+ end
46
+ end
47
+
48
+ if __FILE__ == $PROGRAM_NAME
49
+ if ARGV.delete("--serve")
50
+ team.serve
51
+ else
52
+ topic = ARGV.join(" ")
53
+ topic = "a subscription box for specialty coffee" if topic.empty?
54
+ puts team.reply("analyst", "Research this idea: #{topic}")
55
+ end
56
+ end
57
+
58
+ team
@@ -0,0 +1,20 @@
1
+ # review-panel
2
+
3
+ **Teams trail.** The `Insika.system` snippet from the gem's main examples
4
+ README, promoted to a runnable template: a "reviewer" lead with no
5
+ expertise of its own delegates to two specialists — security and
6
+ performance — IN PARALLEL, then synthesizes one prioritized fix.
7
+
8
+ ```bash
9
+ DEEPSEEK_API_KEY=sk-... ruby review-panel/agent.rb
10
+ ```
11
+
12
+ `panel.reply("reviewer", …)` — the target agent is always explicit; with
13
+ several agents in one system, inferring which one should answer would be a
14
+ guess, and a wrong guess is a silently wrong conversation.
15
+
16
+ ## Edit it
17
+
18
+ Add a third specialist (a `style` reviewer, say), list it in `subagents`,
19
+ and the lead's synthesis prompt already generalizes — it doesn't name the
20
+ specialists, just says "both".
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Review Panel
5
+ # trail: Teams
6
+ # description: Two specialists reviewed in parallel by a synthesizing lead, explicit target agent (Insika.system + subagents).
7
+ # capabilities: subagents, delegation, system
8
+ # ---
9
+ #
10
+ # review-panel — promotes the examples/README.md snippet to a runnable
11
+ # template: a "reviewer" lead that has no expertise of its own delegates to
12
+ # two specialists (security, performance) IN PARALLEL, then synthesizes.
13
+ #
14
+ # DEEPSEEK_API_KEY=sk-... ruby review-panel/agent.rb
15
+ # DEEPSEEK_API_KEY=sk-... ruby review-panel/agent.rb --serve
16
+ require "insika"
17
+
18
+ panel = Insika.system do
19
+ provider :deepseek
20
+
21
+ agent("security") { model "deepseek-v4-flash"; instructions "Review code for SECURITY issues. Two sentences." }
22
+ agent("performance") { model "deepseek-v4-flash"; instructions "Review code for PERFORMANCE issues. Two sentences." }
23
+
24
+ agent "reviewer" do
25
+ model "deepseek-v4-flash"
26
+ instructions <<~PROMPT
27
+ You are a review LEAD with no reviewing expertise of your own — never
28
+ review from your own knowledge. Call spawn_subagents once with both
29
+ specialists (security, performance), then synthesize their findings
30
+ into the single highest-priority fix.
31
+ PROMPT
32
+ subagents "security", "performance"
33
+ end
34
+ end
35
+
36
+ if __FILE__ == $PROGRAM_NAME
37
+ code = <<~RUBY
38
+ def find_user(name)
39
+ User.where("name = '\#{name}'").to_a.select { |u| u.active }
40
+ end
41
+ RUBY
42
+
43
+ if ARGV.delete("--serve")
44
+ panel.serve
45
+ else
46
+ puts panel.reply("reviewer", "Review this code:\n#{code}")
47
+ end
48
+ end
49
+
50
+ panel
@@ -0,0 +1,35 @@
1
+ # travel-planner
2
+
3
+ **Starter trail.** A trip-planning assistant built entirely from **declarative
4
+ data-tools** — no Ruby tool class, no rebuild. Three tools, three public
5
+ HTTPS APIs, zero API keys beyond your LLM provider's:
6
+
7
+ - `geocode_city` — Open-Meteo's geocoding API (city name → coordinates)
8
+ - `get_weather` — Open-Meteo's forecast API (coordinates → today's conditions)
9
+ - `convert_currency` — Frankfurter's reference exchange rates
10
+
11
+ ```bash
12
+ DEEPSEEK_API_KEY=sk-... ruby travel-planner/agent.rb "3 days in Lisbon, budget 200 USD"
13
+ ```
14
+
15
+ The model chains the first two tools itself (geocode, then weather) and
16
+ calls the third when a budget is mentioned — nothing here tells it the
17
+ order, the instructions just describe what each tool is for.
18
+
19
+ ## Egress guard (SSRF protection)
20
+
21
+ Data-tools make **server-side** HTTP calls, so the engine ships an egress
22
+ guard that is strict by default: public HTTPS only. This template works
23
+ with zero configuration because all three endpoints are public HTTPS — a
24
+ tool pointed at `http://…`, `localhost`, or a private IP would be blocked
25
+ instead (`{ error: "destination blocked: …" }` back to the model, a clean
26
+ tool error, never a crash). Opting a private/internal target in is a
27
+ deployment env var (`INSIKA_EGRESS_ALLOW_HTTP`/`_PRIVATE`/`_HOSTS`), never a
28
+ DSL setting — see `docs/TOOLS.md`'s "MCP servers" / egress sections in the
29
+ installed gem's docs for the full contract.
30
+
31
+ ## Edit it
32
+
33
+ Open `agent.rb` — it's the same file `insika new` copied and the same one
34
+ this README describes. Add a fourth data-tool, change the model, or point
35
+ an existing one at a different provider; nothing else needs to change.
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ # ---
4
+ # title: Travel Planner
5
+ # trail: Starter
6
+ # description: Weather + currency data-tools against keyless public APIs (Open-Meteo, Frankfurter) — the egress guard does its job with zero configuration.
7
+ # capabilities: data-tool, egress-guard
8
+ # ---
9
+ #
10
+ # travel-planner — plans a trip: geocodes the destination, checks today's
11
+ # weather, and converts a budget to the local currency. Three declarative
12
+ # data-tools, no Ruby tool class, no API key beyond the LLM provider's.
13
+ #
14
+ # DEEPSEEK_API_KEY=sk-... ruby travel-planner/agent.rb "3 days in Lisbon, budget 200 USD"
15
+ # DEEPSEEK_API_KEY=sk-... ruby travel-planner/agent.rb --serve
16
+ require "insika"
17
+
18
+ travel = Insika.agent("travel-planner") do
19
+ model "deepseek-v4-flash"
20
+ provider :deepseek
21
+
22
+ instructions <<~PROMPT
23
+ You are a travel-planning assistant. Given a destination and, optionally,
24
+ a budget amount + currency:
25
+ 1. geocode_city to find its coordinates — never guess them.
26
+ 2. get_weather for those coordinates and summarize today's conditions.
27
+ 3. If a budget was given, convert_currency to the destination's local
28
+ currency and report the converted amount.
29
+ Never invent coordinates, weather or exchange rates — always call the tools.
30
+ PROMPT
31
+
32
+ data_tool(
33
+ "name" => "geocode_city",
34
+ "description" => "Latitude/longitude for a city name (Open-Meteo geocoding).",
35
+ "parameters" => {
36
+ "type" => "object",
37
+ "properties" => { "city" => { "type" => "string", "description" => "city name, e.g. Lisbon" } },
38
+ "required" => ["city"]
39
+ },
40
+ "request" => { "method" => "GET", "url" => "https://geocoding-api.open-meteo.com/v1/search?name={{city}}&count=1" },
41
+ "response" => { "extract" => "body_raw" }
42
+ )
43
+
44
+ data_tool(
45
+ "name" => "get_weather",
46
+ "description" => "Current weather for a latitude/longitude (Open-Meteo).",
47
+ "parameters" => {
48
+ "type" => "object",
49
+ "properties" => {
50
+ "latitude" => { "type" => "number", "description" => "from geocode_city" },
51
+ "longitude" => { "type" => "number", "description" => "from geocode_city" }
52
+ },
53
+ "required" => %w[latitude longitude]
54
+ },
55
+ "request" => { "method" => "GET", "url" => "https://api.open-meteo.com/v1/forecast?latitude={{latitude}}&longitude={{longitude}}&current_weather=true" },
56
+ "response" => { "extract" => "body_raw" }
57
+ )
58
+
59
+ # Author the FINAL url — the HTTP client does not follow redirects.
60
+ # api.frankfurter.app now redirects to api.frankfurter.dev.
61
+ data_tool(
62
+ "name" => "convert_currency",
63
+ "description" => "Latest reference exchange rate between two currencies.",
64
+ "parameters" => {
65
+ "type" => "object",
66
+ "properties" => {
67
+ "from" => { "type" => "string", "description" => "source currency code, e.g. USD" },
68
+ "to" => { "type" => "string", "description" => "target currency code, e.g. BRL" }
69
+ },
70
+ "required" => %w[from to]
71
+ },
72
+ "request" => { "method" => "GET", "url" => "https://api.frankfurter.dev/v1/latest?from={{from}}&to={{to}}" },
73
+ "response" => { "extract" => "body_raw" }
74
+ )
75
+ end
76
+
77
+ if __FILE__ == $PROGRAM_NAME
78
+ if ARGV.delete("--serve")
79
+ travel.serve
80
+ else
81
+ message = ARGV.join(" ")
82
+ message = "I'm spending 3 days in Lisbon with a budget of 200 USD. What should I pack, and how much is that in EUR?" if message.empty?
83
+ puts travel.reply(message)
84
+ end
85
+ end
86
+
87
+ travel
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Insika
6
+ # Template gallery: example agents shipped INSIDE the gem
7
+ # (`lib/insika/templates/<name>/{agent.rb,README.md}`), one DSL file per
8
+ # template that is BOTH doors — `insika new <name>` copies it for the user
9
+ # to run and edit, and this module `evaluate`s the same file to hand its
10
+ # pack(s) to the Studio's "New from template" gallery. No parallel pack
11
+ # format to drift.
12
+ #
13
+ # A template's `agent.rb` guards its CLI demo footer with
14
+ # `if __FILE__ == $PROGRAM_NAME` (false when this module evaluates it) and
15
+ # ends with the bare `Insika.agent`/`Insika.system` result as its LAST
16
+ # expression, so `evaluate` gets it back as the string-eval's return value
17
+ # — no registration call, no second source of truth.
18
+ module Templates
19
+ ROOT = File.expand_path("templates", __dir__)
20
+
21
+ Entry = Data.define(:name, :title, :trail, :description, :capabilities, :studio, :env, :requires) do
22
+ def studio? = studio
23
+ end
24
+
25
+ module_function
26
+
27
+ # -> [String] template dirs that have an agent.rb, lexicographic.
28
+ def names
29
+ return [] unless Dir.exist?(ROOT)
30
+
31
+ Dir.children(ROOT).select { |n| File.file?(agent_path(n)) }.sort
32
+ end
33
+
34
+ # -> [Entry] every template, parsed metadata only (no evaluation — cheap,
35
+ # safe to call on every render of the Studio gallery).
36
+ def all = names.map { |n| read(n) }
37
+
38
+ # -> Entry for one template. Raises NotFoundError for an unknown name —
39
+ # same discipline as a missing agent/MCP instance.
40
+ def read(name)
41
+ path = agent_path(name)
42
+ raise Insika::NotFoundError, "template '#{name}' not found" unless File.file?(path)
43
+
44
+ meta = frontmatter(File.read(path))
45
+ Entry.new(
46
+ name: name.to_s, title: presence(meta["title"]) || name.to_s, trail: presence(meta["trail"]),
47
+ description: meta["description"].to_s,
48
+ capabilities: split_list(meta["capabilities"]),
49
+ studio: meta.fetch("studio", true) != false,
50
+ env: split_list(meta["env"]), requires: presence(meta["requires"])
51
+ )
52
+ end
53
+
54
+ # Evaluates the template's agent.rb in an ISOLATED scope (a fresh Object's
55
+ # instance_eval) and returns whatever its last expression is — the built
56
+ # `Insika::DSL::Definition` or `Insika::DSL::System`. $PROGRAM_NAME here is
57
+ # whatever process called this (rspec, the CLI, the Studio server), never
58
+ # this file's path, so the template's own `if __FILE__ == $PROGRAM_NAME`
59
+ # demo footer never runs: no network call, no ARGV parsing, no puts.
60
+ #
61
+ # The fresh-Object receiver keeps a template's local variables and `def`s
62
+ # from leaking into the next one evaluated in the same process; a
63
+ # top-level CONSTANT would still leak (Ruby scopes constant assignment
64
+ # lexically, not by `self`) — wave-1 templates simply don't declare any
65
+ # (the conformance spec, would catch a future one that did).
66
+ def evaluate(name)
67
+ path = agent_path(name)
68
+ raise Insika::NotFoundError, "template '#{name}' not found" unless File.file?(path)
69
+
70
+ Object.new.instance_eval(File.read(path), path)
71
+ end
72
+
73
+ # -> [Pack] one per agent, regardless of whether the template is a single
74
+ # `Insika.agent` (Definition#to_pack) or a system (System#to_packs).
75
+ def packs_for(name)
76
+ built = evaluate(name)
77
+ built.respond_to?(:to_packs) ? built.to_packs : [built.to_pack]
78
+ end
79
+
80
+ def agent_path(name) = File.join(ROOT, name.to_s, "agent.rb")
81
+ def readme_path(name) = File.join(ROOT, name.to_s, "README.md")
82
+
83
+ # A `# ---` … `# ---` comment block at the very top of the file, YAML
84
+ # inside (each line stripped of its leading `# `). Not real Ruby
85
+ # frontmatter (there's no such thing) — a convention this module alone
86
+ # parses, so the metadata lives in the one file without needing a
87
+ # side-channel manifest.
88
+ def frontmatter(source)
89
+ lines = source.lines
90
+ # Every template starts with the same magic comment every other .rb
91
+ # file in the gem does — skip it (and any blank line) before looking
92
+ # for the block, so templates don't have to break that convention.
93
+ lines = lines.drop(1) while lines.first && (lines.first.strip.empty? || lines.first.strip == "# frozen_string_literal: true")
94
+ return {} unless lines.first&.strip == "# ---"
95
+
96
+ body = lines.drop(1)
97
+ .take_while { |l| l.strip != "# ---" }
98
+ .map { |l| l.sub(/\A#\s?/, "") }
99
+ .join
100
+ YAML.safe_load(body) || {}
101
+ end
102
+ private_class_method :frontmatter
103
+
104
+ def split_list(value)
105
+ value.to_s.split(",").map(&:strip).reject(&:empty?)
106
+ end
107
+ private_class_method :split_list
108
+
109
+ def presence(str) = Insika::Coercion.presence(str)
110
+ private_class_method :presence
111
+ end
112
+ end