open_ai_bot 0.2.11 → 0.3.1

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: 577e5ceaa83702d924c3d5d5bbdd030f77e6dde813e787d4ceabfe1785301007
4
- data.tar.gz: e2a6bec851ee1d3b2ed7beb33471dcdf39a04f072ca477c72f7d38358b6f479b
3
+ metadata.gz: 21fff768a394b10773c77b9fafea42d01500cca136aea8c418753da1ad61024d
4
+ data.tar.gz: 7b2da8e1e8e0038a5420877ed0644888c0948976876dc065f65bb0f1ba9f94f1
5
5
  SHA512:
6
- metadata.gz: daf9a35778b1933e6992de0198d44e2934f3257a02d9836a4faaf8b1146c8dd3a86515f047cc7b2e7f8ae86f377e28e9872028f3ac74104ccfab54c0c31eacd1
7
- data.tar.gz: 0ba19cee1b518c306e063b84a1ffd0c42e018fd50ddf3d3c4250a0ce6bae7be09da24c065ff42abff2dba0fb2e2331c7ad449a18ad6a2355b79d605d6b11dea5
6
+ metadata.gz: 82fff5e67a384cbcd0cb6d8a99bb1a3c9a42016ced7dbe9bd2d93d694183ab299f5b9b308662660c3997aa3d6407e420151e28e019ed9d63c1a8716b50bb3b6e
7
+ data.tar.gz: e525381d25b1e2d81e3bbd73c6c6990753948d0287c476f531fca32f16e3980b0f8380869945e9254e639925a6247ac799a140442899496d74687554631370e2
data/.rubocop.yml CHANGED
@@ -1,7 +1,7 @@
1
1
  require: rubocop-rspec
2
2
 
3
3
  AllCops:
4
- TargetRubyVersion: 3.2.2
4
+ TargetRubyVersion: 3.3.2
5
5
  NewCops: enable
6
6
  Exclude:
7
7
  - test.rb
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.2
data/Gemfile.lock CHANGED
@@ -1,22 +1,19 @@
1
1
  GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
- addressable (2.8.5)
4
+ addressable (2.8.6)
5
5
  public_suffix (>= 2.0.2, < 6.0)
6
- async (2.6.4)
7
- console (~> 1.10)
6
+ async (2.12.0)
7
+ console (~> 1.25, >= 1.25.2)
8
8
  fiber-annotation
9
- io-event (~> 1.1)
10
- timers (~> 4.1)
11
- base64 (0.1.1)
12
- coderay (1.1.3)
13
- concurrent-ruby (1.2.2)
14
- console (1.23.2)
9
+ io-event (~> 1.6)
10
+ bigdecimal (3.1.8)
11
+ concurrent-ruby (1.3.2)
12
+ console (1.25.2)
15
13
  fiber-annotation
16
- fiber-local
17
- domain_name (0.5.20190701)
18
- unf (>= 0.0.5, < 1.0.0)
19
- down (5.4.1)
14
+ fiber-local (~> 1.1)
15
+ json
16
+ down (5.4.2)
20
17
  addressable (~> 2.8)
21
18
  dry-core (1.0.1)
22
19
  concurrent-ruby (~> 1.0)
@@ -31,55 +28,36 @@ GEM
31
28
  dry-types (>= 1.7, < 2)
32
29
  ice_nine (~> 0.11)
33
30
  zeitwerk (~> 2.6)
34
- dry-types (1.7.1)
31
+ dry-types (1.7.2)
32
+ bigdecimal (~> 3.0)
35
33
  concurrent-ruby (~> 1.0)
36
34
  dry-core (~> 1.0)
37
35
  dry-inflector (~> 1.0)
38
36
  dry-logic (~> 1.4)
39
37
  zeitwerk (~> 2.6)
40
- faraday (2.7.11)
41
- base64
42
- faraday-net_http (>= 2.0, < 3.1)
43
- ruby2_keywords (>= 0.0.4)
38
+ event_stream_parser (0.3.0)
39
+ faraday (2.9.1)
40
+ faraday-net_http (>= 2.0, < 3.2)
44
41
  faraday-multipart (1.0.4)
45
42
  multipart-post (~> 2)
46
- faraday-net_http (3.0.2)
47
- ffi (1.15.5)
48
- ffi-compiler (1.0.1)
49
- ffi (>= 1.0.0)
50
- rake
43
+ faraday-net_http (3.1.0)
44
+ net-http
51
45
  fiber-annotation (0.2.0)
