Class: Insika::DSL::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/dsl.rb

Overview

Collects the declarations and emits a Insika::Pack. Declarations map 1:1 to the pack manifest (AgentProfile.build attrs) + the pack's files/skills/tools — so what you write is exactly the data the engine stores.

Instance Method Summary collapse

Constructor Details

#initialize(id) ⇒ Builder

Returns a new instance of Builder.



140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/insika/dsl.rb', line 140

def initialize(id)
  @id = id.to_s
  @config = {}
  @files = {}
  @skills = {}
  @tools = []
  # Auto-enable the allowlist policies: harmless when the allowlist is nil=all,
  # correct once you restrict tools/skills. Visible in #to_pack — no hidden magic.
  @config[:policies] = %i[tool_allowlist skill_allowlist]
  @runtime = {} # non-pack knobs (llm provider/key/base) consumed by the runtime
  @mcp_instances = []
end

Instance Method Details

#alerts(hash) ⇒ Object

breaker_open / delivery_failed events to the webhook as JSON.



365
# File 'lib/insika/dsl.rb', line 365

def alerts(hash) = (@config[:alerts] ||= {}).merge!(hash.transform_keys(&:to_s))

#api_base(value) ⇒ Object



475
# File 'lib/insika/dsl.rb', line 475

def api_base(value) = @runtime[:api_base] = value.to_s

#api_key(value) ⇒ Object

--- runtime (LLM provider) config — NOT part of the pack ------------ Configures RubyLLM at chat/serve time. Defaults: provider = the agent's provider; key = ENV.



474
# File 'lib/insika/dsl.rb', line 474

def api_key(value) = @runtime[:api_key] = value.to_s

#briefing_fields(*names) ⇒ Object

The per-session working-state schema this agent keeps and asks for a flat list of field names. [] = off (no provider output, no update_briefing/set_next_step tools). briefing_fields "size", "budget", "delivery_day"



390
391
392
# File 'lib/insika/dsl.rb', line 390

def briefing_fields(*names)
  @config[:briefing_fields] = names.flatten.map(&:to_s)
end

#budget(hash) ⇒ Object

Spend caps per calendar window (WS2): daily/monthly token budgets for this agent, per (tenant, agent) when multi-tenant. HARD is the default: absent soft: (or soft: false) turns the cap into a hard wall (the turn fails with budget_exceeded + retry_after); soft: true warns once per window and keeps running. budget daily: 100_000, monthly: 2_000_000, soft: false



275
# File 'lib/insika/dsl.rb', line 275

def budget(hash) = (@config[:budget] ||= {}).merge!(hash.transform_keys(&:to_s))

#build(&block) ⇒ Object



153
154
155
156
# File 'lib/insika/dsl.rb', line 153

def build(&block)
  instance_eval(&block) if block
  Definition.new(pack: to_pack, runtime: @runtime, mcp_instances: @mcp_instances)
end

#data_tool(defn) ⇒ Object

A DATA-DEFINED (declarative HTTP) tool — pure config-over-code. defn is a ToolDefinition hash (name/description/parameters/binding…). Its name is auto-added to the allowlist so the agent can call its own tool.



188
189
190
191
192
193
194
# File 'lib/insika/dsl.rb', line 188

def data_tool(defn)
  h = defn.transform_keys(&:to_s)
  @tools << h
  name = h["name"].to_s
  (@config[:tools_allow] ||= []) << name unless name.empty? || Array(@config[:tools_allow]).include?(name)
  h
end

#declares(*names) ⇒ Object

Facts about THIS deployment that are not tools, so an eval case can declare what it needs and be skipped where it is absent instead of failing for the wrong reason. declares "promotions", "human_handoff"



446
447
448
# File 'lib/insika/dsl.rb', line 446

def declares(*names)
  (@config[:capabilities_declared] ||= []).concat(names.flatten.map(&:to_s))
end

#deny_tools(*names) ⇒ Object



181
182
183
# File 'lib/insika/dsl.rb', line 181

def deny_tools(*names)
  @config[:tools_deny] = names.flatten.map(&:to_s)
end

#distill(hash) ⇒ Object

The session-distillation declaration: what counts as a fact for THIS store, distilled from finished customer conversations. Pack data — merges, so repeated calls accumulate (like budget). prompt and model are pack-authored keys the DSL passes through. distill enabled: true, idle_hours: 6, max_proposals: 10



