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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<%# Shared detail for the Facts page and Turbo frame navigation. %>
|
|
2
|
+
<turbo-frame id="fact-detail" class="drill-pane drill-detail fact-detail">
|
|
3
|
+
<% if turbo_frame?("fact-detail") %>
|
|
4
|
+
<% if flash["error"] %><div class="flash err" role="alert"><%= flash["error"] %></div><% end %>
|
|
5
|
+
<% if flash["notice"] %><div class="flash ok" role="status"><%= flash["notice"] %></div><% end %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% if @selected.nil? %>
|
|
8
|
+
<div class="drill-empty">
|
|
9
|
+
<%== render("_empty", locals: { icon: :facts, text: "No proposals yet — facts appear here when a session's traffic is distilled. Nothing is ever applied automatically." }) %>
|
|
10
|
+
</div>
|
|
11
|
+
<% else %>
|
|
12
|
+
<div class="drill-pane-body">
|
|
13
|
+
<div class="fact-content">
|
|
14
|
+
<header class="fact-proposal" aria-labelledby="fact-title">
|
|
15
|
+
<div class="fact-summary">
|
|
16
|
+
<div class="fact-title-row">
|
|
17
|
+
<h2 id="fact-title"><%= @selected.key.tr("_", " ").sub(/\A./, &:upcase) %></h2>
|
|
18
|
+
<span class="status <%= status_class(@selected.status) %>"><%= @selected.status %></span>
|
|
19
|
+
</div>
|
|
20
|
+
<span class="sr-only">Proposed value</span>
|
|
21
|
+
<p class="fact-value"><%= @selected.value %></p>
|
|
22
|
+
</div>
|
|
23
|
+
<dl class="fact-context">
|
|
24
|
+
<div><dt>Model confidence</dt><dd><%= @selected.confidence ? "#{(@selected.confidence * 100).round}%" : "Not reported" %></dd></div>
|
|
25
|
+
<div><dt>Proposed</dt><dd><time datetime="<%= @selected.created_at %>" title="<%= @selected.created_at %>"><%= time_ago(@selected.created_at) %></time></dd></div>
|
|
26
|
+
</dl>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
<section class="card fact-evidence" aria-labelledby="fact-evidence-title">
|
|
30
|
+
<div class="fact-section-head">
|
|
31
|
+
<h3 id="fact-evidence-title">Conversation evidence</h3>
|
|
32
|
+
<a href="/studio/sessions/<%= Rack::Utils.escape(@selected.session_ref) %>" data-turbo-frame="_top">Open conversation ↗</a>
|
|
33
|
+
</div>
|
|
34
|
+
<% excerpt = proposal_excerpt(@selected) %>
|
|
35
|
+
<% if excerpt.empty? %>
|
|
36
|
+
<p class="muted">Evidence unavailable. The source messages may no longer be available.</p>
|
|
37
|
+
<% else %>
|
|
38
|
+
<div class="fact-quotes" tabindex="0" role="region" aria-label="Evidence excerpts">
|
|
39
|
+
<% excerpt.each do |line| %>
|
|
40
|
+
<blockquote><%= line %></blockquote>
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<% if @selected.status == "pending" %>
|
|
47
|
+
<section class="card fact-decision" aria-labelledby="fact-decision-title">
|
|
48
|
+
<h3 id="fact-decision-title">Save this fact to memory?</h3>
|
|
49
|
+
<p class="muted">Approval makes this value available in future conversations with this customer.</p>
|
|
50
|
+
<form method="post" action="/studio/facts/resolve" data-turbo-frame="_top">
|
|
51
|
+
<%== csrf_tag %>
|
|
52
|
+
<input type="hidden" name="proposal_id" value="<%= @selected.id %>">
|
|
53
|
+
<input type="hidden" name="decision" value="approved">
|
|
54
|
+
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
55
|
+
<button type="submit" class="primary" data-turbo-submits-with="Saving…">Approve & save to memory</button>
|
|
56
|
+
</form>
|
|
57
|
+
<details class="fact-decline">
|
|
58
|
+
<summary>Reject or dismiss</summary>
|
|
59
|
+
<p class="muted">Rejecting or dismissing prevents this same key and value from being proposed again.</p>
|
|
60
|
+
<form method="post" action="/studio/facts/resolve" data-turbo-frame="_top">
|
|
61
|
+
<%== csrf_tag %>
|
|
62
|
+
<input type="hidden" name="proposal_id" value="<%= @selected.id %>">
|
|
63
|
+
<input type="hidden" name="decision" value="rejected">
|
|
64
|
+
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
65
|
+
<label for="fact-rejection-note">Reason for rejection <span class="muted">(optional)</span></label>
|
|
66
|
+
<textarea id="fact-rejection-note" name="note" rows="2" placeholder="Why should this fact not be saved?"></textarea>
|
|
67
|
+
<button type="submit" class="danger" data-turbo-submits-with="Rejecting…">Reject</button>
|
|
68
|
+
</form>
|
|
69
|
+
<form method="post" action="/studio/facts/resolve" data-turbo-frame="_top">
|
|
70
|
+
<%== csrf_tag %>
|
|
71
|
+
<input type="hidden" name="proposal_id" value="<%= @selected.id %>">
|
|
72
|
+
<input type="hidden" name="decision" value="dismissed">
|
|
73
|
+
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
74
|
+
<button type="submit" class="ghost" data-turbo-submits-with="Dismissing…">Dismiss without a reason</button>
|
|
75
|
+
</form>
|
|
76
|
+
</details>
|
|
77
|
+
</section>
|
|
78
|
+
<% elsif @selected.status == "stale" %>
|
|
79
|
+
<section class="card fact-decision" aria-labelledby="fact-decision-title">
|
|
80
|
+
<h3 id="fact-decision-title">Memory changed since this proposal</h3>
|
|
81
|
+
<p class="muted">Your edit was kept. This proposal has not overwritten it.</p>
|
|
82
|
+
<dl class="fact-comparison">
|
|
83
|
+
<div><dt>Proposed value</dt><dd><s><%= @selected.value %></s></dd></div>
|
|
84
|
+
<div><dt>Current value</dt><dd><%= @selected.current_value %></dd></div>
|
|
85
|
+
</dl>
|
|
86
|
+
<form method="post" action="/studio/facts/resolve" data-turbo-frame="_top">
|
|
87
|
+
<%== csrf_tag %>
|
|
88
|
+
<input type="hidden" name="proposal_id" value="<%= @selected.id %>">
|
|
89
|
+
<input type="hidden" name="decision" value="dismissed">
|
|
90
|
+
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
91
|
+
<button type="submit" class="secondary" data-turbo-submits-with="Dismissing…">Dismiss outdated proposal</button>
|
|
92
|
+
</form>
|
|
93
|
+
</section>
|
|
94
|
+
<% else %>
|
|
95
|
+
<section class="card fact-decision" aria-label="Resolution">
|
|
96
|
+
<h3><%= @selected.status == "approved" ? "Saved to memory" : "Not saved to memory" %></h3>
|
|
97
|
+
<p class="muted">Resolved by <%= @selected.operator %>.</p>
|
|
98
|
+
<% if @selected.note && !@selected.note.to_s.empty? %><p class="fact-resolution-note"><%= @selected.note %></p><% end %>
|
|
99
|
+
</section>
|
|
100
|
+
<% end %>
|
|
101
|
+
|
|
102
|
+
<details class="fact-metadata">
|
|
103
|
+
<summary>Technical details</summary>
|
|
104
|
+
<dl>
|
|
105
|
+
<div><dt>Memory key</dt><dd><code><%= @selected.key %></code></dd></div>
|
|
106
|
+
<div><dt>Memory scope</dt><dd><%= @selected.scope %></dd></div>
|
|
107
|
+
<div><dt>Source session</dt><dd><%= @selected.session_ref %></dd></div>
|
|
108
|
+
<div><dt>Proposal ID</dt><dd><%= @selected.id %></dd></div>
|
|
109
|
+
</dl>
|
|
110
|
+
</details>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<% end %>
|
|
114
|
+
</turbo-frame>
|
|
@@ -1,133 +1,32 @@
|
|
|
1
1
|
<%# the Facts (wiki) page — the human gate on distilled proposals.
|
|
2
2
|
Nothing is ever applied automatically: each pending fact needs a click.
|
|
3
|
-
|
|
3
|
+
Console shell: one queue (pending, then stale, then recent) on the left,
|
|
4
|
+
the selected fact's evidence + actions on the right — see _facts_master.erb
|
|
5
|
+
and fact_detail.erb for the shared miller-column pieces. Reads
|
|
6
|
+
ProposalStore + SessionStore directly (evidence is a link, never a
|
|
4
7
|
copy — D7); the approve/reject/dismiss mutations dispatch :resolve_proposal
|
|
5
8
|
on the bus. %>
|
|
6
|
-
<header class="page-head">
|
|
9
|
+
<header class="page-head facts-head">
|
|
7
10
|
<div>
|
|
8
|
-
|
|
9
|
-
<h1>Facts<span class="sub">
|
|
11
|
+
<%== crumbs_for("Facts") %>
|
|
12
|
+
<h1>Facts<span class="sub">Review what your agents remember</span></h1>
|
|
10
13
|
</div>
|
|
11
14
|
<div class="head-actions">
|
|
12
15
|
<form method="get" action="/studio/facts" class="actions inline">
|
|
13
16
|
<label>
|
|
14
17
|
Agent / scope
|
|
15
|
-
|
|
16
|
-
<option value="">all</option>
|
|
17
|
-
<% @stores.each do |scope| %>
|
|
18
|
-
<option value="<%= scope %>"<%== ' selected' if scope == @filter %>><%= scope %></option>
|
|
19
|
-
<% end %>
|
|
20
|
-
</select>
|
|
18
|
+
<%== render("_agent_picker", locals: { name: "agent", options: @stores, value: @filter, blank: "All agents / scopes — type to search", auto_submit: true }) %>
|
|
21
19
|
</label>
|
|
22
20
|
</form>
|
|
23
21
|
</div>
|
|
24
22
|
</header>
|
|
25
23
|
|
|
26
24
|
<% if insika[:proposal_store].nil? %>
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<span class="mono">distill:</span> block in its pack.
|
|
30
|
-
</div>
|
|
31
|
-
<% elsif @pending.empty? && @stale.empty? && @recent.empty? %>
|
|
32
|
-
<div class="empty">
|
|
33
|
-
No proposals yet — facts appear here when a session's traffic is distilled.
|
|
34
|
-
Nothing is ever applied automatically.
|
|
35
|
-
</div>
|
|
25
|
+
<%== render("_empty", locals: { icon: :facts,
|
|
26
|
+
text: "Distillation is off — nothing distills until an agent declares a distill: block in its pack." }) %>
|
|
36
27
|
<% else %>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<h3><span class="mono"><%= p.key %></span> = <strong><%= p.value %></strong>
|
|
43
|
-
<% if p.confidence %><span class="pill info"><%= format("%.2f", p.confidence) %></span><% end %>
|
|
44
|
-
</h3>
|
|
45
|
-
<span class="muted"><%= p.scope %> · <a href="/studio/sessions/<%= Rack::Utils.escape(p.session_ref) %>"><%= p.session_ref %></a></span>
|
|
46
|
-
</div>
|
|
47
|
-
<% excerpt = proposal_excerpt(p) %>
|
|
48
|
-
<% unless excerpt.empty? %>
|
|
49
|
-
<div class="skill-desc muted">
|
|
50
|
-
<% excerpt.each do |line| %>
|
|
51
|
-
<div class="evidence"><%= line %></div>
|
|
52
|
-
<% end %>
|
|
53
|
-
</div>
|
|
54
|
-
<% end %>
|
|
55
|
-
<div class="actions">
|
|
56
|
-
<form method="post" action="/studio/facts/resolve" class="inline">
|
|
57
|
-
<%== csrf_tag %>
|
|
58
|
-
<input type="hidden" name="proposal_id" value="<%= p.id %>">
|
|
59
|
-
<input type="hidden" name="decision" value="approved">
|
|
60
|
-
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
61
|
-
<button type="submit" class="primary">Approve & save to memory</button>
|
|
62
|
-
</form>
|
|
63
|
-
<form method="post" action="/studio/facts/resolve" class="inline">
|
|
64
|
-
<%== csrf_tag %>
|
|
65
|
-
<input type="hidden" name="proposal_id" value="<%= p.id %>">
|
|
66
|
-
<input type="hidden" name="decision" value="rejected">
|
|
67
|
-
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
68
|
-
<input type="text" name="note" placeholder="reason (optional)">
|
|
69
|
-
<button type="submit" class="secondary">Reject</button>
|
|
70
|
-
</form>
|
|
71
|
-
<form method="post" action="/studio/facts/resolve" class="inline">
|
|
72
|
-
<%== csrf_tag %>
|
|
73
|
-
<input type="hidden" name="proposal_id" value="<%= p.id %>">
|
|
74
|
-
<input type="hidden" name="decision" value="dismissed">
|
|
75
|
-
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
76
|
-
<button type="submit" class="ghost">Dismiss<span class="muted"> — will not be proposed again</span></button>
|
|
77
|
-
</form>
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
<% end %>
|
|
81
|
-
<% end %>
|
|
82
|
-
|
|
83
|
-
<% unless @stale.empty? %>
|
|
84
|
-
<h2>Stale<span class="sub">the fact moved after this was distilled — the operator's edit wins (CAS)</span></h2>
|
|
85
|
-
<% @stale.each do |p| %>
|
|
86
|
-
<div class="card">
|
|
87
|
-
<div class="section-head">
|
|
88
|
-
<h3>
|
|
89
|
-
<span class="mono"><%= p.key %></span> =
|
|
90
|
-
<s><%= p.value %></s>
|
|
91
|
-
<span class="pill warn"><%= p.current_value %></span>
|
|
92
|
-
<span class="muted">(your edit)</span>
|
|
93
|
-
</h3>
|
|
94
|
-
<span class="muted"><%= p.scope %> · <a href="/studio/sessions/<%= Rack::Utils.escape(p.session_ref) %>"><%= p.session_ref %></a></span>
|
|
95
|
-
</div>
|
|
96
|
-
<div class="skill-desc">
|
|
97
|
-
The proposed value was not applied — the fact stands as you left it.
|
|
98
|
-
</div>
|
|
99
|
-
<div class="actions">
|
|
100
|
-
<form method="post" action="/studio/facts/resolve" class="inline">
|
|
101
|
-
<%== csrf_tag %>
|
|
102
|
-
<input type="hidden" name="proposal_id" value="<%= p.id %>">
|
|
103
|
-
<input type="hidden" name="decision" value="dismissed">
|
|
104
|
-
<% if @filter %><input type="hidden" name="filter" value="<%= @filter %>"><% end %>
|
|
105
|
-
<button type="submit" class="ghost">Dismiss<span class="muted"> — resolve the re-present</span></button>
|
|
106
|
-
</form>
|
|
107
|
-
</div>
|
|
108
|
-
</div>
|
|
109
|
-
<% end %>
|
|
110
|
-
<% end %>
|
|
111
|
-
|
|
112
|
-
<% unless @recent.empty? %>
|
|
113
|
-
<h2>Recent<span class="sub">resolved, most recent first</span></h2>
|
|
114
|
-
<div class="card">
|
|
115
|
-
<table>
|
|
116
|
-
<thead>
|
|
117
|
-
<tr><th>fact</th><th>status</th><th>by</th><th>note</th><th>session</th></tr>
|
|
118
|
-
</thead>
|
|
119
|
-
<tbody>
|
|
120
|
-
<% @recent.first(20).each do |p| %>
|
|
121
|
-
<tr>
|
|
122
|
-
<td><span class="mono"><%= p.key %></span> = <%= p.value %></td>
|
|
123
|
-
<td><span class="pill <%= %w[approved].include?(p.status) ? 'ok' : 'info' %>"><%= p.status %></span></td>
|
|
124
|
-
<td><%= p.operator %></td>
|
|
125
|
-
<td class="muted"><%= p.note %></td>
|
|
126
|
-
<td><a href="/studio/sessions/<%= Rack::Utils.escape(p.session_ref) %>"><%= p.session_ref %></a></td>
|
|
127
|
-
</tr>
|
|
128
|
-
<% end %>
|
|
129
|
-
</tbody>
|
|
130
|
-
</table>
|
|
131
|
-
</div>
|
|
132
|
-
<% end %>
|
|
133
|
-
<% end %>
|
|
28
|
+
<div class="drill facts-workspace" data-controller="list-filter">
|
|
29
|
+
<%== render("_facts_master", locals: { active_id: @selected&.id }) %>
|
|
30
|
+
<%== render("fact_detail") %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
@@ -1,36 +1,45 @@
|
|
|
1
|
-
<%# the Follow-ups page
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<%# the Follow-ups page — the Board shell: a KPI strip that opens the page
|
|
2
|
+
(pending/fired/cancelled/blocked, scoped to whatever the agent filter
|
|
3
|
+
currently shows) followed by one ledger + A/B card per declaring agent.
|
|
4
|
+
Reads the FollowupStore/ContactStore/OutcomeStore directly (D10); the
|
|
5
|
+
ONLY mutations — cancel a pending record, force-revoke a contact —
|
|
6
|
+
dispatch bus commands. Nothing here is engine vocabulary: the arm names,
|
|
7
|
+
the reasons and the policy values are the pack's (D1/D8). %>
|
|
8
|
+
<header class="page-head operation-head">
|
|
7
9
|
<div>
|
|
8
|
-
|
|
9
|
-
<h1>Follow-ups<span class="sub">scheduled
|
|
10
|
+
<%== crumbs_for("Follow-ups") %>
|
|
11
|
+
<h1>Follow-ups<span class="sub">Review scheduled messages and delivery history</span></h1>
|
|
10
12
|
</div>
|
|
11
13
|
<div class="head-actions">
|
|
12
14
|
<%== agent_filter_form("/studio/followups", @agent) %>
|
|
13
15
|
</div>
|
|
14
16
|
</header>
|
|
15
17
|
|
|
18
|
+
<% all_records = @stores.flat_map { |s| s[:records] } %>
|
|
19
|
+
<div class="kpi-strip">
|
|
20
|
+
<%== render("_kpi", locals: { label: "Pending", value: all_records.count { |r| r.status == "pending" } }) %>
|
|
21
|
+
<%== render("_kpi", locals: { label: "Fired", value: all_records.count { |r| r.status == "fired" } }) %>
|
|
22
|
+
<%== render("_kpi", locals: { label: "Cancelled", value: all_records.count { |r| r.status == "cancelled" } }) %>
|
|
23
|
+
<%== render("_kpi", locals: { label: "Blocked", value: all_records.count { |r| r.status == "blocked" } }) %>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
16
26
|
<% if !@has_followups %>
|
|
17
|
-
|
|
18
|
-
No follow-ups — add a
|
|
19
|
-
the records it schedules land here.
|
|
20
|
-
</div>
|
|
27
|
+
<%== render("_empty", locals: { icon: :followups,
|
|
28
|
+
text: "No follow-ups — add a followup: block to an agent's pack and the records it schedules land here." }) %>
|
|
21
29
|
<% else %>
|
|
30
|
+
<div class="operation-followups" data-controller="list-filter">
|
|
31
|
+
<div class="filter-bar">
|
|
32
|
+
<input type="search" class="filter-input" placeholder="Filter by customer, reason or status…" aria-label="Filter follow-ups"
|
|
33
|
+
data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
|
|
34
|
+
<span class="filter-count mono muted" data-list-filter-target="count"></span>
|
|
35
|
+
</div>
|
|
22
36
|
<% @stores.each do |store| %>
|
|
23
37
|
<% profile, policy, records = store[:profile], store[:policy], store[:records] %>
|
|
24
|
-
|
|
25
|
-
<% fired = records.select { |r| r.status == "fired" } %>
|
|
26
|
-
<% cancelled = records.select { |r| r.status == "cancelled" } %>
|
|
27
|
-
<% blocked = records.select { |r| r.status == "blocked" } %>
|
|
28
|
-
<h2><%= profile.id %><span class="sub">arm <strong><%= policy.arm %></strong> · <%= policy.cancel_keywords.size %> keyword(s) · silence after <%= policy.silence_after_sends %> send(s)</span></h2>
|
|
38
|
+
<h2><a href="<%= agent_path(profile.id) %>"><%= profile.id %></a><span class="sub"><%= records.count { |r| r.status == "pending" } %> pending · <%= records.size %> records</span></h2>
|
|
29
39
|
|
|
30
|
-
<div class="card">
|
|
40
|
+
<div class="card operation-table">
|
|
31
41
|
<div class="section-head">
|
|
32
42
|
<h3>Schedule records</h3>
|
|
33
|
-
<span class="muted"><%= pending.size %> pending · <%= fired.size %> fired · <%= cancelled.size %> cancelled · <%= blocked.size %> blocked</span>
|
|
34
43
|
</div>
|
|
35
44
|
|
|
36
45
|
<% if records.empty? %>
|
|
@@ -39,39 +48,35 @@
|
|
|
39
48
|
<table class="grid">
|
|
40
49
|
<thead>
|
|
41
50
|
<tr>
|
|
42
|
-
<th>
|
|
51
|
+
<th>Customer</th><th>Scheduled for</th><th>Reason</th><th>Arm</th><th>Status</th><th>Actions</th>
|
|
43
52
|
</tr>
|
|
44
53
|
</thead>
|
|
45
54
|
<tbody>
|
|
46
55
|
<% records.each do |r| %>
|
|
47
|
-
<tr>
|
|
48
|
-
<td class="mono"><%= r.customer %></td>
|
|
56
|
+
<tr data-list-filter-target="item" data-filter-text="<%= profile.id %> <%= r.customer %> <%= r.reason %> <%= r.arm %> <%= r.status %> <%= r.blocked_reason %>">
|
|
57
|
+
<td class="mono"><%= r.customer %><% if presence(r.session_id) %><br><a href="/studio/sessions/<%= Rack::Utils.escape(r.session_id) %>">Conversation ↗</a><% end %></td>
|
|
49
58
|
<td class="mono"><%= r.at %></td>
|
|
50
59
|
<td><%= r.reason %></td>
|
|
51
|
-
<td><span class="
|
|
60
|
+
<td><span class="status info"><%= r.arm %></span></td>
|
|
52
61
|
<td>
|
|
53
|
-
<% if r.status == "
|
|
54
|
-
<span class="
|
|
55
|
-
<% elsif r.status == "fired" %>
|
|
56
|
-
<span class="pill ok">fired</span>
|
|
57
|
-
<% elsif r.status == "cancelled" %>
|
|
58
|
-
<span class="pill">cancelled</span>
|
|
62
|
+
<% if r.status == "blocked" %>
|
|
63
|
+
<span class="status <%= status_class(r.status) %>">blocked · <%= r.blocked_reason %></span>
|
|
59
64
|
<% else %>
|
|
60
|
-
<span class="
|
|
65
|
+
<span class="status <%= status_class(r.status) %>"><%= r.status %></span>
|
|
61
66
|
<% end %>
|
|
62
67
|
</td>
|
|
63
|
-
<td>
|
|
68
|
+
<td class="row-actions">
|
|
64
69
|
<% if r.status == "pending" %>
|
|
65
70
|
<form method="post" action="/studio/followups/<%= Rack::Utils.escape(profile.id) %>/cancel" class="actions">
|
|
66
71
|
<%== csrf_tag %>
|
|
67
72
|
<input type="hidden" name="id" value="<%= r.id %>">
|
|
68
|
-
<button type="submit" class="danger">Cancel</button>
|
|
73
|
+
<button type="submit" class="btn ghost btn-sm danger">Cancel</button>
|
|
69
74
|
</form>
|
|
70
75
|
<form method="post" action="/studio/followups/<%= Rack::Utils.escape(profile.id) %>/revoke" class="actions">
|
|
71
76
|
<%== csrf_tag %>
|
|
72
77
|
<input type="hidden" name="customer" value="<%= r.customer %>">
|
|
73
78
|
<input type="hidden" name="tenant" value="<%= r.tenant %>">
|
|
74
|
-
<button type="submit" class="danger" title="A channel opt-out event that never arrived — revokes the contact and cancels its pending follow-ups">Revoke contact</button>
|
|
79
|
+
<button type="submit" class="btn ghost btn-sm danger" title="A channel opt-out event that never arrived — revokes the contact and cancels its pending follow-ups">Revoke contact</button>
|
|
75
80
|
</form>
|
|
76
81
|
<% end %>
|
|
77
82
|
</td>
|
|
@@ -81,40 +86,44 @@
|
|
|
81
86
|
</table>
|
|
82
87
|
<% end %>
|
|
83
88
|
|
|
84
|
-
<
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
<div class="skill-desc">
|
|
89
|
+
<details class="operation-diagnostics">
|
|
90
|
+
<summary>Follow-up policy</summary>
|
|
91
|
+
<p class="muted">Arm <strong><%= policy.arm %></strong> · silence after <%= policy.silence_after_sends %> send(s). <a href="<%= agent_path(profile.id, "config", "cfg=followups") %>">Edit policy ↗</a></p>
|
|
88
92
|
<% qh = policy.quiet_hours %>
|
|
89
|
-
<
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
<p class="skill-desc">
|
|
94
|
+
<span class="status info"><%= qh ? "quiet #{qh.start}-#{qh.end} <#{qh.timezone}>" : "no quiet hours" %></span>
|
|
95
|
+
<span class="status info"><%= policy.max_frequency ? "max #{policy.max_frequency}" : "no frequency ceiling" %></span>
|
|
96
|
+
<span class="muted"><%= policy.cancel_keywords.empty? ? "no cancellation keywords" : "keywords: #{policy.cancel_keywords.join(' · ')}" %></span>
|
|
97
|
+
</p>
|
|
98
|
+
</details>
|
|
93
99
|
</div>
|
|
94
100
|
|
|
95
|
-
<%# D3: the A/B card — a READ-ONLY fold over the same records. The
|
|
96
|
-
outside the card is the operator's
|
|
97
|
-
reads it (the note says so).
|
|
98
|
-
|
|
101
|
+
<%# D3: the A/B card — a READ-ONLY fold over the same records. The
|
|
102
|
+
ruler outside the card is the operator's baseline; this card only
|
|
103
|
+
reads it (the note says so). A regression spec matches the
|
|
104
|
+
conversion-% and opt-outs cells' text content ("…100.0%…1…") to
|
|
105
|
+
prove the opt-out count is arm-scoped, never a global revoked
|
|
106
|
+
count — it doesn't pin the exact opening-tag markup. %>
|
|
107
|
+
<div class="card operation-table">
|
|
99
108
|
<div class="section-head">
|
|
100
109
|
<h3>A/B readout <span class="muted">last 30 days · per arm</span></h3>
|
|
101
|
-
<span class="muted">the A/B is the operator's — the card reads the
|
|
110
|
+
<span class="muted">the A/B is the operator's — the card reads the baseline</span>
|
|
102
111
|
</div>
|
|
103
112
|
<% if store[:ab].empty? %>
|
|
104
113
|
<div class="empty">Nothing fired in the period — the readout appears when the engine fires.</div>
|
|
105
114
|
<% else %>
|
|
106
115
|
<table class="grid">
|
|
107
116
|
<thead>
|
|
108
|
-
<tr><th>arm</th><th>sent</th><th>conversions</th><th>conversion %</th><th>opt-outs</th></tr>
|
|
117
|
+
<tr><th>arm</th><th class="num">sent</th><th class="num">conversions</th><th class="num">conversion %</th><th>opt-outs</th></tr>
|
|
109
118
|
</thead>
|
|
110
119
|
<tbody>
|
|
111
120
|
<% store[:ab].each do |arm, row| %>
|
|
112
121
|
<tr>
|
|
113
|
-
<td><span class="
|
|
114
|
-
<td><%= row[:sent] %></td>
|
|
115
|
-
<td><%= row[:conversions] %></td>
|
|
116
|
-
<td><%= row[:sent].zero? ? "—" : format("%.1f", row[:conversions].to_f / row[:sent] * 100) %>%</td>
|
|
117
|
-
<td><%= row[:opt_outs] %></td>
|
|
122
|
+
<td><span class="status info"><%= arm %></span></td>
|
|
123
|
+
<td class="num tnum"><%= row[:sent] %></td>
|
|
124
|
+
<td class="num tnum"><%= row[:conversions] %></td>
|
|
125
|
+
<td class="num tnum"><%= row[:sent].zero? ? "—" : format("%.1f", row[:conversions].to_f / row[:sent] * 100) %>%</td>
|
|
126
|
+
<td class="num tnum"><%= row[:opt_outs] %></td>
|
|
118
127
|
</tr>
|
|
119
128
|
<% end %>
|
|
120
129
|
</tbody>
|
|
@@ -122,4 +131,6 @@
|
|
|
122
131
|
<% end %>
|
|
123
132
|
</div>
|
|
124
133
|
<% end %>
|
|
134
|
+
<div class="empty" data-list-filter-target="empty" hidden>No follow-ups match your filter.</div>
|
|
135
|
+
</div>
|
|
125
136
|
<% end %>
|