llm_logs 0.1.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 +7 -0
- data/README.md +148 -0
- data/Rakefile +11 -0
- data/app/controllers/llm_logs/application_controller.rb +5 -0
- data/app/controllers/llm_logs/prompt_versions_controller.rb +66 -0
- data/app/controllers/llm_logs/prompts_controller.rb +91 -0
- data/app/controllers/llm_logs/spans_controller.rb +8 -0
- data/app/controllers/llm_logs/traces_controller.rb +19 -0
- data/app/helpers/llm_logs/formatting_helper.rb +34 -0
- data/app/models/llm_logs/application_record.rb +5 -0
- data/app/models/llm_logs/prompt.rb +54 -0
- data/app/models/llm_logs/prompt_version.rb +31 -0
- data/app/models/llm_logs/span.rb +38 -0
- data/app/models/llm_logs/trace.rb +46 -0
- data/app/views/kaminari/tailwind/_first_page.html.erb +5 -0
- data/app/views/kaminari/tailwind/_gap.html.erb +3 -0
- data/app/views/kaminari/tailwind/_last_page.html.erb +5 -0
- data/app/views/kaminari/tailwind/_next_page.html.erb +5 -0
- data/app/views/kaminari/tailwind/_page.html.erb +9 -0
- data/app/views/kaminari/tailwind/_paginator.html.erb +17 -0
- data/app/views/kaminari/tailwind/_prev_page.html.erb +5 -0
- data/app/views/layouts/llm_logs/application.html.erb +52 -0
- data/app/views/llm_logs/prompt_versions/compare.html.erb +41 -0
- data/app/views/llm_logs/prompt_versions/index.html.erb +92 -0
- data/app/views/llm_logs/prompt_versions/show.html.erb +48 -0
- data/app/views/llm_logs/prompts/_form.html.erb +94 -0
- data/app/views/llm_logs/prompts/edit.html.erb +6 -0
- data/app/views/llm_logs/prompts/index.html.erb +43 -0
- data/app/views/llm_logs/prompts/new.html.erb +5 -0
- data/app/views/llm_logs/prompts/show.html.erb +107 -0
- data/app/views/llm_logs/spans/show.html.erb +99 -0
- data/app/views/llm_logs/traces/_span_node.html.erb +66 -0
- data/app/views/llm_logs/traces/index.html.erb +78 -0
- data/app/views/llm_logs/traces/show.html.erb +79 -0
- data/config/routes.rb +18 -0
- data/db/migrate/001_create_llm_logs_traces.rb +22 -0
- data/db/migrate/002_create_llm_logs_spans.rb +31 -0
- data/db/migrate/003_create_llm_logs_prompts.rb +13 -0
- data/db/migrate/004_create_llm_logs_prompt_versions.rb +18 -0
- data/db/migrate/005_add_prompt_version_to_traces.rb +7 -0
- data/lib/generators/llm_logs/install_generator.rb +17 -0
- data/lib/generators/llm_logs/templates/initializer.rb +10 -0
- data/lib/llm_logs/engine.rb +14 -0
- data/lib/llm_logs/instrumentation/ruby_llm_chat.rb +107 -0
- data/lib/llm_logs/prompt_renderer.rb +17 -0
- data/lib/llm_logs/tracer.rb +67 -0
- data/lib/llm_logs/version.rb +3 -0
- data/lib/llm_logs.rb +23 -0
- metadata +145 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full bg-gray-50">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>LLM Logs</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script type="module">
|
|
9
|
+
import "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.12/dist/turbo.es2017-esm.js"
|
|
10
|
+
</script>
|
|
11
|
+
<style>
|
|
12
|
+
pre { white-space: pre-wrap; word-wrap: break-word; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body class="h-full">
|
|
16
|
+
<nav class="bg-gray-900">
|
|
17
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
18
|
+
<div class="flex h-14 items-center justify-between">
|
|
19
|
+
<div class="flex items-center space-x-8">
|
|
20
|
+
<span class="text-white font-semibold text-lg">LLM Logs</span>
|
|
21
|
+
<div class="flex space-x-1">
|
|
22
|
+
<%= link_to "Traces", llm_logs.traces_path,
|
|
23
|
+
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path.start_with?(llm_logs.traces_path) || request.path == llm_logs.root_path ? 'bg-gray-800 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
|
|
24
|
+
<%= link_to "Prompts", llm_logs.prompts_path,
|
|
25
|
+
class: "px-3 py-2 rounded-md text-sm font-medium #{request.path.start_with?(llm_logs.prompts_path) ? 'bg-gray-800 text-white' : 'text-gray-300 hover:bg-gray-700 hover:text-white'}" %>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</nav>
|
|
31
|
+
|
|
32
|
+
<% if notice.present? %>
|
|
33
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
|
|
34
|
+
<div class="rounded-md bg-green-50 p-3 text-sm text-green-800 border border-green-200">
|
|
35
|
+
<%= notice %>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<% if alert.present? %>
|
|
41
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
|
|
42
|
+
<div class="rounded-md bg-red-50 p-3 text-sm text-red-800 border border-red-200">
|
|
43
|
+
<%= alert %>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<main class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
|
|
49
|
+
<%= yield %>
|
|
50
|
+
</main>
|
|
51
|
+
</body>
|
|
52
|
+
</html>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="flex items-center space-x-2 text-sm text-gray-500 mb-2">
|
|
3
|
+
<%= link_to "Prompts", prompts_path, class: "text-indigo-600 hover:text-indigo-900" %>
|
|
4
|
+
<span>/</span>
|
|
5
|
+
<%= link_to @prompt.name, prompt_path(@prompt), class: "text-indigo-600 hover:text-indigo-900" %>
|
|
6
|
+
<span>/</span>
|
|
7
|
+
<span>Compare v<%= @version_a.version_number %> vs v<%= @version_b.version_number %></span>
|
|
8
|
+
</div>
|
|
9
|
+
<h1 class="text-2xl font-bold text-gray-900">
|
|
10
|
+
Compare v<%= @version_a.version_number %> vs v<%= @version_b.version_number %>
|
|
11
|
+
</h1>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.diff ul { list-style: none; margin: 0; padding: 0; }
|
|
16
|
+
.diff li { min-height: 1.25em; }
|
|
17
|
+
.diff li.del { background-color: #fecaca; }
|
|
18
|
+
.diff li.ins { background-color: #bbf7d0; }
|
|
19
|
+
.diff del, .diff ins { text-decoration: none; }
|
|
20
|
+
.diff pre { white-space: pre-wrap; word-wrap: break-word; font-size: 0.875rem; }
|
|
21
|
+
</style>
|
|
22
|
+
|
|
23
|
+
<div class="space-y-6">
|
|
24
|
+
<% @diffs.each do |diff| %>
|
|
25
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5">
|
|
26
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
27
|
+
<span class="text-xs font-medium text-gray-500 uppercase"><%= diff[:role] %></span>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="grid grid-cols-2 divide-x divide-gray-200 diff">
|
|
30
|
+
<div class="p-4">
|
|
31
|
+
<div class="text-xs font-medium text-gray-400 mb-2">v<%= @version_a.version_number %></div>
|
|
32
|
+
<pre class="font-mono"><%= raw diff[:left] %></pre>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="p-4">
|
|
35
|
+
<div class="text-xs font-medium text-gray-400 mb-2">v<%= @version_b.version_number %></div>
|
|
36
|
+
<pre class="font-mono"><%= raw diff[:right] %></pre>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</div>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="flex items-center space-x-2 text-sm text-gray-500 mb-2">
|
|
3
|
+
<%= link_to "Prompts", prompts_path, class: "text-indigo-600 hover:text-indigo-900" %>
|
|
4
|
+
<span>/</span>
|
|
5
|
+
<%= link_to @prompt.name, prompt_path(@prompt), class: "text-indigo-600 hover:text-indigo-900" %>
|
|
6
|
+
<span>/</span>
|
|
7
|
+
<span>Versions</span>
|
|
8
|
+
</div>
|
|
9
|
+
<h1 class="text-2xl font-bold text-gray-900">Version History: <%= @prompt.name %></h1>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div id="compare-bar" class="mb-4 hidden">
|
|
13
|
+
<a id="compare-link" href="#" class="inline-flex items-center bg-indigo-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-indigo-500">
|
|
14
|
+
Compare
|
|
15
|
+
</a>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<% current_version = @versions.first %>
|
|
19
|
+
|
|
20
|
+
<div class="space-y-4">
|
|
21
|
+
<% @versions.each do |version| %>
|
|
22
|
+
<% is_current = version == current_version %>
|
|
23
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-4">
|
|
24
|
+
<div class="flex items-center justify-between mb-3">
|
|
25
|
+
<div class="flex items-center space-x-3">
|
|
26
|
+
<input type="checkbox" class="compare-checkbox rounded border-gray-300 text-indigo-600"
|
|
27
|
+
value="<%= version.version_number %>" data-version="<%= version.version_number %>">
|
|
28
|
+
<%= link_to prompt_version_path(@prompt, version), class: "text-lg font-semibold text-gray-900 hover:text-indigo-600" do %>
|
|
29
|
+
v<%= version.version_number %>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% if is_current %>
|
|
32
|
+
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800">Current</span>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% if version.model.present? %>
|
|
35
|
+
<span class="text-xs bg-gray-100 rounded-full px-2 py-0.5 text-gray-600"><%= version.model %></span>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="flex items-center space-x-3">
|
|
39
|
+
<span class="text-sm text-gray-500"><%= version.created_at.strftime('%b %d, %Y %H:%M') %></span>
|
|
40
|
+
<% unless is_current %>
|
|
41
|
+
<%= button_to "Restore", restore_prompt_version_path(@prompt, version),
|
|
42
|
+
method: :post,
|
|
43
|
+
class: "text-sm text-indigo-600 hover:text-indigo-900",
|
|
44
|
+
data: { turbo_confirm: "Are you sure you want to restore this as the current active prompt version?" } %>
|
|
45
|
+
<%= button_to "Delete", prompt_version_path(@prompt, version),
|
|
46
|
+
method: :delete,
|
|
47
|
+
class: "text-sm text-red-600 hover:text-red-900",
|
|
48
|
+
data: { turbo_confirm: "Are you sure you want to delete this version?" } %>
|
|
49
|
+
<% end %>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<% if version.changelog.present? %>
|
|
54
|
+
<p class="text-sm text-gray-600 mb-3"><%= version.changelog %></p>
|
|
55
|
+
<% end %>
|
|
56
|
+
|
|
57
|
+
<div class="space-y-2">
|
|
58
|
+
<% version.messages.each do |msg| %>
|
|
59
|
+
<div class="rounded p-2 text-xs <%= msg['role'] == 'system' ? 'bg-yellow-50' : msg['role'] == 'user' ? 'bg-blue-50' : 'bg-gray-50' %>">
|
|
60
|
+
<span class="font-medium text-gray-500 uppercase"><%= msg['role'] %>:</span>
|
|
61
|
+
<span class="font-mono"><%= truncate(msg['content'], length: 120) %></span>
|
|
62
|
+
</div>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<script>
|
|
70
|
+
document.addEventListener("turbo:load", function() {
|
|
71
|
+
var checkboxes = document.querySelectorAll(".compare-checkbox");
|
|
72
|
+
var bar = document.getElementById("compare-bar");
|
|
73
|
+
var link = document.getElementById("compare-link");
|
|
74
|
+
var basePath = "<%= prompt_versions_path(@prompt) %>/compare";
|
|
75
|
+
|
|
76
|
+
checkboxes.forEach(function(cb) {
|
|
77
|
+
cb.addEventListener("change", updateCompare);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
function updateCompare() {
|
|
81
|
+
var checked = document.querySelectorAll(".compare-checkbox:checked");
|
|
82
|
+
if (checked.length === 2) {
|
|
83
|
+
var versions = Array.from(checked).map(function(cb) { return parseInt(cb.value); }).sort(function(a, b) { return a - b; });
|
|
84
|
+
link.href = basePath + "?a=" + versions[0] + "&b=" + versions[1];
|
|
85
|
+
link.textContent = "Compare v" + versions[0] + " vs v" + versions[1];
|
|
86
|
+
bar.classList.remove("hidden");
|
|
87
|
+
} else {
|
|
88
|
+
bar.classList.add("hidden");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
</script>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="flex items-center space-x-2 text-sm text-gray-500 mb-2">
|
|
3
|
+
<%= link_to "Prompts", prompts_path, class: "text-indigo-600 hover:text-indigo-900" %>
|
|
4
|
+
<span>/</span>
|
|
5
|
+
<%= link_to @prompt.name, prompt_path(@prompt), class: "text-indigo-600 hover:text-indigo-900" %>
|
|
6
|
+
<span>/</span>
|
|
7
|
+
<span>v<%= @version.version_number %></span>
|
|
8
|
+
</div>
|
|
9
|
+
<h1 class="text-2xl font-bold text-gray-900"><%= @prompt.name %> — v<%= @version.version_number %></h1>
|
|
10
|
+
<% if @version.changelog.present? %>
|
|
11
|
+
<p class="text-sm text-gray-500 mt-1"><%= @version.changelog %></p>
|
|
12
|
+
<% end %>
|
|
13
|
+
<p class="text-sm text-gray-500 mt-1">
|
|
14
|
+
<%= link_to pluralize(@version.traces.count, "trace"),
|
|
15
|
+
traces_path(prompt_version_id: @version.id),
|
|
16
|
+
class: "text-indigo-600 hover:text-indigo-900" %>
|
|
17
|
+
</p>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="space-y-6">
|
|
21
|
+
<% if @version.model.present? %>
|
|
22
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-4">
|
|
23
|
+
<h2 class="text-sm font-medium text-gray-900 mb-2">Model: <span class="font-mono"><%= @version.model %></span></h2>
|
|
24
|
+
<% if @version.model_params.present? && @version.model_params.any? %>
|
|
25
|
+
<dl class="grid grid-cols-4 gap-2 text-sm">
|
|
26
|
+
<% @version.model_params.each do |key, value| %>
|
|
27
|
+
<dt class="text-gray-500"><%= key %></dt>
|
|
28
|
+
<dd class="text-gray-900 font-mono"><%= value %></dd>
|
|
29
|
+
<% end %>
|
|
30
|
+
</dl>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
|
|
35
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5">
|
|
36
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
37
|
+
<h2 class="text-sm font-medium text-gray-900">Messages</h2>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="p-4 space-y-3">
|
|
40
|
+
<% @version.messages.each do |msg| %>
|
|
41
|
+
<div class="rounded-lg p-3 <%= msg['role'] == 'system' ? 'bg-yellow-50' : msg['role'] == 'user' ? 'bg-blue-50' : 'bg-gray-50' %>">
|
|
42
|
+
<div class="text-xs font-medium text-gray-500 uppercase mb-1"><%= msg['role'] %></div>
|
|
43
|
+
<pre class="text-sm whitespace-pre-wrap font-mono"><%= msg['content'] %></pre>
|
|
44
|
+
</div>
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<%= form_with(model: prompt, url: prompt.persisted? ? prompt_path(prompt) : prompts_path, class: "space-y-6") do |f| %>
|
|
2
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-6 space-y-4">
|
|
3
|
+
<h2 class="text-lg font-medium text-gray-900">Prompt Details</h2>
|
|
4
|
+
|
|
5
|
+
<div class="grid grid-cols-2 gap-4">
|
|
6
|
+
<div>
|
|
7
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Slug</label>
|
|
8
|
+
<%= f.text_field :slug, class: "w-full rounded-md shadow-sm text-sm px-3 py-2 border #{prompt.errors[:slug].any? ? 'border-red-500' : 'border-gray-300'}",
|
|
9
|
+
placeholder: "e.g. strategy-analysis" %>
|
|
10
|
+
<% prompt.errors[:slug].each do |error| %>
|
|
11
|
+
<p class="mt-1 text-sm text-red-600"><%= error %></p>
|
|
12
|
+
<% end %>
|
|
13
|
+
</div>
|
|
14
|
+
<div>
|
|
15
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
16
|
+
<%= f.text_field :name, class: "w-full rounded-md shadow-sm text-sm px-3 py-2 border #{prompt.errors[:name].any? ? 'border-red-500' : 'border-gray-300'}" %>
|
|
17
|
+
<% prompt.errors[:name].each do |error| %>
|
|
18
|
+
<p class="mt-1 text-sm text-red-600"><%= error %></p>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div>
|
|
24
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
|
|
25
|
+
<%= f.text_area :description, rows: 2, class: "w-full rounded-md shadow-sm text-sm px-3 py-2 border #{prompt.errors[:description].any? ? 'border-red-500' : 'border-gray-300'}" %>
|
|
26
|
+
<% prompt.errors[:description].each do |error| %>
|
|
27
|
+
<p class="mt-1 text-sm text-red-600"><%= error %></p>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-6 space-y-4">
|
|
33
|
+
<h2 class="text-lg font-medium text-gray-900">Messages</h2>
|
|
34
|
+
<p class="text-xs text-gray-500">Use <code class="bg-gray-100 px-1 rounded">{{variable_name}}</code> for Mustache template variables.</p>
|
|
35
|
+
|
|
36
|
+
<div id="messages-container" class="space-y-4">
|
|
37
|
+
<% messages = (current_version&.messages || [{ "role" => "system", "content" => "" }]) %>
|
|
38
|
+
<% messages.each_with_index do |msg, i| %>
|
|
39
|
+
<div class="message-row border rounded-lg p-4 space-y-2">
|
|
40
|
+
<div class="flex items-center justify-between">
|
|
41
|
+
<select name="prompt[messages][<%= i %>][role]" class="rounded-md border-gray-300 text-sm py-1.5 px-3 bg-white border shadow-sm">
|
|
42
|
+
<% %w[system user assistant].each do |role| %>
|
|
43
|
+
<option value="<%= role %>" <%= 'selected' if msg['role'] == role %>><%= role %></option>
|
|
44
|
+
<% end %>
|
|
45
|
+
</select>
|
|
46
|
+
</div>
|
|
47
|
+
<textarea name="prompt[messages][<%= i %>][content]" rows="4"
|
|
48
|
+
class="w-full rounded-md border-gray-300 shadow-sm text-sm px-3 py-2 border font-mono"
|
|
49
|
+
placeholder="Message content..."><%= msg['content'] %></textarea>
|
|
50
|
+
</div>
|
|
51
|
+
<% end %>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-6 space-y-4">
|
|
56
|
+
<h2 class="text-lg font-medium text-gray-900">Model Configuration</h2>
|
|
57
|
+
|
|
58
|
+
<div class="grid grid-cols-3 gap-4">
|
|
59
|
+
<div>
|
|
60
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Model</label>
|
|
61
|
+
<%= f.text_field :model, value: current_version&.model,
|
|
62
|
+
class: "w-full rounded-md border-gray-300 shadow-sm text-sm px-3 py-2 border",
|
|
63
|
+
placeholder: "e.g. claude-sonnet-4" %>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Temperature</label>
|
|
67
|
+
<input type="number" name="prompt[model_params][temperature]" step="0.1" min="0" max="2"
|
|
68
|
+
value="<%= current_version&.model_params&.dig('temperature') %>"
|
|
69
|
+
class="w-full rounded-md border-gray-300 shadow-sm text-sm px-3 py-2 border">
|
|
70
|
+
</div>
|
|
71
|
+
<div>
|
|
72
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Max Tokens</label>
|
|
73
|
+
<input type="number" name="prompt[model_params][max_tokens]" step="1" min="1"
|
|
74
|
+
value="<%= current_version&.model_params&.dig('max_tokens') %>"
|
|
75
|
+
class="w-full rounded-md border-gray-300 shadow-sm text-sm px-3 py-2 border">
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<% if prompt.persisted? %>
|
|
81
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-6">
|
|
82
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Changelog (optional)</label>
|
|
83
|
+
<%= f.text_field :changelog, class: "w-full rounded-md border-gray-300 shadow-sm text-sm px-3 py-2 border",
|
|
84
|
+
placeholder: "What changed in this version?" %>
|
|
85
|
+
</div>
|
|
86
|
+
<% end %>
|
|
87
|
+
|
|
88
|
+
<div class="flex items-center justify-between">
|
|
89
|
+
<%= link_to "Cancel", prompt.persisted? ? prompt_path(prompt) : prompts_path, class: "text-sm text-gray-600 hover:text-gray-900" %>
|
|
90
|
+
<button type="submit" class="bg-indigo-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-indigo-500">
|
|
91
|
+
<%= prompt.persisted? ? "Save New Version" : "Create Prompt" %>
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
<% end %>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<div class="flex items-center justify-between mb-6">
|
|
2
|
+
<h1 class="text-2xl font-bold text-gray-900">Prompts</h1>
|
|
3
|
+
<%= link_to "New Prompt", new_prompt_path, class: "bg-indigo-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-indigo-500" %>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="bg-white shadow-sm ring-1 ring-gray-900/5 rounded-lg overflow-hidden">
|
|
7
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
8
|
+
<thead class="bg-gray-50">
|
|
9
|
+
<tr>
|
|
10
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
|
|
11
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Slug</th>
|
|
12
|
+
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Version</th>
|
|
13
|
+
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Updated</th>
|
|
14
|
+
</tr>
|
|
15
|
+
</thead>
|
|
16
|
+
<tbody class="divide-y divide-gray-200">
|
|
17
|
+
<% @prompts.each do |prompt| %>
|
|
18
|
+
<tr class="hover:bg-gray-50">
|
|
19
|
+
<td class="px-4 py-3 text-sm">
|
|
20
|
+
<%= link_to prompt.name, prompt_path(prompt), class: "text-indigo-600 hover:text-indigo-900 font-medium" %>
|
|
21
|
+
</td>
|
|
22
|
+
<td class="px-4 py-3 text-sm text-gray-500 font-mono"><%= prompt.slug %></td>
|
|
23
|
+
<td class="px-4 py-3 text-sm text-right">
|
|
24
|
+
<% if prompt.versions.any? %>
|
|
25
|
+
<%= link_to "v#{prompt.versions.maximum(:version_number)}", prompt_versions_path(prompt), class: "text-indigo-600 hover:text-indigo-900" %>
|
|
26
|
+
<% else %>—<% end %>
|
|
27
|
+
</td>
|
|
28
|
+
<td class="px-4 py-3 text-sm text-gray-500"><%= prompt.updated_at.strftime('%b %d %H:%M') %></td>
|
|
29
|
+
</tr>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<% if @prompts.empty? %>
|
|
33
|
+
<tr>
|
|
34
|
+
<td colspan="5" class="px-4 py-8 text-center text-sm text-gray-500">
|
|
35
|
+
No prompts yet. <%= link_to "Create one", new_prompt_path, class: "text-indigo-600 hover:text-indigo-900" %>.
|
|
36
|
+
</td>
|
|
37
|
+
</tr>
|
|
38
|
+
<% end %>
|
|
39
|
+
</tbody>
|
|
40
|
+
</table>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<%= paginate @prompts, theme: "tailwind" %>
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="flex items-center justify-between">
|
|
3
|
+
<div>
|
|
4
|
+
<h1 class="text-2xl font-bold text-gray-900"><%= @prompt.name %></h1>
|
|
5
|
+
<p class="text-sm text-gray-500 mt-1">
|
|
6
|
+
<span class="font-mono"><%= @prompt.slug %></span>
|
|
7
|
+
<% if @prompt.description.present? %>
|
|
8
|
+
· <%= @prompt.description %>
|
|
9
|
+
<% end %>
|
|
10
|
+
</p>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="flex items-center space-x-3">
|
|
13
|
+
<%= link_to "Edit", edit_prompt_path(@prompt), class: "bg-indigo-600 text-white px-3 py-1.5 rounded-md text-sm font-medium hover:bg-indigo-500" %>
|
|
14
|
+
<%= button_to "Delete", prompt_path(@prompt), method: :delete, class: "text-sm text-red-600 hover:text-red-900",
|
|
15
|
+
data: { turbo_confirm: "Delete this prompt and all versions?" } %>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="grid grid-cols-3 gap-6">
|
|
21
|
+
<div class="col-span-2 space-y-6">
|
|
22
|
+
<% if @current_version %>
|
|
23
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5">
|
|
24
|
+
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
|
|
25
|
+
<h2 class="text-sm font-medium text-gray-900">Messages (v<%= @current_version.version_number %>)</h2>
|
|
26
|
+
<% if @current_version.model.present? %>
|
|
27
|
+
<span class="text-xs text-gray-500">Model: <span class="font-medium"><%= @current_version.model %></span></span>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="p-4 space-y-3">
|
|
31
|
+
<% @current_version.messages.each do |msg| %>
|
|
32
|
+
<div class="rounded-lg p-3 <%= msg['role'] == 'system' ? 'bg-yellow-50' : msg['role'] == 'user' ? 'bg-blue-50' : 'bg-gray-50' %>">
|
|
33
|
+
<div class="text-xs font-medium text-gray-500 uppercase mb-1"><%= msg['role'] %></div>
|
|
34
|
+
<pre class="text-sm whitespace-pre-wrap font-mono"><%= msg['content'] %></pre>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<% if @current_version.model_params.present? && @current_version.model_params.any? %>
|
|
41
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5">
|
|
42
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
43
|
+
<h2 class="text-sm font-medium text-gray-900">Model Parameters</h2>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="p-4">
|
|
46
|
+
<dl class="grid grid-cols-2 gap-2 text-sm">
|
|
47
|
+
<% @current_version.model_params.each do |key, value| %>
|
|
48
|
+
<dt class="text-gray-500"><%= key %></dt>
|
|
49
|
+
<dd class="text-gray-900 font-mono"><%= value %></dd>
|
|
50
|
+
<% end %>
|
|
51
|
+
</dl>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
<% end %>
|
|
55
|
+
|
|
56
|
+
<% if @current_version.default_variables.present? && @current_version.default_variables.any? %>
|
|
57
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5">
|
|
58
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
59
|
+
<h2 class="text-sm font-medium text-gray-900">Default Variables</h2>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="p-4">
|
|
62
|
+
<pre class="text-sm bg-gray-50 rounded-lg p-3 font-mono"><%= JSON.pretty_generate(@current_version.default_variables) %></pre>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
<% end %>
|
|
66
|
+
<% else %>
|
|
67
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-8 text-center text-sm text-gray-500">
|
|
68
|
+
No versions yet. <%= link_to "Edit this prompt", edit_prompt_path(@prompt), class: "text-indigo-600" %> to add content.
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div>
|
|
74
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5">
|
|
75
|
+
<div class="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
|
|
76
|
+
<h2 class="text-sm font-medium text-gray-900">Version History</h2>
|
|
77
|
+
<%= link_to "View all", prompt_versions_path(@prompt), class: "text-xs text-indigo-600 hover:text-indigo-900" %>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="divide-y divide-gray-100">
|
|
80
|
+
<% @versions.each do |version| %>
|
|
81
|
+
<%= link_to prompt_version_path(@prompt, version), class: "block px-4 py-3 #{version == @current_version ? 'bg-indigo-50' : 'hover:bg-gray-50'}" do %>
|
|
82
|
+
<div class="flex items-center justify-between">
|
|
83
|
+
<span class="text-sm font-medium text-gray-900">v<%= version.version_number %></span>
|
|
84
|
+
<% if version == @current_version %>
|
|
85
|
+
<span class="inline-flex items-center rounded-full bg-green-100 px-1.5 py-0.5 text-xs font-medium text-green-800">Current</span>
|
|
86
|
+
<% end %>
|
|
87
|
+
<span class="text-xs text-gray-500"><%= version.created_at.strftime('%b %d %H:%M') %></span>
|
|
88
|
+
</div>
|
|
89
|
+
<% if version.changelog.present? %>
|
|
90
|
+
<p class="text-xs text-gray-500 mt-1"><%= version.changelog %></p>
|
|
91
|
+
<% end %>
|
|
92
|
+
<% end %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="mt-4 bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 p-4">
|
|
98
|
+
<h3 class="text-sm font-medium text-gray-900 mb-2">SDK Usage</h3>
|
|
99
|
+
<pre class="text-xs bg-gray-50 rounded p-3 font-mono overflow-x-auto">prompt = LlmLogs::Prompt.load("<%= @prompt.slug %>")
|
|
100
|
+
params = prompt.build(<% if @current_version&.variables&.any? %>
|
|
101
|
+
<%= @current_version.variables.map { |v| " #{v}: \"\"" }.join(",\n") %>
|
|
102
|
+
<% else %>
|
|
103
|
+
# no variables
|
|
104
|
+
<% end %>)</pre>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<div class="flex items-center space-x-2 text-sm text-gray-500 mb-2">
|
|
3
|
+
<%= link_to @trace.name, trace_path(@trace), class: "text-indigo-600 hover:text-indigo-900" %>
|
|
4
|
+
<span>/</span>
|
|
5
|
+
<span><%= @span.name %></span>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<div class="flex items-center space-x-3">
|
|
9
|
+
<h1 class="text-2xl font-bold text-gray-900"><%= @span.name %></h1>
|
|
10
|
+
<% type_colors = { "llm" => "bg-purple-100 text-purple-800", "tool" => "bg-amber-100 text-amber-800", "custom" => "bg-gray-100 text-gray-800" } %>
|
|
11
|
+
<span class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium <%= type_colors[@span.span_type] %>">
|
|
12
|
+
<%= @span.span_type %>
|
|
13
|
+
</span>
|
|
14
|
+
<% if @span.status == "error" %>
|
|
15
|
+
<span class="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium bg-red-100 text-red-800">error</span>
|
|
16
|
+
<% end %>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="grid grid-cols-5 gap-4 mb-6">
|
|
21
|
+
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
22
|
+
<dt class="text-xs font-medium text-gray-500 uppercase">Model</dt>
|
|
23
|
+
<dd class="text-lg font-semibold text-gray-900 mt-1"><%= @span.model || "—" %></dd>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
26
|
+
<dt class="text-xs font-medium text-gray-500 uppercase">Input Tokens</dt>
|
|
27
|
+
<dd class="text-lg font-semibold text-gray-900 mt-1"><%= number_with_delimiter(@span.input_tokens || 0) %></dd>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
30
|
+
<dt class="text-xs font-medium text-gray-500 uppercase">Output Tokens</dt>
|
|
31
|
+
<dd class="text-lg font-semibold text-gray-900 mt-1"><%= number_with_delimiter(@span.output_tokens || 0) %></dd>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
34
|
+
<dt class="text-xs font-medium text-gray-500 uppercase">Cached Tokens</dt>
|
|
35
|
+
<dd class="text-lg font-semibold text-green-600 mt-1"><%= number_with_delimiter(@span.cached_tokens || 0) %></dd>
|
|
36
|
+
<% if (@span.input_tokens || 0) > 0 && (@span.cached_tokens || 0) > 0 %>
|
|
37
|
+
<dd class="text-xs text-green-600 mt-0.5"><%= (@span.cached_tokens.to_f / @span.input_tokens * 100).round %>% of input</dd>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="bg-white rounded-lg p-4 shadow-sm ring-1 ring-gray-900/5">
|
|
41
|
+
<dt class="text-xs font-medium text-gray-500 uppercase">Duration</dt>
|
|
42
|
+
<dd class="text-lg font-semibold text-gray-900 mt-1">
|
|
43
|
+
<%= format_duration_ms(@span.duration_ms) %>
|
|
44
|
+
</dd>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<% if @span.input.present? %>
|
|
49
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 mb-6">
|
|
50
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
51
|
+
<h2 class="text-sm font-medium text-gray-900">Input<%= @span.input.is_a?(Array) ? " Messages" : "" %></h2>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="p-4 space-y-3">
|
|
54
|
+
<% if @span.input.is_a?(Array) %>
|
|
55
|
+
<% @span.input.each do |msg| %>
|
|
56
|
+
<div class="rounded-lg p-3 <%= msg['role'] == 'user' ? 'bg-blue-50' : 'bg-gray-50' %>">
|
|
57
|
+
<div class="text-xs font-medium text-gray-500 uppercase mb-1"><%= msg['role'] %></div>
|
|
58
|
+
<pre class="text-sm whitespace-pre-wrap"><%= msg['content'] %></pre>
|
|
59
|
+
</div>
|
|
60
|
+
<% end %>
|
|
61
|
+
<% else %>
|
|
62
|
+
<pre class="text-sm bg-gray-50 rounded-lg p-3 whitespace-pre-wrap"><%= pretty_json(@span.input) %></pre>
|
|
63
|
+
<% end %>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
|
|
68
|
+
<% if @span.output.present? %>
|
|
69
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5 mb-6">
|
|
70
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
71
|
+
<h2 class="text-sm font-medium text-gray-900">Output</h2>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="p-4">
|
|
74
|
+
<pre class="text-sm bg-green-50 rounded-lg p-3 whitespace-pre-wrap"><%= pretty_json(@span.output) %></pre>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
<% end %>
|
|
78
|
+
|
|
79
|
+
<% if @span.error_message.present? %>
|
|
80
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-red-200 mb-6">
|
|
81
|
+
<div class="px-4 py-3 border-b border-red-200 bg-red-50">
|
|
82
|
+
<h2 class="text-sm font-medium text-red-800">Error</h2>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="p-4">
|
|
85
|
+
<pre class="text-sm text-red-800 whitespace-pre-wrap"><%= @span.error_message %></pre>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
<% end %>
|
|
89
|
+
|
|
90
|
+
<% if @span.metadata.present? && @span.metadata.any? %>
|
|
91
|
+
<div class="bg-white rounded-lg shadow-sm ring-1 ring-gray-900/5">
|
|
92
|
+
<div class="px-4 py-3 border-b border-gray-200">
|
|
93
|
+
<h2 class="text-sm font-medium text-gray-900">Metadata</h2>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="p-4">
|
|
96
|
+
<pre class="text-sm bg-gray-50 rounded-lg p-3"><%= JSON.pretty_generate(@span.metadata) %></pre>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
<% end %>
|