llama_bot_rails 0.1.2 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccb4330c7c68f3756bb04fb817404573a68b93f0461ffe5b961711c24d8ca232
4
- data.tar.gz: 0be907f6035572fabd79a0bee0d88200aac81bcde8136d7bb2c0b024cdce7d09
3
+ metadata.gz: 9bd4c2d542ae02f6d430d6c6233792445b79dcff44b265d508c9d985db22b09b
4
+ data.tar.gz: '09ad72a1840000cf121133633dc104517e5937a843be0cba48dadd0a3bf60a66'
5
5
  SHA512:
6
- metadata.gz: 8636af5a8035803f65fd4c648c8a5474998e3c8198d43682c69013123d9c3ae7e1f383f9ee6bab76c46e4bc0b1b88befedbaa026de2dd0ec631cd1105c6efc20
7
- data.tar.gz: c350dd3572e4174717b0f000c1aa0447ae50cd303a1839b036ac74e1d1e65048fef8bf0f2ecf211845be658af4c51220ef9cd4ff33e60093c75e62f7ac087eb1
6
+ metadata.gz: 192fe783f3c6e91170bb3109dbb116d21ca7bb6c68ecb064a92435d416efa16a1e765e5ff59d07af5978e348724e9f70569dbbf2faaeab8ac9a432f7df1712e6
7
+ data.tar.gz: 02362ff59435fd5a0b88038f276e2cc69d8fd70fac868b16ee4c1fa98bfb4ae36c298e541aec388a37856de636b9ee841bff81be971fbae50949925d2c9e3cd2
data/README.md CHANGED
@@ -27,7 +27,6 @@ Chat with a powerful agent that has access to your models, your application cont
27
27
 
28
28
  ## 🚀 **Quickstart** →
29
29
 
30
-
31
30
  ```bash
32
31
 
33
32
  # 1. Add the gem
@@ -38,14 +37,27 @@ rails generate llama_bot_rails:install
38
37
 
39
38
  # 3. Clone & run the LangGraph backend
40
39
  git clone https://github.com/kodykendall/llamabot
41
- cd llamabot
42
- OPENAI_API_KEY=your_key
43
- cd backend && uvicorn app:app --reload
44
40
 
45
- # Start your Rails server
41
+ # 4. Set up your environment
42
+ python3 -m venv venv
43
+
44
+ source venv/bin/activate
45
+
46
+ pip install -r requirements.txt
47
+
48
+ echo "OPENAI_API_KEY=your_openai_api_key_here" > .env
49
+
50
+ # 5. Run the agent
51
+ cd backend
52
+ uvicorn app:app --reload
53
+
54
+ # 6. Confirm our agent is running properly. You should see: Hello, World! 🦙💬
55
+ curl http://localhost:8000/hello
56
+
57
+ # 7. Start your Rails server.
46
58
  rails server
47
59
 
48
- # Visit the chat interface and start chatting.
60
+ # 8. Visit the chat interface and start chatting.
49
61
  open http://localhost:3000/llama_bot/agent/chat
50
62
 
51
63
  ```
@@ -1,7 +1,22 @@
1
1
  //= require action_cable
2
2
  //= require_self
3
3
 
4
- (function() {
4
+ (function () {
5
5
  this.LlamaBotRails = this.LlamaBotRails || {};
6
- LlamaBotRails.cable = ActionCable.createConsumer();
7
- }).call(this);
6
+
7
+ function createLlamaCable() {
8
+ if (window.ActionCable && !LlamaBotRails.cable) {
9
+ console.log("🦙 Creating LlamaBot ActionCable consumer");
10
+ LlamaBotRails.cable = ActionCable.createConsumer();
11
+ }
12
+ }
13
+
14
+ // Run immediately if ActionCable is already present (classic asset pipeline)
15
+ if (window.ActionCable) {
16
+ createLlamaCable();
17
+ } else {
18
+ // Wait until DOM + importmap load finishes
19
+ document.addEventListener("DOMContentLoaded", createLlamaCable);
20
+ document.addEventListener("turbo:load", createLlamaCable); // covers Turbo + importmap apps
21
+ }
22
+ }).call(this);
@@ -1,3 +1,7 @@
1
+ require 'async'
2
+ require 'async/http'
3
+ require 'async/websocket'
4
+
1
5
  require 'json' # Ensure JSON is required if not already
