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.



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/insika/dsl.rb', line 110

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
end

Instance Method Details

#alerts(hash) ⇒ Object

breaker_open / delivery_failed events to the webhook as JSON.



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

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

#api_base(value) ⇒ Object



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

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.



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

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"



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

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



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

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

#build(&block) ⇒ Object



122
123
124
125
# File 'lib/insika/dsl.rb', line 122

def build(&block)
  instance_eval(&block) if block
  Definition.new(pack: to_pack, runtime: @runtime)
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.



157
158
159
160
161
162
163
# File 'lib/insika/dsl.rb', line 157

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"



345
346
347
# File 'lib/insika/dsl.rb', line 345

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

#deny_tools(*names) ⇒ Object



150
151
152
# File 'lib/insika/dsl.rb', line 150

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



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

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



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

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 }



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

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"



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

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' }



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

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." }



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

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 } }



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

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

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



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

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

#limit(key, value) ⇒ Object

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



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

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

#limits(hash) ⇒ Object



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

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

#max_tokens(value) ⇒ Object



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

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

#memory(on = true) ⇒ Object

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



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

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

#metadata(hash) ⇒ Object



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

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

#model(name) ⇒ Object

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



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

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" }



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

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

#param(key, value) ⇒ Object

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



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

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

#params(hash) ⇒ Object



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

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

#policies(*names) ⇒ Object



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

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").



140
141
142
# File 'lib/insika/dsl.rb', line 140

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.



131
132
133
134
# File 'lib/insika/dsl.rb', line 131

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 }



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

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 }



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

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"



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

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

#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.



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

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.



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

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

#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



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

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.



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

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

#temperature(value) ⇒ Object



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

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

#to_packObject

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



377
378
379
380
381
382
# File 'lib/insika/dsl.rb', line 377

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.



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

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


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

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

#tools(*names) ⇒ Object

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



146
147
148
# File 'lib/insika/dsl.rb', line 146

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