52
- fiber-local (1.0.0)
53
- http (5.1.1)
54
- addressable (~> 2.8)
55
- http-cookie (~> 1.0)
56
- http-form_data (~> 2.2)
57
- llhttp-ffi (~> 0.4.0)
58
- http-cookie (1.0.5)
59
- domain_name (~> 0.5)
60
- http-form_data (2.3.0)
46
+ fiber-local (1.1.0)
47
+ fiber-storage
48
+ fiber-storage (0.1.1)
61
49
  ice_nine (0.11.2)
62
- io-event (1.3.2)
63
- llhttp-ffi (0.4.0)
64
- ffi-compiler (~> 1.0)
65
- rake (~> 13.0)
66
- method_source (1.0.0)
67
- multipart-post (2.3.0)
68
- nokogiri (1.15.4-arm64-darwin)
69
- racc (~> 1.4)
70
- nokogiri (1.15.4-x86_64-linux)
71
- racc (~> 1.4)
72
- pry (0.14.2)
73
- coderay (~> 1.1)
74
- method_source (~> 1.0)
75
- public_suffix (5.0.3)
76
- racc (1.7.1)
77
- rake (13.0.6)
78
- ruby-openai (5.1.0)
50
+ io-event (1.6.0)
51
+ json (2.7.2)
52
+ multipart-post (2.4.1)
53
+ net-http (0.4.1)
54
+ uri
55
+ public_suffix (5.0.5)
56
+ ruby-openai (5.2.0)
57
+ event_stream_parser (>= 0.3.0, < 1.0.0)
79
58
  faraday (>= 1)
80
59
  faraday-multipart (>= 1)
81
- ruby2_keywords (0.0.5)
82
- rubydium (0.2.5)
60
+ rubydium (0.4.1)
83
61
  async (~> 2.3)
84
62
  telegram-bot-ruby (~> 1.0.0)
85
63
  telegram-bot-ruby (1.0.0)
@@ -87,23 +65,17 @@ GEM
87
65
  faraday (~> 2.0)
88
66
  faraday-multipart (~> 1.0)
89
67
  zeitwerk (~> 2.6)
90
- timers (4.3.5)
91
- unf (0.1.4)
92
- unf_ext
93
- unf_ext (0.0.8.2)
94
- zeitwerk (2.6.11)
68
+ uri (0.13.0)
69
+ zeitwerk (2.6.15)
95
70
 
96
71
  PLATFORMS
97
- arm64-darwin-22
72
+ ruby
98
73
  x86_64-linux
99
74
 
100
75
  DEPENDENCIES
101
76
  down (~> 5.4)
102
- http (~> 5.1)
103
- nokogiri (~> 1.15)
104
- pry
105
77
  ruby-openai (~> 5.1)
106
78
  rubydium (>= 0.2.5)
107
79
 
108
80
  BUNDLED WITH
109
- 2.4.19
81
+ 2.5.9
data/README.md CHANGED
@@ -16,8 +16,8 @@ sudo apt install autoconf patch build-essential rustc libssl-dev libyaml-dev lib
16
16
  git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.13.1
17
17
  echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc
18
18
  asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git
19
- asdf install ruby 3.2.2
20
- echo 'ruby 3.2.2' >> ~/.tool-versions
19
+ asdf install ruby 3.3.2
20
+ echo 'ruby 3.3.2' >> ~/.tool-versions
21
21
  ```
22
22
 
23
23
  2. `ffmpeg`
@@ -86,7 +86,9 @@ module OpenAI
86
86
  replies_to: @replies_to&.message_id,
87
87
  from: username(@user),
88
88
  body: @text_without_bot_mentions,
89
- chat_id: @chat.id
89
+ chat_id: @chat.id,
90
+ chat_thread: current_thread,
91
+ image: Image.from_tg_photo(download_file(@msg.photo&.last), model: current_thread.model)
90
92
  )
91
93
 
92
94
  return unless current_message.valid?
@@ -98,7 +100,9 @@ module OpenAI
98
100
  replies_to: @replies_to.reply_to_message&.message_id,
99
101
  from: username(@target),
100
102
  body: @replies_to.text.to_s.gsub(/@#{config.bot_username}\b/, ""),
101
- chat_id: @chat.id
103
+ chat_id: @chat.id,
104
+ chat_thread: current_thread,
105
+ image: Image.from_tg_photo(download_file(@replies_to.photo&.last), model: current_thread.model)
102
106
  )
103
107
  else
104
108
  nil
@@ -115,20 +119,21 @@ module OpenAI
115
119
 
116
120
  response = open_ai.chat(
117
121
  parameters: {
118
- model: current_thread.model || config.open_ai["chat_gpt_model"],
122
+ model: current_thread.model.to_s,
119
123
  messages: current_thread.as_json
120
124
  }
121
125
  )
122
126
 
123
127
  if response["error"]
124
- error_text = "```#{response["error"]["message"]}```"
128
+ error_text = "```\n#{response["error"]["message"]}```"
125
129
  error_text += "\n\nHint: send /restart command to reset the context." if error_text.match? "tokens"
