insika 0.1.0 → 0.3.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 +199 -5
- data/README.md +8 -2
- data/bin/insika +231 -13
- data/docs/AGENTS.md +505 -6
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +100 -10
- data/docs/CONTEXT.md +147 -19
- data/docs/DEPLOY.md +34 -11
- data/docs/EMBEDDING.md +11 -7
- data/docs/EVALS.md +20 -1
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +17 -10
- data/docs/OBSERVABILITY.md +65 -2
- data/docs/REFINEMENT.md +9 -9
- data/docs/RELEASING.md +34 -7
- data/docs/RUNNING-LOCAL.md +4 -4
- data/docs/SECURITY.md +85 -11
- data/docs/SKILLS.md +189 -3
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +2 -2
- data/docs/onboarding/start.md +1 -1
- data/lib/insika/agent_profile.rb +228 -26
- data/lib/insika/alert_dispatcher.rb +139 -0
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/baseline_store.rb +2 -2
- data/lib/insika/budget_ledger.rb +166 -0
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +132 -24
- data/lib/insika/channel_registry.rb +1 -1
- data/lib/insika/channels/relay.rb +80 -6
- data/lib/insika/channels/web/widget.js +2 -2
- data/lib/insika/channels/web.rb +9 -9
- data/lib/insika/channels/webhook.rb +58 -0
- data/lib/insika/chat_builder.rb +145 -13
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/circuit_state.rb +114 -0
- data/lib/insika/coercion.rb +8 -0
- data/lib/insika/commands/agent_payload.rb +6 -4
- data/lib/insika/commands/cancel_followup.rb +49 -0
- data/lib/insika/commands/create_agent.rb +2 -2
- data/lib/insika/commands/create_session.rb +1 -1
- data/lib/insika/commands/delete_llm_provider.rb +1 -1
- data/lib/insika/commands/delete_skill.rb +43 -0
- data/lib/insika/commands/delete_tenant_data.rb +95 -0
- data/lib/insika/commands/export_customer_memory.rb +48 -0
- data/lib/insika/commands/forget_customer.rb +117 -0
- data/lib/insika/commands/freeze_funnel_baseline.rb +113 -0
- data/lib/insika/commands/gate_harvest.rb +138 -0
- data/lib/insika/commands/gate_refinement.rb +12 -12
- data/lib/insika/commands/import_mcp_tools.rb +1 -1
- data/lib/insika/commands/import_tools.rb +4 -4
- data/lib/insika/commands/issue_tenant_token.rb +41 -0
- data/lib/insika/commands/judge_shadow_pairs.rb +124 -0
- data/lib/insika/commands/memory_forget_fact.rb +20 -4
- data/lib/insika/commands/memory_put_fact.rb +23 -4
- data/lib/insika/commands/promote_harvest.rb +130 -0
- data/lib/insika/commands/record_outcome.rb +46 -0
- data/lib/insika/commands/record_shadow_reply.rb +68 -0
- data/lib/insika/commands/reject_harvest.rb +38 -0
- data/lib/insika/commands/resolve_proposal.rb +108 -0
- data/lib/insika/commands/resolve_refinement.rb +1 -1
- data/lib/insika/commands/revoke_contact.rb +49 -0
- data/lib/insika/commands/revoke_token.rb +39 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/rotate_tenant_token.rb +43 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/run_refinement.rb +5 -5
- data/lib/insika/commands/send_message.rb +112 -15
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/commands/set_agent_tools.rb +1 -1
- data/lib/insika/commands/set_skill_agents.rb +60 -19
- data/lib/insika/commands/trigger_workflow.rb +1 -1
- data/lib/insika/commands/update_agent.rb +1 -1
- data/lib/insika/commands/write_data_tool.rb +1 -1
- data/lib/insika/commands/write_golden.rb +1 -1
- data/lib/insika/commands/write_skill.rb +19 -9
- data/lib/insika/config_store.rb +8 -4
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +23 -5
- data/lib/insika/context/fragment.rb +31 -3
- data/lib/insika/context/priority.rb +6 -2
- data/lib/insika/context/provider.rb +17 -3
- data/lib/insika/context/providers/briefing.rb +96 -0
- data/lib/insika/context/providers/memory.rb +16 -7
- data/lib/insika/context/providers/prompt.rb +30 -2
- data/lib/insika/context/providers/request.rb +1 -1
- data/lib/insika/context/providers/session.rb +17 -2
- data/lib/insika/context/providers/skill.rb +7 -1
- data/lib/insika/context/providers/skill_trigger.rb +128 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +128 -0
- data/lib/insika/delegation_store.rb +2 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +962 -7
- data/lib/insika/dsl/runtime.rb +20 -11
- data/lib/insika/dsl/server_boot.rb +74 -4
- data/lib/insika/dsl/system.rb +1 -1
- data/lib/insika/dsl.rb +152 -15
- data/lib/insika/edge_limiter.rb +167 -8
- data/lib/insika/egress_guard.rb +3 -3
- data/lib/insika/env_schema.rb +22 -12
- data/lib/insika/errors.rb +72 -5
- data/lib/insika/evals/assertions.rb +15 -14
- data/lib/insika/evals/baseline.rb +3 -3
- data/lib/insika/evals/golden.rb +8 -8
- data/lib/insika/evals/judge.rb +7 -7
- data/lib/insika/evals/pairwise.rb +21 -9
- data/lib/insika/evals/report.rb +2 -2
- data/lib/insika/evals/runner.rb +6 -6
- data/lib/insika/evals/transport.rb +2 -2
- data/lib/insika/event_stream.rb +23 -5
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +1092 -160
- data/lib/insika/followup_engine.rb +207 -0
- data/lib/insika/followup_policy.rb +221 -0
- data/lib/insika/followup_store.rb +306 -0
- data/lib/insika/frontmatter.rb +1 -1
- data/lib/insika/funnel_declaration.rb +106 -0
- data/lib/insika/funnel_fold.rb +179 -0
- data/lib/insika/funnel_store.rb +163 -0
- data/lib/insika/golden_store.rb +3 -3
- data/lib/insika/grounding/matcher.rb +69 -0
- data/lib/insika/grounding.rb +44 -0
- data/lib/insika/harvest/conversion_gate.rb +159 -0
- data/lib/insika/harvest/criterion.rb +98 -0
- data/lib/insika/harvest/gate.rb +194 -0
- data/lib/insika/harvest/negative_list.rb +199 -0
- data/lib/insika/harvest.rb +241 -0
- data/lib/insika/harvest_engine.rb +193 -0
- data/lib/insika/harvest_store.rb +548 -0
- data/lib/insika/http_client.rb +3 -3
- data/lib/insika/inbound_log.rb +1 -1
- data/lib/insika/llm_configurator.rb +3 -3
- data/lib/insika/loop_detector.rb +143 -0
- data/lib/insika/mcp_http_client.rb +4 -4
- data/lib/insika/mcp_tool_ingestor.rb +6 -6
- data/lib/insika/media.rb +298 -0
- data/lib/insika/memory_audit_store.rb +85 -0
- data/lib/insika/memory_store.rb +264 -23
- data/lib/insika/message_origin.rb +8 -3
- data/lib/insika/model_resolver.rb +1 -1
- data/lib/insika/model_selection.rb +5 -4
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +8 -3
- data/lib/insika/outbox_store.rb +44 -6
- data/lib/insika/outcome_store.rb +147 -0
- data/lib/insika/overlay_tool_registry.rb +3 -4
- data/lib/insika/pack.rb +3 -3
- data/lib/insika/pack_importer.rb +17 -15
- data/lib/insika/packaging.rb +163 -0
- data/lib/insika/parity/criterion.rb +79 -0
- data/lib/insika/parity/verdict.rb +318 -0
- data/lib/insika/pending_action_store.rb +1 -1
- data/lib/insika/plugin/loader.rb +2 -2
- data/lib/insika/policy/policy.rb +1 -1
- data/lib/insika/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +34 -7
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/provider_error_classifier.rb +160 -0
- data/lib/insika/queue_policy.rb +6 -3
- data/lib/insika/recovery.rb +47 -6
- data/lib/insika/refinement/candidate.rb +4 -4
- data/lib/insika/refinement/evidence_collector.rb +6 -6
- data/lib/insika/refinement/gate.rb +7 -7
- data/lib/insika/refinement/panel.rb +7 -7
- data/lib/insika/refinement/proposer.rb +10 -10
- data/lib/insika/refinement_store.rb +12 -12
- data/lib/insika/reliability.rb +211 -0
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +46 -6
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +34 -115
- data/lib/insika/safety/factory.rb +18 -5
- data/lib/insika/safety/grounding_enforcer.rb +59 -0
- data/lib/insika/safety/grounding_validator.rb +49 -0
- data/lib/insika/safety/input_guardrail.rb +20 -5
- data/lib/insika/safety/moderator.rb +19 -11
- data/lib/insika/safety/output_filter.rb +10 -6
- data/lib/insika/safety/output_validator.rb +13 -7
- data/lib/insika/safety/safe_responses.rb +1 -1
- data/lib/insika/sandbox/boundary.rb +2 -2
- data/lib/insika/sandbox.rb +1 -1
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +366 -54
- data/lib/insika/server/boot.rb +4 -4
- data/lib/insika/server/rack_app.rb +31 -7
- data/lib/insika/server/responses.rb +58 -9
- data/lib/insika/server/tenant_auth.rb +61 -0
- data/lib/insika/session_actor.rb +11 -7
- data/lib/insika/session_store.rb +66 -3
- data/lib/insika/settings_store.rb +15 -5
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/shutdown.rb +4 -4
- data/lib/insika/skill_catalog.rb +131 -20
- data/lib/insika/skill_store.rb +70 -22
- data/lib/insika/soak/envelope.rb +140 -0
- data/lib/insika/soak/report.rb +392 -0
- data/lib/insika/soak/runner.rb +554 -0
- data/lib/insika/steer_injector.rb +1 -1
- data/lib/insika/store.rb +11 -2
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +1058 -75
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +27 -26
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +274 -22
- data/lib/insika/studio/nav_icons.rb +7 -2
- data/lib/insika/studio/views/_message.erb +2 -2
- data/lib/insika/studio/views/agent_detail.erb +629 -86
- data/lib/insika/studio/views/agents.erb +11 -7
- data/lib/insika/studio/views/approvals.erb +4 -1
- data/lib/insika/studio/views/chats.erb +4 -1
- data/lib/insika/studio/views/customer.erb +94 -0
- data/lib/insika/studio/views/customers.erb +32 -0
- data/lib/insika/studio/views/evals.erb +4 -1
- data/lib/insika/studio/views/facts.erb +133 -0
- data/lib/insika/studio/views/followups.erb +125 -0
- data/lib/insika/studio/views/funnel.erb +106 -0
- data/lib/insika/studio/views/harvest.erb +234 -0
- data/lib/insika/studio/views/home.erb +2 -1
- data/lib/insika/studio/views/layout.erb +1 -0
- data/lib/insika/studio/views/parity.erb +147 -0
- data/lib/insika/studio/views/playground.erb +7 -1
- data/lib/insika/studio/views/refinement.erb +4 -4
- data/lib/insika/studio/views/session.erb +133 -3
- data/lib/insika/studio/views/settings.erb +9 -12
- data/lib/insika/studio/views/skills.erb +66 -12
- data/lib/insika/studio/views/system_files.erb +1 -1
- data/lib/insika/studio/views/task.erb +13 -0
- data/lib/insika/studio/views/tasks.erb +4 -1
- data/lib/insika/studio/views/tools.erb +0 -1
- data/lib/insika/subagent_graph.rb +3 -3
- data/lib/insika/task_actor.rb +3 -3
- data/lib/insika/task_store.rb +22 -2
- data/lib/insika/telemetry/pricing.rb +3 -3
- data/lib/insika/telemetry/recorder.rb +1 -1
- data/lib/insika/telemetry.rb +2 -2
- data/lib/insika/testing/store_contract.rb +54 -33
- data/lib/insika/tick.rb +146 -0
- data/lib/insika/token_store.rb +168 -0
- data/lib/insika/tool_assembly.rb +5 -5
- data/lib/insika/tool_definition.rb +25 -15
- data/lib/insika/tool_envelope.rb +70 -1
- data/lib/insika/tool_manifest.rb +11 -7
- data/lib/insika/tool_output_compressor.rb +100 -0
- data/lib/insika/tool_store.rb +1 -1
- data/lib/insika/tool_trace_store.rb +1 -1
- data/lib/insika/tools/concurrency.rb +2 -2
- data/lib/insika/tools/data_defined_tool.rb +14 -5
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/load_skill.rb +61 -3
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/stuck_signal.rb +44 -0
- data/lib/insika/tools/subagent.rb +4 -4
- data/lib/insika/tools/subagents.rb +1 -1
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_output.rb +2 -2
- data/lib/insika/turn_state.rb +54 -13
- data/lib/insika/turn_timing.rb +24 -4
- data/lib/insika/usage_ledger.rb +1 -1
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +372 -34
- data/lib/insika/workflow.rb +1 -1
- data/lib/insika/workflow_registry.rb +1 -1
- data/lib/insika.rb +122 -16
- metadata +95 -2
- data/lib/insika/server/admin_auth.rb +0 -29
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<rect width="64" height="64" rx="14" fill="#15161b"/>
|
|
3
|
+
<circle cx="32" cy="32" r="17" fill="#e8b23a" opacity=".18"/>
|
|
4
|
+
<circle cx="32" cy="32" r="12" fill="#e8b23a"/>
|
|
5
|
+
<circle cx="32" cy="32" r="4.5" fill="#15161b"/>
|
|
6
|
+
</svg>
|
data/lib/insika/studio/forms.rb
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Studio
|
|
4
|
-
# Form → command-payload parsing for the Studio App
|
|
4
|
+
# Form → command-payload parsing for the Studio App. A mixin, NOT a
|
|
5
5
|
# standalone object: `include`d into App so every method keeps `self` == the
|
|
6
6
|
# App instance and reaches its request helpers (`presence`, `split_list`) and
|
|
7
7
|
# per-request state (`@agent`) exactly as before. Pure extraction — the bodies
|
|
8
8
|
# moved verbatim from app.rb; app_spec's POST cases are the safety net.
|
|
9
9
|
module Forms
|
|
10
|
+
# The agent config form OWNS every rendered field: a save reflects the
|
|
11
|
+
# whole form state, so a blank field is a deliberate "clear to default",
|
|
12
|
+
# never a carry-over. The one exception is keys the form cannot express —
|
|
13
|
+
# those ride the `carry` merge below (the guardrails corpora precedent).
|
|
10
14
|
def config_patch(r)
|
|
11
15
|
limits = @agent.limits.dup
|
|
12
16
|
# Always present via DEFAULT_LIMITS (tool_concurrency defaults to 1 = serial),
|
|
@@ -15,7 +19,7 @@ module Studio
|
|
|
15
19
|
v = presence(r.params[field])
|
|
16
20
|
limits[field.to_sym] = Integer(v) if v
|
|
17
21
|
end
|
|
18
|
-
# Per-agent edge overrides
|
|
22
|
+
# Per-agent edge overrides. Unlike the timeouts (always present via
|
|
19
23
|
# DEFAULT_LIMITS), these are OPT-IN keys: blank DELETES the override (inherit
|
|
20
24
|
# the platform edge settings); 0 explicitly disables for this agent.
|
|
21
25
|
%w[chat_rate_limit agent_token_ceiling].each do |field|
|
|
@@ -34,14 +38,260 @@ module Studio
|
|
|
34
38
|
limits: limits,
|
|
35
39
|
params: params_patch(r),
|
|
36
40
|
model_policy: model_policy_patch(r),
|
|
37
|
-
guardrails: guardrails_patch(r)
|
|
41
|
+
guardrails: guardrails_patch(r),
|
|
42
|
+
# Every remaining AgentProfile field is editable here — the pack
|
|
43
|
+
# import is one door, this form is the other (hot, no restart).
|
|
44
|
+
grounding: grounding_patch(r),
|
|
45
|
+
funnel: funnel_patch(r),
|
|
46
|
+
followup: followup_patch(r),
|
|
47
|
+
distill: distill_patch(r),
|
|
48
|
+
harvest: harvest_patch(r),
|
|
49
|
+
refinement: refinement_patch(r),
|
|
50
|
+
budget: budget_patch(r),
|
|
51
|
+
reliability: reliability_patch(r),
|
|
52
|
+
routes: json_patch(r, "routes"),
|
|
53
|
+
outputs: json_patch(r, "outputs"),
|
|
54
|
+
metadata: json_patch(r, "metadata") || {},
|
|
55
|
+
alerts: alerts_patch(r),
|
|
56
|
+
edge_stream: edge_stream_patch(r),
|
|
57
|
+
stuck_signal: r.params["stuck_signal"] == "1",
|
|
58
|
+
prompt_caching: r.params["prompt_caching"] == "1",
|
|
59
|
+
# The one opt-out flag: checked (the rendered default) saves true, an
|
|
60
|
+
# unchecked box saves an explicit false — both spellings of ON collapse.
|
|
61
|
+
tool_persistence: r.params["tool_persistence"] == "1",
|
|
62
|
+
tool_output_compression: r.params["tool_output_compression"] == "1",
|
|
63
|
+
subagents: list_patch(r, "subagents"),
|
|
64
|
+
capabilities: list_patch(r, "capabilities"),
|
|
65
|
+
tools_deferred: list_patch(r, "tools_deferred"),
|
|
66
|
+
briefing_fields: list_patch(r, "briefing_fields"),
|
|
67
|
+
context_providers: allowlist_patch(r, "context_providers"),
|
|
68
|
+
workflows_allow: allowlist_patch(r, "workflows_allow"),
|
|
69
|
+
approvals_required: list_patch(r, "approvals_required"),
|
|
70
|
+
policies: list_patch(r, "policies").map(&:to_sym),
|
|
71
|
+
prompt_refs: list_patch(r, "prompt_refs"),
|
|
72
|
+
capabilities_declared: list_patch(r, "capabilities_declared"),
|
|
73
|
+
skills_eager: skills_eager_patch(r)
|
|
38
74
|
}
|
|
39
75
|
end
|
|
40
76
|
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
|
|
77
|
+
# grounding — { "mode" => "flag"|"enforce", "matcher" =>
|
|
78
|
+
# { "sku" => "sku:…", "name_keys" => [...] } }. Blank mode = no grounding
|
|
79
|
+
# (parity). The matcher keys are carried only while a mode is declared.
|
|
80
|
+
def grounding_patch(r)
|
|
81
|
+
mode = presence(r.params["grounding_mode"])
|
|
82
|
+
return nil unless %w[flag enforce].include?(mode)
|
|
83
|
+
|
|
84
|
+
matcher = {}
|
|
85
|
+
(sku = presence(r.params["grounding_matcher_sku"])) && (matcher["sku"] = sku)
|
|
86
|
+
keys = list(r.params["grounding_name_keys"])
|
|
87
|
+
matcher["name_keys"] = keys unless keys.empty?
|
|
88
|
+
{ "mode" => mode, "matcher" => matcher }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# the outcome funnel — the pack's own stage vocabulary, edited
|
|
92
|
+
# hot. Blank stages = no funnel (parity). Stages: one per line.
|
|
93
|
+
def funnel_patch(r)
|
|
94
|
+
stages = lines(r.params["funnel_stages"])
|
|
95
|
+
return nil if stages.empty?
|
|
96
|
+
|
|
97
|
+
out = { "stages" => stages }
|
|
98
|
+
(p = presence(r.params["funnel_primary"])) && (out["primary"] = p)
|
|
99
|
+
(w = presence(r.params["funnel_attribution_window"])) && (out["attribution_window"] = w)
|
|
100
|
+
advance = json_block(r.params["funnel_advance_on"], "funnel advance_on")
|
|
101
|
+
out["advance_on"] = advance unless advance.nil? || advance.empty?
|
|
102
|
+
out
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# the follow-up declaration. Blank arm = the feature is off
|
|
106
|
+
# (parity). The policy keys are all rendered; a blank one drops the key.
|
|
107
|
+
def followup_patch(r)
|
|
108
|
+
arm = presence(r.params["followup_arm"])
|
|
109
|
+
return nil unless arm
|
|
110
|
+
|
|
111
|
+
policy = {}
|
|
112
|
+
tz = presence(r.params["followup_tz"])
|
|
113
|
+
if tz
|
|
114
|
+
quiet = { "timezone" => tz }
|
|
115
|
+
(s = presence(r.params["followup_quiet_start"])) && (quiet["start"] = s)
|
|
116
|
+
(e = presence(r.params["followup_quiet_end"])) && (quiet["end"] = e)
|
|
117
|
+
policy["quiet_hours"] = quiet
|
|
118
|
+
end
|
|
119
|
+
(f = presence(r.params["followup_max_frequency"])) && (policy["max_frequency"] = f)
|
|
120
|
+
keywords = list(r.params["followup_cancel_keywords"])
|
|
121
|
+
policy["cancel_keywords"] = keywords unless keywords.empty?
|
|
122
|
+
silence = edge_int(r.params["followup_silence_after_sends"], "followup_silence_after_sends")
|
|
123
|
+
policy["silence_after_sends"] = silence unless silence.nil?
|
|
124
|
+
{ "arm" => arm, "policy" => policy }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# distillation — enabled checkbox + the forge's knobs. Blank
|
|
128
|
+
# prompt/model drop the keys (the engine defaults take over).
|
|
129
|
+
def distill_patch(r)
|
|
130
|
+
enabled = r.params["distill_enabled"] == "1"
|
|
131
|
+
out = { "enabled" => enabled }
|
|
132
|
+
(p = presence(r.params["distill_prompt"])) && (out["prompt"] = p)
|
|
133
|
+
(m = presence(r.params["distill_model"])) && (out["model"] = m)
|
|
134
|
+
%w[idle_hours min_messages max_proposals].each do |key|
|
|
135
|
+
v = edge_int(r.params["distill_#{key}"], "distill_#{key}")
|
|
136
|
+
out[key] = v unless v.nil?
|
|
137
|
+
end
|
|
138
|
+
out
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# the gated harvest. Same shape discipline as distill; the
|
|
142
|
+
# negative list is a JSON array of { rule, pattern, note }.
|
|
143
|
+
def harvest_patch(r)
|
|
144
|
+
enabled = r.params["harvest_enabled"] == "1"
|
|
145
|
+
out = { "enabled" => enabled }
|
|
146
|
+
(p = presence(r.params["harvest_prompt"])) && (out["prompt"] = p)
|
|
147
|
+
%w[idle_hours min_messages].each do |key|
|
|
148
|
+
v = edge_int(r.params["harvest_#{key}"], "harvest_#{key}")
|
|
149
|
+
out[key] = v unless v.nil?
|
|
150
|
+
end
|
|
151
|
+
negative = json_block(r.params["harvest_negative_list"], "harvest negative_list")
|
|
152
|
+
out["negative_list"] = negative unless negative.nil?
|
|
153
|
+
miner = harvest_miner_patch(r)
|
|
154
|
+
out["miner"] = miner unless miner.empty?
|
|
155
|
+
out
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def harvest_miner_patch(r)
|
|
159
|
+
miner = {}
|
|
160
|
+
(m = presence(r.params["harvest_miner_model"])) && (miner["model"] = m)
|
|
161
|
+
window = {}
|
|
162
|
+
(w = edge_int(r.params["harvest_miner_window"], "harvest_miner_window")) && (window["last_sessions"] = w)
|
|
163
|
+
miner["window"] = window unless window.empty?
|
|
164
|
+
(m = edge_int(r.params["harvest_miner_max_proposals"], "harvest_miner_max_proposals")) && (miner["max_proposals"] = m)
|
|
165
|
+
budget = {}
|
|
166
|
+
(t = edge_int(r.params["harvest_miner_budget_tokens"], "harvest_miner_budget_tokens")) && (budget["tokens"] = t)
|
|
167
|
+
miner["budget"] = budget unless budget.empty?
|
|
168
|
+
miner
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Refinement: mode select + the report/propose/auto_apply knobs. Blank
|
|
172
|
+
# mode = report-only (the no-opt-in default — the form's "report" option
|
|
173
|
+
# writes it down explicitly).
|
|
174
|
+
def refinement_patch(r)
|
|
175
|
+
mode = presence(r.params["refinement_mode"])
|
|
176
|
+
out = { "mode" => mode } if mode
|
|
177
|
+
(w = edge_int(r.params["refinement_window"], "refinement_window")) && (out["window"] = { "last_sessions" => w })
|
|
178
|
+
(f = edge_int(r.params["refinement_max_findings"], "refinement_max_findings")) && (out["max_findings"] = f)
|
|
179
|
+
files = list(r.params["refinement_files"])
|
|
180
|
+
out["files"] = files unless files.empty?
|
|
181
|
+
proposers = list(r.params["refinement_proposers"])
|
|
182
|
+
out["proposers"] = proposers unless proposers.empty?
|
|
183
|
+
(t = edge_int(r.params["refinement_budget_tokens"], "refinement_budget_tokens")) && (out["budget"] = { "tokens" => t })
|
|
184
|
+
(e = edge_int(r.params["refinement_auto_apply_max_edits"], "refinement_auto_apply_max_edits")) && (out["auto_apply_max_edits"] = e)
|
|
185
|
+
out
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# WS2: the calendar spend caps. All blank = no budget (parity).
|
|
189
|
+
def budget_patch(r)
|
|
190
|
+
daily = edge_int(r.params["budget_daily"], "budget_daily")
|
|
191
|
+
monthly = edge_int(r.params["budget_monthly"], "budget_monthly")
|
|
192
|
+
return nil if daily.nil? && monthly.nil?
|
|
193
|
+
|
|
194
|
+
out = {}
|
|
195
|
+
out["daily"] = daily unless daily.nil?
|
|
196
|
+
out["monthly"] = monthly unless monthly.nil?
|
|
197
|
+
(a = edge_float(r.params["budget_alert_at"], "budget_alert_at")) && (out["alert_at"] = a)
|
|
198
|
+
out["soft"] = r.params["budget_soft"] == "1"
|
|
199
|
+
out
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# WS3: retries/backoff/fallback/breaker/timeout. All blank = the plain
|
|
203
|
+
# single attempt (parity).
|
|
204
|
+
def reliability_patch(r)
|
|
205
|
+
retries = edge_int(r.params["reliability_retries"], "reliability_retries")
|
|
206
|
+
backoff = presence(r.params["reliability_backoff"])
|
|
207
|
+
timeout = edge_int(r.params["reliability_timeout"], "reliability_timeout")
|
|
208
|
+
fallback = list(r.params["reliability_fallback"])
|
|
209
|
+
return nil if retries.nil? && backoff.nil? && timeout.nil? && fallback.empty?
|
|
210
|
+
|
|
211
|
+
out = {}
|
|
212
|
+
out["retries"] = retries unless retries.nil?
|
|
213
|
+
out["backoff"] = backoff if backoff
|
|
214
|
+
out["timeout"] = timeout unless timeout.nil?
|
|
215
|
+
out["fallback"] = fallback unless fallback.empty?
|
|
216
|
+
breaker = {}
|
|
217
|
+
%w[after within cooldown].each do |key|
|
|
218
|
+
v = edge_int(r.params["reliability_breaker_#{key}"], "reliability_breaker_#{key}")
|
|
219
|
+
breaker[key] = v unless v.nil?
|
|
220
|
+
end
|
|
221
|
+
out["circuit_breaker"] = breaker unless breaker.empty?
|
|
222
|
+
out
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# WS6: the operator-alert webhook. Blank = no webhook (parity).
|
|
226
|
+
def alerts_patch(r)
|
|
227
|
+
url = presence(r.params["alerts_webhook"])
|
|
228
|
+
url ? { "webhook" => url } : nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Which internal channels may cross to the customer. Both checkboxes are
|
|
232
|
+
# rendered, so the patch reflects the whole state (false = held back).
|
|
233
|
+
def edge_stream_patch(r)
|
|
234
|
+
{ "thinking" => r.params["edge_thinking"] == "1",
|
|
235
|
+
"intermediate" => r.params["edge_intermediate"] == "1" }
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# skills_eager: blank = none (progressive disclosure, parity); "all" =
|
|
239
|
+
# blanket eager; otherwise a comma list of exactly these.
|
|
240
|
+
def skills_eager_patch(r)
|
|
241
|
+
raw = r.params["skills_eager"].to_s.strip
|
|
242
|
+
return nil if raw.empty?
|
|
243
|
+
|
|
244
|
+
raw == "all" ? true : list(raw)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# A JSON textarea -> parsed Hash/Array, or nil when blank. Malformed JSON
|
|
248
|
+
# is a ValidationError (a red flash, never a silent drop).
|
|
249
|
+
def json_patch(r, field)
|
|
250
|
+
json_block(r.params[field], field)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def json_block(raw, field)
|
|
254
|
+
text = raw.to_s.strip
|
|
255
|
+
return nil if text.empty?
|
|
256
|
+
|
|
257
|
+
begin
|
|
258
|
+
JSON.parse(text)
|
|
259
|
+
rescue JSON::ParserError => e
|
|
260
|
+
raise Insika::ValidationError, "#{field}: invalid JSON (#{e.message.lines.first.to_s.strip})"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# A comma-separated list field. Blank = [] (none) — the explicit empty
|
|
265
|
+
# reading; an allowlist-style field wants nil=all and should not use this.
|
|
266
|
+
def list_patch(r, field)
|
|
267
|
+
list(r.params[field])
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# An ALLOWLIST-style field (nil = all, [] = none). Blank → nil (all), the
|
|
271
|
+
# open reading; a typed list closes it to exactly those names.
|
|
272
|
+
def allowlist_patch(r, field)
|
|
273
|
+
names = list(r.params[field])
|
|
274
|
+
names.empty? ? nil : names
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def list(str)
|
|
278
|
+
str.to_s.split(/[,\n]/).map(&:strip).reject(&:empty?).uniq
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# One non-blank token per line.
|
|
282
|
+
def lines(str)
|
|
283
|
+
str.to_s.lines.map(&:strip).reject(&:empty?)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Guardrails config from the form. The config form OWNS the fields it
|
|
287
|
+
# renders, so the patch reflects the whole rendered state. String values
|
|
288
|
+
# round-trip cleanly through the JSON store; Safety::Config normalizes on
|
|
289
|
+
# read. A blank moderator drops the key (deterministic only).
|
|
290
|
+
#
|
|
291
|
+
# `corpora` (the removability knob — languages/extra) has NO
|
|
292
|
+
# form field: it is DSL/pack data (docs/domain.md), not Studio UI. The
|
|
293
|
+
# form must not erase it on save — carry the existing value through
|
|
294
|
+
# (the shallow patch merge would otherwise wipe it, the halt_when class).
|
|
45
295
|
def guardrails_patch(r)
|
|
46
296
|
out = {
|
|
47
297
|
"input" => r.params["guardrail_input"] == "1",
|
|
@@ -51,10 +301,12 @@ module Studio
|
|
|
51
301
|
(mod = presence(r.params["guardrail_moderator"])) && (out["moderator"] = mod)
|
|
52
302
|
responses = guardrail_responses_patch(r)
|
|
53
303
|
out["responses"] = responses unless responses.empty?
|
|
304
|
+
existing = @agent.guardrails || {}
|
|
305
|
+
out["corpora"] = existing["corpora"] if existing["corpora"]
|
|
54
306
|
out
|
|
55
307
|
end
|
|
56
308
|
|
|
57
|
-
# Per-category safe-reply overrides (
|
|
309
|
+
# Per-category safe-reply overrides (config over convention). Only
|
|
58
310
|
# the non-blank fields are persisted; Safety::Config normalizes on read.
|
|
59
311
|
def guardrail_responses_patch(r)
|
|
60
312
|
%w[default injection sexual abuse escalate].each_with_object({}) do |cat, acc|
|
|
@@ -62,7 +314,7 @@ module Studio
|
|
|
62
314
|
end
|
|
63
315
|
end
|
|
64
316
|
|
|
65
|
-
# Per-agent generation params
|
|
317
|
+
# Per-agent generation params. The config form OWNS these fields, so
|
|
66
318
|
# the patch reflects the whole form state: a blank field drops the key (and an
|
|
67
319
|
# all-blank form clears params to {}). temperature/max_tokens MUST be Numeric —
|
|
68
320
|
# ModelSelection#apply_params guards on `numeric?` and silently skips a String —
|
|
@@ -79,7 +331,7 @@ module Studio
|
|
|
79
331
|
out
|
|
80
332
|
end
|
|
81
333
|
|
|
82
|
-
# Per-agent model fence
|
|
334
|
+
# Per-agent model fence: { "allow" => [refs] } where a ref is
|
|
83
335
|
# "provider/model", "provider/*" or "model". Blank textarea -> nil (NO fence,
|
|
84
336
|
# all models — parity). Enforced on the RESOLVED model by the ModelResolver,
|
|
85
337
|
# so a per-chat pin can never escape it.
|
|
@@ -101,7 +353,7 @@ module Studio
|
|
|
101
353
|
|
|
102
354
|
# Fields a tool CAN carry that this form does not render. The store REPLACES the
|
|
103
355
|
# 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
|
|
356
|
+
# typo in the description would silently drop them (the class of bug already
|
|
105
357
|
# paid for once). `stored` carries them through untouched.
|
|
106
358
|
UNEDITED_TOOL_FIELDS = %w[group tags halt_when].freeze
|
|
107
359
|
|
|
@@ -166,22 +418,22 @@ module Studio
|
|
|
166
418
|
|
|
167
419
|
def settings_patch(r)
|
|
168
420
|
patch = {
|
|
169
|
-
"streaming" => r.params["streaming"] == "1"
|
|
170
|
-
"compaction" => {
|
|
171
|
-
"enabled" => r.params["compaction_enabled"] == "1"
|
|
172
|
-
}
|
|
421
|
+
"streaming" => r.params["streaming"] == "1"
|
|
173
422
|
}
|
|
174
423
|
{ "request_timeout" => "request_timeout", "max_retries" => "max_retries",
|
|
175
424
|
"turn_timeout" => "turn_timeout", "tool_timeout" => "tool_timeout" }.each_key do |f|
|
|
176
425
|
v = presence(r.params[f])
|
|
177
426
|
patch[f] = Integer(v) if v
|
|
178
427
|
end
|
|
179
|
-
|
|
180
|
-
|
|
428
|
+
# memory TTL (days). Blank = off (nil); a number sets the
|
|
429
|
+
# platform default for EVERY cell. Saving here replaces an ops-authored
|
|
430
|
+
# per-tenant map in the record — the view says so.
|
|
431
|
+
v = presence(r.params["memory_ttl_days"])
|
|
432
|
+
patch["memory_ttl_days"] = v.nil? ? nil : Integer(v)
|
|
181
433
|
patch
|
|
182
434
|
end
|
|
183
435
|
|
|
184
|
-
# Edge limits
|
|
436
|
+
# Edge limits — the platform rate-limit/cost layer, saved
|
|
185
437
|
# from its OWN form (a sub-resource, like models). The limit fields write nil
|
|
186
438
|
# when blank (off — the EdgeLimiter reads nil/0 as off); the windows fall back
|
|
187
439
|
# to the built-in defaults when cleared (the limiter guards non-positive).
|
|
@@ -209,7 +461,7 @@ module Studio
|
|
|
209
461
|
{ case: parsed }
|
|
210
462
|
end
|
|
211
463
|
|
|
212
|
-
# Evals graders
|
|
464
|
+
# Evals graders. `judges` is one `provider/model` per LINE — a
|
|
213
465
|
# textarea, because the SIZE of the panel is the point and a fixed pair of fields
|
|
214
466
|
# would cap it at two. A bare `model` (no slash) is valid: the provider is then
|
|
215
467
|
# inferred by RubyLLM, same as everywhere else.
|
|
@@ -258,7 +510,7 @@ module Studio
|
|
|
258
510
|
raise Insika::ValidationError, "#{field} must be an integer (got #{raw.inspect})"
|
|
259
511
|
end
|
|
260
512
|
|
|
261
|
-
# LLM config v2
|
|
513
|
+
# LLM config v2 — the platform model layer, saved from its OWN form (a
|
|
262
514
|
# sub-resource, like providers) so the general-settings save never touches it and
|
|
263
515
|
# vice-versa. The scalar refs are always present (blank -> nil, which the
|
|
264
516
|
# deep-merge writes and the ModelResolver reads as "no platform default");
|
|
@@ -270,12 +522,12 @@ module Studio
|
|
|
270
522
|
"default_provider" => presence(r.params["default_provider"]),
|
|
271
523
|
"utility_model" => presence(r.params["utility_model"]),
|
|
272
524
|
"fallback_models" => split_list(r.params["fallback_models"]),
|
|
273
|
-
# GLOBAL reasoning default (
|
|
525
|
+
# GLOBAL reasoning default (4-layer). Blank -> nil = provider default.
|
|
274
526
|
"thinking" => presence(r.params["thinking"])
|
|
275
527
|
}
|
|
276
528
|
end
|
|
277
529
|
|
|
278
|
-
# Per-model reasoning defaults (
|
|
530
|
+
# Per-model reasoning defaults (the per-model layer). One line per model:
|
|
279
531
|
# <provider/model | model> | <off|on|low|medium|high>
|
|
280
532
|
# CSP forbids JS for dynamic rows, so a textarea of lines is the honest path
|
|
281
533
|
# (same idiom as the MCP env / tool params). A blank effort -> {} for that ref
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Studio
|
|
4
|
-
# Nav SVG icons for the Studio App
|
|
4
|
+
# Nav SVG icons for the Studio App. A mixin included into App: the
|
|
5
5
|
# `nav_icon` view helper and its icon table, moved verbatim out of app.rb.
|
|
6
6
|
# Inline SVG is CSP-safe (an HTML element, not an external resource).
|
|
7
7
|
module NavIcons
|
|
@@ -20,7 +20,12 @@ module Studio
|
|
|
20
20
|
tasks: '<path d="M11 12H3"/><path d="M16 6H3"/><path d="M16 18H3"/><path d="m18 9 3 3-3 3"/>',
|
|
21
21
|
approvals: '<path d="M9 12l2 2 4-4"/><path d="M12 3a9 9 0 1 0 9 9"/>',
|
|
22
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"/>'
|
|
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
|
+
parity: '<path d="M7 21h10"/><path d="M12 3v12"/><path d="m8 11 4 4 4-4"/><path d="M3 17h4l1.5-1.5L6 13 3 15.5z"/><path d="M21 17h-4l-1.5-1.5L18 13l3 2.5z"/>',
|
|
25
|
+
funnel: '<path d="M3 5h18l-8 9v6l-2 1v-7z"/>',
|
|
26
|
+
followups: '<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/>',
|
|
27
|
+
facts: '<path d="M7 3h11a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H7z"/><path d="M7 3v18"/><path d="M11 8h5"/><path d="M11 12h5"/><path d="M11 16h3"/>',
|
|
28
|
+
harvest: '<path d="M4 20V10"/><path d="M20 20V4"/><path d="M4 10h4v10"/><path d="M20 4h-4v16"/><path d="M4 20h16"/><path d="m8 8 4-4 4 4"/>'
|
|
24
29
|
}.freeze
|
|
25
30
|
|
|
26
31
|
def nav_icon(key)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# Shared transcript bubble/tool-card
|
|
1
|
+
<%# Shared transcript bubble/tool-card — one visual language for the
|
|
2
2
|
playground history, the optimistic echo and the read-only session viewer.
|
|
3
3
|
Locals: m = stored message hash {"role","content","at"}; prev = the message before
|
|
4
4
|
it in the thread, or nil (used only to spot a steered message). `content` may be a
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
when it is NOT the natural producer for the role — otherwise every bubble
|
|
23
23
|
would carry a label that says nothing. %>
|
|
24
24
|
<% origin = Insika::MessageOrigin.origin_of(m) unless Insika::MessageOrigin.customer?(m) || Insika::MessageOrigin.agent?(m) %>
|
|
25
|
-
<%# STEERED
|
|
25
|
+
<%# STEERED: a customer message sitting right after a tool result arrived
|
|
26
26
|
while the turn was running and was appended at a tool-batch boundary. Derived
|
|
27
27
|
from position, not from a field — a steered message declares no origin, and it
|
|
28
28
|
is right not to: a person typed it. Nothing else in the engine ever puts a user
|