2
6
 
3
7
  module LlamaBotRails
@@ -147,28 +151,27 @@ module LlamaBotRails
147
151
  Rails.logger.info "[LlamaBot] Setting up external websocket for connection: #{connection_id}"
148
152
 
149
153
  # Check if the WebSocket URL is configured
150
- websocket_url = ENV['LLAMABOT_WEBSOCKET_URL']
154
+ websocket_url = Rails.application.config.llama_bot_rails.websocket_url
151
155
  if websocket_url.blank?
152
- Rails.logger.warn "[LlamaBot] LLAMABOT_WEBSOCKET_URL not configured, skipping external WebSocket setup"
156
+ Rails.logger.warn "[LlamaBot] LlamaBot Websocket URL is not configured in the config/initializers/llama_bot_rails.rb file, skipping external WebSocket setup"
153
157
  return
154
158
  end
155
159
 
156
- # endpoint = Async::HTTP::Endpoint.parse(ENV['LLAMABOT_WEBSOCKET_URL'])
157
160
  uri = URI(websocket_url)
158
161
 
159
162
  uri.scheme = 'wss'
160
- uri.scheme = 'ws' if ENV['DEVELOPMENT_ENVIRONMENT'] == 'true'
163
+ uri.scheme = 'ws' if Rails.env.development?
161
164
 
162
165
  endpoint = Async::HTTP::Endpoint.new(
163
166
  uri,
164
167
  ssl_context: OpenSSL::SSL::SSLContext.new.tap do |ctx|
165
168
  ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
166
- if ENV["STAGING_ENVIRONMENT"] == 'true'
169
+ if Rails.env.staging?
167
170
  ctx.ca_file = '/usr/local/etc/ca-certificates/cert.pem'
168
171
  # M2 Air : ctx.ca_file = '/etc//ssl/cert.pem'
169
172
  ctx.cert = OpenSSL::X509::Certificate.new(File.read(File.expand_path('~/.ssl/llamapress/cert.pem')))
170
173
  ctx.key = OpenSSL::PKey::RSA.new(File.read(File.expand_path('~/.ssl/llamapress/key.pem')))
171
- elsif ENV['DEVELOPMENT_ENVIRONMENT'] == 'true'
174
+ elsif Rails.env.development?
172
175
  # do no ctx stuff
173
176
  ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
174
177
  else # production
@@ -405,7 +405,111 @@
405
405
  font-size: 1em;
406
406
  color: var(--text-secondary);
407
407
  }
408
+
409
+ /* Clean loading indicator - just animated text */
410
+ .loading-indicator {
411
+ display: none;
412
+ align-items: center;
413
+ padding: 16px 20px;
414
+ color: var(--text-secondary);
415
+ font-size: 14px;
416
+ margin-bottom: 10px;
417
+ background: rgba(255, 255, 255, 0.02);
418
+ border-radius: 8px;
419
+ border: 1px solid rgba(255, 255, 255, 0.08);
420
+ }
421
+
422
+ .loading-indicator.visible {
423
+ display: flex;
424
+ }
425
+
426
+ .loading-text {
427
+ font-style: italic;
428
+ }
429
+
430
+ .loading-dots::after {
431
+ content: '';
432
+ animation: dots 1.5s steps(4, end) infinite;
433
+ }
434
+
435
+ @keyframes dots {
436
+ 0%, 20% { content: ''; }
437
+ 40% { content: '.'; }
438
+ 60% { content: '..'; }
439
+ 80%, 100% { content: '...'; }
440
+ }
441
+
442
+ /* Suggested Prompts Styling - Always visible above input */
443
+ .suggested-prompts {
444
+ margin-bottom: 16px;
445
+ padding: 0 4px;
446
+ }
447
+
448
+ .prompts-label {
449
+ font-size: 13px;
450
+ color: var(--text-secondary);
451
+ margin-bottom: 8px;
452
+ font-weight: 500;
453
+ letter-spacing: 0.3px;
454
+ }
455
+
456
+ .prompts-container {
457
+ display: flex;
458
+ flex-direction: column;
459
+ gap: 6px;
460
+ }
461
+
462
+ .prompts-row {
463
+ display: flex;
464
+ gap: 8px;
465
+ overflow-x: auto;
466
+ padding: 2px;
467
+ scrollbar-width: none; /* Firefox */
468
+ -ms-overflow-style: none; /* IE and Edge */
469
+ }
470
+
471
+ .prompts-row::-webkit-scrollbar {
472
+ display: none; /* Chrome, Safari, Opera */
473
+ }
474
+
475
+ .prompt-button {
476
+ background: rgba(255, 255, 255, 0.03);
477
+ border: 1px solid rgba(255, 255, 255, 0.08);
478
+ border-radius: 6px;
479
+ padding: 8px 12px;
480
+ color: var(--text-secondary);
481
+ font-size: 13px;
482
+ cursor: pointer;
483
+ transition: all 0.2s ease;
484
+ font-family: inherit;
485
+ white-space: nowrap;
486
+ flex-shrink: 0;
487
+ min-width: fit-content;
488
+ }
489
+
490
+ .prompt-button:hover {
491
+ background: rgba(33, 150, 243, 0.08);
492
+ border-color: rgba(33, 150, 243, 0.2);
493
+ color: var(--text-primary);
494
+ transform: translateY(-1px);
495
+ }
496
+
497
+ .prompt-button:active {
498
+ transform: translateY(0);
499
+ }
500
+
501
+ @media (max-width: 768px) {
502
+ .prompts-grid {
503
+ grid-template-columns: 1fr;
504
+ }
505
+
506
+ .prompt-button {
507
+ font-size: 13px;
508
+ padding: 10px 14px;
509
+ }
510
+ }
408
511
  </style>
