Module: Foobara::LlmBackedExecuteMethod

Includes:
Concern
Included in:
LlmBackedCommand
Defined in:
foobara-llm-backed-command-1.0.1/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

#answerObject

Returns the value of attribute answer.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def answer
  @answer
end

#assistant_serializerObject

Returns the value of attribute assistant_serializer.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def assistant_serializer
  @assistant_serializer
end

#computed_assistant_association_depthObject

Returns the value of attribute computed_assistant_association_depth.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def computed_assistant_association_depth
  @computed_assistant_association_depth
end

#computed_user_association_depthObject

Returns the value of attribute computed_user_association_depth.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def computed_user_association_depth
  @computed_user_association_depth
end

#llm_instructionsObject

Returns the value of attribute llm_instructions.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def llm_instructions
  @llm_instructions
end

#messagesObject

Returns the value of attribute messages.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def messages
  @messages
end

#parsed_answerObject

Returns the value of attribute parsed_answer.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def parsed_answer
  @parsed_answer
end

#user_serializerObject

Returns the value of attribute user_serializer.



42
43
44
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 42

def user_serializer
  @user_serializer
end

Instance Method Details

#build_messagesObject



134
135
136
137
138
139
140
141
142
143
144
145
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 134

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

#construct_messagesObject



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 108

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.fast_generate(content))
    end
  end
end

#depth_to_serializer(depth) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 74

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
    # :nocov:
    raise "Unknown depth: #{depth}"
    # :nocov:
  end.instance
end

#determine_assistant_association_depthObject



46
47
48
49
50
51
52
53
54
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 46

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_serializerObject



56
57
58
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 56

def determine_assistant_serializer
  self.assistant_serializer = depth_to_serializer(computed_assistant_association_depth)
end

#determine_llm_instructionsObject



127
128
129
130
131
132
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 127

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

#determine_user_association_depthObject



60
61
62
63
64
65
66
67
68
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 60

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_serializerObject



70
71
72
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 70

def determine_user_serializer
  self.user_serializer = depth_to_serializer(computed_user_association_depth)
end

#executeObject



29
30
31
32
33
34
35
36
37
38
39
40
# File 'foobara-llm-backed-command-1.0.1/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

  parsed_answer
end

#generate_answerObject



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 89

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_answerObject



147
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
# File 'foobara-llm-backed-command-1.0.1/src/llm_backed_execute_method.rb', line 147

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
        # :nocov:
        raise
        # :nocov:
      end
    rescue JSON::ParserError => e
      # TODO: figure out how to test this code path
      # :nocov:
      add_runtime_error :could_not_parse_result_json,
                        "Could not parse result JSON: #{e.message}",
                        raw_answer: answer,
                        stripped_answer: fencepostless_answer
      # :nocov:
    end
  end
end