scout-ai 1.0.1 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +20 -2
  3. data/Rakefile +1 -0
  4. data/VERSION +1 -1
  5. data/bin/scout-ai +46 -0
  6. data/lib/scout/llm/agent/chat.rb +4 -7
  7. data/lib/scout/llm/agent/delegate.rb +12 -0
  8. data/lib/scout/llm/agent.rb +2 -2
  9. data/lib/scout/llm/ask.rb +18 -2
  10. data/lib/scout/llm/backends/huggingface.rb +0 -2
  11. data/lib/scout/llm/backends/ollama.rb +6 -6
  12. data/lib/scout/llm/backends/openai.rb +7 -4
  13. data/lib/scout/llm/backends/openwebui.rb +1 -4
  14. data/lib/scout/llm/backends/relay.rb +1 -3
  15. data/lib/scout/llm/backends/responses.rb +34 -18
  16. data/lib/scout/llm/chat/annotation.rb +195 -0
  17. data/lib/scout/llm/chat/parse.rb +139 -0
  18. data/lib/scout/llm/chat/process/clear.rb +29 -0
  19. data/lib/scout/llm/chat/process/files.rb +96 -0
  20. data/lib/scout/llm/chat/process/options.rb +52 -0
  21. data/lib/scout/llm/chat/process/tools.rb +173 -0
  22. data/lib/scout/llm/chat/process.rb +16 -0
  23. data/lib/scout/llm/chat.rb +26 -662
  24. data/lib/scout/llm/mcp.rb +1 -1
  25. data/lib/scout/llm/tools/call.rb +22 -1
  26. data/lib/scout/llm/tools/knowledge_base.rb +15 -14
  27. data/lib/scout/llm/tools/mcp.rb +4 -0
  28. data/lib/scout/llm/tools/workflow.rb +54 -15
  29. data/lib/scout/llm/tools.rb +42 -0
  30. data/lib/scout/llm/utils.rb +2 -17
  31. data/scout-ai.gemspec +13 -4
  32. data/scout_commands/agent/ask +36 -12
  33. data/scout_commands/llm/ask +17 -7
  34. data/scout_commands/llm/process +1 -1
  35. data/test/scout/llm/backends/test_anthropic.rb +2 -2
  36. data/test/scout/llm/backends/test_ollama.rb +1 -1
  37. data/test/scout/llm/backends/test_responses.rb +9 -9
  38. data/test/scout/llm/chat/test_parse.rb +126 -0
  39. data/test/scout/llm/chat/test_process.rb +123 -0
  40. data/test/scout/llm/test_agent.rb +2 -25
  41. data/test/scout/llm/test_chat.rb +2 -178
  42. metadata +25 -3
  43. data/lib/scout/llm/parse.rb +0 -91
data/lib/scout/llm/mcp.rb CHANGED
@@ -6,7 +6,7 @@ module Workflow
6
6
  tasks = self.tasks.keys if tasks.empty?
7
7
 
8
8
  tools = tasks.collect do |task,inputs=nil|
9
- tool_definition = LLM.task_tool_definition(self, task, inputs)[:function]
9
+ tool_definition = LLM.task_tool_definition(self, task, inputs)
10
10
  description = tool_definition[:description]
11
11
  input_schema = tool_definition[:parameters].slice(:properties, :required)
12
12
  annotations = tool_definition.slice(:title)
@@ -1,4 +1,7 @@
1
1
  module LLM
2
+ @max_content_length = Scout::Config.get(:max_content_length, :llm_tools, :tools, :llm, :ask, default: 5_000)
3
+ self.singleton_class.attr_accessor :max_content_length
4
+
2
5
  def self.call_id_name_and_arguments(tool_call)
3
6
  tool_call_id = tool_call.dig("call_id") || tool_call.dig("id")
4
7
  if tool_call['function']
@@ -15,12 +18,21 @@ module LLM
15
18
  end
16
19
 
17
20
  def self.process_calls(tools, calls, &block)
21
+ max_content_length = LLM.max_content_length
18
22
  IndiferentHash.setup tools
19
23
  calls.collect do |tool_call|
20
24
  tool_call_id, function_name, function_arguments = call_id_name_and_arguments(tool_call)
21
25
 
26
+ function_arguments = IndiferentHash.setup function_arguments
27
+
22
28
  obj, definition = tools[function_name]
23
29
 
