rcrewai 0.2.0 → 0.3.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.rubocop_todo.yml +99 -0
  4. data/CHANGELOG.md +24 -0
  5. data/README.md +33 -1
  6. data/Rakefile +53 -53
  7. data/bin/rcrewai +3 -3
  8. data/docs/mcp.md +109 -0
  9. data/docs/superpowers/plans/2026-05-11-llm-modernization.md +2753 -0
  10. data/docs/superpowers/specs/2026-05-11-llm-modernization-design.md +479 -0
  11. data/docs/upgrading-to-0.3.md +163 -0
  12. data/examples/async_execution_example.rb +82 -81
  13. data/examples/hierarchical_crew_example.rb +68 -72
  14. data/examples/human_in_the_loop_example.rb +73 -74
  15. data/examples/mcp_example.rb +48 -0
  16. data/examples/native_tools_example.rb +64 -0
  17. data/examples/streaming_example.rb +56 -0
  18. data/lib/rcrewai/agent.rb +148 -287
  19. data/lib/rcrewai/async_executor.rb +43 -43
  20. data/lib/rcrewai/cli.rb +11 -11
  21. data/lib/rcrewai/configuration.rb +14 -9
  22. data/lib/rcrewai/crew.rb +56 -39
  23. data/lib/rcrewai/events.rb +30 -0
  24. data/lib/rcrewai/human_input.rb +104 -114
  25. data/lib/rcrewai/legacy_react_runner.rb +172 -0
  26. data/lib/rcrewai/llm_client.rb +1 -1
  27. data/lib/rcrewai/llm_clients/anthropic.rb +174 -54
  28. data/lib/rcrewai/llm_clients/azure.rb +23 -128
  29. data/lib/rcrewai/llm_clients/base.rb +11 -7
  30. data/lib/rcrewai/llm_clients/google.rb +159 -95
  31. data/lib/rcrewai/llm_clients/ollama.rb +150 -106
  32. data/lib/rcrewai/llm_clients/openai.rb +140 -63
  33. data/lib/rcrewai/mcp/client.rb +101 -0
  34. data/lib/rcrewai/mcp/tool_adapter.rb +59 -0
  35. data/lib/rcrewai/mcp/transport/http.rb +53 -0
  36. data/lib/rcrewai/mcp/transport/stdio.rb +55 -0
  37. data/lib/rcrewai/mcp.rb +8 -0
  38. data/lib/rcrewai/memory.rb +45 -37
  39. data/lib/rcrewai/pricing.rb +34 -0
  40. data/lib/rcrewai/process.rb +86 -95
  41. data/lib/rcrewai/provider_schema.rb +38 -0
  42. data/lib/rcrewai/sse_parser.rb +55 -0
  43. data/lib/rcrewai/task.rb +56 -64
  44. data/lib/rcrewai/tool_runner.rb +132 -0
  45. data/lib/rcrewai/tool_schema.rb +97 -0
  46. data/lib/rcrewai/tools/base.rb +98 -37
  47. data/lib/rcrewai/tools/code_executor.rb +71 -74
  48. data/lib/rcrewai/tools/email_sender.rb +70 -78
  49. data/lib/rcrewai/tools/file_reader.rb +38 -30
  50. data/lib/rcrewai/tools/file_writer.rb +40 -38
  51. data/lib/rcrewai/tools/pdf_processor.rb +115 -130
  52. data/lib/rcrewai/tools/sql_database.rb +58 -55
  53. data/lib/rcrewai/tools/web_search.rb +26 -25
  54. data/lib/rcrewai/version.rb +2 -2
  55. data/lib/rcrewai.rb +18 -10
  56. data/rcrewai.gemspec +55 -36
  57. metadata +86 -50
@@ -1,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'faraday'
4
+ require 'json'
3
5
  require_relative 'base'
6
+ require_relative '../events'
7
+ require_relative '../sse_parser'
8
+ require_relative '../provider_schema'
9
+ require_relative '../pricing'
4
10
 
