insika 0.0.1 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +361 -0
- data/LICENSE +21 -0
- data/README.md +136 -2
- data/bin/insika +366 -0
- data/docs/AGENTS.md +618 -0
- data/docs/ARCHITECTURE.md +333 -0
- data/docs/BENCHMARK.md +114 -0
- data/docs/CHANNELS.md +453 -0
- data/docs/CONTEXT.md +117 -0
- data/docs/DEPLOY.md +354 -0
- data/docs/EMBEDDING.md +198 -0
- data/docs/EVALS.md +273 -0
- data/docs/LOADTEST.md +232 -0
- data/docs/OBSERVABILITY.md +374 -0
- data/docs/PLUGINS.md +211 -0
- data/docs/REFINEMENT.md +477 -0
- data/docs/RELEASING.md +70 -0
- data/docs/RUNNING-LOCAL.md +153 -0
- data/docs/SANDBOX.md +114 -0
- data/docs/SECURITY.md +375 -0
- data/docs/SKILLS.md +284 -0
- data/docs/TOOLS.md +302 -0
- data/docs/WHY.md +137 -0
- data/docs/WORKFLOWS.md +225 -0
- data/docs/build.md +14 -0
- data/docs/index.md +68 -0
- data/docs/onboarding/start.md +126 -0
- data/docs/operate.md +12 -0
- data/docs/ship.md +10 -0
- data/docs/understand.md +10 -0
- data/lib/insika/agent_file_store.rb +125 -0
- data/lib/insika/agent_profile.rb +255 -0
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/allowlist.rb +28 -0
- data/lib/insika/baseline_store.rb +74 -0
- data/lib/insika/budget_ledger.rb +135 -0
- data/lib/insika/capability/resolved_tool.rb +34 -0
- data/lib/insika/capability_registry.rb +112 -0
- data/lib/insika/channel_delivery.rb +153 -0
- data/lib/insika/channel_registry.rb +30 -0
- data/lib/insika/channels/relay.rb +178 -0
- data/lib/insika/channels/web/widget.js +283 -0
- data/lib/insika/channels/web.rb +211 -0
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +303 -0
- data/lib/insika/checkpoint.rb +13 -0
- data/lib/insika/checkpoint_store.rb +153 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +58 -0
- data/lib/insika/command.rb +32 -0
- data/lib/insika/command_bus.rb +39 -0
- data/lib/insika/commands/agent_payload.rb +43 -0
- data/lib/insika/commands/approve_action.rb +46 -0
- data/lib/insika/commands/cancel_task.rb +33 -0
- data/lib/insika/commands/create_agent.rb +54 -0
- data/lib/insika/commands/create_session.rb +67 -0
- data/lib/insika/commands/delete_agent.rb +33 -0
- data/lib/insika/commands/delete_agent_file.rb +50 -0
- data/lib/insika/commands/delete_data_tool.rb +33 -0
- data/lib/insika/commands/delete_llm_provider.rb +36 -0
- data/lib/insika/commands/delete_mcp.rb +30 -0
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_system_file.rb +29 -0
- data/lib/insika/commands/gate_refinement.rb +245 -0
- data/lib/insika/commands/import_mcp_tools.rb +48 -0
- data/lib/insika/commands/import_tools.rb +81 -0
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/memory_add_note.rb +32 -0
- data/lib/insika/commands/memory_forget_fact.rb +32 -0
- data/lib/insika/commands/memory_put_fact.rb +35 -0
- data/lib/insika/commands/pause_task.rb +29 -0
- data/lib/insika/commands/resolve_refinement.rb +126 -0
- data/lib/insika/commands/restore_agent_file.rb +36 -0
- data/lib/insika/commands/restore_data_tool.rb +34 -0
- data/lib/insika/commands/restore_system_file.rb +31 -0
- data/lib/insika/commands/resume_task.rb +85 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_refinement.rb +133 -0
- data/lib/insika/commands/send_message.rb +150 -0
- data/lib/insika/commands/set_agent_tools.rb +39 -0
- data/lib/insika/commands/set_skill_agents.rb +112 -0
- data/lib/insika/commands/trigger_workflow.rb +80 -0
- data/lib/insika/commands/update_agent.rb +49 -0
- data/lib/insika/commands/update_settings.rb +33 -0
- data/lib/insika/commands/upsert_llm_provider.rb +34 -0
- data/lib/insika/commands/upsert_mcp.rb +32 -0
- data/lib/insika/commands/write_agent_file.rb +57 -0
- data/lib/insika/commands/write_data_tool.rb +43 -0
- data/lib/insika/commands/write_golden.rb +58 -0
- data/lib/insika/commands/write_skill.rb +60 -0
- data/lib/insika/commands/write_system_file.rb +31 -0
- data/lib/insika/config_store.rb +89 -0
- data/lib/insika/context/builder.rb +166 -0
- data/lib/insika/context/catalog_provider.rb +23 -0
- data/lib/insika/context/fragment.rb +43 -0
- data/lib/insika/context/priority.rb +30 -0
- data/lib/insika/context/provider.rb +19 -0
- data/lib/insika/context/providers/memory.rb +60 -0
- data/lib/insika/context/providers/prompt.rb +105 -0
- data/lib/insika/context/providers/request.rb +32 -0
- data/lib/insika/context/providers/session.rb +123 -0
- data/lib/insika/context/providers/skill.rb +24 -0
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +20 -0
- data/lib/insika/context_trace_store.rb +92 -0
- data/lib/insika/delegation_store.rb +153 -0
- data/lib/insika/doctor.rb +539 -0
- data/lib/insika/dsl/definition.rb +55 -0
- data/lib/insika/dsl/runtime.rb +382 -0
- data/lib/insika/dsl/server_boot.rb +98 -0
- data/lib/insika/dsl/system.rb +93 -0
- data/lib/insika/dsl/workflow_adapter.rb +59 -0
- data/lib/insika/dsl.rb +364 -0
- data/lib/insika/edge_limiter.rb +268 -0
- data/lib/insika/egress_guard.rb +75 -0
- data/lib/insika/env_schema.rb +249 -0
- data/lib/insika/errors.rb +201 -0
- data/lib/insika/evals/assertions.rb +247 -0
- data/lib/insika/evals/baseline.rb +69 -0
- data/lib/insika/evals/golden.rb +172 -0
- data/lib/insika/evals/judge.rb +225 -0
- data/lib/insika/evals/pairwise.rb +178 -0
- data/lib/insika/evals/report.rb +115 -0
- data/lib/insika/evals/runner.rb +141 -0
- data/lib/insika/evals/transport.rb +178 -0
- data/lib/insika/event.rb +18 -0
- data/lib/insika/event_stream.rb +132 -0
- data/lib/insika/executor.rb +1995 -0
- data/lib/insika/frontmatter.rb +42 -0
- data/lib/insika/golden_store.rb +145 -0
- data/lib/insika/hooks.rb +48 -0
- data/lib/insika/http_client.rb +63 -0
- data/lib/insika/inbound_log.rb +84 -0
- data/lib/insika/llm_configurator.rb +99 -0
- data/lib/insika/llm_provider_store.rb +83 -0
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +67 -0
- data/lib/insika/mcp_store.rb +115 -0
- data/lib/insika/mcp_tool_ingestor.rb +143 -0
- data/lib/insika/memory_store.rb +93 -0
- data/lib/insika/message_origin.rb +76 -0
- data/lib/insika/middleware.rb +36 -0
- data/lib/insika/model_policy.rb +52 -0
- data/lib/insika/model_resolver.rb +176 -0
- data/lib/insika/model_selection.rb +115 -0
- data/lib/insika/onboarding.rb +208 -0
- data/lib/insika/outbox_store.rb +166 -0
- data/lib/insika/overlay_tool_registry.rb +102 -0
- data/lib/insika/pack.rb +102 -0
- data/lib/insika/pack_importer.rb +123 -0
- data/lib/insika/pending_action_store.rb +120 -0
- data/lib/insika/plugin/loader.rb +356 -0
- data/lib/insika/plugin.rb +35 -0
- data/lib/insika/policy/engine.rb +83 -0
- data/lib/insika/policy/policy.rb +120 -0
- data/lib/insika/policy_registry.rb +23 -0
- data/lib/insika/profile_source.rb +143 -0
- data/lib/insika/prompt_catalog.rb +61 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +167 -0
- data/lib/insika/recovery.rb +168 -0
- data/lib/insika/refinement/candidate.rb +159 -0
- data/lib/insika/refinement/evidence_collector.rb +371 -0
- data/lib/insika/refinement/gate.rb +234 -0
- data/lib/insika/refinement/panel.rb +222 -0
- data/lib/insika/refinement/proposer.rb +262 -0
- data/lib/insika/refinement_store.rb +295 -0
- data/lib/insika/registry.rb +59 -0
- data/lib/insika/reliability.rb +185 -0
- data/lib/insika/safety/config.rb +109 -0
- data/lib/insika/safety/detectors.rb +176 -0
- data/lib/insika/safety/factory.rb +102 -0
- data/lib/insika/safety/input_guardrail.rb +102 -0
- data/lib/insika/safety/moderator.rb +94 -0
- data/lib/insika/safety/output_filter.rb +79 -0
- data/lib/insika/safety/output_validator.rb +101 -0
- data/lib/insika/safety/safe_responses.rb +47 -0
- data/lib/insika/sandbox/boundary.rb +93 -0
- data/lib/insika/sandbox/docker.rb +74 -0
- data/lib/insika/sandbox/local.rb +33 -0
- data/lib/insika/sandbox/runner.rb +80 -0
- data/lib/insika/sandbox.rb +85 -0
- data/lib/insika/schema_guard.rb +147 -0
- data/lib/insika/secret_masking.rb +34 -0
- data/lib/insika/server/a2a/agent_card.rb +27 -0
- data/lib/insika/server/a2a/app.rb +112 -0
- data/lib/insika/server/a2a/client.rb +101 -0
- data/lib/insika/server/a2a/errors.rb +32 -0
- data/lib/insika/server/a2a/http.rb +42 -0
- data/lib/insika/server/a2a/message.rb +27 -0
- data/lib/insika/server/a2a/protocol.rb +45 -0
- data/lib/insika/server/a2a/remotes.rb +25 -0
- data/lib/insika/server/a2a/task_projection.rb +40 -0
- data/lib/insika/server/app.rb +1022 -0
- data/lib/insika/server/boot.rb +119 -0
- data/lib/insika/server/rack_app.rb +118 -0
- data/lib/insika/server/responses.rb +165 -0
- data/lib/insika/server/sse_body.rb +96 -0
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +162 -0
- data/lib/insika/session_store.rb +143 -0
- data/lib/insika/settings_store.rb +154 -0
- data/lib/insika/shutdown.rb +125 -0
- data/lib/insika/skill_catalog.rb +220 -0
- data/lib/insika/skill_store.rb +127 -0
- data/lib/insika/steer_injector.rb +110 -0
- data/lib/insika/store.rb +52 -0
- data/lib/insika/stores/memory.rb +123 -0
- data/lib/insika/stores/sqlite.rb +183 -0
- data/lib/insika/studio/app.rb +1693 -0
- data/lib/insika/studio/assets/dist/application.css +1 -0
- data/lib/insika/studio/assets/dist/application.js +70 -0
- data/lib/insika/studio/forms.rb +335 -0
- data/lib/insika/studio/nav_icons.rb +31 -0
- data/lib/insika/studio/views/_message.erb +44 -0
- data/lib/insika/studio/views/agent_detail.erb +285 -0
- data/lib/insika/studio/views/agents.erb +63 -0
- data/lib/insika/studio/views/approvals.erb +41 -0
- data/lib/insika/studio/views/chats.erb +34 -0
- data/lib/insika/studio/views/evals.erb +83 -0
- data/lib/insika/studio/views/home.erb +72 -0
- data/lib/insika/studio/views/layout.erb +94 -0
- data/lib/insika/studio/views/login.erb +17 -0
- data/lib/insika/studio/views/mcp.erb +91 -0
- data/lib/insika/studio/views/not_found.erb +5 -0
- data/lib/insika/studio/views/playground.erb +47 -0
- data/lib/insika/studio/views/refinement.erb +234 -0
- data/lib/insika/studio/views/session.erb +137 -0
- data/lib/insika/studio/views/settings.erb +168 -0
- data/lib/insika/studio/views/skills.erb +141 -0
- data/lib/insika/studio/views/system_files.erb +65 -0
- data/lib/insika/studio/views/task.erb +105 -0
- data/lib/insika/studio/views/tasks.erb +33 -0
- data/lib/insika/studio/views/tool_edit.erb +107 -0
- data/lib/insika/studio/views/tools.erb +89 -0
- data/lib/insika/subagent_graph.rb +96 -0
- data/lib/insika/system_file_store.rb +96 -0
- data/lib/insika/task_actor.rb +128 -0
- data/lib/insika/task_store.rb +250 -0
- data/lib/insika/telemetry/pricing.rb +104 -0
- data/lib/insika/telemetry/recorder.rb +228 -0
- data/lib/insika/telemetry.rb +127 -0
- data/lib/insika/testing/store_contract.rb +270 -0
- data/lib/insika/tick.rb +122 -0
- data/lib/insika/token_estimator.rb +16 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +140 -0
- data/lib/insika/tool_catalog.rb +89 -0
- data/lib/insika/tool_definition.rb +518 -0
- data/lib/insika/tool_envelope.rb +140 -0
- data/lib/insika/tool_manifest.rb +218 -0
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_registry.rb +21 -0
- data/lib/insika/tool_store.rb +135 -0
- data/lib/insika/tool_trace_store.rb +92 -0
- data/lib/insika/tools/a2a_remote.rb +48 -0
- data/lib/insika/tools/agent_enum.rb +68 -0
- data/lib/insika/tools/concurrency.rb +54 -0
- data/lib/insika/tools/data_defined_tool.rb +219 -0
- data/lib/insika/tools/load_skill.rb +99 -0
- data/lib/insika/tools/remember.rb +53 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +75 -0
- data/lib/insika/tools/subagents.rb +77 -0
- data/lib/insika/tools/tool_search.rb +94 -0
- data/lib/insika/turn_output.rb +139 -0
- data/lib/insika/turn_state.rb +162 -0
- data/lib/insika/turn_timing.rb +56 -0
- data/lib/insika/usage_ledger.rb +47 -0
- data/lib/insika/version.rb +3 -1
- data/lib/insika/wiring/graph.rb +249 -0
- data/lib/insika/workflow.rb +185 -0
- data/lib/insika/workflow_registry.rb +33 -0
- data/lib/insika.rb +220 -4
- metadata +412 -8
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# Form → command-payload parsing for the Studio App. A mixin, NOT a
|
|
5
|
+
# standalone object: `include`d into App so every method keeps `self` == the
|
|
6
|
+
# App instance and reaches its request helpers (`presence`, `split_list`) and
|
|
7
|
+
# per-request state (`@agent`) exactly as before. Pure extraction — the bodies
|
|
8
|
+
# moved verbatim from app.rb; app_spec's POST cases are the safety net.
|
|
9
|
+
module Forms
|
|
10
|
+
def config_patch(r)
|
|
11
|
+
limits = @agent.limits.dup
|
|
12
|
+
# Always present via DEFAULT_LIMITS (tool_concurrency defaults to 1 = serial),
|
|
13
|
+
# so a blank field just keeps whatever is stored.
|
|
14
|
+
%w[turn_timeout tool_timeout tool_concurrency].each do |field|
|
|
15
|
+
v = presence(r.params[field])
|
|
16
|
+
limits[field.to_sym] = Integer(v) if v
|
|
17
|
+
end
|
|
18
|
+
# Per-agent edge overrides. Unlike the timeouts (always present via
|
|
19
|
+
# DEFAULT_LIMITS), these are OPT-IN keys: blank DELETES the override (inherit
|
|
20
|
+
# the platform edge settings); 0 explicitly disables for this agent.
|
|
21
|
+
%w[chat_rate_limit agent_token_ceiling].each do |field|
|
|
22
|
+
v = edge_int(r.params[field], field)
|
|
23
|
+
if v.nil?
|
|
24
|
+
limits.delete(field.to_sym)
|
|
25
|
+
else
|
|
26
|
+
limits[field.to_sym] = v
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
{
|
|
30
|
+
id: @agent.id,
|
|
31
|
+
model: presence(r.params["model"]),
|
|
32
|
+
provider: r.params["provider"].to_s,
|
|
33
|
+
memory: r.params["memory"] == "1",
|
|
34
|
+
limits: limits,
|
|
35
|
+
params: params_patch(r),
|
|
36
|
+
model_policy: model_policy_patch(r),
|
|
37
|
+
guardrails: guardrails_patch(r)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Guardrails config from the form. The config form OWNS these fields,
|
|
42
|
+
# so the patch reflects the whole guardrails state. String values round-trip
|
|
43
|
+
# cleanly through the JSON store; Safety::Config normalizes on read. A blank
|
|
44
|
+
# moderator drops the key (deterministic only).
|
|
45
|
+
def guardrails_patch(r)
|
|
46
|
+
out = {
|
|
47
|
+
"input" => r.params["guardrail_input"] == "1",
|
|
48
|
+
"output" => r.params["guardrail_output"] == "1",
|
|
49
|
+
"strictness" => presence(r.params["guardrail_strictness"]) || "medium"
|
|
50
|
+
}
|
|
51
|
+
(mod = presence(r.params["guardrail_moderator"])) && (out["moderator"] = mod)
|
|
52
|
+
responses = guardrail_responses_patch(r)
|
|
53
|
+
out["responses"] = responses unless responses.empty?
|
|
54
|
+
out
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Per-category safe-reply overrides (config over convention). Only
|
|
58
|
+
# the non-blank fields are persisted; Safety::Config normalizes on read.
|
|
59
|
+
def guardrail_responses_patch(r)
|
|
60
|
+
%w[default injection sexual abuse escalate].each_with_object({}) do |cat, acc|
|
|
61
|
+
(v = presence(r.params["guardrail_response_#{cat}"])) && (acc[cat] = v)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Per-agent generation params. The config form OWNS these fields, so
|
|
66
|
+
# the patch reflects the whole form state: a blank field drops the key (and an
|
|
67
|
+
# all-blank form clears params to {}). temperature/max_tokens MUST be Numeric —
|
|
68
|
+
# ModelSelection#apply_params guards on `numeric?` and silently skips a String —
|
|
69
|
+
# so coerce here; a malformed number is dropped rather than 500-ing the save.
|
|
70
|
+
# thinking is a reasoning-effort string (low/medium/high), applied verbatim.
|
|
71
|
+
def params_patch(r)
|
|
72
|
+
out = {}
|
|
73
|
+
t = coerce(r.params["temperature"]) { |v| Float(v) }
|
|
74
|
+
out["temperature"] = t unless t.nil?
|
|
75
|
+
m = coerce(r.params["max_tokens"]) { |v| Integer(v) }
|
|
76
|
+
out["max_tokens"] = m unless m.nil?
|
|
77
|
+
thinking = presence(r.params["thinking"])
|
|
78
|
+
out["thinking"] = thinking if thinking
|
|
79
|
+
out
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Per-agent model fence: { "allow" => [refs] } where a ref is
|
|
83
|
+
# "provider/model", "provider/*" or "model". Blank textarea -> nil (NO fence,
|
|
84
|
+
# all models — parity). Enforced on the RESOLVED model by the ModelResolver,
|
|
85
|
+
# so a per-chat pin can never escape it.
|
|
86
|
+
def model_policy_patch(r)
|
|
87
|
+
refs = split_list(r.params["model_policy_allow"])
|
|
88
|
+
refs.empty? ? nil : { "allow" => refs }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Parses a numeric-ish form field, returning nil for blank or unparseable input
|
|
92
|
+
# (never raises — a bad value must not turn a config save into a 500).
|
|
93
|
+
def coerce(raw)
|
|
94
|
+
v = presence(raw)
|
|
95
|
+
return nil unless v
|
|
96
|
+
|
|
97
|
+
yield(v)
|
|
98
|
+
rescue ArgumentError, TypeError
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Fields a tool CAN carry that this form does not render. The store REPLACES the
|
|
103
|
+
# record on write, so anything missing here is erased — a save that only fixed a
|
|
104
|
+
# typo in the description would silently drop them (the class of bug already
|
|
105
|
+
# paid for once). `stored` carries them through untouched.
|
|
106
|
+
UNEDITED_TOOL_FIELDS = %w[group tags halt_when].freeze
|
|
107
|
+
|
|
108
|
+
# :write_data_tool payload from the form. nested request/response;
|
|
109
|
+
# headers/query as "key=value" per line (same idiom as the MCP env —
|
|
110
|
+
# a masked secret comes back as a sentinel and is reconciled in the store).
|
|
111
|
+
# `stored` = the definition being edited (nil when creating).
|
|
112
|
+
def tool_patch(r, stored = nil)
|
|
113
|
+
preserved = (stored || {}).slice(*UNEDITED_TOOL_FIELDS).compact
|
|
114
|
+
preserved.merge(
|
|
115
|
+
name: presence(r.params["name"]),
|
|
116
|
+
description: r.params["description"].to_s,
|
|
117
|
+
parameters: parse_parameters(r.params["parameters"]),
|
|
118
|
+
request: {
|
|
119
|
+
method: presence(r.params["method"]) || "GET",
|
|
120
|
+
url: r.params["url"].to_s,
|
|
121
|
+
headers: parse_kv_lines(r.params["headers"]),
|
|
122
|
+
query: parse_kv_lines(r.params["query"]),
|
|
123
|
+
body: presence(r.params["body"])
|
|
124
|
+
},
|
|
125
|
+
response: {
|
|
126
|
+
extract: presence(r.params["extract"]) || "body_raw",
|
|
127
|
+
path: presence(r.params["path"])
|
|
128
|
+
},
|
|
129
|
+
secret_headers: split_list(r.params["secret_headers"]),
|
|
130
|
+
timeout: presence(r.params["timeout"])
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parameters, TWO accepted syntaxes in one field (auto-detected, no mode toggle):
|
|
135
|
+
# text starting with `{` is a full JSON Schema — the only form that expresses
|
|
136
|
+
# nesting (an array of objects, a nested object); anything else is the flat
|
|
137
|
+
# pipe-delimited sugar. This is what keeps the editor from being a lossy
|
|
138
|
+
# round-trip: a nested tool renders as JSON here and saves back as the same JSON,
|
|
139
|
+
# instead of being flattened into a shape its author never wrote.
|
|
140
|
+
def parse_parameters(text)
|
|
141
|
+
s = text.to_s.strip
|
|
142
|
+
return parse_param_lines(text) unless s.start_with?("{")
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
JSON.parse(s)
|
|
146
|
+
rescue JSON::ParserError => e
|
|
147
|
+
raise Insika::ValidationError, "parameters: invalid JSON Schema (#{e.message.lines.first.to_s.strip})"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Flat sugar: one line per param, pipe-delimited (CSP forbids JS for
|
|
152
|
+
# dynamic lines; a textarea is the honest path, like the MCP env):
|
|
153
|
+
# name | type | required|optional | description
|
|
154
|
+
def parse_param_lines(text)
|
|
155
|
+
text.to_s.each_line.filter_map do |line|
|
|
156
|
+
line = line.strip
|
|
157
|
+
next if line.empty? || line.start_with?("#")
|
|
158
|
+
|
|
159
|
+
name, type, req, desc = line.split("|", 4).map(&:strip)
|
|
160
|
+
next if name.to_s.empty?
|
|
161
|
+
|
|
162
|
+
{ "name" => name, "type" => (presence(type) || "string"),
|
|
163
|
+
"required" => req.to_s.downcase != "optional", "description" => desc.to_s }
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def settings_patch(r)
|
|
168
|
+
patch = {
|
|
169
|
+
"streaming" => r.params["streaming"] == "1"
|
|
170
|
+
}
|
|
171
|
+
{ "request_timeout" => "request_timeout", "max_retries" => "max_retries",
|
|
172
|
+
"turn_timeout" => "turn_timeout", "tool_timeout" => "tool_timeout" }.each_key do |f|
|
|
173
|
+
v = presence(r.params[f])
|
|
174
|
+
patch[f] = Integer(v) if v
|
|
175
|
+
end
|
|
176
|
+
patch
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Edge limits — the platform rate-limit/cost layer, saved
|
|
180
|
+
# from its OWN form (a sub-resource, like models). The limit fields write nil
|
|
181
|
+
# when blank (off — the EdgeLimiter reads nil/0 as off); the windows fall back
|
|
182
|
+
# to the built-in defaults when cleared (the limiter guards non-positive).
|
|
183
|
+
def edge_patch(r)
|
|
184
|
+
edge = {}
|
|
185
|
+
%w[chat_rate_limit chat_rate_window agent_token_ceiling agent_token_window].each do |f|
|
|
186
|
+
edge[f] = edge_int(r.params[f], f)
|
|
187
|
+
end
|
|
188
|
+
edge["limit_response"] = presence(r.params["limit_response"])
|
|
189
|
+
{ "edge" => edge }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# An eval case arrives as the YAML text the operator edited — the same shape the
|
|
193
|
+
# corpus files hold, so there is one format to learn and a pull request can review
|
|
194
|
+
# what was authored. Decoding happens HERE, at the transport edge; the SHAPE is
|
|
195
|
+
# validated by the one loader inside the store, so both doors agree.
|
|
196
|
+
def golden_patch(r)
|
|
197
|
+
parsed = begin
|
|
198
|
+
YAML.safe_load(r.params["yaml"].to_s, permitted_classes: [], aliases: false)
|
|
199
|
+
rescue Psych::SyntaxError => e
|
|
200
|
+
raise Insika::ValidationError, "invalid YAML: #{e.message}"
|
|
201
|
+
end
|
|
202
|
+
raise Insika::ValidationError, "the case must be a YAML mapping (id/agent/turns/expect)" unless parsed.is_a?(Hash)
|
|
203
|
+
|
|
204
|
+
{ case: parsed }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Evals graders. `judges` is one `provider/model` per LINE — a
|
|
208
|
+
# textarea, because the SIZE of the panel is the point and a fixed pair of fields
|
|
209
|
+
# would cap it at two. A bare `model` (no slash) is valid: the provider is then
|
|
210
|
+
# inferred by RubyLLM, same as everywhere else.
|
|
211
|
+
def evals_patch(r)
|
|
212
|
+
judges = r.params["judges"].to_s.lines.filter_map do |line|
|
|
213
|
+
ref = presence(line)
|
|
214
|
+
next unless ref
|
|
215
|
+
|
|
216
|
+
provider, model = ref.include?("/") ? ref.split("/", 2) : [nil, ref]
|
|
217
|
+
{ "provider" => presence(provider), "model" => presence(model) }.compact
|
|
218
|
+
end.reject { |j| j["model"].nil? }
|
|
219
|
+
|
|
220
|
+
{
|
|
221
|
+
"evals" => {
|
|
222
|
+
"judges" => judges,
|
|
223
|
+
"aggregate" => %w[median mean min].include?(r.params["aggregate"]) ? r.params["aggregate"] : "median",
|
|
224
|
+
# Same strictness as the edge fields: a typo must not silently become "off"
|
|
225
|
+
# (min_agreement 0 would pass a case no judge liked).
|
|
226
|
+
"min_agreement" => edge_float(r.params["min_agreement"], "min_agreement"),
|
|
227
|
+
"quorum" => edge_int(r.params["quorum"], "quorum"),
|
|
228
|
+
"tolerance" => edge_float(r.params["tolerance"], "tolerance")
|
|
229
|
+
}.compact
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Strict float, blank = nil (inherit the default). Mirrors edge_int.
|
|
234
|
+
def edge_float(raw, field)
|
|
235
|
+
v = presence(raw)
|
|
236
|
+
return nil unless v
|
|
237
|
+
|
|
238
|
+
Float(v)
|
|
239
|
+
rescue ArgumentError, TypeError
|
|
240
|
+
raise Insika::ValidationError, "#{field} must be a number (got #{raw.inspect})"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Strict integer for the edge fields: blank = nil (off / inherit — intentional),
|
|
244
|
+
# but an UNPARSEABLE value must not silently disable a production limit (the
|
|
245
|
+
# `coerce` drop-to-nil semantics would turn a typo into "off" with a green
|
|
246
|
+
# flash). ValidationError -> with_flash renders it as the red flash, no dispatch.
|
|
247
|
+
def edge_int(raw, field)
|
|
248
|
+
v = presence(raw)
|
|
249
|
+
return nil unless v
|
|
250
|
+
|
|
251
|
+
Integer(v)
|
|
252
|
+
rescue ArgumentError, TypeError
|
|
253
|
+
raise Insika::ValidationError, "#{field} must be an integer (got #{raw.inspect})"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# LLM config v2 — the platform model layer, saved from its OWN form (a
|
|
257
|
+
# sub-resource, like providers) so the general-settings save never touches it and
|
|
258
|
+
# vice-versa. The scalar refs are always present (blank -> nil, which the
|
|
259
|
+
# deep-merge writes and the ModelResolver reads as "no platform default");
|
|
260
|
+
# fallback_models is a full-list replace (deep_merge substitutes arrays, so a
|
|
261
|
+
# resubmit never accumulates).
|
|
262
|
+
def model_defaults_patch(r)
|
|
263
|
+
{
|
|
264
|
+
"default_model" => presence(r.params["default_model"]),
|
|
265
|
+
"default_provider" => presence(r.params["default_provider"]),
|
|
266
|
+
"utility_model" => presence(r.params["utility_model"]),
|
|
267
|
+
"fallback_models" => split_list(r.params["fallback_models"]),
|
|
268
|
+
# GLOBAL reasoning default (4-layer). Blank -> nil = provider default.
|
|
269
|
+
"thinking" => presence(r.params["thinking"])
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Per-model reasoning defaults (the per-model layer). One line per model:
|
|
274
|
+
# <provider/model | model> | <off|on|low|medium|high>
|
|
275
|
+
# CSP forbids JS for dynamic rows, so a textarea of lines is the honest path
|
|
276
|
+
# (same idiom as the MCP env / tool params). A blank effort -> {} for that ref
|
|
277
|
+
# (an inherit no-op). deep_merge merges per-ref, so refs accumulate; clearing an
|
|
278
|
+
# override = set its effort blank (removing the ref entirely is a later refinement).
|
|
279
|
+
def model_params_patch(r)
|
|
280
|
+
map = r.params["model_params"].to_s.each_line.each_with_object({}) do |line, acc|
|
|
281
|
+
line = line.strip
|
|
282
|
+
next if line.empty? || line.start_with?("#")
|
|
283
|
+
|
|
284
|
+
ref, thinking = line.split("|", 2).map(&:strip)
|
|
285
|
+
next if ref.to_s.empty?
|
|
286
|
+
|
|
287
|
+
acc[ref] = { "thinking" => presence(thinking) }.compact
|
|
288
|
+
end
|
|
289
|
+
{ "model_params" => map }
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# LLM provider from the form. api_key is sentinel-aware: the form
|
|
293
|
+
# pre-fills with the sentinel when a key already exists, so resubmitting
|
|
294
|
+
# without touching preserves it; a new string replaces it; "" clears it. models = CSV.
|
|
295
|
+
def provider_patch(r)
|
|
296
|
+
{
|
|
297
|
+
api: presence(r.params["api"]),
|
|
298
|
+
base_url: r.params["base_url"].to_s,
|
|
299
|
+
auth_header: r.params["auth_header"].to_s,
|
|
300
|
+
api_key: r.params["api_key"].to_s,
|
|
301
|
+
models: split_list(r.params["models"])
|
|
302
|
+
}
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# MCP instance from the form. `env` comes as "KEY=value" lines (CSP
|
|
306
|
+
# forbids inline JS for add/remove line; a textarea is the simple, honest
|
|
307
|
+
# path). Masked values come back as a sentinel — keeping them preserves
|
|
308
|
+
# the secret; changing replaces; deleting the line clears it.
|
|
309
|
+
def mcp_patch(r)
|
|
310
|
+
{
|
|
311
|
+
name: presence(r.params["name"]),
|
|
312
|
+
transport: presence(r.params["transport"]) || "stdio",
|
|
313
|
+
command: r.params["command"].to_s,
|
|
314
|
+
url: r.params["url"].to_s,
|
|
315
|
+
description: r.params["description"].to_s,
|
|
316
|
+
enabled: r.params["enabled"] == "1",
|
|
317
|
+
env: parse_kv_lines(r.params["env"])
|
|
318
|
+
}
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# "KEY=value" per line -> Hash. Ignores blank lines and comments (#).
|
|
322
|
+
def parse_kv_lines(text)
|
|
323
|
+
text.to_s.each_line.filter_map do |line|
|
|
324
|
+
line = line.strip
|
|
325
|
+
next if line.empty? || line.start_with?("#")
|
|
326
|
+
|
|
327
|
+
k, v = line.split("=", 2)
|
|
328
|
+
k = k.to_s.strip
|
|
329
|
+
next if k.empty?
|
|
330
|
+
|
|
331
|
+
[k, v.to_s.strip]
|
|
332
|
+
end.to_h
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Studio
|
|
4
|
+
# Nav SVG icons for the Studio App. A mixin included into App: the
|
|
5
|
+
# `nav_icon` view helper and its icon table, moved verbatim out of app.rb.
|
|
6
|
+
# Inline SVG is CSP-safe (an HTML element, not an external resource).
|
|
7
|
+
module NavIcons
|
|
8
|
+
# 20×20 stroke icons (currentColor), one per nav key. Source: a Lucide-style
|
|
9
|
+
# line icon set.
|
|
10
|
+
NAV_ICONS = {
|
|
11
|
+
home: '<path d="M3 9.5 12 3l9 6.5"/><path d="M5 10v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V10"/><path d="M9 21v-6h6v6"/>',
|
|
12
|
+
agents: '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/>',
|
|
13
|
+
skills: '<path d="m12 3-1.9 5.8a2 2 0 0 1-1.3 1.3L3 12l5.8 1.9a2 2 0 0 1 1.3 1.3L12 21l1.9-5.8a2 2 0 0 1 1.3-1.3L21 12l-5.8-1.9a2 2 0 0 1-1.3-1.3z"/>',
|
|
14
|
+
tools: '<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>',
|
|
15
|
+
system: '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h8"/>',
|
|
16
|
+
mcp: '<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/>',
|
|
17
|
+
settings: '<path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/>',
|
|
18
|
+
chats: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
|
|
19
|
+
playground: '<polygon points="6 3 20 12 6 21 6 3"/>',
|
|
20
|
+
tasks: '<path d="M11 12H3"/><path d="M16 6H3"/><path d="M16 18H3"/><path d="m18 9 3 3-3 3"/>',
|
|
21
|
+
approvals: '<path d="M9 12l2 2 4-4"/><path d="M12 3a9 9 0 1 0 9 9"/>',
|
|
22
|
+
refinement: '<path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 3v5h-5"/><path d="m9 12 2 2 4-4"/>',
|
|
23
|
+
evals: '<path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>'
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def nav_icon(key)
|
|
27
|
+
%(<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" ) +
|
|
28
|
+
%(stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">#{NAV_ICONS[key]}</svg>)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<%# Shared transcript bubble/tool-card — one visual language for the
|
|
2
|
+
playground history, the optimistic echo and the read-only session viewer.
|
|
3
|
+
Locals: m = stored message hash {"role","content","at"}; prev = the message before
|
|
4
|
+
it in the thread, or nil (used only to spot a steered message). `content` may be a
|
|
5
|
+
String or a structured payload (rendered as pretty JSON, never #inspect).
|
|
6
|
+
Assistant bubbles carry data-controller="markdown" (markdown.js re-escapes
|
|
7
|
+
before markup — safe under the strict CSP). %>
|
|
8
|
+
<% role = m["role"].to_s %>
|
|
9
|
+
<% at = short_time(m["at"]) %>
|
|
10
|
+
<% if role == "tool" %>
|
|
11
|
+
<details class="toolcard result">
|
|
12
|
+
<summary><span class="arrow">←</span><strong>tool result</strong><% unless at.empty? %><time class="stamp" datetime="<%= m["at"] %>"><%= at %></time><% end %></summary>
|
|
13
|
+
<pre><%= message_content(m["content"]) %></pre>
|
|
14
|
+
</details>
|
|
15
|
+
<% else %>
|
|
16
|
+
<div class="msg <%= role %>">
|
|
17
|
+
<div class="who"><%= role[0, 1].upcase %></div>
|
|
18
|
+
<div class="bubble">
|
|
19
|
+
<%# `role` says where a message sits; `origin` says who wrote it, and the two
|
|
20
|
+
come apart (the engine delivering a subagent result as a "user" turn, a
|
|
21
|
+
guardrail's canned reply, a human operator after a handoff). Shown only
|
|
22
|
+
when it is NOT the natural producer for the role — otherwise every bubble
|
|
23
|
+
would carry a label that says nothing. %>
|
|
24
|
+
<% origin = Insika::MessageOrigin.origin_of(m) unless Insika::MessageOrigin.customer?(m) || Insika::MessageOrigin.agent?(m) %>
|
|
25
|
+
<%# STEERED: a customer message sitting right after a tool result arrived
|
|
26
|
+
while the turn was running and was appended at a tool-batch boundary. Derived
|
|
27
|
+
from position, not from a field — a steered message declares no origin, and it
|
|
28
|
+
is right not to: a person typed it. Nothing else in the engine ever puts a user
|
|
29
|
+
message after a tool result. Without the marker an operator reads two customer
|
|
30
|
+
messages in one turn with no explanation. %>
|
|
31
|
+
<% steered = role == "user" && prev && prev["role"].to_s == "tool" %>
|
|
32
|
+
<div class="role-tag"><%= role %><% if origin %> · <span class="origin"><%= origin %></span><% end %><% if steered %> · <span class="origin">steered</span><% end %><% unless at.empty? %> · <time class="stamp" datetime="<%= m["at"] %>"><%= at %></time><% end %></div>
|
|
33
|
+
<% unless m["content"].to_s.empty? && Array(m["tool_calls"]).any? %>
|
|
34
|
+
<div class="content<%== ' md' if role == 'assistant' %>"<%== ' data-controller="markdown"' if role == 'assistant' %>><%= message_content(m["content"]) %></div>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% Array(m["tool_calls"]).each do |tc| %>
|
|
37
|
+
<details class="toolcard">
|
|
38
|
+
<summary><span class="arrow">→</span><strong><%= tc["name"] %></strong></summary>
|
|
39
|
+
<pre><%= message_content(tc["arguments"]) %></pre>
|
|
40
|
+
</details>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|