30
+ definition = obj if Hash === obj
31
+
32
+ defaults = definition[:parameters][:defaults] if definition && definition[:parameters]
33
+ function_arguments = function_arguments.merge(defaults) if defaults
34
+
35
+ Log.high "Calling #{function_name} (#{Log.fingerprint function_arguments}): "
24
36
  function_response = case obj
25
37
  when Proc
26
38
  obj.call function_name, function_arguments
@@ -32,7 +44,7 @@ module LLM
32
44
  if block_given?
33
45
  block.call function_name, function_arguments
34
46
  else
35
- raise "Unkown executor #{Log.fingerprint obj} for function #{function_name}"
47
+ ParameterException.new "Tool or function not found '#{function_name}'. Called with parameters #{Log.fingerprint function_arguments}" if obj.nil? && definition.nil?
36
48
  end
37
49
  end
38
50
 
@@ -46,8 +58,17 @@ module LLM
46
58
  else
47
59
  function_response.to_json
48
60
  end
61
+
49
62
  content = content.to_s if Numeric === content
50
63
 
64
+ Log.high "Called #{function_name}: " + Log.fingerprint(content)
65
+
66
+ if content.length > max_content_length
67
+ exception_msg = "Function #{function_name} called with parameters #{Log.fingerprint function_arguments} returned #{content.length} characters, which is more than the maximum set of #{max_content_length}."
68
+ Log.high exception_msg
69
+ content = {exception: exception_msg, stack: caller}.to_json
70
+ end
71
+
51
72
  response_message = {
52
73
  id: tool_call_id,
53
74
  role: "tool",
@@ -68,9 +68,9 @@ Returns a list in the format source~target.
68
68
  description: "Associations in the form of source~target or target~source"
69
69
  },
70
70
  fields: {
71
- type: "string",
72
- enum: select_options,
73
- description: "Limit the response to these detail fields fields"
71
+ type: "array",
72
+ items: { type: :string },
73
+ description: "Limit the response to these fields"
74
74
  },
75
75
  }
76
76
  else
@@ -93,22 +93,22 @@ Multiple values may be present and use the charater ';' to separate them.
93
93
  else
94
94
  properties.delete(:fields)
95
95
  description = <<-EOF
96
- Return the #{field} of association.
96
+ Return the #{fields.first} of association.
97
97
  Multiple values may be present and use the charater ';' to separate them.
98
98
  EOF
99
99
  end
100
100
 
101
101
  function = {
102
- name: database + '_association_details',
103
- description: description,
104
- parameters: {
105
- type: "object",
106
- properties: properties,
107
- required: ['associations']
108
- }
102
+ name: database.to_s + '_association_details',
103
+ description: description,
104
+ parameters: {
105
+ type: "object",
106
+ properties: properties,
107
+ required: ['associations']
108
+ }
109
109
  }
110
110
 
111
- IndiferentHash.setup function.merge(type: 'function', function: function)
111
+ IndiferentHash.setup function
112
112
  end
113
113
 
114
114
 
@@ -119,11 +119,12 @@ Multiple values may be present and use the charater ';' to separate them.
119
119
  database_description = knowledge_base.description(database)
120
120
  undirected = knowledge_base.undirected(database)
121
121
  definition = self.database_tool_definition(database, undirected, database_description)
122
- tool_definitions.merge(database => [knowledge_base, definition])
122
+ tool_definitions.merge!(database => [knowledge_base, definition])
123
123
  if (fields = knowledge_base.get_database(database).fields).any?
124
124
  details_definition = self.database_details_tool_definition(database, undirected, fields)
125
- tool_definitions.merge(database + '_association_details' => [knowledge_base, details_definition])
125
+ tool_definitions.merge!(database.to_s + '_association_details' => [knowledge_base, details_definition])
126
126
  end
127
+ tool_definitions
127
128
  }
128
129
  end
129
130
 
@@ -3,6 +3,10 @@ require 'mcp_client'
3
3
 
4
4
  module LLM
5
5
  def self.mcp_tools(url, options = {})
6
+ timeout = Scout::Config.get :timeout, :mcp, :tools
7
+
8
+ options = IndiferentHash.add_defaults options, read_timeout: timeout.to_i if timeout && timeout != ""
9
+
6
10
  if url == 'stdio'
7
11
  client = MCPClient.create_client(mcp_server_configs: [options.merge(type: 'stdio')])
8
12
  else
