aiagent 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f54f71f8b08b031f351313bcf6fd253fdd2a588ef7d17da74115ff2b72561bb0
4
- data.tar.gz: 2a8004689fc829d08872840029e8729be303aaf236f33500e8f979d8b8c4e92d
3
+ metadata.gz: '02942755b5e166577ba8df4d4ec900b09aa0d4b18bffad704ac13c46c2d46dec'
4
+ data.tar.gz: 60ffc49db83174ad4df31da6b94b9a71d732deb594300c1bfd7447d75db16649
5
5
  SHA512:
6
- metadata.gz: bf0f1f4447b4608769fdffe194bf210cd9f409bcf00c387c0e370466b2ca3188e3e3880075ac14267f62597ed62b2389632e7943e410004d80cedf82a0cab3f0
7
- data.tar.gz: 44a6f23bfe09e4ce4e3a32ccbf8eecaa935d7b4b63aa59ffd98bb237a574731804036c51a79b42b14fd8a9113a23e4c8ad0b91484f1f938277eeb1f235049c68
6
+ metadata.gz: 47c92de7b86be56966a0a02b427f1daa3e2044453af2061de9ba7a0fa09d9d6451b6c28fd6fd37bcc92b8910292afeebb64f7e4088ee22c9854aa58b0653f471
7
+ data.tar.gz: d8254076ca058ec7b07c512838a4e3ec030051ff05cfda098d3afa0681a29403383a83998d3e5ec06e8ebc243bbe0ae97c90528ba84b5bc3704da7a2090f34a3
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## [0.3.0] - 2024-07-09
2
+
3
+ - Added OpenAI support
4
+
5
+ ## [0.2.0] - 2024-07-09
6
+
7
+ - Allow API key to be set when instantiating the agent
8
+ - Allow timeout to be set
9
+ - Allow endpoint to be customised
10
+ - Improved documentation
11
+
1
12
  ## [0.1.0] - 2024-07-09
2
13
 
3
14
  - Initial release