126
130
  send_chat_gpt_error(error_text.strip)
127
131
  else
128
132
  text = response.dig("choices", 0, "message", "content")
129
- tokens = response.dig("usage", "total_tokens")
130
133
 
131
- send_chat_gpt_response(text, tokens)
134
+ tokens_info = get_tokens_info!(response)
135
+
136
+ send_chat_gpt_response(text, tokens_info)
132
137
  end
133
138
  end
134
139
 
@@ -136,15 +141,28 @@ module OpenAI
136
141
  reply(text, parse_mode: "Markdown")
137
142
  end
138
143
 
139
- def send_chat_gpt_response(text, tokens)
140
- id = reply(text).dig("result", "message_id")
144
+ def get_tokens_info!(response)
145
+ completion_tokens = response.dig("usage", "completion_tokens")
146
+ prompt_tokens = response.dig("usage", "prompt_tokens")
147
+ vision_tokens = current_thread.claim_vision_tokens!
148
+
149
+ result = current_thread.model.request_cost(completion_tokens:, prompt_tokens:, vision_tokens:, current_thread:)
150
+ end
151
+
152
+ def send_chat_gpt_response(text, tokens_info)
153
+ tokens_text = tokens_info[:info]
154
+
155
+ id = reply(text + tokens_text).dig("result", "message_id")
156
+
141
157
  bot_message = BotMessage.new(
142
158
  id: id,
143
159
  replies_to: @message_id,
144
160
  body: text,
145
161
  chat_id: @chat.id,
146
- tokens: tokens
162
+ chat_thread: current_thread,
163
+ cost: tokens_info[:total]
147
164
  )
165
+
148
166
  current_thread.add(bot_message)
149
167
  end
150
168
  end
@@ -3,8 +3,9 @@
3
3
  module OpenAI
4
4
  class ChatThread
5
5
  def initialize(defaults = [], model = nil)
6
+ model ||= OpenAIBot.config.open_ai["chat_gpt_model"].to_sym
6
7
  @history ||= defaults
7
- @model = model
8
+ @model = model.is_a?(Model) ? model : Model.new(model)
8
9
  puts @history
9
10
  end
10
11
 
@@ -20,6 +21,14 @@ module OpenAI
20
21
  true
21
22
  end
22
23
 
24
+ def total_cost
25
+ @history.map(&:cost).compact.sum
26
+ end
27
+
28
+ def claim_vision_tokens!
29
+ @history.reject(&:vision_tokens_claimed?).map(&:claim_vision_tokens!).compact.sum
30
+ end
31
+
23
32
  def add(message)
24
33
  return false unless message&.valid?
25
34
  return false if @history.any? { message.id == _1.id}
@@ -0,0 +1,38 @@
1
+ module OpenAI
2
+ class Image
3
+ BASE_TOKENS = 85
4
+ TILE_TOKENS = 170
5
+ TILE_SIZE = 512
6
+
7
+ def self.from_tg_photo(file, model:)
8
+ return unless file
9
+ return unless model.has_vision?
10
+
11
+ base64 = Base64.encode64(file.read)
12
+ size = `identify -format "%w %h" ./#{file.original_filename}`
13
+ width, height = size.split(" ")
14
+ FileUtils.rm_rf("./#{file.original_filename}")
15
+
16
+ new(width, height, base64)
17
+ end
18
+
19
+ attr_accessor :width, :height, :base64
20
+
21
+ def initialize(width, height, base64)
22
+ @width = width
23
+ @height = height
24
+ @base64 = base64
25
+ end
26
+
27
+ def tokens
28
+ @tokens ||= begin
29
+ tiles = tiles(width) * tiles(height)
30
+ (tiles * TILE_TOKENS) + BASE_TOKENS
31
+ end
32
+ end
33
+
34
+ def tiles(pixels)
35
+ (pixels.to_f / TILE_SIZE).ceil
36
+ end
37
+ end
38
+ end
@@ -3,23 +3,45 @@ module OpenAI
3
3
  # (ChatGPT isn't brilliant at parsing JSON sructures without starting to reply in JSON, so most of it is useless)
