Module: LLM::HuggingfaceMethods

Included in:
Huggingface
Defined in:
lib/scout/llm/backends/huggingface.rb

Constant Summary collapse

MODEL_OPTION_KEYS =
%i[
  task checkpoint dir
  chat_template chat_template_kwargs generation_kwargs
  response_parser tool_argument
  tokenizer_args tokenizer_options
  training_args training_options
  trust_remote_code torch_dtype device_map device
]

Instance Method Summary collapse

Instance Method Details

#embed(text, options = {}) ⇒ Object



190
191
192
193
194
195
196
# File 'lib/scout/llm/backends/huggingface.rb', line 190

def embed(text, options = {})
  model_options = self.model_options(options)
  model_options[:task] = 'Embedding'
  model = self.model(model_options)

  (Array === text) ? model.eval_list(text) : model.eval(text)
end

#format_tool_call(message) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/scout/llm/backends/huggingface.rb', line 88

def format_tool_call(message)
  tool_call = IndiferentHash.setup(JSON.parse(message[:content]))
  arguments = tool_call.delete('arguments') || tool_call.dig('function', 'arguments') || {}
  arguments = JSON.parse(arguments) rescue arguments if String === arguments
  id = tool_call.delete('call_id') || tool_call.delete('id')
  name = tool_call.delete('name') || tool_call.dig('function', 'name')

  {
    role: 'assistant',
    tool_calls: [IndiferentHash.setup({
      type: 'function',
      id: id,
      function: {
        name: name,
        arguments: arguments
      }
    })]
  }
end

#format_tool_definitions(tools) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/scout/llm/backends/huggingface.rb', line 68

def format_tool_definitions(tools)
  return [] if tools.nil?

  tools.values.collect do |obj, definition|
    definition = obj if Hash === obj
    definition = IndiferentHash.setup(definition)

    definition = case definition[:function]
                 when Hash
                   definition
                 else
                   { type: :function, function: definition }
                 end

    definition[:function][:parameters].delete(:defaults) if definition.dig(:function, :parameters)

    definition
  end
end

#format_tool_output(message, last_id = nil) ⇒ Object



108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/scout/llm/backends/huggingface.rb', line 108

def format_tool_output(message, last_id = nil)
  info = IndiferentHash.setup(JSON.parse(message[:content]))
  id = info.delete('call_id') || info.delete('id') || last_id
  name = info.delete('name')
  content = info.delete('content')

  {
    role: 'tool',
    name: name,
    content: content,
    tool_call_id: id,
  }.compact
end

#model(model_options = {}) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/scout/llm/backends/huggingface.rb', line 26

def model(model_options = {})
  require 'scout/model/python/huggingface'
  require 'scout/model/python/huggingface/causal'

  model_options = IndiferentHash.setup(model_options.dup)
  model_options = IndiferentHash.add_defaults(model_options, task: 'CausalLM')

  model_name = IndiferentHash.process_options(model_options, :model)
  dir = model_options[:dir]

  CausalModel.new model_name, dir, model_options
end

#model_options(options = {}) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
# File 'lib/scout/llm/backends/huggingface.rb', line 14

def model_options(options = {})
  options = IndiferentHash.setup(options.dup)
  model_options = IndiferentHash.pull_keys(options, :model) || {}

  MODEL_OPTION_KEYS.each do |key|
    model_options[key] = options[key] if options.include?(key)
  end

  model_options[:model] ||= Scout::Config.get(:model, :huggingface, env: 'HUGGINGFACE_MODEL,HF_MODEL')
  model_options
end

#parse_tool_call(info) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/scout/llm/backends/huggingface.rb', line 122

def parse_tool_call(info)
  info = IndiferentHash.setup(info)
  function = IndiferentHash.setup(info[:function] || {})

  arguments = function[:arguments] || info[:arguments] || info[:parameters] || {}
  arguments = JSON.parse(arguments) rescue arguments if String === arguments

  name = function[:name] || info[:name]
  id = info[:id] || info[:call_id] || info[:tool_call_id] || (name.to_s + '_' + Misc.digest(arguments.to_json))

  { arguments: arguments, id: id, name: name }
end

#prepare_client(options, messages = nil) ⇒ Object



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/scout/llm/backends/huggingface.rb', line 39

def prepare_client(options, messages = nil)
  client = IndiferentHash.process_options(options, :client)

  if client.nil?
    model_options = self.model_options(options)
    Log.debug "Client options: #{model_options.inspect}"

    client = self.model(model_options)
    options[:model] ||= model_options[:model]
  else
    Log.debug "Reusing client: #{Log.fingerprint client}"
  end

  client
end

#process_response(messages, response, tools, options, &block) ⇒ Object



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/scout/llm/backends/huggingface.rb', line 135

def process_response(messages, response, tools, options, &block)
  message = response['message']
  content = message['content']
  
  output = []
  output << IndiferentHash.setup(role: :assistant, content: content) if String === content && !content.empty?

  tool_calls = Array(message[:tool_calls]).collect do |tool_call|
    parse_tool_call(tool_call)
  end.compact

  if tool_calls.any?
    output.concat LLM.process_calls(tools, tool_calls, &block)
  elsif output.empty?
    output << IndiferentHash.setup(role: :assistant, content: '') if message.include?(:content)
  end

  output
end

#query(client, messages, tools = [], parameters = {}) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/scout/llm/backends/huggingface.rb', line 55

def query(client, messages, tools = [], parameters = {})
  formatted_tools = format_tool_definitions(tools)
  parameters[:generation_kwargs] ||= IndiferentHash.pull_keys parameters, :generation_kwargs
  parameters[:chat_template] ||= IndiferentHash.pull_keys parameters, :chat_template
  parameters = parameters.keys_to_sym 
  response = client.chat(messages, formatted_tools, parameters)
  response = ScoutPython.dict2hash(response)
  IndiferentHash.setup({message: response})
rescue
  Log.debug 'Input parameters: ' + "\n" + JSON.pretty_generate(parameters.except(:tools))
  raise $!
end

#reasoning(response, current_meta = nil) ⇒ Object



181
182
183
184
185
186
187
188
# File 'lib/scout/llm/backends/huggingface.rb', line 181

def reasoning(response, current_meta = nil)
  response = IndiferentHash.setup(response)
  message = IndiferentHash.setup(response[:message] || response)
  reasoning_content = message[:thinking]
  reasoning_content = reasoning_content.gsub("\n", ' ') if String === reasoning_content
  Log.medium "Reasoning:\n" + Log.color(:cyan, reasoning_content) if reasoning_content
  reasoning_content
end

#tools(messages, options) ⇒ Object



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
# File 'lib/scout/llm/backends/huggingface.rb', line 155

def tools(messages, options)
  tools = options.delete :tools

  case tools
  when Array
    tools = tools.inject({}) do |acc, definition|
      IndiferentHash.setup definition
      name = definition.dig('name') || definition.dig('function', 'name')
      acc.merge(name => definition)
    end
  when nil
    tools = {}
  end

  chat_messages = messages.reject do |message|
    message[:role].to_s == 'tool' && (message.include?(:tool_call_id) || message.include?(:name))
  end

  tools.merge!(LLM.tools(chat_messages))
  tools.merge!(LLM.associations(chat_messages))

  Log.high "Tools: #{Log.fingerprint tools.keys}" if tools

  tools
end