5
11
  module RCrewAI
6
12
  module LLMClients
@@ -12,113 +18,184 @@ module RCrewAI
12
18
  @base_url = BASE_URL
13
19
  end
14
20
 
15
- def chat(messages:, **options)
21
+ def chat(messages:, tools: nil, tool_choice: :auto, stream: nil, **options)
16
22
  payload = {
17
23
  model: config.model,
18
- messages: format_messages(messages),
24
+ messages: messages,
19
25
  temperature: options[:temperature] || config.temperature,
20
26
  max_tokens: options[:max_tokens] || config.max_tokens
21
- }
27
+ }.compact
22
28
 
23
- # Add additional OpenAI-specific options
24
29
  payload[:top_p] = options[:top_p] if options[:top_p]
25
30
  payload[:frequency_penalty] = options[:frequency_penalty] if options[:frequency_penalty]
26
31
  payload[:presence_penalty] = options[:presence_penalty] if options[:presence_penalty]
27
32
  payload[:stop] = options[:stop] if options[:stop]
28
33
 
29
- url = "#{@base_url}/chat/completions"
30
- log_request(:post, url, payload)
31
-
32
- response = http_client.post(url, payload, build_headers.merge(authorization_header))
33
- log_response(response)
34
-
35
- result = handle_response(response)
36
- format_response(result)
37
- end
34
+ if tools && !tools.empty?
35
+ payload[:tools] = ProviderSchema.for_many(:openai, tools)
36
+ payload[:tool_choice] = tool_choice if tool_choice != :auto
37
+ end
38
38
 
39
- def complete(prompt:, **options)
40
- # For older models that use completions endpoint
41
- if config.model.include?('davinci') || config.model.include?('curie') ||
42
- config.model.include?('babbage') || config.model.include?('ada')
43
- completion_request(prompt, **options)
39
+ if stream
40
+ payload[:stream] = true
41
+ payload[:stream_options] = { include_usage: true }
42
+ stream_chat(payload, stream)
44
43
  else
45
- # Use chat endpoint for newer models
46
- super
44
+ plain_chat(payload)
47
45
  end
48
46
  end
49
47
 
48
+ def supports_native_tools?(model: config.model) # rubocop:disable Lint/UnusedMethodArgument
49
+ true
50
+ end
51
+
50
52
  def models
51
53
  url = "#{@base_url}/models"
52
- response = http_client.get(url, {}, build_headers.merge(authorization_header))
54
+ response = http_client.get(url, {}, build_headers.merge(auth_header))
53
55
  result = handle_response(response)
54
56
  result['data'].map { |model| model['id'] }
55
57
  end
56
58
 
57
59
  private
58
60
 
59
- def authorization_header
60
- { 'Authorization' => "Bearer #{config.api_key}" }
61
+ def chat_url
62
+ "#{@base_url}/chat/completions"
61
63
  end
62
64
 
63
- def completion_request(prompt, **options)
64
- payload = {
65
- model: config.model,
66
- prompt: prompt,
67
- temperature: options[:temperature] || config.temperature,
68
- max_tokens: options[:max_tokens] || config.max_tokens
69
- }
65
+ def plain_chat(payload)
66
+ url = chat_url
67
+ log_request(:post, url, payload)
68
+ response = http_client.post(url, payload, build_headers.merge(auth_header))
69
+ log_response(response)
70
+ body = handle_response(response)
71
+ normalize_non_streaming(body)
72
+ end
70
73
 
71
- url = "#{@base_url}/completions"
74
+ def stream_chat(payload, sink) # rubocop:disable Metrics/AbcSize
75
+ url = chat_url
72
76
  log_request(:post, url, payload)
73
77
 
