insika 0.8.0 → 1.0.1
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 +312 -2
- data/README.md +10 -6
- data/bin/insika +4 -1
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +142 -18
- data/docs/API.md +73 -0
- data/docs/ARCHITECTURE.md +56 -50
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CHANNELS.md +19 -2
- data/docs/CONTEXT.md +96 -27
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +70 -13
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +129 -10
- data/docs/FACTS.md +12 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +14 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +164 -28
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +26 -14
- data/docs/RELEASING.md +9 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +34 -1
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +238 -42
- data/docs/operate.md +3 -0
- data/docs/prompts/ADD-TOOL.md +12 -2
- data/docs/prompts/DIAGNOSE-TURN.md +3 -0
- data/docs/prompts/GO-LIVE.md +3 -1
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +99 -17
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/channels/web/widget.js +33 -0
- data/lib/insika/channels/web.rb +5 -2
- data/lib/insika/chat_builder.rb +124 -28
- data/lib/insika/checkpoint.rb +6 -1
- data/lib/insika/checkpoint_store.rb +59 -7
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/delete_kit.rb +26 -0
- data/lib/insika/commands/gate_harvest.rb +3 -0
- data/lib/insika/commands/review_guardrail_flag.rb +38 -0
- data/lib/insika/commands/run_distillation.rb +5 -8
- data/lib/insika/commands/run_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +128 -0
- data/lib/insika/commands/send_message.rb +9 -3
- data/lib/insika/commands/update_settings.rb +23 -1
- data/lib/insika/commands/write_kit.rb +27 -0
- data/lib/insika/compaction.rb +33 -7
- data/lib/insika/config_store.rb +87 -7
- data/lib/insika/context/builder.rb +29 -9
- data/lib/insika/context/priority.rb +5 -0
- data/lib/insika/context/provider.rb +7 -2
- data/lib/insika/context/providers/briefing.rb +11 -8
- data/lib/insika/context/providers/fence_notice.rb +27 -0
- data/lib/insika/context/providers/knowledge.rb +32 -6
- data/lib/insika/context/providers/memory.rb +61 -8
- data/lib/insika/context/providers/pending_confirmation.rb +52 -0
- data/lib/insika/context/providers/prompt.rb +17 -4
- data/lib/insika/context/providers/session.rb +46 -6
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +142 -4
- data/lib/insika/dsl/runtime.rb +37 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +19 -5
- data/lib/insika/edge_limiter.rb +27 -16
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/errors.rb +1 -0
- data/lib/insika/evals/assertions.rb +259 -18
- data/lib/insika/evals/golden.rb +230 -4
- data/lib/insika/evals/judge.rb +19 -10
- data/lib/insika/evals/moderator_compare.rb +47 -0
- data/lib/insika/evals/report.rb +61 -9
- data/lib/insika/evals/runner.rb +64 -4
- data/lib/insika/evals/simulator.rb +21 -3
- data/lib/insika/evals/transport.rb +317 -15
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +105 -17
- data/lib/insika/executor.rb +698 -164
- data/lib/insika/fence.rb +96 -0
- data/lib/insika/golden_store.rb +3 -0
- data/lib/insika/harvest.rb +26 -14
- data/lib/insika/harvest_engine.rb +1 -0
- data/lib/insika/harvest_store.rb +12 -0
- data/lib/insika/http_client.rb +87 -4
- data/lib/insika/kits.rb +54 -0
- data/lib/insika/knowledge.rb +34 -20
- data/lib/insika/llm_configurator.rb +46 -4
- data/lib/insika/llm_provider_store.rb +1 -3
- data/lib/insika/llm_trace_store.rb +54 -0
- data/lib/insika/mcp_client.rb +27 -63
- data/lib/insika/mcp_live_tool.rb +55 -9
- data/lib/insika/mcp_store.rb +37 -3
- data/lib/insika/mcp_tool_registry.rb +76 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +16 -3
- data/lib/insika/memory_trim.rb +60 -0
- data/lib/insika/middleware.rb +12 -3
- data/lib/insika/model_metrics_store.rb +285 -0
- data/lib/insika/model_registry.rb +110 -0
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +14 -21
- data/lib/insika/model_visible.rb +17 -6
- data/lib/insika/onboarding.rb +1 -0
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +30 -2
- data/lib/insika/pack_importer.rb +54 -9
- data/lib/insika/packaging.rb +14 -5
- data/lib/insika/pending_action_store.rb +45 -18
- data/lib/insika/plugin/loader.rb +4 -2
- data/lib/insika/prefix_fingerprint.rb +32 -27
- data/lib/insika/profile_source.rb +5 -0
- data/lib/insika/provider_error_classifier.rb +18 -3
- data/lib/insika/queue_policy.rb +7 -2
- data/lib/insika/refinement/proposer.rb +42 -17
- data/lib/insika/reliability.rb +12 -3
- data/lib/insika/router/app.rb +14 -5
- data/lib/insika/router/session_key.rb +35 -5
- data/lib/insika/safety/config.rb +20 -3
- data/lib/insika/safety/corpus.rb +10 -3
- data/lib/insika/safety/factory.rb +39 -1
- data/lib/insika/safety/input_guardrail.rb +21 -1
- data/lib/insika/safety/judge_moderator.rb +70 -0
- data/lib/insika/safety/output_validator.rb +29 -5
- data/lib/insika/safety/prompt_echo.rb +89 -0
- data/lib/insika/schema_guard.rb +53 -76
- data/lib/insika/server/app.rb +58 -7
- data/lib/insika/server/rack_app.rb +2 -0
- data/lib/insika/server/responses.rb +50 -5
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +163 -11
- data/lib/insika/settings_store.rb +57 -2
- data/lib/insika/shared_conversations.rb +305 -0
- data/lib/insika/skill_catalog.rb +28 -2
- data/lib/insika/skill_store.rb +6 -0
- data/lib/insika/soak/runner.rb +1 -1
- data/lib/insika/spoken_transcript.rb +31 -0
- data/lib/insika/steer_injector.rb +7 -48
- data/lib/insika/store.rb +29 -1
- data/lib/insika/stores/json_value.rb +37 -0
- data/lib/insika/stores/memory.rb +22 -35
- data/lib/insika/stores/postgres.rb +295 -0
- data/lib/insika/stores/sqlite.rb +257 -43
- data/lib/insika/stores/turn_counter.rb +25 -0
- data/lib/insika/studio/app.rb +1459 -163
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +25 -25
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
- data/lib/insika/studio/assets/dist/fonts/ibm-plex-sans-latin-wght-normal.woff2 +0 -0
- data/lib/insika/studio/forms.rb +189 -12
- data/lib/insika/studio/nav_icons.rb +3 -0
- data/lib/insika/studio/views/_agent_config_retrieval.erb +73 -0
- data/lib/insika/studio/views/_agent_picker.erb +15 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +83 -243
- data/lib/insika/studio/views/_agent_tab_history.erb +56 -1
- data/lib/insika/studio/views/_agent_tab_performance.erb +34 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +7 -9
- data/lib/insika/studio/views/_agent_tab_skills.erb +16 -7
- data/lib/insika/studio/views/_agent_tab_tools.erb +90 -0
- data/lib/insika/studio/views/_agents_master.erb +8 -12
- data/lib/insika/studio/views/_cfg_alerts.erb +4 -0
- data/lib/insika/studio/views/_cfg_budget.erb +20 -0
- data/lib/insika/studio/views/_cfg_distill.erb +26 -0
- data/lib/insika/studio/views/_cfg_followup.erb +30 -0
- data/lib/insika/studio/views/_cfg_grounding.erb +21 -0
- data/lib/insika/studio/views/_cfg_guardrails.erb +47 -0
- data/lib/insika/studio/views/_cfg_harvest.erb +35 -0
- data/lib/insika/studio/views/_cfg_reliability.erb +32 -0
- data/lib/insika/studio/views/_chats_master.erb +66 -0
- data/lib/insika/studio/views/_customers_master.erb +37 -0
- data/lib/insika/studio/views/_empty.erb +15 -0
- data/lib/insika/studio/views/_facts_master.erb +42 -0
- data/lib/insika/studio/views/_harvest_candidate_detail.erb +14 -0
- data/lib/insika/studio/views/_identity.erb +16 -0
- data/lib/insika/studio/views/_kpi.erb +17 -0
- data/lib/insika/studio/views/_message.erb +40 -2
- data/lib/insika/studio/views/_model_chart.erb +82 -0
- data/lib/insika/studio/views/_refinement_master.erb +38 -0
- data/lib/insika/studio/views/{tool_edit.erb → _tool_edit.erb} +38 -11
- data/lib/insika/studio/views/_tool_users.erb +17 -0
- data/lib/insika/studio/views/_why.erb +53 -0
- data/lib/insika/studio/views/agent_detail.erb +17 -10
- data/lib/insika/studio/views/agents.erb +28 -16
- data/lib/insika/studio/views/approvals.erb +16 -11
- data/lib/insika/studio/views/artifact.erb +20 -7
- data/lib/insika/studio/views/artifacts.erb +19 -14
- data/lib/insika/studio/views/chats.erb +22 -29
- data/lib/insika/studio/views/customer.erb +88 -43
- data/lib/insika/studio/views/customers.erb +16 -26
- data/lib/insika/studio/views/evals.erb +49 -31
- data/lib/insika/studio/views/fact_detail.erb +114 -0
- data/lib/insika/studio/views/facts.erb +15 -116
- data/lib/insika/studio/views/followups.erb +63 -52
- data/lib/insika/studio/views/funnel.erb +104 -43
- data/lib/insika/studio/views/harvest.erb +85 -59
- data/lib/insika/studio/views/home.erb +138 -23
- data/lib/insika/studio/views/inbox.erb +36 -0
- data/lib/insika/studio/views/kits.erb +176 -0
- data/lib/insika/studio/views/knowledge.erb +92 -80
- data/lib/insika/studio/views/layout.erb +42 -11
- data/lib/insika/studio/views/live.erb +45 -0
- data/lib/insika/studio/views/mcp.erb +43 -13
- data/lib/insika/studio/views/models.erb +187 -0
- data/lib/insika/studio/views/parity.erb +38 -32
- data/lib/insika/studio/views/playground.erb +13 -9
- data/lib/insika/studio/views/refinement.erb +15 -224
- data/lib/insika/studio/views/refinement_detail.erb +243 -0
- data/lib/insika/studio/views/session.erb +153 -118
- data/lib/insika/studio/views/settings.erb +86 -22
- data/lib/insika/studio/views/skills.erb +53 -19
- data/lib/insika/studio/views/system_files.erb +7 -7
- data/lib/insika/studio/views/task.erb +45 -15
- data/lib/insika/studio/views/tasks.erb +35 -18
- data/lib/insika/studio/views/tools.erb +62 -73
- data/lib/insika/task_actor.rb +4 -1
- data/lib/insika/task_store.rb +67 -1
- data/lib/insika/telemetry/pricing.rb +16 -14
- data/lib/insika/telemetry/recorder.rb +115 -9
- data/lib/insika/telemetry/ruby_llm_instrumenter.rb +110 -0
- data/lib/insika/telemetry.rb +12 -2
- data/lib/insika/templates/daily-digest/README.md +13 -0
- data/lib/insika/templates/daily-digest/agent.rb +22 -11
- data/lib/insika/testing/store_contract.rb +65 -0
- data/lib/insika/text_tool_call.rb +66 -0
- data/lib/insika/tick.rb +7 -1
- data/lib/insika/token_store.rb +5 -0
- data/lib/insika/tool_assembly.rb +21 -13
- data/lib/insika/tool_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +82 -13
- data/lib/insika/tool_envelope.rb +266 -29
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +12 -4
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +14 -4
- data/lib/insika/tools/a2a_remote.rb +1 -1
- data/lib/insika/tools/agent_enum.rb +1 -1
- data/lib/insika/tools/confirm_pending.rb +112 -0
- data/lib/insika/tools/data_defined_tool.rb +24 -15
- data/lib/insika/tools/generate_image.rb +2 -2
- data/lib/insika/tools/load_knowledge.rb +7 -3
- data/lib/insika/tools/load_skill.rb +3 -3
- data/lib/insika/tools/present.rb +115 -0
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +9 -6
- data/lib/insika/tools/save_artifact.rb +16 -6
- data/lib/insika/tools/schedule_followup.rb +3 -3
- data/lib/insika/tools/stuck_signal.rb +3 -3
- data/lib/insika/tools/subagent.rb +5 -5
- data/lib/insika/tools/subagents.rb +3 -3
- data/lib/insika/tools/tool_search.rb +11 -7
- data/lib/insika/tools/tts.rb +2 -2
- data/lib/insika/tools/update_briefing.rb +4 -4
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +31 -1
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +88 -16
- data/lib/insika/wiring/graph_chat.rb +4 -0
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +23 -3
- metadata +74 -13
- data/lib/insika/commands/import_mcp_tools.rb +0 -48
- data/lib/insika/mcp_http_client.rb +0 -67
- data/lib/insika/mcp_tool_ingestor.rb +0 -165
data/lib/insika/studio/forms.rb
CHANGED
|
@@ -45,11 +45,36 @@ module Studio
|
|
|
45
45
|
end
|
|
46
46
|
smm = edge_int(r.params["steer_max_messages"], "steer_max_messages")
|
|
47
47
|
smm.nil? ? limits.delete(:steer_max_messages) : limits[:steer_max_messages] = smm
|
|
48
|
+
# The debounce window is what makes `collect`/`steer` merge a burst AT THE
|
|
49
|
+
# DOOR; without it a steer agent only ever absorbs mid-run, at a tool
|
|
50
|
+
# boundary, and a turn that never calls one absorbs nothing. Same opt-in
|
|
51
|
+
# convention as above: blank DELETES, 0 = no window.
|
|
52
|
+
%w[debounce_ms debounce_max_ms].each do |field|
|
|
53
|
+
v = edge_int(r.params[field], field)
|
|
54
|
+
v.nil? ? limits.delete(field.to_sym) : limits[field.to_sym] = v
|
|
55
|
+
end
|
|
56
|
+
# Refused HERE (config time) and not at resolve time: a template without
|
|
57
|
+
# the placeholder drops the customer's message while the turn still looks
|
|
58
|
+
# steered — the same rule QueuePolicy#join! enforces, moved to where the
|
|
59
|
+
# operator is.
|
|
60
|
+
if (join = presence(r.params["steer_join"]))
|
|
61
|
+
unless join.include?(Insika::QueuePolicy::JOIN_PLACEHOLDER)
|
|
62
|
+
raise Insika::ValidationError,
|
|
63
|
+
"steer_join must contain #{Insika::QueuePolicy::JOIN_PLACEHOLDER}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
limits[:steer_join] = join
|
|
67
|
+
else
|
|
68
|
+
limits.delete(:steer_join)
|
|
69
|
+
end
|
|
70
|
+
model, provider = picked_model(r, model_ref_of(@agent.model, @agent.provider))
|
|
48
71
|
{
|
|
49
72
|
id: @agent.id,
|
|
50
|
-
model:
|
|
51
|
-
provider:
|
|
73
|
+
model: model,
|
|
74
|
+
provider: provider.to_s,
|
|
52
75
|
memory: r.params["memory"] == "1",
|
|
76
|
+
memory_retrieval: memory_retrieval_patch(r),
|
|
77
|
+
knowledge: knowledge_patch(r),
|
|
53
78
|
limits: limits,
|
|
54
79
|
params: params_patch(r),
|
|
55
80
|
model_policy: model_policy_patch(r),
|
|
@@ -77,6 +102,7 @@ module Studio
|
|
|
77
102
|
# unchecked box saves an explicit false — both spellings of ON collapse.
|
|
78
103
|
tool_persistence: r.params["tool_persistence"] == "1",
|
|
79
104
|
tool_output_compression: r.params["tool_output_compression"] == "1",
|
|
105
|
+
fencing: r.params["fencing"] == "1",
|
|
80
106
|
subagents: list_patch(r, "subagents"),
|
|
81
107
|
capabilities: list_patch(r, "capabilities"),
|
|
82
108
|
tools_deferred: list_patch(r, "tools_deferred"),
|
|
@@ -84,11 +110,99 @@ module Studio
|
|
|
84
110
|
context_providers: allowlist_patch(r, "context_providers"),
|
|
85
111
|
workflows_allow: allowlist_patch(r, "workflows_allow"),
|
|
86
112
|
approvals_required: list_patch(r, "approvals_required"),
|
|
113
|
+
customer_confirm: list_patch(r, "customer_confirm"),
|
|
87
114
|
policies: list_patch(r, "policies").map(&:to_sym),
|
|
88
115
|
prompt_refs: list_patch(r, "prompt_refs"),
|
|
89
116
|
capabilities_declared: list_patch(r, "capabilities_declared"),
|
|
90
117
|
skills_eager: skills_eager_patch(r)
|
|
91
|
-
}
|
|
118
|
+
}.merge(inherited_fields(r))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The Studio picker posts `model_ref` (checked against the list; `kept` = the
|
|
122
|
+
# stored ref, allowed back unchanged). Without it, the plain `model`/`provider`
|
|
123
|
+
# pair passes as given (API clients, older forms). -> [model | nil, provider | nil]
|
|
124
|
+
def picked_model(r, kept = nil)
|
|
125
|
+
return model_ref_parts(r.params["model"], r.params["provider"]) unless r.params.key?("model_ref")
|
|
126
|
+
|
|
127
|
+
model_ref_parts(checked_model_ref(r.params["model_ref"], kept), nil)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Any other model field the Studio renders as a picker posts "<field>_ref" (one
|
|
131
|
+
# ref) or "<field>_refs[]" (one per slot), checked like the agent model; `kept`
|
|
132
|
+
# (the stored value) may come back unchanged. The plain field passes as given.
|
|
133
|
+
def ref_param(r, field, kept)
|
|
134
|
+
key = "#{field}_ref"
|
|
135
|
+
r.params.key?(key) ? checked_model_ref(r.params[key], kept) : presence(r.params[field])
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ref_list_param(r, field, kept)
|
|
139
|
+
key = "#{field}_refs"
|
|
140
|
+
return list(r.params[field]) unless r.params.key?(key)
|
|
141
|
+
|
|
142
|
+
Array(r.params[key]).filter_map { |ref| checked_model_ref(ref, Array(kept)) }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# ONE ref, "provider/model" (an OpenRouter model keeps its own slash:
|
|
146
|
+
# "openrouter/deepseek/x"). A separate `provider` wins as given.
|
|
147
|
+
def model_ref_parts(ref, provider)
|
|
148
|
+
model = presence(ref)
|
|
149
|
+
return [model, presence(provider)] if model.nil? || !presence(provider).nil? || !model.include?("/")
|
|
150
|
+
|
|
151
|
+
provider, model = model.split("/", 2)
|
|
152
|
+
[model, provider]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The sections whose "use the platform default" box is checked save nil, so
|
|
156
|
+
# the agent keeps inheriting (Insika::AgentDefaults).
|
|
157
|
+
def inherited_fields(r)
|
|
158
|
+
(Array(r.params["inherit"]) & Insika::AgentDefaults::FIELDS).to_h { |field| [field.to_sym, nil] }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The platform agent defaults form: the same sections and parsers as the
|
|
162
|
+
# agent form, read against the current defaults (so pack-only keys survive).
|
|
163
|
+
def agent_defaults_patch(r)
|
|
164
|
+
@agent = agent_defaults_profile
|
|
165
|
+
Insika::AgentDefaults::FIELDS.to_h { |field| [field, public_send(:"#{field}_patch", r)] }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def memory_retrieval_patch(r)
|
|
169
|
+
rerank = retrieval_rerank_patch(r, "memory")
|
|
170
|
+
return nil unless rerank
|
|
171
|
+
|
|
172
|
+
{ "top_k" => retrieval_top_k(r, "memory"), "rerank" => rerank }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def knowledge_patch(r)
|
|
176
|
+
# Preserve pack options the form does not expose, such as the index.
|
|
177
|
+
out = (@agent.knowledge || {}).except("extract", "retrieve", "top_k", "types", "model", "prompt", "rerank")
|
|
178
|
+
out.merge!("extract" => r.params["knowledge_extract"] == "1",
|
|
179
|
+
"retrieve" => r.params["knowledge_retrieve"] == "1",
|
|
180
|
+
"top_k" => retrieval_top_k(r, "knowledge"))
|
|
181
|
+
types = list(r.params["knowledge_types"])
|
|
182
|
+
out["types"] = types unless types.empty?
|
|
183
|
+
(m = ref_param(r, "knowledge_model", (@agent.knowledge || {})["model"])) && (out["model"] = m)
|
|
184
|
+
(p = presence(r.params["knowledge_prompt"])) && (out["prompt"] = p)
|
|
185
|
+
rerank = retrieval_rerank_patch(r, "knowledge")
|
|
186
|
+
out["rerank"] = rerank if rerank
|
|
187
|
+
out
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def retrieval_top_k(r, source)
|
|
191
|
+
field = "#{source}_top_k"
|
|
192
|
+
value = edge_int(r.params[field], field) || 5
|
|
193
|
+
raise Insika::ValidationError, "#{field} must be positive" unless value.positive?
|
|
194
|
+
|
|
195
|
+
value
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def retrieval_rerank_patch(r, source)
|
|
199
|
+
return nil unless r.params["#{source}_rerank_enabled"] == "1"
|
|
200
|
+
|
|
201
|
+
prefix = "#{source}_rerank"
|
|
202
|
+
{ "provider" => presence(r.params["#{prefix}_provider"]),
|
|
203
|
+
"model" => presence(r.params["#{prefix}_model"]),
|
|
204
|
+
"candidate_limit" => edge_int(r.params["#{prefix}_candidate_limit"], "#{prefix}_candidate_limit") || 20,
|
|
205
|
+
"timeout_seconds" => edge_float(r.params["#{prefix}_timeout_seconds"], "#{prefix}_timeout_seconds") || 2 }
|
|
92
206
|
end
|
|
93
207
|
|
|
94
208
|
# grounding — { "mode" => "flag"|"enforce", "matcher" =>
|
|
@@ -161,7 +275,7 @@ module Studio
|
|
|
161
275
|
enabled = r.params["distill_enabled"] == "1"
|
|
162
276
|
out = { "enabled" => enabled }
|
|
163
277
|
(p = presence(r.params["distill_prompt"])) && (out["prompt"] = p)
|
|
164
|
-
(m =
|
|
278
|
+
(m = ref_param(r, "distill_model", (@agent.distill || {})["model"])) && (out["model"] = m)
|
|
165
279
|
%w[idle_hours min_messages max_proposals].each do |key|
|
|
166
280
|
v = edge_int(r.params["distill_#{key}"], "distill_#{key}")
|
|
167
281
|
out[key] = v unless v.nil?
|
|
@@ -188,7 +302,7 @@ module Studio
|
|
|
188
302
|
|
|
189
303
|
def harvest_miner_patch(r)
|
|
190
304
|
miner = {}
|
|
191
|
-
(m =
|
|
305
|
+
(m = ref_param(r, "harvest_miner_model", (@agent.harvest || {}).dig("miner", "model"))) && (miner["model"] = m)
|
|
192
306
|
window = {}
|
|
193
307
|
(w = edge_int(r.params["harvest_miner_window"], "harvest_miner_window")) && (window["last_sessions"] = w)
|
|
194
308
|
miner["window"] = window unless window.empty?
|
|
@@ -209,7 +323,7 @@ module Studio
|
|
|
209
323
|
(f = edge_int(r.params["refinement_max_findings"], "refinement_max_findings")) && (out["max_findings"] = f)
|
|
210
324
|
files = list(r.params["refinement_files"])
|
|
211
325
|
out["files"] = files unless files.empty?
|
|
212
|
-
proposers =
|
|
326
|
+
proposers = ref_list_param(r, "refinement_proposers", (@agent.refinement || {})["proposers"])
|
|
213
327
|
out["proposers"] = proposers unless proposers.empty?
|
|
214
328
|
(t = edge_int(r.params["refinement_budget_tokens"], "refinement_budget_tokens")) && (out["budget"] = { "tokens" => t })
|
|
215
329
|
(e = edge_int(r.params["refinement_auto_apply_max_edits"], "refinement_auto_apply_max_edits")) && (out["auto_apply_max_edits"] = e)
|
|
@@ -236,7 +350,7 @@ module Studio
|
|
|
236
350
|
retries = edge_int(r.params["reliability_retries"], "reliability_retries")
|
|
237
351
|
backoff = presence(r.params["reliability_backoff"])
|
|
238
352
|
timeout = edge_int(r.params["reliability_timeout"], "reliability_timeout")
|
|
239
|
-
fallback =
|
|
353
|
+
fallback = ref_list_param(r, "reliability_fallback", (@agent.reliability || {})["fallback"])
|
|
240
354
|
return nil if retries.nil? && backoff.nil? && timeout.nil? && fallback.empty?
|
|
241
355
|
|
|
242
356
|
out = {}
|
|
@@ -327,7 +441,8 @@ module Studio
|
|
|
327
441
|
out = {
|
|
328
442
|
"input" => r.params["guardrail_input"] == "1",
|
|
329
443
|
"output" => r.params["guardrail_output"] == "1",
|
|
330
|
-
"strictness" => presence(r.params["guardrail_strictness"]) || "medium"
|
|
444
|
+
"strictness" => presence(r.params["guardrail_strictness"]) || "medium",
|
|
445
|
+
"injection" => r.params["guardrail_injection"] == "block" ? "block" : "flag"
|
|
331
446
|
}
|
|
332
447
|
(mod = presence(r.params["guardrail_moderator"])) && (out["moderator"] = mod)
|
|
333
448
|
responses = guardrail_responses_patch(r)
|
|
@@ -386,7 +501,7 @@ module Studio
|
|
|
386
501
|
# record on write, so anything missing here is erased — a save that only fixed a
|
|
387
502
|
# typo in the description would silently drop them (the class of bug already
|
|
388
503
|
# paid for once). `stored` carries them through untouched.
|
|
389
|
-
UNEDITED_TOOL_FIELDS = %w[group tags halt_when].freeze
|
|
504
|
+
UNEDITED_TOOL_FIELDS = %w[group tags halt_when evidence presentation side_effect].freeze
|
|
390
505
|
|
|
391
506
|
# :write_data_tool payload from the form. nested request/response;
|
|
392
507
|
# headers/query as "key=value" per line (same idiom as the MCP env —
|
|
@@ -394,12 +509,17 @@ module Studio
|
|
|
394
509
|
# `stored` = the definition being edited (nil when creating).
|
|
395
510
|
def tool_patch(r, stored = nil)
|
|
396
511
|
preserved = (stored || {}).slice(*UNEDITED_TOOL_FIELDS).compact
|
|
397
|
-
|
|
512
|
+
method = presence(r.params["method"]) || "GET"
|
|
513
|
+
# A stored side_effect describes the stored method. When the method changes
|
|
514
|
+
# (a GET turned POST) it is dropped so the store re-derives it — the write
|
|
515
|
+
# gate and the resume skip both read it.
|
|
516
|
+
preserved.delete("side_effect") unless method == (stored || {}).dig("request", "method")
|
|
517
|
+
patch = preserved.merge(
|
|
398
518
|
name: presence(r.params["name"]),
|
|
399
519
|
description: r.params["description"].to_s,
|
|
400
520
|
parameters: parse_parameters(r.params["parameters"]),
|
|
401
521
|
request: {
|
|
402
|
-
method:
|
|
522
|
+
method: method,
|
|
403
523
|
url: r.params["url"].to_s,
|
|
404
524
|
headers: parse_kv_lines(r.params["headers"]),
|
|
405
525
|
query: parse_kv_lines(r.params["query"]),
|
|
@@ -410,8 +530,17 @@ module Studio
|
|
|
410
530
|
path: presence(r.params["path"])
|
|
411
531
|
},
|
|
412
532
|
secret_headers: split_list(r.params["secret_headers"]),
|
|
533
|
+
requires_evidence: if r.params.key?("requires_evidence")
|
|
534
|
+
split_list(r.params["requires_evidence"]).then { |names| names.empty? ? nil : names }
|
|
535
|
+
else
|
|
536
|
+
(stored || {})["requires_evidence"]
|
|
537
|
+
end,
|
|
413
538
|
timeout: presence(r.params["timeout"])
|
|
414
539
|
)
|
|
540
|
+
# A presentation tool makes no request: the form's HTTP fields are blank and
|
|
541
|
+
# the definition refuses both halves at once.
|
|
542
|
+
patch.delete(:request) if preserved["presentation"]
|
|
543
|
+
patch
|
|
415
544
|
end
|
|
416
545
|
|
|
417
546
|
# Parameters, TWO accepted syntaxes in one field (auto-detected, no mode toggle):
|
|
@@ -447,6 +576,10 @@ module Studio
|
|
|
447
576
|
end
|
|
448
577
|
end
|
|
449
578
|
|
|
579
|
+
def stored_compaction_model
|
|
580
|
+
insika[:settings_store] ? insika[:settings_store].get.dig("compaction", "model") : nil
|
|
581
|
+
end
|
|
582
|
+
|
|
450
583
|
def settings_patch(r)
|
|
451
584
|
patch = {
|
|
452
585
|
"streaming" => r.params["streaming"] == "1"
|
|
@@ -466,7 +599,7 @@ module Studio
|
|
|
466
599
|
# when cleared (deep_merge — the defaults backstop a fresh record);
|
|
467
600
|
# model blank = nil = the platform utility_model.
|
|
468
601
|
compaction = { "enabled" => r.params["compaction_enabled"] == "1",
|
|
469
|
-
"model" =>
|
|
602
|
+
"model" => ref_param(r, "compaction_model", stored_compaction_model) }
|
|
470
603
|
%w[keep_last compact_after].each do |f|
|
|
471
604
|
v = presence(r.params["compaction_#{f}"])
|
|
472
605
|
compaction[f] = Integer(v) if v
|
|
@@ -488,6 +621,30 @@ module Studio
|
|
|
488
621
|
{ "edge" => edge }
|
|
489
622
|
end
|
|
490
623
|
|
|
624
|
+
# Burst policy — the PLATFORM layer of the QueuePolicy, saved from its own form
|
|
625
|
+
# (a sub-resource, like edge/models). Every field writes nil when blank, and nil
|
|
626
|
+
# here means "no platform default" (QueuePolicy.pick skips it) — NOT 0, which on
|
|
627
|
+
# `steer_max_messages` would read as "never steer" deploy-wide. The two refusals
|
|
628
|
+
# are the agent form's, verbatim: an unknown mode and a `steer_join` that would
|
|
629
|
+
# drop the customer's message are config errors, caught where the operator is.
|
|
630
|
+
def queue_patch(r)
|
|
631
|
+
mode = presence(r.params["queue_mode"])
|
|
632
|
+
if mode && !Insika::QueuePolicy::MODES.map(&:to_s).include?(mode)
|
|
633
|
+
raise Insika::ValidationError, "queue_mode must be one of: #{Insika::QueuePolicy::MODES.join(', ')}"
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
join = presence(r.params["steer_join"])
|
|
637
|
+
if join && !join.include?(Insika::QueuePolicy::JOIN_PLACEHOLDER)
|
|
638
|
+
raise Insika::ValidationError, "steer_join must contain #{Insika::QueuePolicy::JOIN_PLACEHOLDER}"
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
queue = { "queue_mode" => mode, "steer_join" => join }
|
|
642
|
+
%w[debounce_ms debounce_max_ms steer_max_messages].each do |f|
|
|
643
|
+
queue[f] = edge_int(r.params[f], f)
|
|
644
|
+
end
|
|
645
|
+
{ "queue" => queue }
|
|
646
|
+
end
|
|
647
|
+
|
|
491
648
|
# An eval case arrives as the YAML text the operator edited — the same shape the
|
|
492
649
|
# corpus files hold, so there is one format to learn and a pull request can review
|
|
493
650
|
# what was authored. Decoding happens HERE, at the transport edge; the SHAPE is
|
|
@@ -558,7 +715,27 @@ module Studio
|
|
|
558
715
|
# deep-merge writes and the ModelResolver reads as "no platform default");
|
|
559
716
|
# fallback_models is a full-list replace (deep_merge substitutes arrays, so a
|
|
560
717
|
# resubmit never accumulates).
|
|
718
|
+
# The pickers post `*_ref` fields, each checked against the list (a stored
|
|
719
|
+
# value may come back unchanged): default_model splits into model/provider like
|
|
720
|
+
# the agent's; fallback_refs[] is one picker per slot (blank slots dropped).
|
|
721
|
+
# Without them, the plain fields pass as given (API clients, older forms).
|
|
561
722
|
def model_defaults_patch(r)
|
|
723
|
+
return plain_model_defaults(r) unless r.params.key?("default_model_ref")
|
|
724
|
+
|
|
725
|
+
current = insika[:settings_store] ? insika[:settings_store].get : {}
|
|
726
|
+
kept_default = model_ref_of(current["default_model"], current["default_provider"])
|
|
727
|
+
default_model, default_provider = model_ref_parts(checked_model_ref(r.params["default_model_ref"], kept_default), nil)
|
|
728
|
+
kept_fallbacks = Array(current["fallback_models"])
|
|
729
|
+
{
|
|
730
|
+
"default_model" => default_model,
|
|
731
|
+
"default_provider" => default_provider,
|
|
732
|
+
"utility_model" => checked_model_ref(r.params["utility_model_ref"], current["utility_model"]),
|
|
733
|
+
"fallback_models" => Array(r.params["fallback_refs"]).filter_map { |f| checked_model_ref(f, kept_fallbacks) },
|
|
734
|
+
"thinking" => presence(r.params["thinking"])
|
|
735
|
+
}
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def plain_model_defaults(r)
|
|
562
739
|
{
|
|
563
740
|
"default_model" => presence(r.params["default_model"]),
|
|
564
741
|
"default_provider" => presence(r.params["default_provider"]),
|
|
@@ -8,13 +8,16 @@ module Studio
|
|
|
8
8
|
# 20×20 stroke icons (currentColor), one per nav key. Source: a Lucide-style
|
|
9
9
|
# line icon set.
|
|
10
10
|
NAV_ICONS = {
|
|
11
|
+
search: '<circle cx="11" cy="11" r="7"/><path d="m20 20-3.5-3.5"/>',
|
|
11
12
|
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
13
|
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
14
|
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"/>',
|
|
15
|
+
kits: '<path d="M21 8 12 3 3 8v8l9 5 9-5z"/><path d="m3 8 9 5 9-5"/><path d="M12 13v8"/>',
|
|
14
16
|
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
17
|
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
18
|
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
19
|
settings: '<path d="M20 7h-9"/><path d="M14 17H5"/><circle cx="17" cy="17" r="3"/><circle cx="7" cy="7" r="3"/>',
|
|
20
|
+
live: '<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>',
|
|
18
21
|
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
22
|
playground: '<polygon points="6 3 20 12 6 21 6 3"/>',
|
|
20
23
|
tasks: '<path d="M11 12H3"/><path d="M16 6H3"/><path d="M16 18H3"/><path d="m18 9 3 3-3 3"/>',
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<% knowledge = @agent.knowledge || {} %>
|
|
2
|
+
<p class="muted">Memory keeps facts and notes across conversations. Knowledge stores reusable concepts. Reranking only changes which saved information is selected for a turn.</p>
|
|
3
|
+
<% [["memory", "Memory", @agent.memory_retrieval || {}], ["knowledge", "Knowledge", knowledge]].each do |source, title, config| %>
|
|
4
|
+
<fieldset class="subgroup">
|
|
5
|
+
<legend><%= title %></legend>
|
|
6
|
+
<%== inherit_toggle(source == "memory" ? "memory_retrieval" : "knowledge") %>
|
|
7
|
+
<% if source == "memory" %>
|
|
8
|
+
<% unless @agent_defaults_form %>
|
|
9
|
+
<label class="check own">
|
|
10
|
+
<input type="checkbox" name="memory" value="1"<%== " checked" if @agent.memory %>>
|
|
11
|
+
Remember across conversations
|
|
12
|
+
</label>
|
|
13
|
+
<% end %>
|
|
14
|
+
<p class="muted">Without reranking, memory uses saved facts and the ten most recent notes. Turning memory off does not delete them.</p>
|
|
15
|
+
<% else %>
|
|
16
|
+
<label class="check">
|
|
17
|
+
<input type="checkbox" name="knowledge_extract" value="1"<%== " checked" if Insika::Coercion.truthy?(knowledge["extract"]) %>>
|
|
18
|
+
Learn concepts from conversations
|
|
19
|
+
</label>
|
|
20
|
+
<label class="check">
|
|
21
|
+
<input type="checkbox" name="knowledge_retrieve" value="1"<%== " checked" if Insika::Coercion.truthy?(knowledge["retrieve"]) %>>
|
|
22
|
+
Use knowledge in replies
|
|
23
|
+
</label>
|
|
24
|
+
<p class="muted">Learning and retrieval work independently. Turning them off does not delete saved concepts.</p>
|
|
25
|
+
<label>Concepts selected per turn
|
|
26
|
+
<input type="number" name="knowledge_top_k" value="<%= knowledge.fetch("top_k", 5) %>" min="1" step="1">
|
|
27
|
+
</label>
|
|
28
|
+
<details class="newfile">
|
|
29
|
+
<summary>Knowledge learning settings</summary>
|
|
30
|
+
<div class="grid-2">
|
|
31
|
+
<label>Concept types <span class="muted">— comma-separated; blank = default types</span>
|
|
32
|
+
<input type="text" name="knowledge_types" value="<%= Array(knowledge["types"]).join(", ") %>" placeholder="fact, policy, objection">
|
|
33
|
+
</label>
|
|
34
|
+
<label>Learning model <span class="muted">— blank = platform utility model</span>
|
|
35
|
+
<%== model_select("knowledge_model_ref", knowledge["model"], "platform utility model") %>
|
|
36
|
+
</label>
|
|
37
|
+
</div>
|
|
38
|
+
<label>Learning instructions <span class="muted">— blank = default instructions</span>
|
|
39
|
+
<textarea name="knowledge_prompt" rows="4"><%= knowledge["prompt"] %></textarea>
|
|
40
|
+
</label>
|
|
41
|
+
</details>
|
|
42
|
+
<% end %>
|
|
43
|
+
<% rerank = config["rerank"] || {} %>
|
|
44
|
+
<label class="check">
|
|
45
|
+
<input type="checkbox" name="<%= source %>_rerank_enabled" value="1"<%== " checked" if config["rerank"] %>>
|
|
46
|
+
Rerank <%= source %> by relevance
|
|
47
|
+
</label>
|
|
48
|
+
<p class="muted">Off by default. Adds a provider call and may increase cost. Requires <%= source == "memory" ? "memory" : "knowledge retrieval" %> to be on. Provider credentials are configured separately.</p>
|
|
49
|
+
<details class="newfile"<%== " open" if config["rerank"] %>>
|
|
50
|
+
<summary><%= title %> rerank settings</summary>
|
|
51
|
+
<div class="grid-2">
|
|
52
|
+
<label>Rerank provider
|
|
53
|
+
<input type="text" name="<%= source %>_rerank_provider" value="<%= rerank["provider"] %>" placeholder="cohere">
|
|
54
|
+
</label>
|
|
55
|
+
<label>Rerank model
|
|
56
|
+
<input type="text" name="<%= source %>_rerank_model" value="<%= rerank["model"] %>" placeholder="rerank-v3.5">
|
|
57
|
+
</label>
|
|
58
|
+
<% if source == "memory" %>
|
|
59
|
+
<label>Memory items selected per turn
|
|
60
|
+
<input type="number" name="memory_top_k" value="<%= config.fetch("top_k", 5) %>" min="1" max="100" step="1">
|
|
61
|
+
</label>
|
|
62
|
+
<% end %>
|
|
63
|
+
<label>Candidate limit <span class="muted">— at least the selected count, up to 100</span>
|
|
64
|
+
<input type="number" name="<%= source %>_rerank_candidate_limit" value="<%= rerank.fetch("candidate_limit", 20) %>" min="1" max="100" step="1">
|
|
65
|
+
</label>
|
|
66
|
+
<label>Rerank timeout (seconds)
|
|
67
|
+
<input type="number" name="<%= source %>_rerank_timeout_seconds" value="<%= rerank.fetch("timeout_seconds", 2) %>" min="0.001" step="any">
|
|
68
|
+
</label>
|
|
69
|
+
</div>
|
|
70
|
+
<p class="muted">Choose a rerank model registered for this provider. On failure, the agent uses the usual retrieval. Disabling rerank clears these rerank settings.</p>
|
|
71
|
+
</details>
|
|
72
|
+
</fieldset>
|
|
73
|
+
<% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%# Type-to-search agent picker: an <input list> over a <datalist> — native
|
|
2
|
+
search in every browser, no JS. Locals: name, options (ids), value,
|
|
3
|
+
blank (placeholder meaning "none / all"; nil = a value is required),
|
|
4
|
+
auto_submit (submit the form when a pick is committed). The server still
|
|
5
|
+
validates the value: a typed id that is not an agent is refused there. %>
|
|
6
|
+
<%# unique per page: a page can carry two pickers (a filter and a form) %>
|
|
7
|
+
<% list_id = "agent-options-#{name}-#{@agent_picker_seq = (@agent_picker_seq || 0) + 1}" %>
|
|
8
|
+
<% blank = defined?(blank) ? blank : nil %>
|
|
9
|
+
<% auto_submit = defined?(auto_submit) && auto_submit %>
|
|
10
|
+
<input type="search" class="filter-input agent-picker" name="<%= name %>" list="<%= list_id %>" value="<%= value %>" autocomplete="off"
|
|
11
|
+
placeholder="<%= blank || "Type to search agents…" %>"<%== " required" unless blank %>
|
|
12
|
+
<%== %(data-controller="auto-submit" data-action="change->auto-submit#submit") if auto_submit %>>
|
|
13
|
+
<datalist id="<%= list_id %>">
|
|
14
|
+
<% Array(options).each do |id| %><option value="<%= id %>"></option><% end %>
|
|
15
|
+
</datalist>
|