Class: LLM::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/scout/llm/agent.rb,
lib/scout/llm/agent/chat.rb,
lib/scout/llm/agent/iterate.rb,
lib/scout/llm/agent/delegate.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(workflow: nil, knowledge_base: nil, start_chat: nil, **kwargs) ⇒ Agent

Returns a new instance of Agent.



10
11
12
13
14
15
16
# File 'lib/scout/llm/agent.rb', line 10

def initialize(workflow: nil, knowledge_base: nil, start_chat: nil, **kwargs)
  @workflow = workflow
  @workflow = Workflow.require_workflow @workflow if String === @workflow
  @knowledge_base = knowledge_base
  @other_options = IndiferentHash.setup(kwargs.dup)
  @start_chat = start_chat
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name) ⇒ Object



22
23
24
# File 'lib/scout/llm/agent/chat.rb', line 22

def method_missing(name,...)
  current_chat.send(name, ...)
end

Instance Attribute Details

#knowledge_baseObject

Returns the value of attribute knowledge_base.



9
10
11
# File 'lib/scout/llm/agent.rb', line 9

def knowledge_base
  @knowledge_base
end

#other_optionsObject

Returns the value of attribute other_options.



9
10
11
# File 'lib/scout/llm/agent.rb', line 9

def other_options
  @other_options
end

#process_exceptionObject

Returns the value of attribute process_exception.



9
10
11
# File 'lib/scout/llm/agent.rb', line 9

def process_exception
  @process_exception
end

#start_chatObject

Returns the value of attribute start_chat.



9
10
11
# File 'lib/scout/llm/agent.rb', line 9

def start_chat
  @start_chat
end

#workflowObject

Returns the value of attribute workflow.



9
10
11
# File 'lib/scout/llm/agent.rb', line 9

def workflow
  @workflow
end

Class Method Details

.load_from_path(path, workflow: nil, knowledge_base: nil, chat: nil) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
# File 'lib/scout/llm/agent.rb', line 83

def self.load_from_path(path, workflow: nil, knowledge_base: nil, chat: nil)
  workflow_path = path['workflow.rb'].find
  knowledge_base_path = path['knowledge_base']
  chat_path = path['start_chat']

  workflow = Workflow.require_workflow workflow_path if workflow_path.exists?
  knowledge_base = KnowledgeBase.new knowledge_base_path if knowledge_base_path.exists?
  chat = Chat.setup LLM.chat(chat_path.find) if chat_path.exists?

  LLM::Agent.new workflow: workflow, knowledge_base: knowledge_base, start_chat: chat
end

Instance Method Details

#ask(messages, options = {}) ⇒ Object

function: takes an array of messages and calls LLM.ask with them



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/scout/llm/agent.rb', line 52

def ask(messages, options = {})
  messages = [messages] unless messages.is_a? Array
  model ||= @model if model

  tools = options[:tools] || {}
  tools = tools.merge @other_options[:tools] if @other_options[:tools]
  options[:tools] = tools
  begin
    if workflow || knowledge_base
      tools.merge!(LLM.workflow_tools(workflow)) if workflow
      tools.merge!(LLM.knowledge_base_tool_definition(knowledge_base)) if knowledge_base and knowledge_base.all_databases.any?
      options[:tools] = tools
      LLM.ask messages, @other_options.merge(log_errors: true).merge(options)
    else
      LLM.ask messages, @other_options.merge(log_errors: true).merge(options)
    end
  rescue
    exception = $!
    if Proc === self.process_exception
      try_again = self.process_exception.call exception
      if try_again
        retry
      else
        raise exception
      end
    else
      raise exception
    end
  end
end

#chat(options = {}) ⇒ Object



31
32
33
34
35
36
37
38
39
40
# File 'lib/scout/llm/agent/chat.rb', line 31

def chat(options = {})
  response = ask(current_chat, options.merge(return_messages: true))
  if Array === response
    current_chat.concat(response)
    current_chat.answer
  else
    current_chat.push({role: :assistant, content: response})
    response
  end
end

#current_chatObject



18
19
20
# File 'lib/scout/llm/agent/chat.rb', line 18

def current_chat
  @current_chat ||= start
end

#delegate(agent, name, description, &block) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/scout/llm/agent/delegate.rb', line 4