74
- response = http_client.post(url, payload, build_headers.merge(authorization_header))
75
- log_response(response)
78
+ assembled_text = +''
79
+ tool_calls_by_index = {}
80
+ final_usage = nil
81
+ finish_reason = nil
82
+
83
+ parser = SSEParser.new do |sse|
84
+ data_str = sse[:data]
85
+ next if data_str == '[DONE]'
86
+
87
+ data = JSON.parse(data_str)
88
+ choice = data.dig('choices', 0) || {}
89
+ delta = choice['delta'] || {}
90
+
91
+ if delta['content']
92
+ assembled_text << delta['content']
93
+ sink.call(Events::TextDelta.new(
94
+ type: :text_delta, timestamp: Time.now, agent: nil, iteration: nil,
95
+ text: delta['content']
96
+ ))
97
+ end
76
98
 
77
- result = handle_response(response)
78
- format_completion_response(result)
79
- end
99
+ Array(delta['tool_calls']).each do |tc|
100
+ idx = tc['index']
101
+ tool_calls_by_index[idx] ||= { id: nil, name: nil, arguments: +'' }
102
+ tool_calls_by_index[idx][:id] ||= tc['id']
103
+ tool_calls_by_index[idx][:name] ||= tc.dig('function', 'name')
104
+ tool_calls_by_index[idx][:arguments] << (tc.dig('function', 'arguments') || '')
105
+ end
106
+
107
+ finish_reason ||= choice['finish_reason']&.to_sym
80
108
 
81
- def format_messages(messages)
82
- messages.map do |msg|
83
- if msg.is_a?(Hash)
84
- msg
85
- else
86
- { role: 'user', content: msg.to_s }
109
+ if data['usage']
110
+ final_usage = {
111
+ prompt_tokens: data['usage']['prompt_tokens'],
112
+ completion_tokens: data['usage']['completion_tokens'],
113
+ total_tokens: data['usage']['total_tokens']
114
+ }
87
115
  end
88
116
  end
89
- end
90
117
 
91
- def format_response(response)
92
- choice = response.dig('choices', 0)
93
- return nil unless choice
118
+ streaming_post(url, payload) { |chunk| parser.feed(chunk) }
119
+
120
+ tool_calls = tool_calls_by_index.values.map do |tc|
121
+ {
122
+ id: tc[:id],
123
+ name: tc[:name],
124
+ arguments: tc[:arguments].empty? ? {} : JSON.parse(tc[:arguments])
125
+ }
126
+ end
127
+
128
+ if final_usage
129
+ sink.call(Events::Usage.new(
130
+ type: :usage, timestamp: Time.now, agent: nil, iteration: nil,
131
+ prompt_tokens: final_usage[:prompt_tokens],
132
+ completion_tokens: final_usage[:completion_tokens],
133
+ total_tokens: final_usage[:total_tokens],
134
+ cost_usd: Pricing.cost_for(config.model,
135
+ prompt_tokens: final_usage[:prompt_tokens],
136
+ completion_tokens: final_usage[:completion_tokens])
137
+ ))
138
+ end
94
139
 
95
140
  {
96
- content: choice.dig('message', 'content'),
97
- role: choice.dig('message', 'role'),
98
- finish_reason: choice['finish_reason'],
99
- usage: response['usage'],
100
- model: response['model'],
101
- provider: :openai
141
+ content: assembled_text.empty? ? nil : assembled_text,
142
+ tool_calls: tool_calls,
143
+ usage: final_usage || {},
144
+ finish_reason: finish_reason || :stop,
145
+ model: config.model,
146
+ provider: provider_name
102
147
  }
103
148
  end
104
149
 
105
- def format_completion_response(response)
106
- choice = response.dig('choices', 0)
107
- return nil unless choice
150
+ def provider_name
151
+ :openai
152
+ end
108
153
 
