aia 0.9.7 → 0.9.9

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/.config/tocer/configuration.yml +2 -1
  3. data/.version +1 -1
  4. data/CHANGELOG.md +15 -1
  5. data/README.md +43 -0
  6. data/Rakefile +16 -8
  7. data/examples/directives/ask.rb +21 -0
  8. data/examples/tools/edit_file.rb +2 -0
  9. data/examples/tools/incomplete/calculator_tool.rb +70 -0
  10. data/examples/tools/incomplete/composite_analysis_tool.rb +89 -0
  11. data/examples/tools/incomplete/data_science_kit.rb +128 -0
  12. data/examples/tools/incomplete/database_query_tool.rb +100 -0
  13. data/examples/tools/incomplete/devops_toolkit.rb +112 -0
  14. data/examples/tools/incomplete/error_handling_tool.rb +109 -0
  15. data/examples/tools/incomplete/pdf_page_reader.rb +32 -0
  16. data/examples/tools/incomplete/secure_tool_template.rb +117 -0
  17. data/examples/tools/incomplete/weather_tool.rb +110 -0
  18. data/examples/tools/incomplete/workflow_manager_tool.rb +145 -0
  19. data/examples/tools/list_files.rb +2 -0
  20. data/examples/tools/mcp/README.md +1 -0
  21. data/examples/tools/mcp/github_mcp_server.rb +41 -0
  22. data/examples/tools/mcp/imcp.rb +15 -0
  23. data/examples/tools/read_file.rb +2 -0
  24. data/examples/tools/run_shell_command.rb +2 -0
  25. data/lib/aia/chat_processor_service.rb +3 -26
  26. data/lib/aia/config.rb +542 -414
  27. data/lib/aia/context_manager.rb +3 -8
  28. data/lib/aia/directive_processor.rb +24 -11
  29. data/lib/aia/ruby_llm_adapter.rb +78 -10
  30. data/lib/aia/session.rb +313 -215
  31. data/lib/aia/ui_presenter.rb +7 -5
  32. data/lib/aia/utility.rb +26 -6
  33. data/lib/aia.rb +5 -1
  34. metadata +32 -12
  35. data/lib/aia/shell_command_executor.rb +0 -109
@@ -26,14 +26,9 @@ module AIA
26
26
  # @param system_prompt [String, nil] The system prompt to potentially prepend.
27
27
  # @return [Array<Hash>] The conversation context array.
28
28
  def get_context(system_prompt: nil)
29
- # Ensure system prompt is present if provided and not already the first message
30
- if system_prompt &&
31
- !system_prompt.strip.empty? &&
32
- (
33
- @context.empty? ||
34
- @context.first[:role] != 'system'
35
- )
36
- add_system_prompt(system_prompt)
29
+ # Add or replace system prompt if provided and not empty
30
+ if system_prompt && !system_prompt.strip.empty?
31
+ add_system_prompt(system_prompt)
37
32
  end
38
33
  @context
39
34
  end
@@ -162,19 +162,19 @@ module AIA
162
162
  spaces = " "*indent
163
163
  width = TTY::Screen.width - indent - 2
164
164
 
165
- if !AIA.config.tools.empty?
165
+ if AIA.config.tools.empty?
166
+ puts "No tools are available"
167
+ else
166
168
  puts
167
169
  puts "Available Tools"
168
170
  puts "==============="
169
171
 
170
- AIA.config.tools.split(',').map(&:strip).each do |tool|
171
- klass = tool.constantize
172
- puts "\n#{klass.name}"
173
- puts "-"*klass.name.size
174
- puts WordWrapper::MinimumRaggedness.new(width, klass.description).wrap.split("\n").map{|s| spaces+s+"\n"}.join
172
+ AIA.config.tools.each do |tool|
173
+ name = tool.respond_to?(:name) ? tool.name : tool.class.name
174
+ puts "\n#{name}"
175
+ puts "-"*name.size
176
+ puts WordWrapper::MinimumRaggedness.new(width, tool.description).wrap.split("\n").map{|s| spaces+s+"\n"}.join
175
177
  end
176
- else
177
- puts "No tools configured"
178
178
  end
179
179
  puts
180
180
 
@@ -185,8 +185,10 @@ module AIA
185
185
  def pipeline(args = [], context_manager=nil)
186
186
  if args.empty?
187
187
  ap AIA.config.pipeline
188
+ elsif 1 == args.size
189
+ AIA.config.pipeline += args.first.split(',').map(&:strip).reject{|id| id.empty?}
188
190
  else
189
- AIA.config.pipeline += args.map {|id| id.gsub(',', '').strip}
191
+ AIA.config.pipeline += args.map{|id| id.gsub(',', '').strip}.reject{|id| id.empty?}
190
192
  end
191
193
  ''
192
194
  end
@@ -258,7 +260,15 @@ module AIA
258
260
 
259
261
  desc "Shortcut for //config model _and_ //config model = value"
260
262
  def model(args, context_manager=nil)
261
- send(:config, args.prepend('model'), context_manager)
263
+ if args.empty?
264
+ puts
265
+ puts AIA.config.client.model.to_h.pretty_inspect
266
+ puts
267
+ else
268
+ send(:config, args.prepend('model'), context_manager)
269
+ end
270
+
271
+ return ''
262
272
  end
