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
|
@@ -1,24 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
<div class="reports-page reports-funnel">
|
|
2
|
+
<%# Outcomes & conversion by agent. Reads the fold's cells and the
|
|
2
3
|
BudgetLedger's current counters (D6); the ONLY mutation on this page — the
|
|
3
4
|
baseline freeze — dispatches :freeze_funnel_baseline on the bus (D7). No
|
|
4
5
|
stage vocabulary of the engine's own: everything here comes from the pack
|
|
5
6
|
declaration (D1). %>
|
|
6
7
|
<header class="page-head">
|
|
7
8
|
<div>
|
|
8
|
-
|
|
9
|
-
<h1>Funnel<span class="sub">
|
|
9
|
+
<%== crumbs_for("Funnel") %>
|
|
10
|
+
<h1>Funnel<span class="sub">Outcomes & conversion by agent</span></h1>
|
|
10
11
|
</div>
|
|
11
12
|
<div class="head-actions">
|
|
12
|
-
<%== agent_filter_form("/studio/funnel", @agent) %>
|
|
13
13
|
<form method="get" action="/studio/funnel" class="actions inline">
|
|
14
|
-
<label>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
<label>Agent
|
|
15
|
+
<%== render("_agent_picker", locals: { name: "agent", options: insika[:profile_source].ids.sort, value: @agent, blank: "All agents — type to search" }) %>
|
|
16
|
+
</label>
|
|
17
|
+
<label>Period
|
|
18
|
+
<select name="period">
|
|
19
|
+
<% ([7, 30, 90] + [@period]).uniq.sort.each do |p| %>
|
|
20
|
+
<option value="<%= p %>"<%== ' selected' if @period == p %>>Last <%= p %> days</option>
|
|
19
21
|
<% end %>
|
|
20
22
|
</select>
|
|
21
23
|
</label>
|
|
24
|
+
<button type="submit">Apply filters</button>
|
|
22
25
|
</form>
|
|
23
26
|
</div>
|
|
24
27
|
</header>
|
|
@@ -42,41 +45,97 @@
|
|
|
42
45
|
<% max = [counts.values.max || 0, 1].max %>
|
|
43
46
|
<div class="card">
|
|
44
47
|
<div class="section-head">
|
|
45
|
-
<h3><%= tenant || "platform"
|
|
46
|
-
<span class="muted">
|
|
48
|
+
<h3><%= tenant || "platform" %></h3>
|
|
49
|
+
<span class="muted">Primary outcome · <strong><%= decl.primary %></strong></span>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<%# D6: spend next to the counts, NOT cost-per-conversion. The
|
|
53
|
+
BudgetLedger holds current-window TOKEN totals, no currency and no
|
|
54
|
+
per-day series; a cost/conversion ratio is deferred until
|
|
55
|
+
the ruler proves itself (E3). %>
|
|
56
|
+
<div class="kpi-strip">
|
|
57
|
+
<%== render("_kpi", locals: { label: decl.first_stage, value: first, sub: "last #{@period}d" }) %>
|
|
58
|
+
<%== render("_kpi", locals: { label: decl.primary, value: counts[decl.primary].to_i }) %>
|
|
59
|
+
<%== render("_kpi", locals: { label: "Conversion", value: conversion ? format("%.4f", conversion) : "—" }) %>
|
|
60
|
+
<%== render("_kpi", locals: { label: "Tokens today", value: spend[:daily], sub: "month #{spend[:monthly]}" }) %>
|
|
47
61
|
</div>
|
|
48
|
-
|
|
49
|
-
|
|
62
|
+
|
|
63
|
+
<%# Chart geometry lives in a 720-unit viewBox and the .chart-funnel
|
|
64
|
+
cap holds the RENDERED width at 720px too — a wide viewport never
|
|
65
|
+
stretches the SVG past that, so a bar drawn at <= 28 units stays
|
|
66
|
+
<= 28 real pixels instead of scaling up with the page. %>
|
|
67
|
+
<svg class="chart chart-funnel" viewBox="0 0 720 120" preserveAspectRatio="none" role="img" aria-label="stage counts, last <%= @period %> days">
|
|
68
|
+
<% bw = [720.0 / decl.stages.length * 0.6, 28.0].min %>
|
|
50
69
|
<% decl.stages.each_with_index do |stage, i| %>
|
|
51
70
|
<% c = counts[stage].to_i %>
|
|
52
71
|
<% h = (c.to_f / max) * 96.0 %>
|
|
53
|
-
<% x = (i * (
|
|
72
|
+
<% x = (i * (720.0 / decl.stages.length)) + ((720.0 / decl.stages.length - bw) / 2.0) %>
|
|
54
73
|
<rect class="bar<%== ' bar-empty' if c.zero? %>" x="<%= format('%.1f', x) %>" y="<%= format('%.1f', c.zero? ? 108.0 : 108.0 - h) %>"
|
|
55
74
|
width="<%= format('%.1f', bw) %>" height="<%= format('%.1f', c.zero? ? 3.0 : [h, 3.0].max) %>" rx="2">
|
|
56
75
|
<title><%= stage %> · <%= c %></title>
|
|
57
76
|
</rect>
|
|
58
77
|
<% end %>
|
|
59
|
-
<line class="
|
|
78
|
+
<line class="axis" x1="0" y1="108" x2="720" y2="108" />
|
|
60
79
|
</svg>
|
|
61
|
-
<div class="
|
|
80
|
+
<div class="chart-labels">
|
|
62
81
|
<% decl.stages.each do |stage| %>
|
|
63
82
|
<span><%= stage %> · <%= counts[stage].to_i %></span>
|
|
64
83
|
<% end %>
|
|
65
84
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
|
|
86
|
+
<%# Where conversations stop: the loss between consecutive stages, the
|
|
87
|
+
biggest one marked. Each row drills (?drop=) into the conversations
|
|
88
|
+
that reached the stage but not the next, grouped by the last thing
|
|
89
|
+
that happened in them. %>
|
|
90
|
+
<% if decl.stages.length > 1 %>
|
|
91
|
+
<div class="section-head">
|
|
92
|
+
<h3>Where conversations stop</h3>
|
|
93
|
+
<span class="muted">lost between stages · last <%= @period %>d</span>
|
|
94
|
+
</div>
|
|
95
|
+
<table class="grid">
|
|
96
|
+
<thead><tr><th>between</th><th class="num">lost</th><th class="num">share</th><th></th></tr></thead>
|
|
97
|
+
<tbody>
|
|
98
|
+
<% funnel_drops(decl, counts).each do |d| %>
|
|
99
|
+
<tr><td><a href="<%= funnel_drop_path(profile.id, tenant, d[:from]) %>"><%= d[:from] %> → <%= d[:to] %></a></td>
|
|
100
|
+
<td class="num"><%= d[:lost] %></td>
|
|
101
|
+
<td class="num"><%= d[:of].zero? ? "—" : "#{(100.0 * d[:lost] / d[:of]).round}%" %></td>
|
|
102
|
+
<td><% if d[:biggest] %><span class="pill warn">biggest drop</span><% end %></td></tr>
|
|
103
|
+
<% end %>
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
<% groups = @agent == profile.id && @drop_tenant == tenant && @drop ? funnel_drop_sessions(decl, tenant, profile.id, @drop, @period) : nil %>
|
|
107
|
+
<% if groups %>
|
|
108
|
+
<h4>Reached <%= @drop %>, not <%= decl.stages[decl.index_of(@drop) + 1] %></h4>
|
|
109
|
+
<% if @drop_total.to_i > Studio::App::DROP_CLASSIFY_CAP %>
|
|
110
|
+
<p class="muted"><%= @drop_total %> stopped here; the newest <%= Studio::App::DROP_CLASSIFY_CAP %> are grouped below.</p>
|
|
111
|
+
<% end %>
|
|
112
|
+
<% if groups.empty? %>
|
|
113
|
+
<div class="empty">No conversation with an outcome stopped at <%= @drop %> in this window.</div>
|
|
114
|
+
<% else %>
|
|
115
|
+
<ul class="kv">
|
|
116
|
+
<% groups.each do |reason, sids| %>
|
|
117
|
+
<li><span class="k"><%= Studio::App::DROP_REASONS[reason] %> · <%= sids.size %></span>
|
|
118
|
+
<span class="v">
|
|
119
|
+
<% sids.first(Studio::App::DROP_LIST_CAP).each do |sid| %>
|
|
120
|
+
<a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>" title="<%= sid %>"><%= sid[0, 20] %></a>
|
|
121
|
+
<% end %>
|
|
122
|
+
<% if sids.size > Studio::App::DROP_LIST_CAP %><span class="muted">+<%= sids.size - Studio::App::DROP_LIST_CAP %> more</span><% end %>
|
|
123
|
+
</span></li>
|
|
124
|
+
<% end %>
|
|
125
|
+
</ul>
|
|
126
|
+
<% end %>
|
|
127
|
+
<% end %>
|
|
128
|
+
<% end %>
|
|
129
|
+
|
|
130
|
+
<details class="report-details">
|
|
131
|
+
<summary>How this funnel is counted</summary>
|
|
132
|
+
<p>Counts are folded by the tick and can be recomputed from the outcome store. Conversion is the primary outcome count divided by the first-stage count.</p>
|
|
133
|
+
<p>Stage losses compare these folded counts. A drill lists conversations instead: each one's furthest stage across its outcomes in the window.</p>
|
|
134
|
+
<p>Attribution window: <%= decl.attribution_window %> (carried, never computed). Token totals use the current budget window.</p>
|
|
135
|
+
</details>
|
|
77
136
|
|
|
78
137
|
<% baseline = insika[:funnel_store]&.baseline(tenant: tenant, agent: profile.id) %>
|
|
79
|
-
<div class="
|
|
138
|
+
<div class="actions">
|
|
80
139
|
<% if baseline %>
|
|
81
140
|
<span class="pill ok">baseline frozen</span>
|
|
82
141
|
<span class="muted"><%= baseline["from"] %> → <%= baseline["to"] %> · <%= baseline["primary"] %> <%= baseline["primary_count"] %> · conversion <%= baseline["conversion"] ? format("%.4f", baseline["conversion"]) : "—" %> · <%= baseline["window"] %> window · frozen <%= baseline["frozen_at"] %></span>
|
|
@@ -84,23 +143,25 @@
|
|
|
84
143
|
<span class="pill warn">no baseline</span>
|
|
85
144
|
<span class="muted">≥ 28 days of data required — the freeze refuses a shorter span</span>
|
|
86
145
|
<% end %>
|
|
146
|
+
<form method="post" action="/studio/funnel/<%= Rack::Utils.escape(profile.id) %>/freeze" class="actions">
|
|
147
|
+
<%== csrf_tag %>
|
|
148
|
+
<label>
|
|
149
|
+
From
|
|
150
|
+
<input type="date" name="from" value="<%= baseline && baseline["from"] %>">
|
|
151
|
+
</label>
|
|
152
|
+
<label>
|
|
153
|
+
To
|
|
154
|
+
<input type="date" name="to" value="<%= baseline && baseline["to"] %>">
|
|
155
|
+
</label>
|
|
156
|
+
<button type="submit" class="primary">Freeze baseline</button>
|
|
157
|
+
<% if tenant %>
|
|
158
|
+
<input type="hidden" name="tenant" value="<%= tenant %>">
|
|
159
|
+
<% end %>
|
|
160
|
+
</form>
|
|
87
161
|
</div>
|
|
88
|
-
<form method="post" action="/studio/funnel/<%= Rack::Utils.escape(profile.id) %>/freeze" class="actions">
|
|
89
|
-
<%== csrf_tag %>
|
|
90
|
-
<label>
|
|
91
|
-
From
|
|
92
|
-
<input type="date" name="from" value="<%= baseline && baseline["from"] %>">
|
|
93
|
-
</label>
|
|
94
|
-
<label>
|
|
95
|
-
To
|
|
96
|
-
<input type="date" name="to" value="<%= baseline && baseline["to"] %>">
|
|
97
|
-
</label>
|
|
98
|
-
<button type="submit" class="primary">Freeze baseline</button>
|
|
99
|
-
<% if tenant %>
|
|
100
|
-
<input type="hidden" name="tenant" value="<%= tenant %>">
|
|
101
|
-
<% end %>
|
|
102
|
-
</form>
|
|
103
162
|
</div>
|
|
104
163
|
<% end %>
|
|
105
164
|
<% end %>
|
|
106
165
|
<% end %>
|
|
166
|
+
|
|
167
|
+
</div>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
<header class="page-head">
|
|
1
|
+
<header class="page-head harvest-head">
|
|
2
2
|
<div>
|
|
3
|
-
|
|
4
|
-
<h1>Harvest<span class="sub"
|
|
3
|
+
<%== crumbs_for("Harvest") %>
|
|
4
|
+
<h1>Harvest<span class="sub">Turn conversation evidence into reusable skills</span></h1>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="head-actions">
|
|
7
|
+
<%== agent_filter_form("/studio/harvest", @agent) %>
|
|
5
8
|
</div>
|
|
6
9
|
</header>
|
|
7
10
|
|
|
@@ -11,44 +14,47 @@
|
|
|
11
14
|
<div class="empty">No agents yet. Add a <span class="mono">harvest:</span> block to an agent's pack to mine its finished traffic.</div>
|
|
12
15
|
<% else %>
|
|
13
16
|
<div class="card">
|
|
17
|
+
<h2>Find new skills</h2>
|
|
14
18
|
<form method="post" action="/studio/harvest" class="actions">
|
|
15
19
|
<%== csrf_tag %>
|
|
16
20
|
<label>
|
|
17
21
|
Agent
|
|
18
|
-
|
|
19
|
-
<% @agents.each do |id| %>
|
|
20
|
-
<option value="<%= id %>"<%== ' selected' if id == @agent %>><%= id %></option>
|
|
21
|
-
<% end %>
|
|
22
|
-
</select>
|
|
22
|
+
<%== render("_agent_picker", locals: { name: "agent", options: @agents, value: @agent }) %>
|
|
23
23
|
</label>
|
|
24
|
-
<label title="Ignore the mined markers and re-read the whole window (the ledger dedup still protects).">
|
|
25
|
-
<input type="checkbox" name="full" value="1"> full window
|
|
24
|
+
<label class="check" title="Ignore the mined markers and re-read the whole window (the ledger dedup still protects).">
|
|
25
|
+
<input type="checkbox" name="full" value="1"> Re-read full window
|
|
26
26
|
</label>
|
|
27
|
-
<button type="submit" class="primary">Mine now</button>
|
|
27
|
+
<button type="submit" class="primary" data-turbo-submits-with="Mining…">Mine now</button>
|
|
28
28
|
</form>
|
|
29
29
|
<div class="skill-desc muted">
|
|
30
|
-
|
|
31
|
-
proposals, and filters them through the negative list and the evidence ledger.
|
|
32
|
-
Nothing is ever applied automatically — a candidate only lands after the double
|
|
33
|
-
gate AND a human approval.
|
|
30
|
+
Reviews finished conversations and proposes skills. Nothing is applied until the evidence checks pass and you approve it.
|
|
34
31
|
</div>
|
|
35
32
|
</div>
|
|
36
33
|
|
|
37
|
-
<% if @
|
|
38
|
-
<div class="
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
<% if @idle_reason == :off %>
|
|
35
|
+
<div class="empty">Harvest is off for this agent — turn it on in <a href="/studio/agents/<%= Rack::Utils.escape(@agent) %>?cfg=harvest">Agents › Config › harvest</a>.</div>
|
|
36
|
+
<% elsif @idle_reason == :no_matcher %>
|
|
37
|
+
<div class="empty">Not mining: harvest is on but there is <strong>no grounding matcher</strong>, so the automatic loop skips every conversation. Set a matcher SKU (and uncheck "use the platform default") in <a href="/studio/agents/<%= Rack::Utils.escape(@agent) %>?cfg=grounding">Agents › Config › grounding</a>.</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<% if @candidates.empty? && @blocked.empty? && @pending_gates.empty? %>
|
|
41
|
+
<div class="empty">No skills awaiting review. Mine finished conversations to find candidates.</div>
|
|
43
42
|
<% end %>
|
|
44
43
|
|
|
45
44
|
<%# The human's inbox. %>
|
|
46
45
|
<% if @candidates.any? %>
|
|
47
|
-
<h2>Awaiting approval<span class="sub">
|
|
46
|
+
<h2>Awaiting approval<span class="sub">review evidence before applying</span></h2>
|
|
48
47
|
<% @candidates.each do |cand| %>
|
|
49
|
-
<div class="card">
|
|
48
|
+
<div class="card harvest-candidate">
|
|
49
|
+
<div class="review-heading"><div><h3><%= cand.name %></h3><p class="muted"><%= cand.description %></p></div><span class="pill warn">Awaiting approval</span></div>
|
|
50
|
+
<section class="harvest-proposal">
|
|
51
|
+
<h3>Proposed skill</h3>
|
|
52
|
+
<p class="muted"><%= cand.rationale %></p>
|
|
53
|
+
<div class="markdown-preview" data-controller="markdown"><%= cand.body %></div>
|
|
54
|
+
</section>
|
|
55
|
+
<details class="review-details"><summary>Evaluation results</summary>
|
|
50
56
|
<div class="skill-desc">
|
|
51
|
-
<span class="
|
|
57
|
+
<span class="status <%= status_class(cand.status) %>">gated</span>
|
|
52
58
|
<span class="mono"><%= cand.name %></span>
|
|
53
59
|
<span class="muted">· <%= cand.description %></span>
|
|
54
60
|
</div>
|
|
@@ -60,19 +66,23 @@
|
|
|
60
66
|
<span class="mono"><%= cand.proposer %></span> · <%= (cand.eval_gate || {})["passed_cases"] %>/<%= (cand.eval_gate || {})["cases"] %> case(s) no regression
|
|
61
67
|
· conversion <%= (cand.conversion_gate || {})["current"] %> vs <%= (cand.conversion_gate || {})["baseline"] %> (threshold <%= (cand.conversion_gate || {})["threshold"] %>)
|
|
62
68
|
</div>
|
|
69
|
+
</details>
|
|
63
70
|
<% if (excerpt = candidate_excerpt(cand)).any? %>
|
|
64
|
-
<details class="
|
|
65
|
-
<summary>evidence
|
|
71
|
+
<details class="review-details" open>
|
|
72
|
+
<summary>Conversation evidence</summary>
|
|
66
73
|
<% excerpt.each do |sid, idx, text| %>
|
|
67
|
-
<div class="
|
|
68
|
-
<a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"
|
|
74
|
+
<div class="harvest-evidence">
|
|
75
|
+
<a class="mono" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>">Open conversation ↗</a>
|
|
76
|
+
<p class="review-source"><%= text %></p>
|
|
69
77
|
</div>
|
|
70
78
|
<% end %>
|
|
71
79
|
</details>
|
|
72
80
|
<% end %>
|
|
81
|
+
<details class="review-details"><summary>Source conversations</summary>
|
|
73
82
|
<% Array(cand.origin).each do |sid| %>
|
|
74
83
|
<a class="mono muted" href="/studio/sessions/<%= Rack::Utils.escape(sid) %>"><%= sid %></a>
|
|
75
84
|
<% end %>
|
|
85
|
+
</details>
|
|
76
86
|
<form method="post" action="/studio/harvest/promote" class="actions">
|
|
77
87
|
<%== csrf_tag %>
|
|
78
88
|
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
@@ -81,13 +91,11 @@
|
|
|
81
91
|
Note
|
|
82
92
|
<input type="text" name="note" placeholder="why (optional)">
|
|
83
93
|
</label>
|
|
84
|
-
<button type="submit" class="primary">Promote</button>
|
|
85
|
-
<button type="submit" class="
|
|
94
|
+
<button type="submit" class="primary" data-turbo-submits-with="Promoting…">Promote</button>
|
|
95
|
+
<button type="submit" class="danger" formaction="/studio/harvest/reject">Reject</button>
|
|
86
96
|
</form>
|
|
87
97
|
<div class="skill-desc muted">
|
|
88
|
-
Promoting
|
|
89
|
-
scope, enables it on the allowlist and appends the promotion log row. Rollback
|
|
90
|
-
is the one-click mirror.
|
|
98
|
+
Promoting saves and enables this skill for the agent. The previous state is saved so you can roll it back.
|
|
91
99
|
</div>
|
|
92
100
|
</div>
|
|
93
101
|
<% end %>
|
|
@@ -95,13 +103,17 @@
|
|
|
95
103
|
|
|
96
104
|
<%# Gated-but-blocked: the ruler's hole, named. %>
|
|
97
105
|
<% if @blocked.any? %>
|
|
98
|
-
<h2>Blocked<span class="sub">
|
|
106
|
+
<h2>Blocked<span class="sub">resolve these before approval</span></h2>
|
|
99
107
|
<div class="card">
|
|
100
108
|
<ul class="history">
|
|
101
109
|
<% @blocked.each do |cand| %>
|
|
102
110
|
<li>
|
|
103
|
-
<span class="
|
|
111
|
+
<span class="status <%= status_class('blocked') %>">blocked</span>
|
|
104
112
|
<span class="mono"><%= cand.name %></span>
|
|
113
|
+
<% ev = cand.eval_gate || {} %>
|
|
114
|
+
<% if ev["passed"] == false && ev["reason"] && (cand.conversion_gate || {})["reason"] %>
|
|
115
|
+
<span class="preview muted"><%= ev["reason"] %> ·</span>
|
|
116
|
+
<% end %>
|
|
105
117
|
<span class="preview muted">
|
|
106
118
|
<% reason = (cand.conversion_gate || {})["reason"] %>
|
|
107
119
|
<% if reason == "no_frozen_baseline" %>
|
|
@@ -113,6 +125,14 @@
|
|
|
113
125
|
<%= (cand.eval_gate || {})["reason"] %>
|
|
114
126
|
<% end %>
|
|
115
127
|
</span>
|
|
128
|
+
<%== render("_harvest_candidate_detail", locals: { cand: cand }) %>
|
|
129
|
+
<form method="post" action="/studio/harvest/gate" class="actions inline">
|
|
130
|
+
<%== csrf_tag %>
|
|
131
|
+
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
132
|
+
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
133
|
+
<button type="submit" data-turbo-submits-with="Checking…" title="Re-run once the missing piece is fixed">Run checks again</button>
|
|
134
|
+
<button type="submit" class="danger" formaction="/studio/harvest/reject">Reject</button>
|
|
135
|
+
</form>
|
|
116
136
|
</li>
|
|
117
137
|
<% end %>
|
|
118
138
|
</ul>
|
|
@@ -121,20 +141,21 @@
|
|
|
121
141
|
|
|
122
142
|
<%# Mined, not yet gated. %>
|
|
123
143
|
<% if @pending_gates.any? %>
|
|
124
|
-
<h2>Pending<span class="sub">
|
|
144
|
+
<h2>Pending<span class="sub">awaiting evaluation and conversion checks</span></h2>
|
|
125
145
|
<div class="card">
|
|
126
146
|
<ul class="history">
|
|
127
147
|
<% @pending_gates.each do |cand| %>
|
|
128
148
|
<li>
|
|
129
|
-
<span class="
|
|
149
|
+
<span class="status <%= status_class(cand.status) %>"><%= cand.status %></span>
|
|
130
150
|
<span class="mono"><%= cand.name %></span>
|
|
131
151
|
<span class="preview muted"><%= cand.description %></span>
|
|
152
|
+
<%== render("_harvest_candidate_detail", locals: { cand: cand }) %>
|
|
132
153
|
<form method="post" action="/studio/harvest/gate" class="actions inline">
|
|
133
154
|
<%== csrf_tag %>
|
|
134
155
|
<input type="hidden" name="agent" value="<%= cand.agent %>">
|
|
135
156
|
<input type="hidden" name="candidate_id" value="<%= cand.id %>">
|
|
136
|
-
<button type="submit" class="primary">
|
|
137
|
-
<button type="submit" class="
|
|
157
|
+
<button type="submit" class="primary" data-turbo-submits-with="Checking…">Run checks</button>
|
|
158
|
+
<button type="submit" class="danger" formaction="/studio/harvest/reject">Reject</button>
|
|
138
159
|
</form>
|
|
139
160
|
</li>
|
|
140
161
|
<% end %>
|
|
@@ -144,15 +165,15 @@
|
|
|
144
165
|
|
|
145
166
|
<%# The promotion log — the single ledger, with the rollback mirror. %>
|
|
146
167
|
<% if @promotions.any? %>
|
|
147
|
-
<h2>Promoted<span class="sub">
|
|
168
|
+
<h2>Promoted<span class="sub">promotion history</span></h2>
|
|
148
169
|
<div class="card">
|
|
149
170
|
<ul class="history">
|
|
150
171
|
<% @promotions.each do |row| %>
|
|
151
172
|
<li>
|
|
152
173
|
<% if row.rolled_back_at %>
|
|
153
|
-
<span class="
|
|
174
|
+
<span class="status <%= status_class('rejected') %>">rolled back</span>
|
|
154
175
|
<% else %>
|
|
155
|
-
<span class="
|
|
176
|
+
<span class="status <%= status_class('approved') %>">promoted</span>
|
|
156
177
|
<% end %>
|
|
157
178
|
<span class="mono"><%= row.skill %></span>
|
|
158
179
|
<span class="preview muted">
|
|
@@ -164,7 +185,7 @@
|
|
|
164
185
|
<%== csrf_tag %>
|
|
165
186
|
<input type="hidden" name="agent" value="<%= row.agent %>">
|
|
166
187
|
<input type="hidden" name="snapshot_ref" value="<%= row.snapshot_ref %>">
|
|
167
|
-
<input type="text" name="reason" placeholder="
|
|
188
|
+
<label>Rollback reason <input type="text" name="reason" placeholder="Optional"></label>
|
|
168
189
|
<button type="submit" class="ghost">Rollback</button>
|
|
169
190
|
</form>
|
|
170
191
|
<% end %>
|
|
@@ -176,7 +197,8 @@
|
|
|
176
197
|
|
|
177
198
|
<%# The negative list block. %>
|
|
178
199
|
<% if @negative && @negative.rules.any? %>
|
|
179
|
-
<
|
|
200
|
+
<details class="review-details"><summary>Negative list rules</summary>
|
|
201
|
+
<h2>Negative list<span class="sub">rules that exclude candidates</span></h2>
|
|
180
202
|
<div class="card">
|
|
181
203
|
<ul class="history">
|
|
182
204
|
<% @negative.rules.each do |rule| %>
|
|
@@ -193,11 +215,12 @@
|
|
|
193
215
|
<span class="mono">insika harvest:negative import --agent ID --file F</span>.
|
|
194
216
|
</div>
|
|
195
217
|
</div>
|
|
218
|
+
</details>
|
|
196
219
|
<% end %>
|
|
197
220
|
|
|
198
221
|
<%# The ruler block. %>
|
|
199
222
|
<% if @criterion %>
|
|
200
|
-
<h2>
|
|
223
|
+
<h2>Conversion criterion<span class="sub">frozen before the first promotion</span></h2>
|
|
201
224
|
<div class="card">
|
|
202
225
|
<div class="skill-desc">
|
|
203
226
|
<span class="pill">metric <%= @criterion.rule.metric %></span>
|
|
@@ -215,20 +238,23 @@
|
|
|
215
238
|
|
|
216
239
|
<% if @runs.any? %>
|
|
217
240
|
<h2>Runs<span class="sub">most recent first</span></h2>
|
|
218
|
-
<div class="card">
|
|
219
|
-
<
|
|
220
|
-
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
241
|
+
<div class="card table-scroll" tabindex="0" role="region" aria-label="Harvest runs">
|
|
242
|
+
<table class="grid">
|
|
243
|
+
<thead>
|
|
244
|
+
<tr><th>status</th><th>run</th><th class="num">candidates</th><th class="num">tokens</th><th class="time">started</th></tr>
|
|
245
|
+
</thead>
|
|
246
|
+
<tbody>
|
|
247
|
+
<% @runs.each do |run| %>
|
|
248
|
+
<tr>
|
|
249
|
+
<td><span class="status <%= status_class(run.status) %>"><%= run.status %></span></td>
|
|
250
|
+
<td class="mono"><%= run.id[0, 12] %></td>
|
|
251
|
+
<td class="num"><%= run.candidates %></td>
|
|
252
|
+
<td class="num"><%= run.cost && run.cost["spent"].to_i.positive? ? run.cost["spent"] : "—" %></td>
|
|
253
|
+
<td class="time"><%= run.started_at %></td>
|
|
254
|
+
</tr>
|
|
255
|
+
<% end %>
|
|
256
|
+
</tbody>
|
|
257
|
+
</table>
|
|
232
258
|
</div>
|
|
233
259
|
<% end %>
|
|
234
260
|
<% end %>
|