4
4
 
5
5
  class Message
6
- attr_accessor :body, :from, :id, :replies_to, :tokens, :chat_id
6
+ attr_accessor :body, :from, :id, :replies_to, :chat_id, :image, :chat_thread, :cost
7
7
  attr_reader :role, :timestamp
8
8
 
9
9
  def initialize(**kwargs)
10
10
  kwargs.each_pair { public_send("#{_1}=", _2) }
11
11
  @role = :user
12
12
  @timestamp = Time.now.to_i
13
+ @vision_tokens_claimed = !image
14
+ end
15
+
16
+ def vision_tokens_claimed?
17
+ @vision_tokens_claimed
18
+ end
19
+
20
+ def claim_vision_tokens!
21
+ # binding.pry
22
+ @vision_tokens_claimed = true
23
+ image&.tokens
13
24
  end
14
25
 
15
26
  def valid?
16
- [body, from, id, chat_id].all?(&:present?)
27
+ [(image || body), from, id, chat_id, chat_thread].all?(&:present?)
17
28
  end
18
29
 
19
30
  # Format for OpenAI API
20
31
  def as_json
21
- content = [from, body].compact.join("\n")
22
- { role:, content: content }
32
+ msg = [from, body].compact.join("\n")
33
+
34
+ if image
35
+ {
36
+ role: role,
37
+ content: [
38
+ { type: "text", text: msg },
39
+ { type: "image_url", image_url: { url: "data:image/jpeg;base64,#{image.base64}" } }
40
+ ]
41
+ }
42
+ else
43
+ { role:, content: msg }
44
+ end
23
45
  end
24
46
 
25
47
  # Format for machine-readable logs
@@ -35,7 +57,8 @@ module OpenAI
35
57
  "From" => from,
36
58
  "To" => replies_to,
37
59
  "Body" => body,
