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.



14
15
16
17
18
19
20
# File 'lib/scout/llm/agent.rb', line 14

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

#chatsObject

Returns the value of attribute chats.



4
5
6
# File 'lib/scout/llm/agent/delegate.rb', line 4

def chats
  @chats
end

#knowledge_baseObject

Returns the value of attribute knowledge_base.



13
14
15
# File 'lib/scout/llm/agent.rb', line 13

def knowledge_base
  @knowledge_base
end

#other_optionsObject

Returns the value of attribute other_options.



13
14
15
# File 'lib/scout/llm/agent.rb', line 13

def other_options
  @other_options
end

#pathObject

Returns the value of attribute path.



13
14
15
# File 'lib/scout/llm/agent.rb', line 13

def path
  @path
end

#process_exceptionObject

Returns the value of attribute process_exception.



13
14
15
# File 'lib/scout/llm/agent.rb', line 13

def process_exception
  @process_exception
end

#societyObject

Returns the value of attribute society.



4
5
6
# File 'lib/scout/llm/agent/delegate.rb', line 4

def society
  @society
end

#start_chatObject

Returns the value of attribute start_chat.



13
14
15
# File 'lib/scout/llm/agent.rb', line 13

def start_chat
  @start_chat
end

#workflow(&block) ⇒ Object

Returns the value of attribute workflow.



13
14
15
# File 'lib/scout/llm/agent.rb', line 13

def workflow
  @workflow
end

Class Method Details

.load_agent(agent_name = nil, options = {}) ⇒ Object

Raises:

  • (ScoutException)


142
143
144
145
146
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
183
184
185
186
187
188
189
190
191
192
# File 'lib/scout/llm/agent.rb', line 142

def self.load_agent(agent_name = nil, options = {})
  if agent_name && Path.is_filename?(agent_name) 
    if File.directory?(agent_name)
      dir = Path.setup(agent_name) unless Path === agent_name
      if dir.agent.find_with_extension("rb").exists?
        return load dir.agent.find_with_extension("rb")
      end
    else
      return load agent_name
    end
  end

  agent_name ||= 'default'

  workflow_path = Scout.workflows[agent_name]
  agent_path = Scout.var.Agent[agent_name]
  agent_path = Scout.chats[agent_name] unless agent_path.exists?
  agent_path = Scout.chats.Agent[agent_name] unless agent_path.exists?

  raise ScoutException, "No agent found with name #{agent_name}" unless workflow_path.exists? || agent_path.exists?

  workflow = if workflow_path.exists?
               agent_path = workflow_path
               Workflow.require_workflow agent_name
             elsif agent_path.workflow.find_with_extension("rb").exists?
               Workflow.require_workflow_file agent_path.workflow.find_with_extension("rb")
             elsif agent_path.python.exists? && agent_path.python.glob('*.py').any?
               require 'scout/workflow/python'
               PythonWorkflow.load_directory agent_path.python, 'ScoutAgent'
             end

  knowledge_base = if agent_path.knowledge_base.exists?
                     KnowledgeBase.load agent_path.knowledge_base.find
                   elsif workflow_path.knowledge_base.exists?
                     KnowledgeBase.load workflow_path.knowledge_base.find
                   end

  chat = if agent_path.start_chat.exists?
           Chat.setup LLM.chat(agent_path.start_chat.find)
         elsif workflow_path.start_chat.exists?
           Chat.setup LLM.chat(workflow_path.start_chat.find)
         elsif agent_path.start_chat.exists?
           Chat.setup LLM.chat(agent_path.start_chat.find)
         elsif workflow && workflow.documentation[:description]
           Chat.setup([ {role: 'introduce', content: workflow.name} ])
         end

  agent = LLM::Agent.new **options.merge(workflow: workflow, knowledge_base: knowledge_base, start_chat: chat)
  agent.path = agent_path.find if agent_path
  agent
end

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