512
+ <%= javascript_importmap_tags %>
409
513
  <%= javascript_include_tag "llama_bot_rails/application" %>
410
514
  <%= action_cable_meta_tag %>
411
515
  <!-- Add Snarkdown CDN -->
@@ -430,7 +534,7 @@
430
534
  </button>
431
535
  <div class="logo-container">
432
536
  <img src="https://service-jobs-images.s3.us-east-2.amazonaws.com/7rl98t1weu387r43il97h6ipk1l7" alt="LlamaBot Logo" class="logo">
433
- <div id="connectionStatusIconForLlamaBot" class="connection-status status-green"></div>
537
+ <div id="connectionStatusIconForLlamaBot" class="connection-status status-yellow"></div>
434
538
  </div>
435
539
  <h1>LlamaBot Chat</h1>
436
540
  </div>
@@ -444,6 +548,30 @@
444
548
  <div class="chat-messages" id="chat-messages">
445
549
  <!-- Messages will be added here dynamically -->
446
550
  </div>
551
+
552
+ <!-- Simple loading indicator with just animated text -->
553
+ <div class="loading-indicator" id="loading-indicator">
554
+ <span class="loading-text">LlamaBot is thinking<span class="loading-dots"></span></span>
555
+ </div>
556
+
557
+ <!-- Suggested Prompts - Always visible above input -->
558
+ <div class="suggested-prompts" id="suggested-prompts">
559
+ <div class="prompts-label">Quick actions:</div>
560
+ <div class="prompts-container">
561
+ <div class="prompts-row">
562
+ <button class="prompt-button" onclick="selectPrompt(this)">What models are defined in this app?</button>
563
+ <button class="prompt-button" onclick="selectPrompt(this)">What routes exist?</button>
564
+ <button class="prompt-button" onclick="selectPrompt(this)">How many users are in the database?</button>
565
+ <button class="prompt-button" onclick="selectPrompt(this)">Show me the schema for the User model</button>
566
+ </div>
567
+ <div class="prompts-row">
568
+ <button class="prompt-button" onclick="selectPrompt(this)">Send a text with Twilio</button>
569
+ <button class="prompt-button" onclick="selectPrompt(this)">Create a BlogPost with title and body fields</button>
570
+ <button class="prompt-button" onclick="selectPrompt(this)">Generate a scaffolded Page model</button>
571
+ </div>
572
+ </div>
573
+ </div>
574
+
447
575
  <div class="input-container">
448
576
  <input type="text" id="message-input" placeholder="Type your message...">
449
577
  <button onclick="sendMessage()">Send</button>
@@ -465,37 +593,49 @@
465
593
  let redStatusStartTime = null;
466
594
  let errorModalShown = false;
467
595
  let connectionCheckInterval;
596
+ let subscription = null;
468
597
 