38
- "Tokens used" => tokens
60
+ # "Tokens used" => tokens,
61
+ "Image" => (image ? "Some image" : "None")
39
62
  }.reject { |_k, v|
40
63
  v.blank?
41
64
  }.map { |k, v|
@@ -57,7 +80,7 @@ module OpenAI
57
80
  end
58
81
 
59
82
  def valid?
60
- body.present?
83
+ [body, chat_thread].all?(&:present?)
61
84
  end
62
85
  end
63
86
 
@@ -68,7 +91,7 @@ module OpenAI
68
91
  end
69
92
 
70
93
  def valid?
71
- [body, id, chat_id, tokens].all?(&:present?)
94
+ [body, id, chat_id, chat_thread].all?(&:present?)
72
95
  end
73
96
  end
74
97
  end
@@ -0,0 +1,63 @@
1
+ module OpenAI
2
+ class Model
3
+ # All prices are per 1K tokens
4
+ MODEL_INFO = {
5
+ "gpt-4o": {
6
+ max_context: 128_000,
7
+ prompt_price: 0.005,
8
+ completion_price: 0.015,
9
+ vision_price: 0.005
10
+ },
11
+ "gpt-3.5-turbo": {
12
+ max_context: 16385,
13
+ prompt_price: 0.0005,
14
+ completion_price: 0.0015,
15
+ vision_price: 0
16
+ }
17
+ }
18
+
19
+ attr_accessor :max_context, :prompt_price, :completion_price, :vision_price
20
+
21
+ [:max_context, :prompt_price, :completion_price, :vision_price].each do |attr|
22
+ define_method(attr) do
23
+ MODEL_INFO[@model][attr]
24
+ end
25
+ end
26
+
27
+ def initialize(model)
28
+ raise ArgumentError.new("Unknown model: #{model}") unless MODEL_INFO[model]
29
+
30
+ @model = model
31
+ end
32
+
33
+ def to_s
34
+ @model
35
+ end
36
+
37
+ def has_vision?
38
+ MODEL_INFO[@model][:vision_price].positive?
39
+ end
40
+
41
+ def request_cost(prompt_tokens:, completion_tokens:, vision_tokens:, current_thread:)
42
+ prompt_cost = prompt_tokens * prompt_price / 1000
43
+ completion_cost = completion_tokens * completion_price / 1000
44
+ vision_cost = vision_tokens * vision_price / 1000
45
+
46
+ total = prompt_cost + completion_cost + vision_cost
47
+ thread_total = current_thread.total_cost
48
+
49
+ info = "\n\n" + {
50
+ prompt: "#{prompt_tokens} tokens (#{prompt_cost.round(5)}$)",
51
+ completion: "#{completion_tokens} tokens (#{completion_cost.round(5)}$)",
52
+ vision: "#{vision_tokens} tokens (#{vision_cost.round(5)}$)",
53
+ total: "#{total.round(5)}$",
54
+ total_for_this_conversation: "#{(thread_total + total).round(5)}$",
55
+ max_context: max_context
56
+ }.map { |k, v|
57
+ "#{k}: #{v}"
58
+ }.join("\n")
59
+
60
+ { info:, total: }
61
+ end
62
+ end
63
+ end
data/lib/open_ai/utils.rb CHANGED
@@ -15,6 +15,8 @@ module OpenAI
15
15
  end
16
16
 
17
17
  def download_file(voice, dir=nil)
18
+ return unless voice
19
+
18
20
  file_path = @api.get_file(file_id: voice.file_id)["result"]["file_path"]
19
21
 
20
22
  url = "https://api.telegram.org/file/bot#{config.token}/#{file_path}"
@@ -40,7 +40,7 @@ module OpenAI
40
40
  reply(text)
41
41
  end
42
42
 
43
- def send_whisper_error
43
+ def send_whisper_error(text)
44
44
  reply_code(text)
45
45
  end
46
46
 
data/lib/open_ai_bot.rb CHANGED
@@ -6,11 +6,12 @@ require_relative "open_ai/message"
6
6
  require_relative "open_ai/dalle"
7
7
  require_relative "open_ai/utils"
8
8
  require_relative "open_ai/whisper"
9
+ require_relative "open_ai/model"
10
+ require_relative "open_ai/image"
9
11
 
10
12
  require_relative "ext/blank"
11
13
  require_relative "ext/in"
12
14
 
13
-
14
15
  class OpenAIBot < Rubydium::Bot
15
16
  include OpenAI::ChatGPT
16
17
  include OpenAI::Dalle
data/main.rb CHANGED
@@ -5,6 +5,7 @@ require "openai"
5
5
  require "yaml"
6
6
  require "down"
7
7
  require "rubydium"
8
+ require "base64"
8
9
 
9
10
  require_relative "lib/open_ai_bot"
10
11
  require_relative "lib/clean_bot"
data/open_ai_bot.gemspec CHANGED
@@ -8,7 +8,7 @@ require_relative "lib/ext/in"
8
8
 
9
9
  Gem::Specification.new do |spec|
10
10
  spec.name = "open_ai_bot"
11
- spec.version = "0.2.11"
11
+ spec.version = "0.3.1"
12
12
  spec.authors = ["bulgakke"]
13
13
  spec.email = ["vvp835@yandex.ru"]
14
14
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: open_ai_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.11
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - bulgakke
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-03 00:00:00.000000000 Z
11
+ date: 2024-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: down
@@ -80,7 +80,7 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: '5.1'
83
- description:
83
+ description:
84
84
  email:
85
85
  - vvp835@yandex.ru
86
86
  executables: []
@@ -89,6 +89,7 @@ extra_rdoc_files: []
89
89
  files:
90
90
  - ".gitignore"
91
91
  - ".rubocop.yml"
92
+ - ".ruby-version"
92
93
  - Gemfile
93
94
  - Gemfile.lock
94
95
  - README.md
@@ -100,7 +101,9 @@ files:
100
101
  - lib/open_ai/chat_gpt.rb
101
102
  - lib/open_ai/chat_thread.rb
102
103
  - lib/open_ai/dalle.rb
104
+ - lib/open_ai/image.rb
103
105
  - lib/open_ai/message.rb
106
+ - lib/open_ai/model.rb
104
107
  - lib/open_ai/utils.rb
105
108
  - lib/open_ai/whisper.rb
106
109
  - lib/open_ai_bot.rb
@@ -113,7 +116,7 @@ metadata:
113
116
  homepage_uri: https://github.com/bulgakke/open_ai_bot
114
117
  source_code_uri: https://github.com/bulgakke/open_ai_bot
115
118
  rubygems_mfa_required: 'true'
116
- post_install_message:
119
+ post_install_message:
117
120
  rdoc_options: []
118
121
  require_paths:
119
122
  - lib
@@ -128,8 +131,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
131
  - !ruby/object:Gem::Version
129
132
  version: '0'
130
133
  requirements: []
131
- rubygems_version: 3.4.10
132
- signing_key:
134
+ rubygems_version: 3.5.9
135
+ signing_key:
133
136
  specification_version: 4
134
137
  summary: Telegram bot for using ChatGPT, DALL-E and Whisper
135
138
  test_files: []