llm_ruby 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edf2b09bc3a9416193088298e41577369bf5198230c7278d6a832854f04c7e20
4
- data.tar.gz: 735c0e1735d90e5c41a93d7e123bfe359c0a3525bf292ba46d0a6b8c23580c05
3
+ metadata.gz: 0de9e01920a472c7a04f9f794747090e3bf279b403d4efa206b1db7c2b006987
4
+ data.tar.gz: bd77b3a4c5540a82d4a6f1b93213a8b3da60cc9342e581335adeac6fe93c999f
5
5
  SHA512:
6
- metadata.gz: d7509110f0f53028e6d6c6cc899ab31985748e2f5775be64286fc1a66659723953fee49f8ea70c1f797e9c7786bf401634d35f201d4b44d0f4abd11a22d691ed
7
- data.tar.gz: 447086c9fed992e31db3e0bec1f9a92d0b52995ad899f11e9eeba0674eaf0a734918b4ee144a3b869ba6e61b7f2c73dcba2eaec5b602700d8c126a4de4c0ad33
6
+ metadata.gz: bc4b0263dfeaf1db4dded66667ea13e65dfc035d27579a2b032a59ae63f29fd588630d749d492d281322269fbc9d3909f8648f464d250baa99ff5b2afd582193
7
+ data.tar.gz: a4c5713df43e27127afa0b3cbf044ce8d805235f73abd05b3f79dc9fc7a814c193caac9192392a8670523cf74be96da2b52dc648d965116e8e9bbc3f8a2aaaaa
data/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # LLMRuby
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/llm_ruby.svg)](https://badge.fury.io/rb/llm_ruby)
4
+ ![Workflow Status](https://github.com/agamble/llm_ruby/actions/workflows/main.yml/badge.svg)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+
8
+
3
9
  LLMRuby is a Ruby gem that provides a consistent interface for interacting with multiple Large Language Model (LLM) APIs. Most OpenAI, Anthropic and Gemini models are currently supported.
4
10
 
5
11
  ## Installation
@@ -14,9 +14,18 @@ class LLM
14
14
  end
15
15
 
16
16
  def params
17
+ generation_config = {}
18
+ if options[:response_format]
19
+ generation_config = {
20
+ responseMimeType: "application/json",
21
+ responseSchema: options[:response_format]&.gemini_response_format
22
+ }
23
+ end
24
+
17
25
  {
18
26
  systemInstruction: normalized_prompt,
19
- contents: normalized_messages
27
+ contents: normalized_messages,
28
+ generationConfig: generation_config
20
29
  }
21
30
  end
22
31
 
@@ -12,7 +12,8 @@ class LLM
12
12
  LLM::Response.new(
13
13
  content: content,
14
14
  raw_response: parsed_response,
15
- stop_reason: translated_stop_reason
15
+ stop_reason: translated_stop_reason,
16
+ structured_output: structured_output
16
17
  )
17
18
  end
18
19
 
@@ -48,6 +49,12 @@ class LLM
48
49
  def parsed_response
49
50
  raw_response.parsed_response
50
51
  end
52
+
53
+ def structured_output
54
+ @structured_output ||= JSON.parse(parsed_response.dig("candidates", 0, "content", "parts", 0, "text"))
55
+ rescue JSON::ParserError
56
+ nil
57
+ end
51
58
  end
52
59
  end
53
60
  end
@@ -12,7 +12,8 @@ class LLM
12
12
  LLM::Response.new(
13
13
  content: content,
14
14
  raw_response: parsed_response,
15
- stop_reason: normalize_stop_reason
15
+ stop_reason: normalize_stop_reason,
16
+ structured_output: structured_output
16
17
  )
17
18
  end
18
19
 
@@ -42,6 +43,12 @@ class LLM
42
43
  def parsed_response
43
44
  @raw_response.parsed_response
44
45
  end
46
+
47
+ def structured_output
48
+ @structured_output ||= JSON.parse(parsed_response.dig("choices", 0, "message", "content"))
49
+ rescue JSON::ParserError
50
+ nil
51
+ end
45
52
  end
46
53
  end
47
54
  end
@@ -18,7 +18,7 @@ class LLM
18
18
  model: @llm.canonical_name,
19
19
  messages: messages,
20
20
  temperature: options[:temperature],
21
- response_format: options[:response_format],
21
+ response_format: options[:response_format]&.response_format,
22
22
  max_tokens: options[:max_output_tokens],
23
23
  top_p: options[:top_p],
24
24
  stop: options[:stop_sequences],
data/lib/llm/info.rb CHANGED
@@ -76,19 +76,22 @@ class LLM
76
76
  canonical_name: "gpt-4o",
77
77
  display_name: "GPT-4o",
78
78
  provider: :openai,
79
- client_class: LLM::Clients::OpenAI
79
+ client_class: LLM::Clients::OpenAI,
80
+ supports_structured_outputs: true
80
81
  },
81
82
  {
82
83
  canonical_name: "gpt-4o-mini",
83
84
  display_name: "GPT-4o Mini",
84
85
  provider: :openai,
85
- client_class: LLM::Clients::OpenAI
86
+ client_class: LLM::Clients::OpenAI,
87
+ supports_structured_outputs: true
86
88
  },
87
89
  {
88
90
  canonical_name: "gpt-4o-mini-2024-07-18",
89
91
  display_name: "GPT-4o Mini 2024-07-18",
90
92
  provider: :openai,
91
- client_class: LLM::Clients::OpenAI
93
+ client_class: LLM::Clients::OpenAI,
94
+ supports_structured_outputs: true
92
95
  },
93
96
  {
94
97
  canonical_name: "gpt-4o-2024-05-13",
@@ -100,13 +103,15 @@ class LLM
100
103
  canonical_name: "gpt-4o-2024-08-06",
101
104
  display_name: "GPT-4o 2024-08-06",
102
105
  provider: :openai,
103
- client_class: LLM::Clients::OpenAI
106
+ client_class: LLM::Clients::OpenAI,
107
+ supports_structured_outputs: true
104
108
  },
105
109
  {
106
110
  canonical_name: "gpt-4o-2024-11-20",
107
111
  display_name: "GPT-4o 2024-11-20",
108
112
  provider: :openai,
109
- client_class: LLM::Clients::OpenAI
113
+ client_class: LLM::Clients::OpenAI,
114
+ supports_structured_outputs: true
110
115
  },
111
116
  {
112
117
  canonical_name: "chatgpt-4o-latest",
@@ -118,13 +123,15 @@ class LLM
118
123
  canonical_name: "o1",
119
124
  display_name: "o1",
120
125
  provider: :openai,
121
- client_class: LLM::Clients::OpenAI
126
+ client_class: LLM::Clients::OpenAI,
127
+ supports_structured_outputs: true
122
128
  },
123
129
  {
124
130
  canonical_name: "o1-2024-12-17",
125
131
  display_name: "o1 2024-12-17",
126
132
  provider: :openai,
127
- client_class: LLM::Clients::OpenAI
133
+ client_class: LLM::Clients::OpenAI,
134
+ supports_structured_outputs: true
128
135
  },
129
136
  {
130
137
  canonical_name: "o1-preview",
@@ -154,13 +161,15 @@ class LLM
154
161
  canonical_name: "o3-mini",
155
162
  display_name: "o3 Mini",
156
163
  provider: :openai,
157
- client_class: LLM::Clients::OpenAI
164
+ client_class: LLM::Clients::OpenAI,
165
+ supports_structured_outputs: true
158
166
  },
159
167
  {
160
168
  canonical_name: "o3-mini-2025-01-31",
161
169
  display_name: "o3 Mini 2025-01-31",
162
170
  provider: :openai,
163
- client_class: LLM::Clients::OpenAI
171
+ client_class: LLM::Clients::OpenAI,
172
+ supports_structured_outputs: true
164
173
  },
165
174
 
166
175
  # Anthropic Models
@@ -224,13 +233,15 @@ class LLM
224
233
  canonical_name: "gemini-2.0-flash",
225
234
  display_name: "Gemini 2.0 Flash",
226
235
  provider: :google,
227
- client_class: LLM::Clients::Gemini
236
+ client_class: LLM::Clients::Gemini,
237
+ supports_structured_outputs: true
228
238
  },