304
# File 'lib/insika/dsl.rb', line 304

def distill(hash) = (@config[:distill] ||= {}).merge!(hash.transform_keys(&:to_s))

#edge_stream(hash) ⇒ Object

Which internal channels may cross to the CUSTOMER. Both off by default: the answer is the answer, and the provider's reasoning (thinking) or the model narrating its tool loop (intermediate) is for the Studio and the trace. Each opted-in channel gets its OWN frame type at /v1/responses — never the answer's — so a consumer that only reads the answer is unaffected either way. edge_stream thinking: true, intermediate: false



456
# File 'lib/insika/dsl.rb', line 456

def edge_stream(hash) = (@config[:edge_stream] ||= {}).merge!(hash.transform_keys(&:to_s))

#followup(hash) ⇒ Object

The follow-up declaration: the agent may book a follow-up with a customer at a future time (schedule tool); the engine fires the synthetic turn and enforces the policy (quiet hours, max frequency, cancellation keywords, silence detection) at fire time. Pack data — merges, so repeated calls accumulate (like budget). followup arm: "schedule", policy: { quiet_hours: { timezone: "America/Sao_Paulo", start: "21:30", end: "09:00" }, max_frequency: "2/24h", cancel_keywords: ["não quero mais contato"], silence_after_sends: 3 }



297
# File 'lib/insika/dsl.rb', line 297

def followup(hash) = (@config[:followup] ||= {}).merge!(hash.transform_keys(&:to_s))

#funnel(hash) ⇒ Object

The outcome funnel declaration: the store's stage vocabulary as pack data — the engine folds WS7 outcome kinds into the DECLARED stages, and never hard-codes one itself. Merges, so repeated calls accumulate (like budget). funnel stages: %w[greeted qualified cart paid], advance_on: { "pix_paid" => "paid", "abandoned_cart" => "cart" }, primary: "paid", attribution_window: "72h"



284
# File 'lib/insika/dsl.rb', line 284

def funnel(hash) = (@config[:funnel] ||= {}).merge!(hash.transform_keys(&:to_s))

#grounding(hash = {}) ⇒ Object

Evidence-grounding policy: the pack declares how the engine polices product claims against the evidence ledger. Same config-over-code shape as guardrails; absent = off (parity). grounding mode: :flag, matcher: { sku: '\b[A-Z]2,4\d4,8\b' }



432
# File 'lib/insika/dsl.rb', line 432

def grounding(hash = {}) = (@config[:grounding] ||= {}).merge!(hash.transform_keys(&:to_s))

#guardrails(hash) ⇒ Object

Content-safety guardrails — opt-in and configurable per agent. Pure config-over-code: the hash is stored on the profile and consumed by Safety::Config.from_profile. Merges, so repeated calls accumulate. guardrails input: true, output: true, strictness: "medium", moderator: "deepseek/deepseek-v4-flash", responses: { "injection" => "I can't help with that." }



426
# File 'lib/insika/dsl.rb', line 426

def guardrails(hash) = (@config[:guardrails] ||= {}).merge!(hash.transform_keys(&:to_s))

#harvest(hash) ⇒ Object

The gated-harvest declaration: the engine may read this agent's finished traffic and propose SKILLS for the store's playbook — through the negative list, the grounding filter and the double gate, never applied automatically. Pack data — merges, so repeated calls accumulate (like budget). prompt/model are pack-authored keys the DSL passes through. harvest enabled: true, negative_list: [ { rule: "no-competitor-prices", pattern: "concorrente" } ], miner: { model: "deepseek-v4-flash", window: { last_sessions: 200 } }



315
# File 'lib/insika/dsl.rb', line 315

def harvest(hash) = (@config[:harvest] ||= {}).merge!(hash.transform_keys(&:to_s))

#instructions(text) ⇒ Object Also known as: prompt



167
# File 'lib/insika/dsl.rb', line 167

def instructions(text) = @config[:base_prompt] = text.to_s

#knowledge(hash) ⇒ Object

The post-turn knowledge declaration: after a turn completes, the engine may extract durable CONCEPTS (facts, procedures, policies, objections) from it and persist them for later turns to retrieve. Pack data — merges, so repeated calls accumulate (like budget). prompt/model are pack-authored keys the DSL passes through. knowledge extract: true, retrieve: true, types: %w[fact policy]