data/Gemfile.lock CHANGED
@@ -1,22 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- aiagent (0.1.0)
5
- httparty
4
+ aiagent (0.2.0)
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
@@ -2,8 +2,6 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/aiagent.svg)](https://badge.fury.io/rb/aiagent) [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
 
5
- ** Example usage coming soon in the next version of this gem **
6
-
7
5
  ## Installation
8
6
 
9
7
  Add this line to your application's Gemfile:
@@ -29,14 +27,102 @@ $ gem install aiagent
29
27
  Create an initializer called config/initializers/ai_agent.rb \
30
28
  And in that file simply require the agents that you'll use in your project.
31
29
 
32
- E.g. \
30
+ Example initializer:
31
+ ```ruby
32
+ # config/initializers/ai_agent.rb
33
+
33
34
  require "ai_agent/ai_agent/claude"
35
+ ```
36
+ ## Setup
37
+
38
+ To use this gem you'll need an API key for the agents that you want to use.
39
+
40
+ Set your API keys as environment variables, or pass them to the AiAgent initialize method.
41
+
42
+ Example with environment variables:
43
+ ```ruby
44
+ # ENV['ANTHROPIC_API_KEY'] = 'YOUR_ANTHROPIC_API_KEY'
45
+
46
+ ai_agent = AiAgent::Claude.new
47
+ ```
48
+
49
+ Example passing the api_key to the initalizer:
50
+ ```ruby
51
+ ai_agent = AiAgent::Claude.new(api_key: 'ANTHROPIC_API_KEY')
52
+ ```
34
53
 
35
54
  ## Usage
36
55
 
37
- To use this gem you'll need an API key for the agents that you want to use.
56
+ Basic example usage for OpenAI ChatGPT:
57
+
58
+ ```ruby
59
+ ai_agent = AiAgent::OpenAI.new(timeout: 30)
60
+ prompt = "Generate 5 inspirational quotes."
61
+ messages = [{ 'role': 'user', 'content': prompt }]
62
+ response = ai_agent.chat(messages, options: {})
63
+ ai_agent.format_response(response)
64
+ ```
65
+
66
+ Basic example usage for Anthropic Claude:
67
+
68
+ ```ruby
69
+ ai_agent = AiAgent::Claude.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
+
77
+ At the API level Claude 'system' message needs to be a parameter in the options rather than an element in the messages array.
38
78
 
39
- Set your API keys as environment variables.
79
+ 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.
80
+
81
+ Example using send_messages:
82
+
83
+ ```ruby
84
+ ai_agent = AiAgent::Claude.new(timeout: 30)
85
+ prompt = "Generate 5 inspirational quotes."
86
+ messages = [{ 'role': 'user', 'content': prompt }]
87
+ response = ai_agent.send_messages(messages, options: { system: 'Reply only in Spanish.' })
88
+ ai_agent.format_response(response)
89
+ ```
90
+
91
+ Example using chat:
92
+
93
+ ```ruby
94
+ ai_agent = AiAgent::Claude.new(timeout: 30)
95
+ prompt = "Generate 5 inspirational quotes."
96
+ messages = [{ 'role': 'system', 'content': 'Reply only in Spanish' }, { 'role': 'user', 'content': prompt }]
97
+ response = ai_agent.chat(messages, options: {})
98
+ ai_agent.format_response(response)
99
+ ```
100
+
101
+ ## Prepared prompts
102
+
103
+ Sentiment analysis:
104
+ ```ruby
105
+ ai_agent = AiAgent::Claude.new
106
+ review = "The product quality is excellent and the customer service was very helpful!"
107
+ response = ai_agent.analyze_sentiment(review, options: { model: Claude::Model::CLAUDE_CHEAPEST })
108
+ ai_agent.format_response(response)
109
+ ```
110
+
111
+ Named entity recognition:
112
+ ```ruby
113
+ ai_agent = AiAgent::Claude.new
114
+ abstract = "Anthropic released Claude 3.5 Sonnet on 21 June 2024."
115
+ response = ai_agent.recognize_entities(abstract, options: { model: Claude::Model::CLAUDE_CHEAPEST })
116
+ ai_agent.format_response(response)
117
+ ```
118
+
119
+ Text summarization:
120
+ ```ruby
121
+ ai_agent = AiAgent::Claude.new
122
+ abstract = "A long message" # customise this for your own example
123
+ response = ai_agent.summarize_text(abstract, strict: false, options: { model: Claude::Model::CLAUDE_SMARTEST })
124
+ ai_agent.format_response(response)
125
+ ```
40
126
 
41
127
  ## Changelog
42
128
 
@@ -9,16 +9,34 @@ module AiAgent
9
9
  CLAUDE = 'claude'.freeze
10
10
 
11
11
  class Claude < Base
12
- def initialize
13
- super
12
+ def initialize(api_key: nil, endpoint: nil, timeout: 60)
14
13
  self.agent = CLAUDE
14
+ self.api_key = api_key || ENV['ANTHROPIC_API_KEY']
15
+ self.endpoint = endpoint # nil for default as defined in claude/client
16
+ self.timeout = timeout
15
17
  end
16
18
 
17
19
  def client
18
20
  claude if agent == CLAUDE
19
21
  end
20
22
 
21
- def send_messages(messages, options)
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: {})
22
40
  client.messages(messages, options)
23
41
  end
24
42
 
@@ -29,11 +47,7 @@ module AiAgent
29
47
  private
30
48
 
31
49
  def claude
32
- @claude ||= ::Claude::Client.new(anthropic_api_key)
33
- end
34
-
35
- def anthropic_api_key
36
- @anthropic_api_key ||= ENV['ANTHROPIC_API_KEY']
50
+ @claude ||= ::Claude::Client.new(api_key, endpoint: endpoint, timeout: timeout)
37
51
  end
38
52
  end
39
53
  end
@@ -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
@@ -4,6 +4,9 @@ require 'json'
4
4
  module AiAgent
5
5
  class Base
6
6
  attr_accessor :agent
7
+ attr_accessor :api_key
8
+ attr_accessor :endpoint
9
+ attr_accessor :timeout
7
10
 
8
11
  def initialize
9
12
  # be sure to set agent in the subclass initialize method
@@ -13,8 +16,11 @@ module AiAgent
13
16
  raise NotImplementedError, "Subclasses must implement the client method"
14
17
  end
15
18
 
16
- def send_messages(messages, options)
17
- raise NotImplementedError, "Subclasses must implement the send_message method"
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"
18
24
  end
19
25
 
20
26
  def format_response(response)
@@ -23,41 +29,47 @@ module AiAgent
23
29
 
24
30
  def analyze_sentiment(text, strict: true, options: {})
25
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)
26
34
  if strict
27
- system = "If you are asked to return a word, then return only that word with no preamble or postamble. " if strict
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 })
28
37
  max_tokens = 2
29
38
  else
30
39
  max_tokens = 100
31
40
  end
32
41
 
33
- send_messages([ { 'role': 'user', 'content': prompt } ],
34
- options.reverse_merge( system: system, max_tokens: max_tokens ))
42
+ chat(messages, options: options.reverse_merge( max_tokens: max_tokens ))
35
43
  end
36
44
 
37
45
  def recognize_entities(text, strict: true, options: {})
38
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)
39
49
  if strict
40
- 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 })
41
52
  max_tokens = 100
42
53
  else
43
54
  max_tokens = 500
44
55
  end
45
56
 
46
- send_messages([ { 'role': 'user', 'content': prompt } ],
47
- options.reverse_merge( system: system, max_tokens: max_tokens ))
57
+ chat(messages, options: options.reverse_merge( max_tokens: max_tokens ))
48
58
  end
49
59
 
50
60
  def summarize_text(text, strict: true, options: {})
51
61
  prompt = "Summarize the following text:\n\n#{text}\n\nSummary: "
62
+ messages = [{ 'role': 'user', 'content': prompt }]
63
+ system = options.delete(:system)
52
64
  if strict
53
- 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 })
54
67
  max_tokens = 100
55
68
  else
56
69
  max_tokens = 500
57
70
  end
58
71
 
59
- send_messages([ { 'role': 'user', 'content': prompt } ],
60
- options.reverse_merge( system: system, max_tokens: max_tokens ))
72
+ chat(messages, options: options.reverse_merge( max_tokens: max_tokens ))
61
73
  end
62
74
  end
63
75
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AiAgent
4
4
  module Ruby
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aiagent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Web Ventures Ltd
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-08 00:00:00.000000000 Z
11
+ date: 2024-07-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby SDK for interacting with LLM agents such as OpenAI's ChatGPT, Anthropic's
14
14
  Claude, and Ollama.
@@ -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