foobara-llm-backed-command 0.0.10 → 1.0.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: ad9f2ebc5f7f9277ed85e0f8cc410470a8ee00d9b6d88c8afb8dabab1bd604fb
4
- data.tar.gz: 1076fec92769e65d72a4674e77989520f562e0c5c6f432afe4a11cab5e38a853
3
+ metadata.gz: 7747f5873ca5862ac16e10edc62f856fe9f41b480ca9537e2fac7d967b2647ba
4
+ data.tar.gz: 718b24e6f4e33bc993fd39fd043a17343ea7e7b8342b5cc1807d5b7f662f009b
5
5
  SHA512:
6
- metadata.gz: 03e6f1c662e353803a097cc675e8f71d8911d02036fa668a66f64dbd6940b7c48125d538dd975ae80ff8348a9f43cfdecd602bb0a4393fd8c0d27f9871c31e1c
7
- data.tar.gz: 99f9120528785f7260d896967c4d9737e3ebdd75967aec9432e20ce9a3dbed5f85a1d6ebd1a254a23e370562e4ab4f44825f8b662ce3b7732abb0068d2c6e706
6
+ metadata.gz: 7425c556694c8350956580d8492321b8d622c66245d0853911602229ecf894457f1bdb5c08934de8bc4a4e9bf06583983173a84f7dd563bd25f949cccfc41b19
7
+ data.tar.gz: 14847e3e1fc3de9d2f438b4a19690a018341e09d40f9ff617175ce8be2604f8abb99d6b860b8015f92136bc373b903d0c72e644e9080df1e4df3a19324d5ed58
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [1.0.0] - 2025-07-08
2
+
3
+ - Add support for separate serializers for user and assistant messages
4
+
1
5
  ## [0.0.10] - 2025-07-06
2
6
 
3
7
  - Handle bizarre deepseek-r1 stranded </think> tag
@@ -2,13 +2,20 @@
2
2
  # on the class.
3
3
  #
4
4
  # inputs do
5
- # association_depth :symbol, one_of: JsonSchemaGenerator::AssociationDepth, default: AssociationDepth::ATOM
5
+ # association_depth :symbol, one_of: AssociationDepth, default: AssociationDepth::ATOM
6
6
  # llm_model :symbol, one_of: Foobara::Ai::AnswerBot::Types::ModelEnum
7
7
  # end
8
8
  module Foobara
9
9
  module LlmBackedExecuteMethod
10
10
  include Concern
11
11
 
12
+ LLM_INTEGRATION_KEYS = [
13
+ :llm_model,
14
+ :association_depth,
15
+ :user_association_depth,
16
+ :assistant_association_depth
17
+ ].freeze
18
+
12
19
  on_include do
13
20
  depends_on Ai::AnswerBot::GenerateNextMessage
14
21
  possible_error :could_not_parse_result_json,
@@ -20,7 +27,11 @@ module Foobara
20
27
  end
21
28
 
22
29
  def execute
23
- determine_serializer
30
+ determine_user_association_depth
31
+ determine_assistant_association_depth
32
+ determine_user_serializer
33
+ determine_assistant_serializer
34
+ determine_llm_instructions
24
35
  construct_messages
25
36
  generate_answer
26
37
  parse_answer
@@ -28,32 +39,51 @@ module Foobara
28
39
  parsed_answer
29
40
  end
30
41
 
