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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +108 -0
- data/LICENSE +21 -0
- data/README.md +328 -0
- data/Rakefile +130 -0
- data/bin/rcrewai +7 -0
- data/docs/_config.yml +59 -0
- data/docs/_layouts/api.html +16 -0
- data/docs/_layouts/default.html +78 -0
- data/docs/_layouts/example.html +24 -0
- data/docs/_layouts/tutorial.html +33 -0
- data/docs/api/configuration.md +327 -0
- data/docs/api/crew.md +345 -0
- data/docs/api/index.md +41 -0
- data/docs/api/tools.md +412 -0
- data/docs/assets/css/style.css +416 -0
- data/docs/examples/human-in-the-loop.md +382 -0
- data/docs/examples/index.md +78 -0
- data/docs/examples/production-ready-crew.md +485 -0
- data/docs/examples/simple-research-crew.md +297 -0
- data/docs/index.md +353 -0
- data/docs/tutorials/getting-started.md +341 -0
- data/examples/async_execution_example.rb +294 -0
- data/examples/hierarchical_crew_example.rb +193 -0
- data/examples/human_in_the_loop_example.rb +233 -0
- data/lib/rcrewai/agent.rb +636 -0
- data/lib/rcrewai/async_executor.rb +248 -0
- data/lib/rcrewai/cli.rb +39 -0
- data/lib/rcrewai/configuration.rb +100 -0
- data/lib/rcrewai/crew.rb +292 -0
- data/lib/rcrewai/human_input.rb +520 -0
- data/lib/rcrewai/llm_client.rb +41 -0
- data/lib/rcrewai/llm_clients/anthropic.rb +127 -0
- data/lib/rcrewai/llm_clients/azure.rb +158 -0
- data/lib/rcrewai/llm_clients/base.rb +82 -0
- data/lib/rcrewai/llm_clients/google.rb +158 -0
- data/lib/rcrewai/llm_clients/ollama.rb +199 -0
- data/lib/rcrewai/llm_clients/openai.rb +124 -0
- data/lib/rcrewai/memory.rb +194 -0
- data/lib/rcrewai/process.rb +421 -0
- data/lib/rcrewai/task.rb +376 -0
- data/lib/rcrewai/tools/base.rb +82 -0
- data/lib/rcrewai/tools/code_executor.rb +333 -0
- data/lib/rcrewai/tools/email_sender.rb +210 -0
- data/lib/rcrewai/tools/file_reader.rb +111 -0
- data/lib/rcrewai/tools/file_writer.rb +115 -0
- data/lib/rcrewai/tools/pdf_processor.rb +342 -0
- data/lib/rcrewai/tools/sql_database.rb +226 -0
- data/lib/rcrewai/tools/web_search.rb +131 -0
- data/lib/rcrewai/version.rb +5 -0
- data/lib/rcrewai.rb +36 -0
- data/rcrewai.gemspec +54 -0
- 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
|