rails-llm 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/Gemfile +4 -0
- data/LICENSE +13 -0
- data/README.md +127 -0
- data/app/assets/images/llm.png +0 -0
- data/app/assets/javascripts/rails_llm/application.js +188 -0
- data/app/controllers/rails_llm/agents_controller.rb +63 -0
- data/app/views/layouts/rails_llm/application.html.erb +410 -0
- data/app/views/rails_llm/agents/_message.html.erb +10 -0
- data/app/views/rails_llm/agents/index.html.erb +21 -0
- data/app/views/rails_llm/agents/show.html.erb +30 -0
- data/config/routes.rb +11 -0
- data/lib/generators/rails_llm/install_generator.rb +43 -0
- data/lib/generators/rails_llm/templates/agent_model.rb.tt +25 -0
- data/lib/generators/rails_llm/templates/initializer.rb.tt +8 -0
- data/lib/generators/rails_llm/templates/knowledge_tool.rb.tt +61 -0
- data/lib/generators/rails_llm/templates/migration.rb.tt +9 -0
- data/lib/generators/rails_llm/ui_generator.rb +28 -0
- data/lib/rails-llm.rb +47 -0
- data/lib/rails_llm/engine.rb +27 -0
- data/lib/rails_llm/stream.rb +69 -0
- data/lib/rails_llm/version.rb +5 -0
- data/lib/rails_llm.rb +3 -0
- data/rails-llm.gemspec +47 -0
- metadata +183 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>llm.rb Agents</title>
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<%= csrf_meta_tags %>
|
|
7
|
+
<%= csp_meta_tag %>
|
|
8
|
+
<%= javascript_include_tag "rails_llm/application", defer: true %>
|
|
9
|
+
<%= yield :head %>
|
|
10
|
+
<style>
|
|
11
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
|
+
body {
|
|
13
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
14
|
+
Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
15
|
+
background: #f8f9fa;
|
|
16
|
+
color: #1a1a2e;
|
|
17
|
+
height: 100vh;
|
|
18
|
+
}
|
|
19
|
+
.layout { display: flex; height: 100vh; }
|
|
20
|
+
.sidebar {
|
|
21
|
+
width: 260px;
|
|
22
|
+
background: #1a1a2e;
|
|
23
|
+
color: #c8d6e5;
|
|
24
|
+
padding: 20px 16px;
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
gap: 6px;
|
|
28
|
+
}
|
|
29
|
+
.sidebar .brand {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 10px;
|
|
33
|
+
margin-bottom: 20px;
|
|
34
|
+
padding-bottom: 16px;
|
|
35
|
+
border-bottom: 1px solid #2d2d4a;
|
|
36
|
+
}
|
|
37
|
+
.sidebar .brand img {
|
|
38
|
+
width: 32px; height: 32px;
|
|
39
|
+
border-radius: 6px;
|
|
40
|
+
}
|
|
41
|
+
.sidebar .brand h1 {
|
|
42
|
+
font-size: 16px;
|
|
43
|
+
font-weight: 600;
|
|
44
|
+
color: #fff;
|
|
45
|
+
}
|
|
46
|
+
.sidebar .brand span {
|
|
47
|
+
font-size: 11px;
|
|
48
|
+
color: #00d68f;
|
|
49
|
+
font-weight: 500;
|
|
50
|
+
text-transform: uppercase;
|
|
51
|
+
letter-spacing: 0.5px;
|
|
52
|
+
}
|
|
53
|
+
.sidebar .new-agent {
|
|
54
|
+
background: transparent;
|
|
55
|
+
color: #c8d6e5;
|
|
56
|
+
border: 1px dashed #2d2d4a;
|
|
57
|
+
border-radius: 8px;
|
|
58
|
+
padding: 10px;
|
|
59
|
+
text-align: center;
|
|
60
|
+
font-size: 13px;
|
|
61
|
+
font-weight: 500;
|
|
62
|
+
text-decoration: none;
|
|
63
|
+
transition: all 0.15s;
|
|
64
|
+
margin-bottom: 12px;
|
|
65
|
+
}
|
|
66
|
+
.sidebar .new-agent:hover {
|
|
67
|
+
background: #2d2d4a;
|
|
68
|
+
border-color: #00d68f;
|
|
69
|
+
color: #fff;
|
|
70
|
+
}
|
|
71
|
+
.sidebar .agent-link {
|
|
72
|
+
display: block;
|
|
73
|
+
padding: 8px 12px;
|
|
74
|
+
border-radius: 6px;
|
|
75
|
+
color: #8899b4;
|
|
76
|
+
text-decoration: none;
|
|
77
|
+
font-size: 13px;
|
|
78
|
+
transition: all 0.12s;
|
|
79
|
+
overflow: hidden;
|
|
80
|
+
text-overflow: ellipsis;
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
}
|
|
83
|
+
.sidebar .agent-link:hover {
|
|
84
|
+
background: #2d2d4a;
|
|
85
|
+
color: #e0e6ed;
|
|
86
|
+
}
|
|
87
|
+
.sidebar .agent-link.active {
|
|
88
|
+
background: #2d2d4a;
|
|
89
|
+
color: #00d68f;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
border-left: 3px solid #00d68f;
|
|
92
|
+
}
|
|
93
|
+
.main {
|
|
94
|
+
flex: 1;
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
min-width: 0;
|
|
98
|
+
}
|
|
99
|
+
.messages {
|
|
100
|
+
flex: 1;
|
|
101
|
+
overflow-y: auto;
|
|
102
|
+
padding: 32px 24px;
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
gap: 20px;
|
|
106
|
+
scroll-behavior: smooth;
|
|
107
|
+
}
|
|
108
|
+
.message {
|
|
109
|
+
max-width: 760px;
|
|
110
|
+
padding: 14px 18px;
|
|
111
|
+
border-radius: 12px;
|
|
112
|
+
line-height: 1.6;
|
|
113
|
+
font-size: 15px;
|
|
114
|
+
animation: fadeIn 0.2s ease;
|
|
115
|
+
}
|
|
116
|
+
@keyframes fadeIn {
|
|
117
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
118
|
+
to { opacity: 1; transform: translateY(0); }
|
|
119
|
+
}
|
|
120
|
+
.message.user {
|
|
121
|
+
background: #1a1a2e;
|
|
122
|
+
color: #fff;
|
|
123
|
+
align-self: flex-end;
|
|
124
|
+
border-bottom-right-radius: 4px;
|
|
125
|
+
}
|
|
126
|
+
.message.assistant {
|
|
127
|
+
background: #fff;
|
|
128
|
+
border: 1px solid #e8ecf1;
|
|
129
|
+
align-self: flex-start;
|
|
130
|
+
border-bottom-left-radius: 4px;
|
|
131
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
|
132
|
+
}
|
|
133
|
+
.message.assistant > :first-child { margin-top: 0; }
|
|
134
|
+
.message.assistant > :last-child { margin-bottom: 0; }
|
|
135
|
+
.message.assistant p + p,
|
|
136
|
+
.message.assistant ul,
|
|
137
|
+
.message.assistant ol,
|
|
138
|
+
.message.assistant pre,
|
|
139
|
+
.message.assistant blockquote,
|
|
140
|
+
.message.assistant table { margin-top: 12px; }
|
|
141
|
+
.message.assistant h1,
|
|
142
|
+
.message.assistant h2,
|
|
143
|
+
.message.assistant h3,
|
|
144
|
+
.message.assistant h4,
|
|
145
|
+
.message.assistant h5,
|
|
146
|
+
.message.assistant h6 {
|
|
147
|
+
line-height: 1.25;
|
|
148
|
+
color: #101828;
|
|
149
|
+
margin: 14px 0 8px;
|
|
150
|
+
}
|
|
151
|
+
.message.assistant h1,
|
|
152
|
+
.message.assistant h2 { font-size: 17px; }
|
|
153
|
+
.message.assistant h3 { font-size: 16px; }
|
|
154
|
+
.message.assistant h4,
|
|
155
|
+
.message.assistant h5,
|
|
156
|
+
.message.assistant h6 { font-size: 15px; }
|
|
157
|
+
.message.assistant ul,
|
|
158
|
+
.message.assistant ol {
|
|
159
|
+
padding-left: 22px;
|
|
160
|
+
}
|
|
161
|
+
.message.assistant li + li {
|
|
162
|
+
margin-top: 4px;
|
|
163
|
+
}
|
|
164
|
+
.message.assistant a {
|
|
165
|
+
color: #0f766e;
|
|
166
|
+
text-decoration: underline;
|
|
167
|
+
text-underline-offset: 2px;
|
|
168
|
+
word-break: break-word;
|
|
169
|
+
}
|
|
170
|
+
.message.assistant code {
|
|
171
|
+
font-family: "SF Mono", "Fira Code", monospace;
|
|
172
|
+
font-size: 13px;
|
|
173
|
+
background: #f3f5f7;
|
|
174
|
+
color: #0f172a;
|
|
175
|
+
padding: 2px 5px;
|
|
176
|
+
border-radius: 5px;
|
|
177
|
+
}
|
|
178
|
+
.message.assistant pre {
|
|
179
|
+
overflow-x: auto;
|
|
180
|
+
padding: 12px 14px;
|
|
181
|
+
border-radius: 8px;
|
|
182
|
+
background: #0f172a;
|
|
183
|
+
color: #e2e8f0;
|
|
184
|
+
font-size: 13px;
|
|
185
|
+
line-height: 1.55;
|
|
186
|
+
}
|
|
187
|
+
.message.assistant pre code {
|
|
188
|
+
display: block;
|
|
189
|
+
background: transparent;
|
|
190
|
+
color: inherit;
|
|
191
|
+
padding: 0;
|
|
192
|
+
border-radius: 0;
|
|
193
|
+
white-space: pre;
|
|
194
|
+
tab-size: 2;
|
|
195
|
+
}
|
|
196
|
+
.message.assistant pre code[class*="language-"] .c,
|
|
197
|
+
.message.assistant pre code[class*="language-"] .cm,
|
|
198
|
+
.message.assistant pre code[class*="language-"] .c1 {
|
|
199
|
+
color: #94a3b8;
|
|
200
|
+
}
|
|
201
|
+
.message.assistant pre code[class*="language-"] .k,
|
|
202
|
+
.message.assistant pre code[class*="language-"] .kd,
|
|
203
|
+
.message.assistant pre code[class*="language-"] .kn {
|
|
204
|
+
color: #f472b6;
|
|
205
|
+
}
|
|
206
|
+
.message.assistant pre code[class*="language-"] .s,
|
|
207
|
+
.message.assistant pre code[class*="language-"] .s1,
|
|
208
|
+
.message.assistant pre code[class*="language-"] .s2 {
|
|
209
|
+
color: #86efac;
|
|
210
|
+
}
|
|
211
|
+
.message.assistant pre code[class*="language-"] .nf,
|
|
212
|
+
.message.assistant pre code[class*="language-"] .nc {
|
|
213
|
+
color: #7dd3fc;
|
|
214
|
+
}
|
|
215
|
+
.message.assistant pre code[class*="language-"] .mi,
|
|
216
|
+
.message.assistant pre code[class*="language-"] .mf {
|
|
217
|
+
color: #fdba74;
|
|
218
|
+
}
|
|
219
|
+
.message.assistant blockquote {
|
|
220
|
+
border-left: 3px solid #00d68f;
|
|
221
|
+
padding-left: 12px;
|
|
222
|
+
color: #52606d;
|
|
223
|
+
}
|
|
224
|
+
.message.assistant table {
|
|
225
|
+
width: 100%;
|
|
226
|
+
border-collapse: collapse;
|
|
227
|
+
font-size: 14px;
|
|
228
|
+
}
|
|
229
|
+
.message.assistant th,
|
|
230
|
+
.message.assistant td {
|
|
231
|
+
border: 1px solid #e5e7eb;
|
|
232
|
+
padding: 8px 10px;
|
|
233
|
+
text-align: left;
|
|
234
|
+
vertical-align: top;
|
|
235
|
+
}
|
|
236
|
+
.message.assistant th {
|
|
237
|
+
background: #f8fafc;
|
|
238
|
+
font-weight: 600;
|
|
239
|
+
}
|
|
240
|
+
.message.assistant img {
|
|
241
|
+
max-width: 100%;
|
|
242
|
+
height: auto;
|
|
243
|
+
border-radius: 8px;
|
|
244
|
+
}
|
|
245
|
+
.reasoning {
|
|
246
|
+
max-width: 760px;
|
|
247
|
+
align-self: flex-start;
|
|
248
|
+
font-size: 13px;
|
|
249
|
+
color: #6b7a8f;
|
|
250
|
+
background: #f0f2f5;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
padding: 10px 14px;
|
|
253
|
+
margin-top: -12px;
|
|
254
|
+
border-left: 3px solid #00d68f;
|
|
255
|
+
}
|
|
256
|
+
.reasoning summary {
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
font-weight: 500;
|
|
259
|
+
color: #4a5a6f;
|
|
260
|
+
user-select: none;
|
|
261
|
+
}
|
|
262
|
+
.reasoning .content {
|
|
263
|
+
margin-top: 8px;
|
|
264
|
+
font-size: 13px;
|
|
265
|
+
line-height: 1.5;
|
|
266
|
+
color: #5a6a7f;
|
|
267
|
+
white-space: pre-wrap;
|
|
268
|
+
}
|
|
269
|
+
.tool-call {
|
|
270
|
+
max-width: 760px;
|
|
271
|
+
align-self: flex-start;
|
|
272
|
+
font-size: 13px;
|
|
273
|
+
background: #f8f9fc;
|
|
274
|
+
border: 1px solid #e8ecf1;
|
|
275
|
+
border-radius: 8px;
|
|
276
|
+
padding: 10px 14px;
|
|
277
|
+
margin-top: -12px;
|
|
278
|
+
}
|
|
279
|
+
.tool-call summary {
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
font-weight: 500;
|
|
282
|
+
color: #4a5a6f;
|
|
283
|
+
user-select: none;
|
|
284
|
+
}
|
|
285
|
+
.tool-call .tool-name {
|
|
286
|
+
color: #00d68f;
|
|
287
|
+
font-weight: 600;
|
|
288
|
+
font-family: "SF Mono", "Fira Code", "Fira Mono", monospace;
|
|
289
|
+
font-size: 12px;
|
|
290
|
+
}
|
|
291
|
+
.tool-call pre {
|
|
292
|
+
margin-top: 8px;
|
|
293
|
+
background: #1a1a2e;
|
|
294
|
+
color: #e0e6ed;
|
|
295
|
+
padding: 10px 12px;
|
|
296
|
+
border-radius: 6px;
|
|
297
|
+
font-size: 12px;
|
|
298
|
+
overflow-x: auto;
|
|
299
|
+
font-family: "SF Mono", "Fira Code", monospace;
|
|
300
|
+
}
|
|
301
|
+
.input-area {
|
|
302
|
+
border-top: 1px solid #e8ecf1;
|
|
303
|
+
padding: 16px 24px;
|
|
304
|
+
background: #fff;
|
|
305
|
+
}
|
|
306
|
+
.input-area form {
|
|
307
|
+
display: flex;
|
|
308
|
+
gap: 8px;
|
|
309
|
+
max-width: 800px;
|
|
310
|
+
margin: 0 auto;
|
|
311
|
+
}
|
|
312
|
+
.input-area input[type="text"] {
|
|
313
|
+
flex: 1;
|
|
314
|
+
padding: 12px 18px;
|
|
315
|
+
border: 1px solid #e0e4e8;
|
|
316
|
+
border-radius: 10px;
|
|
317
|
+
font-size: 15px;
|
|
318
|
+
outline: none;
|
|
319
|
+
transition: border 0.15s, box-shadow 0.15s;
|
|
320
|
+
}
|
|
321
|
+
.input-area input[type="text"]:focus {
|
|
322
|
+
border-color: #00d68f;
|
|
323
|
+
box-shadow: 0 0 0 3px rgba(0,214,143,0.12);
|
|
324
|
+
}
|
|
325
|
+
.input-area button {
|
|
326
|
+
background: #1a1a2e;
|
|
327
|
+
color: #fff;
|
|
328
|
+
border: none;
|
|
329
|
+
border-radius: 10px;
|
|
330
|
+
padding: 12px 24px;
|
|
331
|
+
font-size: 15px;
|
|
332
|
+
font-weight: 500;
|
|
333
|
+
cursor: pointer;
|
|
334
|
+
transition: background 0.15s;
|
|
335
|
+
}
|
|
336
|
+
.input-area button:hover { background: #2d2d4a; }
|
|
337
|
+
.input-area button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
338
|
+
.empty-state {
|
|
339
|
+
flex: 1;
|
|
340
|
+
display: flex;
|
|
341
|
+
flex-direction: column;
|
|
342
|
+
align-items: center;
|
|
343
|
+
justify-content: center;
|
|
344
|
+
gap: 12px;
|
|
345
|
+
color: #8899b4;
|
|
346
|
+
}
|
|
347
|
+
.empty-state .icon { font-size: 48px; opacity: 0.4; }
|
|
348
|
+
.empty-state p { font-size: 16px; }
|
|
349
|
+
.runtime-bar {
|
|
350
|
+
display: flex;
|
|
351
|
+
align-items: center;
|
|
352
|
+
gap: 16px;
|
|
353
|
+
padding: 6px 24px;
|
|
354
|
+
background: #f0f2f5;
|
|
355
|
+
border-top: 1px solid #e0e4e8;
|
|
356
|
+
font-size: 11px;
|
|
357
|
+
color: #8899b4;
|
|
358
|
+
font-family: "SF Mono", "Fira Code", monospace;
|
|
359
|
+
}
|
|
360
|
+
.runtime-bar .dot {
|
|
361
|
+
width: 6px; height: 6px;
|
|
362
|
+
border-radius: 50%;
|
|
363
|
+
background: #00d68f;
|
|
364
|
+
display: inline-block;
|
|
365
|
+
}
|
|
366
|
+
.runtime-bar .dot.idle { background: #8899b4; }
|
|
367
|
+
.runtime-bar span { white-space: nowrap; }
|
|
368
|
+
.streaming-cursor::after {
|
|
369
|
+
content: "▊";
|
|
370
|
+
animation: blink 0.8s step-end infinite;
|
|
371
|
+
color: #1a1a2e;
|
|
372
|
+
margin-left: 2px;
|
|
373
|
+
}
|
|
374
|
+
@keyframes blink {
|
|
375
|
+
50% { opacity: 0; }
|
|
376
|
+
}
|
|
377
|
+
@media (max-width: 768px) {
|
|
378
|
+
.sidebar { display: none; }
|
|
379
|
+
.messages { padding: 16px; }
|
|
380
|
+
.input-area { padding: 12px 16px; }
|
|
381
|
+
}
|
|
382
|
+
</style>
|
|
383
|
+
</head>
|
|
384
|
+
<body>
|
|
385
|
+
<div class="layout">
|
|
386
|
+
<div class="sidebar">
|
|
387
|
+
<div class="brand">
|
|
388
|
+
<%= image_tag "llm.png", alt: "llm.rb" %>
|
|
389
|
+
<div>
|
|
390
|
+
<h1>llm.rb</h1>
|
|
391
|
+
<span>Agents</span>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
<%= link_to "+ New Agent", agents_path, method: :post,
|
|
395
|
+
class: "new-agent" %>
|
|
396
|
+
<nav>
|
|
397
|
+
<% if @agents %>
|
|
398
|
+
<% @agents.each do |agent| %>
|
|
399
|
+
<%= link_to agent.title_or_default, agent_path(agent),
|
|
400
|
+
class: "agent-link #{'active' if agent == @agent}" %>
|
|
401
|
+
<% end %>
|
|
402
|
+
<% end %>
|
|
403
|
+
</nav>
|
|
404
|
+
</div>
|
|
405
|
+
<div class="main">
|
|
406
|
+
<%= yield %>
|
|
407
|
+
</div>
|
|
408
|
+
</div>
|
|
409
|
+
</body>
|
|
410
|
+
</html>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<div class="message <%= msg.role %>">
|
|
2
|
+
<%= RailsLLM.markdown(msg.content).html_safe %>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<% if msg.reasoning_content.present? %>
|
|
6
|
+
<details class="reasoning">
|
|
7
|
+
<summary>Reasoning</summary>
|
|
8
|
+
<div class="content"><%= msg.reasoning_content %></div>
|
|
9
|
+
</details>
|
|
10
|
+
<% end %>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<div class="empty-state">
|
|
2
|
+
<div class="icon">⚡</div>
|
|
3
|
+
<p>Start a conversation below.</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="input-area">
|
|
7
|
+
<%= form_tag agents_path, method: :post, id: "new-agent-form" do %>
|
|
8
|
+
<%= text_field_tag :prompt, nil,
|
|
9
|
+
placeholder: "Ask anything...",
|
|
10
|
+
autofocus: true,
|
|
11
|
+
autocomplete: "off" %>
|
|
12
|
+
<%= submit_tag "Start Chat", data: { disable_with: "..." } %>
|
|
13
|
+
<% end %>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="runtime-bar">
|
|
17
|
+
<span class="dot idle"></span>
|
|
18
|
+
<span>llm.rb</span>
|
|
19
|
+
<span>0 tokens</span>
|
|
20
|
+
<span>0 messages</span>
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="messages" id="messages">
|
|
2
|
+
<% if @messages.any? %>
|
|
3
|
+
<% @messages.each do |msg| %>
|
|
4
|
+
<%= render partial: "message", locals: { msg: } %>
|
|
5
|
+
<% end %>
|
|
6
|
+
<% else %>
|
|
7
|
+
<div class="empty-state">
|
|
8
|
+
<div class="icon">⚡</div>
|
|
9
|
+
<p>Start a conversation by typing below.</p>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<div class="input-area">
|
|
15
|
+
<%= form_tag ask_agent_path(@agent), method: :post, id: "ask-form", data: { turbo: false } do %>
|
|
16
|
+
<%= text_field_tag :prompt, nil,
|
|
17
|
+
placeholder: "Ask anything...",
|
|
18
|
+
autofocus: true,
|
|
19
|
+
required: true,
|
|
20
|
+
autocomplete: "off" %>
|
|
21
|
+
<%= submit_tag "Send", data: { disable_with: "..." } %>
|
|
22
|
+
<% end %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="runtime-bar">
|
|
26
|
+
<span class="dot idle"></span>
|
|
27
|
+
<span>llm.rb</span>
|
|
28
|
+
<span><%= @agent.usage.total_tokens || 0 %> tokens</span>
|
|
29
|
+
<span><%= @messages.size %> messages</span>
|
|
30
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module RailsLLM
|
|
7
|
+
##
|
|
8
|
+
# Install generator for rails-llm.rb.
|
|
9
|
+
# Creates the Agent model, migration, initializer, and engine route.
|
|
10
|
+
# @usage rails generate rails_llm:install
|
|
11
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
12
|
+
include ::ActiveRecord::Generators::Migration
|
|
13
|
+
namespace "rails_llm:install"
|
|
14
|
+
source_root File.expand_path("templates", __dir__)
|
|
15
|
+
desc "Install rails-llm.rb — creates the model, migration, initializer, and engine route."
|
|
16
|
+
|
|
17
|
+
def create_agent_model
|
|
18
|
+
template "agent_model.rb.tt", "app/models/rails_llm/agent.rb"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_knowledge_tool
|
|
22
|
+
template "knowledge_tool.rb.tt", "app/tools/rails_llm/knowledge_tool.rb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def create_install_migration
|
|
26
|
+
migration_template "migration.rb.tt", "db/migrate/create_rails_llm_agents.rb"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_initializer
|
|
30
|
+
template "initializer.rb.tt", "config/initializers/rails_llm.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mount_engine
|
|
34
|
+
route %(mount RailsLLM::Engine => "/ai")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def migration_version
|
|
40
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsLLM
|
|
4
|
+
class Agent < ApplicationRecord
|
|
5
|
+
self.table_name = "rails_llm_agents"
|
|
6
|
+
|
|
7
|
+
acts_as_agent provider: :set_provider, context: :set_context
|
|
8
|
+
tools { [RailsLLM::KnowledgeTool] }
|
|
9
|
+
scope :ordered, -> { order(updated_at: :desc) }
|
|
10
|
+
|
|
11
|
+
def title_or_default
|
|
12
|
+
title.presence || "Agent ##{id}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def set_provider
|
|
18
|
+
LLM.deepseek(key: ENV["DEEPSEEK_API_KEY"])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set_context
|
|
22
|
+
{model: "deepseek-v4-flash"}
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module RailsLLM
|
|
7
|
+
##
|
|
8
|
+
# The {RailsLLM::KnowledgeTool} tool provides the LLM with
|
|
9
|
+
# documentation about rails-llm, llm.rb, and mruby-llm.
|
|
10
|
+
class KnowledgeTool < LLM::Tool
|
|
11
|
+
name "rails-llm-knowledge"
|
|
12
|
+
description "Returns rails-llm, llm.rb or mruby-llm documentation"
|
|
13
|
+
parameter :topic, Enum["rails-llm", "llm.rb", "mruby-llm"], "The knowledge topic"
|
|
14
|
+
required %i[topic]
|
|
15
|
+
|
|
16
|
+
##
|
|
17
|
+
# Provides project documentation
|
|
18
|
+
# @return [Hash]
|
|
19
|
+
def call(topic:)
|
|
20
|
+
case topic
|
|
21
|
+
when "rails-llm" then {directions:, documentation: fetch(rails_llm_resources)}
|
|
22
|
+
when "llm.rb" then {directions:, documentation: fetch(llmrb_resources)}
|
|
23
|
+
when "mruby-llm" then {directions:, documentation: fetch(mruby_llm_resources)}
|
|
24
|
+
else {error: "unknown topic: #{topic}"}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def fetch(resources)
|
|
31
|
+
resources.each_with_object({}) do |(key, url), docs|
|
|
32
|
+
res = Net::HTTP.get_response(URI.parse(url))
|
|
33
|
+
docs[key] = res.body
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def rails_llm_resources
|
|
38
|
+
{
|
|
39
|
+
"readme" => "https://raw.githubusercontent.com/llmrb/rails-llm/main/README.md"
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def llmrb_resources
|
|
44
|
+
{
|
|
45
|
+
"readme" => "https://raw.githubusercontent.com/llmrb/llm.rb/main/README.md",
|
|
46
|
+
"deepdive" => "https://raw.githubusercontent.com/llmrb/llm.rb/main/resources/deepdive.md",
|
|
47
|
+
"changelog" => "https://raw.githubusercontent.com/llmrb/llm.rb/main/CHANGELOG.md"
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mruby_llm_resources
|
|
52
|
+
{
|
|
53
|
+
"readme" => "https://raw.githubusercontent.com/llmrb/mruby-llm/main/README.md"
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def directions
|
|
58
|
+
"Reference links from the associated document in your response"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
|
|
5
|
+
module RailsLLM
|
|
6
|
+
##
|
|
7
|
+
# Model generator for rails-llm.
|
|
8
|
+
# Creates an Agent model with acts_as_agent.
|
|
9
|
+
# @usage rails generate rails_llm:model
|
|
10
|
+
class ModelGenerator < ::Rails::Generators::Base
|
|
11
|
+
namespace "rails_llm:model"
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
desc "Generate an Agent model with acts_as_agent."
|
|
14
|
+
|
|
15
|
+
def create_agent_model
|
|
16
|
+
template "agent_model.rb.tt", "app/models/rails_llm/agent.rb"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def show_instructions
|
|
20
|
+
say ""
|
|
21
|
+
say "Done! Next steps:", :green
|
|
22
|
+
say " 1. Run `rails db:migrate`", :bold
|
|
23
|
+
say " 2. Visit http://localhost:3000/ai/agents", :bold
|
|
24
|
+
say " 3. Set DEEPSEEK_API_KEY in your environment", :bold
|
|
25
|
+
say ""
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|