31
- attr_accessor :serializer, :answer, :parsed_answer, :messages
32
-
33
- def determine_serializer
34
- depth = if respond_to?(:association_depth)
35
- association_depth
36
- else
37
- Foobara::JsonSchemaGenerator::AssociationDepth::AGGREGATE
38
- end
39
-
40
- serializer = case depth
41
- when Foobara::JsonSchemaGenerator::AssociationDepth::ATOM
42
- Foobara::CommandConnectors::Serializers::AtomicSerializer
43
- when Foobara::JsonSchemaGenerator::AssociationDepth::AGGREGATE
44
- Foobara::CommandConnectors::Serializers::AggregateSerializer
45
- when Foobara::JsonSchemaGenerator::AssociationDepth::PRIMARY_KEY_ONLY
46
- # :nocov:
47
- raise "PRIMARY_KEY_ONLY depth not yet implemented"
48
- # :nocov:
49
- else
50
- # :nocov:
51
- raise "Unknown depth: #{depth}"
52
- # :nocov:
53
- end
54
-
55
- # cache this?
56
- self.serializer = serializer.new
42
+ attr_accessor :answer, :parsed_answer, :messages, :assistant_serializer, :user_serializer,
43
+ :computed_assistant_association_depth, :computed_user_association_depth,
44
+ :llm_instructions
45
+
46
+ def determine_assistant_association_depth
47
+ self.computed_assistant_association_depth = if respond_to?(:assistant_association_depth)
48
+ assistant_association_depth
49
+ elsif respond_to?(:association_depth)
50
+ association_depth
51
+ else
52
+ Foobara::AssociationDepth::PRIMARY_KEY_ONLY
53
+ end
54
+ end
55
+
56
+ def determine_assistant_serializer
57
+ self.assistant_serializer = depth_to_serializer(computed_assistant_association_depth)
58
+ end
59
+
60
+ def determine_user_association_depth
61
+ self.computed_user_association_depth = if respond_to?(:user_association_depth)
62
+ user_association_depth
63
+ elsif respond_to?(:association_depth)
64
+ association_depth
65
+ else
66
+ Foobara::AssociationDepth::ATOM
67
+ end
68
+ end
69
+
70
+ def determine_user_serializer
71
+ self.user_serializer = depth_to_serializer(computed_user_association_depth)
72
+ end
73
+
74
+ def depth_to_serializer(depth)
75
+ case depth
76
+ when Foobara::AssociationDepth::ATOM
77
+ Foobara::CommandConnectors::Serializers::AtomicSerializer
78
+ when Foobara::AssociationDepth::AGGREGATE
79
+ Foobara::CommandConnectors::Serializers::AggregateSerializer
80
+ when Foobara::AssociationDepth::PRIMARY_KEY_ONLY
81
+ Foobara::CommandConnectors::Serializers::EntitiesToPrimaryKeysSerializer
82
+ else
83
+ # :nocov:
84
+ raise "Unknown depth: #{depth}"
85
+ # :nocov:
86
+ end.instance
57
87
  end
58
88
 
59
89
  def generate_answer
@@ -61,6 +91,7 @@ module Foobara
61
91
  chat: Ai::AnswerBot::Types::Chat.new(messages:)
62
92
  }
63
93
 
94
+ # NOTE: some models don't allow 0 such as o1. Manually set to 1 in calling code for such models for now.
64
95
  inputs[:temperature] = if respond_to?(:temperature)
65
96
  temperature
66
97
  end || 0
@@ -81,12 +112,25 @@ module Foobara
81
112
  if content.is_a?(String)
82
113
  message
83
114
  else
115
+ serializer = if message[:role] == :user
116
+ user_serializer
117
+ else
118
+ assistant_serializer
119
+ end
120
+
84
121
  content = serializer.serialize(content)
85
122
  message.merge(content: JSON.fast_generate(content))
86
123
  end
87
124
  end
88
125
  end
89
126
 
127
+ def determine_llm_instructions
128
+ self.llm_instructions = self.class.llm_instructions(
129
+ computed_user_association_depth,
130
+ computed_assistant_association_depth
131
+ )
132
+ end
133
+
90
134
  def build_messages
91
135
  [
92
136
  {
@@ -95,20 +139,16 @@ module Foobara
95
139
  },
96
140
  {
97
141
  role: :user,
98
- content: inputs.except(:llm_model, :association_depth)
142
+ content: inputs.except(*LLM_INTEGRATION_KEYS)
99
143
  }
100
144
  ]
101
145
  end
102
146
 
103
- def llm_instructions
104
- self.class.llm_instructions
105
- end
106
-
107
147
  def parse_answer
108
148
  stripped_answer = answer.gsub(/<THINK>.*?<\/THINK>/mi, "")
109
149
  # For some reason sometimes deepseek-r1:32b starts the answer with "\n\n</think>\n\n"
110
150
  # so removing it as a special case
111
- stripped_answer = stripped_answer.gsub(/\A\s*<\/think>\s*/mi, "")
151
+ stripped_answer = stripped_answer.gsub(/\A\s*<\/?think>\s*/mi, "")
112
152
  fencepostless_answer = stripped_answer.gsub(/^\s*```\w*\n(.*)```\s*\z/m, "\\1")
113
153
 
114
154
  # TODO: should we verify against json-schema or no?
@@ -142,71 +182,81 @@ module Foobara
142
182
  end
143
183
 
144
184
  module ClassMethods
