langsmithrb_rails 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rspec +3 -0
- data/.rspec_status +161 -0
- data/CHANGELOG.md +38 -0
- data/Gemfile +20 -0
- data/Gemfile.lock +321 -0
- data/LICENSE +21 -0
- data/README.md +421 -0
- data/Rakefile +10 -0
- data/langsmithrb_rails-0.1.0.gem +0 -0
- data/langsmithrb_rails-0.1.1.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 +292 -0
- data/lib/langsmithrb_rails/config.rb +169 -0
- data/lib/langsmithrb_rails/evaluation/evaluator.rb +178 -0
- data/lib/langsmithrb_rails/evaluation/llm_evaluator.rb +154 -0
- data/lib/langsmithrb_rails/evaluation/string_evaluator.rb +158 -0
- data/lib/langsmithrb_rails/evaluation.rb +76 -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/otel/exporter.rb +120 -0
- data/lib/langsmithrb_rails/otel.rb +135 -0
- data/lib/langsmithrb_rails/railtie.rb +33 -0
- data/lib/langsmithrb_rails/redactor.rb +76 -0
- data/lib/langsmithrb_rails/run_trees.rb +157 -0
- data/lib/langsmithrb_rails/version.rb +5 -0
- data/lib/langsmithrb_rails/wrappers/anthropic.rb +146 -0
- data/lib/langsmithrb_rails/wrappers/base.rb +81 -0
- data/lib/langsmithrb_rails/wrappers/llm.rb +151 -0
- data/lib/langsmithrb_rails/wrappers/openai.rb +193 -0
- data/lib/langsmithrb_rails/wrappers.rb +41 -0
- data/lib/langsmithrb_rails.rb +151 -0
- data/pkg/langsmithrb_rails-0.3.0.gem +0 -0
- metadata +74 -7
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :langsmith do
|
4
|
+
desc "Flush the LangSmith buffer"
|
5
|
+
task flush: :environment do
|
6
|
+
puts "Flushing LangSmith buffer..."
|
7
|
+
|
8
|
+
# Get counts before
|
9
|
+
pending_count = LangsmithRunBuffer.pending.count
|
10
|
+
failed_count = LangsmithRunBuffer.ready_to_retry.count
|
11
|
+
|
12
|
+
# Process pending traces
|
13
|
+
Langsmith::FlushBufferJob.new.perform(retry_failed: true)
|
14
|
+
|
15
|
+
# Get counts after
|
16
|
+
pending_after = LangsmithRunBuffer.pending.count
|
17
|
+
failed_after = LangsmithRunBuffer.ready_to_retry.count
|
18
|
+
sent_count = pending_count + failed_count - pending_after - failed_after
|
19
|
+
|
20
|
+
puts "Sent #{sent_count} traces to LangSmith"
|
21
|
+
puts "#{pending_after} traces still pending"
|
22
|
+
puts "#{failed_after} traces failed"
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Replay traces from a file"
|
26
|
+
task :replay, [:file] => :environment do |_t, args|
|
27
|
+
file_path = args[:file]
|
28
|
+
|
29
|
+
unless file_path && File.exist?(file_path)
|
30
|
+
puts "Error: File not found or not specified"
|
31
|
+
puts "Usage: rake langsmith:replay[path/to/file.json]"
|
32
|
+
exit 1
|
33
|
+
end
|
34
|
+
|
35
|
+
puts "Replaying traces from #{file_path}..."
|
36
|
+
|
37
|
+
# Load the file
|
38
|
+
traces = JSON.parse(File.read(file_path))
|
39
|
+
|
40
|
+
# Create buffer entries
|
41
|
+
traces.each do |trace|
|
42
|
+
LangsmithRunBuffer.create!(
|
43
|
+
name: trace["name"] || "replayed_trace",
|
44
|
+
run_type: trace["run_type"] || "llm",
|
45
|
+
status: "pending",
|
46
|
+
request_id: trace["request_id"],
|
47
|
+
user_ref: trace["user_ref"],
|
48
|
+
run_id: trace["run_id"],
|
49
|
+
parent_run_id: trace["parent_run_id"],
|
50
|
+
started_at: trace["started_at"],
|
51
|
+
ended_at: trace["ended_at"],
|
52
|
+
meta: trace["meta"] || {},
|
53
|
+
payload: trace,
|
54
|
+
error: nil,
|
55
|
+
retry_count: 0
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
puts "Created #{traces.size} buffer entries"
|
60
|
+
puts "Run rake langsmith:flush to send them to LangSmith"
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "Clean up old sent traces"
|
64
|
+
task cleanup: :environment do
|
65
|
+
# Delete traces that were sent more than 7 days ago
|
66
|
+
cutoff_date = 7.days.ago
|
67
|
+
count = LangsmithRunBuffer.sent.where("updated_at < ?", cutoff_date).delete_all
|
68
|
+
|
69
|
+
puts "Deleted #{count} old traces from the buffer"
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Model for buffering LangSmith traces
|
4
|
+
class LangsmithRunBuffer < ApplicationRecord
|
5
|
+
# Validations
|
6
|
+
validates :name, presence: true
|
7
|
+
validates :run_type, presence: true
|
8
|
+
validates :status, presence: true
|
9
|
+
|
10
|
+
# Scopes
|
11
|
+
scope :pending, -> { where(status: "pending") }
|
12
|
+
scope :failed, -> { where(status: "failed") }
|
13
|
+
scope :sent, -> { where(status: "sent") }
|
14
|
+
scope :ready_to_retry, -> { where(status: "failed").where("retry_count < ?", 5) }
|
15
|
+
|
16
|
+
# Send the trace to LangSmith
|
17
|
+
# @return [Boolean] Whether the trace was sent successfully
|
18
|
+
def send_to_langsmith
|
19
|
+
client = LangsmithrbRails::Client.new
|
20
|
+
|
21
|
+
if run_id.present?
|
22
|
+
# This is an update operation
|
23
|
+
response = client.update_run(run_id, payload)
|
24
|
+
else
|
25
|
+
# This is a create operation
|
26
|
+
response = client.create_run(payload)
|
27
|
+
|
28
|
+
# Store the run ID if available
|
29
|
+
if response&.dig(:status) == 200 && response&.dig(:body, "id").present?
|
30
|
+
update(run_id: response.dig(:body, "id"))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
if response&.dig(:status) == 200
|
35
|
+
update(status: "sent")
|
36
|
+
true
|
37
|
+
else
|
38
|
+
increment!(:retry_count)
|
39
|
+
update(
|
40
|
+
status: "failed",
|
41
|
+
error: response&.dig(:error) || "Unknown error"
|
42
|
+
)
|
43
|
+
false
|
44
|
+
end
|
45
|
+
rescue => e
|
46
|
+
increment!(:retry_count)
|
47
|
+
update(
|
48
|
+
status: "failed",
|
49
|
+
error: e.message
|
50
|
+
)
|
51
|
+
false
|
52
|
+
end
|
53
|
+
|
54
|
+
# Mark the trace as failed
|
55
|
+
# @param error [String] Error message
|
56
|
+
# @return [Boolean] Whether the update was successful
|
57
|
+
def mark_failed(error)
|
58
|
+
increment!(:retry_count)
|
59
|
+
update(
|
60
|
+
status: "failed",
|
61
|
+
error: error
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Mark the trace as sent
|
66
|
+
# @return [Boolean] Whether the update was successful
|
67
|
+
def mark_sent
|
68
|
+
update(status: "sent")
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateLangsmithRunBuffers < ActiveRecord::Migration[6.0]
|
4
|
+
def change
|
5
|
+
create_table :langsmith_run_buffers do |t|
|
6
|
+
t.string :name, null: false
|
7
|
+
t.string :run_type, null: false
|
8
|
+
t.string :status, null: false, default: "pending"
|
9
|
+
t.string :request_id
|
10
|
+
t.string :user_ref
|
11
|
+
t.string :run_id
|
12
|
+
t.string :parent_run_id
|
13
|
+
t.datetime :started_at
|
14
|
+
t.datetime :ended_at
|
15
|
+
t.jsonb :meta
|
16
|
+
t.jsonb :payload
|
17
|
+
t.text :error
|
18
|
+
t.integer :retry_count, null: false, default: 0
|
19
|
+
|
20
|
+
t.timestamps
|
21
|
+
end
|
22
|
+
|
23
|
+
add_index :langsmith_run_buffers, :status
|
24
|
+
add_index :langsmith_run_buffers, :request_id
|
25
|
+
add_index :langsmith_run_buffers, :run_id
|
26
|
+
add_index :langsmith_run_buffers, :parent_run_id
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LangsmithrbRails
|
4
|
+
module Generators
|
5
|
+
# Generator for adding CI integration for LangSmith evaluations
|
6
|
+
class CiGenerator < Rails::Generators::Base
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
|
9
|
+
desc "Adds CI integration for LangSmith evaluations"
|
10
|
+
|
11
|
+
def create_github_workflow
|
12
|
+
empty_directory ".github/workflows"
|
13
|
+
template "langsmith-evals.yml", ".github/workflows/langsmith-evals.yml"
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_ci_script
|
17
|
+
empty_directory "script/ci"
|
18
|
+
template "langsmith_export_summary.rb", "script/ci/langsmith_export_summary.rb"
|
19
|
+
chmod "script/ci/langsmith_export_summary.rb", 0755
|
20
|
+
end
|
21
|
+
|
22
|
+
def display_post_install_message
|
23
|
+
say "\n"
|
24
|
+
say "LangSmith CI integration has been added to your Rails application! 🎉", :green
|
25
|
+
say "\n"
|
26
|
+
say "This adds:", :yellow
|
27
|
+
say " 1. GitHub Actions workflow for running evaluations", :yellow
|
28
|
+
say " 2. Script for generating evaluation summaries for PR comments", :yellow
|
29
|
+
say "\n"
|
30
|
+
say "To customize the CI configuration:", :yellow
|
31
|
+
say " 1. Edit .github/workflows/langsmith-evals.yml", :yellow
|
32
|
+
say " 2. Configure the evaluation dataset and target in the workflow", :yellow
|
33
|
+
say "\n"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -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
|