aiagent 0.2.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +2 -8
- data/README.md +50 -4
- data/lib/ai_agent/ai_agent/claude.rb +17 -1
- data/lib/ai_agent/ai_agent/open_ai.rb +45 -0
- data/lib/ai_agent/base.rb +20 -11
- data/lib/ai_agent/ruby/version.rb +1 -1
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c46f1d1aaa8f9ec271a8e04aa6afa8d08b207f940c99e9f0ae66f7336631a2dd
|
4
|
+
data.tar.gz: f9f9e087a83a2dc914c609f80f8afb740a7304102911c9ed043910c8db8f0288
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce5d258e8fba960b0b55a561480e2196ebe8db708bfc9063a70d28964b616cbe10470b57d921947cb96c7dd32d89e66c94ebbf9b8fa9bc2eb916a078e3b584e1
|
7
|
+
data.tar.gz: b91e1b1e07654e0e4d63de223e462f38f361c27e988e5d71059560d8a64319ac66e665d73e5921ada9a44661474eca3db7f67e6805825369e760e5778f7fbf22
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,22 +1,16 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
aiagent (0.1
|
5
|
-
httparty
|
4
|
+
aiagent (0.3.1)
|
6
5
|
|
7
6
|
GEM
|
8
7
|
remote: https://rubygems.org/
|
9
8
|
specs:
|
10
9
|
ast (2.4.2)
|
11
10
|
docile (1.4.0)
|
12
|
-
httparty (0.21.0)
|
13
|
-
mini_mime (>= 1.0.0)
|
14
|
-
multi_xml (>= 0.5.2)
|
15
11
|
json (2.7.1)
|
16
12
|
language_server-protocol (3.17.0.3)
|
17
|
-
mini_mime (1.1.5)
|
18
13
|
minitest (5.22.2)
|
19
|
-
multi_xml (0.6.0)
|
20
14
|
parallel (1.24.0)
|
21
15
|
parser (3.3.0.5)
|
22
16
|
ast (~> 2.4.1)
|
@@ -51,10 +45,10 @@ GEM
|
|
51
45
|
PLATFORMS
|
52
46
|
arm64-darwin-22
|
53
47
|
ruby
|
48
|
+
x86_64-linux
|
54
49
|
|
55
50
|
DEPENDENCIES
|
56
51
|
aiagent!
|
57
|
-
httparty
|
58
52
|
minitest (~> 5.0)
|
59
53
|
rake (~> 13.0)
|
60
54
|
rubocop (~> 1.21)
|
data/README.md
CHANGED
@@ -32,7 +32,17 @@ Example initializer:
|
|
32
32
|
# config/initializers/ai_agent.rb
|
33
33
|
|
34
34
|
require "ai_agent/ai_agent/claude"
|
35
|
+
require "ai_agent/ai_agent/open_ai"
|
35
36
|
```
|
37
|
+
|
38
|
+
You'll also need to add supporting gems to your Gemfile for each of the agents that you enable.
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
gem 'claude-ruby'
|
42
|
+
gem 'ruby-openai'
|
43
|
+
```
|
44
|
+
|
45
|
+
|
36
46
|
## Setup
|
37
47
|
|
38
48
|
To use this gem you'll need an API key for the agents that you want to use.
|
@@ -41,7 +51,7 @@ Set your API keys as environment variables, or pass them to the AiAgent initiali
|
|
41
51
|
|
42
52
|
Example with environment variables:
|
43
53
|
```ruby
|
44
|
-
|
54
|
+
ENV['ANTHROPIC_API_KEY'] = 'YOUR_ANTHROPIC_API_KEY'
|
45
55
|
|
46
56
|
ai_agent = AiAgent::Claude.new
|
47
57
|
```
|
@@ -53,17 +63,53 @@ ai_agent = AiAgent::Claude.new(api_key: 'ANTHROPIC_API_KEY')
|
|
53
63
|
|
54
64
|
## Usage
|
55
65
|
|
56
|
-
Basic example usage for
|
66
|
+
Basic example usage for OpenAI ChatGPT:
|
67
|
+
|
68
|
+
```ruby
|
69
|
+
ai_agent = AiAgent::OpenAI.new(timeout: 30)
|
70
|
+
prompt = "Generate 5 inspirational quotes."
|
71
|
+
messages = [{ 'role': 'user', 'content': prompt }]
|
72
|
+
response = ai_agent.chat(messages, options: {})
|
73
|
+
ai_agent.format_response(response)
|
74
|
+
```
|
75
|
+
|
76
|
+
Basic example usage for Anthropic Claude:
|
77
|
+
|
78
|
+
```ruby
|
79
|
+
ai_agent = AiAgent::Claude.new(timeout: 30)
|
80
|
+
prompt = "Generate 5 inspirational quotes."
|
81
|
+
messages = [{ 'role': 'user', 'content': prompt }]
|
82
|
+
response = ai_agent.chat(messages, options: {})
|
83
|
+
ai_agent.format_response(response)
|
84
|
+
```
|
85
|
+
|
86
|
+
|
87
|
+
At the API level Claude 'system' message needs to be a parameter in the options rather than an element in the messages array.
|
88
|
+
|
89
|
+
Using AiAgent you have the choice of using a Claude-specific 'send_messages' method which takes the data in the format expected by Claude, or you can use the more standard 'chat' interface, which follows the openai convention and will be mapped seamlessly by AiAgent.
|
90
|
+
|
91
|
+
Example using send_messages:
|
57
92
|
|
58
93
|
```ruby
|
59
94
|
ai_agent = AiAgent::Claude.new(timeout: 30)
|
60
95
|
prompt = "Generate 5 inspirational quotes."
|
61
96
|
messages = [{ 'role': 'user', 'content': prompt }]
|
62
|
-
|
63
|
-
response = ai_agent.send_messages(messages, options)
|
97
|
+
response = ai_agent.send_messages(messages, options: { system: 'Reply only in Spanish.' })
|
64
98
|
ai_agent.format_response(response)
|
65
99
|
```
|
66
100
|
|
101
|
+
Example using chat:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
ai_agent = AiAgent::Claude.new(timeout: 30)
|
105
|
+
prompt = "Generate 5 inspirational quotes."
|
106
|
+
messages = [{ 'role': 'system', 'content': 'Reply only in Spanish' }, { 'role': 'user', 'content': prompt }]
|
107
|
+
response = ai_agent.chat(messages, options: {})
|
108
|
+
ai_agent.format_response(response)
|
109
|
+
```
|
110
|
+
|
111
|
+
## Prepared prompts
|
112
|
+
|
67
113
|
Sentiment analysis:
|
68
114
|
```ruby
|
69
115
|
ai_agent = AiAgent::Claude.new
|
@@ -20,7 +20,23 @@ module AiAgent
|
|
20
20
|
claude if agent == CLAUDE
|
21
21
|
end
|
22
22
|
|
23
|
-
|
23
|
+
# When using the 'chat' interface we need to do a bit of rejigging because
|
24
|
+
# Claude expects the system message to be in options instead of messages.
|
25
|
+
def chat(messages, options: {})
|
26
|
+
system_content = nil
|
27
|
+
messages.reject! do |hash|
|
28
|
+
if hash[:role] == 'system'
|
29
|
+
system_content = hash[:content]
|
30
|
+
true
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
send_messages(messages, options: options.reverse_merge(system: system_content))
|
37
|
+
end
|
38
|
+
|
39
|
+
def send_messages(messages, options: {})
|
24
40
|
client.messages(messages, options)
|
25
41
|
end
|
26
42
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "ai_agent/base"
|
2
|
+
if Gem.loaded_specs.has_key?('ruby-openai')
|
3
|
+
# require 'ruby-openai'
|
4
|
+
else
|
5
|
+
Rails.logger.warn "ruby-openai gem is not loaded"
|
6
|
+
end
|
7
|
+
|
8
|
+
module AiAgent
|
9
|
+
OPENAI = 'openai'.freeze
|
10
|
+
|
11
|
+
class OpenAI < Base
|
12
|
+
module Model
|
13
|
+
GPT_4O = 'gpt-4o'
|
14
|
+
GPT_4_TURBO = 'gpt-4-turbo'
|
15
|
+
GPT_4 = 'gpt-4'
|
16
|
+
GPT_3_5_TURBO= 'gpt-3.5-turbo'
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(api_key: nil, endpoint: nil, timeout: 60)
|
20
|
+
self.agent = OPENAI
|
21
|
+
self.api_key = api_key || ENV['OPENAI_API_KEY']
|
22
|
+
self.endpoint = endpoint # nil for default as defined in openai/client
|
23
|
+
self.timeout = timeout
|
24
|
+
end
|
25
|
+
|
26
|
+
def client
|
27
|
+
openai if agent == OPENAI
|
28
|
+
end
|
29
|
+
|
30
|
+
def chat(messages, options: {})
|
31
|
+
params = options.reverse_merge(model: Model::GPT_4O, temperature: 0.1)
|
32
|
+
client.chat(parameters: params.merge(messages: messages))
|
33
|
+
end
|
34
|
+
|
35
|
+
def format_response(response)
|
36
|
+
response['choices'][0]['message']['content'] rescue "ERROR: Couldn't extract text from OpenAI response"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def openai
|
42
|
+
@openai ||= ::OpenAI::Client.new(access_token: api_key, uri_base: endpoint, request_timeout: timeout)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/ai_agent/base.rb
CHANGED
@@ -16,8 +16,11 @@ module AiAgent
|
|
16
16
|
raise NotImplementedError, "Subclasses must implement the client method"
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
19
|
+
# messages should be an array of hashes, where each hash contains role and content.
|
20
|
+
# role can be 'system', 'assistant', or 'user'.
|
21
|
+
# e.g. [{ 'role': 'system', 'content': 'You are a helpful assistant' }, { 'role': 'user', 'content': 'Tell me a joke' }]
|
22
|
+
def chat(messages, options: {})
|
23
|
+
raise NotImplementedError, "Subclasses must implement the chat (completions) method"
|
21
24
|
end
|
22
25
|
|
23
26
|
def format_response(response)
|
@@ -26,41 +29,47 @@ module AiAgent
|
|
26
29
|
|
27
30
|
def analyze_sentiment(text, strict: true, options: {})
|
28
31
|
prompt = "Analyze the sentiment of the following text and classify it as positive, negative, or neutral:\n\n#{text}\n\nSentiment: "
|
32
|
+
messages = [{ 'role': 'user', 'content': prompt }]
|
33
|
+
system = options.delete(:system)
|
29
34
|
if strict
|
30
|
-
system = "If you are asked to return a word, then return only that word with no preamble or postamble.
|
35
|
+
system = ["If you are asked to return a word, then return only that word with no preamble or postamble.", system].compact.join(' ')
|
36
|
+
messages.prepend({ 'role': 'system', 'content': system })
|
31
37
|
max_tokens = 2
|
32
38
|
else
|
33
39
|
max_tokens = 100
|
34
40
|
end
|
35
41
|
|
36
|
-
|
37
|
-
options.reverse_merge( system: system, max_tokens: max_tokens ))
|
42
|
+
chat(messages, options: options.reverse_merge( max_tokens: max_tokens ))
|
38
43
|
end
|
39
44
|
|
40
45
|
def recognize_entities(text, strict: true, options: {})
|
41
46
|
prompt = "Identify and list the named entities in the following text:\n\n#{text}\n\nEntities: "
|
47
|
+
messages = [{ 'role': 'user', 'content': prompt }]
|
48
|
+
system = options.delete(:system)
|
42
49
|
if strict
|
43
|
-
system = "Be specific in your answer, with no preamble or postamble. If you are asked to list some names, then return only a list of those names, nothing else. "
|
50
|
+
system = ["Be specific in your answer, with no preamble or postamble. If you are asked to list some names, then return only a list of those names, nothing else. ", system].compact.join(' ')
|
51
|
+
messages.prepend({ 'role': 'system', 'content': system })
|
44
52
|
max_tokens = 100
|
45
53
|
else
|
46
54
|
max_tokens = 500
|
47
55
|
end
|
48
56
|
|
49
|
-
|
50
|
-
options.reverse_merge( system: system, max_tokens: max_tokens ))
|
57
|
+
chat(messages, options: options.reverse_merge( max_tokens: max_tokens ))
|
51
58
|
end
|
52
59
|
|
53
60
|
def summarize_text(text, strict: true, options: {})
|
54
61
|
prompt = "Summarize the following text:\n\n#{text}\n\nSummary: "
|
62
|
+
messages = [{ 'role': 'user', 'content': prompt }]
|
63
|
+
system = options.delete(:system)
|
55
64
|
if strict
|
56
|
-
system = "Be specific in your answer, with no preamble or postamble. I.e. return only what the user asks for, nothing else. "
|
65
|
+
system = ["Be specific in your answer, with no preamble or postamble. I.e. return only what the user asks for, nothing else. ", system].compact.join(' ')
|
66
|
+
messages.prepend({ 'role': 'system', 'content': system })
|
57
67
|
max_tokens = 100
|
58
68
|
else
|
59
69
|
max_tokens = 500
|
60
70
|
end
|
61
71
|
|
62
|
-
|
63
|
-
options.reverse_merge( system: system, max_tokens: max_tokens ))
|
72
|
+
chat(messages, options: options.reverse_merge( max_tokens: max_tokens ))
|
64
73
|
end
|
65
74
|
end
|
66
75
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aiagent
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Web Ventures Ltd
|
@@ -10,8 +10,8 @@ bindir: exe
|
|
10
10
|
cert_chain: []
|
11
11
|
date: 2024-07-09 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
|
-
description:
|
14
|
-
|
13
|
+
description: AiAgent provides a common way to interact with the APIs provided by OpenAI,
|
14
|
+
Anthropic, and other AI assistants.
|
15
15
|
email:
|
16
16
|
- webven@mailgab.com
|
17
17
|
executables: []
|
@@ -25,6 +25,7 @@ files:
|
|
25
25
|
- README.md
|
26
26
|
- Rakefile
|
27
27
|
- lib/ai_agent/ai_agent/claude.rb
|
28
|
+
- lib/ai_agent/ai_agent/open_ai.rb
|
28
29
|
- lib/ai_agent/base.rb
|
29
30
|
- lib/ai_agent/ruby.rb
|
30
31
|
- lib/ai_agent/ruby/version.rb
|
@@ -54,5 +55,6 @@ requirements: []
|
|
54
55
|
rubygems_version: 3.5.3
|
55
56
|
signing_key:
|
56
57
|
specification_version: 4
|
57
|
-
summary:
|
58
|
+
summary: An interface for interacting with AI Agents such as ChatGPT, Claude, Gemini,
|
59
|
+
LeChat, Ollama.
|
58
60
|
test_files: []
|