Module: Foobara::LlmBackedExecuteMethod

Includes:
Concern
Included in:
LlmBackedCommand
Defined in:
foobara-llm-backed-command-1.0.5/src/llm_backed_execute_method.rb

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

LLM_INTEGRATION_KEYS =
[
  :llm_model,
  :association_depth,
  :user_association_depth,
  :assistant_association_depth
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Concern

foobara_class_methods_module_for, foobara_concern?, included

Instance Attribute Details

#answer ⇒ Object

Returns the value of attribute answer.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def answer
  @answer
end

#assistant_serializer ⇒ Object

Returns the value of attribute assistant_serializer.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def assistant_serializer
  @assistant_serializer
end

#computed_assistant_association_depth ⇒ Object

Returns the value of attribute computed_assistant_association_depth.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def computed_assistant_association_depth
  @computed_assistant_association_depth
end

#computed_user_association_depth ⇒ Object

Returns the value of attribute computed_user_association_depth.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def computed_user_association_depth
  @computed_user_association_depth
end

#final_answer ⇒ Object

Returns the value of attribute final_answer.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def final_answer
  @final_answer
end

#llm_instructions ⇒ Object

Returns the value of attribute llm_instructions.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def llm_instructions
  @llm_instructions
end

#messages ⇒ Object

Returns the value of attribute messages.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def messages
  @messages
end

#parsed_answer ⇒ Object

Returns the value of attribute parsed_answer.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def parsed_answer
  @parsed_answer
end

#user_serializer ⇒ Object

Returns the value of attribute user_serializer.



43
44
45
# File 'src/llm_backed_execute_method.rb', line 43

def user_serializer
  @user_serializer
end

Instance Method Details

#attempt_to_recover_from_bad_format ⇒ Object

Sometimes we get "whatever" instead of just "whatever" from some smaller models. Let's detect that and handle it...



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'src/llm_backed_execute_method.rb', line 194

def attempt_to_recover_from_bad_format
  self.final_answer = if parsed_answer.is_a?(::Hash) && parsed_answer.keys == ["result"]
                        result_type = self.class.result_type

                        # TODO: implement a Type#valid? method
                        if result_type.process_value(parsed_answer).success?
                          parsed_answer
                        else
                          result = parsed_answer["result"]

                          if result_type.process_value(result).success?
                            result
                          else
                            # simplecov:disable
                            parsed_answer
                            # simplecov:enable
                          end
                        end
                      else
                        parsed_answer
                      end
end

#build_messages ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
# File 'src/llm_backed_execute_method.rb', line 135

def build_messages
  [
    {
      role: :system,
      content: llm_instructions
    },
    {
      role: :user,
      content: inputs.except(*LLM_INTEGRATION_KEYS)
    }
  ]
end

#construct_messages ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'src/llm_backed_execute_method.rb', line 109

def construct_messages
  self.messages = build_messages.map do |message|
    content = message[:content]

    if content.is_a?(String)
      message
    else
      serializer = if message[:role] == :user
                     user_serializer
                   else
                     assistant_serializer
                   end

      content = serializer.serialize(content)
      message.merge(content: JSON.generate(content))
    end
  end
end

#depth_to_serializer(depth) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'src/llm_backed_execute_method.rb', line 75

def depth_to_serializer(depth)
  case depth
  when Foobara::AssociationDepth::ATOM
    Foobara::CommandConnectors::Serializers::AtomicSerializer
  when Foobara::AssociationDepth::AGGREGATE
    Foobara::CommandConnectors::Serializers::AggregateSerializer
  when Foobara::AssociationDepth::PRIMARY_KEY_ONLY
    Foobara::CommandConnectors::Serializers::EntitiesToPrimaryKeysSerializer
  else
    # simplecov:disable
    raise "Unknown depth: #{depth}"
    # simplecov:enable
  end.instance
end

#determine_assistant_association_depth ⇒ Object



47
48
49
50
51
52
53
54
55
# File 'src/llm_backed_execute_method.rb', line 47

def determine_assistant_association_depth
  self.computed_assistant_association_depth = if respond_to?(:assistant_association_depth)
                                                assistant_association_depth
                                              elsif respond_to?(:association_depth)
                                                association_depth
                                              else
                                                Foobara::AssociationDepth::PRIMARY_KEY_ONLY
                                              end
end

#determine_assistant_serializer ⇒ Object



57
58
59
# File 'src/llm_backed_execute_method.rb', line 57

def determine_assistant_serializer
  self.assistant_serializer = depth_to_serializer(computed_assistant_association_depth)
end

#determine_llm_instructions ⇒ Object



128
129
130
131
132
133
# File 'src/llm_backed_execute_method.rb', line 128

def determine_llm_instructions
  self.llm_instructions = self.class.llm_instructions(
    computed_user_association_depth,
    computed_assistant_association_depth
  )
end

#determine_user_association_depth ⇒ Object



61
62
63
64
65
66
67
68
69
# File 'src/llm_backed_execute_method.rb', line 61

def determine_user_association_depth
  self.computed_user_association_depth = if respond_to?(:user_association_depth)
                                           user_association_depth
                                         elsif respond_to?(:association_depth)
                                           association_depth
                                         else
                                           Foobara::AssociationDepth::ATOM
                                         end
end

#determine_user_serializer ⇒ Object



71
72
73
# File 'src/llm_backed_execute_method.rb', line 71

def determine_user_serializer
  self.user_serializer = depth_to_serializer(computed_user_association_depth)
end

#execute ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'src/llm_backed_execute_method.rb', line 29

def execute
  determine_user_association_depth
  determine_assistant_association_depth
  determine_user_serializer
  determine_assistant_serializer
  determine_llm_instructions
  construct_messages
  generate_answer
  parse_answer
  attempt_to_recover_from_bad_format

  final_answer
end

#generate_answer ⇒ Object



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'src/llm_backed_execute_method.rb', line 90

def generate_answer
  inputs = {
    chat: Ai::AnswerBot::Types::Chat.new(messages:)
  }

  # NOTE: some models don't allow 0 such as o1.  Manually set to 1 in calling code for such models for now.
  inputs[:temperature] = if respond_to?(:temperature)
                           temperature
                         end || 0

  if respond_to?(:llm_model)
    inputs[:model] = llm_model
  end

  message = run_subcommand!(Ai::AnswerBot::GenerateNextMessage, inputs)

  self.answer = message.content
end

#parse_answer ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'src/llm_backed_execute_method.rb', line 148

def parse_answer
  stripped_answer = answer.gsub(/<THINK>.*?<\/THINK>/mi, "")
  # For some reason sometimes deepseek-r1:32b starts the answer with "\n\n</think>\n\n"
  # so removing it as a special case
  stripped_answer = stripped_answer.gsub(/\A\s*<\/?think>\s*/mi, "")
  fencepostless_answer = stripped_answer.gsub(/^\s*```\w*\n(.*)```\s*\z/m, "\\1")

  # TODO: should we verify against json-schema or no?
  self.parsed_answer = begin
    JSON.parse(fencepostless_answer)
  rescue JSON::ParserError
    # see if we can extract the last fence-posts content just in case
    last_fence_post_regex = /```\w*\s*\n((?:(?!```).)+)\n```(?:(?!```).)*\z/m

    begin
      match = last_fence_post_regex.match(stripped_answer)

      if match
        fencepostless_answer = match[1]
        JSON.parse(fencepostless_answer)
      else
        # simplecov:disable
        # TODO: raise some specific error here
        raise
        # simplecov:enable
      end
    rescue JSON::ParserError => e
      # TODO: should we retry letting it see the error?
      # simplecov:disable
      if self.class.result_type.extends?(Foobara::BuiltinTypes[:string])
        # let's assume the LLM just didn't bother JSON encoding the string... maybe not safe?
        fencepostless_answer
      else
        # TODO: figure out how to test this code path
        add_runtime_error :could_not_parse_result_json,
                          "Could not parse result JSON: #{e.message}",
                          raw_answer: answer,
                          stripped_answer: fencepostless_answer
      end
      # simplecov:enable
    end
  end
end