@@ -2,11 +2,25 @@ require 'scout/workflow'
2
2
  module LLM
3
3
  def self.task_tool_definition(workflow, task_name, inputs = nil)
4
4
  task_info = workflow.task_info(task_name)
5
+ return nil if task_info.nil?
5
6
 
6
- inputs = inputs.collect{|i| i.to_sym } if inputs
7
+ if inputs
8
+ names = []
9
+ defaults = {}
10
+
11
+ inputs.each do |i|
12
+ if String === i && i.include?('=')
13
+ name,_ , value = i.partition("=")
14
+ defaults[name] = value
15
+ else
16
+ names << i.to_sym
17
+ end
18
+ end
19
+
20
+ end
7
21
 
8
22
  properties = task_info[:inputs].inject({}) do |acc,input|
9
- next acc if inputs and not inputs.include?(input)
23
+ next acc if names and not names.include?(input)
10
24
  type = task_info[:input_types][input]
11
25
  description = task_info[:input_descriptions][input]
12
26
 
@@ -14,12 +28,17 @@ module LLM
14
28
  type = :string if type == :select
15
29
  type = :string if type == :path
16
30
  type = :number if type == :float
31
+ type = :array if type.to_s.end_with?('_array')
17
32
 
18
33
  acc[input] = {
19
34
  "type": type,
20
35
  "description": description
21
36
  }
22
37
 
38
+ if type == :array
39
+ acc[input]['items'] = {type: :string}
40
+ end
41
+
23
42
  if input_options = task_info[:input_options][input]
24
43
  if select_options = input_options[:select_options]
25
44
  select_options = select_options.values if Hash === select_options
@@ -31,7 +50,7 @@ module LLM
31
50
  end
32
51
 
33
52
  required_inputs = task_info[:inputs].select do |input|
34
- next if inputs and not inputs.include?(input.to_sym)
53
+ next if names and not names.include?(input.to_sym)
35
54
  task_info[:input_options].include?(input) && task_info[:input_options][input][:required]
36
55
  end
37
56
 
@@ -41,29 +60,49 @@ module LLM
41
60
  parameters: {
42
61
  type: "object",
43
62
  properties: properties,
44
- required: required_inputs
63
+ required: required_inputs,
45
64
  }
46
65
  }
47
66
 
48
- IndiferentHash.setup function.merge(type: 'function', function: function)
67
+ function[:parameters][:defaults] = defaults if defaults
68
+
69
+ #IndiferentHash.setup function.merge(type: 'function', function: function)
70
+ IndiferentHash.setup function
49
71
  end
50
72
 
51
73
  def self.workflow_tools(workflow, tasks = nil)
52
- tasks = workflow.all_exports if tasks.nil?
53
- tasks = workflow.all_tasks if tasks.empty?
74
+ if Array === workflow
75
+ workflow.inject({}){|tool_definitions,wf| tool_definitions.merge(workflow_tools(wf, tasks)) }
54
76
 
55
- tasks.inject({}){|tool_definitions,task_name|
56
- definition = self.task_tool_definition(workflow, task_name)
57
- tool_definitions.merge(task_name => [workflow, definition])
58
- }
77
+ else
78
+ tasks = workflow.all_exports if tasks.nil?
79
+ tasks = workflow.all_tasks if tasks.empty?
80
+
81
+ tasks.inject({}){|tool_definitions,task_name|
82
+ definition = self.task_tool_definition(workflow, task_name)
83
+ next if definition.nil?
84
+ tool_definitions.merge(task_name => [workflow, definition])
85
+ }
86
+ end
59
87
  end
60
88
 
61
89
  def self.call_workflow(workflow, task_name, parameters={})
62
90
  jobname = parameters.delete :jobname
63
- if workflow.exec_exports.include? task_name.to_sym
64
- workflow.job(task_name, jobname, parameters).exec
65
- else
66
- workflow.job(task_name, jobname, parameters).run
91
+ begin
92
+ exec_type = parameters[:exec_type]
93
+ job = workflow.job(task_name, jobname, parameters)
94
+ if workflow.exec_exports.include?(task_name.to_sym) || parameters[:exec_type].to_s == 'exec'
95
+ job.exec
96
+ else
97
+ raise ScoutException, 'Potential recursive call' if parameters[:allow_recursive] != 'true' &&
98
+ (job.running? and job.info[:pid] == Process.pid)
99
+
100
+ Workflow.produce(job)
101
+ job.join
102
+ job.load
103
+ end
104
+ rescue ScoutException
105
+ return $!
67
106
  end
