langsmith-sdk 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 +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +120 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +48 -0
- data/LICENSE +22 -0
- data/README.md +224 -0
- data/Rakefile +8 -0
- data/examples/LLM_TRACING.md +439 -0
- data/examples/complex_agent.rb +472 -0
- data/examples/llm_tracing.rb +304 -0
- data/examples/openai_integration.rb +751 -0
- data/langsmith.gemspec +38 -0
- data/lib/langsmith/batch_processor.rb +237 -0
- data/lib/langsmith/client.rb +181 -0
- data/lib/langsmith/configuration.rb +96 -0
- data/lib/langsmith/context.rb +73 -0
- data/lib/langsmith/errors.rb +13 -0
- data/lib/langsmith/railtie.rb +86 -0
- data/lib/langsmith/run.rb +320 -0
- data/lib/langsmith/run_tree.rb +154 -0
- data/lib/langsmith/traceable.rb +120 -0
- data/lib/langsmith/version.rb +5 -0
- data/lib/langsmith.rb +144 -0
- metadata +134 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example: Integration with ruby-openai gem
|
|
4
|
+
#
|
|
5
|
+
# This example shows how to integrate LangSmith tracing with the ruby-openai gem.
|
|
6
|
+
# Install: gem install ruby-openai
|
|
7
|
+
#
|
|
8
|
+
# Run with: OPENAI_API_KEY=sk-... LANGSMITH_API_KEY=ls_... ruby examples/openai_integration.rb
|
|
9
|
+
|
|
10
|
+
require_relative "../lib/langsmith"
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
require "openai"
|
|
14
|
+
require "json"
|
|
15
|
+
rescue LoadError
|
|
16
|
+
puts "This example requires the ruby-openai gem."
|
|
17
|
+
puts "Install with: gem install ruby-openai"
|
|
18
|
+
exit 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Configure Langsmith
|
|
22
|
+
Langsmith.configure do |config|
|
|
23
|
+
config.api_key = ENV.fetch("LANGSMITH_API_KEY")
|
|
24
|
+
config.tracing_enabled = true
|
|
25
|
+
config.project = "openai-ruby-examples"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Create OpenAI client
|
|
29
|
+
OPENAI_CLIENT = OpenAI::Client.new(access_token: ENV.fetch("OPENAI_API_KEY"))
|
|
30
|
+
|
|
31
|
+
# Wrapper for traced OpenAI chat completions
|
|
32
|
+
module TracedOpenAI
|
|
33
|
+
module_function
|
|
34
|
+
|
|
35
|
+
# Traced chat completion
|
|
36
|
+
def chat(messages:, model: "gpt-4o-mini", temperature: 0.7, **options)
|
|
37
|
+
Langsmith.trace(
|
|
38
|
+
"openai.chat.completions",
|
|
39
|
+
run_type: "llm",
|
|
40
|
+
inputs: { messages: messages, model: model, temperature: temperature }
|
|
41
|
+
) do |run|
|
|
42
|
+
# Set model for LangSmith to display
|
|
43
|
+
run.set_model(model: model, provider: "openai")
|
|
44
|
+
|
|
45
|
+
# Add request metadata
|
|
46
|
+
run.add_metadata(
|
|
47
|
+
temperature: temperature,
|
|
48
|
+
**options.slice(:max_tokens, :top_p, :frequency_penalty, :presence_penalty)
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Make the actual API call
|
|
52
|
+
response = OPENAI_CLIENT.chat(
|
|
53
|
+
parameters: {
|
|
54
|
+
model: model,
|
|
55
|
+
messages: messages,
|
|
56
|
+
temperature: temperature,
|
|
57
|
+
**options
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Extract and set token usage (Python SDK uses input_tokens/output_tokens)
|
|
62
|
+
if response["usage"]
|
|
63
|
+
run.set_token_usage(
|
|
64
|
+
input_tokens: response["usage"]["prompt_tokens"],
|
|
65
|
+
output_tokens: response["usage"]["completion_tokens"],
|
|
66
|
+
total_tokens: response["usage"]["total_tokens"]
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Add response metadata
|
|
71
|
+
run.add_metadata(
|
|
72
|
+
response_id: response["id"],
|
|
73
|
+
finish_reason: response.dig("choices", 0, "finish_reason")
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Return the response
|
|
77
|
+
response
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Traced embedding
|
|
82
|
+
def embed(input:, model: "text-embedding-3-small")
|
|
83
|
+
Langsmith.trace(
|
|
84
|
+
"openai.embeddings",
|
|
85
|
+
run_type: "llm",
|
|
86
|
+
inputs: { input: input, model: model }
|
|
87
|
+
) do |run|
|
|
88
|
+
run.set_model(model: model, provider: "openai")
|
|
89
|
+
|
|
90
|
+
response = OPENAI_CLIENT.embeddings(
|
|
91
|
+
parameters: { model: model, input: input }
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Set token usage for embeddings (no output tokens)
|
|
95
|
+
if response["usage"]
|
|
96
|
+
run.set_token_usage(
|
|
97
|
+
input_tokens: response["usage"]["prompt_tokens"],
|
|
98
|
+
total_tokens: response["usage"]["total_tokens"]
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
run.add_metadata(
|
|
103
|
+
dimensions: response.dig("data", 0, "embedding")&.length,
|
|
104
|
+
input_count: Array(input).length
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
response
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Traced structured output with JSON schema
|
|
112
|
+
# Uses OpenAI's response_format for guaranteed structured responses
|
|
113
|
+
def structured_output(messages:, schema:, schema_name: "response", model: "gpt-4o-mini", **options)
|
|
114
|
+
Langsmith.trace(
|
|
115
|
+
"openai.chat.structured",
|
|
116
|
+
run_type: "llm",
|
|
117
|
+
inputs: {
|
|
118
|
+
messages: messages,
|
|
119
|
+
model: model,
|
|
120
|
+
schema_name: schema_name,
|
|
121
|
+
schema: schema
|
|
122
|
+
}
|
|
123
|
+
) do |run|
|
|
124
|
+
run.set_model(model: model, provider: "openai")
|
|
125
|
+
run.add_metadata(structured_output: true, schema_name: schema_name)
|
|
126
|
+
run.add_tags("structured-output", "json-schema")
|
|
127
|
+
|
|
128
|
+
# Build the response_format for structured outputs
|
|
129
|
+
response_format = {
|
|
130
|
+
type: "json_schema",
|
|
131
|
+
json_schema: {
|
|
132
|
+
name: schema_name,
|
|
133
|
+
strict: true,
|
|
134
|
+
schema: schema
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
response = OPENAI_CLIENT.chat(
|
|
139
|
+
parameters: {
|
|
140
|
+
model: model,
|
|
141
|
+
messages: messages,
|
|
142
|
+
response_format: response_format,
|
|
143
|
+
**options
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Extract and set token usage
|
|
148
|
+
if response["usage"]
|
|
149
|
+
run.set_token_usage(
|
|
150
|
+
input_tokens: response["usage"]["prompt_tokens"],
|
|
151
|
+
output_tokens: response["usage"]["completion_tokens"],
|
|
152
|
+
total_tokens: response["usage"]["total_tokens"]
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Parse the structured response
|
|
157
|
+
content = response.dig("choices", 0, "message", "content")
|
|
158
|
+
parsed = JSON.parse(content, symbolize_names: true)
|
|
159
|
+
|
|
160
|
+
run.add_metadata(
|
|
161
|
+
response_id: response["id"],
|
|
162
|
+
finish_reason: response.dig("choices", 0, "finish_reason")
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Return just the parsed result (cleaner output)
|
|
166
|
+
parsed
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Traced function calling (tools)
|
|
171
|
+
def function_call(messages:, tools:, model: "gpt-4o-mini", tool_choice: "auto", **options)
|
|
172
|
+
Langsmith.trace(
|
|
173
|
+
"openai.chat.function_call",
|
|
174
|
+
run_type: "llm",
|
|
175
|
+
inputs: {
|
|
176
|
+
messages: messages,
|
|
177
|
+
model: model,
|
|
178
|
+
tools: tools.map { |t| t[:function][:name] }
|
|
179
|
+
}
|
|
180
|
+
) do |run|
|
|
181
|
+
run.set_model(model: model, provider: "openai")
|
|
182
|
+
run.add_metadata(
|
|
183
|
+
tool_choice: tool_choice,
|
|
184
|
+
available_tools: tools.map { |t| t[:function][:name] }
|
|
185
|
+
)
|
|
186
|
+
run.add_tags("function-calling", "tools")
|
|
187
|
+
|
|
188
|
+
response = OPENAI_CLIENT.chat(
|
|
189
|
+
parameters: {
|
|
190
|
+
model: model,
|
|
191
|
+
messages: messages,
|
|
192
|
+
tools: tools,
|
|
193
|
+
tool_choice: tool_choice,
|
|
194
|
+
**options
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if response["usage"]
|
|
199
|
+
run.set_token_usage(
|
|
200
|
+
input_tokens: response["usage"]["prompt_tokens"],
|
|
201
|
+
output_tokens: response["usage"]["completion_tokens"],
|
|
202
|
+
total_tokens: response["usage"]["total_tokens"]
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Extract tool calls if any
|
|
207
|
+
tool_calls = response.dig("choices", 0, "message", "tool_calls") || []
|
|
208
|
+
parsed_tool_calls = tool_calls.map do |tc|
|
|
209
|
+
{
|
|
210
|
+
id: tc["id"],
|
|
211
|
+
function: tc["function"]["name"],
|
|
212
|
+
arguments: JSON.parse(tc["function"]["arguments"], symbolize_names: true)
|
|
213
|
+
}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
run.add_metadata(
|
|
217
|
+
response_id: response["id"],
|
|
218
|
+
finish_reason: response.dig("choices", 0, "finish_reason"),
|
|
219
|
+
tool_calls_count: parsed_tool_calls.length
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Return just the tool calls (cleaner output)
|
|
223
|
+
parsed_tool_calls
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# ============================================================================
|
|
228
|
+
# OpenAI Responses API (new agent-focused API)
|
|
229
|
+
# ============================================================================
|
|
230
|
+
|
|
231
|
+
# Traced Responses API call - OpenAI's new simplified API for agents
|
|
232
|
+
def responses(input:, model: "gpt-4o-mini", instructions: nil, tools: nil, **options)
|
|
233
|
+
Langsmith.trace(
|
|
234
|
+
"openai.responses",
|
|
235
|
+
run_type: "llm",
|
|
236
|
+
inputs: { input: input, model: model, instructions: instructions&.slice(0, 200) }
|
|
237
|
+
) do |run|
|
|
238
|
+
run.set_model(model: model, provider: "openai")
|
|
239
|
+
run.add_metadata(api: "responses")
|
|
240
|
+
run.add_tags("responses-api")
|
|
241
|
+
|
|
242
|
+
params = {
|
|
243
|
+
model: model,
|
|
244
|
+
input: input,
|
|
245
|
+
**options
|
|
246
|
+
}
|
|
247
|
+
params[:instructions] = instructions if instructions
|
|
248
|
+
params[:tools] = tools if tools
|
|
249
|
+
|
|
250
|
+
response = OPENAI_CLIENT.responses.create(parameters: params)
|
|
251
|
+
|
|
252
|
+
# Extract token usage from Responses API format (uses input_tokens/output_tokens)
|
|
253
|
+
if response["usage"]
|
|
254
|
+
run.set_token_usage(
|
|
255
|
+
input_tokens: response["usage"]["input_tokens"],
|
|
256
|
+
output_tokens: response["usage"]["output_tokens"],
|
|
257
|
+
total_tokens: response["usage"]["total_tokens"]
|
|
258
|
+
)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Extract the output text
|
|
262
|
+
output_text = response.dig("output", 0, "content", 0, "text") ||
|
|
263
|
+
response.dig("output_text") ||
|
|
264
|
+
response["output"]
|
|
265
|
+
|
|
266
|
+
run.add_metadata(
|
|
267
|
+
response_id: response["id"],
|
|
268
|
+
status: response["status"]
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Return just the output text (cleaner output)
|
|
272
|
+
output_text
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Traced Responses API with tool use
|
|
277
|
+
def responses_with_tools(input:, tools:, model: "gpt-4o-mini", instructions: nil, **options)
|
|
278
|
+
Langsmith.trace(
|
|
279
|
+
"openai.responses.with_tools",
|
|
280
|
+
run_type: "chain",
|
|
281
|
+
inputs: { input: input, tools: tools.map { |t| t[:name] } }
|
|
282
|
+
) do |run|
|
|
283
|
+
run.add_metadata(
|
|
284
|
+
api: "responses",
|
|
285
|
+
tool_count: tools.length
|
|
286
|
+
)
|
|
287
|
+
run.add_tags("responses-api", "tools")
|
|
288
|
+
|
|
289
|
+
# Initial response
|
|
290
|
+
response = Langsmith.trace("responses.initial", run_type: "llm") do |llm_run|
|
|
291
|
+
llm_run.set_model(model: model, provider: "openai")
|
|
292
|
+
|
|
293
|
+
result = OPENAI_CLIENT.responses.create(
|
|
294
|
+
parameters: {
|
|
295
|
+
model: model,
|
|
296
|
+
input: input,
|
|
297
|
+
instructions: instructions,
|
|
298
|
+
tools: tools,
|
|
299
|
+
**options
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Responses API uses input_tokens/output_tokens
|
|
304
|
+
if result["usage"]
|
|
305
|
+
llm_run.set_token_usage(
|
|
306
|
+
input_tokens: result["usage"]["input_tokens"],
|
|
307
|
+
output_tokens: result["usage"]["output_tokens"],
|
|
308
|
+
total_tokens: result["usage"]["total_tokens"]
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
result
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Check for tool calls in output
|
|
316
|
+
tool_calls = []
|
|
317
|
+
outputs = response["output"] || []
|
|
318
|
+
outputs.each do |output|
|
|
319
|
+
next unless output["type"] == "function_call"
|
|
320
|
+
|
|
321
|
+
tool_calls << {
|
|
322
|
+
id: output["call_id"],
|
|
323
|
+
name: output["name"],
|
|
324
|
+
arguments: JSON.parse(output["arguments"], symbolize_names: true)
|
|
325
|
+
}
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
run.add_metadata(
|
|
329
|
+
tool_calls_count: tool_calls.length
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Return just the tool calls (cleaner output)
|
|
333
|
+
tool_calls
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Traced streaming chat completion with TTFT (time to first token) tracking
|
|
338
|
+
# Follows Python SDK pattern: adds "new_token" event for first token
|
|
339
|
+
def chat_stream(messages:, model: "gpt-4o-mini", temperature: 0.7, &block)
|
|
340
|
+
Langsmith.trace(
|
|
341
|
+
"openai.chat.completions.stream",
|
|
342
|
+
run_type: "llm",
|
|
343
|
+
inputs: { messages: messages, model: model, streaming: true }
|
|
344
|
+
) do |run|
|
|
345
|
+
run.set_model(model: model, provider: "openai")
|
|
346
|
+
run.add_metadata(temperature: temperature, streaming: true)
|
|
347
|
+
|
|
348
|
+
full_content = ""
|
|
349
|
+
finish_reason = nil
|
|
350
|
+
first_token_logged = false
|
|
351
|
+
first_token_time = nil
|
|
352
|
+
stream_start_time = Time.now
|
|
353
|
+
|
|
354
|
+
OPENAI_CLIENT.chat(
|
|
355
|
+
parameters: {
|
|
356
|
+
model: model,
|
|
357
|
+
messages: messages,
|
|
358
|
+
temperature: temperature,
|
|
359
|
+
stream: proc do |chunk, _bytesize|
|
|
360
|
+
delta = chunk.dig("choices", 0, "delta", "content")
|
|
361
|
+
if delta
|
|
362
|
+
# Log "new_token" event for first token (Python SDK pattern)
|
|
363
|
+
# LangSmith uses this to calculate time-to-first-token
|
|
364
|
+
unless first_token_logged
|
|
365
|
+
first_token_time = Time.now.utc
|
|
366
|
+
run.add_event(name: "new_token", time: first_token_time, token: delta)
|
|
367
|
+
first_token_logged = true
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
full_content += delta
|
|
371
|
+
block&.call(delta)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Capture finish reason from final chunk
|
|
375
|
+
if (fr = chunk.dig("choices", 0, "finish_reason"))
|
|
376
|
+
finish_reason = fr
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
stream_end_time = Time.now
|
|
383
|
+
|
|
384
|
+
# Calculate TTFT for display
|
|
385
|
+
time_to_first_token = first_token_time ? (first_token_time - stream_start_time) : nil
|
|
386
|
+
|
|
387
|
+
# Estimate tokens for streaming (OpenAI doesn't return usage for streams)
|
|
388
|
+
estimated_input_tokens = messages.sum { |m| (m[:content].to_s.length / 4.0).ceil }
|
|
389
|
+
estimated_output_tokens = (full_content.length / 4.0).ceil
|
|
390
|
+
|
|
391
|
+
# Calculate tokens per second
|
|
392
|
+
generation_time = first_token_time ? (stream_end_time - first_token_time) : (stream_end_time - stream_start_time)
|
|
393
|
+
tokens_per_second = generation_time.positive? ? (estimated_output_tokens / generation_time).round(2) : nil
|
|
394
|
+
|
|
395
|
+
run.set_token_usage(
|
|
396
|
+
input_tokens: estimated_input_tokens,
|
|
397
|
+
output_tokens: estimated_output_tokens,
|
|
398
|
+
total_tokens: estimated_input_tokens + estimated_output_tokens
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
run.add_metadata(
|
|
402
|
+
finish_reason: finish_reason,
|
|
403
|
+
response_length: full_content.length,
|
|
404
|
+
tokens_per_second: tokens_per_second
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
{
|
|
408
|
+
content: full_content,
|
|
409
|
+
finish_reason: finish_reason,
|
|
410
|
+
time_to_first_token: time_to_first_token&.round(3),
|
|
411
|
+
tokens_per_second: tokens_per_second
|
|
412
|
+
}
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Example: RAG chain with OpenAI
|
|
418
|
+
class RAGChain
|
|
419
|
+
include Langsmith::Traceable
|
|
420
|
+
|
|
421
|
+
def initialize(knowledge_base:)
|
|
422
|
+
@knowledge_base = knowledge_base
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
traceable run_type: "chain", name: "rag_chain"
|
|
426
|
+
def answer(question)
|
|
427
|
+
# Step 1: Embed the question
|
|
428
|
+
question_embedding = embed_query(question)
|
|
429
|
+
|
|
430
|
+
# Step 2: Retrieve relevant context
|
|
431
|
+
context = retrieve_context(question_embedding)
|
|
432
|
+
|
|
433
|
+
# Step 3: Generate answer
|
|
434
|
+
generate_answer(question, context)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
private
|
|
438
|
+
|
|
439
|
+
traceable run_type: "llm", name: "embed_query"
|
|
440
|
+
def embed_query(text)
|
|
441
|
+
response = TracedOpenAI.embed(input: text)
|
|
442
|
+
response.dig("data", 0, "embedding")
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
traceable run_type: "retriever", name: "retrieve_context"
|
|
446
|
+
def retrieve_context(embedding)
|
|
447
|
+
# Simulate vector search - in real app, query your vector DB
|
|
448
|
+
Langsmith.current_run&.add_metadata(
|
|
449
|
+
index: "knowledge_base",
|
|
450
|
+
top_k: 3
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
@knowledge_base.first(3)
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
traceable run_type: "llm", name: "generate_answer"
|
|
457
|
+
def generate_answer(question, context)
|
|
458
|
+
messages = [
|
|
459
|
+
{
|
|
460
|
+
role: "system",
|
|
461
|
+
content: "Answer the question based on the following context:\n\n#{context.join("\n\n")}"
|
|
462
|
+
},
|
|
463
|
+
{ role: "user", content: question }
|
|
464
|
+
]
|
|
465
|
+
|
|
466
|
+
response = TracedOpenAI.chat(messages: messages, model: "gpt-4o-mini")
|
|
467
|
+
response.dig("choices", 0, "message", "content")
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# ============================================================================
|
|
472
|
+
# Run the examples
|
|
473
|
+
# ============================================================================
|
|
474
|
+
|
|
475
|
+
if __FILE__ == $PROGRAM_NAME
|
|
476
|
+
puts "=" * 60
|
|
477
|
+
puts "LangSmith + OpenAI Integration Examples"
|
|
478
|
+
puts "=" * 60
|
|
479
|
+
|
|
480
|
+
# Example 1: Simple chat
|
|
481
|
+
puts "\n1. Simple chat completion:"
|
|
482
|
+
response = TracedOpenAI.chat(
|
|
483
|
+
messages: [{ role: "user", content: "What is Ruby programming language? Be brief." }],
|
|
484
|
+
model: "gpt-3.5-turbo",
|
|
485
|
+
max_tokens: 100
|
|
486
|
+
)
|
|
487
|
+
puts " Response: #{response.dig("choices", 0, "message", "content")}"
|
|
488
|
+
puts " Tokens: #{response.dig("usage", "total_tokens")}"
|
|
489
|
+
|
|
490
|
+
# Example 2: Embeddings
|
|
491
|
+
puts "\n2. Text embeddings:"
|
|
492
|
+
response = TracedOpenAI.embed(input: "Hello, world!")
|
|
493
|
+
puts " Embedding dimensions: #{response.dig("data", 0, "embedding")&.length}"
|
|
494
|
+
puts " Tokens used: #{response.dig("usage", "prompt_tokens")}"
|
|
495
|
+
|
|
496
|
+
# Example 3: Streaming
|
|
497
|
+
puts "\n3. Streaming chat:"
|
|
498
|
+
print " Response: "
|
|
499
|
+
result = TracedOpenAI.chat_stream(
|
|
500
|
+
messages: [{ role: "user", content: "Count from 1 to 5." }],
|
|
501
|
+
model: "gpt-3.5-turbo"
|
|
502
|
+
) do |chunk|
|
|
503
|
+
print chunk
|
|
504
|
+
end
|
|
505
|
+
puts "\n Finish reason: #{result[:finish_reason]}"
|
|
506
|
+
puts " Time to first token: #{result[:time_to_first_token]}s"
|
|
507
|
+
puts " Tokens/sec: #{result[:tokens_per_second]}"
|
|
508
|
+
|
|
509
|
+
# Example 4: Structured output - Entity extraction
|
|
510
|
+
puts "\n4. Structured output (entity extraction):"
|
|
511
|
+
# Note: OpenAI strict mode requires ALL properties to be in `required`
|
|
512
|
+
entity_schema = {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {
|
|
515
|
+
people: {
|
|
516
|
+
type: "array",
|
|
517
|
+
items: {
|
|
518
|
+
type: "object",
|
|
519
|
+
properties: {
|
|
520
|
+
name: { type: "string", description: "Person's full name" },
|
|
521
|
+
role: { type: ["string", "null"], description: "Their role or title, null if unknown" },
|
|
522
|
+
organization: { type: ["string", "null"], description: "Organization, null if unknown" }
|
|
523
|
+
},
|
|
524
|
+
required: %w[name role organization],
|
|
525
|
+
additionalProperties: false
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
locations: {
|
|
529
|
+
type: "array",
|
|
530
|
+
items: { type: "string" }
|
|
531
|
+
},
|
|
532
|
+
summary: { type: "string", description: "Brief summary of the text" }
|
|
533
|
+
},
|
|
534
|
+
required: %w[people locations summary],
|
|
535
|
+
additionalProperties: false
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
result = TracedOpenAI.structured_output(
|
|
539
|
+
messages: [
|
|
540
|
+
{
|
|
541
|
+
role: "user",
|
|
542
|
+
content: "Extract entities from: Yukihiro Matsumoto created Ruby in Japan. " \
|
|
543
|
+
"DHH built Rails while at 37signals in Chicago."
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
schema: entity_schema,
|
|
547
|
+
schema_name: "entity_extraction"
|
|
548
|
+
)
|
|
549
|
+
puts " People found: #{result[:people].map { |p| p[:name] }.join(", ")}"
|
|
550
|
+
puts " Locations: #{result[:locations].join(", ")}"
|
|
551
|
+
|
|
552
|
+
# Example 5: Structured output - Classification
|
|
553
|
+
puts "\n5. Structured output (sentiment classification):"
|
|
554
|
+
sentiment_schema = {
|
|
555
|
+
type: "object",
|
|
556
|
+
properties: {
|
|
557
|
+
sentiment: {
|
|
558
|
+
type: "string",
|
|
559
|
+
enum: %w[positive negative neutral mixed],
|
|
560
|
+
description: "Overall sentiment"
|
|
561
|
+
},
|
|
562
|
+
confidence: {
|
|
563
|
+
type: "number",
|
|
564
|
+
description: "Confidence score 0-1"
|
|
565
|
+
},
|
|
566
|
+
key_phrases: {
|
|
567
|
+
type: "array",
|
|
568
|
+
items: { type: "string" },
|
|
569
|
+
description: "Key phrases that indicate the sentiment"
|
|
570
|
+
},
|
|
571
|
+
reasoning: { type: "string", description: "Brief explanation" }
|
|
572
|
+
},
|
|
573
|
+
required: %w[sentiment confidence key_phrases reasoning],
|
|
574
|
+
additionalProperties: false
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
result = TracedOpenAI.structured_output(
|
|
578
|
+
messages: [
|
|
579
|
+
{ role: "system", content: "Analyze the sentiment of the given text." },
|
|
580
|
+
{ role: "user", content: "I love Ruby! The syntax is beautiful and elegant." }
|
|
581
|
+
],
|
|
582
|
+
schema: sentiment_schema,
|
|
583
|
+
schema_name: "sentiment_analysis"
|
|
584
|
+
)
|
|
585
|
+
puts " Sentiment: #{result[:sentiment]} (confidence: #{result[:confidence]})"
|
|
586
|
+
puts " Reasoning: #{result[:reasoning]}"
|
|
587
|
+
|
|
588
|
+
# Example 6: Function calling (tools)
|
|
589
|
+
puts "\n6. Function calling:"
|
|
590
|
+
# Note: With strict: true, ALL properties must be in required
|
|
591
|
+
weather_tools = [
|
|
592
|
+
{
|
|
593
|
+
type: "function",
|
|
594
|
+
function: {
|
|
595
|
+
name: "get_weather",
|
|
596
|
+
description: "Get current weather for a location",
|
|
597
|
+
parameters: {
|
|
598
|
+
type: "object",
|
|
599
|
+
properties: {
|
|
600
|
+
location: { type: "string", description: "City name" },
|
|
601
|
+
unit: { type: "string", enum: %w[celsius fahrenheit], description: "Temperature unit" }
|
|
602
|
+
},
|
|
603
|
+
required: %w[location unit],
|
|
604
|
+
additionalProperties: false
|
|
605
|
+
},
|
|
606
|
+
strict: true
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
type: "function",
|
|
611
|
+
function: {
|
|
612
|
+
name: "get_forecast",
|
|
613
|
+
description: "Get weather forecast for upcoming days",
|
|
614
|
+
parameters: {
|
|
615
|
+
type: "object",
|
|
616
|
+
properties: {
|
|
617
|
+
location: { type: "string", description: "City name" },
|
|
618
|
+
days: { type: "integer", description: "Number of days (1-7)" }
|
|
619
|
+
},
|
|
620
|
+
required: %w[location days],
|
|
621
|
+
additionalProperties: false
|
|
622
|
+
},
|
|
623
|
+
strict: true
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
]
|
|
627
|
+
|
|
628
|
+
result = TracedOpenAI.function_call(
|
|
629
|
+
messages: [{ role: "user", content: "What's the weather in Tokyo and the 3-day forecast?" }],
|
|
630
|
+
tools: weather_tools
|
|
631
|
+
)
|
|
632
|
+
puts " Tool calls: #{result.length}"
|
|
633
|
+
result.each do |tc|
|
|
634
|
+
puts " - #{tc[:function]}(#{tc[:arguments]})"
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Example 7: RAG chain
|
|
638
|
+
puts "\n7. RAG chain:"
|
|
639
|
+
knowledge = [
|
|
640
|
+
"Ruby is a dynamic, interpreted programming language created by Yukihiro Matsumoto.",
|
|
641
|
+
"Rails is a web application framework written in Ruby.",
|
|
642
|
+
"LangSmith provides observability for LLM applications."
|
|
643
|
+
]
|
|
644
|
+
rag = RAGChain.new(knowledge_base: knowledge)
|
|
645
|
+
answer = rag.answer("What is Ruby?")
|
|
646
|
+
puts " Answer: #{answer}"
|
|
647
|
+
|
|
648
|
+
# Example 8: Responses API (new OpenAI agent API)
|
|
649
|
+
puts "\n8. Responses API (simple):"
|
|
650
|
+
begin
|
|
651
|
+
result = TracedOpenAI.responses(
|
|
652
|
+
input: "What is the capital of France? Answer in one word.",
|
|
653
|
+
model: "gpt-4o-mini"
|
|
654
|
+
)
|
|
655
|
+
puts " Output: #{result}"
|
|
656
|
+
rescue StandardError => e
|
|
657
|
+
puts " (Responses API not available: #{e.message.split("\n").first})"
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Example 9: Responses API with tools
|
|
661
|
+
puts "\n9. Responses API with tools:"
|
|
662
|
+
begin
|
|
663
|
+
calculator_tools = [
|
|
664
|
+
{
|
|
665
|
+
type: "function",
|
|
666
|
+
name: "calculate",
|
|
667
|
+
description: "Perform a mathematical calculation",
|
|
668
|
+
parameters: {
|
|
669
|
+
type: "object",
|
|
670
|
+
properties: {
|
|
671
|
+
expression: { type: "string", description: "Math expression to evaluate" }
|
|
672
|
+
},
|
|
673
|
+
required: ["expression"],
|
|
674
|
+
additionalProperties: false
|
|
675
|
+
},
|
|
676
|
+
strict: true
|
|
677
|
+
}
|
|
678
|
+
]
|
|
679
|
+
|
|
680
|
+
result = TracedOpenAI.responses_with_tools(
|
|
681
|
+
input: "What is 25 * 4?",
|
|
682
|
+
tools: calculator_tools,
|
|
683
|
+
model: "gpt-4o-mini"
|
|
684
|
+
)
|
|
685
|
+
puts " Tool calls: #{result.length}"
|
|
686
|
+
result.each do |tc|
|
|
687
|
+
puts " - #{tc[:name]}(#{tc[:arguments]})"
|
|
688
|
+
end
|
|
689
|
+
rescue StandardError => e
|
|
690
|
+
puts " (Responses API not available: #{e.message.split("\n").first})"
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Example 10: Structured output in a traced chain
|
|
694
|
+
puts "\n10. Chained structured extraction:"
|
|
695
|
+
Langsmith.trace("document_analysis_pipeline", run_type: "chain") do |run|
|
|
696
|
+
run.add_metadata(pipeline_version: "1.0")
|
|
697
|
+
|
|
698
|
+
# Step 1: Extract entities
|
|
699
|
+
entities = Langsmith.trace("extract_entities", run_type: "chain") do
|
|
700
|
+
TracedOpenAI.structured_output(
|
|
701
|
+
messages: [{ role: "user", content: "Extract from: OpenAI was founded in San Francisco." }],
|
|
702
|
+
schema: {
|
|
703
|
+
type: "object",
|
|
704
|
+
properties: {
|
|
705
|
+
companies: { type: "array", items: { type: "string" } },
|
|
706
|
+
cities: { type: "array", items: { type: "string" } }
|
|
707
|
+
},
|
|
708
|
+
required: %w[companies cities],
|
|
709
|
+
additionalProperties: false
|
|
710
|
+
},
|
|
711
|
+
schema_name: "simple_entities"
|
|
712
|
+
)
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Step 2: Analyze sentiment
|
|
716
|
+
sentiment = Langsmith.trace("analyze_sentiment", run_type: "chain") do
|
|
717
|
+
TracedOpenAI.structured_output(
|
|
718
|
+
messages: [{ role: "user", content: "Sentiment of: This is amazing news!" }],
|
|
719
|
+
schema: {
|
|
720
|
+
type: "object",
|
|
721
|
+
properties: {
|
|
722
|
+
sentiment: { type: "string", enum: %w[positive negative neutral] },
|
|
723
|
+
score: { type: "number" }
|
|
724
|
+
},
|
|
725
|
+
required: %w[sentiment score],
|
|
726
|
+
additionalProperties: false
|
|
727
|
+
},
|
|
728
|
+
schema_name: "quick_sentiment"
|
|
729
|
+
)
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
run.add_metadata(
|
|
733
|
+
entities_found: entities[:companies].length + entities[:cities].length,
|
|
734
|
+
sentiment_result: sentiment[:sentiment]
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
{ entities: entities, sentiment: sentiment }
|
|
738
|
+
end
|
|
739
|
+
puts " Pipeline complete!"
|
|
740
|
+
|
|
741
|
+
# Flush traces
|
|
742
|
+
Langsmith.shutdown
|
|
743
|
+
|
|
744
|
+
puts "\n" + "=" * 60
|
|
745
|
+
puts "Done! Check LangSmith for detailed traces with:"
|
|
746
|
+
puts "- JSON schemas captured in inputs"
|
|
747
|
+
puts "- Parsed structured outputs"
|
|
748
|
+
puts "- Function/tool call details"
|
|
749
|
+
puts "- Full token usage"
|
|
750
|
+
puts "=" * 60
|
|
751
|
+
end
|