154
+ def streaming_post(url, payload, &on_chunk)
155
+ conn = Faraday.new do |f|
156
+ f.request :json
157
+ f.options.timeout = config.timeout
158
+ f.adapter Faraday.default_adapter
159
+ end
160
+ conn.post(url) do |req|
161
+ req.headers = build_headers.merge(auth_header)
162
+ req.body = payload.to_json
163
+ req.options.on_data = proc { |chunk, _| on_chunk.call(chunk) }
164
+ end
165
+ end
166
+
167
+ def normalize_non_streaming(body)
168
+ choice = body.dig('choices', 0) || {}
169
+ msg = choice['message'] || {}
170
+ tool_calls = Array(msg['tool_calls']).map do |tc|
171
+ {
172
+ id: tc['id'],
173
+ name: tc.dig('function', 'name'),
174
+ arguments: JSON.parse(tc.dig('function', 'arguments') || '{}')
175
+ }
176
+ end
109
177
  {
110
- content: choice['text'],
111
- finish_reason: choice['finish_reason'],
112
- usage: response['usage'],
113
- model: response['model'],
114
- provider: :openai
178
+ content: msg['content'],
179
+ tool_calls: tool_calls,
180
+ usage: {
181
+ prompt_tokens: body.dig('usage', 'prompt_tokens'),
182
+ completion_tokens: body.dig('usage', 'completion_tokens'),
183
+ total_tokens: body.dig('usage', 'total_tokens')
184
+ },
185
+ finish_reason: (choice['finish_reason'] || 'stop').to_sym,
186
+ model: body['model'] || config.model,
187
+ provider: provider_name
115
188
  }
116
189
  end
117
190
 
191
+ def auth_header
192
+ { 'Authorization' => "Bearer #{config.openai_api_key || config.api_key}" }
193
+ end
194
+
118
195
  def validate_config!
119
- raise ConfigurationError, "OpenAI API key is required" unless config.openai_api_key || config.api_key
120
- raise ConfigurationError, "Model is required" unless config.model
196
+ raise ConfigurationError, 'OpenAI API key is required' unless config.openai_api_key || config.api_key
197
+ raise ConfigurationError, 'Model is required' unless config.model
121
198
  end
122
199
  end
123
200
  end