323
# File 'lib/insika/dsl.rb', line 323

def knowledge(hash) = (@config[:knowledge] ||= {}).merge!(hash.transform_keys(&:to_s))

#limit(key, value) ⇒ Object

Per-agent limits (timeouts/budgets). limit :turn_timeout, 120 or limits(...).



465
# File 'lib/insika/dsl.rb', line 465

def limit(key, value) = (@config[:limits] ||= {})[key.to_sym] = value

#limits(hash) ⇒ Object



466
# File 'lib/insika/dsl.rb', line 466

def limits(hash) = (@config[:limits] ||= {}).merge!(hash.transform_keys(&:to_sym))

#max_tokens(value) ⇒ Object



462
# File 'lib/insika/dsl.rb', line 462

def max_tokens(value) = param(:max_tokens, value)

#mcp(name, transport: nil, command: nil, args: nil, url: nil, headers: nil, env: nil, description: nil, enabled: true) ⇒ Object

--- mcp --------------------------------------------------------------- Declares an MCP server instance — see Insika::DSL::SystemBuilder#mcp for the transport/lifecycle doc; identical shape here for a standalone Insika.agent { … } script, or one member agent of a system, that wants one.

Auto-adds "mcp:" to THIS agent's tools_allow_groups — without it, a pack with no data_tool gets PackImporter's tools_allow: [] (isolation default) and no tools_allow_groups at all, so Policy::ToolAllowlist#allowed_names resolves an EMPTY allowlist and the agent could never call the MCP tool it just declared (found writing the MCP templates — no existing spec exercised this path end to end). Same "auto-added to the allowlist" contract data_tool already gives its own tool name; deny_tools has no group-string equivalent yet, so a whole MCP group cannot be denied by name today.

Raises:

  • (ArgumentError)


253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/insika/dsl.rb', line 253

def mcp(name, transport: nil, command: nil, args: nil, url: nil,
        headers: nil, env: nil, description: nil, enabled: true)
  n = name.to_s
  raise ArgumentError, "duplicate mcp instance in agent: #{n}" if @mcp_instances.any? { |m| m[:name] == n }

  @mcp_instances << { name: n, transport: transport&.to_s, command: command, args: args,
                       url: url, headers: headers, env: env, description: description,
                       enabled: enabled }
  group = "mcp:#{n}"
  (@config[:tools_allow_groups] ||= []) << group unless Array(@config[:tools_allow_groups]).include?(group)
  n
end

#memory(on = true) ⇒ Object

--- knobs -----------------------------------------------------------



267
# File 'lib/insika/dsl.rb', line 267

def memory(on = true) = @config[:memory] = on

#metadata(hash) ⇒ Object



469
# File 'lib/insika/dsl.rb', line 469

def (hash) = (@config[:metadata] ||= {}).merge!(hash.transform_keys(&:to_s))

#model(name) ⇒ Object

--- identity & model ------------------------------------------------



159
# File 'lib/insika/dsl.rb', line 159

def model(name) = @config[:model] = name.to_s

#outputs(hash) ⇒ Object

Generated-media output policy (WS9, saída): the media kinds this agent MAY generate as turn outputs, with per-kind config. The other half of the gate is the CHANNEL's: the request must declare the matching capability for the tools to exist at all. outputs image: { model: "gpt-image-1", size: "1024x1024" }, tts: { model: "tts-1", voice: "alloy" }



400
# File 'lib/insika/dsl.rb', line 400

def outputs(hash) = (@config[:outputs] ||= {}).merge!(hash.transform_keys(&:to_s))

#param(key, value) ⇒ Object

LLM generation params. param:temperature, 0.2 or params(...).



459
# File 'lib/insika/dsl.rb', line 459

def param(key, value) = (@config[:params] ||= {})[key.to_sym] = value

#params(hash) ⇒ Object



460
# File 'lib/insika/dsl.rb', line 460

def params(hash) = (@config[:params] ||= {}).merge!(hash.transform_keys(&:to_sym))

#policies(*names) ⇒ Object



468
# File 'lib/insika/dsl.rb', line 468

