active_agent_rails 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 +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +151 -0
- data/Rakefile +12 -0
- data/app/controllers/active_agent/chats_controller.rb +84 -0
- data/app/helpers/active_agent/chat_helper.rb +427 -0
- data/config/routes.rb +6 -0
- data/lib/active_agent/base.rb +121 -0
- data/lib/active_agent/configuration.rb +30 -0
- data/lib/active_agent/engine.rb +16 -0
- data/lib/active_agent/memory/active_record.rb +68 -0
- data/lib/active_agent/memory/base.rb +42 -0
- data/lib/active_agent/memory/in_memory.rb +29 -0
- data/lib/active_agent/provider.rb +90 -0
- data/lib/active_agent/providers/anthropic.rb +208 -0
- data/lib/active_agent/providers/gemini.rb +169 -0
- data/lib/active_agent/providers/openai.rb +178 -0
- data/lib/active_agent/tool.rb +37 -0
- data/lib/active_agent/version.rb +5 -0
- data/lib/active_agent.rb +25 -0
- data/lib/active_agent_rails.rb +3 -0
- data/lib/generators/active_agent/agent/agent_generator.rb +15 -0
- data/lib/generators/active_agent/agent/templates/agent.rb.erb +22 -0
- data/lib/generators/active_agent/install/install_generator.rb +31 -0
- data/lib/generators/active_agent/install/templates/active_agent.rb +15 -0
- data/lib/generators/active_agent/install/templates/create_active_agent_messages.rb +18 -0
- metadata +144 -0
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveAgent
|
|
4
|
+
module ChatHelper
|
|
5
|
+
def active_agent_chat_widget(agent:, conversation_id:, title: "AI Assistant", placeholder: "Ask me anything...")
|
|
6
|
+
agent_name = agent.to_s.underscore
|
|
7
|
+
widget_id = "active_agent_#{agent_name}_#{conversation_id.to_s.parameterize.underscore}"
|
|
8
|
+
|
|
9
|
+
raw <<~HTML
|
|
10
|
+
<div id="#{widget_id}" class="active-agent-widget-root">
|
|
11
|
+
<!-- Floating Chat Launcher Button -->
|
|
12
|
+
<button class="active-agent-launcher" onclick="toggleActiveAgentChat('#{widget_id}')">
|
|
13
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.8" stroke="currentColor" class="active-agent-icon-open">
|
|
14
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.58 16.24 3 14.201 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" />
|
|
15
|
+
</svg>
|
|
16
|
+
</button>
|
|
17
|
+
|
|
18
|
+
<!-- Chat Window Box -->
|
|
19
|
+
<div class="active-agent-window hidden">
|
|
20
|
+
<div class="active-agent-header">
|
|
21
|
+
<div class="active-agent-header-info">
|
|
22
|
+
<span class="active-agent-header-title">#{title}</span>
|
|
23
|
+
<span class="active-agent-header-status"><span class="status-dot"></span> Online</span>
|
|
24
|
+
</div>
|
|
25
|
+
<button class="active-agent-close" onclick="toggleActiveAgentChat('#{widget_id}')">×</button>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="active-agent-messages">
|
|
29
|
+
<div class="active-agent-message assistant">
|
|
30
|
+
<div class="message-content">Hello! How can I help you today?</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="active-agent-typing hidden">
|
|
35
|
+
<span></span>
|
|
36
|
+
<span></span>
|
|
37
|
+
<span></span>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<form class="active-agent-form" onsubmit="submitActiveAgentMessage(event, '#{widget_id}', '#{agent_name}', '#{conversation_id}')">
|
|
41
|
+
<input type="text" class="active-agent-input" placeholder="#{placeholder}" autocomplete="off" required />
|
|
42
|
+
<button type="submit" class="active-agent-send">
|
|
43
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
44
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
|
|
45
|
+
</svg>
|
|
46
|
+
</button>
|
|
47
|
+
</form>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.active-agent-widget-root {
|
|
53
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
54
|
+
position: fixed;
|
|
55
|
+
bottom: 24px;
|
|
56
|
+
right: 24px;
|
|
57
|
+
z-index: 9999;
|
|
58
|
+
}
|
|
59
|
+
.active-agent-launcher {
|
|
60
|
+
width: 56px;
|
|
61
|
+
height: 56px;
|
|
62
|
+
border-radius: 28px;
|
|
63
|
+
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
|
|
64
|
+
color: #ffffff;
|
|
65
|
+
border: none;
|
|
66
|
+
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.4);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
display: flex;
|
|
69
|
+
align-items: center;
|
|
70
|
+
justify-content: center;
|
|
71
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
72
|
+
}
|
|
73
|
+
.active-agent-launcher:hover {
|
|
74
|
+
transform: scale(1.08) rotate(5deg);
|
|
75
|
+
box-shadow: 0 6px 16px rgba(79, 70, 229, 0.5);
|
|
76
|
+
}
|
|
77
|
+
.active-agent-icon-open {
|
|
78
|
+
width: 28px;
|
|
79
|
+
height: 28px;
|
|
80
|
+
}
|
|
81
|
+
.active-agent-window {
|
|
82
|
+
position: fixed;
|
|
83
|
+
bottom: 96px;
|
|
84
|
+
right: 24px;
|
|
85
|
+
width: 380px;
|
|
86
|
+
height: 520px;
|
|
87
|
+
max-height: 80vh;
|
|
88
|
+
max-width: 90vw;
|
|
89
|
+
background: rgba(30, 41, 59, 0.95);
|
|
90
|
+
backdrop-filter: blur(12px);
|
|
91
|
+
-webkit-backdrop-filter: blur(12px);
|
|
92
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
93
|
+
border-radius: 16px;
|
|
94
|
+
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
99
|
+
}
|
|
100
|
+
.active-agent-window.hidden {
|
|
101
|
+
display: none;
|
|
102
|
+
}
|
|
103
|
+
.active-agent-header {
|
|
104
|
+
background: rgba(15, 23, 42, 0.5);
|
|
105
|
+
padding: 16px;
|
|
106
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
107
|
+
display: flex;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
align-items: center;
|
|
110
|
+
}
|
|
111
|
+
.active-agent-header-info {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
}
|
|
115
|
+
.active-agent-header-title {
|
|
116
|
+
color: #ffffff;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
font-size: 15px;
|
|
119
|
+
}
|
|
120
|
+
.active-agent-header-status {
|
|
121
|
+
font-size: 11px;
|
|
122
|
+
color: #10b981;
|
|
123
|
+
display: flex;
|
|
124
|
+
align-items: center;
|
|
125
|
+
margin-top: 2px;
|
|
126
|
+
}
|
|
127
|
+
.status-dot {
|
|
128
|
+
width: 6px;
|
|
129
|
+
height: 6px;
|
|
130
|
+
background: #10b981;
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
display: inline-block;
|
|
133
|
+
margin-right: 4px;
|
|
134
|
+
box-shadow: 0 0 6px #10b981;
|
|
135
|
+
}
|
|
136
|
+
.active-agent-close {
|
|
137
|
+
background: transparent;
|
|
138
|
+
border: none;
|
|
139
|
+
color: #94a3b8;
|
|
140
|
+
font-size: 24px;
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
padding: 0;
|
|
143
|
+
line-height: 1;
|
|
144
|
+
transition: color 0.2s;
|
|
145
|
+
}
|
|
146
|
+
.active-agent-close:hover {
|
|
147
|
+
color: #ffffff;
|
|
148
|
+
}
|
|
149
|
+
.active-agent-messages {
|
|
150
|
+
flex: 1;
|
|
151
|
+
padding: 16px;
|
|
152
|
+
overflow-y: auto;
|
|
153
|
+
display: flex;
|
|
154
|
+
flex-direction: column;
|
|
155
|
+
gap: 12px;
|
|
156
|
+
}
|
|
157
|
+
.active-agent-message {
|
|
158
|
+
max-width: 85%;
|
|
159
|
+
padding: 10px 14px;
|
|
160
|
+
border-radius: 12px;
|
|
161
|
+
font-size: 14px;
|
|
162
|
+
line-height: 1.45;
|
|
163
|
+
word-wrap: break-word;
|
|
164
|
+
}
|
|
165
|
+
.active-agent-message.user {
|
|
166
|
+
background: #4f46e5;
|
|
167
|
+
color: #ffffff;
|
|
168
|
+
align-self: flex-end;
|
|
169
|
+
border-bottom-right-radius: 2px;
|
|
170
|
+
box-shadow: 0 2px 6px rgba(79, 70, 229, 0.2);
|
|
171
|
+
}
|
|
172
|
+
.active-agent-message.assistant {
|
|
173
|
+
background: rgba(51, 65, 85, 0.7);
|
|
174
|
+
color: #f1f5f9;
|
|
175
|
+
align-self: flex-start;
|
|
176
|
+
border-bottom-left-radius: 2px;
|
|
177
|
+
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
178
|
+
}
|
|
179
|
+
.active-agent-message code {
|
|
180
|
+
font-family: monospace;
|
|
181
|
+
background: rgba(0,0,0,0.3);
|
|
182
|
+
padding: 2px 4px;
|
|
183
|
+
border-radius: 4px;
|
|
184
|
+
font-size: 12px;
|
|
185
|
+
color: #f472b6;
|
|
186
|
+
}
|
|
187
|
+
.active-agent-message pre {
|
|
188
|
+
background: rgba(0,0,0,0.4);
|
|
189
|
+
padding: 8px;
|
|
190
|
+
border-radius: 6px;
|
|
191
|
+
overflow-x: auto;
|
|
192
|
+
margin: 6px 0;
|
|
193
|
+
}
|
|
194
|
+
.active-agent-message pre code {
|
|
195
|
+
background: transparent;
|
|
196
|
+
padding: 0;
|
|
197
|
+
color: #38bdf8;
|
|
198
|
+
}
|
|
199
|
+
.active-agent-typing {
|
|
200
|
+
align-self: flex-start;
|
|
201
|
+
margin: 0 16px 12px 16px;
|
|
202
|
+
padding: 8px 12px;
|
|
203
|
+
background: rgba(51, 65, 85, 0.5);
|
|
204
|
+
border-radius: 12px;
|
|
205
|
+
display: flex;
|
|
206
|
+
gap: 4px;
|
|
207
|
+
}
|
|
208
|
+
.active-agent-typing.hidden {
|
|
209
|
+
display: none !important;
|
|
210
|
+
}
|
|
211
|
+
.active-agent-typing span {
|
|
212
|
+
width: 6px;
|
|
213
|
+
height: 6px;
|
|
214
|
+
background: #94a3b8;
|
|
215
|
+
border-radius: 50%;
|
|
216
|
+
animation: bounce 1.4s infinite ease-in-out both;
|
|
217
|
+
}
|
|
218
|
+
.active-agent-typing span:nth-child(1) { animation-delay: -0.32s; }
|
|
219
|
+
.active-agent-typing span:nth-child(2) { animation-delay: -0.16s; }
|
|
220
|
+
@keyframes bounce {
|
|
221
|
+
0%, 80%, 100% { transform: scale(0); }
|
|
222
|
+
40% { transform: scale(1.0); }
|
|
223
|
+
}
|
|
224
|
+
.active-agent-form {
|
|
225
|
+
padding: 12px;
|
|
226
|
+
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
227
|
+
background: rgba(15, 23, 42, 0.4);
|
|
228
|
+
display: flex;
|
|
229
|
+
gap: 8px;
|
|
230
|
+
}
|
|
231
|
+
.active-agent-input {
|
|
232
|
+
flex: 1;
|
|
233
|
+
background: rgba(15, 23, 42, 0.6);
|
|
234
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
235
|
+
border-radius: 8px;
|
|
236
|
+
color: #ffffff;
|
|
237
|
+
padding: 10px 14px;
|
|
238
|
+
font-size: 13.5px;
|
|
239
|
+
outline: none;
|
|
240
|
+
transition: border-color 0.2s;
|
|
241
|
+
}
|
|
242
|
+
.active-agent-input:focus {
|
|
243
|
+
border-color: #6366f1;
|
|
244
|
+
}
|
|
245
|
+
.active-agent-send {
|
|
246
|
+
background: #4f46e5;
|
|
247
|
+
color: #ffffff;
|
|
248
|
+
border: none;
|
|
249
|
+
width: 38px;
|
|
250
|
+
height: 38px;
|
|
251
|
+
border-radius: 8px;
|
|
252
|
+
cursor: pointer;
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: center;
|
|
255
|
+
justify-content: center;
|
|
256
|
+
transition: background-color 0.2s;
|
|
257
|
+
}
|
|
258
|
+
.active-agent-send:hover {
|
|
259
|
+
background: #4338ca;
|
|
260
|
+
}
|
|
261
|
+
.active-agent-send svg {
|
|
262
|
+
width: 18px;
|
|
263
|
+
height: 18px;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
|
266
|
+
|
|
267
|
+
<script>
|
|
268
|
+
function toggleActiveAgentChat(widgetId) {
|
|
269
|
+
const root = document.getElementById(widgetId);
|
|
270
|
+
const windowEl = root.querySelector('.active-agent-window');
|
|
271
|
+
windowEl.classList.toggle('hidden');
|
|
272
|
+
if (!windowEl.classList.contains('hidden')) {
|
|
273
|
+
// Scroll to bottom on open
|
|
274
|
+
const messagesEl = root.querySelector('.active-agent-messages');
|
|
275
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
276
|
+
|
|
277
|
+
// Load historical messages from the server
|
|
278
|
+
fetchActiveAgentHistory(widgetId, '#{agent_name}', '#{conversation_id}');
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let historyLoaded = {};
|
|
283
|
+
|
|
284
|
+
function fetchActiveAgentHistory(widgetId, agentName, conversationId) {
|
|
285
|
+
if (historyLoaded[widgetId]) return;
|
|
286
|
+
const root = document.getElementById(widgetId);
|
|
287
|
+
const messagesEl = root.querySelector('.active-agent-messages');
|
|
288
|
+
|
|
289
|
+
fetch(`/active_agent/chats?agent=${agentName}&conversation_id=${conversationId}`)
|
|
290
|
+
.then(response => response.json())
|
|
291
|
+
.then(data => {
|
|
292
|
+
if (data.messages && data.messages.length > 0) {
|
|
293
|
+
messagesEl.innerHTML = '';
|
|
294
|
+
data.messages.forEach(msg => {
|
|
295
|
+
if (msg.role === 'user' || msg.role === 'assistant') {
|
|
296
|
+
appendActiveAgentMessage(widgetId, msg.content, msg.role);
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
300
|
+
historyLoaded[widgetId] = true;
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
.catch(err => console.error("Error loading chat history:", err));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function appendActiveAgentMessage(widgetId, content, role) {
|
|
307
|
+
const root = document.getElementById(widgetId);
|
|
308
|
+
const messagesEl = root.querySelector('.active-agent-messages');
|
|
309
|
+
|
|
310
|
+
const msgDiv = document.createElement('div');
|
|
311
|
+
msgDiv.className = `active-agent-message ${role}`;
|
|
312
|
+
|
|
313
|
+
// Simple markdown formatter for bold and code blocks
|
|
314
|
+
let formatted = content
|
|
315
|
+
.replace(/&/g, "&")
|
|
316
|
+
.replace(/</g, "<")
|
|
317
|
+
.replace(/>/g, ">")
|
|
318
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
319
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
320
|
+
.replace(/\n/g, '<br/>');
|
|
321
|
+
|
|
322
|
+
msgDiv.innerHTML = `<div class="message-content">${formatted}</div>`;
|
|
323
|
+
messagesEl.appendChild(msgDiv);
|
|
324
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
325
|
+
return msgDiv;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function submitActiveAgentMessage(event, widgetId, agentName, conversationId) {
|
|
329
|
+
event.preventDefault();
|
|
330
|
+
const root = document.getElementById(widgetId);
|
|
331
|
+
const inputEl = root.querySelector('.active-agent-input');
|
|
332
|
+
const typingEl = root.querySelector('.active-agent-typing');
|
|
333
|
+
const text = inputEl.value.trim();
|
|
334
|
+
if (!text) return;
|
|
335
|
+
|
|
336
|
+
inputEl.value = '';
|
|
337
|
+
|
|
338
|
+
// Add user message
|
|
339
|
+
appendActiveAgentMessage(widgetId, text, 'user');
|
|
340
|
+
|
|
341
|
+
// Show typing indicator
|
|
342
|
+
typingEl.classList.remove('hidden');
|
|
343
|
+
|
|
344
|
+
// Send via fetch SSE
|
|
345
|
+
fetch('/active_agent/chats', {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
headers: {
|
|
348
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
349
|
+
'Accept': 'text/event-stream'
|
|
350
|
+
},
|
|
351
|
+
body: new URLSearchParams({
|
|
352
|
+
agent: agentName,
|
|
353
|
+
conversation_id: conversationId,
|
|
354
|
+
message: text
|
|
355
|
+
})
|
|
356
|
+
}).then(response => {
|
|
357
|
+
typingEl.classList.add('hidden');
|
|
358
|
+
|
|
359
|
+
if (!response.body) {
|
|
360
|
+
// Fallback for non-live response
|
|
361
|
+
return response.json().then(data => {
|
|
362
|
+
if (data.reply) {
|
|
363
|
+
appendActiveAgentMessage(widgetId, data.reply, 'assistant');
|
|
364
|
+
} else if (data.error) {
|
|
365
|
+
appendActiveAgentMessage(widgetId, "Error: " + data.error, 'assistant');
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const reader = response.body.getReader();
|
|
371
|
+
const decoder = new TextDecoder();
|
|
372
|
+
let assistantMsgDiv = null;
|
|
373
|
+
let accumulatedResponse = "";
|
|
374
|
+
|
|
375
|
+
function readStream() {
|
|
376
|
+
reader.read().then(({ done, value }) => {
|
|
377
|
+
if (done) return;
|
|
378
|
+
|
|
379
|
+
const chunkStr = decoder.decode(value, { stream: true });
|
|
380
|
+
const lines = chunkStr.split("\n");
|
|
381
|
+
|
|
382
|
+
lines.forEach(line => {
|
|
383
|
+
if (line.startsWith("data:")) {
|
|
384
|
+
try {
|
|
385
|
+
const data = JSON.parse(line.replace("data:", "").trim());
|
|
386
|
+
if (data.chunk) {
|
|
387
|
+
if (!assistantMsgDiv) {
|
|
388
|
+
assistantMsgDiv = appendActiveAgentMessage(widgetId, "", 'assistant');
|
|
389
|
+
}
|
|
390
|
+
accumulatedResponse += data.chunk;
|
|
391
|
+
|
|
392
|
+
// Update text content with simple formatting
|
|
393
|
+
let formatted = accumulatedResponse
|
|
394
|
+
.replace(/&/g, "&")
|
|
395
|
+
.replace(/</g, "<")
|
|
396
|
+
.replace(/>/g, ">")
|
|
397
|
+
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
398
|
+
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
399
|
+
.replace(/\n/g, '<br/>');
|
|
400
|
+
|
|
401
|
+
assistantMsgDiv.querySelector('.message-content').innerHTML = formatted;
|
|
402
|
+
const messagesEl = root.querySelector('.active-agent-messages');
|
|
403
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
404
|
+
} else if (data.error) {
|
|
405
|
+
appendActiveAgentMessage(widgetId, "Error: " + data.error, 'assistant');
|
|
406
|
+
}
|
|
407
|
+
} catch (e) {
|
|
408
|
+
// Incomplete JSON or other parse error
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
readStream();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
readStream();
|
|
417
|
+
}).catch(err => {
|
|
418
|
+
typingEl.classList.add('hidden');
|
|
419
|
+
appendActiveAgentMessage(widgetId, "Network error: could not connect.", 'assistant');
|
|
420
|
+
console.error(err);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
</script>
|
|
424
|
+
HTML
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/class/attribute"
|
|
4
|
+
require_relative "tool"
|
|
5
|
+
require_relative "memory/base"
|
|
6
|
+
require_relative "provider"
|
|
7
|
+
|
|
8
|
+
module ActiveAgent
|
|
9
|
+
class Base
|
|
10
|
+
class_attribute :_provider
|
|
11
|
+
class_attribute :_model
|
|
12
|
+
class_attribute :_system_prompt
|
|
13
|
+
class_attribute :_tools
|
|
14
|
+
|
|
15
|
+
# Initialize class attributes
|
|
16
|
+
self._tools = {}
|
|
17
|
+
self._system_prompt = ""
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def provider(name)
|
|
21
|
+
self._provider = name.to_sym
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def model(name)
|
|
25
|
+
self._model = name.to_s
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def system_prompt(prompt)
|
|
29
|
+
self._system_prompt = prompt.to_s
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def tool(name, description: "", &block)
|
|
33
|
+
# Duplicate to avoid mutating parent class tools
|
|
34
|
+
self._tools = _tools.dup
|
|
35
|
+
self._tools[name.to_s] = Tool.new(name, description: description, &block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def tools
|
|
39
|
+
_tools.values
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_reader :conversation_id, :memory, :provider_client
|
|
44
|
+
|
|
45
|
+
def initialize(conversation_id:, memory_store: nil)
|
|
46
|
+
@conversation_id = conversation_id
|
|
47
|
+
|
|
48
|
+
store = memory_store || ActiveAgent.configuration.memory_store
|
|
49
|
+
@memory = Memory.for(store, conversation_id)
|
|
50
|
+
|
|
51
|
+
provider_name = self.class._provider || ActiveAgent.configuration.default_provider
|
|
52
|
+
model_name = self.class._model
|
|
53
|
+
@provider_client = Provider.for(provider_name, model_name)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Core chat loop that handles automatic tool execution
|
|
57
|
+
def chat(user_input, &block)
|
|
58
|
+
memory.add_message(role: :user, content: user_input)
|
|
59
|
+
|
|
60
|
+
loop_count = 0
|
|
61
|
+
loop do
|
|
62
|
+
loop_count += 1
|
|
63
|
+
if loop_count > 10
|
|
64
|
+
raise "ActiveAgent Loop Limit: Exceeded 10 iterations of tool execution."
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Assemble system prompt + chat history
|
|
68
|
+
history = []
|
|
69
|
+
if self.class._system_prompt.present?
|
|
70
|
+
history << { role: :system, content: self.class._system_prompt }
|
|
71
|
+
end
|
|
72
|
+
history += memory.messages
|
|
73
|
+
|
|
74
|
+
# Fetch response from provider (passing block for streaming)
|
|
75
|
+
response = provider_client.chat(history, tools: self.class.tools, &block)
|
|
76
|
+
|
|
77
|
+
# Store response in memory
|
|
78
|
+
memory.add_message(
|
|
79
|
+
role: response[:role],
|
|
80
|
+
content: response[:content],
|
|
81
|
+
tool_calls: response[:tool_calls]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if response[:tool_calls]&.any?
|
|
85
|
+
response[:tool_calls].each do |tool_call|
|
|
86
|
+
tool_name = tool_call[:name]
|
|
87
|
+
tool_args = tool_call[:args] || {}
|
|
88
|
+
tool_id = tool_call[:id]
|
|
89
|
+
|
|
90
|
+
ActiveAgent.logger.info("[ActiveAgent] Executing tool '#{tool_name}' with args: #{tool_args}")
|
|
91
|
+
|
|
92
|
+
output_str = if respond_to?(tool_name)
|
|
93
|
+
begin
|
|
94
|
+
# Call the agent instance method
|
|
95
|
+
result = send(tool_name, **tool_args)
|
|
96
|
+
result.is_a?(String) ? result : result.to_json
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
ActiveAgent.logger.error("[ActiveAgent] Tool '#{tool_name}' error: #{e.message}")
|
|
99
|
+
{ error: e.message }.to_json
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
ActiveAgent.logger.warn("[ActiveAgent] Tool '#{tool_name}' not implemented on #{self.class.name}")
|
|
103
|
+
{ error: "Tool '#{tool_name}' is not implemented on the agent." }.to_json
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Store tool response
|
|
107
|
+
memory.add_message(
|
|
108
|
+
role: :tool,
|
|
109
|
+
content: output_str,
|
|
110
|
+
name: tool_name,
|
|
111
|
+
tool_call_id: tool_id
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
# Loop again to let LLM process the tool output
|
|
115
|
+
else
|
|
116
|
+
return response[:content]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
class Configuration
|
|
7
|
+
attr_accessor :gemini_api_key, :openai_api_key, :anthropic_api_key,
|
|
8
|
+
:default_provider, :memory_store, :logger
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@default_provider = :gemini
|
|
12
|
+
@memory_store = :in_memory
|
|
13
|
+
@logger = defined?(Rails) ? Rails.logger : Logger.new($stdout)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def configuration
|
|
19
|
+
@configuration ||= Configuration.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def configure
|
|
23
|
+
yield(configuration)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def logger
|
|
27
|
+
configuration.logger
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
require "rails/railtie"
|
|
5
|
+
|
|
6
|
+
module ActiveAgent
|
|
7
|
+
class Engine < ::Rails::Engine
|
|
8
|
+
isolate_namespace ActiveAgent
|
|
9
|
+
|
|
10
|
+
initializer "active_agent.helpers" do
|
|
11
|
+
ActiveSupport.on_load(:action_view) do
|
|
12
|
+
include ActiveAgent::ChatHelper
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module ActiveAgent
|
|
6
|
+
module Memory
|
|
7
|
+
# AR Model defined dynamically or referenced if exists
|
|
8
|
+
class MessageRecord < ::ActiveRecord::Base
|
|
9
|
+
self.table_name = "active_agent_messages"
|
|
10
|
+
|
|
11
|
+
# Handle serialization dynamically for compatibility with older/newer Rails
|
|
12
|
+
if respond_to?(:serialize)
|
|
13
|
+
begin
|
|
14
|
+
serialize :tool_calls, coder: JSON
|
|
15
|
+
rescue StandardError
|
|
16
|
+
serialize :tool_calls, JSON
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ActiveRecord < Base
|
|
22
|
+
def messages
|
|
23
|
+
ensure_table_exists!
|
|
24
|
+
|
|
25
|
+
MessageRecord.where(conversation_id: conversation_id.to_s)
|
|
26
|
+
.order(:created_at, :id)
|
|
27
|
+
.map do |record|
|
|
28
|
+
msg = {
|
|
29
|
+
role: record.role,
|
|
30
|
+
content: record.content
|
|
31
|
+
}
|
|
32
|
+
if record.tool_calls.present?
|
|
33
|
+
msg[:tool_calls] = record.tool_calls.map { |tc| tc.is_a?(Hash) ? tc.deep_symbolize_keys : tc }
|
|
34
|
+
end
|
|
35
|
+
msg[:tool_call_id] = record.tool_call_id if record.tool_call_id.present?
|
|
36
|
+
msg[:name] = record.name if record.name.present?
|
|
37
|
+
msg
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add_message(role:, content:, **options)
|
|
42
|
+
ensure_table_exists!
|
|
43
|
+
|
|
44
|
+
MessageRecord.create!(
|
|
45
|
+
conversation_id: conversation_id.to_s,
|
|
46
|
+
role: role.to_s,
|
|
47
|
+
content: content,
|
|
48
|
+
tool_calls: options[:tool_calls],
|
|
49
|
+
tool_call_id: options[:tool_call_id],
|
|
50
|
+
name: options[:name]
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def clear
|
|
55
|
+
ensure_table_exists!
|
|
56
|
+
MessageRecord.where(conversation_id: conversation_id.to_s).delete_all
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def ensure_table_exists!
|
|
62
|
+
return if ::ActiveRecord::Base.connection.table_exists?("active_agent_messages")
|
|
63
|
+
|
|
64
|
+
raise "active_agent_messages table does not exist. Please run: rails generate active_agent:install && rails db:migrate"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|