rcrewai 0.1.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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +108 -0
  3. data/LICENSE +21 -0
  4. data/README.md +328 -0
  5. data/Rakefile +130 -0
  6. data/bin/rcrewai +7 -0
  7. data/docs/_config.yml +59 -0
  8. data/docs/_layouts/api.html +16 -0
  9. data/docs/_layouts/default.html +78 -0
  10. data/docs/_layouts/example.html +24 -0
  11. data/docs/_layouts/tutorial.html +33 -0
  12. data/docs/api/configuration.md +327 -0
  13. data/docs/api/crew.md +345 -0
  14. data/docs/api/index.md +41 -0
  15. data/docs/api/tools.md +412 -0
  16. data/docs/assets/css/style.css +416 -0
  17. data/docs/examples/human-in-the-loop.md +382 -0
  18. data/docs/examples/index.md +78 -0
  19. data/docs/examples/production-ready-crew.md +485 -0
  20. data/docs/examples/simple-research-crew.md +297 -0
  21. data/docs/index.md +353 -0
  22. data/docs/tutorials/getting-started.md +341 -0
  23. data/examples/async_execution_example.rb +294 -0
  24. data/examples/hierarchical_crew_example.rb +193 -0
  25. data/examples/human_in_the_loop_example.rb +233 -0
  26. data/lib/rcrewai/agent.rb +636 -0
  27. data/lib/rcrewai/async_executor.rb +248 -0
  28. data/lib/rcrewai/cli.rb +39 -0
  29. data/lib/rcrewai/configuration.rb +100 -0
  30. data/lib/rcrewai/crew.rb +292 -0
  31. data/lib/rcrewai/human_input.rb +520 -0
  32. data/lib/rcrewai/llm_client.rb +41 -0
  33. data/lib/rcrewai/llm_clients/anthropic.rb +127 -0
  34. data/lib/rcrewai/llm_clients/azure.rb +158 -0
  35. data/lib/rcrewai/llm_clients/base.rb +82 -0
  36. data/lib/rcrewai/llm_clients/google.rb +158 -0
  37. data/lib/rcrewai/llm_clients/ollama.rb +199 -0
  38. data/lib/rcrewai/llm_clients/openai.rb +124 -0
  39. data/lib/rcrewai/memory.rb +194 -0
  40. data/lib/rcrewai/process.rb +421 -0
  41. data/lib/rcrewai/task.rb +376 -0
  42. data/lib/rcrewai/tools/base.rb +82 -0
  43. data/lib/rcrewai/tools/code_executor.rb +333 -0
  44. data/lib/rcrewai/tools/email_sender.rb +210 -0
  45. data/lib/rcrewai/tools/file_reader.rb +111 -0
  46. data/lib/rcrewai/tools/file_writer.rb +115 -0
  47. data/lib/rcrewai/tools/pdf_processor.rb +342 -0
  48. data/lib/rcrewai/tools/sql_database.rb +226 -0
  49. data/lib/rcrewai/tools/web_search.rb +131 -0
  50. data/lib/rcrewai/version.rb +5 -0
  51. data/lib/rcrewai.rb +36 -0
  52. data/rcrewai.gemspec +54 -0
  53. metadata +365 -0
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module RCrewAI
6
+ module LLMClients
7
+ class OpenAI < Base
8
+ BASE_URL = 'https://api.openai.com/v1'
9
+
10
+ def initialize(config = RCrewAI.configuration)
11
+ super
12
+ @base_url = BASE_URL
13
+ end
14
+
15
+ def chat(messages:, **options)
16
+ payload = {
17
+ model: config.model,
18
+ messages: format_messages(messages),
19
+ temperature: options[:temperature] || config.temperature,
20
+ max_tokens: options[:max_tokens] || config.max_tokens
21
+ }
22
+
23
+ # Add additional OpenAI-specific options
24
+ payload[:top_p] = options[:top_p] if options[:top_p]
25
+ payload[:frequency_penalty] = options[:frequency_penalty] if options[:frequency_penalty]
26
+ payload[:presence_penalty] = options[:presence_penalty] if options[:presence_penalty]
27
+ payload[:stop] = options[:stop] if options[:stop]
28
+
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
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)
44
+ else
45
+ # Use chat endpoint for newer models
46
+ super
47
+ end
48
+ end
49
+
50
+ def models
51
+ url = "#{@base_url}/models"
52
+ response = http_client.get(url, {}, build_headers.merge(authorization_header))
53
+ result = handle_response(response)
54
+ result['data'].map { |model| model['id'] }
55
+ end
56
+
57
+ private
58
+
59
+ def authorization_header
60
+ { 'Authorization' => "Bearer #{config.api_key}" }
61
+ end
62
+
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
+ }
70
+
71
+ url = "#{@base_url}/completions"
72
+ log_request(:post, url, payload)
73
+
74
+ response = http_client.post(url, payload, build_headers.merge(authorization_header))
75
+ log_response(response)
76
+
77
+ result = handle_response(response)
78
+ format_completion_response(result)
79
+ end
80
+
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 }
87
+ end
88
+ end
89
+ end
90
+
91
+ def format_response(response)
92
+ choice = response.dig('choices', 0)
93
+ return nil unless choice
94
+
95
+ {
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
102
+ }
103
+ end
104
+
105
+ def format_completion_response(response)
106
+ choice = response.dig('choices', 0)
107
+ return nil unless choice
108
+
109
+ {
110
+ content: choice['text'],
111
+ finish_reason: choice['finish_reason'],
112
+ usage: response['usage'],
113
+ model: response['model'],
114
+ provider: :openai
115
+ }
116
+ end
117
+
118
+ 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
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest'
5
+
6
+ module RCrewAI
7
+ class Memory
8
+ attr_reader :short_term, :long_term, :tool_usage
9
+
10
+ def initialize
11
+ @short_term = [] # Recent executions, limited size
12
+ @long_term = {} # Persistent memory, keyed by task type/similarity
13
+ @tool_usage = [] # Tool usage history
14
+ @max_short_term = 100
15
+ @similarity_threshold = 0.7
16
+ end
17
+
18
+ def add_execution(task, result, execution_time)
19
+ execution_data = {
20
+ task_name: task.name,
21
+ task_description: task.description,
22
+ task_type: classify_task_type(task),
23
+ result: result,
24
+ execution_time: execution_time,
25
+ timestamp: Time.now,
26
+ success: !result.to_s.downcase.include?('failed'),
27
+ hash: generate_task_hash(task)
28
+ }
29
+
30
+ # Add to short-term memory
31
+ @short_term.unshift(execution_data)
32
+ @short_term = @short_term.first(@max_short_term)
33
+
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
45
+ end
46
+
47
+ def add_tool_usage(tool_name, params, result)
48
+ usage_data = {
49
+ tool_name: tool_name,
50
+ params: params,
51
+ result: result,
52
+ timestamp: Time.now,
53
+ success: !result.to_s.downcase.include?('error')
54
+ }
55
+
56
+ @tool_usage.unshift(usage_data)
57
+ @tool_usage = @tool_usage.first(50) # Keep last 50 tool usages
58
+ end
59
+
60
+ def relevant_executions(task, limit = 3)
61
+ task_type = classify_task_type(task)
62
+ task_hash = generate_task_hash(task)
63
+
64
+ # Get similar executions from both short and long term memory
65
+ candidates = []
66
+
67
+ # Check short-term for exact or similar matches
68
+ @short_term.each do |execution|
69
+ if execution[:hash] == task_hash
70
+ candidates << { execution: execution, similarity: 1.0 }
71
+ elsif execution[:task_type] == task_type
72
+ similarity = calculate_similarity(task, execution)
73
+ candidates << { execution: execution, similarity: similarity } if similarity > @similarity_threshold
74
+ end
75
+ end
76
+
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
83
+ end
84
+
85
+ # Sort by similarity and success, return top results
86
+ 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]) }
90
+
91
+ relevant.empty? ? nil : relevant.join("\n---\n")
92
+ end
93
+
94
+ def tool_usage_for(tool_name, limit = 5)
95
+ @tool_usage
96
+ .select { |usage| usage[:tool_name] == tool_name }
97
+ .first(limit)
98
+ .map { |usage| format_tool_usage_for_context(usage) }
99
+ .join("\n")
100
+ end
101
+
102
+ def clear_short_term!
103
+ @short_term.clear
104
+ end
105
+
106
+ def clear_all!
107
+ @short_term.clear
108
+ @long_term.clear
109
+ @tool_usage.clear
110
+ end
111
+
112
+ def stats
113
+ {
114
+ short_term_count: @short_term.length,
115
+ long_term_types: @long_term.keys.length,
116
+ long_term_total: @long_term.values.flatten.length,
117
+ tool_usage_count: @tool_usage.length,
118
+ success_rate: calculate_success_rate
119
+ }
120
+ end
121
+
122
+ private
123
+
124
+ def classify_task_type(task)
125
+ 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
+
133
+ :general
134
+ end
135
+
136
+ def generate_task_hash(task)
137
+ content = "#{task.name}:#{task.description}"
138
+ Digest::SHA256.hexdigest(content)[0..16]
139
+ end
140
+
141
+ def calculate_similarity(task, execution)
142
+ # Simple similarity based on common words and task type
143
+ task_words = extract_keywords(task.description)
144
+ execution_words = extract_keywords(execution[:task_description])
145
+
146
+ common_words = (task_words & execution_words).length
147
+ total_words = (task_words | execution_words).length
148
+
149
+ return 0.0 if total_words == 0
150
+
151
+ word_similarity = common_words.to_f / total_words
152
+
153
+ # Boost similarity if task types match
154
+ type_bonus = (classify_task_type(task) == execution[:task_type]) ? 0.2 : 0.0
155
+
156
+ [word_similarity + type_bonus, 1.0].min
157
+ end
158
+
159
+ def extract_keywords(text)
160
+ # Simple keyword extraction - remove common words
161
+ stopwords = %w[the a an and or but in on at to for of with by]
162
+ text.downcase.split(/\W+/).reject { |w| w.length < 3 || stopwords.include?(w) }
163
+ end
164
+
165
+ def format_execution_for_context(execution)
166
+ success_indicator = execution[:success] ? "✓" : "✗"
167
+ <<~CONTEXT
168
+ #{success_indicator} Task: #{execution[:task_name]}
169
+ Description: #{execution[:task_description]}
170
+ Result: #{execution[:result][0..200]}#{'...' if execution[:result].length > 200}
171
+ Time: #{execution[:execution_time].round(2)}s
172
+ Date: #{execution[:timestamp].strftime('%Y-%m-%d %H:%M')}
173
+ CONTEXT
174
+ end
175
+
176
+ def format_tool_usage_for_context(usage)
177
+ success_indicator = usage[:success] ? "✓" : "✗"
178
+ params_str = usage[:params].map { |k, v| "#{k}=#{v}" }.join(', ')
179
+ <<~CONTEXT
180
+ #{success_indicator} Tool: #{usage[:tool_name]}
181
+ Params: #{params_str}
182
+ Result: #{usage[:result][0..100]}#{'...' if usage[:result].to_s.length > 100}
183
+ Date: #{usage[:timestamp].strftime('%Y-%m-%d %H:%M')}
184
+ CONTEXT
185
+ end
186
+
187
+ def calculate_success_rate
188
+ return 0.0 if @short_term.empty?
189
+
190
+ successful = @short_term.count { |e| e[:success] }
191
+ (successful.to_f / @short_term.length * 100).round(1)
192
+ end
193
+ end
194
+ end