From 64f01f6cc60676d861c6ed52951b928b0f12eaa4 Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 19:52:14 -0700 Subject: [PATCH 01/16] Initial Architecture.md, doc/, system sequence diagram --- ARCHITECTURE.md | 59 ++++++++++++++++++ doc/chat-interface-ssdiagram.svg | 102 +++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 ARCHITECTURE.md create mode 100644 doc/chat-interface-ssdiagram.svg diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..862bd55 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,59 @@ +# Overview + +Web chat interface to chatGPT + +# System Design + +## Chat Interface System Sequence Diagram +![Chat Architecture Diagram](./doc/chat-interface-ssdiagram.svg) + +```mermaid +sequenceDiagram + participant F as 🧑‍💻 Frontend (Turbo) + participant C as 🎯 ChatController (Rails) + participant W as ⚙️ Sidekiq Worker + participant O as 🤖 OpenAI (Spectre) + participant D as 🗄️ Database (Chat Table) + participant T as 📡 Turbo Stream Channel + + %% 1. Controller creates or updates session chat record + F->>C: 1️⃣ Send message via POST + C->>D: Create or update chat session record + + %% 2. Controller sends message to worker + C->>W: Enqueue job with message payload + + %% 3. Worker sends message to OpenAI + W->>O: Send user message via Spectre API + + %% 4. OpenAI responds + O-->>W: Return generated AI response + + %% 5. Worker saves message to DB + W->>D: Save AI response message record + + %% 6. Worker broadcasts to Turbo Stream + W->>T: Broadcast Turbo Stream update + T-->>F: Realtime update (new AI message displayed) +``` + +## Design Decisions + +### Decision: Session Handling Strategy + +A unique SecureRandom.uuid will be generated per-browser session. + +The SecureRandom.uuid will be stored in session[conversation_id]. + +Effectively, each chat will be tied to a per-browser session[conversation_id]. + + +# Integration & External Services + +The chat interface uses Ruby Spectre gem to interface with the OpenAI foundation model. + +# Future Work + +- Add Roadauth user authentication and user table, tie chats to user +- Add chat session tab feature with new data table modeling (chat, questions, responses) +- Add Tailwind for nice UI layout \ No newline at end of file diff --git a/doc/chat-interface-ssdiagram.svg b/doc/chat-interface-ssdiagram.svg new file mode 100644 index 0000000..f41f194 --- /dev/null +++ b/doc/chat-interface-ssdiagram.svg @@ -0,0 +1,102 @@ +📡 Turbo Stream Channel🗄️ Database (Chat Table)🤖 OpenAI (Spectre)⚙️ Sidekiq Worker🎯 ChatController (Rails)🧑‍💻 Frontend (Turbo)📡 Turbo Stream Channel🗄️ Database (Chat Table)🤖 OpenAI (Spectre)⚙️ Sidekiq Worker🎯 ChatController (Rails)🧑‍💻 Frontend (Turbo)1️⃣ Send message via POSTCreate or update chat session recordEnqueue job with message payloadSend user message via Spectre APIReturn generated AI responseSave AI response message recordBroadcast Turbo Stream updateRealtime update (new AI message displayed) \ No newline at end of file From a7b7c3daaecdd5a74ce1af7dcf4b09c499c982d1 Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 19:53:11 -0700 Subject: [PATCH 02/16] Add Spectre gem for OpenAI API interface --- Gemfile | 3 +++ Gemfile.lock | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index 65ae1fb..563d1da 100644 --- a/Gemfile +++ b/Gemfile @@ -17,6 +17,9 @@ gem "stimulus-rails" # Build JSON APIs with ease [https://github.com/rails/jbuilder] gem "jbuilder" +# Enable interface with text generative OpenAI foundation model +gem 'spectre_ai' + # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index 1651c41..687d89e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -260,6 +260,7 @@ GEM logger (>= 1.6.2) rack (>= 3.1.0) redis-client (>= 0.23.2) + spectre_ai (2.0.0) stimulus-rails (1.3.4) railties (>= 6.0.0) stringio (3.1.7) @@ -311,6 +312,7 @@ DEPENDENCIES rails (~> 8.1.0) selenium-webdriver sidekiq + spectre_ai stimulus-rails turbo-rails tzinfo-data From 3ae4a5499940828a51daedb547a17338a4bdd3fe Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 20:17:32 -0700 Subject: [PATCH 03/16] Add .idea & /idea/* to .gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index fbcab40..3bd2d44 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ # Ignore key files for decrypting credentials and more. /config/*.key +# Ignore .idea SDE files +/idea/* +.idea From 210e7fcf559b083b53c2f1db8af919ea9d76269a Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 20:18:00 -0700 Subject: [PATCH 04/16] Add rails generate scaffold chat --- ARCHITECTURE.md | 3 +- app/controllers/application_controller.rb | 8 +++ app/controllers/chats_controller.rb | 70 +++++++++++++++++++++++ app/helpers/chats_helper.rb | 2 + app/models/chat.rb | 2 + app/views/chats/_chat.html.erb | 2 + app/views/chats/_chat.json.jbuilder | 2 + app/views/chats/_form.html.erb | 17 ++++++ app/views/chats/edit.html.erb | 12 ++++ app/views/chats/index.html.erb | 16 ++++++ app/views/chats/index.json.jbuilder | 1 + app/views/chats/new.html.erb | 11 ++++ app/views/chats/show.html.erb | 10 ++++ app/views/chats/show.json.jbuilder | 1 + config/routes.rb | 1 + db/migrate/20251028025508_create_chats.rb | 7 +++ db/schema.rb | 21 +++++++ test/controllers/chats_controller_test.rb | 48 ++++++++++++++++ test/fixtures/chats.yml | 11 ++++ test/models/chat_test.rb | 7 +++ 20 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 app/controllers/chats_controller.rb create mode 100644 app/helpers/chats_helper.rb create mode 100644 app/models/chat.rb create mode 100644 app/views/chats/_chat.html.erb create mode 100644 app/views/chats/_chat.json.jbuilder create mode 100644 app/views/chats/_form.html.erb create mode 100644 app/views/chats/edit.html.erb create mode 100644 app/views/chats/index.html.erb create mode 100644 app/views/chats/index.json.jbuilder create mode 100644 app/views/chats/new.html.erb create mode 100644 app/views/chats/show.html.erb create mode 100644 app/views/chats/show.json.jbuilder create mode 100644 db/migrate/20251028025508_create_chats.rb create mode 100644 db/schema.rb create mode 100644 test/controllers/chats_controller_test.rb create mode 100644 test/fixtures/chats.yml create mode 100644 test/models/chat_test.rb diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 862bd55..71a9308 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -56,4 +56,5 @@ The chat interface uses Ruby Spectre gem to interface with the OpenAI foundation - Add Roadauth user authentication and user table, tie chats to user - Add chat session tab feature with new data table modeling (chat, questions, responses) -- Add Tailwind for nice UI layout \ No newline at end of file +- Add Tailwind for nice UI layout +- Cleanup `rails generate scaffold chat` files \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index c353756..ea5f046 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,4 +4,12 @@ class ApplicationController < ActionController::Base # Changes to the importmap will invalidate the etag for HTML responses stale_when_importmap_changes + + before_action :ensure_conversation_id + + private + + def ensure_conversation_id + session[:conversation_id] ||= SecureRandom.uuid + end end diff --git a/app/controllers/chats_controller.rb b/app/controllers/chats_controller.rb new file mode 100644 index 0000000..5fe812f --- /dev/null +++ b/app/controllers/chats_controller.rb @@ -0,0 +1,70 @@ +class ChatsController < ApplicationController + before_action :set_chat, only: %i[ show edit update destroy ] + + # GET /chats or /chats.json + def index + @chats = Chat.all + end + + # GET /chats/1 or /chats/1.json + def show + end + + # GET /chats/new + def new + @chat = Chat.new + end + + # GET /chats/1/edit + def edit + end + + # POST /chats or /chats.json + def create + @chat = Chat.new(chat_params) + + respond_to do |format| + if @chat.save + format.html { redirect_to @chat, notice: "Chat was successfully created." } + format.json { render :show, status: :created, location: @chat } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @chat.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /chats/1 or /chats/1.json + def update + respond_to do |format| + if @chat.update(chat_params) + format.html { redirect_to @chat, notice: "Chat was successfully updated.", status: :see_other } + format.json { render :show, status: :ok, location: @chat } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @chat.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /chats/1 or /chats/1.json + def destroy + @chat.destroy! + + respond_to do |format| + format.html { redirect_to chats_path, notice: "Chat was successfully destroyed.", status: :see_other } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_chat + @chat = Chat.find(params.expect(:id)) + end + + # Only allow a list of trusted parameters through. + def chat_params + params.fetch(:chat, {}) + end +end diff --git a/app/helpers/chats_helper.rb b/app/helpers/chats_helper.rb new file mode 100644 index 0000000..cf92be3 --- /dev/null +++ b/app/helpers/chats_helper.rb @@ -0,0 +1,2 @@ +module ChatsHelper +end diff --git a/app/models/chat.rb b/app/models/chat.rb new file mode 100644 index 0000000..9fcfdd3 --- /dev/null +++ b/app/models/chat.rb @@ -0,0 +1,2 @@ +class Chat < ApplicationRecord +end diff --git a/app/views/chats/_chat.html.erb b/app/views/chats/_chat.html.erb new file mode 100644 index 0000000..edc9538 --- /dev/null +++ b/app/views/chats/_chat.html.erb @@ -0,0 +1,2 @@ +
+
diff --git a/app/views/chats/_chat.json.jbuilder b/app/views/chats/_chat.json.jbuilder new file mode 100644 index 0000000..2dd791d --- /dev/null +++ b/app/views/chats/_chat.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! chat, :id, :created_at, :updated_at +json.url chat_url(chat, format: :json) diff --git a/app/views/chats/_form.html.erb b/app/views/chats/_form.html.erb new file mode 100644 index 0000000..2f14097 --- /dev/null +++ b/app/views/chats/_form.html.erb @@ -0,0 +1,17 @@ +<%= form_with(model: chat) do |form| %> + <% if chat.errors.any? %> +
+

