insika 0.2.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 +145 -17
- data/README.md +7 -1
- data/bin/insika +211 -8
- data/docs/AGENTS.md +377 -2
- data/docs/API.md +56 -0
- data/docs/CHANNELS.md +99 -9
- data/docs/CONTEXT.md +127 -16
- data/docs/DEPLOY.md +5 -2
- data/docs/EVALS.md +19 -0
- data/docs/FACTS.md +135 -0
- data/docs/HARVEST.md +117 -0
- data/docs/LOADTEST.md +14 -8
- data/docs/OBSERVABILITY.md +54 -0
- data/docs/REFINEMENT.md +3 -3
- data/docs/RELEASING.md +27 -0
- data/docs/RUNNING-LOCAL.md +3 -3
- data/docs/SECURITY.md +61 -0
- data/docs/SKILLS.md +2 -2
- data/docs/SOAK.md +127 -0
- data/docs/TOOLS.md +70 -2
- data/docs/domain.md +115 -0
- data/docs/index.md +1 -1
- data/lib/insika/agent_profile.rb +145 -10
- data/lib/insika/balloon_splitter.rb +102 -0
- data/lib/insika/budget_ledger.rb +34 -3
- data/lib/insika/cache_series_store.rb +49 -0
- data/lib/insika/channel_delivery.rb +118 -13
- data/lib/insika/channels/relay.rb +77 -3
- data/lib/insika/channels/web.rb +2 -2
- data/lib/insika/chat_builder.rb +84 -1
- data/lib/insika/checkpoint_store.rb +16 -0
- data/lib/insika/commands/agent_payload.rb +3 -3
- data/lib/insika/commands/cancel_followup.rb +49 -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 +1 -1
- 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/revoke_contact.rb +49 -0
- data/lib/insika/commands/rollback_harvest.rb +86 -0
- data/lib/insika/commands/run_distillation.rb +186 -0
- data/lib/insika/commands/run_harvest.rb +393 -0
- data/lib/insika/commands/send_message.rb +103 -6
- data/lib/insika/commands/session_purge.rb +67 -0
- data/lib/insika/contact_store.rb +183 -0
- data/lib/insika/context/builder.rb +21 -3
- data/lib/insika/context/fragment.rb +7 -3
- data/lib/insika/context/priority.rb +3 -0
- 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/skill.rb +2 -0
- data/lib/insika/context/providers/tool_search.rb +2 -0
- data/lib/insika/context_trace_store.rb +38 -2
- data/lib/insika/distill.rb +224 -0
- data/lib/insika/distill_engine.rb +169 -0
- data/lib/insika/doctor.rb +716 -6
- data/lib/insika/dsl/runtime.rb +9 -3
- data/lib/insika/dsl/server_boot.rb +70 -1
- data/lib/insika/dsl.rb +83 -3
- data/lib/insika/edge_limiter.rb +28 -7
- data/lib/insika/env_schema.rb +9 -2
- data/lib/insika/errors.rb +11 -0
- data/lib/insika/evals/assertions.rb +3 -2
- data/lib/insika/evals/pairwise.rb +18 -6
- data/lib/insika/evidence.rb +183 -0
- data/lib/insika/executor.rb +687 -70
- 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/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 +1 -1
- 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/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 +6 -1
- data/lib/insika/model_visible.rb +87 -0
- data/lib/insika/model_visible_trace_store.rb +66 -0
- data/lib/insika/onboarding.rb +6 -1
- data/lib/insika/outbox_store.rb +42 -4
- data/lib/insika/outcome_store.rb +147 -0
- 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/prefix_fingerprint.rb +58 -0
- data/lib/insika/profile_source.rb +22 -1
- data/lib/insika/proposal_store.rb +271 -0
- data/lib/insika/queue_policy.rb +4 -1
- data/lib/insika/refinement/proposer.rb +1 -1
- data/lib/insika/reliability.rb +32 -6
- data/lib/insika/retention.rb +281 -0
- data/lib/insika/routing.rb +101 -0
- data/lib/insika/safety/config.rb +44 -4
- data/lib/insika/safety/corpus.rb +255 -0
- data/lib/insika/safety/detectors.rb +32 -113
- data/lib/insika/safety/factory.rb +15 -2
- 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 +1 -1
- data/lib/insika/safety/moderator.rb +1 -1
- data/lib/insika/safety/output_filter.rb +9 -5
- data/lib/insika/safety/output_validator.rb +11 -5
- data/lib/insika/schema_guard.rb +35 -0
- data/lib/insika/server/app.rb +144 -4
- data/lib/insika/server/rack_app.rb +16 -0
- data/lib/insika/server/responses.rb +40 -1
- data/lib/insika/session_actor.rb +8 -4
- data/lib/insika/session_store.rb +65 -2
- data/lib/insika/settings_store.rb +10 -0
- data/lib/insika/shadow_pair_store.rb +258 -0
- data/lib/insika/skill_catalog.rb +4 -0
- 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/store.rb +10 -1
- data/lib/insika/stores/memory.rb +6 -0
- data/lib/insika/stores/sqlite.rb +8 -0
- data/lib/insika/studio/app.rb +875 -14
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +22 -22
- data/lib/insika/studio/assets/dist/favicon.svg +6 -0
- data/lib/insika/studio/forms.rb +262 -5
- data/lib/insika/studio/nav_icons.rb +6 -1
- data/lib/insika/studio/views/agent_detail.erb +627 -84
- data/lib/insika/studio/views/agents.erb +10 -6
- 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/session.erb +56 -1
- data/lib/insika/studio/views/settings.erb +2 -0
- data/lib/insika/studio/views/skills.erb +0 -1
- 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/task_store.rb +21 -1
- data/lib/insika/testing/store_contract.rb +27 -6
- data/lib/insika/tick.rb +25 -1
- data/lib/insika/tool_definition.rb +17 -7
- data/lib/insika/tool_envelope.rb +69 -0
- data/lib/insika/tool_manifest.rb +5 -1
- data/lib/insika/tools/data_defined_tool.rb +10 -0
- data/lib/insika/tools/generate_image.rb +44 -0
- data/lib/insika/tools/schedule_followup.rb +164 -0
- data/lib/insika/tools/tts.rb +47 -0
- data/lib/insika/tools/update_briefing.rb +126 -0
- data/lib/insika/turn_state.rb +38 -1
- data/lib/insika/turn_timing.rb +22 -2
- data/lib/insika/version.rb +1 -1
- data/lib/insika/vitals.rb +84 -0
- data/lib/insika/wiring/graph.rb +300 -13
- data/lib/insika.rb +92 -3
- metadata +77 -1
|
@@ -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
|
@@ -7,6 +7,10 @@ module Studio
|
|
|
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),
|
|
@@ -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,6 +301,8 @@ 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
|
|
|
@@ -173,6 +425,11 @@ module Studio
|
|
|
173
425
|
v = presence(r.params[f])
|
|
174
426
|
patch[f] = Integer(v) if v
|
|
175
427
|
end
|
|
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)
|
|
176
433
|
patch
|
|
177
434
|
end
|
|
178
435
|
|
|
@@ -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)
|