68
107
  end
69
108
  end
@@ -137,4 +137,46 @@ module LLM
137
137
  end
138
138
  end.flatten
139
139
  end
140
+
141
+ def self.tool_definitions_to_reponses(tools)
142
+ tools.values.collect do |obj,definition|
143
+ definition = obj if Hash === obj
144
+ definition
145
+
146
+ definition = case definition[:function]
147
+ when Hash
148
+ definition.merge(definition.delete :function)
149
+ else
150
+ definition
151
+ end
152
+
153
+ definition = IndiferentHash.add_defaults definition, type: :function
154
+
155
+ definition[:parameters].delete :defaults if definition[:parameters]
156
+
157
+ definition
158
+ end
159
+ end
160
+
161
+ class << self
162
+ alias tool_definitions_to_openai tool_definitions_to_reponses
163
+ end
164
+
165
+ def self.tool_definitions_to_ollama(tools)
166
+ tools.values.collect do |obj,definition|
167
+ definition = obj if Hash === obj
168
+ definition = IndiferentHash.setup definition
169
+
170
+ definition = case definition[:function]
171
+ when Hash
172
+ definition
173
+ else
174
+ {type: :function, function: definition}
175
+ end
176
+
177
+ definition = IndiferentHash.add_defaults definition, type: :function
178
+
179
+ definition
180
+ end
181
+ end
140
182
  end
@@ -1,21 +1,4 @@
1
1
  module LLM
2
-
3
- def self.tag(tag, content, name = nil)
4
- if name
5
- <<-EOF.strip
6
- <#{tag} name="#{name}">
7
- #{content}
8
- </#{tag}>
9
- EOF
10
- else
11
- <<-EOF.strip
12
- <#{tag}>
13
- #{content}
14
- </#{tag}>
15
- EOF
16
- end
17
- end
18
-
19
2
  def self.get_url_server_tokens(url, prefix=nil)
20
3
  return get_url_server_tokens(url).collect{|e| prefix.to_s + "." + e } if prefix
21
4
 
@@ -39,4 +22,6 @@ module LLM
39
22
  end
40
23
  Scout::Config.get(key, *all_tokens, hash)
41
24
  end
25
+
26
+
42
27
  end
data/scout-ai.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: scout-ai 1.0.1 ruby lib
5
+ # stub: scout-ai 1.1.1 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "scout-ai".freeze
9
- s.version = "1.0.1".freeze
9
+ s.version = "1.1.1".freeze
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
@@ -49,9 +49,15 @@ Gem::Specification.new do |s|
49
49
  "lib/scout/llm/backends/relay.rb",
50
50
  "lib/scout/llm/backends/responses.rb",
51
51
  "lib/scout/llm/chat.rb",
52
+ "lib/scout/llm/chat/annotation.rb",
53
+ "lib/scout/llm/chat/parse.rb",
54
+ "lib/scout/llm/chat/process.rb",
55
+ "lib/scout/llm/chat/process/clear.rb",
56
+ "lib/scout/llm/chat/process/files.rb",
57
+ "lib/scout/llm/chat/process/options.rb",
58
+ "lib/scout/llm/chat/process/tools.rb",
52
59
  "lib/scout/llm/embed.rb",
53
60
  "lib/scout/llm/mcp.rb",
54
- "lib/scout/llm/parse.rb",
55
61
  "lib/scout/llm/rag.rb",
56
62
  "lib/scout/llm/tools.rb",
57
63
  "lib/scout/llm/tools/call.rb",
@@ -105,6 +111,8 @@ Gem::Specification.new do |s|
105
111
  "test/scout/llm/backends/test_openwebui.rb",
106
112
  "test/scout/llm/backends/test_relay.rb",
107
113
  "test/scout/llm/backends/test_responses.rb",
114
+ "test/scout/llm/chat/test_parse.rb",
115
+ "test/scout/llm/chat/test_process.rb",
108
116
  "test/scout/llm/test_agent.rb",
109
117
  "test/scout/llm/test_ask.rb",
110
118
  "test/scout/llm/test_chat.rb",
@@ -131,13 +139,14 @@ Gem::Specification.new do |s|
131
139
  ]
132
140
  s.homepage = "http://github.com/mikisvaz/scout-ai".freeze