124
- end
201
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'transport/stdio'
5
+ require_relative 'transport/http'
6
+ require_relative 'tool_adapter'
7
+
8
+ module RCrewAI
9
+ module MCP
10
+ class Error < RCrewAI::Error; end
11
+
12
+ # Minimal MCP (Model Context Protocol) JSON-RPC client. Connects to a
13
+ # server via stdio or streamable HTTP, performs the initialize/initialized
14
+ # handshake, lists tools, and exposes them as RCrewAI::Tools::Base instances.
15
+ class Client
16
+ PROTOCOL_VERSION = '2024-11-05'
17
+
18
+ attr_reader :server_name, :tools
19
+
20
+ def self.connect(**opts)
21
+ new(**opts).tap(&:open)
22
+ end
23
+
24
+ def self.with_connection(**opts)
25
+ c = connect(**opts)
26
+ begin
27
+ yield c
28
+ ensure
29
+ c&.close
30
+ end
31
+ end
32
+
33
+ def initialize(command: nil, args: [], env: {}, url: nil, headers: {})
34
+ @transport = if url
35
+ Transport::Http.new(url: url, headers: headers)
36
+ else
37
+ Transport::Stdio.new(command: command, args: args, env: env)
38
+ end
39
+ @request_id = 0
40
+ @tools = []
41
+ @server_name = nil
42
+ end
43
+
44
+ def open
45
+ @transport.open
46
+ handshake
47
+ load_tools
48
+ end
49
+
50
+ def close
51
+ @transport.close
52
+ end
53
+
54
+ def call_tool(name, args)
55
+ result = request('tools/call',
56
+ name: strip_prefix(name),
57
+ arguments: args)
58
+ text = result.dig('content', 0, 'text')
59
+ text || result['content']
60
+ end
61
+
62
+ private
63
+
64
+ def handshake
65
+ info = request('initialize',
66
+ protocolVersion: PROTOCOL_VERSION,
67
+ capabilities: { tools: {} },
68
+ clientInfo: { name: 'rcrewai', version: RCrewAI::VERSION })
69
+ @server_name = info.dig('serverInfo', 'name') || 'mcp'
70
+ notify('notifications/initialized', {})
71
+ end
72
+
73
+ def load_tools
74
+ result = request('tools/list', {})
75
+ @tools = Array(result['tools']).map { |t| ToolAdapter.new(self, t, @server_name) }
76
+ end
77
+
78
+ def request(method, params)
79
+ @request_id += 1
80
+ msg = { jsonrpc: '2.0', id: @request_id, method: method, params: params }
81
+ @transport.send_line(msg.to_json)
82
+ line = @transport.recv_line
83
+ raise Error, 'connection closed before response' if line.nil?
84
+
85
+ reply = JSON.parse(line)
86
+ raise Error, reply['error']['message'] if reply['error']
87
+
88
+ reply['result']
89
+ end
90
+
91
+ def notify(method, params)
92
+ msg = { jsonrpc: '2.0', method: method, params: params }
93
+ @transport.send_line(msg.to_json)
94
+ end
95
+
96
+ def strip_prefix(prefixed_name)
97
+ prefixed_name.sub(/^#{Regexp.escape(@server_name)}__/, '')
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../tools/base'
4
+
5
+ module RCrewAI
6
+ module MCP
7
+ # Wraps a tool advertised by an MCP server as an RCrewAI::Tools::Base.
8
+ # Names are prefixed with the server name to avoid collisions when an
9
+ # agent has tools from multiple MCP servers.
10
+ class ToolAdapter < RCrewAI::Tools::Base
11
+ def initialize(client, mcp_tool_descriptor, server_name)
12
+ super()
13
+ @client = client
14
+ @descriptor = mcp_tool_descriptor
15
+ @server_name = server_name
16
+ @adapter_name = "#{server_name}__#{mcp_tool_descriptor['name']}"
17
+ @adapter_description = mcp_tool_descriptor['description'].to_s
18
+ end
19
+
20
+ def name
21
+ @adapter_name
22
+ end
23
+
24
+ def description
25
+ @adapter_description
26
+ end
27
+
28
+ def json_schema
29
+ {
30
+ name: @adapter_name,
31
+ description: @adapter_description,
32
+ parameters: stringify_keys(@descriptor['inputSchema'] ||
33
+ { 'type' => 'object', 'additionalProperties' => true })
34
+ }
35
+ end
36
+
37
+ def execute(**args)
38
+ @client.call_tool(@adapter_name, args)
39
+ end
40
+
41
+ def execute_with_validation(args_hash)
42
+ execute(**args_hash.transform_keys(&:to_sym))
43
+ end
44
+
45
+ private
46
+
47
+ def stringify_keys(value)
48
+ case value
49
+ when Hash
50
+ value.each_with_object({}) { |(k, v), out| out[k.to_s] = stringify_keys(v) }
51
+ when Array
52
+ value.map { |v| stringify_keys(v) }
53
+ else
54
+ value
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require_relative '../../sse_parser'
5
+
6
+ module RCrewAI
7
+ module MCP
8
+ module Transport
9
+ # Streamable HTTP transport for MCP: server pushes JSON-RPC responses
10
+ # via a long-lived SSE stream (GET); client sends requests via POST.
11
+ class Http
12
+ def initialize(url:, headers: {})
13
+ @url = url
14
+ @headers = headers
15
+ @queue = Queue.new
16
+ @sse_thread = nil
17
+ end
18
+
19
+ def open
20
+ @http = Faraday.new(url: @url) { |f| f.adapter Faraday.default_adapter }
21
+ @sse_thread = Thread.new { start_sse_stream }
22
+ end
23
+
24
+ def send_line(json)
25
+ @http.post('') do |req|
26
+ req.headers.merge!(@headers).merge!('Content-Type' => 'application/json')
27
+ req.body = json
28
+ end
29
+ end
30
+
31
+ def recv_line
32
+ @queue.pop
33
+ end
34
+
35
+ def close
36
+ @sse_thread&.kill
37
+ end
38
+
39
+ private
40
+
41
+ def start_sse_stream
42
+ parser = SSEParser.new do |evt|
43
+ @queue << "#{evt[:data]}\n" if evt[:event] == 'message' || evt[:event].nil?
44
+ end
45
+ @http.get('') do |req|
46
+ req.headers.merge!(@headers).merge!('Accept' => 'text/event-stream')
47
+ req.options.on_data = proc { |chunk, _| parser.feed(chunk) }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module RCrewAI
7
+ module MCP
8
+ module Transport
9
+ class Stdio
10
+ def initialize(command:, args: [], env: {})
11
+ @command = command
12
+ @args = args
13
+ @env = env
14
+ @stdin = nil
15
+ @stdout = nil
16
+ @stderr_thread = nil
17
+ @wait_thr = nil
18
+ end
19
+
20
+ def open
21
+ @stdin, @stdout, stderr, @wait_thr = Open3.popen3(@env, @command, *@args)
22
+ @stderr_thread = Thread.new do
23
+ stderr.each_line { |l| Kernel.warn "[mcp-stderr] #{l}" }
24
+ rescue IOError
25
+ # stream closed
26
+ end
27
+ end
28
+
29
+ def send_line(json)
30
+ @stdin.write("#{json}\n")
31
+ @stdin.flush
32
+ end
33
+
34
+ def recv_line
35
+ @stdout.gets
36
+ end
37
+
38
+ def close
39
+ return unless @wait_thr&.alive?
40
+
41
+ begin
42
+ ::Process.kill('TERM', @wait_thr.pid)
43
+ rescue Errno::ESRCH, Errno::EPERM
44
+ # already dead
45
+ end
46
+ @stdin&.close
47
+ @stdout&.close
48
+ @stderr_thread&.kill
49
+ rescue IOError
50
+ # streams already closed
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mcp/client'
4
+
5
+ module RCrewAI
6
+ module MCP
7
+ end
8
+ end
@@ -32,16 +32,16 @@ module RCrewAI
32
32
  @short_term = @short_term.first(@max_short_term)
33
33
 
34
34
  # Add to long-term memory if successful
35
- if execution_data[:success]
36
- task_type = execution_data[:task_type]
37
- @long_term[task_type] ||= []
38
- @long_term[task_type] << execution_data
39
-
40
- # Keep only best executions for each type
41
- @long_term[task_type] = @long_term[task_type]
42
- .sort_by { |e| [e[:success] ? 0 : 1, -e[:execution_time]] }
43
- .first(10)
44
- end
35
+ return unless execution_data[:success]
36
+
37
+ task_type = execution_data[:task_type]
38
+ @long_term[task_type] ||= []
39
+ @long_term[task_type] << execution_data
40
+
41
+ # Keep only best executions for each type
42
+ @long_term[task_type] = @long_term[task_type]
43
+ .sort_by { |e| [e[:success] ? 0 : 1, -e[:execution_time]] }
44
+ .first(10)
45
45
  end
46
46
 
47
47
  def add_tool_usage(tool_name, params, result)
@@ -54,7 +54,7 @@ module RCrewAI
54
54
  }
