llm.rb 4.1.0 → 4.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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +2 -2
  3. data/README.md +241 -166
  4. data/lib/llm/agent.rb +65 -37
  5. data/lib/llm/bot.rb +118 -30
  6. data/lib/llm/buffer.rb +6 -0
  7. data/lib/llm/function/tracing.rb +19 -0
  8. data/lib/llm/function.rb +28 -3
  9. data/lib/llm/json_adapter.rb +12 -12
  10. data/lib/llm/message.rb +19 -4
  11. data/lib/llm/prompt.rb +85 -0
  12. data/lib/llm/provider.rb +62 -10
  13. data/lib/llm/providers/anthropic/error_handler.rb +27 -5
  14. data/lib/llm/providers/anthropic/files.rb +22 -16
  15. data/lib/llm/providers/anthropic/models.rb +4 -3
  16. data/lib/llm/providers/anthropic.rb +6 -5
  17. data/lib/llm/providers/deepseek.rb +3 -3
  18. data/lib/llm/providers/gemini/error_handler.rb +34 -12
  19. data/lib/llm/providers/gemini/files.rb +19 -14
  20. data/lib/llm/providers/gemini/images.rb +4 -3
  21. data/lib/llm/providers/gemini/models.rb +4 -3
  22. data/lib/llm/providers/gemini.rb +9 -7
  23. data/lib/llm/providers/llamacpp.rb +3 -3
  24. data/lib/llm/providers/ollama/error_handler.rb +28 -6
  25. data/lib/llm/providers/ollama/models.rb +4 -3
  26. data/lib/llm/providers/ollama.rb +9 -7
  27. data/lib/llm/providers/openai/audio.rb +10 -7
  28. data/lib/llm/providers/openai/error_handler.rb +41 -14
  29. data/lib/llm/providers/openai/files.rb +19 -14
  30. data/lib/llm/providers/openai/images.rb +10 -7
  31. data/lib/llm/providers/openai/models.rb +4 -3
  32. data/lib/llm/providers/openai/moderations.rb +4 -3
  33. data/lib/llm/providers/openai/responses.rb +10 -7
  34. data/lib/llm/providers/openai/vector_stores.rb +34 -23
  35. data/lib/llm/providers/openai.rb +9 -7
  36. data/lib/llm/providers/xai.rb +3 -3
  37. data/lib/llm/providers/zai.rb +2 -2
  38. data/lib/llm/schema/object.rb +4 -4
  39. data/lib/llm/schema.rb +16 -2
  40. data/lib/llm/server_tool.rb +3 -3
  41. data/lib/llm/session/deserializer.rb +36 -0
  42. data/lib/llm/session.rb +3 -0
  43. data/lib/llm/tracer/logger.rb +192 -0
  44. data/lib/llm/tracer/null.rb +49 -0
  45. data/lib/llm/tracer/telemetry.rb +255 -0
  46. data/lib/llm/tracer.rb +134 -0
  47. data/lib/llm/version.rb +1 -1
  48. data/lib/llm.rb +4 -3
  49. data/llm.gemspec +6 -3
  50. metadata +41 -5
  51. data/lib/llm/builder.rb +0 -79
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc70b8eb2d7ce82b3959d2b7dc795a89511a1962ed443a5344bb00ef55863033
4
- data.tar.gz: a9245348fccc085710ae28097b9ce9c0ec9ce8e8f5ea4e23f97a9bde5fc50fee
3
+ metadata.gz: f35a8e2b21d2bf7914c9b261897eaa6d9c6d0ff03dc51c0e751f86d22e08f093
4
+ data.tar.gz: 92ffab3eab12fd6393ef8e324360d13e9725403a9571cf6c28427be090021965
5
5
  SHA512:
6
- metadata.gz: b1a0e67e1d938792da4cf52ff6b05dba568b71c77d28ef18c11510c7f0c37b21d5514f659ae6997193774755aede0bd5af4a1239247fc396b8a4815258723eb6
7
- data.tar.gz: 87bfee8769ba983ffccef6bfb276922501e8cc68b2b4f2be6857408739b7307403c120de7db1b83c612a6af61e30860366419abb790662feb679ddf6f1234102
6
+ metadata.gz: 02cdeb6b969b4ec2d76ffce29236fb8c189cdf3a71ea121e2b218ffb8fdadff0005cfd4211bcdac6e17b9814f9533949f0bdec19efdd8fb2262096d5bd440dde
7
+ data.tar.gz: 3cf0abeefd9927df9e600a694387e0f7d334f9c614b6e64cd54cf3e5f82b6a73895d74c392a5a6f68a92c529155cfb3db067bf8b2ef85e0df4505105c759870c
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
- Copyright (C) 2025
1
+ Copyright (C) 2026
2
2
  Antar Azri <azantar@proton.me>