263
273
 
264
274
  desc "Shortcut for //config temperature _and_ //config temperature = value"
@@ -343,9 +353,12 @@ module AIA
343
353
  counter = 0
344
354
 
345
355
  RubyLLM.models.all.each do |llm|
356
+ cw = llm.context_window
357
+ caps = llm.capabilities.join(',')
346
358
  inputs = llm.modalities.input.join(',')
347
359
  outputs = llm.modalities.output.join(',')
348
- entry = "- #{llm.id} (#{llm.provider}) #{inputs} to #{outputs}"
360
+ mode = "#{inputs} to #{outputs}"
361
+ entry = "- #{llm.id} (#{llm.provider}) cw: #{cw} mode: #{mode} caps: #{caps}"
349
362
 
350
363
  if query.nil? || query.empty?
351
364
  counter += 1
@@ -66,31 +66,81 @@ module AIA
66
66
  end
67
67
  end
68
68
 
69
+
69
70
  def setup_chat_with_tools
70
71
  begin
71
- @chat = RubyLLM.chat(model: @model)
72
+ @chat = RubyLLM.chat(model: @model)
73
+ @model = @chat.model.name if @model.nil? # using default model
72
74
  rescue => e
73
75
  STDERR.puts "ERROR: #{e.message}"
74
76
  exit 1
75
77
  end
76
78
 
77
- return unless @chat.model.supports_functions?
79
+ unless @chat.model.supports_functions?
80
+ AIA.config.tools = []
81
+ AIA.config.tool_names = ""
82
+ return
83
+ end
84
+
85
+ load_tools
78
86
 
79
- if !AIA.config.tool_paths.empty? && !@chat.model.supports?(:function_calling)
80
- STDERR.puts "ERROR: The model #{@model} does not support tools"
81
- exit 1
87
+ @chat.with_tools(*tools) unless tools.empty?
88
+ end
89
+
90
+
91
+ def load_tools
92
+ @tools = []
93
+
94
+ support_local_tools
95
+ support_mcp
96
+ filter_tools_by_allowed_list
97
+ filter_tools_by_rejected_list
98
+ drop_duplicate_tools
99
+
100
+ if tools.empty?
101
+ AIA.config.tool_names = ""
102
+ else
103
+ AIA.config.tool_names = @tools.map(&:name).join(', ')
104
+ AIA.config.tools = @tools
82
105
  end
106
+ end
107
+
83
108
 
84
- @tools = ObjectSpace.each_object(Class).select do |klass|
109
+ def support_local_tools
110
+ @tools += ObjectSpace.each_object(Class).select do |klass|
85
111
  klass < RubyLLM::Tool
86
112
  end
113
+ end
114
+
87
115
 
88
- unless tools.empty?
89
- @chat.with_tools(*tools)
90
- AIA.config.tools = tools.map(&:name).join(', ')
116
+ def support_mcp
117
+ RubyLLM::MCP.establish_connection
118
+ @tools += RubyLLM::MCP.tools
119
+ rescue => e
120
+ STDERR.puts "Warning: Failed to connect MCP clients: #{e.message}"
121
+ end
122
+
123
+
124
+ def drop_duplicate_tools
125
+ seen_names = Set.new
126
+ original_size = @tools.size
127
+
128
+ @tools.select! do |tool|
129
+ tool_name = tool.name
130
+ if seen_names.include?(tool_name)
131
+ STDERR.puts "WARNING: Duplicate tool name detected: '#{tool_name}'. Only the first occurrence will be used."
132
+ false
133
+ else
134
+ seen_names.add(tool_name)
135
+ true
136
+ end
91
137
  end
138
+
139
+ removed_count = original_size - @tools.size
140
+ STDERR.puts "Removed #{removed_count} duplicate tools" if removed_count > 0
92
141
  end
93
142
 
143
+
94
144
  # TODO: Need to rethink this dispatcher pattern w/r/t RubyLLM's capabilities
95
145
  # This code was originally designed for AiClient
96
146
  #
@@ -117,7 +167,7 @@ module AIA
117
167
  end
118
168
 
119
169
  def transcribe(audio_file)
120
- @chat.ask("Transcribe this audio", with: audio_file)
170
+ @chat.ask("Transcribe this audio", with: audio_file).content
121
171
  end
122
172
 
123
173
  def speak(text)
@@ -195,6 +245,24 @@ module AIA
195
245
 
196
246
  private
197
247
 
248
+ def filter_tools_by_allowed_list
249
+ return if AIA.config.allowed_tools.nil?
250
+
251
+ @tools.select! do |tool|
252
+ tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
253
+ AIA.config.allowed_tools.any? { |allowed| tool_name.include?(allowed) }
254
+ end
255
+ end
256
+
257
+ def filter_tools_by_rejected_list
258
+ return if AIA.config.rejected_tools.nil?
259
+
260
+ @tools.reject! do |tool|
261
+ tool_name = tool.respond_to?(:name) ? tool.name : tool.class.name
262
+ AIA.config.rejected_tools.any? { |rejected| tool_name.include?(rejected) }
263
+ end
264
+ end
265
+
198
266
  def extract_model_parts
199
267
  parts = AIA.config.model.split('/')
200
268
  parts.map!(&:strip)