55
55
 
56
56
  @tool_usage.unshift(usage_data)
57
- @tool_usage = @tool_usage.first(50) # Keep last 50 tool usages
57
+ @tool_usage = @tool_usage.first(50) # Keep last 50 tool usages
58
58
  end
59
59
 
60
60
  def relevant_executions(task, limit = 3)
@@ -75,18 +75,16 @@ module RCrewAI
75
75
  end
76
76
 
77
77
  # Check long-term memory
78
- if @long_term[task_type]
79
- @long_term[task_type].each do |execution|
80
- similarity = calculate_similarity(task, execution)
81
- candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
82
- end
78
+ @long_term[task_type]&.each do |execution|
79
+ similarity = calculate_similarity(task, execution)
80
+ candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
83
81
  end
84
82
 
85
83
  # Sort by similarity and success, return top results
86
84
  relevant = candidates
87
- .sort_by { |c| [-c[:similarity], c[:execution][:success] ? 0 : 1] }
88
- .first(limit)
89
- .map { |c| format_execution_for_context(c[:execution]) }
85
+ .sort_by { |c| [-c[:similarity], c[:execution][:success] ? 0 : 1] }
86
+ .first(limit)
87
+ .map { |c| format_execution_for_context(c[:execution]) }
90
88
 