def policies(*names) = @config[:policies] = names.flatten.map(&:to_sym)

#prompt_file(name, content) ⇒ Object

An extra prompt FILE (identity fragment). Name = the file name (e.g. "SOUL.md").



171
172
173
# File 'lib/insika/dsl.rb', line 171

def prompt_file(name, content)
  @files[name.to_s] = content.to_s
end

#provider(name) ⇒ Object

Provider for both the profile AND the RubyLLM configuration at run time.



162
163
164
165
# File 'lib/insika/dsl.rb', line 162

def provider(name)
  @config[:provider] = name.to_s
  @runtime[:provider] ||= name.to_s
end

#refine(hash) ⇒ Object

Refinement — how the agent's own instruction files may be improved from real traffic. Same config-over-code shape as guardrails; omitting it entirely leaves the agent report-only (writes nothing). refine mode: "propose", window: { last_sessions: 200 }, files: %w, proposers: ["deepseek/deepseek-v4-flash", "gpt-5-mini"], budget: { tokens: 200_000 }



440
# File 'lib/insika/dsl.rb', line 440

def refine(hash) = (@config[:refinement] ||= {}).merge!(hash.transform_keys(&:to_s))

#reliability(hash) ⇒ Object

Provider-interaction reliability, as DATA (WS3): retries + exponential backoff on transient failures, a fallback model chain (mid-turn rotation), and a circuit breaker per (tenant, provider/model) that fail-fasts once the window trips. fallback/circuit_breaker entries are "provider/model" refs or plain model ids. reliability retries: 3, backoff: "exponential", fallback: ["gpt-4o-mini"], circuit_breaker: { after: 10, within: 60, cooldown: 300 }



359
360
361
# File 'lib/insika/dsl.rb', line 359

def reliability(hash)
  (@config[:reliability] ||= {}).merge!(hash.transform_keys(&:to_s))
end

#routes(hash) ⇒ Object

Intent routing (WS4): classify each turn's message into one route with a cheap model BEFORE the ask. A Hash: route name -> description (or a Hash with description/delegate/stuck/message), plus the reserved keys "default" (the deterministic fallback) and "model"/"provider" (the cheap classifier). The classifier prompt is generated — data only. routes "shopping" => "the customer wants to browse products", "order" => { "description" => "asks about an existing order", "delegate" => "order-agent" }, "human" => { "description" => "the customer asks for a person", "stuck" => true }, "default" => "shopping", "model" => "deepseek-v4-flash"



378
# File 'lib/insika/dsl.rb', line 378

def routes(hash) = (@config[:routes] ||= {}).merge!(hash.transform_keys(&:to_s))

#schedule(name, every: nil, cron: nil, tz: nil, message: nil, session_mode: nil, session_id: nil, overrides: nil, enabled: nil) ⇒ Object

A recurring schedule: one declaration per call, named — a turn the ENGINE fires on its own tick, nobody has to remember. cron (5 fields) or every (plain interval), a tz for cron materialization, the synthetic inbound message that kicks each run, a session mode (new = a fresh session per run — the report case; fixed = one standing session), per-run overrides (turn_timeout / max_tool_calls / model) and enabled. schedule "daily_report", cron: "0 22 * * *", tz: "America/Sao_Paulo", message: "Run the daily report now.", overrides: { turn_timeout: 900, max_tool_calls: 200 } Distinct by shape from the schedule_followup TOOL (a one-shot, customer-facing, consent-gated contact); see docs/SCHEDULING.md.



337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/insika/dsl.rb', line 337

def schedule(name, every: nil, cron: nil, tz: nil, message: nil,
             session_mode: nil, session_id: nil, overrides: nil, enabled: nil)
  id = name.to_s.downcase # the engine canonicalizes ids to lowercase
  if Array(@config[:schedules]).any? { |s| s["id"] == id }
    raise ArgumentError, "duplicate schedule in agent: #{id}"
  end

  entry = { "id" => id, "every" => every, "cron" => cron, "tz" => tz,
            "message" => message, "session_mode" => session_mode,
            "session_id" => session_id, "overrides" => overrides,
            "enabled" => enabled }.compact
  @config[:schedules] = Array(@config[:schedules]) + [entry]
  id
end

#skill(name, content = nil, description: nil, instructions: nil) ⇒ Object