133
141
  s.licenses = ["MIT".freeze]
134
- s.rubygems_version = "3.7.0.dev".freeze
142
+ s.rubygems_version = "3.7.2".freeze
135
143
  s.summary = "AI gear for scouts".freeze
136
144
 
137
145
  s.specification_version = 4
138
146
 
139
147
  s.add_runtime_dependency(%q<scout-rig>.freeze, [">= 0".freeze])
140
148
  s.add_runtime_dependency(%q<ruby-openai>.freeze, [">= 0".freeze])
149
+ s.add_runtime_dependency(%q<ollama-ai>.freeze, [">= 0".freeze])
141
150
  s.add_runtime_dependency(%q<ruby-mcp-client>.freeze, [">= 0".freeze])
142
151
  end
143
152
 
@@ -41,13 +41,12 @@ agent_name, *question_parts = ARGV
41
41
 
42
42
  question = question_parts * " "
43
43
 
44
- file, chat, inline, template, dry_run, imports = IndiferentHash.process_options options, :file, :chat, :inline, :template, :dry_run, :imports
44
+ file, chat, inline, template, dry_run, imports, endpoint, model = IndiferentHash.process_options options, :file, :chat, :inline, :template, :dry_run, :imports, :endpoint, :model
45
45
 
46
46
  file = Path.setup(file) if file
47
47
 
48
48
  imports = imports.split(/,\s*/) if imports
49
49
 
50
-
51
50
  agent_name ||= 'default'
52
51
 
53
52
  agent_file = Scout.workflows[agent_name]
@@ -71,6 +70,9 @@ else
71
70
  #raise ParameterException agent_file
72
71
  end
73
72
 
73
+ agent.other_options[:endpoint] = endpoint if endpoint
74
+ agent.other_options[:model] = model if model
75
+
74
76
  if template
75
77
  if Open.exists?(template)
76
78
  template_question = Open.read(template)
@@ -98,24 +100,43 @@ elsif file
98
100
  end
99
101
 
100
102
  if chat
101
- conversation = Open.exist?(chat)? LLM.chat(chat) : []
103
+ #conversation = Open.exist?(chat)? LLM.chat(chat) : []
104
+ #convo_options = LLM.options conversation
105
+ #conversation = question.empty? ? conversation : conversation + LLM.chat(question)
106
+
107
+ #if dry_run
108
+ # ppp LLM.print conversation
109
+ # exit 0
110
+ #end
111
+
112
+ #new = agent.ask(conversation, convo_options.merge(options.merge(return_messages: true)))
113
+ #conversation = Open.read(chat) + LLM.print(new)
114
+ #Open.write(chat, conversation)
115
+
116
+ conversation = Open.exist?(chat)? LLM.chat(chat) : []
102
117
  convo_options = LLM.options conversation
103
- conversation = question.empty? ? conversation : conversation + LLM.chat(question)
118
+ agent.start(Chat.setup(conversation)) if conversation.any?
119
+ agent.current_chat.concat LLM.chat(question) if question && ! question.empty?
120
+ imports.each{|import| agent.import import } if imports
104
121
 
105
122
  if dry_run
106
123
  ppp LLM.print conversation
107
124
  exit 0
108
125
  end
109
- new = agent.ask(conversation, convo_options.merge(options.merge(return_messages: true)))
110
- conversation = Open.read(chat) + LLM.print(new)
111
- Open.write(chat, conversation)
126
+
127
+ new = agent.ask(agent.current_chat, convo_options.merge(options.merge(return_messages: true)))
128
+ conversation += LLM.chat(question) if question
129
+ conversation += new
130
+
131
+ Open.open(chat, mode: 'a'){|f| f.puts LLM.print(new) }
132
+ puts LLM.purge(new).last[:content]
112
133
  elsif inline
113
134
 
114
135
  file = Open.read inline
115
136
 
116
137
  new_file = ""
117
138
  while true