91
89
  relevant.empty? ? nil : relevant.join("\n---\n")
92
90
  end
@@ -123,13 +121,23 @@ module RCrewAI
123
121
 
124
122
  def classify_task_type(task)
125
123
  description = task.description.downcase
126
-
127
- return :research if description.include?('research') || description.include?('find') || description.include?('search')
128
- return :analysis if description.include?('analyze') || description.include?('examine') || description.include?('study')
129
- return :writing if description.include?('write') || description.include?('create') || description.include?('compose')
130
- return :coding if description.include?('code') || description.include?('program') || description.include?('develop')
131
- return :planning if description.include?('plan') || description.include?('strategy') || description.include?('organize')
132
-
124
+
125
+ if description.include?('research') || description.include?('find') || description.include?('search')
126
+ return :research
127
+ end
128
+ if description.include?('analyze') || description.include?('examine') || description.include?('study')
129
+ return :analysis
130
+ end
131
+ if description.include?('write') || description.include?('create') || description.include?('compose')
132
+ return :writing
133
+ end
134
+ if description.include?('code') || description.include?('program') || description.include?('develop')
135
+ return :coding
136
+ end
137
+ if description.include?('plan') || description.include?('strategy') || description.include?('organize')
138
+ return :planning
139
+ end
140
+
133
141
  :general
134
142
  end
135
143
 
@@ -142,17 +150,17 @@ module RCrewAI
142
150
  # Simple similarity based on common words and task type
143
151
  task_words = extract_keywords(task.description)
144
152
  execution_words = extract_keywords(execution[:task_description])
145
-
153
+
146
154
  common_words = (task_words & execution_words).length
147
155
  total_words = (task_words | execution_words).length
148
-
149
- return 0.0 if total_words == 0
150
-
156
+
157
+ return 0.0 if total_words.zero?
158
+
151
159
  word_similarity = common_words.to_f / total_words
152
-
160
+
153
161
  # Boost similarity if task types match
154
- type_bonus = (classify_task_type(task) == execution[:task_type]) ? 0.2 : 0.0
155
-
162
+ type_bonus = classify_task_type(task) == execution[:task_type] ? 0.2 : 0.0
163
+
156
164
  [word_similarity + type_bonus, 1.0].min
157
165
  end
158
166
 
@@ -163,7 +171,7 @@ module RCrewAI
163
171
  end
164
172
 
165
173
  def format_execution_for_context(execution)
166
- success_indicator = execution[:success] ? "" : ""
174
+ success_indicator = execution[:success] ? '' : ''
167
175
  <<~CONTEXT
168
176
  #{success_indicator} Task: #{execution[:task_name]}
169
177
  Description: #{execution[:task_description]}
@@ -174,7 +182,7 @@ module RCrewAI
174
182
  end
175
183
 
176
184
  def format_tool_usage_for_context(usage)
177
- success_indicator = usage[:success] ? "" : ""
185
+ success_indicator = usage[:success] ? '' : ''
178
186
  params_str = usage[:params].map { |k, v| "#{k}=#{v}" }.join(', ')
179
187
  <<~CONTEXT
180
188
  #{success_indicator} Tool: #{usage[:tool_name]}
@@ -186,9 +194,9 @@ module RCrewAI
186
194
 
187
195
  def calculate_success_rate
188
196
  return 0.0 if @short_term.empty?
189
-
197
+
190
198
  successful = @short_term.count { |e| e[:success] }
191
199
  (successful.to_f / @short_term.length * 100).round(1)
192
200
  end
193
201
  end
194
- end
202
+ end