145
- def inputs_json_schema
146
- @inputs_json_schema ||= JsonSchemaGenerator.to_json_schema(inputs_type_without_llm_integration_inputs)
185
+ def inputs_json_schema(association_depth)
186
+ JsonSchemaGenerator.to_json_schema(
187
+ inputs_type_without_llm_integration_inputs,
188
+ association_depth:
189
+ )
147
190
  end
148
191
 
149
192
  def inputs_type_without_llm_integration_inputs
150
- return @inputs_type_without_llm_integration_inputs if @inputs_type_without_llm_integration_inputs
151
-
152
193
  type_declaration = Util.deep_dup(inputs_type.declaration_data)
153
194
 
154
195
  element_type_declarations = type_declaration[:element_type_declarations]
155
196
 
156
197
  changed = false
157
198
 
158
- if element_type_declarations.key?(:llm_model)
159
- changed = true
160
- element_type_declarations.delete(:llm_model)
161
- end
162
-
163
- if element_type_declarations.key?(:association_depth)
164
- changed = true
165
- element_type_declarations.delete(:association_depth)
199
+ LLM_INTEGRATION_KEYS.each do |key|
200
+ if element_type_declarations.key?(key)
201
+ changed = true
202
+ element_type_declarations.delete(key)
203
+ end
166
204
  end
167
205
 
168
206
  if type_declaration.key?(:defaults)
169
- if type_declaration[:defaults].key?(:llm_model)
170
- changed = true
171
- type_declaration[:defaults].delete(:llm_model)
207
+ LLM_INTEGRATION_KEYS.each do |key|
208
+ if type_declaration[:defaults].key?(key)
209
+ changed = true
210
+ type_declaration[:defaults].delete(key)
211
+ end
172
212
  end
173
213
 
174
- if type_declaration[:defaults].key?(:association_depth)
175
- changed = true
176
- type_declaration[:defaults].delete(:association_depth)
177
- end
178
214
  if type_declaration[:defaults].empty?
179
215
  type_declaration.delete(:defaults)
180
216
  end
181
217
  end
182
218
 
183
- @inputs_type_without_llm_integration_inputs = if changed
184
- domain.foobara_type_from_declaration(type_declaration)
185
- else
186
- inputs_type
187
- end
219
+ if changed
220
+ domain.foobara_type_from_declaration(type_declaration)
221
+ else
222
+ inputs_type
223
+ end
188
224
  end
189
225
 
190
- def result_json_schema
191
- @result_json_schema ||= JsonSchemaGenerator.to_json_schema(
226
+ def result_json_schema(association_depth)
227
+ JsonSchemaGenerator.to_json_schema(
192
228
  result_type,
193
- association_depth: JsonSchemaGenerator::AssociationDepth::PRIMARY_KEY_ONLY
229
+ association_depth:
194
230
  )
195
231
  end
196
232
 
197
- def llm_instructions
198
- @llm_instructions ||= <<~INSTRUCTIONS
233
+ def llm_instructions(user_association_depth, assistant_association_depth)
234
+ key = [user_association_depth, assistant_association_depth]
235
+
236
+ @llm_instructions_cache ||= {}
237
+
238
+ if @llm_instructions_cache.key?(key)
239
+ # :nocov:
240
+ @llm_instructions_cache[key]
241
+ # :nocov:
242
+ else
243
+ @llm_instructions_cache[key] = build_llm_instructions(user_association_depth, assistant_association_depth)
244
+ end
245
+ end
246
+
247
+ def build_llm_instructions(user_association_depth, assistant_association_depth)
248
+ <<~INSTRUCTIONS
199
249
  You are implementing an API for a command named #{scoped_full_name} which has the following description:
200
250
 
201
- #{description}#{" "}
251
+ #{description}
202
252
 
203
253
  Here is the inputs JSON schema for the data you will receive:
204
254
 
205
- #{inputs_json_schema}
255
+ #{inputs_json_schema(user_association_depth)}
206
256
 
207
257
  Here is the result JSON schema:
208
258
 
209
- #{result_json_schema}
259
+ #{result_json_schema(assistant_association_depth)}
210
260
 
211
261
  You will receive 1 message containing only JSON data according to the inputs JSON schema above
212
262
  and you will generate a JSON response that is a valid response according to the result JSON schema above.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara-llm-backed-command
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.10
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 0.0.6
46
+ version: 1.0.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: 0.0.6
53
+ version: 1.0.0
54
54
  email:
55
55
  - azimux@gmail.com
56
56
  executables: []