3
- 0x1eef <0x1eef@proton.me>
3
+ 0x1eef <0x1eef@hardenedbsd.org>
4
4
 
5
5
  Permission to use, copy, modify, and/or distribute this
6
6
  software for any purpose with or without fee is hereby
data/README.md CHANGED
@@ -1,6 +1,11 @@
1
- > **Minimal footprint** <br>
2
- > Zero dependencies outside Ruby’s standard library. <br>
3
- > Zero runtime dependencies.
1
+ <p align="center">
2
+ <a href="llm.rb"><img src="https://github.com/llmrb/llm.rb/raw/main/llm.png" width="200" height="200" border="0" alt="llm.rb"></a>
3
+ </p>
4
+ <p align="center">
5
+ <a href="https://0x1eef.github.io/x/llm.rb?rebuild=1"><img src="https://img.shields.io/badge/docs-0x1eef.github.io-blue.svg" alt="RubyDoc"></a>
6
+ <a href="https://opensource.org/license/0bsd"><img src="https://img.shields.io/badge/License-0BSD-orange.svg?" alt="License"></a>
7
+ <a href="https://github.com/llmrb/llm.rb/tags"><img src="https://img.shields.io/badge/version-4.3.0-green.svg?" alt="Version"></a>
8
+ </p>
4
9
 
5
10
  ## About
6
11
 
@@ -9,30 +14,39 @@ includes OpenAI, Gemini, Anthropic, xAI (Grok), zAI, DeepSeek, Ollama,
9
14
  and LlamaCpp. The toolkit includes full support for chat, streaming,
10
15
  tool calling, audio, images, files, and structured outputs.
11
16
 
