insika 0.3.0 → 0.7.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 +180 -0
- data/README.md +45 -10
- data/bin/insika +684 -0
- data/bin/insika-router +87 -0
- data/docs/AGENTS.md +94 -403
- data/docs/API.md +5 -5
- data/docs/ARCHITECTURE.md +3 -2
- data/docs/ARTIFACTS.md +95 -0
- data/docs/BENCHMARK.md +2 -2
- data/docs/CHANNELS.md +14 -14
- data/docs/CONTEXT.md +9 -7
- data/docs/DEMO.md +80 -0
- data/docs/DEPLOY.md +71 -3
- data/docs/EMBEDDING.md +1 -1
- data/docs/EVALS.md +128 -3
- data/docs/FACTS.md +3 -3
- data/docs/HARVEST.md +5 -6
- data/docs/KNOWLEDGE.md +290 -0
- data/docs/LOADTEST.md +2 -2
- data/docs/MEDIA.md +128 -0
- data/docs/OBSERVABILITY.md +15 -10
- data/docs/OUTCOMES.md +137 -0
- data/docs/PLUGINS.md +51 -6
- data/docs/POLICY.md +216 -0
- data/docs/REFINEMENT.md +14 -9
- data/docs/RELEASING.md +4 -4
- data/docs/ROUTER.md +213 -0
- data/docs/RUNNING-LOCAL.md +3 -3
- data/docs/SCHEDULING.md +121 -0
- data/docs/SECURITY.md +22 -6
- data/docs/SKILLS.md +11 -2
- data/docs/SOAK.md +2 -2
- data/docs/TEMPLATES.md +134 -0
- data/docs/TOOLS.md +152 -27
- data/docs/WHY.md +1 -1
- data/docs/WORKFLOWS.md +2 -2
- data/docs/_includes/head_custom.html +5 -0
- data/docs/_includes/title.html +13 -0
- data/docs/_sass/color_schemes/insika.scss +32 -0
- data/docs/_sass/custom/custom.scss +199 -0
- data/docs/_sass/custom/setup.scss +26 -0
- data/docs/assets/img/favicon.svg +7 -0
- data/docs/assets/img/insika-mark.svg +7 -0
- data/docs/core-concepts.md +21 -0
- data/docs/domain.md +4 -4
- data/docs/improve.md +20 -0
- data/docs/index.md +8 -5
- data/docs/integrate.md +20 -0
- data/docs/operate.md +13 -6
- data/docs/prompts/ADD-TOOL.md +118 -0
- data/docs/prompts/DIAGNOSE-TURN.md +65 -0
- data/docs/prompts/GO-LIVE.md +138 -0
- data/docs/prompts/RUN-EXAMPLES.md +70 -0
- data/docs/reference.md +19 -0
- data/docs/ship.md +10 -2
- data/docs/start-here.md +18 -0
- data/lib/insika/agent_profile.rb +73 -16
- data/lib/insika/artifact_signing.rb +82 -0
- data/lib/insika/artifact_store.rb +160 -0
- data/lib/insika/channel_delivery.rb +1 -1
- data/lib/insika/chat_builder.rb +22 -2
- data/lib/insika/commands/agent_payload.rb +2 -2
- data/lib/insika/commands/backfill_knowledge.rb +145 -0
- data/lib/insika/commands/delete_artifact.rb +35 -0
- data/lib/insika/commands/delete_concept.rb +34 -0
- data/lib/insika/commands/delete_mcp.rb +6 -2
- data/lib/insika/commands/delete_tenant_data.rb +15 -3
- data/lib/insika/commands/gate_refinement.rb +1 -1
- data/lib/insika/commands/refresh_mcp_tools.rb +47 -0
- data/lib/insika/commands/restore_concept.rb +34 -0
- data/lib/insika/commands/seed_demo_data.rb +31 -0
- data/lib/insika/commands/upsert_mcp.rb +6 -3
- data/lib/insika/commands/write_concept.rb +57 -0
- data/lib/insika/context/priority.rb +2 -0
- data/lib/insika/context/providers/knowledge.rb +108 -0
- data/lib/insika/context/providers/prompt.rb +30 -24
- data/lib/insika/cron.rb +189 -0
- data/lib/insika/demo/agent_attrs.rb +43 -0
- data/lib/insika/demo/golden_cases.rb +81 -0
- data/lib/insika/demo/seeder.rb +336 -0
- data/lib/insika/doctor.rb +176 -8
- data/lib/insika/dsl/definition.rb +3 -2
- data/lib/insika/dsl/runtime.rb +60 -79
- data/lib/insika/dsl/server_boot.rb +23 -1
- data/lib/insika/dsl/system.rb +10 -2
- data/lib/insika/dsl.rb +103 -2
- data/lib/insika/env_schema.rb +16 -1
- data/lib/insika/evals/golden.rb +41 -4
- data/lib/insika/evals/judge.rb +47 -2
- data/lib/insika/evals/pairwise.rb +11 -0
- data/lib/insika/evals/persona.rb +98 -0
- data/lib/insika/evals/runner.rb +9 -0
- data/lib/insika/evals/simulator.rb +225 -0
- data/lib/insika/evals/transport.rb +83 -1
- data/lib/insika/event_stream.rb +10 -0
- data/lib/insika/executor.rb +231 -55
- data/lib/insika/followup_policy.rb +2 -25
- data/lib/insika/golden_store.rb +16 -1
- data/lib/insika/grounding/matcher.rb +1 -1
- data/lib/insika/knowledge.rb +680 -0
- data/lib/insika/knowledge_store.rb +140 -0
- data/lib/insika/mcp_client.rb +94 -0
- data/lib/insika/mcp_json.rb +74 -0
- data/lib/insika/mcp_live_tool.rb +43 -0
- data/lib/insika/mcp_store.rb +98 -26
- data/lib/insika/mcp_tool_ingestor.rb +30 -8
- data/lib/insika/mcp_tool_registry.rb +100 -0
- data/lib/insika/media.rb +115 -31
- data/lib/insika/message_origin.rb +1 -1
- data/lib/insika/middleware.rb +9 -0
- data/lib/insika/onboarding.rb +17 -1
- data/lib/insika/outcome_store.rb +1 -1
- data/lib/insika/overlay_tool_registry.rb +37 -17
- data/lib/insika/packaging.rb +2 -2
- data/lib/insika/profile_source.rb +8 -1
- data/lib/insika/prompt_catalog.rb +10 -0
- data/lib/insika/retention.rb +36 -1
- data/lib/insika/router/app.rb +157 -0
- data/lib/insika/router/backend_pool.rb +98 -0
- data/lib/insika/router/hash_ring.rb +55 -0
- data/lib/insika/router/proxy_body.rb +34 -0
- data/lib/insika/router/session_key.rb +54 -0
- data/lib/insika/router.rb +18 -0
- data/lib/insika/schedule.rb +177 -0
- data/lib/insika/schedule_engine.rb +314 -0
- data/lib/insika/schedule_store.rb +208 -0
- data/lib/insika/server/app.rb +105 -15
- data/lib/insika/server/rack_app.rb +5 -1
- data/lib/insika/server/responses.rb +1 -1
- data/lib/insika/skill_catalog.rb +12 -0
- data/lib/insika/steer_injector.rb +21 -10
- data/lib/insika/studio/app.rb +567 -45
- data/lib/insika/studio/assets/dist/application.css +1 -1
- data/lib/insika/studio/assets/dist/application.js +21 -21
- data/lib/insika/studio/forms.rb +46 -5
- data/lib/insika/studio/nav_icons.rb +14 -1
- data/lib/insika/studio/views/_agent_tab_cache.erb +25 -0
- data/lib/insika/studio/views/_agent_tab_config.erb +514 -0
- data/lib/insika/studio/views/_agent_tab_history.erb +24 -0
- data/lib/insika/studio/views/_agent_tab_loops.erb +54 -0
- data/lib/insika/studio/views/_agent_tab_memory.erb +51 -0
- data/lib/insika/studio/views/_agent_tab_outcomes.erb +31 -0
- data/lib/insika/studio/views/_agent_tab_prompts.erb +108 -0
- data/lib/insika/studio/views/_agent_tab_skills.erb +38 -0
- data/lib/insika/studio/views/_agents_master.erb +44 -0
- data/lib/insika/studio/views/_message.erb +49 -32
- data/lib/insika/studio/views/agent_detail.erb +61 -820
- data/lib/insika/studio/views/agents.erb +70 -57
- data/lib/insika/studio/views/artifact.erb +23 -0
- data/lib/insika/studio/views/artifacts.erb +59 -0
- data/lib/insika/studio/views/evals.erb +2 -2
- data/lib/insika/studio/views/facts.erb +1 -1
- data/lib/insika/studio/views/funnel.erb +1 -1
- data/lib/insika/studio/views/home.erb +106 -67
- data/lib/insika/studio/views/knowledge.erb +123 -0
- data/lib/insika/studio/views/layout.erb +14 -11
- data/lib/insika/studio/views/mcp.erb +174 -80
- data/lib/insika/studio/views/session.erb +231 -177
- data/lib/insika/studio/views/settings.erb +39 -1
- data/lib/insika/studio/views/skills.erb +1 -1
- data/lib/insika/studio/views/tools.erb +24 -9
- data/lib/insika/templates/browser-agent/README.md +36 -0
- data/lib/insika/templates/browser-agent/agent.rb +49 -0
- data/lib/insika/templates/daily-digest/README.md +38 -0
- data/lib/insika/templates/daily-digest/agent.rb +77 -0
- data/lib/insika/templates/repo-explorer/README.md +36 -0
- data/lib/insika/templates/repo-explorer/agent.rb +45 -0
- data/lib/insika/templates/research-analyst/README.md +26 -0
- data/lib/insika/templates/research-analyst/agent.rb +58 -0
- data/lib/insika/templates/review-panel/README.md +20 -0
- data/lib/insika/templates/review-panel/agent.rb +50 -0
- data/lib/insika/templates/travel-planner/README.md +35 -0
- data/lib/insika/templates/travel-planner/agent.rb +87 -0
- data/lib/insika/templates.rb +112 -0
- data/lib/insika/tick.rb +24 -12
- data/lib/insika/timezone.rb +45 -0
- data/lib/insika/tools/generate_image.rb +52 -7
- data/lib/insika/tools/load_knowledge.rb +74 -0
- data/lib/insika/tools/run_persona_eval.rb +328 -0
- data/lib/insika/tools/save_artifact.rb +95 -0
- data/lib/insika/turn_output.rb +1 -1
- data/lib/insika/turn_state.rb +15 -4
- data/lib/insika/version.rb +1 -1
- data/lib/insika/wiring/graph.rb +184 -12
- data/lib/insika/wiring/graph_chat.rb +102 -0
- data/lib/insika.rb +57 -0
- metadata +105 -5
- data/docs/build.md +0 -14
- data/docs/understand.md +0 -10
|
@@ -1,828 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<div
|
|
12
|
-
<nav class="
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<a href="#memory" data-tabs-target="tab">memory</a>
|
|
17
|
-
<a href="#outcomes" data-tabs-target="tab">outcomes</a>
|
|
18
|
-
<a href="#loops" data-tabs-target="tab">loops</a>
|
|
19
|
-
<a href="#cache" data-tabs-target="tab">cache</a>
|
|
20
|
-
<a href="#history" data-tabs-target="tab">history</a>
|
|
21
|
-
</nav>
|
|
22
|
-
|
|
23
|
-
<!-- ── Config / model ────────────────────────────────────────────────────── -->
|
|
24
|
-
<section class="section" id="config" data-tabs-target="panel">
|
|
25
|
-
<% config_groups = [
|
|
26
|
-
["model", "Model & limits", "model, provider, timeouts, ceilings, generation, fence"],
|
|
27
|
-
["guardrails", "Guardrails", "input/output checks, strictness, safe replies"],
|
|
28
|
-
["grounding", "Grounding", "evidence behind product claims"],
|
|
29
|
-
["funnel", "Outcome funnel", "stage vocabulary"],
|
|
30
|
-
["followups", "Follow-ups", "frequency, quiet hours, silence"],
|
|
31
|
-
["distill", "Distillation", "finished chats → fact proposals"],
|
|
32
|
-
["harvest", "Harvest", "traffic → skill candidates"],
|
|
33
|
-
["refinement", "Refinement", "ranked findings, proposed edits"],
|
|
34
|
-
["budget_rel", "Budget & reliability", "spend caps, retries, fallback, breaker"],
|
|
35
|
-
["routing", "Routing & media", "routes, outputs, alerts, the streaming edge"],
|
|
36
|
-
["advanced", "Advanced", "capabilities, allowlists, behaviour, metadata"]
|
|
37
|
-
]
|
|
38
|
-
active = config_groups.find { |k, _| k == @config_group } || config_groups.first
|
|
39
|
-
@config_group = active[0] %>
|
|
40
|
-
<div class="section-head"><h2>Configuration</h2><p class="muted">every profile field, editable hot — pick a group to the left — saved via <code>update_agent</code>.</p></div>
|
|
41
|
-
<div class="drill config-drill">
|
|
42
|
-
<aside class="drill-pane drill-master">
|
|
43
|
-
<div class="drill-pane-head"><div class="title"><strong>Configuration</strong></div></div>
|
|
44
|
-
<nav class="drill-pane-body drill-list" aria-label="config groups">
|
|
45
|
-
<% config_groups.each do |key, label, sub| %>
|
|
46
|
-
<a class="drill-item<%== ' active' if @config_group == key %>"
|
|
47
|
-
href="<%= agent_path(@agent.id, "config", "cfg=#{key}") %>">
|
|
48
|
-
<div class="drill-item-name"><span class="txt"><%= label %></span></div>
|
|
49
|
-
<div class="drill-item-sub"><%= sub %></div>
|
|
50
|
-
</a>
|
|
51
|
-
<% end %>
|
|
52
|
-
</nav>
|
|
53
|
-
</aside>
|
|
54
|
-
<section class="drill-pane drill-detail">
|
|
55
|
-
<div class="drill-pane-head">
|
|
56
|
-
<div class="title"><strong><%= active[1] %></strong></div>
|
|
57
|
-
<button type="submit" form="config-form" class="btn primary" data-turbo-submits-with="Saving…">Save config</button>
|
|
58
|
-
</div>
|
|
59
|
-
<div class="drill-pane-body">
|
|
60
|
-
<form id="config-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/config">
|
|
61
|
-
<%== csrf_tag %>
|
|
62
|
-
<input type="hidden" name="cfg" value="<%= @config_group %>">
|
|
63
|
-
<div class="config-group" data-config-group="model"<%== " hidden" unless @config_group == "model" %>>
|
|
64
|
-
<div class="grid-2">
|
|
65
|
-
<label>model <span class="muted">(blank = platform <code>default_model</code>)</span>
|
|
66
|
-
<input type="text" name="model" value="<%= @agent.model %>" placeholder="inherit platform default">
|
|
67
|
-
</label>
|
|
68
|
-
<label>provider
|
|
69
|
-
<input type="text" name="provider" value="<%= @agent.provider %>" placeholder="deepseek">
|
|
70
|
-
</label>
|
|
71
|
-
<label>turn_timeout (s)
|
|
72
|
-
<input type="text" name="turn_timeout" value="<%= @agent.limits[:turn_timeout] %>" inputmode="numeric">
|
|
73
|
-
</label>
|
|
74
|
-
<label>tool_timeout (s)
|
|
75
|
-
<input type="text" name="tool_timeout" value="<%= @agent.limits[:tool_timeout] %>" inputmode="numeric">
|
|
76
|
-
</label>
|
|
77
|
-
<label>tool_concurrency <span class="muted">(parallel tool calls; 1 = serial, off for turns with approvals)</span>
|
|
78
|
-
<input type="text" name="tool_concurrency" value="<%= @agent.limits[:tool_concurrency] %>" inputmode="numeric">
|
|
79
|
-
</label>
|
|
80
|
-
<label>chat_rate_limit <span class="muted">(turns/chat/window; blank = platform, 0 = off)</span>
|
|
81
|
-
<input type="text" name="chat_rate_limit" value="<%= @agent.limits[:chat_rate_limit] %>" inputmode="numeric" placeholder="inherit platform edge">
|
|
82
|
-
</label>
|
|
83
|
-
<label>agent_token_ceiling <span class="muted">(tokens/window; blank = platform, 0 = off)</span>
|
|
84
|
-
<input type="text" name="agent_token_ceiling" value="<%= @agent.limits[:agent_token_ceiling] %>" inputmode="numeric" placeholder="inherit platform edge">
|
|
85
|
-
</label>
|
|
1
|
+
<%# Agent detail — the DETAIL pane of the Agents miller shell.
|
|
2
|
+
Full page (direct visit/refresh): the two-column shell — master list plus
|
|
3
|
+
this pane. Turbo-Frame request (a row click, a config save, a prompt-file
|
|
4
|
+
switch): the pane alone, so the master's scroll state and selection
|
|
5
|
+
survive every interaction. The tab sections live in _agent_tab_* partials
|
|
6
|
+
(one per section, named after it — no abstraction, just containment). %>
|
|
7
|
+
<%# Roda renders with plain locals — no local_assigns hash (Rails-ism). %>
|
|
8
|
+
<% frame_only = defined?(frame_only) && frame_only %>
|
|
9
|
+
<% unless frame_only %>
|
|
10
|
+
<header class="page-head">
|
|
11
|
+
<div>
|
|
12
|
+
<nav class="crumbs"><a href="/studio/agents">agents</a> / <span><%= @agent.id %></span></nav>
|
|
13
|
+
<h1><span class="avatar sm <%= avatar_class(@agent.id) %>"><%= @agent.id[0, 2].upcase %></span><%= @agent.id %>
|
|
14
|
+
<span class="sub"><%= @agent.model %><%== " · #{h(@agent.provider)}" if @agent.provider %></span>
|
|
15
|
+
</h1>
|
|
86
16
|
</div>
|
|
87
|
-
<
|
|
88
|
-
|
|
89
|
-
cross-session memory (the agent remembers across conversations)
|
|
90
|
-
</label>
|
|
91
|
-
|
|
92
|
-
<fieldset class="subgroup">
|
|
93
|
-
<legend>generation params <span class="muted">— blank = provider default</span></legend>
|
|
94
|
-
<div class="grid-3">
|
|
95
|
-
<label>temperature
|
|
96
|
-
<input type="text" name="temperature" value="<%= agent_param(@params, :temperature) %>" inputmode="decimal" placeholder="0.7">
|
|
97
|
-
</label>
|
|
98
|
-
<label>max_tokens
|
|
99
|
-
<input type="text" name="max_tokens" value="<%= agent_param(@params, :max_tokens) %>" inputmode="numeric" placeholder="4096">
|
|
100
|
-
</label>
|
|
101
|
-
<label>reasoning <span class="muted">(off/on/effort — blank = inherit model/platform)</span>
|
|
102
|
-
<%== thinking_select("thinking", agent_param(@params, :thinking), "inherit") %>
|
|
103
|
-
</label>
|
|
104
|
-
</div>
|
|
105
|
-
</fieldset>
|
|
106
|
-
|
|
107
|
-
<label>model fence <span class="muted">(<code>model_policy.allow</code> — one ref per line: <code>provider/model</code>, <code>provider/*</code> or <code>model</code>; blank = no fence)</span>
|
|
108
|
-
<textarea name="model_policy_allow" rows="3" placeholder="deepseek/* openai/gpt-4o-mini"><%= @model_policy_allow %></textarea>
|
|
109
|
-
</label>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div class="config-group" data-config-group="guardrails"<%== " hidden" unless @config_group == "guardrails" %>>
|
|
113
|
-
<fieldset class="subgroup">
|
|
114
|
-
<legend>guardrails <span class="muted">— content safety</span></legend>
|
|
115
|
-
<label class="check">
|
|
116
|
-
<input type="checkbox" name="guardrail_input" value="1"<%== " checked" if guardrail_field(@guardrails, :input, true) %>>
|
|
117
|
-
input guardrail <span class="muted">— block injection / abuse / inappropriate before the LLM</span>
|
|
118
|
-
</label>
|
|
119
|
-
<label class="check">
|
|
120
|
-
<input type="checkbox" name="guardrail_output" value="1"<%== " checked" if guardrail_field(@guardrails, :output, true) %>>
|
|
121
|
-
output guardrail <span class="muted">— redact PII/secret in the stream + flag policy slips</span>
|
|
122
|
-
</label>
|
|
123
|
-
<div class="grid-2">
|
|
124
|
-
<label>strictness
|
|
125
|
-
<% strictness = guardrail_field(@guardrails, :strictness, "medium").to_s %>
|
|
126
|
-
<select name="guardrail_strictness">
|
|
127
|
-
<% %w[low medium high].each do |lvl| %>
|
|
128
|
-
<option value="<%= lvl %>"<%== " selected" if lvl == strictness %>><%= lvl %></option>
|
|
129
|
-
<% end %>
|
|
130
|
-
</select>
|
|
131
|
-
</label>
|
|
132
|
-
<label>LLM moderator <span class="muted">(model ref, or <code>on</code> for the platform utility_model; blank = deterministic only)</span>
|
|
133
|
-
<input type="text" name="guardrail_moderator" value="<%= guardrail_field(@guardrails, :moderator, "") %>" placeholder="deepseek/deepseek-v4-flash">
|
|
134
|
-
</label>
|
|
135
|
-
</div>
|
|
136
|
-
<details class="newfile">
|
|
137
|
-
<summary>safe replies — override per category <span class="muted">(blank = engine default; this agent's voice/language)</span></summary>
|
|
138
|
-
<% gr_resp = (@guardrails["responses"] || @guardrails[:responses] || {}) %>
|
|
139
|
-
<label>default <span class="muted">(catch-all for any blocked category)</span>
|
|
140
|
-
<textarea name="guardrail_response_default" rows="2" placeholder="I can't help with that particular request…"><%= guardrail_field(gr_resp, :default, "") %></textarea>
|
|
141
|
-
</label>
|
|
142
|
-
<div class="grid-2">
|
|
143
|
-
<% %w[injection sexual abuse escalate].each do |cat| %>
|
|
144
|
-
<label><%= cat %>
|
|
145
|
-
<textarea name="guardrail_response_<%= cat %>" rows="2"><%= guardrail_field(gr_resp, cat, "") %></textarea>
|
|
146
|
-
</label>
|
|
147
|
-
<% end %>
|
|
148
|
-
</div>
|
|
149
|
-
</details>
|
|
150
|
-
</fieldset>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<div class="config-group" data-config-group="grounding"<%== " hidden" unless @config_group == "grounding" %>>
|
|
154
|
-
<fieldset class="subgroup">
|
|
155
|
-
<legend>grounding <span class="muted">— product claims must point at evidence the session actually saw</span></legend>
|
|
156
|
-
<% gr = @agent.grounding || {} %>
|
|
157
|
-
<div class="grid-2">
|
|
158
|
-
<label>mode <span class="muted">(blank = off)</span>
|
|
159
|
-
<select name="grounding_mode">
|
|
160
|
-
<option value="">off</option>
|
|
161
|
-
<% %w[flag enforce].each do |m| %>
|
|
162
|
-
<option value="<%= m %>"<%== ' selected' if gr["mode"] == m %>><%= m %></option>
|
|
163
|
-
<% end %>
|
|
164
|
-
</select>
|
|
165
|
-
</label>
|
|
166
|
-
<label>matcher SKU <span class="muted">(evidence id prefix, e.g. <code>sku:</code>)</span>
|
|
167
|
-
<input type="text" name="grounding_matcher_sku" value="<%= gr.dig("matcher", "sku") %>" placeholder="sku:">
|
|
168
|
-
</label>
|
|
169
|
-
<label>name keys <span class="muted">(comma-separated metadata keys with product names)</span>
|
|
170
|
-
<input type="text" name="grounding_name_keys" value="<%= agent_list(gr.dig("matcher", "name_keys")) %>" placeholder="product, name">
|
|
171
|
-
</label>
|
|
172
|
-
</div>
|
|
173
|
-
</fieldset>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
<div class="config-group" data-config-group="funnel"<%== " hidden" unless @config_group == "funnel" %>>
|
|
177
|
-
<fieldset class="subgroup">
|
|
178
|
-
<legend>outcome funnel <span class="muted">— the stage vocabulary the engine folds WS7 outcomes into</span></legend>
|
|
179
|
-
<% fun = @agent.funnel || {} %>
|
|
180
|
-
<div class="grid-2">
|
|
181
|
-
<label>stages <span class="muted">(one per line, in order; blank = no funnel)</span>
|
|
182
|
-
<textarea name="funnel_stages" rows="4" placeholder="greeted qualified cart paid"><%= agent_lines(fun["stages"]) %></textarea>
|
|
183
|
-
</label>
|
|
184
|
-
<div class="grid-2">
|
|
185
|
-
<label>primary
|
|
186
|
-
<input type="text" name="funnel_primary" value="<%= fun["primary"] %>" placeholder="paid">
|
|
187
|
-
</label>
|
|
188
|
-
<label>attribution window <span class="muted">(carried, never computed)</span>
|
|
189
|
-
<input type="text" name="funnel_attribution_window" value="<%= fun["attribution_window"] %>" placeholder="72h">
|
|
190
|
-
</label>
|
|
191
|
-
<label class="full">
|
|
192
|
-
advance_on <span class="muted">— JSON: outcome kind → reached stage</span>
|
|
193
|
-
<textarea name="funnel_advance_on" rows="4" spellcheck="false" class="mono" placeholder='{ "pix_paid": "paid" }'><%= agent_json(fun["advance_on"]) %></textarea>
|
|
194
|
-
</label>
|
|
195
|
-
</div>
|
|
196
|
-
</div>
|
|
197
|
-
</fieldset>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div class="config-group" data-config-group="followups"<%== " hidden" unless @config_group == "followups" %>>
|
|
201
|
-
<fieldset class="subgroup">
|
|
202
|
-
<legend>follow-ups <span class="muted">— the seller who comes back (blank arm = off)</span></legend>
|
|
203
|
-
<% fu = @agent.followup || {}; fup = fu["policy"] || {}; fqh = fup["quiet_hours"] || {} %>
|
|
204
|
-
<div class="grid-2">
|
|
205
|
-
<label>arm
|
|
206
|
-
<input type="text" name="followup_arm" value="<%= fu["arm"] %>" placeholder="schedule">
|
|
207
|
-
</label>
|
|
208
|
-
<label>max frequency <span class="muted">(e.g. <code>2/24h</code>)</span>
|
|
209
|
-
<input type="text" name="followup_max_frequency" value="<%= fup["max_frequency"] %>" placeholder="2/24h">
|
|
210
|
-
</label>
|
|
211
|
-
<label>quiet hours timezone <span class="muted">(blank = no quiet hours)</span>
|
|
212
|
-
<input type="text" name="followup_tz" value="<%= fqh["timezone"] %>" placeholder="America/Sao_Paulo">
|
|
213
|
-
</label>
|
|
214
|
-
<div class="grid-2">
|
|
215
|
-
<label>quiet start
|
|
216
|
-
<input type="text" name="followup_quiet_start" value="<%= fqh["start"] %>" placeholder="21:30">
|
|
217
|
-
</label>
|
|
218
|
-
<label>quiet end
|
|
219
|
-
<input type="text" name="followup_quiet_end" value="<%= fqh["end"] %>" placeholder="09:00">
|
|
220
|
-
</label>
|
|
221
|
-
</div>
|
|
222
|
-
<label>cancel keywords <span class="muted">(comma-separated)</span>
|
|
223
|
-
<input type="text" name="followup_cancel_keywords" value="<%= agent_list(fup["cancel_keywords"]) %>" placeholder="não quero mais contato">
|
|
224
|
-
</label>
|
|
225
|
-
<label>silence after N sends
|
|
226
|
-
<input type="text" name="followup_silence_after_sends" value="<%= fup["silence_after_sends"] %>" placeholder="3" inputmode="numeric">
|
|
227
|
-
</label>
|
|
228
|
-
</div>
|
|
229
|
-
</fieldset>
|
|
230
|
-
</div>
|
|
231
|
-
|
|
232
|
-
<div class="config-group" data-config-group="distill"<%== " hidden" unless @config_group == "distill" %>>
|
|
233
|
-
<fieldset class="subgroup">
|
|
234
|
-
<legend>distillation <span class="muted">— finished conversations → proposed customer facts (Facts page)</span></legend>
|
|
235
|
-
<% dist = @agent.distill || {} %>
|
|
236
|
-
<label class="check">
|
|
237
|
-
<input type="checkbox" name="distill_enabled" value="1"<%== " checked" if Insika::Coercion.truthy?(dist["enabled"]) %>>
|
|
238
|
-
distillation on — idle sessions are read back as fact proposals; a human always approves
|
|
239
|
-
</label>
|
|
240
|
-
<div class="grid-3">
|
|
241
|
-
<label>idle hours <span class="muted">(blank = 6)</span>
|
|
242
|
-
<input type="text" name="distill_idle_hours" value="<%= dist["idle_hours"] %>" placeholder="6" inputmode="numeric">
|
|
243
|
-
</label>
|
|
244
|
-
<label>min messages <span class="muted">(blank = 3)</span>
|
|
245
|
-
<input type="text" name="distill_min_messages" value="<%= dist["min_messages"] %>" placeholder="3" inputmode="numeric">
|
|
246
|
-
</label>
|
|
247
|
-
<label>max proposals <span class="muted">(blank = 10)</span>
|
|
248
|
-
<input type="text" name="distill_max_proposals" value="<%= dist["max_proposals"] %>" placeholder="10" inputmode="numeric">
|
|
249
|
-
</label>
|
|
250
|
-
</div>
|
|
251
|
-
<label>model <span class="muted">(blank = the platform utility_model)</span>
|
|
252
|
-
<input type="text" name="distill_model" value="<%= dist["model"] %>" placeholder="deepseek-v4-flash">
|
|
253
|
-
</label>
|
|
254
|
-
<label>prompt <span class="muted">— what counts as a fact for THIS store (blank = the engine default)</span>
|
|
255
|
-
<textarea name="distill_prompt" rows="3" placeholder="what counts as a customer fact for this store"><%= dist["prompt"] %></textarea>
|
|
256
|
-
</label>
|
|
257
|
-
</fieldset>
|
|
258
|
-
</div>
|
|
259
|
-
|
|
260
|
-
<div class="config-group" data-config-group="harvest"<%== " hidden" unless @config_group == "harvest" %>>
|
|
261
|
-
<fieldset class="subgroup">
|
|
262
|
-
<legend>harvest <span class="muted">— finished traffic → proposed SKILLs, double-gated, human-promoted (Harvest page)</span></legend>
|
|
263
|
-
<% hv = @agent.harvest || {}; hvm = hv["miner"] || {} %>
|
|
264
|
-
<label class="check">
|
|
265
|
-
<input type="checkbox" name="harvest_enabled" value="1"<%== " checked" if Insika::Coercion.truthy?(hv["enabled"]) %>>
|
|
266
|
-
harvest on — idle conversations are mined for skills; a human promotes every one
|
|
267
|
-
</label>
|
|
268
|
-
<div class="grid-3">
|
|
269
|
-
<label>idle hours <span class="muted">(blank = 24)</span>
|
|
270
|
-
<input type="text" name="harvest_idle_hours" value="<%= hv["idle_hours"] %>" placeholder="24" inputmode="numeric">
|
|
271
|
-
</label>
|
|
272
|
-
<label>min messages <span class="muted">(blank = 3)</span>
|
|
273
|
-
<input type="text" name="harvest_min_messages" value="<%= hv["min_messages"] %>" placeholder="3" inputmode="numeric">
|
|
274
|
-
</label>
|
|
275
|
-
<label>miner model <span class="muted">(blank = the platform utility_model)</span>
|
|
276
|
-
<input type="text" name="harvest_miner_model" value="<%= hvm["model"] %>" placeholder="deepseek-v4-flash">
|
|
277
|
-
</label>
|
|
278
|
-
<label>miner window <span class="muted">(last N conversations)</span>
|
|
279
|
-
<input type="text" name="harvest_miner_window" value="<%= hvm.dig("window", "last_sessions") %>" placeholder="200" inputmode="numeric">
|
|
280
|
-
</label>
|
|
281
|
-
<label>miner max proposals
|
|
282
|
-
<input type="text" name="harvest_miner_max_proposals" value="<%= hvm["max_proposals"] %>" placeholder="10" inputmode="numeric">
|
|
283
|
-
</label>
|
|
284
|
-
<label>miner budget (tokens)
|
|
285
|
-
<input type="text" name="harvest_miner_budget_tokens" value="<%= hvm.dig("budget", "tokens") %>" placeholder="20000" inputmode="numeric">
|
|
286
|
-
</label>
|
|
287
|
-
</div>
|
|
288
|
-
<label>negative list <span class="muted">— JSON: <code>[{ "rule": "…", "pattern": "…" }]</code> (blank = the deployment seed)</span>
|
|
289
|
-
<textarea name="harvest_negative_list" rows="3" spellcheck="false" class="mono" placeholder='[ { "rule": "no-competitor-prices", "pattern": "concorrente" } ]'><%= agent_json(hv["negative_list"]) %></textarea>
|
|
290
|
-
</label>
|
|
291
|
-
<label>prompt <span class="muted">— what a harvestable skill is for THIS store (blank = the engine default)</span>
|
|
292
|
-
<textarea name="harvest_prompt" rows="3" placeholder="what makes a skill harvestable for this store"><%= hv["prompt"] %></textarea>
|
|
293
|
-
</label>
|
|
294
|
-
</fieldset>
|
|
295
|
-
</div>
|
|
296
|
-
|
|
297
|
-
<div class="config-group" data-config-group="refinement"<%== " hidden" unless @config_group == "refinement" %>>
|
|
298
|
-
<fieldset class="subgroup">
|
|
299
|
-
<legend>refinement <span class="muted">— read traffic back as a ranked report / proposed edits (Refinement page)</span></legend>
|
|
300
|
-
<% rf = @agent.refinement || {}; rfw = rf["window"] || {}; rfb = rf["budget"] || {} %>
|
|
301
|
-
<div class="grid-2">
|
|
302
|
-
<label>mode <span class="muted">(blank = report-only, nothing ever writes)</span>
|
|
303
|
-
<select name="refinement_mode">
|
|
304
|
-
<option value="">report-only</option>
|
|
305
|
-
<% %w[report propose auto_apply].each do |m| %>
|
|
306
|
-
<option value="<%= m %>"<%== ' selected' if rf["mode"] == m %>><%= m %></option>
|
|
307
|
-
<% end %>
|
|
308
|
-
</select>
|
|
309
|
-
</label>
|
|
310
|
-
<label>window <span class="muted">(last N conversations; blank = 200)</span>
|
|
311
|
-
<input type="text" name="refinement_window" value="<%= rfw["last_sessions"] %>" placeholder="200" inputmode="numeric">
|
|
312
|
-
</label>
|
|
313
|
-
<label>max findings
|
|
314
|
-
<input type="text" name="refinement_max_findings" value="<%= rf["max_findings"] %>" placeholder="20" inputmode="numeric">
|
|
315
|
-
</label>
|
|
316
|
-
<label>auto_apply max edits <span class="muted">(propose+ mode)</span>
|
|
317
|
-
<input type="text" name="refinement_auto_apply_max_edits" value="<%= rf["auto_apply_max_edits"] %>" inputmode="numeric">
|
|
318
|
-
</label>
|
|
319
|
-
<label>files <span class="muted">(allowlist, comma-separated)</span>
|
|
320
|
-
<input type="text" name="refinement_files" value="<%= agent_list(rf["files"]) %>" placeholder="IDENTITY.md">
|
|
321
|
-
</label>
|
|
322
|
-
<label>proposers <span class="muted">(model refs, comma-separated)</span>
|
|
323
|
-
<input type="text" name="refinement_proposers" value="<%= agent_list(rf["proposers"]) %>" placeholder="deepseek/deepseek-v4-flash">
|
|
324
|
-
</label>
|
|
325
|
-
</div>
|
|
326
|
-
<label>budget (tokens)
|
|
327
|
-
<input type="text" name="refinement_budget_tokens" value="<%= rfb["tokens"] %>" inputmode="numeric">
|
|
328
|
-
</label>
|
|
329
|
-
</fieldset>
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
<div class="config-group" data-config-group="budget_rel"<%== " hidden" unless @config_group == "budget_rel" %>>
|
|
333
|
-
<fieldset class="subgroup">
|
|
334
|
-
<legend>budget <span class="muted">— WS2: calendar spend caps per (tenant, agent)</span></legend>
|
|
335
|
-
<% bd = @agent.budget || {} %>
|
|
336
|
-
<div class="grid-3">
|
|
337
|
-
<label>daily (tokens)
|
|
338
|
-
<input type="text" name="budget_daily" value="<%= bd["daily"] %>" inputmode="numeric">
|
|
339
|
-
</label>
|
|
340
|
-
<label>monthly (tokens)
|
|
341
|
-
<input type="text" name="budget_monthly" value="<%= bd["monthly"] %>" inputmode="numeric">
|
|
342
|
-
</label>
|
|
343
|
-
<label>alert at <span class="muted">(0–1 of the cap)</span>
|
|
344
|
-
<input type="text" name="budget_alert_at" value="<%= bd["alert_at"] %>" placeholder="0.8" inputmode="decimal">
|
|
345
|
-
</label>
|
|
346
|
-
</div>
|
|
347
|
-
<label class="check">
|
|
348
|
-
<input type="checkbox" name="budget_soft" value="1"<%== " checked" if bd["soft"] %>>
|
|
349
|
-
soft cap <span class="muted">— crossing it warns and runs; hard (default) fails the turn</span>
|
|
350
|
-
</label>
|
|
351
|
-
</fieldset>
|
|
352
|
-
|
|
353
|
-
<fieldset class="subgroup">
|
|
354
|
-
<legend>reliability <span class="muted">— WS3: retries, fallback chain, circuit breaker</span></legend>
|
|
355
|
-
<% rel = @agent.reliability || {}; brk = rel["circuit_breaker"] || {} %>
|
|
356
|
-
<div class="grid-3">
|
|
357
|
-
<label>retries
|
|
358
|
-
<input type="text" name="reliability_retries" value="<%= rel["retries"] %>" inputmode="numeric">
|
|
359
|
-
</label>
|
|
360
|
-
<label>backoff
|
|
361
|
-
<select name="reliability_backoff">
|
|
362
|
-
<option value="">none</option>
|
|
363
|
-
<% %w[constant exponential].each do |m| %>
|
|
364
|
-
<option value="<%= m %>"<%== ' selected' if rel["backoff"] == m %>><%= m %></option>
|
|
365
|
-
<% end %>
|
|
366
|
-
</select>
|
|
367
|
-
</label>
|
|
368
|
-
<label>timeout (s)
|
|
369
|
-
<input type="text" name="reliability_timeout" value="<%= rel["timeout"] %>" placeholder="30" inputmode="numeric">
|
|
370
|
-
</label>
|
|
371
|
-
<label>breaker after <span class="muted">(failures)</span>
|
|
372
|
-
<input type="text" name="reliability_breaker_after" value="<%= brk["after"] %>" inputmode="numeric">
|
|
373
|
-
</label>
|
|
374
|
-
<label>breaker within (s)
|
|
375
|
-
<input type="text" name="reliability_breaker_within" value="<%= brk["within"] %>" inputmode="numeric">
|
|
376
|
-
</label>
|
|
377
|
-
<label>breaker cooldown (s)
|
|
378
|
-
<input type="text" name="reliability_breaker_cooldown" value="<%= brk["cooldown"] %>" inputmode="numeric">
|
|
379
|
-
</label>
|
|
380
|
-
</div>
|
|
381
|
-
<label>fallback models <span class="muted">(comma-separated refs, tried after retries)</span>
|
|
382
|
-
<input type="text" name="reliability_fallback" value="<%= agent_list(rel["fallback"]) %>" placeholder="openai/gpt-4o-mini">
|
|
383
|
-
</label>
|
|
384
|
-
</fieldset>
|
|
385
|
-
</div>
|
|
386
|
-
|
|
387
|
-
<div class="config-group" data-config-group="routing"<%== " hidden" unless @config_group == "routing" %>>
|
|
388
|
-
<fieldset class="subgroup">
|
|
389
|
-
<legend>routing, media, alerts & the edge</legend>
|
|
390
|
-
<label>routes <span class="muted">— WS4 JSON: intent → description / delegate / stuck (blank = no routing)</span>
|
|
391
|
-
<textarea name="routes" rows="4" spellcheck="false" class="mono" placeholder='{ "shopping": "the customer wants to browse", "default": "shopping" }'><%= agent_json(@agent.routes) %></textarea>
|
|
392
|
-
</label>
|
|
393
|
-
<label>outputs <span class="muted">— WS9 JSON: generated media the agent may produce (blank = none)</span>
|
|
394
|
-
<textarea name="outputs" rows="3" spellcheck="false" class="mono" placeholder='{ "image": { "model": "gpt-image-1" } }'><%= agent_json(@agent.outputs) %></textarea>
|
|
395
|
-
</label>
|
|
396
|
-
<div class="grid-2">
|
|
397
|
-
<label>alerts webhook <span class="muted">(budget_warning / breaker_open / delivery_failed)</span>
|
|
398
|
-
<input type="text" name="alerts_webhook" value="<%= @agent.alerts && @agent.alerts["webhook"] %>" placeholder="https://ops.example.com/insika-alerts">
|
|
399
|
-
</label>
|
|
400
|
-
<label class="check">
|
|
401
|
-
<input type="checkbox" name="stuck_signal" value="1"<%== " checked" if @agent.stuck_signal %>>
|
|
402
|
-
stuck signal <span class="muted">— the model may end a turn <code>stuck</code> (WS5)</span>
|
|
403
|
-
</label>
|
|
404
|
-
</div>
|
|
405
|
-
<div class="grid-2">
|
|
406
|
-
<label class="check">
|
|
407
|
-
<input type="checkbox" name="edge_thinking" value="1"<%== " checked" if @agent.stream_public?(:thinking) %>>
|
|
408
|
-
stream reasoning to the customer
|
|
409
|
-
</label>
|
|
410
|
-
<label class="check">
|
|
411
|
-
<input type="checkbox" name="edge_intermediate" value="1"<%== " checked" if @agent.stream_public?(:intermediate) %>>
|
|
412
|
-
stream tool-loop narration to the customer
|
|
413
|
-
</label>
|
|
414
|
-
</div>
|
|
415
|
-
<p class="muted">both off = the answer only; the Studio and the trace always keep the rest.</p>
|
|
416
|
-
</fieldset>
|
|
417
|
-
</div>
|
|
418
|
-
|
|
419
|
-
<div class="config-group" data-config-group="advanced"<%== " hidden" unless @config_group == "advanced" %>>
|
|
420
|
-
<fieldset class="subgroup">
|
|
421
|
-
<legend>capabilities <span class="muted">— comma-separated; blank = none (opt-in)</span></legend>
|
|
422
|
-
<div class="grid-2">
|
|
423
|
-
<label>subagents
|
|
424
|
-
<input type="text" name="subagents" value="<%= agent_list(@agent.subagents) %>" placeholder="order-agent, security">
|
|
425
|
-
</label>
|
|
426
|
-
<label>capabilities
|
|
427
|
-
<input type="text" name="capabilities" value="<%= agent_list(@agent.capabilities) %>">
|
|
428
|
-
</label>
|
|
429
|
-
<label>tools_deferred <span class="muted">(searchable-not-wired tools)</span>
|
|
430
|
-
<input type="text" name="tools_deferred" value="<%= agent_list(@agent.tools_deferred) %>">
|
|
431
|
-
</label>
|
|
432
|
-
<label>briefing_fields <span class="muted">(per-session state the agent keeps)</span>
|
|
433
|
-
<input type="text" name="briefing_fields" value="<%= agent_list(@agent.briefing_fields) %>" placeholder="customer_name, wishlist">
|
|
434
|
-
</label>
|
|
435
|
-
</div>
|
|
436
|
-
</fieldset>
|
|
437
|
-
|
|
438
|
-
<fieldset class="subgroup">
|
|
439
|
-
<legend>allowlists <span class="muted">— comma-separated; blank = all (open) or none, per field</span></legend>
|
|
440
|
-
<div class="grid-2">
|
|
441
|
-
<label>context providers <span class="muted">(blank = all)</span>
|
|
442
|
-
<input type="text" name="context_providers" value="<%= agent_list(@agent.context_providers) %>">
|
|
443
|
-
</label>
|
|
444
|
-
<label>workflows <span class="muted">(blank = all)</span>
|
|
445
|
-
<input type="text" name="workflows_allow" value="<%= agent_list(@agent.workflows_allow) %>">
|
|
446
|
-
</label>
|
|
447
|
-
<label>approvals required <span class="muted">(tools whose call suspends for a human)</span>
|
|
448
|
-
<input type="text" name="approvals_required" value="<%= agent_list(@agent.approvals_required) %>" placeholder="send_discount_coupon">
|
|
449
|
-
</label>
|
|
450
|
-
<label>policies <span class="muted">(registry names)</span>
|
|
451
|
-
<input type="text" name="policies" value="<%= agent_list(@agent.policies) %>" placeholder="tool_allowlist, skill_allowlist">
|
|
452
|
-
</label>
|
|
453
|
-
<label>prompt refs <span class="muted">(catalog names)</span>
|
|
454
|
-
<input type="text" name="prompt_refs" value="<%= agent_list(@agent.prompt_refs) %>">
|
|
455
|
-
</label>
|
|
456
|
-
<label>declares <span class="muted">(facts about this deployment, read by evals)</span>
|
|
457
|
-
<input type="text" name="capabilities_declared" value="<%= agent_list(@agent.capabilities_declared) %>" placeholder="promotions, human_handoff">
|
|
458
|
-
</label>
|
|
459
|
-
</div>
|
|
460
|
-
</fieldset>
|
|
461
|
-
|
|
462
|
-
<fieldset class="subgroup">
|
|
463
|
-
<legend>behaviour switches</legend>
|
|
464
|
-
<label>skills_eager <span class="muted">(blank = progressive loading; <code>all</code> = every allowed skill each turn; or a comma list)</span>
|
|
465
|
-
<input type="text" name="skills_eager" value="<%= @agent.skills_eager == true ? "all" : agent_list(@agent.skills_eager) %>" placeholder="all">
|
|
466
|
-
</label>
|
|
467
|
-
<label class="check">
|
|
468
|
-
<input type="checkbox" name="prompt_caching" value="1"<%== " checked" if @agent.prompt_caching %>>
|
|
469
|
-
prompt caching <span class="muted">— Anthropic breakpoint; ON requires a byte-stable system prompt</span>
|
|
470
|
-
</label>
|
|
471
|
-
<label class="check">
|
|
472
|
-
<input type="checkbox" name="tool_output_compression" value="1"<%== " checked" if @agent.tool_output_compression %>>
|
|
473
|
-
tool output compression <span class="muted">— mechanical dedupe of repeated tool results</span>
|
|
474
|
-
</label>
|
|
475
|
-
<label class="check">
|
|
476
|
-
<input type="checkbox" name="tool_persistence" value="1"<%== " checked" unless @agent.tool_persistence == false %>>
|
|
477
|
-
tool persistence <span class="muted">— the engine's "Tool discipline" prompt block (retry weak/empty tool results differently); ON by default</span>
|
|
478
|
-
</label>
|
|
479
|
-
</fieldset>
|
|
480
|
-
|
|
481
|
-
<fieldset class="subgroup">
|
|
482
|
-
<legend>metadata <span class="muted">— free-form JSON, stable per agent (home of <code>store_id</code>)</span></legend>
|
|
483
|
-
<textarea name="metadata" rows="3" spellcheck="false" class="mono" placeholder='{ "store_id": "acme" }'><%= agent_json(@agent.metadata) %></textarea>
|
|
484
|
-
</fieldset>
|
|
485
|
-
</div>
|
|
486
|
-
</form>
|
|
487
|
-
</div>
|
|
488
|
-
</section>
|
|
489
|
-
</div>
|
|
490
|
-
</section>
|
|
491
|
-
|
|
492
|
-
<!-- ── Prompts / identity (drill: file list | editor, like skills) ────────── -->
|
|
493
|
-
<section class="section" id="prompts" data-tabs-target="panel">
|
|
494
|
-
<div class="section-head">
|
|
495
|
-
<h2>Prompts <span class="pill info plain"><%= @prompt_files.size %></span></h2>
|
|
496
|
-
<p class="muted">the agent's own identity (store-backed, versioned). Empty = uses the deployment default.</p>
|
|
497
|
-
</div>
|
|
17
|
+
<a class="btn primary" href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>">Open playground →</a>
|
|
18
|
+
</header>
|
|
498
19
|
|
|
499
20
|
<div class="drill" data-controller="list-filter">
|
|
500
|
-
|
|
21
|
+
<%== render("_agents_master", locals: { active_id: @agent.id }) %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<turbo-frame id="agent-detail" class="drill-pane drill-detail">
|
|
24
|
+
<%# A frame response never sees the layout, so the flash strip lives here
|
|
25
|
+
too — without it, save confirmations would vanish on frame submits
|
|
26
|
+
(the layout's copy only renders on full pages). %>
|
|
27
|
+
<% if frame_only %>
|
|
28
|
+
<% if flash["error"] %><div class="flash err" role="alert"><%= flash["error"] %></div><% end %>
|
|
29
|
+
<% if flash["notice"] %><div class="flash ok" role="status"><%= flash["notice"] %></div><% end %>
|
|
30
|
+
<% end %>
|
|
501
31
|
<div class="drill-pane-head">
|
|
502
|
-
<div class="title"
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
<div class="drill-search">
|
|
506
|
-
<input type="search" class="filter-input" placeholder="Search prompts…" aria-label="Filter prompts"
|
|
507
|
-
data-list-filter-target="query" data-action="input->list-filter#filter keydown->list-filter#clear">
|
|
508
|
-
</div>
|
|
509
|
-
<nav class="drill-pane-body drill-list" aria-label="prompt files">
|
|
510
|
-
<% if @prompt_files.empty? %>
|
|
511
|
-
<div class="empty">No prompt files yet — create the first one.</div>
|
|
512
|
-
<% else %>
|
|
513
|
-
<% @prompt_files.each do |pf| %>
|
|
514
|
-
<a class="drill-item<%== ' active' if @prompt_selected == pf[:name] %>"
|
|
515
|
-
href="<%= agent_path(@agent.id) %>/prompts/<%= Rack::Utils.escape(pf[:name]) %>#prompts"
|
|
516
|
-
data-list-filter-target="item" data-filter-text="<%= pf[:name] %>">
|
|
517
|
-
<div class="drill-item-name"><span class="txt"><%= pf[:name] %></span></div>
|
|
518
|
-
<div class="drill-item-sub"><%= pf[:versions].size %> version<%= 's' unless pf[:versions].size == 1 %></div>
|
|
519
|
-
</a>
|
|
520
|
-
<% end %>
|
|
521
|
-
<div class="empty" data-list-filter-target="empty" hidden>No prompts match your filter.</div>
|
|
522
|
-
<% end %>
|
|
523
|
-
</nav>
|
|
524
|
-
</aside>
|
|
525
|
-
|
|
526
|
-
<section class="drill-pane drill-detail">
|
|
527
|
-
<% if @prompt_selected.nil? %>
|
|
528
|
-
<div class="drill-empty">
|
|
529
|
-
<div>
|
|
530
|
-
<h2>Prompts</h2>
|
|
531
|
-
<p>Pick a prompt file on the left to edit it — or <a href="<%= agent_path(@agent.id) %>/prompts/new#prompts">create a new one</a>.</p>
|
|
532
|
-
</div>
|
|
533
|
-
</div>
|
|
534
|
-
<% else %>
|
|
535
|
-
<% new = @prompt_selected == "" %>
|
|
536
|
-
<% pf = new ? nil : @prompt_files.find { |f| f[:name] == @prompt_selected } %>
|
|
537
|
-
<div class="drill-pane-head">
|
|
538
|
-
<div class="title">
|
|
539
|
-
<strong class="mono"><%= new ? "New prompt file" : @prompt_selected %></strong>
|
|
540
|
-
</div>
|
|
541
|
-
<button type="submit" form="prompt-form" class="btn primary" data-turbo-submits-with="Saving…"><%= new ? "Create prompt" : "Save prompt" %></button>
|
|
32
|
+
<div class="title">
|
|
33
|
+
<strong class="mono"><%= @agent.id %></strong>
|
|
34
|
+
<span class="count"><%= @agent.model %><%== " · #{h(@agent.provider)}" if @agent.provider %></span>
|
|
542
35
|
</div>
|
|
543
|
-
<
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
<
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
</
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
<%== csrf_tag %>
|
|
572
|
-
<input type="hidden" name="file" value="<%= pf[:name] %>">
|
|
573
|
-
<input type="hidden" name="version" value="<%= i %>">
|
|
574
|
-
<button type="submit" class="ghost">Restore</button>
|
|
575
|
-
</form>
|
|
576
|
-
</li>
|
|
577
|
-
<% end %>
|
|
578
|
-
</ul>
|
|
579
|
-
</div>
|
|
580
|
-
<% end %>
|
|
581
|
-
|
|
582
|
-
<div class="drill-section">
|
|
583
|
-
<div class="drill-section-head">
|
|
584
|
-
<h3>Remove</h3>
|
|
585
|
-
<span class="muted">deletes the file and drops it from <code>prompt_files</code> — the agent falls back to the deployment default.</span>
|
|
586
|
-
</div>
|
|
587
|
-
<form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/prompts/delete" data-turbo-confirm="Remove prompt file "<%= pf[:name] %>"? This can't be undone.">
|
|
588
|
-
<%== csrf_tag %>
|
|
589
|
-
<input type="hidden" name="file" value="<%= pf[:name] %>">
|
|
590
|
-
<div class="actions"><button type="submit" class="danger" data-turbo-submits-with="Removing…">Remove this file</button></div>
|
|
591
|
-
</form>
|
|
592
|
-
</div>
|
|
593
|
-
<% end %>
|
|
36
|
+
<a class="btn btn-sm" href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>" data-turbo-frame="_top">Test in playground</a>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="drill-pane-body">
|
|
39
|
+
<div class="tabs-page" data-controller="tabs" data-tabs-default-value="<%= @active_tab %>">
|
|
40
|
+
<nav class="subnav" aria-label="agent sections">
|
|
41
|
+
<%# data-turbo="false": these are plain in-page hash links. Without
|
|
42
|
+
the opt-out, Turbo intercepts them as frame navigations (they sit
|
|
43
|
+
inside turbo-frame#agent-detail), dropping the #fragment and
|
|
44
|
+
re-fetching the frame instead of letting the browser set
|
|
45
|
+
location.hash — which is the only thing tabs_controller reads. %>
|
|
46
|
+
<a href="#config" data-tabs-target="tab" data-turbo="false">config</a>
|
|
47
|
+
<a href="#prompts" data-tabs-target="tab" data-turbo="false">prompts</a>
|
|
48
|
+
<a href="#skills" data-tabs-target="tab" data-turbo="false">skills</a>
|
|
49
|
+
<a href="#memory" data-tabs-target="tab" data-turbo="false">memory</a>
|
|
50
|
+
<a href="#outcomes" data-tabs-target="tab" data-turbo="false">outcomes</a>
|
|
51
|
+
<a href="#loops" data-tabs-target="tab" data-turbo="false">loops</a>
|
|
52
|
+
<a href="#cache" data-tabs-target="tab" data-turbo="false">cache</a>
|
|
53
|
+
<a href="#history" data-tabs-target="tab" data-turbo="false">history</a>
|
|
54
|
+
</nav>
|
|
55
|
+
|
|
56
|
+
<%== render("_agent_tab_config") %>
|
|
57
|
+
<%== render("_agent_tab_prompts") %>
|
|
58
|
+
<%== render("_agent_tab_skills") %>
|
|
59
|
+
<%== render("_agent_tab_memory") %>
|
|
60
|
+
<%== render("_agent_tab_loops") %>
|
|
61
|
+
<%== render("_agent_tab_outcomes") %>
|
|
62
|
+
<%== render("_agent_tab_cache") %>
|
|
63
|
+
<%== render("_agent_tab_history") %>
|
|
594
64
|
</div>
|
|
595
|
-
<% end %>
|
|
596
|
-
</section>
|
|
597
|
-
</div>
|
|
598
|
-
</section>
|
|
599
|
-
|
|
600
|
-
<!-- ── Agent skills ──────────────────────────────────────────────────────── -->
|
|
601
|
-
<section class="section" id="skills" data-tabs-target="panel">
|
|
602
|
-
<div class="section-head">
|
|
603
|
-
<h2>Skills</h2>
|
|
604
|
-
<p class="muted">which catalog skills this agent may load (<code>load_skill</code>).</p>
|
|
605
|
-
</div>
|
|
606
|
-
<form class="card" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/skills" data-controller="toggle-counter">
|
|
607
|
-
<%== csrf_tag %>
|
|
608
|
-
<div class="matrix-topline">
|
|
609
|
-
<% if @all_skills.empty? %>
|
|
610
|
-
<span class="muted">No skills in the catalog yet — create one under <a href="/studio/skills/new">skills → new</a>.</span>
|
|
611
|
-
<% else %>
|
|
612
|
-
<span class="count-badge mono" data-toggle-counter-target="count"><%= @agent_skills.nil? ? "all" : "#{@agent_skills.size}/#{@all_skills.size} on" %></span>
|
|
613
|
-
<label class="switch" title="open allowlist — every catalog skill flows through">
|
|
614
|
-
<input type="checkbox" name="all_skills" value="1"<%== " checked" if @agent_skills.nil? %>
|
|
615
|
-
data-toggle-counter-target="all" data-action="change->toggle-counter#update">
|
|
616
|
-
<span class="switch-track" aria-hidden="true"></span>
|
|
617
|
-
<span class="switch-label">all skills</span>
|
|
618
|
-
</label>
|
|
619
|
-
<% end %>
|
|
620
|
-
</div>
|
|
621
|
-
<% unless @all_skills.empty? %>
|
|
622
|
-
<div class="check-grid tool-grid" data-toggle-counter-target="grid">
|
|
623
|
-
<% @all_skills.sort_by(&:name).each do |s| %>
|
|
624
|
-
<label class="check tool-check">
|
|
625
|
-
<input type="checkbox" name="skills[]" value="<%= s.name %>"<%== " checked" if @agent_skills&.include?(s.name) %>
|
|
626
|
-
data-toggle-counter-target="tool" data-locked="false" data-action="change->toggle-counter#update">
|
|
627
|
-
<span class="mono"><%= s.name %></span>
|
|
628
|
-
</label>
|
|
629
|
-
<% end %>
|
|
630
65
|
</div>
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
<button type="submit" class="primary" data-turbo-submits-with="Saving…">Save skills</button>
|
|
634
|
-
<a class="btn ghost" href="/studio/tools">Edit tools →</a>
|
|
635
|
-
</div>
|
|
636
|
-
</form>
|
|
637
|
-
</section>
|
|
638
|
-
|
|
639
|
-
<!-- ── Agent memory (tenant = id) ────────────────────────────────────────── -->
|
|
640
|
-
<section class="section" id="memory" data-tabs-target="panel">
|
|
641
|
-
<div class="section-head">
|
|
642
|
-
<h2>Memory</h2>
|
|
643
|
-
<p class="muted">facts and notes the agent reads each turn (tenant <code><%= @agent.id %></code>).</p>
|
|
644
|
-
</div>
|
|
645
|
-
<div class="card">
|
|
646
|
-
<h3>Facts</h3>
|
|
647
|
-
<% if @facts.empty? %>
|
|
648
|
-
<p class="muted">No facts.</p>
|
|
649
|
-
<% else %>
|
|
650
|
-
<ul class="kv">
|
|
651
|
-
<% @facts.each do |f| %>
|
|
652
|
-
<li>
|
|
653
|
-
<span class="k mono"><%= f.key %></span>
|
|
654
|
-
<span class="v"><%= f.value %></span>
|
|
655
|
-
<form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/forget" data-turbo-confirm="Forget "<%= f.key %>"? The agent loses this fact from the next turn on.">
|
|
656
|
-
<%== csrf_tag %>
|
|
657
|
-
<input type="hidden" name="key" value="<%= f.key %>">
|
|
658
|
-
<button type="submit" class="ghost danger" data-turbo-submits-with="Forgetting…">Forget</button>
|
|
659
|
-
</form>
|
|
660
|
-
</li>
|
|
661
|
-
<% end %>
|
|
662
|
-
</ul>
|
|
663
|
-
<% end %>
|
|
664
|
-
<form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/fact">
|
|
665
|
-
<%== csrf_tag %>
|
|
666
|
-
<input type="text" name="key" placeholder="key (e.g. customer_name)" required>
|
|
667
|
-
<input type="text" name="value" placeholder="value" required>
|
|
668
|
-
<button type="submit" class="primary" data-turbo-submits-with="Saving…">Save fact</button>
|
|
669
|
-
</form>
|
|
670
|
-
</div>
|
|
671
|
-
|
|
672
|
-
<div class="card">
|
|
673
|
-
<h3>Notes <span class="muted">(recent)</span></h3>
|
|
674
|
-
<% if @notes.empty? %>
|
|
675
|
-
<p class="muted">No notes.</p>
|
|
676
|
-
<% else %>
|
|
677
|
-
<ul class="notes">
|
|
678
|
-
<% @notes.each do |n| %>
|
|
679
|
-
<li><span class="muted mono"><%= n.created_at %></span> <%= n.text %></li>
|
|
680
|
-
<% end %>
|
|
681
|
-
</ul>
|
|
682
|
-
<% end %>
|
|
683
|
-
<form class="inline-form" method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/memory/note">
|
|
684
|
-
<%== csrf_tag %>
|
|
685
|
-
<input type="text" name="text" placeholder="free-form note…" required>
|
|
686
|
-
<button type="submit" class="primary" data-turbo-submits-with="Adding…">Add note</button>
|
|
687
|
-
</form>
|
|
66
|
+
</turbo-frame>
|
|
67
|
+
<% unless frame_only %>
|
|
688
68
|
</div>
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
<!-- ── Automatic loops ─────────────────────────────────────────────────────── -->
|
|
692
|
-
<section class="section" id="loops" data-tabs-target="panel">
|
|
693
|
-
<div class="section-head">
|
|
694
|
-
<h2>Automatic loops</h2>
|
|
695
|
-
<p class="muted">each loop runs on its own worker fiber when this agent declares it — no cron, no restart. The buttons ask for a pass NOW, without waiting for the window; the loops that need a human (facts, skills, edits) always stop at the gate.</p>
|
|
696
|
-
</div>
|
|
697
|
-
|
|
698
|
-
<% distill_on = Insika::Coercion.truthy?((@agent.distill || {})["enabled"]) %>
|
|
699
|
-
<% harvest_on = Insika::Coercion.truthy?((@agent.harvest || {})["enabled"]) %>
|
|
700
|
-
<div class="card">
|
|
701
|
-
<div class="section-head">
|
|
702
|
-
<h3>Distillation <span class="muted">— idle conversations → fact proposals</span></h3>
|
|
703
|
-
<span class="pill <%= distill_on ? 'ok' : 'info' %>"><%= distill_on ? "active · idle #{(@agent.distill["idle_hours"] || 6)}h" : "off" %></span>
|
|
704
|
-
</div>
|
|
705
|
-
<p class="muted">Proposals land on the <a href="/studio/facts">Facts page</a> — nothing is ever applied without your click. Runs automatically on the engine's window.</p>
|
|
706
|
-
<% if distill_on && insika[:command_bus].registered?(:run_distillation) %>
|
|
707
|
-
<form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/distill" class="actions">
|
|
708
|
-
<%== csrf_tag %>
|
|
709
|
-
<button type="submit" class="primary" data-turbo-submits-with="Distilling…">Run distillation now</button>
|
|
710
|
-
<span class="muted">the due sessions (idle, long enough, undistilled)</span>
|
|
711
|
-
</form>
|
|
712
|
-
<% end %>
|
|
713
|
-
</div>
|
|
714
|
-
|
|
715
|
-
<div class="card">
|
|
716
|
-
<div class="section-head">
|
|
717
|
-
<h3>Harvest <span class="muted">— idle traffic → skill candidates</span></h3>
|
|
718
|
-
<span class="pill <%= harvest_on ? 'ok' : 'info' %>"><%= harvest_on ? "active · idle #{(@agent.harvest["idle_hours"] || 24)}h" : "off" %></span>
|
|
719
|
-
</div>
|
|
720
|
-
<p class="muted">Candidates are double-gated and promoted by a human on the <a href="/studio/harvest?agent=<%= Rack::Utils.escape(@agent.id) %>">Harvest page</a>. Runs automatically on the engine's window.</p>
|
|
721
|
-
<% if harvest_on && insika[:command_bus].registered?(:run_harvest) %>
|
|
722
|
-
<form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/harvest" class="actions">
|
|
723
|
-
<%== csrf_tag %>
|
|
724
|
-
<button type="submit" class="primary" data-turbo-submits-with="Mining…">Run harvest now</button>
|
|
725
|
-
<span class="muted">mines one window of this agent's finished traffic</span>
|
|
726
|
-
</form>
|
|
727
|
-
<% end %>
|
|
728
|
-
</div>
|
|
729
|
-
|
|
730
|
-
<div class="card">
|
|
731
|
-
<div class="section-head">
|
|
732
|
-
<h3>Refinement <span class="muted">— traffic → ranked findings</span></h3>
|
|
733
|
-
<span class="pill info"><%= @agent.refinement ? "configured · #{(@agent.refinement["mode"] || "report")}" : "report-only" %></span>
|
|
734
|
-
</div>
|
|
735
|
-
<p class="muted">A run reads this agent's own traffic and records what broke, ranked. It never edits the agent unless <span class="mono">auto_apply</span> is declared. Results on the <a href="/studio/refinement?agent=<%= Rack::Utils.escape(@agent.id) %>">Refinement page</a>.</p>
|
|
736
|
-
<% if insika[:command_bus].registered?(:run_refinement) %>
|
|
737
|
-
<form method="post" action="/studio/agents/<%= Rack::Utils.escape(@agent.id) %>/refinement" class="actions">
|
|
738
|
-
<%== csrf_tag %>
|
|
739
|
-
<button type="submit" class="primary" data-turbo-submits-with="Reading traffic…">Run refinement now</button>
|
|
740
|
-
<span class="muted">no model call — the collector reads stores only</span>
|
|
741
|
-
</form>
|
|
742
|
-
<% end %>
|
|
743
|
-
</div>
|
|
744
|
-
</section>
|
|
745
|
-
|
|
746
|
-
<!-- ── Outcomes (WS7) ────────────────────────────────────────────────────── -->
|
|
747
|
-
<section class="section" id="outcomes" data-tabs-target="panel">
|
|
748
|
-
<div class="section-head">
|
|
749
|
-
<h2>Outcomes</h2>
|
|
750
|
-
<p class="muted">business results recorded via <code>POST /v1/outcomes</code> — last outcome and per-day series. The engine transports them; it does not interpret them.</p>
|
|
751
|
-
</div>
|
|
752
|
-
<div class="card">
|
|
753
|
-
<% if @latest_outcome %>
|
|
754
|
-
<p>last
|
|
755
|
-
<span class="pill <%= @latest_outcome[:outcome] == "conversion" ? "ok" : "warn" %>"><%= @latest_outcome[:outcome] %><%== format(" · R$ %0.2f", @latest_outcome[:value]) if @latest_outcome[:value] && @latest_outcome[:value].positive? %></span>
|
|
756
|
-
<span class="muted mono"><%= @latest_outcome[:at] %></span>
|
|
757
|
-
</p>
|
|
758
|
-
<% end %>
|
|
759
|
-
<% if @outcome_series.nil? || @outcome_series.empty? %>
|
|
760
|
-
<p class="muted">No outcomes recorded.</p>
|
|
761
|
-
<% else %>
|
|
762
|
-
<ul class="kv">
|
|
763
|
-
<% @outcome_series.sort.reverse.each do |day, by_outcome| %>
|
|
764
|
-
<li>
|
|
765
|
-
<span class="k mono"><%= day %></span>
|
|
766
|
-
<span class="v"><%= by_outcome.map { |name, cell|
|
|
767
|
-
bits = ["#{name} ×#{cell[:count]}"]
|
|
768
|
-
bits << format("R$ %0.2f", cell[:value]) if cell[:value].to_f.positive?
|
|
769
|
-
bits.join(" · ")
|
|
770
|
-
}.join(" · ") %></span>
|
|
771
|
-
</li>
|
|
772
|
-
<% end %>
|
|
773
|
-
</ul>
|
|
774
|
-
<% end %>
|
|
775
|
-
</div>
|
|
776
|
-
</section>
|
|
777
|
-
|
|
778
|
-
<!-- ── Cache ─────────────────────────────────────────────────── -->
|
|
779
|
-
<section class="section" id="cache" data-tabs-target="panel">
|
|
780
|
-
<div class="section-head">
|
|
781
|
-
<h2>Cache</h2>
|
|
782
|
-
<p class="muted">prefix cache-hit per turn (provider-reported) — the invalidation reason names the first block whose bytes changed vs the previous turn.</p>
|
|
783
|
-
</div>
|
|
784
|
-
<div class="card">
|
|
785
|
-
<% if @cache_series.empty? %>
|
|
786
|
-
<p class="muted">No turns recorded.</p>
|
|
787
|
-
<% else %>
|
|
788
|
-
<ul class="kv">
|
|
789
|
-
<% @cache_series.last(20).reverse.each do |c| %>
|
|
790
|
-
<li>
|
|
791
|
-
<span class="k mono"><%= c["at"] %></span>
|
|
792
|
-
<span class="v">
|
|
793
|
-
<%== c["hit_pct"].nil? ? "—" : "#{c['hit_pct']}% cached" %>
|
|
794
|
-
· <%= c["cached_tokens"] %>/<%= c["prompt_tokens"] %> tokens
|
|
795
|
-
<% if c["invalidation_reason"] %><span class="lat">broke: <%= c["invalidation_reason"] %></span><% end %>
|
|
796
|
-
</span>
|
|
797
|
-
</li>
|
|
798
|
-
<% end %>
|
|
799
|
-
</ul>
|
|
800
|
-
<% end %>
|
|
801
|
-
</div>
|
|
802
|
-
</section>
|
|
803
|
-
|
|
804
|
-
<!-- ── History ───────────────────────────────────────────────────────────── -->
|
|
805
|
-
<section class="section" id="history" data-tabs-target="panel">
|
|
806
|
-
<div class="section-head">
|
|
807
|
-
<h2>History</h2>
|
|
808
|
-
<p class="muted">recent conversations (all agents — a session doesn't stamp its author).</p>
|
|
809
|
-
</div>
|
|
810
|
-
<% if @recent_sessions.empty? %>
|
|
811
|
-
<div class="empty">No conversations yet. Start one in the <a href="/studio/playground?agent=<%= Rack::Utils.escape(@agent.id) %>">playground</a>.</div>
|
|
812
|
-
<% else %>
|
|
813
|
-
<div class="card">
|
|
814
|
-
<ul class="history">
|
|
815
|
-
<% @recent_sessions.each do |s| %>
|
|
816
|
-
<li>
|
|
817
|
-
<a href="/studio/sessions/<%= Rack::Utils.escape(s.id) %>">
|
|
818
|
-
<span class="mono"><%= s.id[0, 12] %></span>
|
|
819
|
-
</a>
|
|
820
|
-
<span class="muted"><%= Array(s.messages).size %> msg · <%= s.updated_at %></span>
|
|
821
|
-
<% if (p = session_preview(s)) %><span class="preview muted"><%= p[0, 80] %></span><% end %>
|
|
822
|
-
</li>
|
|
823
|
-
<% end %>
|
|
824
|
-
</ul>
|
|
825
|
-
</div>
|
|
826
|
-
<% end %>
|
|
827
|
-
</section>
|
|
828
|
-
</div>
|
|
69
|
+
<% end %>
|