229
239
  {
230
240
  canonical_name: "gemini-2.0-flash-lite-preview-02-05",
231
241
  display_name: "Gemini 2.0 Flash Lite Preview 02-05",
232
242
  provider: :google,
233
- client_class: LLM::Clients::Gemini
243
+ client_class: LLM::Clients::Gemini,
244
+ supports_structured_outputs: true
234
245
  },
235
246
  {
236
247
  canonical_name: "gemini-1.5-flash-8b",
data/lib/llm/response.rb CHANGED
@@ -1,3 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- LLM::Response = Struct.new(:content, :raw_response, :stop_reason, keyword_init: true)
3
+ require "ostruct"
4
+
5
+ LLM::Response = Struct.new(:content, :raw_response, :stop_reason, :structured_output, keyword_init: true) do
6
+ def structured_output_object
7
+ return nil unless structured_output
8
+
9
+ OpenStruct.new(structured_output)
10
+ end
11
+ end
data/lib/llm/schema.rb ADDED
@@ -0,0 +1,75 @@
1
+ class LLM
2
+ class Schema
3
+ def initialize(name, schema)
4
+ @name = name
5
+ @schema = schema
6
+ end
7
+
8
+ def self.from_file(file_path)
9
+ new(File.basename(file_path, ".json"), JSON.parse(File.read(file_path)))
10
+ end
11
+
12
+ def response_format
13
+ {
14
+ type: "json_schema",
15
+ json_schema: {
16
+ name: @name,
17
+ strict: true,
18
+ schema: @schema
19
+ }
20
+ }
21
+ end
22
+
23
+ def gemini_response_format
24
+ transform_schema(@schema)
25
+ end
26
+
27
+ def transform_schema(schema)
28
+ # Initialize the result as an empty hash.
29
+ openapi_schema = {}
30
+
31
+ # Process the "type" field and handle nullability.
32
+ if schema.key?("type")
33
+ if schema["type"].is_a?(Array)
34
+ # Check for "null" in the type array to mark the schema as nullable.
35
+ if schema["type"].include?("null")
36
+ openapi_schema["nullable"] = true
37
+ # Remove "null" from the type array; if a single type remains, use that.
38
+ remaining_types = schema["type"] - ["null"]
39
+ openapi_schema["type"] = (remaining_types.size == 1) ? remaining_types.first : remaining_types
40
+ else
41
+ openapi_schema["type"] = schema["type"]
42
+ end
43
+ else
44
+ openapi_schema["type"] = schema["type"]
45
+ end
46
+ end
47
+
48
+ # Map simple fields directly: "format", "description", "enum", "maxItems", "minItems".
49
+ ["format", "description", "enum", "maxItems", "minItems"].each do |field|
50
+ openapi_schema[field] = schema[field] if schema.key?(field)
51
+ end
52
+
53
+ # Recursively process "properties" if present.
54
+ if schema.key?("properties") && schema["properties"].is_a?(Hash)
55
+ openapi_schema["properties"] = {}
56
+ schema["properties"].each do |prop, prop_schema|
57
+ openapi_schema["properties"][prop] = transform_schema(prop_schema)
58
+ end
59
+ end
60
+
61
+ # Copy "required" if present.
62
+ openapi_schema["required"] = schema["required"] if schema.key?("required")
63
+
64
+ # Copy "propertyOrdering" if present (non-standard field).
65
+ openapi_schema["propertyOrdering"] = schema["propertyOrdering"] if schema.key?("propertyOrdering")
66
+
67
+ # Recursively process "items" for array types.
68
+ if schema.key?("items")
69
+ openapi_schema["items"] = transform_schema(schema["items"])
70
+ end
71
+
72
+ openapi_schema
73
+ end
74
+ end
75
+ end
data/lib/llm.rb CHANGED
@@ -15,6 +15,7 @@ class LLM
15
15
  @provider = model[:provider]
16
16
  @client_class = model[:client_class]
17
17
  @default_params = model[:additional_default_required_parameters] || {}
18
+ @supports_structured_outputs = model[:supports_structured_outputs] || false
18
19
  end
19
20
 
20
21
  def client
@@ -26,6 +27,10 @@ class LLM
26
27
  :provider,
27
28
  :default_params
28
29
 
30
+ def supports_structured_outputs?
31
+ @supports_structured_outputs
32
+ end
33
+
29
34
  private
30
35
 
31
36
  attr_reader :client_class
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Gamble
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-23 00:00:00.000000000 Z
10
+ date: 2025-02-24 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: event_stream_parser
@@ -155,6 +155,7 @@ files:
155
155
  - lib/llm/info.rb
156
156
  - lib/llm/provider.rb
157
157
  - lib/llm/response.rb
158
+ - lib/llm/schema.rb
158
159
  - lib/llm/stop_reason.rb
159
160
  - lib/llm/version.rb
160
161
  homepage: https://github.com/agamble/llm_ruby