17
+ And it is licensed under the [0BSD License](https://choosealicense.com/licenses/0bsd/) &ndash;
18
+ one of the most permissive open source licenses, with minimal conditions for use,
19
+ modification, and/or distribution. Attribution is appreciated, but not required
20
+ by the license. Built with [good music](https://www.youtube.com/watch?v=SNvaqwTbn14)
21
+ and a lot of ☕️.
22
+
12
23
  ## Quick start
13
24
 
14
25
  #### REPL
15
26
 
16
- The [LLM::Bot](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Bot.html) class provides
27
+ The [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html) class provides
17
28
  a session with an LLM provider that maintains conversation history and context across
18
- multiple requests. The following example implements a simple REPL loop:
29
+ multiple requests. The following example implements a simple REPL loop, and the response
30
+ is streamed to the terminal in real-time as it arrives from the provider. The provider
31
+ happens to be OpenAI in this case but it could be any other provider, and `$stdout`
32
+ could be any object that implements the `#<<` method:
19
33
 
20
34
  ```ruby
21
35
  #!/usr/bin/env ruby
22
36
  require "llm"
23
37
 
24
38
  llm = LLM.openai(key: ENV["KEY"])
25
- bot = LLM::Bot.new(llm, stream: $stdout)
39
+ ses = LLM::Session.new(llm, stream: $stdout)
26
40
  loop do
27
41
  print "> "
28
- bot.chat(STDIN.gets)
42
+ ses.talk(STDIN.gets || break)
29
43
  puts
30
44
  end
31
45
  ```
32
46
 
33
47
  #### Schema
34
48
 
35
- The [LLM::Schema](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Schema.html) class provides
49
+ The [LLM::Schema](https://0x1eef.github.io/x/llm.rb/LLM/Schema.html) class provides
36
50
  a simple DSL for describing the structure of a response that an LLM emits according
37
51
  to a JSON schema. The schema lets a client describe what JSON object an LLM should
38
52
  emit, and the LLM will abide by the schema to the best of its ability:
@@ -40,21 +54,32 @@ emit, and the LLM will abide by the schema to the best of its ability:
40
54
  ```ruby
41
55
  #!/usr/bin/env ruby
42
56
  require "llm"
57
+ require "pp"
43
58
 
44
- class Estimation < LLM::Schema
45
- property :age, Integer, "Estimated age", required: true
46
- property :confidence, Number, "0.0–1.0", required: true
47
- property :notes, String, "Short notes", optional: true
59
+ class Report < LLM::Schema
60
+ property :category, String, "Report category", required: true
61
+ property :summary, String, "Short summary", required: true
62
+ property :services, Array[String], "Impacted services", required: true
63
+ property :timestamp, String, "When it happened", optional: true
48
64
  end
49
65
 
50
66
  llm = LLM.openai(key: ENV["KEY"])
51
- bot = LLM::Bot.new(llm, schema: Estimation)
52
- bot.chat("Estimate age and confidence for a man in his 30s.")
67
+ ses = LLM::Session.new(llm, schema: Report)
68
+ res = ses.talk("Structure this report: 'Database latency spiked at 10:42 UTC, causing 5% request timeouts for 12 minutes.'")
69
+ pp res.messages.first(&:assistant?).content!
70
+
71
+ ##
72
+ # {
73
+ # "category" => "Performance Incident",
74
+ # "summary" => "Database latency spiked, causing 5% request timeouts for 12 minutes.",
75
+ # "services" => ["Database"],
76
+ # "timestamp" => "2024-06-05T10:42:00Z"
77
+ # }
53
78
  ```
54
79
 
55
80
  #### Tools
56
81
 
57
- The [LLM::Tool](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Tool.html) class lets you
82
+ The [LLM::Tool](https://0x1eef.github.io/x/llm.rb/LLM/Tool.html) class lets you
58
83
  define callable tools for the model. Each tool is described to the LLM as a function
59
84
  it can invoke to fetch information or perform an action. The model decides when to
60
85
  call tools based on the conversation; when it does, llm.rb runs the tool and sends
@@ -76,19 +101,19 @@ class System < LLM::Tool
76
101
  end
77
102
 
78
103
  llm = LLM.openai(key: ENV["KEY"])
79
- bot = LLM::Bot.new(llm, tools: [System])
80
- bot.chat("Run `date`.")
81
- bot.chat(bot.functions.map(&:call)) # report return value to the LLM
104
+ ses = LLM::Session.new(llm, tools: [System])
105
+ ses.talk("Run `date`.")
106
+ ses.talk(ses.functions.map(&:call)) # report return value to the LLM
82
107
  ```
83
108
 
84
109
  #### Agents
85
110
 
86
- The [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Agent.html)
111
+ The [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html)
87
112
  class provides a class-level DSL for defining reusable, preconfigured
88
113
  assistants with defaults for model, tools, schema, and instructions.
89
114
  Instructions are injected only on the first request, and unlike
90
- [LLM::Bot](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Bot.html),
91
- an [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Agent.html)
115
+ [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html),
116
+ an [LLM::Agent](https://0x1eef.github.io/x/llm.rb/LLM/Agent.html)
92
117
  will automatically call tools when needed:
93
118
 
94
119
  ```ruby
@@ -104,28 +129,52 @@ end
104
129
 
105
130
  llm = LLM.openai(key: ENV["KEY"])
106
131
  agent = SystemAdmin.new(llm)
107
- res = agent.chat("Run 'date'")
132
+ res = agent.talk("Run 'date'")
108
133
  ```
109
134
 
110
135
  #### Prompts
111
136
 
112
- The [LLM::Bot#build_prompt](https://0x1eef.github.io/x/llm.rb/LLM/LLM/Bot.html#build_prompt-instance_method)
113
- method provides a simple DSL for building a chain of messages that
114
- can be sent in a single request. A conversation with an LLM consists
115
- of messages that have a role (eg system, user), and content:
137
+ The [LLM::Prompt](https://0x1eef.github.io/x/llm.rb/LLM/Prompt.html)
138
+ class represents a single request composed of multiple messages.
139
+ It is useful when a single turn needs more than one message, for example:
140
+ system instructions plus one or more user messages, or a replay of
141
+ prior context:
116
142
 
117
143
  ```ruby
118
144
  #!/usr/bin/env ruby
119
145
  require "llm"
120
146
 
121
147
  llm = LLM.openai(key: ENV["KEY"])
122
- bot = LLM::Bot.new(llm)
123
- prompt = bot.build_prompt do
124
- it.system "Answer concisely."
125
- it.user "Was 2024 a leap year?"
126
- it.user "How many days were in that year?"
148
+ ses = LLM::Session.new(llm)
149
+
150
+ prompt = ses.prompt do
151
+ system "Be concise and show your reasoning briefly."
152
+ user "If a train goes 60 mph for 1.5 hours, how far does it travel?"
153
+ user "Now double the speed for the same time."
127
154
  end
128
- bot.chat(prompt)
155
+
156
+ ses.talk(prompt)
157
+ ```
158
+
159
+ But prompts are not session-scoped. [LLM::Prompt](https://0x1eef.github.io/x/llm.rb/LLM/Prompt.html)
160
+ is a first-class object that you can build and pass around independently of a session.
161
+ This enables patterns where you compose a prompt in one part of your code,
162
+ and execute it through a session elsewhere:
163
+
164
+ ```ruby
165
+ #!/usr/bin/env ruby
166
+ require "llm"
167
+
168
+ llm = LLM.openai(key: ENV["KEY"])
169
+ ses = LLM::Session.new(llm)
170
+
171
+ prompt = LLM::Prompt.new(llm) do
172
+ system "Be concise and show your reasoning briefly."
173
+ user "If a train goes 60 mph for 1.5 hours, how far does it travel?"
174
+ user "Now double the speed for the same time."
175
+ end
176
+
177
+ ses.talk(prompt)
129
178
  ```
130
179
 
131
180
  ## Features
@@ -134,10 +183,17 @@ bot.chat(prompt)
134
183
  - ✅ Unified API across providers
135
184
  - 📦 Zero runtime deps (stdlib-only)
136
185
  - 🧩 Pluggable JSON adapters (JSON, Oj, Yajl, etc)
137
- - ♻️ Optional persistent HTTP pool (net-http-persistent)
186
+ - 🧱 Builtin tracer API ([LLM::Tracer](https://0x1eef.github.io/x/llm.rb/LLM/Tracer.html))
187
+
188
+ #### Optionals
189
+
190
+ - ♻️ Optional persistent HTTP pool via net-http-persistent ([net-http-persistent](https://github.com/drbrain/net-http-persistent))
191
+ - 📈 Optional telemetry support via OpenTelemetry ([opentelemetry-sdk](https://github.com/open-telemetry/opentelemetry-ruby))
192
+ - 🪵 Optional logging support via Ruby's standard library ([ruby/logger](https://github.com/ruby/logger))
138
193
 
139
194
  #### Chat, Agents
140
195
  - 🧠 Stateless + stateful chat (completions + responses)
196
+ - 💾 Save and restore sessions across processes
141
197
  - 🤖 Tool calling / function execution
142
198
  - 🔁 Agent tool-call auto-execution (bounded)
143
199
  - 🗂️ JSON Schema structured output
@@ -250,115 +306,151 @@ res3 = llm.responses.create "message 3", previous_response_id: res2.response_id
250
306
  puts res3.output_text
251
307
  ```
252
308
 
253
- #### Thread Safety
254
-
255
- The llm.rb library is thread-safe and can be used in a multi-threaded
256
- environments but it is important to keep in mind that the
257
- [LLM::Provider](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html)
258
- and
259
- [LLM::Bot](https://0x1eef.github.io/x/llm.rb/LLM/Bot.html)
260
- classes should be instantiated once per thread, and not shared
261
- between threads. Generally the library tries to avoid global or
262
- shared state but where it exists reentrant locks are used to
263
- ensure thread-safety.
264
-
265
- ### Conversations
309
+ #### Telemetry
266
310
 
267
- #### Completions
311
+ The llm.rb library includes telemetry support through its tracer API, and it
312
+ can be used to trace LLM requests. It can be useful for debugging, monitoring,
313
+ and observability. The primary use case in mind is integration with tools like
314
+ [LangSmith](https://www.langsmith.com/).
268
315
 
269
- The following example creates an instance of
270
- [LLM::Bot](https://0x1eef.github.io/x/llm.rb/LLM/Bot.html)
271
- and enters into a conversation where each call to "bot.chat" immediately
272
- sends a request to the provider, updates the conversation history, and
273
- returns an [LLM::Response](https://0x1eef.github.io/x/llm.rb/LLM/Response.html).
274
- The full conversation history is automatically included in
275
- each subsequent request:
316
+ The telemetry implementation uses the [opentelemetry-sdk](https://github.com/open-telemetry/opentelemetry-ruby)
317
+ and is based on the [gen-ai telemetry spec(s)](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/gen-ai/).
318
+ This feature is optional, disabled by default, and the [opentelemetry-sdk](https://github.com/open-telemetry/opentelemetry-ruby)
319
+ gem should be installed separately. Please also note that llm.rb will take care of
320
+ loading and configuring the [opentelemetry-sdk](https://github.com/open-telemetry/opentelemetry-ruby)
321
+ library for you, and llm.rb configures an in-memory exporter that doesn't have
322
+ external dependencies by default:
276
323
 
277
324
  ```ruby
278
325
  #!/usr/bin/env ruby
279
326
  require "llm"
327
+ require "pp"
280
328
 
281
- llm = LLM.openai(key: ENV["KEY"])
282
- bot = LLM::Bot.new(llm)
283
- image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
284
- image_path = "/tmp/llm-logo.png"
285
- pdf_path = "/tmp/llm-handbook.pdf"
286
-
287
- prompt = bot.build_prompt do
288
- it.user ["Tell me about this image", bot.image_url(image_url)]
289
- it.user ["Tell me about this image", bot.local_file(image_path)]
290
- it.user ["Tell me about this PDF", bot.local_file(pdf_path)]
291
- end
292
- bot.chat(prompt)
293
- bot.messages.each { |m| puts "[#{m.role}] #{m.content}" }
329
+ llm = LLM.openai(key: ENV["KEY"])
330
+ llm.tracer = LLM::Tracer::Telemetry.new(llm)
331
+
332
+ ses = LLM::Session.new(llm)
333
+ ses.talk "Hello world!"
334
+ ses.talk "Adios."
335
+ ses.tracer.spans.each { |span| pp span }
294
336
  ```
295
337
 
296
- #### Streaming
338
+ The llm.rb library also supports export through the OpenTelemetry Protocol (OTLP).
339
+ OTLP is a standard protocol for exporting telemetry data, and it is supported by
340
+ multiple observability tools. By default the export is batched in the background,
341
+ and happens automatically but short lived scripts might need to
342
+ [explicitly flush](https://0x1eef.github.io/x/llm.rb/LLM/Tracer/Telemetry#flush!-instance_method)
343
+ the exporter before they exit &ndash; otherwise some telemetry data could be lost:
344
+
345
+ ```ruby
346
+ #!/usr/bin/env ruby
347
+ require "llm"
348
+ require "opentelemetry-exporter-otlp"
349
+
350
+ endpoint = "https://api.smith.langchain.com/otel/v1/traces"
351
+ exporter = OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint:)
352
+
353
+ llm = LLM.openai(key: ENV["KEY"])
354
+ llm.tracer = LLM::Tracer::Telemetry.new(llm, exporter:)
355
+
356
+ ses = LLM::Session.new(llm)
357
+ ses.talk "hello"
358
+ ses.talk "how are you?"
297
359
 
298
- The following example streams the messages in a conversation
299
- as they are generated in real-time. The `stream` option can
300
- be set to an IO object, or the value `true` to enable streaming.
301
- When streaming, the `bot.chat` method will block until the entire
302
- stream is received. At the end, it returns the `LLM::Response` object
303
- containing the full aggregated content:
360
+ at_exit do
361
+ # Helpful for short-lived scripts, otherwise the exporter
362
+ # might not have time to flush pending telemetry data
363
+ ses.tracer.flush!
364
+ end
365
+ ```
366
+
367
+ #### Logger
368
+
369
+ The llm.rb library includes simple logging support through its
370
+ tracer API, and Ruby's standard library ([ruby/logger](https://github.com/ruby/logger)).
371
+ This feature is optional, disabled by default, and it can be useful for debugging and/or
372
+ monitoring requests to LLM providers. The `path` or `io` options can be used to choose
373
+ where logs are written to, and by default it is set to `$stdout`:
304
374
 
305
375
  ```ruby
306
376
  #!/usr/bin/env ruby
307
377
  require "llm"
308
378
 
309
379
  llm = LLM.openai(key: ENV["KEY"])
310
- bot = LLM::Bot.new(llm, stream: $stdout)
311
- image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
312
- image_path = "/tmp/llm-logo.png"
313
- pdf_path = "/tmp/llm-handbook.pdf"
314
-
315
- prompt = bot.build_prompt do
316
- it.user ["Tell me about this image", bot.image_url(image_url)]
317
- it.user ["Tell me about this image", bot.local_file(image_path)]
318
- it.user ["Tell me about the PDF", bot.local_file(pdf_path)]
319
- end
320
- bot.chat(prompt)
380
+ llm.tracer = LLM::Tracer::Logger.new(llm, io: $stdout)
381
+
382
+ ses = LLM::Session.new(llm)
383
+ ses.talk "Hello world!"
384
+ ses.talk "Adios."
321
385
  ```
322
386
 
323
- ### Schema
387
+ #### Serialization
324
388
 
325
- All LLM providers except Anthropic and DeepSeek allow a client to describe
326
- the structure of a response that a LLM emits according to a schema that is
327
- described by JSON. The schema lets a client describe what JSON object
328
- an LLM should emit, and the LLM will abide by the schema to the best of
329
- its ability:
389
+ [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html) can be
390
+ serialized and deserialized across process boundaries and persisted to
391
+ storage such as files, a `jsonb` column (PostgreSQL), or other backends
392
+ through a JSON representation of the history encapsulated by
393
+ [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html)
394
+ &ndash; inclusive of tool metadata as well:
330
395
 
396
+ * Process 1
331
397
  ```ruby
332
398
  #!/usr/bin/env ruby
333
399
  require "llm"
334
400
 
335
- class Player < LLM::Schema
336
- property :name, String, "The player's name", required: true
337
- property :position, Array[Number], "The player's [x, y] position", required: true
338
- end
339
-
340
401
  llm = LLM.openai(key: ENV["KEY"])
341
- bot = LLM::Bot.new(llm, schema: Player)
342
- prompt = bot.build_prompt do
343
- it.system "The player's name is Sam and their position is (7, 12)."
344
- it.user "Return the player's name and position"
345
- end
402
+ ses = LLM::Session.new(llm)
403
+ ses.talk "Howdy partner"
404
+ ses.talk "I'll see you later"
405
+ ses.save(path: "session.json")
406
+ ```
407
+ * Process 2
408
+ ```ruby
409
+ #!/usr/bin/env ruby
410
+ require "llm"
411
+ require "pp"
346
412
 
347
- player = bot.chat(prompt).content!
348
- puts "name: #{player['name']}"
349
- puts "position: #{player['position'].join(', ')}"
413
+ llm = LLM.openai(key: ENV["KEY"])
414
+ ses = LLM::Session.new(llm)
415
+ ses.restore(path: "session.json")
416
+ ses.talk "Howdy partner. I'm back"
417
+ pp ses.messages
350
418
  ```
351
419
 
352
- ### Tools
420
+ But how does it work without a file ? The [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html)
421
+ class implements `#to_json` and it can be used to obtain a JSON representation
422
+ of a session that can be stored in a `jsonb` column in PostgreSQL, or any
423
+ other storage backend. The session can then be restored from the JSON
424
+ representation via the restore method and its `string` argument:
425
+
426
+ ```ruby
427
+ #!/usr/bin/env ruby
428
+ require "llm"
429
+
430
+ llm = LLM.openai(key: ENV["KEY"])
431
+ ses1 = LLM::Session.new(llm)
432
+ ses1.talk "Howdy partner"
433
+ ses1.talk "I'll see you later"
434
+
435
+ json = ses1.to_json
436
+ ses2 = LLM::Session.new(llm)
437
+ ses2.restore(string: json)
438
+ ses2.talk "Howdy partner. I'm back"
439
+ ```
353
440
 
354
- #### Introduction
441
+ #### Thread Safety
355
442
 
356
- All providers support a powerful feature known as tool calling, and although
357
- it is a little complex to understand at first, it can be powerful for building
358
- agents. There are three main interfaces to understand: [LLM::Function](https://0x1eef.github.io/x/llm.rb/LLM/Function.html),
359
- [LLM::Tool](https://0x1eef.github.io/x/llm.rb/LLM/Tool.html), and
360
- [LLM::ServerTool](https://0x1eef.github.io/x/llm.rb/LLM/ServerTool.html).
443
+ The llm.rb library is thread-safe and can be used in a multi-threaded
444
+ environments but it is important to keep in mind that the
445
+ [LLM::Provider](https://0x1eef.github.io/x/llm.rb/LLM/Provider.html)
446
+ and
447
+ [LLM::Session](https://0x1eef.github.io/x/llm.rb/LLM/Session.html)
448
+ classes should be instantiated once per thread, and not shared
449
+ between threads. Generally the library tries to avoid global or
450
+ shared state but where it exists reentrant locks are used to
451
+ ensure thread-safety.
361
452
 
453
+ ### Tools
362
454
 
363
455
  #### LLM::Function
364
456
 
@@ -366,13 +458,7 @@ The following example demonstrates [LLM::Function](https://0x1eef.github.io/x/ll
366
458
  and how it can define a local function (which happens to be a tool), and how
367
459
  a provider (such as OpenAI) can then detect when we should call the function.
368
460
  Its most notable feature is that it can act as a closure and has access to
369
- its surrounding scope, which can be useful in some situations.
370
-
371
- The
372
- [LLM::Bot#functions](https://0x1eef.github.io/x/llm.rb/LLM/Bot.html#functions-instance_method)
373
- method returns an array of functions that can be called after a `chat` interaction
374
- if the LLM detects a function should be called. You would then typically call these
375
- functions and send their results back to the LLM in a subsequent `chat` call:
461
+ its surrounding scope, which can be useful in some situations:
376
462
 
377
463
  ```ruby
378
464
  #!/usr/bin/env ruby
@@ -393,14 +479,14 @@ tool = LLM.function(:system) do |fn|
393
479
  end
394
480
  end
395
481
 
396
- bot = LLM::Bot.new(llm, tools: [tool])
397
- bot.chat "Your task is to run shell commands via a tool.", role: :user
482
+ ses = LLM::Session.new(llm, tools: [tool])
483
+ ses.talk "Your task is to run shell commands via a tool.", role: :user
398
484
 
399
- bot.chat "What is the current date?", role: :user
400
- bot.chat bot.functions.map(&:call) # report return value to the LLM
485
+ ses.talk "What is the current date?", role: :user
486
+ ses.talk ses.functions.map(&:call) # report return value to the LLM
401
487
 
402
- bot.chat "What operating system am I running? (short version please!)", role: :user
403
- bot.chat bot.functions.map(&:call) # report return value to the LLM
488
+ ses.talk "What operating system am I running?", role: :user
489
+ ses.talk ses.functions.map(&:call) # report return value to the LLM
404
490
 
405
491
  ##
406
492
  # {stderr: "", stdout: "Thu May 1 10:01:02 UTC 2025"}
@@ -440,14 +526,14 @@ class System < LLM::Tool
440
526
  end
441
527
 
442
528
  llm = LLM.openai(key: ENV["KEY"])
443
- bot = LLM::Bot.new(llm, tools: [System])
444
- bot.chat "Your task is to run shell commands via a tool.", role: :user
529
+ ses = LLM::Session.new(llm, tools: [System])
530
+ ses.talk "Your task is to run shell commands via a tool.", role: :user
445
531
 
446
- bot.chat "What is the current date?", role: :user
447
- bot.chat bot.functions.map(&:call) # report return value to the LLM
532
+ ses.talk "What is the current date?", role: :user
533
+ ses.talk ses.functions.map(&:call) # report return value to the LLM
448
534
 
449
- bot.chat "What operating system am I running? (short version please!)", role: :user
450
- bot.chat bot.functions.map(&:call) # report return value to the LLM
535
+ ses.talk "What operating system am I running?", role: :user
536
+ ses.talk ses.functions.map(&:call) # report return value to the LLM
451
537
 
452
538
  ##
453
539
  # {stderr: "", stdout: "Thu May 1 10:01:02 UTC 2025"}
@@ -470,53 +556,36 @@ it has been uploaded. The file (a specialized instance of
470
556
  require "llm"
471
557
 
472
558
  llm = LLM.openai(key: ENV["KEY"])
473
- bot = LLM::Bot.new(llm)
559
+ ses = LLM::Session.new(llm)
474
560
  file = llm.files.create(file: "/tmp/llm-book.pdf")
475
- res = bot.chat ["Tell me about this file", file]
476
- res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
561
+ res = ses.talk ["Tell me about this file", file]
562
+ res.messages.each { |m| puts "[#{m.role}] #{m.content}" }
477
563
  ```
478
564
 
479
565
  ### Prompts
480
566
 
481
567
  #### Multimodal
482
568
 
483
- While LLMs inherently understand text, they can also process and
484
- generate other types of media such as audio, images, video, and
485
- even URLs. To provide these multimodal inputs to the LLM, llm.rb
486
- uses explicit tagging methods on the `LLM::Bot` instance.
487
- These methods wrap your input into a special `LLM::Object`,
488
- clearly indicating its type and intent to the underlying LLM
489
- provider.
569
+ LLMs are great with text, but many can also handle images, audio, video,
570
+ and URLs. With llm.rb you pass those inputs by tagging them with one of
571
+ the following methods. And for multipart prompts, we can pass an array
572
+ where each element is a part of the input. See the example below for
573
+ details, in the meantime here are the methods to know for multimodal
574
+ inputs:
490
575
 
491
- For instance, to specify an image URL, you would use
492
- `bot.image_url`. For a local file, `bot.local_file`. For an
493
- already uploaded file managed by the LLM provider's Files API,
494
- `bot.remote_file`. This approach ensures clarity and allows
495
- llm.rb to correctly format the input for each provider's
496
- specific requirements.
497
-
498
- An array can be used for a prompt with multiple parts, where each
499
- element contributes to the overall input:
576
+ * `ses.image_url` for an image URL
577
+ * `ses.local_file` for a local file
578
+ * `ses.remote_file` for a file already uploaded via the provider's Files API
500
579
 
501
580
  ```ruby
502
581
  #!/usr/bin/env ruby
503
582
  require "llm"
504
583
 
505
584
  llm = LLM.openai(key: ENV["KEY"])
506
- bot = LLM::Bot.new(llm)
507
- image_url = "https://upload.wikimedia.org/wikipedia/commons/9/97/The_Earth_seen_from_Apollo_17.jpg"
508
- image_path = "/tmp/llm-logo.png"
509
- pdf_path = "/tmp/llm-book.pdf"
510
-
511
- res1 = bot.chat ["Tell me about this image URL", bot.image_url(image_url)]
512
- res1.choices.each { |m| puts "[#{m.role}] #{m.content}" }
513
-
514
- file = llm.files.create(file: pdf_path)
515
- res2 = bot.chat ["Tell me about this PDF", bot.remote_file(file)]
516
- res2.choices.each { |m| puts "[#{m.role}] #{m.content}" }
517
-
518
- res3 = bot.chat ["Tell me about this image", bot.local_file(image_path)]
519
- res3.choices.each { |m| puts "[#{m.role}] #{m.content}" }
585
+ ses = LLM::Session.new(llm)
586
+ res = ses.talk ["Tell me about this image URL", ses.image_url(url)]
587
+ res = ses.talk ["Tell me about this PDF", ses.remote_file(file)]
588
+ res = ses.talk ["Tell me about this image", ses.local_file(path)]
520
589
  ```
521
590
 
522
591
  ### Audio
@@ -694,9 +763,9 @@ end
694
763
  ##
695
764
  # Select a model
696
765
  model = llm.models.all.find { |m| m.id == "gpt-3.5-turbo" }
697
- bot = LLM::Bot.new(llm, model: model.id)
698
- res = bot.chat "Hello #{model.id} :)"
699
- res.choices.each { |m| puts "[#{m.role}] #{m.content}" }
766
+ ses = LLM::Session.new(llm, model: model.id)
767
+ res = ses.talk "Hello #{model.id} :)"
768
+ res.messages.each { |m| puts "[#{m.role}] #{m.content}" }
700
769
  ```
701
770
 
702
771
  ## Install
@@ -705,6 +774,12 @@ llm.rb can be installed via rubygems.org:
705
774
 
706
775
  gem install llm.rb
707
776
 
777
+ ## Sources
778
+
779
+ * [GitHub.com](https://github.com/llmrb/llm.rb)
780
+ * [GitLab.com](https://gitlab.com/llmrb/llm.rb)
781
+ * [Codeberg.org](https://codeberg.org/llmrb/llm.rb)
782
+
708
783
  ## License
709
784
 
710
785
  [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)