insika 0.9.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 +237 -2
- data/README.md +5 -3
- data/bin/insika +3 -0
- data/bin/insika-router +0 -1
- data/docs/AGENTS.md +90 -7
- data/docs/ARCHITECTURE.md +12 -7
- data/docs/ARTIFACTS.md +33 -3
- data/docs/BENCHMARK.md +50 -7
- data/docs/CONTEXT.md +63 -0
- data/docs/CROSS-HARNESS-BENCH.md +157 -0
- data/docs/DEPLOY.md +57 -11
- data/docs/EMBEDDING.md +10 -0
- data/docs/EVALS.md +33 -4
- data/docs/FACTS.md +8 -6
- data/docs/HARVEST.md +8 -3
- data/docs/KNOWLEDGE.md +7 -0
- data/docs/MEDIA.md +6 -2
- data/docs/OBSERVABILITY.md +143 -22
- data/docs/PERFORMANCE.md +110 -0
- data/docs/PLUGINS.md +4 -4
- data/docs/POLICY.md +22 -13
- data/docs/RELEASING.md +5 -0
- data/docs/ROUTER.md +7 -0
- data/docs/RUBYLLM_2_MIGRATION.md +307 -0
- data/docs/SECURITY.md +7 -0
- data/docs/SHARED-CONVERSATIONS.md +40 -0
- data/docs/SKILLS.md +5 -3
- data/docs/STUDIO.md +122 -0
- data/docs/TOOLS.md +111 -9
- data/docs/operate.md +3 -0
- data/lib/insika/agent_defaults.rb +140 -0
- data/lib/insika/agent_file_store.rb +12 -0
- data/lib/insika/agent_profile.rb +78 -8
- data/lib/insika/artifact_signing.rb +11 -0
- data/lib/insika/chat_builder.rb +79 -25
- 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_harvest.rb +17 -1
- data/lib/insika/commands/seed_session.rb +11 -1
- 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/priority.rb +3 -0
- data/lib/insika/context/provider.rb +2 -2
- data/lib/insika/context/providers/knowledge.rb +25 -2
- data/lib/insika/context/providers/memory.rb +53 -4
- 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 +39 -3
- data/lib/insika/context/reranker.rb +75 -0
- data/lib/insika/distill.rb +22 -13
- data/lib/insika/doctor.rb +34 -4
- data/lib/insika/dsl/runtime.rb +36 -4
- data/lib/insika/dsl/server_boot.rb +4 -12
- data/lib/insika/dsl.rb +13 -5
- data/lib/insika/edge_limiter.rb +23 -15
- data/lib/insika/egress_guard.rb +11 -3
- data/lib/insika/env_schema.rb +33 -73
- data/lib/insika/evals/assertions.rb +171 -16
- data/lib/insika/evals/golden.rb +141 -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 +44 -4
- data/lib/insika/evals/simulator.rb +10 -1
- data/lib/insika/evals/transport.rb +200 -0
- data/lib/insika/event_stream.rb +63 -7
- data/lib/insika/evidence.rb +29 -8
- data/lib/insika/executor.rb +668 -141
- 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 +32 -1
- data/lib/insika/mcp_tool_registry.rb +69 -26
- data/lib/insika/media.rb +38 -25
- data/lib/insika/memory_store.rb +4 -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 +25 -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/profile_source.rb +4 -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 +17 -8
- data/lib/insika/server/responses.rb +19 -1
- data/lib/insika/session_actor.rb +58 -28
- data/lib/insika/session_store.rb +159 -10
- data/lib/insika/settings_store.rb +47 -1
- 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/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 +1453 -161
- 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 +171 -9
- 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 +78 -242
- 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} +32 -9
- 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 +152 -117
- 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 +103 -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_batch.rb +1 -1
- data/lib/insika/tool_catalog.rb +12 -2
- data/lib/insika/tool_definition.rb +10 -4
- data/lib/insika/tool_envelope.rb +165 -28
- data/lib/insika/tool_manifest.rb +1 -0
- data/lib/insika/tool_store.rb +3 -0
- data/lib/insika/tool_trace_store.rb +15 -8
- data/lib/insika/tool_usage_report.rb +2 -2
- 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 +23 -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 +1 -8
- data/lib/insika/tools/remember.rb +7 -2
- data/lib/insika/tools/run_persona_eval.rb +3 -5
- 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 +7 -5
- 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 +18 -0
- data/lib/insika/turn_timing.rb +51 -14
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +81 -16
- data/lib/insika/workflow.rb +25 -64
- data/lib/insika.rb +19 -3
- metadata +69 -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),
|
|
@@ -85,11 +110,99 @@ module Studio
|
|
|
85
110
|
context_providers: allowlist_patch(r, "context_providers"),
|
|
86
111
|
workflows_allow: allowlist_patch(r, "workflows_allow"),
|
|
87
112
|
approvals_required: list_patch(r, "approvals_required"),
|
|
113
|
+
customer_confirm: list_patch(r, "customer_confirm"),
|
|
88
114
|
policies: list_patch(r, "policies").map(&:to_sym),
|
|
89
115
|
prompt_refs: list_patch(r, "prompt_refs"),
|
|
90
116
|
capabilities_declared: list_patch(r, "capabilities_declared"),
|
|
91
117
|
skills_eager: skills_eager_patch(r)
|
|
92
|
-
}
|
|
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 }
|
|
93
206
|
end
|
|
94
207
|
|
|
95
208
|
# grounding — { "mode" => "flag"|"enforce", "matcher" =>
|
|
@@ -162,7 +275,7 @@ module Studio
|
|
|
162
275
|
enabled = r.params["distill_enabled"] == "1"
|
|
163
276
|
out = { "enabled" => enabled }
|
|
164
277
|
(p = presence(r.params["distill_prompt"])) && (out["prompt"] = p)
|
|
165
|
-
(m =
|
|
278
|
+
(m = ref_param(r, "distill_model", (@agent.distill || {})["model"])) && (out["model"] = m)
|
|
166
279
|
%w[idle_hours min_messages max_proposals].each do |key|
|
|
167
280
|
v = edge_int(r.params["distill_#{key}"], "distill_#{key}")
|
|
168
281
|
out[key] = v unless v.nil?
|
|
@@ -189,7 +302,7 @@ module Studio
|
|
|
189
302
|
|
|
190
303
|
def harvest_miner_patch(r)
|
|
191
304
|
miner = {}
|
|
192
|
-
(m =
|
|
305
|
+
(m = ref_param(r, "harvest_miner_model", (@agent.harvest || {}).dig("miner", "model"))) && (miner["model"] = m)
|
|
193
306
|
window = {}
|
|
194
307
|
(w = edge_int(r.params["harvest_miner_window"], "harvest_miner_window")) && (window["last_sessions"] = w)
|
|
195
308
|
miner["window"] = window unless window.empty?
|
|
@@ -210,7 +323,7 @@ module Studio
|
|
|
210
323
|
(f = edge_int(r.params["refinement_max_findings"], "refinement_max_findings")) && (out["max_findings"] = f)
|
|
211
324
|
files = list(r.params["refinement_files"])
|
|
212
325
|
out["files"] = files unless files.empty?
|
|
213
|
-
proposers =
|
|
326
|
+
proposers = ref_list_param(r, "refinement_proposers", (@agent.refinement || {})["proposers"])
|
|
214
327
|
out["proposers"] = proposers unless proposers.empty?
|
|
215
328
|
(t = edge_int(r.params["refinement_budget_tokens"], "refinement_budget_tokens")) && (out["budget"] = { "tokens" => t })
|
|
216
329
|
(e = edge_int(r.params["refinement_auto_apply_max_edits"], "refinement_auto_apply_max_edits")) && (out["auto_apply_max_edits"] = e)
|
|
@@ -237,7 +350,7 @@ module Studio
|
|
|
237
350
|
retries = edge_int(r.params["reliability_retries"], "reliability_retries")
|
|
238
351
|
backoff = presence(r.params["reliability_backoff"])
|
|
239
352
|
timeout = edge_int(r.params["reliability_timeout"], "reliability_timeout")
|
|
240
|
-
fallback =
|
|
353
|
+
fallback = ref_list_param(r, "reliability_fallback", (@agent.reliability || {})["fallback"])
|
|
241
354
|
return nil if retries.nil? && backoff.nil? && timeout.nil? && fallback.empty?
|
|
242
355
|
|
|
243
356
|
out = {}
|
|
@@ -328,7 +441,8 @@ module Studio
|
|
|
328
441
|
out = {
|
|
329
442
|
"input" => r.params["guardrail_input"] == "1",
|
|
330
443
|
"output" => r.params["guardrail_output"] == "1",
|
|
331
|
-
"strictness" => presence(r.params["guardrail_strictness"]) || "medium"
|
|
444
|
+
"strictness" => presence(r.params["guardrail_strictness"]) || "medium",
|
|
445
|
+
"injection" => r.params["guardrail_injection"] == "block" ? "block" : "flag"
|
|
332
446
|
}
|
|
333
447
|
(mod = presence(r.params["guardrail_moderator"])) && (out["moderator"] = mod)
|
|
334
448
|
responses = guardrail_responses_patch(r)
|
|
@@ -462,6 +576,10 @@ module Studio
|
|
|
462
576
|
end
|
|
463
577
|
end
|
|
464
578
|
|
|
579
|
+
def stored_compaction_model
|
|
580
|
+
insika[:settings_store] ? insika[:settings_store].get.dig("compaction", "model") : nil
|
|
581
|
+
end
|
|
582
|
+
|
|
465
583
|
def settings_patch(r)
|
|
466
584
|
patch = {
|
|
467
585
|
"streaming" => r.params["streaming"] == "1"
|
|
@@ -481,7 +599,7 @@ module Studio
|
|
|
481
599
|
# when cleared (deep_merge — the defaults backstop a fresh record);
|
|
482
600
|
# model blank = nil = the platform utility_model.
|
|
483
601
|
compaction = { "enabled" => r.params["compaction_enabled"] == "1",
|
|
484
|
-
"model" =>
|
|
602
|
+
"model" => ref_param(r, "compaction_model", stored_compaction_model) }
|
|
485
603
|
%w[keep_last compact_after].each do |f|
|
|
486
604
|
v = presence(r.params["compaction_#{f}"])
|
|
487
605
|
compaction[f] = Integer(v) if v
|
|
@@ -503,6 +621,30 @@ module Studio
|
|
|
503
621
|
{ "edge" => edge }
|
|
504
622
|
end
|
|
505
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
|
+
|
|
506
648
|
# An eval case arrives as the YAML text the operator edited — the same shape the
|
|
507
649
|
# corpus files hold, so there is one format to learn and a pull request can review
|
|
508
650
|
# what was authored. Decoding happens HERE, at the transport edge; the SHAPE is
|
|
@@ -573,7 +715,27 @@ module Studio
|
|
|
573
715
|
# deep-merge writes and the ModelResolver reads as "no platform default");
|
|
574
716
|
# fallback_models is a full-list replace (deep_merge substitutes arrays, so a
|
|
575
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).
|
|
576
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)
|
|
577
739
|
{
|
|
578
740
|
"default_model" => presence(r.params["default_model"]),
|
|
579
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>
|