def delegate(agent, name, description, &block)
  @other_options[:tools] ||= {}
  task_name = "hand_off_to_#{name}"

  block ||= Proc.new do |name, parameters|
    message = parameters[:message]
    new_conversation = parameters[:new_conversation]
    Log.medium "Delegated to #{agent}: " + Log.fingerprint(message)
    if new_conversation
      agent.start
    else
      agent.purge
    end
    agent.user message
    agent.chat
  end

  properties = {
    message: {
      "type": :string,
      "description": "Message to pass to the agent"
    },
    new_conversation: {
      "type": :boolean,
      "description": "Erase conversation history and start a new conversation with this message",
      "default": false
    }
  }

  required_inputs = [:message]

  function = {
    name: task_name,
    description: description,
    parameters: {
      type: "object",
      properties: properties,
      required: required_inputs
    }
  }

  definition = IndiferentHash.setup function.merge(type: 'function', function: function)


  @other_options[:tools][task_name] = [block, definition]
end

#format_message(message, prefix = "user") ⇒ Object



18
19
20
21
22
# File 'lib/scout/llm/agent.rb', line 18

def format_message(message, prefix = "user")
  message.split(/\n\n+/).reject{|line| line.empty? }.collect do |line|
    prefix + "\t" + line.gsub("\n", ' ')
  end * "\n"
end

#get_previous_response_idObject



65
66
67
68
# File 'lib/scout/llm/agent/chat.rb', line 65

def get_previous_response_id
  msg = current_chat.reverse.find{|msg| msg[:role].to_sym == :previous_response_id }
  msg.nil? ? nil : msg['content']
end

#iterate(prompt = nil, &block) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# File 'lib/scout/llm/agent/iterate.rb', line 4

def iterate(prompt = nil, &block)
  self.endpoint :responses
  self.user prompt if prompt

  obj = self.json_format({
    "$schema": "http://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
      "content": {
        "type": "array",
        "items": { "type": "string" }
      }
    },
    "required": ["content"],
    "additionalProperties": false
  })

  self.option :format, :text

  list = Hash === obj ? obj['content'] : obj

  list.each &block
end

#iterate_dictionary(prompt = nil, &block) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/scout/llm/agent/iterate.rb', line 28

def iterate_dictionary(prompt = nil, &block)
  self.endpoint :responses
  self.user prompt if prompt

  dict = self.json_format({
    name: 'dictionary',
    type: 'object',
    properties: {},
    additionalProperties: {type: :string}
  })

  self.option :format, :text

  dict.each &block
end

#jsonObject



43
44
45
46
47
48
49
50
51
52
# File 'lib/scout/llm/agent/chat.rb', line 43

def json(...)
  current_chat.format :json
  output = ask(current_chat, ...)
  obj = JSON.parse output
  if (Hash === obj) and obj.keys == ['content']
    obj['content']
  else
    obj
  end
end

#json_format(format) ⇒ Object



54
55
56
57
58
59
60
61
62
63
# File 'lib/scout/llm/agent/chat.rb', line 54

def json_format(format, ...)
  current_chat.format format
  output = ask(current_chat, ...)
  obj = JSON.parse output
  if (Hash === obj) and obj.keys == ['content']
    obj['content']
  else
    obj
  end
end

#prompt(messages) ⇒ Object



43
44
45
46
47
48
49
# File 'lib/scout/llm/agent.rb', line 43

def prompt(messages)
  if system_prompt
    [format_message(system_prompt, "system"), messages.collect{|m| format_message(m)}.flatten] * "\n"
  else
    messages.collect{|m| format_message(m)}.flatten
  end
end

#respondObject



26
27
28
# File 'lib/scout/llm/agent/chat.rb', line 26

def respond(...)
  self.ask(current_chat, ...)
end

#start(chat = nil) ⇒ Object



7
8
9
10
11
12
13
14
15
16
# File 'lib/scout/llm/agent/chat.rb', line 7

def start(chat=nil)
  if chat
    (@current_chat || start_chat).annotate chat unless Chat === chat
    @current_chat = chat
  else
    start_chat = self.start_chat
    Chat.setup(start_chat) unless Chat === start_chat
    @current_chat = start_chat.branch
  end
end

#system_promptObject



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/scout/llm/agent.rb', line 24

def system_prompt
  system = @system
  system = [] if system.nil?
  system = [system] unless system.nil? || system.is_a?(Array)
  system = [] if system.nil?

  if @knowledge_base and @knowledge_base.all_databases.any?
    system << "You have access to the following databases associating entities:\n    EOF\n\n    knowledge_base.all_databases.each do |database|\n      system << knowledge_base.markdown(database)\n    end\n  end\n\n  system * \"\\n\"\nend\n"