130
131
132
133
134
135
136
137
138
139
140
# File 'lib/scout/llm/agent.rb', line 130

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 = nil, options = {}) ⇒ Object

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



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/scout/llm/agent.rb', line 64

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

  messages.delete_if{|info| info[:role] == 'agent' }
  if (list = messages.select{|info| info[:role] == 'socialize'}).any?
    socialize = list.last[:content]
    messages.delete_if{|info| info[:role] == 'socialize' }
    self.socialize(options.dup) if socialize && %w(true TRUE True T 1).include?(socialize.to_s)
  end

  tools = options[:tools] || {}
  if other_tools = @other_options[:tools]
    other_tools = JSON.parse other_tools if String === other_tools
    tools = tools.merge other_tools
  end

  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?
    end

    if workflow && workflow.tasks.include?(:ask)
      options.each do |key,value|
        messages.push(IndiferentHash.setup({role: :option, content: "#{key} #{value}"})) 
      end

      job = workflow.job(:ask, chat: Chat.print(messages))
      job.clean
      job.produce
      
      messages = LLM.chat job.path
      if options[:return_messages]
        messages
      else
        Chat.answer messages
      end
    else
      options[:tools] = tools
      LLM.ask messages, @other_options.merge(log_errors: true).merge(options).merge(agent: false)
    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

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



38
39
40
41
42
# File 'lib/scout/llm/agent.rb', line 38

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



71
72
73
74
# File 'lib/scout/llm/agent/chat.rb', line 71

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
53
# File 'lib/scout/llm/agent/chat.rb', line 43

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

#json_format(format, options = {}) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/scout/llm/agent/chat.rb', line 55

def json_format(format, options = {})
  current_chat.format format
  output = ask(current_chat, options.merge({return_messages: false}))
  obj = begin
          JSON.parse output
        rescue JSON::ParserError
          Log.warn "Not valid JSON:" + output
          raise $!
        end
  if (Hash === obj) and obj.keys == ['content']
    obj['content']
  else
    obj
  end
end

#load_agent(agent_name, options) ⇒ Object

Raises:

  • (ParameterException)


6
7
8
9
10
11
# File 'lib/scout/llm/agent/delegate.rb', line 6

def load_agent(agent_name, options)
  raise ParameterException, 
    "Agent name must be a single word optionally including a few puntuation characters" unless agent_name =~ /^[a-z_.-]*$/i
  @society ||= {}
  @society[agent_name] ||= LLM.load_agent agent_name, options.merge(self.other_options)
end

#load_chat(agent_name, options, chat) ⇒ Object



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

def load_chat(agent_name, options, chat)
  @chats ||= {}
  @chats[chat] ||= load_agent(agent_name, options).clone
end

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



124
125
126
127
128
# File 'lib/scout/llm/agent.rb', line 124

def prompt(messages, options = {})
  messages = LLM.chat messages if String === messages
  messages = Chat.follow start_chat, messages
  ask messages, options
end

#respondObject



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

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

#socialize(options = {}) ⇒ Object



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
50
51
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
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/scout/llm/agent/delegate.rb', line 18

def socialize(options = {})
  @other_options[:tools] ||= {}
  @society ||= {}

  task_name = :ask
  block ||= Proc.new do |name, parameters|
    agent_name, prompt, chat_id = IndiferentHash.process_options parameters.dup, 
      :agent, :prompt, :chat, 
      chat: 'current'

    begin
      options = options.dup

      res = case chat_id
            when 'current'
              agent = load_chat agent_name, options, 'current'
              chat = self.current_chat - self.start_chat
              agent.concat chat
              agent.user prompt
              agent.chat 
            when 'none', nil, 'false'
              agent = load_agent agent_name, options
              agent.prompt prompt
            else
              agent = load_chat agent_name, options, chat_id
              agent.user prompt
              agent.chat
            end
      res
    rescue ScoutException
      next $!
    end
  end

  properties = {
    agent: {
      "type": :string,
      "description": "Name of the agent"
    },
    prompt: {
      "type": :string,
      "description": "Prompt to pass to the agent"
    },
    chat: {
      "type": :string,
      "description": "(Optional) Chat identifier used to keep conversation history. The default is 'current' uses the conversation that the caller agent is involved with",
      "default": 'current'
    }
  }

  required_inputs = [:agent, :prompt]

  description =<<-EOF

The 'ask' function is used to send a prompt to an agent, returning the agents
response. You can keep one-shot questions or keep running conversations with
the same agent by giving the chat an identifier. The chat id 'current' has the
special meaning of passing the entire conversation to the agent, not just the
prompt.

  EOF

  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

#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