--- skills ---------------------------------------------------------- skill "escalate", "" — or — skill "escalate", description: "…", instructions: "…" The name is auto-added to the agent's skill allowlist.



200
201
202
203
204
205
# File 'lib/insika/dsl.rb', line 200

def skill(name, content = nil, description: nil, instructions: nil)
  n = name.to_s
  @skills[n] = normalize_skill(n, content, description, instructions)
  (@config[:skills] ||= []) << n unless @config.fetch(:skills, []).include?(n)
  n
end

#skills_eager(*names) ⇒ Object

skills_eager — turns progressive disclosure off for THIS agent, wholly or in part. The body of an eager skill is in the prompt on every turn, so its activation is not a decision and cannot be missed; it is paid for on every turn, so measure the bodies against context_budget first (a stable position makes them a cacheable prefix).

skills_eager                       # every allowed skill
skills_eager "formato", "markers"  # exactly these
skills_eager false                 # none (the default)

A LIST and not a per-skill flag because skills are shared: escalation-to-human sits in several allowlists, and one flag on the skill would force one decision onto every agent holding it.



220
221
222
223
224
225
226
227
# File 'lib/insika/dsl.rb', line 220

def skills_eager(*names)
  flat = names.flatten
  @config[:skills_eager] =
    if flat.empty? then true
    elsif flat == [true] || flat == [false] then flat.first
    else flat.map(&:to_s)
    end
end

#stt_prompt(text) ⇒ Object

STT vocabulary hint (WS9): domain words (product names, brand terms) the transcriber should expect on THIS agent's voice notes — passed straight through to the Whisper-family provider's prompt:. Falls back to INSIKA_STT_PROMPT (deployment default) when unset. stt_prompt "Ocean Drop, tênis, boné trucker, chinelo"



407
# File 'lib/insika/dsl.rb', line 407

def stt_prompt(text) = @config[:stt_prompt] = text.to_s

#stuck_signal(on = true) ⇒ Object

The agent may signal it cannot proceed (WS5): when on, the model can call signal_stuck, which ends the turn with outcome: :stuck + a final message + a :turn_stuck event. What "stuck" means is the consumer's call. stuck_signal true



384
# File 'lib/insika/dsl.rb', line 384

def stuck_signal(on = true) = @config[:stuck_signal] = on

#subagents(*ids) ⇒ Object

--- delegation ------------------------------------------------------ subagents "security", "performance" → the child agents this one MAY spawn. CAPACITY field: opt-in, never inherited, and the ids must be agents of the same system (Insika.system { … }) or already in the store. Present ⇒ the engine wires spawn_subagent/spawn_subagents.



234
235
236
# File 'lib/insika/dsl.rb', line 234

def subagents(*ids)
  @config[:subagents] = ids.flatten.map(&:to_s)
end

#temperature(value) ⇒ Object



461
# File 'lib/insika/dsl.rb', line 461

def temperature(value) = param(:temperature, value)

#to_packObject

The generated portable artifact — the heart of "generates the data".



478
479
480
481
482
483
# File 'lib/insika/dsl.rb', line 478

def to_pack
  Insika::Pack.from_h(
    config: @config.merge(id: @id),
    files: @files, skills: @skills, tools: @tools
  )
end

#tool_output_compression(on = true) ⇒ Object

Mechanical tool-result dedupe in the replayed history (no-LLM compaction, apt for bloated transcripts). CHANGES WHAT THE MODEL SEES: repeated identical tool results collapse to a back-reference.



418
# File 'lib/insika/dsl.rb', line 418

def tool_output_compression(on = true) = @config[:tool_output_compression] = on

#tool_persistence(on = true) ⇒ Object

The engine's "Tool discipline" block in the system prompt (retry a weak/empty tool result with a different approach before giving up). ON by default — this setter exists to turn it OFF:

tool_persistence false


413
# File 'lib/insika/dsl.rb', line 413

def tool_persistence(on = true) = @config[:tool_persistence] = on

#tools(*names) ⇒ Object

--- tools ----------------------------------------------------------- tools "a", "b" → allowlist [names]. Not called → nil = all (parity).



177
178
179
# File 'lib/insika/dsl.rb', line 177

def tools(*names)
  @config[:tools_allow] = names.flatten.map(&:to_s)
end