118
- pre, question, post =
139
+ pre, question, post =
119
140
  file.partition(/^\s*#\s*ask:(?:.*?)(?=^\s*[^\s#])/smu)
120
141
 
121
142
  break if post.empty?
@@ -124,7 +145,7 @@ elsif inline
124
145
  new_file << question
125
146
  clean_question = question.gsub('#', '').gsub(/\s+/,' ').sub(/.*ask:\s*/,'').strip
126
147
  chat = [
127
- {role: :system, content: "Write a succint reply with no commentary and no formatting."},
148
+ {role: :system, content: "Write a succint reply with no commentary and no formatting."},
128
149
  {role: :user, content: "Find the following question as a comment in the file give a response to be placed inline: #{question}"},
129
150
  LLM.tag('file', file, inline)
130
151
  ]
@@ -139,7 +160,10 @@ elsif inline
139
160
  new_file << file
140
161
  Open.write(inline, new_file)
141
162
  else
142
- conversation = Chat.setup(LLM.chat question)
143
- imports.each{|import| conversation.import import } if imports
144
- puts agent.ask(conversation, nil, options)
163
+ #conversation = Chat.setup(LLM.chat question)
164
+ #imports.each{|import| conversation.import import } if imports
165
+ #puts agent.ask(conversation, nil, options)
166
+ agent.current_chat.concat LLM.chat(question)
167
+ imports.each{|import| agent.import import } if imports
168
+ puts agent.chat
145
169
  end
@@ -21,7 +21,8 @@ characters '???', if they are present.
21
21
  -h--help Print this help
22
22
  -t--template* Use a template
23
23
  -c--chat* Follow a conversation
24
- -i--inline* Ask inline questions about a file
24
+ -i--imports* Chat files to import, separated by comma
25
+ -in--inline* Ask inline questions about a file
25
26
  -f--file* Incorporate file at the start
26
27
  -m--model* Model to use
27
28
  -e--endpoint* Endpoint to use
@@ -39,7 +40,9 @@ end
39
40
 
40
41
  Log.severity = options.delete(:log).to_i if options.include? :log
41
42
 
42
- file, chat, inline, template, dry_run = IndiferentHash.process_options options, :file, :chat, :inline, :template, :dry_run
43
+ file, chat, inline, imports, template, dry_run = IndiferentHash.process_options options, :file, :chat, :inline, :imports, :template, :dry_run
44
+
45
+ imports = imports.split(/,\s*/) if imports
43
46
 
44
47
  question = ARGV * " "
45
48
 
@@ -70,7 +73,8 @@ elsif file
70
73
  end
71
74
 
72
75
  if chat
73
- conversation = Open.exist?(chat)? LLM.chat(chat) : []
76
+ conversation = Open.exist?(chat)? LLM.chat(chat) : Chat.setup([])
77
+ imports.each{|import| conversation.import import } if imports
74
78
  convo_options = LLM.options conversation
75
79
  conversation = question.empty? ? conversation : conversation + LLM.chat(question)
76
80
 
@@ -78,16 +82,17 @@ if chat
78
82
  ppp LLM.print conversation
79
83
  exit 0
80
84
  end
85
+
81
86
  new = LLM.ask(conversation, convo_options.merge(options.merge(return_messages: true)))
82
- conversation = Open.read(chat) + LLM.print(new)
83
- Open.write(chat, conversation)
87
+ Open.open(chat, mode: 'a'){|f| f.puts LLM.print(new) }
88
+ puts LLM.purge(new).last[:content]
84
89
  elsif inline
85
90
 
86
91
  file = Open.read inline
87
92
 
88
93
  new_file = ""
89
94
  while true
90
- pre, question, post =
95
+ pre, question, post =
91
96
  file.partition(/^\s*#\s*ask:(?:.*?)(?=^\s*[^\s#]|\z)/smu)
92
97
 
93
98
  break if question.empty?
@@ -96,10 +101,15 @@ elsif inline
96
101
  new_file << question
97
102
  clean_question = question.gsub('#', '').gsub(/\s+/,' ').sub(/.*ask:\s*/,'').strip
98
103
  chat = [
99
- {role: :system, content: "Write a succint reply with no commentary and no formatting."},
104
+ {role: :system, content: "Write a succint reply with no commentary and no formatting."},
100
105
  {role: :user, content: "Find the following question as a comment in the file give a response to be placed inline: #{question}"},
101
106
  LLM.tag('file', file, inline)
102
107
  ]
108
+
109
+ chat = Chat.chat(chat)
110
+
111
+ imports.each{|import| chat.import import } if imports
112
+
103
113
  response = LLM.ask(LLM.chat(chat))
104
114
  new_file << <<-EOF
105
115
  # Response start
@@ -35,10 +35,10 @@ while true
35
35
  files = directory.glob('*.json')
36
36
 
37
37
  files.each do |file|
38
+ id = File.basename(file, '.json')
38
39
  target = directory.reply[id + '.json']
39
40
 
40
41
  if ! File.exist?(target)
41
- id = File.basename(file, '.json')
42
42
  options = IndiferentHash.setup(JSON.parse(Open.read(file)))
43
43
  question = options.delete(:question)
44
44
  reply = LLM.ask(question, options)
@@ -2,7 +2,7 @@ require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
2
2
  require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
3
3
 
4
4
  class TestLLMAnthropic < Test::Unit::TestCase
5
- def _test_say_hi
5
+ def test_say_hi
6
6
  prompt =<<-EOF
7
7
  user: say hi
8
8
  EOF
@@ -121,7 +121,7 @@ What is the weather in London. Should I take my umbrella?
121
121
  ppp respose
122
122
  end
123
123
 
124
- def test_json_output
124
+ def _test_json_output
125
125
  prompt =<<-EOF
126
126
  user:
127
127
 
@@ -82,7 +82,7 @@ What is the weather in London. Should I take an umbrella?
82
82
  ]
83
83
 
84
84
  sss 0
85
- respose = LLM::OLlama.ask prompt, model: 'mistral', tool_choice: 'required', tools: tools do |name,arguments|
85
+ respose = LLM::OLlama.ask prompt, model: 'gpt-oss', url: 'http://localhost:3330', tool_choice: 'required', tools: tools do |name,arguments|
86
86
  "It's raining cats and dogs"
87
87
  end
88
88
 
@@ -20,7 +20,7 @@ Some text
20
20
  assert(Float === emb.first)
21
21
  end
22
22
 
23
- def test_tool_call_output_weather
23
+ def _test_tool_call_output_weather
24
24
  Log.severity = 0
25
25
  prompt =<<-EOF
26
26
  function_call:
@@ -38,7 +38,7 @@ should i take an umbrella?
38
38
  ppp LLM::Responses.ask prompt, model: 'gpt-4.1-nano'
39
39
  end
40
40
 
41
- def test_tool
41
+ def _test_tool
42
42
  prompt =<<-EOF
43
43
  user:
44
44
  What is the weather in London. Should I take my umbrella?
@@ -75,7 +75,7 @@ What is the weather in London. Should I take my umbrella?
75
75
  ppp respose
76
76
  end
77
77
 
78
- def test_news
78
+ def _test_news
79
79
  prompt =<<-EOF
80
80
  websearch: true
81
81
 
@@ -86,7 +86,7 @@ What was the top new in the US today?
86
86
  ppp LLM::Responses.ask prompt
87
87
  end
88
88
 
89
- def test_image
89
+ def _test_image
90
90
  prompt =<<-EOF
91
91
  image: #{datafile_test 'cat.jpg'}
92
92
 
@@ -98,7 +98,7 @@ What animal is represented in the image?
98
98
  ppp LLM::Responses.ask prompt
99
99
  end
100
100
 
101
- def test_json_output
101
+ def _test_json_output
102
102
  prompt =<<-EOF
103
103
  system:
104
104
 
@@ -112,7 +112,7 @@ What other movies have the protagonists of the original gost busters played on,
112
112
  ppp LLM::Responses.ask prompt, format: :json
113
113
  end
114
114
 
115
- def test_json_format
115
+ def _test_json_format
116
116
  prompt =<<-EOF
117
117
  user:
118
118
 
@@ -130,7 +130,7 @@ Name each actor and the top movie they took part of
130
130
  ppp LLM::Responses.ask prompt, format: format
131
131
  end
132
132
 
133
- def test_json_format_list
133
+ def _test_json_format_list
134
134
  prompt =<<-EOF
135
135
  user:
136
136
 
@@ -148,7 +148,7 @@ Name each actor as keys and the top 3 movies they took part of as values
148
148
  ppp LLM::Responses.ask prompt, format: format
149
149
  end
150
150
 
151
- def test_json_format_actor_list
151
+ def _test_json_format_actor_list
152
152
  prompt =<<-EOF
153
153
  user:
154
154
 
@@ -199,7 +199,7 @@ Name each actor as keys and the top 3 movies they took part of as values
199
199
  ppp LLM::Responses.ask prompt, format: schema
200
200
  end
201
201
 
202
- def test_tool_gpt5
202
+ def _test_tool_gpt5
203
203
  prompt =<<-EOF
204
204
  user:
205
205
  What is the weather in London. Should I take my umbrella?