<%= pluralize(chat.errors.count, "error") %> prohibited this chat from being saved:

+ +
    + <% chat.errors.each do |error| %> +
  • <%= error.full_message %>
  • + <% end %> +
+
+ <% end %> + +
+ <%= form.submit %> +
+<% end %> diff --git a/app/views/chats/edit.html.erb b/app/views/chats/edit.html.erb new file mode 100644 index 0000000..081b9dd --- /dev/null +++ b/app/views/chats/edit.html.erb @@ -0,0 +1,12 @@ +<% content_for :title, "Editing chat" %> + +

Editing chat

+ +<%= render "form", chat: @chat %> + +
+ +
+ <%= link_to "Show this chat", @chat %> | + <%= link_to "Back to chats", chats_path %> +
diff --git a/app/views/chats/index.html.erb b/app/views/chats/index.html.erb new file mode 100644 index 0000000..b1ff6e5 --- /dev/null +++ b/app/views/chats/index.html.erb @@ -0,0 +1,16 @@ +

<%= notice %>

+ +<% content_for :title, "Chats" %> + +

Chats

+ +
+ <% @chats.each do |chat| %> + <%= render chat %> +

+ <%= link_to "Show this chat", chat %> +

+ <% end %> +
+ +<%= link_to "New chat", new_chat_path %> diff --git a/app/views/chats/index.json.jbuilder b/app/views/chats/index.json.jbuilder new file mode 100644 index 0000000..d2e78c5 --- /dev/null +++ b/app/views/chats/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @chats, partial: "chats/chat", as: :chat diff --git a/app/views/chats/new.html.erb b/app/views/chats/new.html.erb new file mode 100644 index 0000000..69e22b5 --- /dev/null +++ b/app/views/chats/new.html.erb @@ -0,0 +1,11 @@ +<% content_for :title, "New chat" %> + +

