llama_bot_rails 0.1.7 → 0.1.9
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/README.md +22 -22
- data/app/channels/llama_bot_rails/chat_channel.rb +51 -4
- data/app/controllers/llama_bot_rails/agent_controller.rb +78 -5
- data/app/views/llama_bot_rails/agent/chat.html.erb +250 -127
- data/app/views/llama_bot_rails/agent/chat_ws.html.erb +1178 -0
- data/config/routes.rb +3 -0
- data/lib/generators/llama_bot_rails/install/install_generator.rb +35 -5
- data/lib/generators/llama_bot_rails/install/templates/agent_state_builder.rb.erb +22 -0
- data/lib/llama_bot_rails/agent_state_builder.rb +12 -7
- data/lib/llama_bot_rails/engine.rb +1 -1
- data/lib/llama_bot_rails/llama_bot.rb +47 -0
- data/lib/llama_bot_rails/railtie.rb +19 -0
- data/lib/llama_bot_rails/version.rb +1 -1
- data/lib/llama_bot_rails.rb +7 -1
- metadata +4 -1
@@ -145,30 +145,7 @@
|
|
145
145
|
display: block;
|
146
146
|
}
|
147
147
|
|
148
|
-
.connection-status {
|
149
|
-
position: absolute;
|
150
|
-
bottom: -2px;
|
151
|
-
right: -2px;
|
152
|
-
width: 12px;
|
153
|
-
height: 12px;
|
154
|
-
border-radius: 50%;
|
155
|
-
border: 2px solid var(--bg-primary);
|
156
|
-
transition: background-color 0.3s ease;
|
157
|
-
z-index: 10;
|
158
|
-
pointer-events: none;
|
159
|
-
}
|
160
|
-
|
161
|
-
.status-green {
|
162
|
-
background-color: #22c55e !important;
|
163
|
-
}
|
164
148
|
|
165
|
-
.status-yellow {
|
166
|
-
background-color: #eab308 !important;
|
167
|
-
}
|
168
|
-
|
169
|
-
.status-red {
|
170
|
-
background-color: #ef4444 !important;
|
171
|
-
}
|
172
149
|
|
173
150
|
.error-modal {
|
174
151
|
display: none;
|
@@ -406,7 +383,7 @@
|
|
406
383
|
color: var(--text-secondary);
|
407
384
|
}
|
408
385
|
|
409
|
-
/*
|
386
|
+
/* Enhanced loading indicator with status updates */
|
410
387
|
.loading-indicator {
|
411
388
|
display: none;
|
412
389
|
align-items: center;
|
@@ -417,6 +394,8 @@
|
|
417
394
|
background: rgba(255, 255, 255, 0.02);
|
418
395
|
border-radius: 8px;
|
419
396
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
397
|
+
transition: all 0.3s ease;
|
398
|
+
min-height: 50px; /* Prevent layout shift during status updates */
|
420
399
|
}
|
421
400
|
|
422
401
|
.loading-indicator.visible {
|
@@ -425,11 +404,15 @@
|
|
425
404
|
|
426
405
|
.loading-text {
|
427
406
|
font-style: italic;
|
407
|
+
flex: 1;
|
408
|
+
transition: color 0.3s ease;
|
409
|
+
line-height: 1.4;
|
428
410
|
}
|
429
411
|
|
430
412
|
.loading-dots::after {
|
431
413
|
content: '';
|
432
414
|
animation: dots 1.5s steps(4, end) infinite;
|
415
|
+
opacity: 0.7;
|
433
416
|
}
|
434
417
|
|
435
418
|
@keyframes dots {
|
@@ -439,6 +422,22 @@
|
|
439
422
|
80%, 100% { content: '...'; }
|
440
423
|
}
|
441
424
|
|
425
|
+
/* Status-specific styling */
|
426
|
+
.loading-indicator:has(.loading-text:contains("Error")) {
|
427
|
+
border-color: rgba(244, 67, 54, 0.3);
|
428
|
+
background: rgba(244, 67, 54, 0.05);
|
429
|
+
}
|
430
|
+
|
431
|
+
.loading-indicator:has(.loading-text:contains("Complete")) {
|
432
|
+
border-color: rgba(76, 175, 80, 0.3);
|
433
|
+
background: rgba(76, 175, 80, 0.05);
|
434
|
+
}
|
435
|
+
|
436
|
+
.loading-indicator:has(.loading-text:contains("Connected")) {
|
437
|
+
border-color: rgba(33, 150, 243, 0.3);
|
438
|
+
background: rgba(33, 150, 243, 0.05);
|
439
|
+
}
|
440
|
+
|
442
441
|
/* Suggested Prompts Styling - Always visible above input */
|
443
442
|
.suggested-prompts {
|
444
443
|
margin-bottom: 16px;
|
@@ -510,16 +509,6 @@
|
|
510
509
|
}
|
511
510
|
</style>
|
512
511
|
|
513
|
-
<% if defined?(javascript_importmap_tags) %> <!-- Rails 7+ -->
|
514
|
-
<%= javascript_importmap_tags %>
|
515
|
-
<% else %> <!-- Rails 6 -->
|
516
|
-
<%= javascript_include_tag "application" %>
|
517
|
-
<% end %>
|
518
|
-
|
519
|
-
<%= javascript_include_tag "llama_bot_rails/application" %>
|
520
|
-
<% if defined?(action_cable_meta_tag) %>
|
521
|
-
<%= action_cable_meta_tag %>
|
522
|
-
<% end %>
|
523
512
|
<!-- Add Snarkdown CDN -->
|
524
513
|
<script src="https://unpkg.com/snarkdown/dist/snarkdown.umd.js"></script>
|
525
514
|
</head>
|
@@ -542,7 +531,6 @@
|
|
542
531
|
</button>
|
543
532
|
<div class="logo-container">
|
544
533
|
<img src="https://service-jobs-images.s3.us-east-2.amazonaws.com/7rl98t1weu387r43il97h6ipk1l7" alt="LlamaBot Logo" class="logo">
|
545
|
-
<div id="connectionStatusIconForLlamaBot" class="connection-status status-yellow"></div>
|
546
534
|
</div>
|
547
535
|
<h1>LlamaBot Chat</h1>
|
548
536
|
</div>
|
@@ -597,95 +585,19 @@
|
|
597
585
|
<script>
|
598
586
|
let currentThreadId = null;
|
599
587
|
let isSidebarCollapsed = false;
|
600
|
-
let
|
601
|
-
|
602
|
-
let errorModalShown = false;
|
603
|
-
let connectionCheckInterval;
|
604
|
-
let subscription = null;
|
605
|
-
|
606
|
-
function waitForCableConnection(callback) {
|
607
|
-
const interval = setInterval(() => {
|
608
|
-
if (window.LlamaBotRails && LlamaBotRails.cable) {
|
609
|
-
clearInterval(interval);
|
610
|
-
callback(LlamaBotRails.cable);
|
611
|
-
}
|
612
|
-
}, 50);
|
613
|
-
}
|
614
|
-
|
615
|
-
waitForCableConnection((consumer) => {
|
616
|
-
const sessionId = crypto.randomUUID();
|
617
|
-
|
618
|
-
subscription = consumer.subscriptions.create({channel: 'LlamaBotRails::ChatChannel', session_id: sessionId}, {
|
619
|
-
connected() {
|
620
|
-
console.log('Connected to chat channel');
|
621
|
-
lastPongTime = Date.now();
|
622
|
-
loadThreads();
|
623
|
-
startConnectionCheck();
|
624
|
-
},
|
625
|
-
disconnected() {
|
626
|
-
console.log('Disconnected from chat channel');
|
627
|
-
updateStatusIcon('status-red');
|
628
|
-
},
|
629
|
-
received(data) {
|
630
|
-
const parsedData = JSON.parse(data).message;
|
631
|
-
switch (parsedData.type) {
|
632
|
-
case "ai":
|
633
|
-
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
634
|
-
break;
|
635
|
-
case "tool":
|
636
|
-
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
637
|
-
break;
|
638
|
-
case "error":
|
639
|
-
addMessage(parsedData.content, parsedData.type, parsedData.base_message);
|
640
|
-
break;
|
641
|
-
case "pong":
|
642
|
-
lastPongTime = Date.now();
|
643
|
-
break;
|
644
|
-
}
|
645
|
-
}
|
646
|
-
});
|
647
|
-
});
|
588
|
+
let streamingTimeout = null;
|
589
|
+
const STREAMING_TIMEOUT_MS = 30000; // 30 seconds timeout
|
648
590
|
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
connectionCheckInterval = setInterval(updateConnectionStatus, 1000);
|
654
|
-
}
|
655
|
-
|
656
|
-
function updateConnectionStatus() {
|
657
|
-
const timeSinceLastPong = Date.now() - lastPongTime;
|
658
|
-
|
659
|
-
if (timeSinceLastPong < 30000) { // Less than 30 seconds
|
660
|
-
updateStatusIcon('status-green');
|
661
|
-
redStatusStartTime = null;
|
662
|
-
errorModalShown = false;
|
663
|
-
} else if (timeSinceLastPong < 50000) { // Between 30-50 seconds
|
664
|
-
updateStatusIcon('status-yellow');
|
665
|
-
redStatusStartTime = null;
|
666
|
-
errorModalShown = false;
|
667
|
-
} else { // More than 50 seconds
|
668
|
-
updateStatusIcon('status-red');
|
669
|
-
if (!redStatusStartTime) {
|
670
|
-
redStatusStartTime = Date.now();
|
671
|
-
} else if (Date.now() - redStatusStartTime > 5000 && !errorModalShown) { // 5 seconds in red status
|
672
|
-
showErrorModal();
|
673
|
-
}
|
674
|
-
}
|
675
|
-
}
|
676
|
-
|
677
|
-
function updateStatusIcon(statusClass) {
|
678
|
-
const statusIndicator = document.getElementById('connectionStatusIconForLlamaBot');
|
679
|
-
statusIndicator.classList.remove('status-green', 'status-yellow', 'status-red');
|
680
|
-
statusIndicator.classList.add(statusClass);
|
681
|
-
}
|
591
|
+
// Initialize the app
|
592
|
+
document.addEventListener('DOMContentLoaded', function() {
|
593
|
+
loadThreads();
|
594
|
+
});
|
682
595
|
|
683
596
|
function showErrorModal() {
|
684
597
|
const modal = document.getElementById('errorModal');
|
685
598
|
const overlay = document.getElementById('modalOverlay');
|
686
599
|
modal.classList.add('visible');
|
687
600
|
overlay.classList.add('visible');
|
688
|
-
errorModalShown = true;
|
689
601
|
}
|
690
602
|
|
691
603
|
function closeErrorModal() {
|
@@ -852,6 +764,54 @@
|
|
852
764
|
loadingIndicator.classList.remove('visible');
|
853
765
|
}
|
854
766
|
|
767
|
+
|
768
|
+
|
769
|
+
function setupStreamingTimeout() {
|
770
|
+
// Clear any existing timeout
|
771
|
+
if (streamingTimeout) {
|
772
|
+
clearTimeout(streamingTimeout);
|
773
|
+
}
|
774
|
+
|
775
|
+
// Set up new timeout
|
776
|
+
streamingTimeout = setTimeout(() => {
|
777
|
+
console.warn('Streaming timeout reached');
|
778
|
+
hideLoadingIndicator();
|
779
|
+
addMessage('Request timed out. LlamaBot may be processing a complex request. Please try again.', 'error');
|
780
|
+
}, STREAMING_TIMEOUT_MS);
|
781
|
+
}
|
782
|
+
|
783
|
+
function clearStreamingTimeout() {
|
784
|
+
if (streamingTimeout) {
|
785
|
+
clearTimeout(streamingTimeout);
|
786
|
+
streamingTimeout = null;
|
787
|
+
}
|
788
|
+
}
|
789
|
+
|
790
|
+
// console.log('🤖 Testing streaming');
|
791
|
+
// testStreaming();
|
792
|
+
|
793
|
+
async function testStreaming() {
|
794
|
+
const response = await fetch('/llama_bot/agent/test_streaming_2');
|
795
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
796
|
+
const decoder = new TextDecoder();
|
797
|
+
let buffer = '';
|
798
|
+
debugger;
|
799
|
+
let infinite_loop_protector = 0;
|
800
|
+
let in_infinite_loop = false;
|
801
|
+
while(!in_infinite_loop){ //infinite_loop_protector to protect against infinite loop
|
802
|
+
infinite_loop_protector++;
|
803
|
+
if (infinite_loop_protector > 10000) { //we shouldn't have 10000 loops, but just in case.
|
804
|
+
in_infinite_loop = true;
|
805
|
+
console.error('∞ ∞ ∞ Infinite loop prevented! ∞ ∞ ∞');
|
806
|
+
}
|
807
|
+
const { done, value } = reader.read();
|
808
|
+
console.log('🤖 SSE message:', value);
|
809
|
+
if (done) {
|
810
|
+
break;
|
811
|
+
}
|
812
|
+
}
|
813
|
+
}
|
814
|
+
|
855
815
|
function selectPrompt(buttonElement) {
|
856
816
|
const promptText = buttonElement.textContent;
|
857
817
|
const messageInput = document.getElementById('message-input');
|
@@ -869,18 +829,11 @@
|
|
869
829
|
}, 150);
|
870
830
|
}
|
871
831
|
|
872
|
-
function sendMessage() {
|
832
|
+
async function sendMessage() {
|
873
833
|
const input = document.getElementById('message-input');
|
874
834
|
const message = input.value.trim();
|
875
835
|
|
876
836
|
if (message) {
|
877
|
-
// Check if subscription is available
|
878
|
-
if (!subscription) {
|
879
|
-
console.error('WebSocket connection not established yet');
|
880
|
-
addMessage('Connection not ready. Please wait...', 'error');
|
881
|
-
return;
|
882
|
-
}
|
883
|
-
|
884
837
|
// Clear welcome message if it exists
|
885
838
|
const welcomeMessage = document.querySelector('.welcome-message');
|
886
839
|
if (welcomeMessage) {
|
@@ -912,8 +865,178 @@
|
|
912
865
|
thread_id: threadId
|
913
866
|
};
|
914
867
|
|
915
|
-
console.log('Sending message with data:', messageData);
|
916
|
-
|
868
|
+
console.log('Sending message with data:', messageData);
|
869
|
+
|
870
|
+
try {
|
871
|
+
// Set up fetch for streaming
|
872
|
+
const response = await fetch('/llama_bot/agent/send_message', {
|
873
|
+
method: 'POST',
|
874
|
+
headers: {
|
875
|
+
'Content-Type': 'application/json',
|
876
|
+
},
|
877
|
+
body: JSON.stringify(messageData)
|
878
|
+
});
|
879
|
+
|
880
|
+
if (!response.ok) {
|
881
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
882
|
+
}
|
883
|
+
|
884
|
+
// Set up streaming timeout
|
885
|
+
setupStreamingTimeout();
|
886
|
+
|
887
|
+
// Set up the reader for the stream
|
888
|
+
const reader = response.body.getReader();
|
889
|
+
const decoder = new TextDecoder();
|
890
|
+
let buffer = '';
|
891
|
+
|
892
|
+
// const response_2 = await fetch('/llama_bot/agent/test_streaming', {
|
893
|
+
// method: 'GET',
|
894
|
+
// headers: {
|
895
|
+
// 'Content-Type': 'text/event-stream'
|
896
|
+
// },
|
897
|
+
// // body: JSON.stringify({
|
898
|
+
// // "message": message,
|
899
|
+
// // "thread_id": threadId
|
900
|
+
// // })
|
901
|
+
// });
|
902
|
+
|
903
|
+
// const reader_2 = response_2.body.pipeThrough(new TextDecoderStream()).getReader();
|
904
|
+
|
905
|
+
// while (true) {
|
906
|
+
// const { done, value } = await reader_2.read();
|
907
|
+
// if (done) {
|
908
|
+
// break;
|
909
|
+
// }
|
910
|
+
// // Process each SSE message (value)
|
911
|
+
// console.log('🤖 SSE message:', value);
|
912
|
+
// }
|
913
|
+
|
914
|
+
try {
|
915
|
+
while (true) {
|
916
|
+
const { done, value } = await reader.read();
|
917
|
+
console.log('Got a value from the stream 🧠 Stream value:', value);
|
918
|
+
|
919
|
+
if (done) {
|
920
|
+
console.log('Stream completed');
|
921
|
+
clearStreamingTimeout();
|
922
|
+
break;
|
923
|
+
}
|
924
|
+
|
925
|
+
// Decode the chunk and add to buffer
|
926
|
+
buffer += decoder.decode(value, { stream: true });
|
927
|
+
|
928
|
+
// Process complete SSE events (separated by \n\n). These \n\n are added by the Rails middleware to make sure it exists.
|
929
|
+
while (buffer.includes('\n\n')) {
|
930
|
+
const eventEnd = buffer.indexOf('\n\n');
|
931
|
+
const eventBlock = buffer.slice(0, eventEnd);
|
932
|
+
buffer = buffer.slice(eventEnd + 2);
|
933
|
+
|
934
|
+
// Parse SSE event
|
935
|
+
const dataLines = eventBlock.split('\n').filter(line => line.startsWith('data:'));
|
936
|
+
if (dataLines.length > 0) {
|
937
|
+
|
938
|
+
//remove the 'data:' prefix from the line
|
939
|
+
const jsonData = dataLines.map(line => line.substring(5).trim()).join('');
|
940
|
+
|
941
|
+
try {
|
942
|
+
const chunk = JSON.parse(jsonData);
|
943
|
+
console.log('Processing chunk:', chunk);
|
944
|
+
|
945
|
+
// Your existing chunk processing logic here...
|
946
|
+
if (chunk.type === 'start') {
|
947
|
+
console.log('Stream started:', chunk.request_id || 'unknown');
|
948
|
+
} else if (chunk.type === 'final') {
|
949
|
+
console.log('Stream completed');
|
950
|
+
hideLoadingIndicator();
|
951
|
+
clearStreamingTimeout();
|
952
|
+
} else if (chunk.type === 'error') {
|
953
|
+
console.error('Server error:', chunk);
|
954
|
+
hideLoadingIndicator();
|
955
|
+
clearStreamingTimeout();
|
956
|
+
addMessage(`Error: ${chunk.content || 'Unknown error occurred'}`, 'error');
|
957
|
+
} else if (chunk.type === 'ai') {
|
958
|
+
addMessage(chunk.content, 'ai', chunk);
|
959
|
+
hideLoadingIndicator();
|
960
|
+
clearStreamingTimeout();
|
961
|
+
} else if (chunk.type === 'tool') {
|
962
|
+
addMessage(chunk.content, 'tool', chunk);
|
963
|
+
} else {
|
964
|
+
if (chunk.content) {
|
965
|
+
addMessage(chunk.content, chunk.type || 'unknown', chunk);
|
966
|
+
}
|
967
|
+
console.log('Other chunk type:', chunk.type, chunk);
|
968
|
+
}
|
969
|
+
|
970
|
+
} catch (parseError) {
|
971
|
+
console.error('Error parsing SSE data:', parseError, 'Data:', jsonData);
|
972
|
+
}
|
973
|
+
}
|
974
|
+
}
|
975
|
+
}
|
976
|
+
} finally {
|
977
|
+
reader.releaseLock();
|
978
|
+
}
|
979
|
+
|
980
|
+
} catch (error) {
|
981
|
+
console.error('Error in sendMessage:', error);
|
982
|
+
hideLoadingIndicator();
|
983
|
+
clearStreamingTimeout();
|
984
|
+
|
985
|
+
// Show specific error message based on error type
|
986
|
+
let errorMessage = 'Error sending message. Please try again.';
|
987
|
+
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
988
|
+
errorMessage = 'Connection error. Please check if LlamaBot is running.';
|
989
|
+
} else if (error.message.includes('HTTP error')) {
|
990
|
+
errorMessage = `Server error: ${error.message}`;
|
991
|
+
}
|
992
|
+
|
993
|
+
addMessage(errorMessage, 'error');
|
994
|
+
}
|
995
|
+
}
|
996
|
+
}
|
997
|
+
|
998
|
+
function processChunk(chunk) {
|
999
|
+
console.log('Processing chunk in fallback handler:', chunk);
|
1000
|
+
|
1001
|
+
// Handle specific chunk types from Python backend
|
1002
|
+
switch (chunk.type) {
|
1003
|
+
case 'start':
|
1004
|
+
console.log('Stream started:', chunk.request_id || 'unknown');
|
1005
|
+
// Loading indicator already showing
|
1006
|
+
break;
|
1007
|
+
|
1008
|
+
case 'final':
|
1009
|
+
console.log('Stream completed');
|
1010
|
+
hideLoadingIndicator();
|
1011
|
+
clearStreamingTimeout();
|
1012
|
+
break;
|
1013
|
+
|
1014
|
+
case 'error':
|
1015
|
+
console.error('Server error:', chunk);
|
1016
|
+
hideLoadingIndicator();
|
1017
|
+
clearStreamingTimeout();
|
1018
|
+
addMessage(`Error: ${chunk.content || 'Unknown error occurred'}`, 'error');
|
1019
|
+
break;
|
1020
|
+
|
1021
|
+
case 'ai':
|
1022
|
+
// AI message from LangGraph
|
1023
|
+
addMessage(chunk.content, 'ai', chunk);
|
1024
|
+
hideLoadingIndicator();
|
1025
|
+
clearStreamingTimeout();
|
1026
|
+
break;
|
1027
|
+
|
1028
|
+
case 'tool':
|
1029
|
+
// Tool message from LangGraph
|
1030
|
+
addMessage(chunk.content, 'tool', chunk);
|
1031
|
+
break;
|
1032
|
+
|
1033
|
+
default:
|
1034
|
+
// Handle any other message types
|
1035
|
+
if (chunk.content) {
|
1036
|
+
addMessage(chunk.content, chunk.type || 'unknown', chunk);
|
1037
|
+
}
|
1038
|
+
console.log('Other chunk type:', chunk.type, chunk);
|
1039
|
+
break;
|
917
1040
|
}
|
918
1041
|
}
|
919
1042
|
|