469
- // Initialize ActionCable connection
470
- const consumer = LlamaBotRails.cable;
471
- const subscription = consumer.subscriptions.create('LlamaBotRails::ChatChannel', {
472
- connected() {
473
- console.log('Connected to chat channel');
474
- lastPongTime = Date.now();
475
- loadThreads();
476
- startConnectionCheck();
477
- },
478
- disconnected() {
479
- console.log('Disconnected from chat channel');
480
- updateStatusIcon('status-red');
481
- },
482
- received(data) {
483
- const parsedData = JSON.parse(data).message;
484
- switch(parsedData.type) {
598
+ function waitForCableConnection(callback) {
599
+ const interval = setInterval(() => {
600
+ if (window.LlamaBotRails && LlamaBotRails.cable) {
601
+ clearInterval(interval);
602
+ callback(LlamaBotRails.cable);
603
+ }
604
+ }, 50);
605
+ }
606
+
607
+ waitForCableConnection((consumer) => {
608
+ const sessionId = crypto.randomUUID();
609
+
610
+ subscription = consumer.subscriptions.create({channel: 'LlamaBotRails::ChatChannel', session_id: sessionId}, {
611
+ connected() {
612
+ console.log('Connected to chat channel');
613
+ lastPongTime = Date.now();
614
+ loadThreads();
615
+ startConnectionCheck();
616
+ },
617
+ disconnected() {
618
+ console.log('Disconnected from chat channel');
619
+ updateStatusIcon('status-red');
620
+ },
621
+ received(data) {
622
+ const parsedData = JSON.parse(data).message;
623
+ switch (parsedData.type) {
485
624
  case "ai":
486
625
  addMessage(parsedData.content, parsedData.type, parsedData.base_message);
487
626
  break;
488
- case "tool":
489
- addMessage(parsedData.content, parsedData.type, parsedData.base_message);
490
- break;
491
- case "error":
492
- addMessage(parsedData.content, parsedData.type, parsedData.base_message);
493
- break;
494
- case "pong":
495
- lastPongTime = Date.now();
496
- break;
627
+ case "tool":
628
+ addMessage(parsedData.content, parsedData.type, parsedData.base_message);
629
+ break;
630
+ case "error":
631
+ addMessage(parsedData.content, parsedData.type, parsedData.base_message);
632
+ break;
633
+ case "pong":
634
+ lastPongTime = Date.now();
635
+ break;
636
+ }
497
637
  }
498
- }
638
+ });
499
639
  });
500
640
 
501
641
  function startConnectionCheck() {
@@ -681,11 +821,6 @@
681
821
 
682
822
  // Show welcome message
683
823
  showWelcomeMessage();
684
-
685
- // Clear active thread selection
686
- document.querySelectorAll('.thread-item').forEach(item => {
687
- item.classList.remove('active');
688
- });
689
824
  }
690
825
 
691
826
  function showWelcomeMessage() {
@@ -699,11 +834,45 @@
699
834
  messagesDiv.appendChild(welcomeDiv);
700
835
  }
701
836
 
837
+ function showLoadingIndicator() {
838
+ const loadingIndicator = document.getElementById('loading-indicator');
839
+ loadingIndicator.classList.add('visible');
840
+ }
841
+
842
+ function hideLoadingIndicator() {
843
+ const loadingIndicator = document.getElementById('loading-indicator');
844
+ loadingIndicator.classList.remove('visible');
845
+ }
846
+
847
+ function selectPrompt(buttonElement) {
848
+ const promptText = buttonElement.textContent;
849
+ const messageInput = document.getElementById('message-input');
850
+
851
+ // Populate the input field
852
+ messageInput.value = promptText;
853
+
854
+ // Focus the input field for better UX
855
+ messageInput.focus();
856
+
857
+ // Add a subtle animation to show the prompt was selected
858
+ buttonElement.style.transform = 'scale(0.98)';
859
+ setTimeout(() => {
860
+ buttonElement.style.transform = '';
861
+ }, 150);
862
+ }
863
+
702
864
  function sendMessage() {
703
865
  const input = document.getElementById('message-input');
704
866
  const message = input.value.trim();
705
867
 
706
868
  if (message) {
869
+ // Check if subscription is available
870
+ if (!subscription) {
871
+ console.error('WebSocket connection not established yet');
872
+ addMessage('Connection not ready. Please wait...', 'error');
873
+ return;
874
+ }
875
+
707
876
  // Clear welcome message if it exists
708
877
  const welcomeMessage = document.querySelector('.welcome-message');
709
878
  if (welcomeMessage) {
@@ -713,6 +882,9 @@
713
882
  addMessage(message, 'human');
714
883
  input.value = '';
715
884
 
885
+ // Show loading indicator
886
+ showLoadingIndicator();
887
+
716
888
  // Generate timestamp-based thread ID if we don't have one
717
889
  let threadId = currentThreadId;
718
890
  if (!threadId || threadId === 'global_thread_id') {
@@ -746,6 +918,11 @@
746
918
  function addMessage(text, sender, base_message=null) {
747
919
  console.log('🧠 Message from LlamaBot:', text, sender, base_message);
748
920
 
921
+ // Hide loading indicator when we receive an AI response
922
+ if (sender === 'ai') {
923
+ hideLoadingIndicator();
924
+ }
925
+
749
926
  const messagesDiv = document.getElementById('chat-messages');
750
927
  const messageDiv = document.createElement('div');
751
928
  messageDiv.className = `message ${sender}-message`;
@@ -0,0 +1,46 @@
1
+ # lib/generators/llama_bot_rails/install/install_generator.rb
2
+ module LlamaBotRails
3
+ module Generators
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path("templates", __dir__)
6
+
7
+ def mount_engine
8
+ say <<~MSG, :yellow
9
+ ⚠️ NOTE: LlamaBotRails requires ActionCable to be available on the frontend.
10
+
11
+ If you're using ImportMap (Rails 7 default), run:
12
+
13
+ bin/importmap pin @rails/actioncable
14
+
15
+ And in app/javascript/application.js, add:
16
+
17
+ import * as ActionCable from "@rails/actioncable"
18
+ window.ActionCable = ActionCable
19
+
20
+ If you're using Webpacker or jsbundling-rails:
21
+ Add @rails/actioncable via yarn/npm
22
+ And import + expose it the same way in your JS pack.
23
+
24
+ 📘 See README → “JavaScript Setup” for full details.
25
+
26
+ MSG
27
+
28
+ route 'mount LlamaBotRails::Engine => "/llama_bot"'
29
+ end
30
+
31
+ def create_initializer
32
+ create_file "config/initializers/llama_bot_rails.rb", <<~RUBY
33
+ Rails.application.configure do
34
+ config.llama_bot_rails.websocket_url = ENV.fetch("LLAMABOT_WEBSOCKET_URL", "ws://localhost:8000/ws")
35
+ config.llama_bot_rails.enable_console_tool = !Rails.env.production?
36
+ end
37
+ RUBY
38
+ end
39
+
40
+ def finish
41
+ say "\n✅ LlamaBotRails installed! Visit http://localhost:3000/llama_bot/agent/chat\n", :green
42
+ end
43
+ end
44
+ end
45
+ end
46
+
@@ -11,6 +11,9 @@ module LlamaBotRails
11
11
  end
12
12
 
13
13
  config.llama_bot_rails = ActiveSupport::OrderedOptions.new
14
+ config.llama_bot_rails.websocket_url = 'ws://localhost:8000/ws' # <-- default
15
+ config.llama_bot_rails.llamabot_api_url = "http://localhost:8000"
16
+ config.llama_bot_rails.enable_console_tool = true
14
17
 
15
18
  initializer "llama_bot_rails.assets.precompile" do |app|
16
19
  app.config.assets.precompile += %w( llama_bot_rails/application.js )
@@ -1,3 +1,3 @@
1
1
  module LlamaBotRails
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llama_bot_rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kody Kendall
@@ -77,6 +77,20 @@ dependencies:
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
79
  version: '0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: async
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
80
94
  description: LlamaBotRails is a gem that turns your existing Rails App into an AI
81
95
  Agent by connecting it to an open source LangGraph agent, LlamaBot.
82
96
  email:
@@ -107,6 +121,7 @@ files:
107
121
  - bin/rubocop
108
122
  - config/initializers/llama_bot_rails.rb
109
123
  - config/routes.rb
124
+ - lib/generators/llama_bot_rails/install/install_generator.rb
110
125
  - lib/llama_bot_rails.rb
111
126
  - lib/llama_bot_rails/agent_state_builder.rb
112
127
  - lib/llama_bot_rails/engine.rb