langsmithrb_rails 0.1.0 → 0.1.1
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/.rspec +3 -0
- data/.rspec_status +82 -0
- data/CHANGELOG.md +25 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +321 -0
- data/LICENSE +21 -0
- data/README.md +268 -0
- data/Rakefile +10 -0
- data/langsmithrb_rails-0.1.0.gem +0 -0
- data/langsmithrb_rails.gemspec +45 -0
- data/lib/generators/langsmithrb_rails/buffer/buffer_generator.rb +94 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/create_langsmith_run_buffers.rb +29 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/flush_buffer_job.rb +40 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/langsmith.rake +71 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/langsmith_run_buffer.rb +70 -0
- data/lib/generators/langsmithrb_rails/buffer/templates/migration.rb +28 -0
- data/lib/generators/langsmithrb_rails/ci/ci_generator.rb +37 -0
- data/lib/generators/langsmithrb_rails/ci/templates/langsmith-evals.yml +85 -0
- data/lib/generators/langsmithrb_rails/ci/templates/langsmith_export_summary.rb +81 -0
- data/lib/generators/langsmithrb_rails/demo/demo_generator.rb +81 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.js +88 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_controller.rb +58 -0
- data/lib/generators/langsmithrb_rails/demo/templates/chat_message.rb +24 -0
- data/lib/generators/langsmithrb_rails/demo/templates/create_chat_messages.rb +19 -0
- data/lib/generators/langsmithrb_rails/demo/templates/index.html.erb +180 -0
- data/lib/generators/langsmithrb_rails/demo/templates/llm_service.rb +165 -0
- data/lib/generators/langsmithrb_rails/evals/evals_generator.rb +52 -0
- data/lib/generators/langsmithrb_rails/evals/templates/checks/correctness.rb +71 -0
- data/lib/generators/langsmithrb_rails/evals/templates/checks/llm_graded.rb +137 -0
- data/lib/generators/langsmithrb_rails/evals/templates/datasets/sample.yml +60 -0
- data/lib/generators/langsmithrb_rails/evals/templates/langsmith_evals.rake +255 -0
- data/lib/generators/langsmithrb_rails/evals/templates/targets/http.rb +120 -0
- data/lib/generators/langsmithrb_rails/evals/templates/targets/ruby.rb +136 -0
- data/lib/generators/langsmithrb_rails/install/install_generator.rb +35 -0
- data/lib/generators/langsmithrb_rails/install/templates/config.yml +45 -0
- data/lib/generators/langsmithrb_rails/install/templates/initializer.rb +34 -0
- data/lib/generators/langsmithrb_rails/privacy/privacy_generator.rb +39 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/custom_redactor.rb +132 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/privacy.yml +88 -0
- data/lib/generators/langsmithrb_rails/privacy/templates/privacy_initializer.rb +41 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced.rb +146 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/langsmith_traced_job.rb +151 -0
- data/lib/generators/langsmithrb_rails/tracing/templates/request_tracing.rb +117 -0
- data/lib/generators/langsmithrb_rails/tracing/tracing_generator.rb +78 -0
- data/lib/langsmithrb_rails/client.rb +77 -0
- data/lib/langsmithrb_rails/config.rb +72 -0
- data/lib/langsmithrb_rails/generators/langsmithrb_rails/langsmith_generator.rb +61 -0
- data/lib/langsmithrb_rails/generators/langsmithrb_rails/templates/langsmith_initializer.rb +22 -0
- data/lib/langsmithrb_rails/langsmith.rb +35 -0
- data/lib/langsmithrb_rails/railtie.rb +33 -0
- data/lib/langsmithrb_rails/redactor.rb +76 -0
- data/lib/langsmithrb_rails/version.rb +5 -0
- data/lib/langsmithrb_rails.rb +31 -0
- metadata +59 -6
@@ -0,0 +1,85 @@
|
|
1
|
+
name: LangSmith Evaluations
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
branches: [ main, master ]
|
6
|
+
workflow_dispatch:
|
7
|
+
inputs:
|
8
|
+
dataset:
|
9
|
+
description: 'Evaluation dataset to use'
|
10
|
+
required: true
|
11
|
+
default: 'sample'
|
12
|
+
target:
|
13
|
+
description: 'Evaluation target to use'
|
14
|
+
required: true
|
15
|
+
default: 'http'
|
16
|
+
experiment_name:
|
17
|
+
description: 'Experiment name'
|
18
|
+
required: false
|
19
|
+
default: ''
|
20
|
+
|
21
|
+
jobs:
|
22
|
+
evaluate:
|
23
|
+
name: Run LangSmith Evaluations
|
24
|
+
runs-on: ubuntu-latest
|
25
|
+
|
26
|
+
env:
|
27
|
+
RAILS_ENV: test
|
28
|
+
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
|
29
|
+
LANGSMITH_PROJECT: ${{ secrets.LANGSMITH_PROJECT || 'ci-evaluations' }}
|
30
|
+
LANGSMITH_EVAL_THRESHOLD: 0.7
|
31
|
+
LANGSMITH_EVAL_TARGET_URL: ${{ secrets.LANGSMITH_EVAL_TARGET_URL || 'http://localhost:3000/api/v1/evaluate' }}
|
32
|
+
|
33
|
+
steps:
|
34
|
+
- name: Checkout code
|
35
|
+
uses: actions/checkout@v3
|
36
|
+
|
37
|
+
- name: Set up Ruby
|
38
|
+
uses: ruby/setup-ruby@v1
|
39
|
+
with:
|
40
|
+
ruby-version: '3.2'
|
41
|
+
bundler-cache: true
|
42
|
+
|
43
|
+
- name: Setup database
|
44
|
+
run: |
|
45
|
+
bundle exec rails db:create
|
46
|
+
bundle exec rails db:schema:load
|
47
|
+
|
48
|
+
- name: Set experiment name
|
49
|
+
run: |
|
50
|
+
if [ -z "${{ github.event.inputs.experiment_name }}" ]; then
|
51
|
+
echo "EXPERIMENT_NAME=pr_${{ github.event.pull_request.number || github.run_id }}" >> $GITHUB_ENV
|
52
|
+
else
|
53
|
+
echo "EXPERIMENT_NAME=${{ github.event.inputs.experiment_name }}" >> $GITHUB_ENV
|
54
|
+
fi
|
55
|
+
|
56
|
+
- name: Run evaluations
|
57
|
+
run: |
|
58
|
+
bundle exec rails langsmith:eval[${{ github.event.inputs.dataset || 'sample' }},${{ github.event.inputs.target || 'http' }},${{ env.EXPERIMENT_NAME }}]
|
59
|
+
|
60
|
+
- name: Generate evaluation summary
|
61
|
+
run: |
|
62
|
+
ruby script/ci/langsmith_export_summary.rb > evaluation_summary.md
|
63
|
+
|
64
|
+
- name: Upload evaluation results
|
65
|
+
uses: actions/upload-artifact@v3
|
66
|
+
with:
|
67
|
+
name: langsmith-evaluation-results
|
68
|
+
path: |
|
69
|
+
tmp/langsmith/last_eval.json
|
70
|
+
evaluation_summary.md
|
71
|
+
|
72
|
+
- name: Comment on PR
|
73
|
+
if: github.event_name == 'pull_request'
|
74
|
+
uses: actions/github-script@v6
|
75
|
+
with:
|
76
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
77
|
+
script: |
|
78
|
+
const fs = require('fs');
|
79
|
+
const summary = fs.readFileSync('evaluation_summary.md', 'utf8');
|
80
|
+
github.rest.issues.createComment({
|
81
|
+
issue_number: context.issue.number,
|
82
|
+
owner: context.repo.owner,
|
83
|
+
repo: context.repo.repo,
|
84
|
+
body: summary
|
85
|
+
});
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "json"
|
5
|
+
require "fileutils"
|
6
|
+
|
7
|
+
# Path to the evaluation results
|
8
|
+
results_path = File.join(Dir.pwd, "tmp/langsmith/last_eval.json")
|
9
|
+
|
10
|
+
unless File.exist?(results_path)
|
11
|
+
puts "## LangSmith Evaluation Results"
|
12
|
+
puts
|
13
|
+
puts "⚠️ No evaluation results found. Please run evaluations first."
|
14
|
+
exit 0
|
15
|
+
end
|
16
|
+
|
17
|
+
# Load the results
|
18
|
+
results = JSON.parse(File.read(results_path))
|
19
|
+
|
20
|
+
# Generate summary
|
21
|
+
puts "## LangSmith Evaluation Results"
|
22
|
+
puts
|
23
|
+
puts "### Summary"
|
24
|
+
puts
|
25
|
+
puts "- **Dataset**: #{results['dataset']}"
|
26
|
+
puts "- **Target**: #{results['target']}"
|
27
|
+
puts "- **Experiment**: #{results['experiment']}"
|
28
|
+
puts "- **Timestamp**: #{results['timestamp']}"
|
29
|
+
puts "- **Passed**: #{results['summary']['passed']}/#{results['summary']['total']} (#{(results['summary']['passed'].to_f / results['summary']['total'] * 100).round(1)}%)"
|
30
|
+
puts "- **Average Score**: #{(results['summary']['avg_score'] * 100).round(1)}%"
|
31
|
+
puts
|
32
|
+
|
33
|
+
# Determine overall status
|
34
|
+
threshold = ENV["LANGSMITH_EVAL_THRESHOLD"] ? ENV["LANGSMITH_EVAL_THRESHOLD"].to_f : 0.7
|
35
|
+
threshold_percent = (threshold * 100).round(1)
|
36
|
+
|
37
|
+
if results['summary']['avg_score'] >= threshold
|
38
|
+
puts "✅ **PASSED**: Score exceeds threshold of #{threshold_percent}%"
|
39
|
+
else
|
40
|
+
puts "❌ **FAILED**: Score below threshold of #{threshold_percent}%"
|
41
|
+
end
|
42
|
+
|
43
|
+
puts
|
44
|
+
|
45
|
+
# Show top 5 items with lowest scores
|
46
|
+
puts "### Lowest Scoring Items"
|
47
|
+
puts
|
48
|
+
|
49
|
+
items_with_scores = results['items'].map do |item|
|
50
|
+
total_score = 0.0
|
51
|
+
item['checks'].each do |_name, check|
|
52
|
+
total_score += check['score']
|
53
|
+
end
|
54
|
+
avg_score = item['checks'].empty? ? 0.0 : (total_score / item['checks'].size)
|
55
|
+
|
56
|
+
[item['id'], avg_score, item]
|
57
|
+
end
|
58
|
+
|
59
|
+
items_with_scores.sort_by! { |_, score, _| score }
|
60
|
+
|
61
|
+
puts "| ID | Score | Input | Status |"
|
62
|
+
puts "| --- | --- | --- | --- |"
|
63
|
+
|
64
|
+
items_with_scores.first(5).each do |id, score, item|
|
65
|
+
input_summary = item['input'].map { |k, v| "#{k}: #{v.to_s[0..30]}..." }.join(", ")
|
66
|
+
status = item['passed'] ? "✅" : "❌"
|
67
|
+
puts "| #{id} | #{(score * 100).round(1)}% | #{input_summary} | #{status} |"
|
68
|
+
end
|
69
|
+
|
70
|
+
puts
|
71
|
+
|
72
|
+
# Link to full results
|
73
|
+
puts "### Detailed Results"
|
74
|
+
puts
|
75
|
+
puts "Full results are available as an artifact in the GitHub Actions workflow."
|
76
|
+
puts
|
77
|
+
puts "To view the results locally, run:"
|
78
|
+
puts
|
79
|
+
puts "```bash"
|
80
|
+
puts "cat tmp/langsmith/last_eval.json | jq"
|
81
|
+
puts "```"
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LangsmithrbRails
|
4
|
+
module Generators
|
5
|
+
# Generator for adding a demo LLM application with LangSmith tracing
|
6
|
+
class DemoGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
|
9
|
+
desc "Adds a demo LLM application with LangSmith tracing"
|
10
|
+
|
11
|
+
class_option :provider, type: :string, default: "openai",
|
12
|
+
desc: "LLM provider to use (openai, anthropic, or mock)"
|
13
|
+
|
14
|
+
def check_dependencies
|
15
|
+
# Check if the required gems are installed
|
16
|
+
unless options[:provider] == "mock"
|
17
|
+
gem_name = options[:provider] == "anthropic" ? "anthropic" : "ruby-openai"
|
18
|
+
|
19
|
+
begin
|
20
|
+
require options[:provider] == "anthropic" ? "anthropic" : "openai"
|
21
|
+
rescue LoadError
|
22
|
+
say_status :error, "#{gem_name} gem is required for this generator", :red
|
23
|
+
say "Please add the following to your Gemfile and run bundle install:", :yellow
|
24
|
+
say "gem '#{gem_name}'", :yellow
|
25
|
+
exit 1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_service
|
31
|
+
template "llm_service.rb", "app/services/llm_service.rb"
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_controller
|
35
|
+
template "chat_controller.rb", "app/controllers/chat_controller.rb"
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_views
|
39
|
+
empty_directory "app/views/chat"
|
40
|
+
template "index.html.erb", "app/views/chat/index.html.erb"
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_model
|
44
|
+
template "chat_message.rb", "app/models/chat_message.rb"
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_migration
|
48
|
+
migration_template "create_chat_messages.rb", "db/migrate/create_chat_messages.rb"
|
49
|
+
end
|
50
|
+
|
51
|
+
def update_routes
|
52
|
+
route "resources :chat, only: [:index, :create]"
|
53
|
+
end
|
54
|
+
|
55
|
+
def create_javascript
|
56
|
+
empty_directory "app/javascript/controllers"
|
57
|
+
template "chat_controller.js", "app/javascript/controllers/chat_controller.js"
|
58
|
+
end
|
59
|
+
|
60
|
+
def display_post_install_message
|
61
|
+
say "\n"
|
62
|
+
say "LangSmith demo application has been added to your Rails application! 🎉", :green
|
63
|
+
say "\n"
|
64
|
+
say "This adds:", :yellow
|
65
|
+
say " 1. LLM service with LangSmith tracing", :yellow
|
66
|
+
say " 2. Chat controller and views", :yellow
|
67
|
+
say " 3. Chat message model and migration", :yellow
|
68
|
+
say "\n"
|
69
|
+
say "To set up the demo:", :yellow
|
70
|
+
say " 1. Run the migration: bin/rails db:migrate", :yellow
|
71
|
+
say " 2. Configure your LLM provider API key in your environment", :yellow
|
72
|
+
say " 3. Start your Rails server: bin/rails server", :yellow
|
73
|
+
say " 4. Visit http://localhost:3000/chat", :yellow
|
74
|
+
say "\n"
|
75
|
+
say "For OpenAI, set OPENAI_API_KEY in your environment", :yellow
|
76
|
+
say "For Anthropic, set ANTHROPIC_API_KEY in your environment", :yellow
|
77
|
+
say "\n"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
// Chat controller for Stimulus.js
|
2
|
+
import { Controller } from "@hotwired/stimulus"
|
3
|
+
|
4
|
+
export default class extends Controller {
|
5
|
+
static targets = ["input", "submitButton"]
|
6
|
+
|
7
|
+
connect() {
|
8
|
+
// Scroll to bottom of messages on load
|
9
|
+
this.scrollToBottom()
|
10
|
+
|
11
|
+
// Setup keyboard shortcuts
|
12
|
+
this.inputTarget.addEventListener("keydown", this.handleKeyDown.bind(this))
|
13
|
+
}
|
14
|
+
|
15
|
+
// Handle form submission
|
16
|
+
async submit(event) {
|
17
|
+
event.preventDefault()
|
18
|
+
|
19
|
+
const message = this.inputTarget.value.trim()
|
20
|
+
if (!message) return
|
21
|
+
|
22
|
+
// Disable input and button during submission
|
23
|
+
this.inputTarget.disabled = true
|
24
|
+
this.submitButton.disabled = true
|
25
|
+
this.submitButton.textContent = "Sending..."
|
26
|
+
|
27
|
+
try {
|
28
|
+
const form = event.target
|
29
|
+
const formData = new FormData(form)
|
30
|
+
|
31
|
+
const response = await fetch(form.action, {
|
32
|
+
method: form.method,
|
33
|
+
body: formData,
|
34
|
+
headers: {
|
35
|
+
"Accept": "application/json",
|
36
|
+
"X-Requested-With": "XMLHttpRequest"
|
37
|
+
}
|
38
|
+
})
|
39
|
+
|
40
|
+
if (response.ok) {
|
41
|
+
const data = await response.json()
|
42
|
+
|
43
|
+
// Add messages to the chat
|
44
|
+
const messagesContainer = document.getElementById("chat-messages")
|
45
|
+
messagesContainer.insertAdjacentHTML("beforeend", data.user_message)
|
46
|
+
|
47
|
+
// Clear input
|
48
|
+
this.inputTarget.value = ""
|
49
|
+
|
50
|
+
// Scroll to bottom
|
51
|
+
this.scrollToBottom()
|
52
|
+
|
53
|
+
// Add assistant message with a slight delay for better UX
|
54
|
+
setTimeout(() => {
|
55
|
+
messagesContainer.insertAdjacentHTML("beforeend", data.assistant_message)
|
56
|
+
this.scrollToBottom()
|
57
|
+
}, 500)
|
58
|
+
} else {
|
59
|
+
const errorData = await response.json()
|
60
|
+
alert(`Error: ${errorData.error || "Something went wrong"}`)
|
61
|
+
}
|
62
|
+
} catch (error) {
|
63
|
+
console.error("Error submitting message:", error)
|
64
|
+
alert("Failed to send message. Please try again.")
|
65
|
+
} finally {
|
66
|
+
// Re-enable input and button
|
67
|
+
this.inputTarget.disabled = false
|
68
|
+
this.submitButton.disabled = false
|
69
|
+
this.submitButton.textContent = "Send"
|
70
|
+
this.inputTarget.focus()
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
// Handle keyboard shortcuts
|
75
|
+
handleKeyDown(event) {
|
76
|
+
// Submit on Enter (without Shift)
|
77
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
78
|
+
event.preventDefault()
|
79
|
+
this.submitButton.click()
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
// Scroll to the bottom of the messages container
|
84
|
+
scrollToBottom() {
|
85
|
+
const messagesContainer = document.getElementById("chat-messages")
|
86
|
+
messagesContainer.scrollTop = messagesContainer.scrollHeight
|
87
|
+
}
|
88
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Controller for the chat demo
|
4
|
+
class ChatController < ApplicationController
|
5
|
+
include LangsmithrbRails::TracedController
|
6
|
+
|
7
|
+
# GET /chat
|
8
|
+
def index
|
9
|
+
@messages = ChatMessage.order(created_at: :asc).last(10)
|
10
|
+
@message = ChatMessage.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# POST /chat
|
14
|
+
def create
|
15
|
+
# Create trace for the entire request
|
16
|
+
langsmith_trace("chat_request", run_type: "chain") do |run|
|
17
|
+
@message = ChatMessage.new(message_params)
|
18
|
+
@message.is_user = true
|
19
|
+
|
20
|
+
if @message.save
|
21
|
+
# Record the user message in the trace
|
22
|
+
run.inputs = { user_message: @message.content }
|
23
|
+
|
24
|
+
# Generate a response
|
25
|
+
llm_service = LlmService.new
|
26
|
+
context = ChatMessage.order(created_at: :asc).last(5).map do |msg|
|
27
|
+
{ is_user: msg.is_user, content: msg.content }
|
28
|
+
end
|
29
|
+
|
30
|
+
response = llm_service.generate(@message.content, context)
|
31
|
+
|
32
|
+
# Create the assistant message
|
33
|
+
@assistant_message = ChatMessage.create!(
|
34
|
+
content: response,
|
35
|
+
is_user: false
|
36
|
+
)
|
37
|
+
|
38
|
+
# Record the assistant response in the trace
|
39
|
+
run.outputs = { assistant_message: @assistant_message.content }
|
40
|
+
|
41
|
+
# Respond with both messages for AJAX updates
|
42
|
+
render json: {
|
43
|
+
user_message: render_to_string(partial: "message", locals: { message: @message }),
|
44
|
+
assistant_message: render_to_string(partial: "message", locals: { message: @assistant_message })
|
45
|
+
}
|
46
|
+
else
|
47
|
+
render json: { error: @message.errors.full_messages.join(", ") }, status: :unprocessable_entity
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Only allow a list of trusted parameters through
|
55
|
+
def message_params
|
56
|
+
params.require(:chat_message).permit(:content)
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Model for storing chat messages
|
4
|
+
class ChatMessage < ApplicationRecord
|
5
|
+
# Validations
|
6
|
+
validates :content, presence: true
|
7
|
+
|
8
|
+
# Scopes
|
9
|
+
scope :user_messages, -> { where(is_user: true) }
|
10
|
+
scope :assistant_messages, -> { where(is_user: false) }
|
11
|
+
scope :recent, -> { order(created_at: :desc) }
|
12
|
+
|
13
|
+
# Get the conversation history as an array of messages
|
14
|
+
# @param limit [Integer] Maximum number of messages to return
|
15
|
+
# @return [Array<Hash>] Array of messages with role and content
|
16
|
+
def self.conversation_history(limit = 10)
|
17
|
+
recent.limit(limit).map do |message|
|
18
|
+
{
|
19
|
+
role: message.is_user? ? "user" : "assistant",
|
20
|
+
content: message.content
|
21
|
+
}
|
22
|
+
end.reverse
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Migration for creating the chat_messages table
|
4
|
+
class CreateChatMessages < ActiveRecord::Migration[7.0]
|
5
|
+
def change
|
6
|
+
create_table :chat_messages do |t|
|
7
|
+
t.text :content, null: false
|
8
|
+
t.boolean :is_user, default: true
|
9
|
+
t.string :metadata
|
10
|
+
t.string :langsmith_run_id
|
11
|
+
|
12
|
+
t.timestamps
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index :chat_messages, :is_user
|
16
|
+
add_index :chat_messages, :langsmith_run_id
|
17
|
+
add_index :chat_messages, :created_at
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
<%# Chat demo index page %>
|
2
|
+
<div class="chat-container" data-controller="chat">
|
3
|
+
<div class="chat-header">
|
4
|
+
<h1>LangSmith Rails Demo</h1>
|
5
|
+
<p class="subtitle">A simple chat interface with LangSmith tracing</p>
|
6
|
+
</div>
|
7
|
+
|
8
|
+
<div class="chat-messages" id="chat-messages">
|
9
|
+
<% if @messages.empty? %>
|
10
|
+
<div class="welcome-message">
|
11
|
+
<h2>Welcome to the LangSmith Rails Demo!</h2>
|
12
|
+
<p>This is a simple chat interface that demonstrates LangSmith tracing in a Rails application.</p>
|
13
|
+
<p>Every message you send will be traced with LangSmith, allowing you to monitor and debug your LLM interactions.</p>
|
14
|
+
<p>Try asking a question below to get started!</p>
|
15
|
+
</div>
|
16
|
+
<% else %>
|
17
|
+
<% @messages.each do |message| %>
|
18
|
+
<%= render partial: "message", locals: { message: message } %>
|
19
|
+
<% end %>
|
20
|
+
<% end %>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div class="chat-form">
|
24
|
+
<%= form_with(model: @message, url: chat_index_path, data: { action: "submit->chat#submit" }) do |form| %>
|
25
|
+
<div class="input-group">
|
26
|
+
<%= form.text_area :content, placeholder: "Type your message...", class: "chat-input", data: { chat_target: "input" } %>
|
27
|
+
<button type="submit" class="send-button" data-chat-target="submitButton">
|
28
|
+
Send
|
29
|
+
</button>
|
30
|
+
</div>
|
31
|
+
<% end %>
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<div class="chat-footer">
|
35
|
+
<p>
|
36
|
+
Powered by LangSmith Rails
|
37
|
+
<% if ENV["LANGSMITH_PROJECT"].present? %>
|
38
|
+
| Project: <%= ENV["LANGSMITH_PROJECT"] %>
|
39
|
+
<% end %>
|
40
|
+
</p>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<%# Partial for rendering a message %>
|
45
|
+
<% content_for :partials do %>
|
46
|
+
<script type="text/html" id="message-template">
|
47
|
+
<div class="message <%= message.is_user? ? 'user-message' : 'assistant-message' %>">
|
48
|
+
<div class="message-content">
|
49
|
+
<%= message.content %>
|
50
|
+
</div>
|
51
|
+
<div class="message-meta">
|
52
|
+
<span class="message-time"><%= message.created_at.strftime("%H:%M") %></span>
|
53
|
+
<span class="message-role"><%= message.is_user? ? "You" : "Assistant" %></span>
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
</script>
|
57
|
+
<% end %>
|
58
|
+
|
59
|
+
<style>
|
60
|
+
.chat-container {
|
61
|
+
max-width: 800px;
|
62
|
+
margin: 0 auto;
|
63
|
+
padding: 20px;
|
64
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
65
|
+
}
|
66
|
+
|
67
|
+
.chat-header {
|
68
|
+
text-align: center;
|
69
|
+
margin-bottom: 20px;
|
70
|
+
padding-bottom: 10px;
|
71
|
+
border-bottom: 1px solid #eaeaea;
|
72
|
+
}
|
73
|
+
|
74
|
+
.chat-header h1 {
|
75
|
+
margin-bottom: 5px;
|
76
|
+
color: #333;
|
77
|
+
}
|
78
|
+
|
79
|
+
.subtitle {
|
80
|
+
color: #666;
|
81
|
+
margin-top: 0;
|
82
|
+
}
|
83
|
+
|
84
|
+
.chat-messages {
|
85
|
+
height: 500px;
|
86
|
+
overflow-y: auto;
|
87
|
+
padding: 10px;
|
88
|
+
background-color: #f9f9f9;
|
89
|
+
border-radius: 8px;
|
90
|
+
margin-bottom: 20px;
|
91
|
+
}
|
92
|
+
|
93
|
+
.welcome-message {
|
94
|
+
text-align: center;
|
95
|
+
padding: 40px 20px;
|
96
|
+
color: #666;
|
97
|
+
}
|
98
|
+
|
99
|
+
.welcome-message h2 {
|
100
|
+
color: #333;
|
101
|
+
margin-bottom: 15px;
|
102
|
+
}
|
103
|
+
|
104
|
+
.message {
|
105
|
+
margin-bottom: 15px;
|
106
|
+
padding: 10px 15px;
|
107
|
+
border-radius: 8px;
|
108
|
+
max-width: 80%;
|
109
|
+
position: relative;
|
110
|
+
}
|
111
|
+
|
112
|
+
.user-message {
|
113
|
+
background-color: #e1f5fe;
|
114
|
+
margin-left: auto;
|
115
|
+
border-bottom-right-radius: 0;
|
116
|
+
}
|
117
|
+
|
118
|
+
.assistant-message {
|
119
|
+
background-color: #f0f0f0;
|
120
|
+
margin-right: auto;
|
121
|
+
border-bottom-left-radius: 0;
|
122
|
+
}
|
123
|
+
|
124
|
+
.message-content {
|
125
|
+
white-space: pre-wrap;
|
126
|
+
}
|
127
|
+
|
128
|
+
.message-meta {
|
129
|
+
font-size: 0.8em;
|
130
|
+
color: #999;
|
131
|
+
margin-top: 5px;
|
132
|
+
text-align: right;
|
133
|
+
}
|
134
|
+
|
135
|
+
.chat-form {
|
136
|
+
margin-bottom: 20px;
|
137
|
+
}
|
138
|
+
|
139
|
+
.input-group {
|
140
|
+
display: flex;
|
141
|
+
gap: 10px;
|
142
|
+
}
|
143
|
+
|
144
|
+
.chat-input {
|
145
|
+
flex-grow: 1;
|
146
|
+
padding: 10px;
|
147
|
+
border: 1px solid #ddd;
|
148
|
+
border-radius: 4px;
|
149
|
+
resize: none;
|
150
|
+
height: 60px;
|
151
|
+
font-family: inherit;
|
152
|
+
}
|
153
|
+
|
154
|
+
.send-button {
|
155
|
+
padding: 10px 20px;
|
156
|
+
background-color: #2196f3;
|
157
|
+
color: white;
|
158
|
+
border: none;
|
159
|
+
border-radius: 4px;
|
160
|
+
cursor: pointer;
|
161
|
+
transition: background-color 0.2s;
|
162
|
+
}
|
163
|
+
|
164
|
+
.send-button:hover {
|
165
|
+
background-color: #0b7dda;
|
166
|
+
}
|
167
|
+
|
168
|
+
.send-button:disabled {
|
169
|
+
background-color: #cccccc;
|
170
|
+
cursor: not-allowed;
|
171
|
+
}
|
172
|
+
|
173
|
+
.chat-footer {
|
174
|
+
text-align: center;
|
175
|
+
color: #999;
|
176
|
+
font-size: 0.8em;
|
177
|
+
padding-top: 10px;
|
178
|
+
border-top: 1px solid #eaeaea;
|
179
|
+
}
|
180
|
+
</style>
|