New chat

+ +<%= render "form", chat: @chat %> + +
+ +
+ <%= link_to "Back to chats", chats_path %> +
diff --git a/app/views/chats/show.html.erb b/app/views/chats/show.html.erb new file mode 100644 index 0000000..42cab1a --- /dev/null +++ b/app/views/chats/show.html.erb @@ -0,0 +1,10 @@ +

<%= notice %>

+ +<%= render @chat %> + +
+ <%= link_to "Edit this chat", edit_chat_path(@chat) %> | + <%= link_to "Back to chats", chats_path %> + + <%= button_to "Destroy this chat", @chat, method: :delete %> +
diff --git a/app/views/chats/show.json.jbuilder b/app/views/chats/show.json.jbuilder new file mode 100644 index 0000000..c480788 --- /dev/null +++ b/app/views/chats/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "chats/chat", chat: @chat diff --git a/config/routes.rb b/config/routes.rb index 6b25521..995ab48 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,4 +1,5 @@ Rails.application.routes.draw do + resources :chats # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/migrate/20251028025508_create_chats.rb b/db/migrate/20251028025508_create_chats.rb new file mode 100644 index 0000000..07c436f --- /dev/null +++ b/db/migrate/20251028025508_create_chats.rb @@ -0,0 +1,7 @@ +class CreateChats < ActiveRecord::Migration[8.1] + def change + create_table :chats do |t| + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..d24b313 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,21 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 2025_10_28_025508) do + # These are extensions that must be enabled in order to support this database + enable_extension "pg_catalog.plpgsql" + + create_table "chats", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end +end diff --git a/test/controllers/chats_controller_test.rb b/test/controllers/chats_controller_test.rb new file mode 100644 index 0000000..957ce78 --- /dev/null +++ b/test/controllers/chats_controller_test.rb @@ -0,0 +1,48 @@ +require "test_helper" + +class ChatsControllerTest < ActionDispatch::IntegrationTest + setup do + @chat = chats(:one) + end + + test "should get index" do + get chats_url + assert_response :success + end + + test "should get new" do + get new_chat_url + assert_response :success + end + + test "should create chat" do + assert_difference("Chat.count") do + post chats_url, params: { chat: {} } + end + + assert_redirected_to chat_url(Chat.last) + end + + test "should show chat" do + get chat_url(@chat) + assert_response :success + end + + test "should get edit" do + get edit_chat_url(@chat) + assert_response :success + end + + test "should update chat" do + patch chat_url(@chat), params: { chat: {} } + assert_redirected_to chat_url(@chat) + end + + test "should destroy chat" do + assert_difference("Chat.count", -1) do + delete chat_url(@chat) + end + + assert_redirected_to chats_url + end +end diff --git a/test/fixtures/chats.yml b/test/fixtures/chats.yml new file mode 100644 index 0000000..d7a3329 --- /dev/null +++ b/test/fixtures/chats.yml @@ -0,0 +1,11 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the "{}" from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/models/chat_test.rb b/test/models/chat_test.rb new file mode 100644 index 0000000..69b9d2c --- /dev/null +++ b/test/models/chat_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class ChatTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end From 463e1adfe45e3e8a76107c364d5e3de9c6f35c4f Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 20:26:25 -0700 Subject: [PATCH 05/16] rails genereate spectre:install files --- app/spectre/prompts/rag/system.yml.erb | 5 +++++ app/spectre/prompts/rag/user.yml.erb | 3 +++ config/initializers/spectre.rb | 25 +++++++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 app/spectre/prompts/rag/system.yml.erb create mode 100644 app/spectre/prompts/rag/user.yml.erb create mode 100644 config/initializers/spectre.rb diff --git a/app/spectre/prompts/rag/system.yml.erb b/app/spectre/prompts/rag/system.yml.erb new file mode 100644 index 0000000..a77fb2d --- /dev/null +++ b/app/spectre/prompts/rag/system.yml.erb @@ -0,0 +1,5 @@ +system: | + You are a helpful assistant designed to provide answers based on specific documents and context provided to you. + Follow these guidelines: + 1. Only provide answers based on the context provided to you. + 2. Never mention the context directly in your responses. diff --git a/app/spectre/prompts/rag/user.yml.erb b/app/spectre/prompts/rag/user.yml.erb new file mode 100644 index 0000000..6ebd97d --- /dev/null +++ b/app/spectre/prompts/rag/user.yml.erb @@ -0,0 +1,3 @@ +user: | + User's query: <%= @query %> + Context: <%= @objects.join(", ") %> diff --git a/config/initializers/spectre.rb b/config/initializers/spectre.rb new file mode 100644 index 0000000..485f72c --- /dev/null +++ b/config/initializers/spectre.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spectre' + +Spectre.setup do |config| + # Chose your LLM (openai, ollama, claude, gemini) + config.default_llm_provider = :openai + + config.openai do |openai| + openai.api_key = ENV['OPENAI_API_KEY'] + end + + config.ollama do |ollama| + ollama.host = ENV['OLLAMA_HOST'] + ollama.api_key = ENV['OLLAMA_API_KEY'] + end + + config.claude do |claude| + claude.api_key = ENV['ANTHROPIC_API_KEY'] + end + + config.gemini do |gemini| + gemini.api_key = ENV['GEMINI_API_KEY'] + end +end From 4f011ab72e0e82671550cde2b3c09c705d981759 Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 21:34:42 -0700 Subject: [PATCH 06/16] Add .env to docker compose --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 54aeadc..201b05d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,9 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: app_development REDIS_URL: redis://redis:6379/0 + OPENAI_API_KEY: ${OPENAI_API_KEY} + env_file: + - .env ports: - "3000:3000" depends_on: From cb237e72c295767a9fa38af1673e3c588b0bb3ca Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 21:38:20 -0700 Subject: [PATCH 07/16] Add Sidekiq job for external OpenAI jobs --- ARCHITECTURE.md | 1 + app/jobs/spectre_openai_job.rb | 20 ++++++++++++++++++++ config/sidekiq.yml | 2 +- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 app/jobs/spectre_openai_job.rb diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 71a9308..6e4940c 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -54,6 +54,7 @@ The chat interface uses Ruby Spectre gem to interface with the OpenAI foundation # Future Work +- Add a new llm queue for llm sidekiq jobs and configure priority - Add Roadauth user authentication and user table, tie chats to user - Add chat session tab feature with new data table modeling (chat, questions, responses) - Add Tailwind for nice UI layout diff --git a/app/jobs/spectre_openai_job.rb b/app/jobs/spectre_openai_job.rb new file mode 100644 index 0000000..b6f33a8 --- /dev/null +++ b/app/jobs/spectre_openai_job.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class SpectreOpenaiJob + include Sidekiq::Job + + def perform(prompt) + messages = [ + { role: "system", content: "You are a concise assistant." }, + { role: "user", content: prompt } + ] + + result = Spectre.provider_module::Completions.create( + messages: messages, + model: "gpt-4", + openai: { max_tokens: 60 } + ) + + puts result[:content] + end +end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index cbdd109..4855818 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -1,3 +1,3 @@ :concurrency: <%= Integer(ENV.fetch("SIDEKIQ_CONCURRENCY", 5)) %> :queues: - - default + - default \ No newline at end of file From baea057a8db79d4b36aff39972782218cbb9127b Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Mon, 27 Oct 2025 21:38:41 -0700 Subject: [PATCH 08/16] Add note for error handling --- ARCHITECTURE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6e4940c..7343c32 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -55,6 +55,7 @@ The chat interface uses Ruby Spectre gem to interface with the OpenAI foundation # Future Work - Add a new llm queue for llm sidekiq jobs and configure priority +- Add SpectreOpenAIJob error handling - Add Roadauth user authentication and user table, tie chats to user - Add chat session tab feature with new data table modeling (chat, questions, responses) - Add Tailwind for nice UI layout From 3de80e5795bd56544511dad8d84be0d0db6cdccd Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Tue, 28 Oct 2025 08:39:52 -0700 Subject: [PATCH 09/16] Add redis in Rails for Turbo Stream --- Gemfile | 3 +++ Gemfile.lock | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index 563d1da..c6d7b5c 100644 --- a/Gemfile +++ b/Gemfile @@ -20,6 +20,9 @@ gem "jbuilder" # Enable interface with text generative OpenAI foundation model gem 'spectre_ai' +# Enable Action cable redis pubsub adapter +gem 'redis' + # Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] # gem "bcrypt", "~> 3.1.7" diff --git a/Gemfile.lock b/Gemfile.lock index 687d89e..efa04f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -237,6 +237,8 @@ GEM erb psych (>= 4.0.0) tsort + redis (5.4.1) + redis-client (>= 0.22.0) redis-client (0.26.1) connection_pool regexp_parser (2.11.3) @@ -310,6 +312,7 @@ DEPENDENCIES propshaft puma (>= 5.0) rails (~> 8.1.0) + redis selenium-webdriver sidekiq spectre_ai From 52cbf10e80ebcaa493e96f4462295ae214ec861c Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Tue, 28 Oct 2025 09:03:04 -0700 Subject: [PATCH 10/16] Add OpenAI API key handling instructions --- .env.example | 1 + .gitignore | 1 + ARCHITECTURE.md | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f94fa37 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +OPENAI_API_KEY=your_openai_api_key diff --git a/.gitignore b/.gitignore index 3bd2d44..5831cc1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ # Ignore all environment files. /.env* +!.env.example # Ignore all logfiles and tempfiles. /log/* diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 7343c32..02d8152 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -52,6 +52,22 @@ Effectively, each chat will be tied to a per-browser session[conversation_id]. The chat interface uses Ruby Spectre gem to interface with the OpenAI foundation model. +Add the OpenAI API key to the app root directory ~/.env file + +or + +If there's no .env file in app root directory yet, add the OpenAI API key to .env.example + +```vim +OPENAI_API_KEY=your_openai_api_key +``` + +Then copy .env.example to .env + +```bash +cp .env.example .env +``` + # Future Work - Add a new llm queue for llm sidekiq jobs and configure priority @@ -59,4 +75,4 @@ The chat interface uses Ruby Spectre gem to interface with the OpenAI foundation - Add Roadauth user authentication and user table, tie chats to user - Add chat session tab feature with new data table modeling (chat, questions, responses) - Add Tailwind for nice UI layout -- Cleanup `rails generate scaffold chat` files \ No newline at end of file +- Cleanup `rails generate scaffold chat` files From dbdab1ba8466803438451c065e7a6b702e3fe53e Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Tue, 28 Oct 2025 13:24:11 -0700 Subject: [PATCH 11/16] Add turbo-stream, minor code cleanup, update architecture.md --- ARCHITECTURE.md | 10 ++++++++-- app/controllers/application_controller.rb | 8 -------- app/controllers/chats_controller.rb | 19 +++++++++---------- app/jobs/spectre_openai_job.rb | 7 ++++++- app/views/chats/_chat.html.erb | 5 +++-- app/views/chats/index.html.erb | 18 +++++++++--------- docker-compose.yml | 3 +++ 7 files changed, 38 insertions(+), 32 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 02d8152..c94056e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,6 +2,10 @@ Web chat interface to chatGPT + +## Application +Visit http://0.0.0.0:3000/chats for the web chat interface to chatGPT + # System Design ## Chat Interface System Sequence Diagram @@ -70,9 +74,11 @@ cp .env.example .env # Future Work -- Add a new llm queue for llm sidekiq jobs and configure priority - Add SpectreOpenAIJob error handling -- Add Roadauth user authentication and user table, tie chats to user +- Add Roadauth user authentication and user table, tie chats to user sessions - Add chat session tab feature with new data table modeling (chat, questions, responses) - Add Tailwind for nice UI layout - Cleanup `rails generate scaffold chat` files +- Add sidekiq worker tests and chat controller tests +- Update system sequence diagrams (include all turbo-stream calls) +- Add persistence table design for users, questions and answers session \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index ea5f046..c353756 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,12 +4,4 @@ class ApplicationController < ActionController::Base # Changes to the importmap will invalidate the etag for HTML responses stale_when_importmap_changes - - before_action :ensure_conversation_id - - private - - def ensure_conversation_id - session[:conversation_id] ||= SecureRandom.uuid - end end diff --git a/app/controllers/chats_controller.rb b/app/controllers/chats_controller.rb index 5fe812f..9119660 100644 --- a/app/controllers/chats_controller.rb +++ b/app/controllers/chats_controller.rb @@ -21,17 +21,16 @@ def edit # POST /chats or /chats.json def create - @chat = Chat.new(chat_params) + user_prompt = params.require(:user_prompt) - respond_to do |format| - if @chat.save - format.html { redirect_to @chat, notice: "Chat was successfully created." } - format.json { render :show, status: :created, location: @chat } - else - format.html { render :new, status: :unprocessable_entity } - format.json { render json: @chat.errors, status: :unprocessable_entity } - end - end + Turbo::StreamsChannel.broadcast_append_to( + "chat", + target: "messages", + partial: "chats/chat", + locals: { user_prompt: user_prompt } + ) + + SpectreOpenaiJob.perform_async(user_prompt) end # PATCH/PUT /chats/1 or /chats/1.json diff --git a/app/jobs/spectre_openai_job.rb b/app/jobs/spectre_openai_job.rb index b6f33a8..f820200 100644 --- a/app/jobs/spectre_openai_job.rb +++ b/app/jobs/spectre_openai_job.rb @@ -15,6 +15,11 @@ def perform(prompt) openai: { max_tokens: 60 } ) - puts result[:content] + Turbo::StreamsChannel.broadcast_append_to( + "chat", + target: "messages", + partial: "chats/chat", + locals: { user_prompt: result[:content] } + ) end end diff --git a/app/views/chats/_chat.html.erb b/app/views/chats/_chat.html.erb index edc9538..64f2da3 100644 --- a/app/views/chats/_chat.html.erb +++ b/app/views/chats/_chat.html.erb @@ -1,2 +1,3 @@ -
-
+
+
<%= user_prompt %>
+
\ No newline at end of file diff --git a/app/views/chats/index.html.erb b/app/views/chats/index.html.erb index b1ff6e5..c053a97 100644 --- a/app/views/chats/index.html.erb +++ b/app/views/chats/index.html.erb @@ -2,15 +2,15 @@ <% content_for :title, "Chats" %> -

Chats

+

OpenAI Chat Interface

-
- <% @chats.each do |chat| %> - <%= render chat %> -

- <%= link_to "Show this chat", chat %> -

- <% end %> +<%= turbo_stream_from "chat" %> + +
+
-<%= link_to "New chat", new_chat_path %> +<%= form_with url: chats_path do |f| %> + <%= f.text_area :user_prompt, rows: 2, placeholder: "What's on your mind?" %> + <%= f.submit "Send" %> +<% end %> diff --git a/docker-compose.yml b/docker-compose.yml index 201b05d..77c9753 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,6 +44,9 @@ services: POSTGRES_PASSWORD: postgres POSTGRES_DB: app_development REDIS_URL: redis://redis:6379/0 + OPENAI_API_KEY: ${OPENAI_API_KEY} + env_file: + - .env depends_on: db: condition: service_healthy From eeb76f9cedb352d0c352bb6bf98c03a0cfff785c Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Tue, 28 Oct 2025 14:37:52 -0700 Subject: [PATCH 12/16] Remove deprecated design decision section --- ARCHITECTURE.md | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c94056e..bfc2844 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,6 @@ Web chat interface to chatGPT - ## Application Visit http://0.0.0.0:3000/chats for the web chat interface to chatGPT @@ -41,17 +40,6 @@ sequenceDiagram T-->>F: Realtime update (new AI message displayed) ``` -## Design Decisions - -### Decision: Session Handling Strategy - -A unique SecureRandom.uuid will be generated per-browser session. - -The SecureRandom.uuid will be stored in session[conversation_id]. - -Effectively, each chat will be tied to a per-browser session[conversation_id]. - - # Integration & External Services The chat interface uses Ruby Spectre gem to interface with the OpenAI foundation model. From 64d5305ddb18cbc48687797503c2216d76a97e54 Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Wed, 29 Oct 2025 15:58:00 -0700 Subject: [PATCH 13/16] Add Procfile for sidekiq config Heroku prod deploy --- Procfile | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Procfile diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..fd4fe01 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: bundle exec puma -C config/puma.rb +worker: bundle exec sidekiq -C config/sidekiq.yml From 9f3b38410a45a19f34643861e84711188e67698d Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Wed, 29 Oct 2025 16:26:01 -0700 Subject: [PATCH 14/16] Add paragraph wrapping & prefix gpt responses --- app/assets/stylesheets/application.css | 8 ++++++++ app/jobs/spectre_openai_job.rb | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index fe93333..90fde16 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -8,3 +8,11 @@ * * Consider organizing styles into separate files for maintainability. */ + +.message pre { + white-space: pre-wrap; /* Preserve line breaks, allow wrapping */ + word-wrap: break-word; /* Break long words if necessary */ + overflow-wrap: anywhere; /* Modern: break at any character if needed */ + max-width: 100%; /* Never exceed parent width */ + box-sizing: border-box; /* Include padding in width calc */ +} \ No newline at end of file diff --git a/app/jobs/spectre_openai_job.rb b/app/jobs/spectre_openai_job.rb index f820200..2ab6cb2 100644 --- a/app/jobs/spectre_openai_job.rb +++ b/app/jobs/spectre_openai_job.rb @@ -19,7 +19,7 @@ def perform(prompt) "chat", target: "messages", partial: "chats/chat", - locals: { user_prompt: result[:content] } + locals: { user_prompt: "ChatGPT-4: #{result[:content]}" } ) end end From 0d06acc65e35199f65da64808ea25258b52c83bd Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Thu, 30 Oct 2025 08:50:39 -0700 Subject: [PATCH 15/16] Additional design: add service & broadcaster objects, progress bar partial --- ARCHITECTURE.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bfc2844..1dc4990 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -62,11 +62,15 @@ cp .env.example .env # Future Work +- Add a external service object OpenAI fetcher for external API fetch (app/services/external/openai.rb) +- Add a message broadcaster object for handling Turbo stream broadcasts (app/broadcasters/message_broadcaster.rb) +- Add turbo partial for progress bar or loading... indicator - Add SpectreOpenAIJob error handling - Add Roadauth user authentication and user table, tie chats to user sessions - Add chat session tab feature with new data table modeling (chat, questions, responses) - Add Tailwind for nice UI layout - Cleanup `rails generate scaffold chat` files -- Add sidekiq worker tests and chat controller tests -- Update system sequence diagrams (include all turbo-stream calls) -- Add persistence table design for users, questions and answers session \ No newline at end of file +- Add sidekiq worker, chat controller, service object, and broadcaster object tests +- Add capybara end to end system test +- Update system sequence diagrams (include all turbo-stream calls, call parameters, service & broadcaster objects) +- Add persistence table design for users, questions and answers session From b8b455b34447d3aea23fe413bbf70f2a4391d2bd Mon Sep 17 00:00:00 2001 From: "Bryan B. Cabalo" Date: Thu, 30 Oct 2025 09:06:47 -0700 Subject: [PATCH 16/16] Add a few more design cleanup items to .md --- ARCHITECTURE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1dc4990..2ff0e85 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -65,7 +65,10 @@ cp .env.example .env - Add a external service object OpenAI fetcher for external API fetch (app/services/external/openai.rb) - Add a message broadcaster object for handling Turbo stream broadcasts (app/broadcasters/message_broadcaster.rb) - Add turbo partial for progress bar or loading... indicator +- Place stylesheets in app/assets/stylesheets/pages/chat_interface.css and import files in application.css - Add SpectreOpenAIJob error handling +- Add prompt validation for user input (mirror at minimum chatGPT prompt validation, excessive spam, SQL injection) +- Add end to end message encryption - Add Roadauth user authentication and user table, tie chats to user sessions - Add chat session tab feature with new data table modeling (chat, questions, responses) - Add Tailwind for nice UI layout