open_ai_bot 0.3.0 → 0.3.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33e8660fa731258c76bc1f3367fe02f866a02a189d49444d0006b791f49df6e4
4
- data.tar.gz: fff2721f65eb41a74c9dc7569bc1be202ed3bd952c787fd371db158662542da8
3
+ metadata.gz: e31ed7bc34cf94ba2d339a1efa6b57a0d9fd972aec39961a98253cb5b9dcc4df
4
+ data.tar.gz: 2f92b54b9788efacd9bf50b66004c777a8eeb3bd934ffc65a2ea93a4a12c39e6
5
5
  SHA512:
6
- metadata.gz: 84ee567855c908efb78b5b4dc8133a35a954eb8b0d21ad0889518037eb3eed5fea210cdcb1a0f948f0b4fa323e0b92da0e5c5a6bfcf6af4d5d64bac796f09f7a
7
- data.tar.gz: 2eab3c94fc4493ce5f02e62ca909cf9edbe24955ea4b3085dab3971f4449f958f61939f4d54e3beb02755f9f1977e1950bb588e6be8e7898e8f88224d3684561
6
+ metadata.gz: 38ff4646a9493b2771d62cce25d8a2ff0ab589bc2d300041fec4536e6e75065e33c79fe452994dd5d9a628be6646e4ed97bb97a7c28503811b3a3db1b8fefa9f
7
+ data.tar.gz: 872adfdeb95db9ccbdd8073ee736136a40d4517b1b4eddee9ddf5885638dfe286c705554dd708c654dd1e9b83c62682626fd63cf3d2e878fea536a4d79c08d67
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
@@ -3,17 +3,17 @@ GEM
3
3
  specs:
4
4
  addressable (2.8.6)
5
5
  public_suffix (>= 2.0.2, < 6.0)
6
- async (2.6.5)
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.2.0)
12
- concurrent-ruby (1.2.2)
13
- console (1.23.3)
9
+ io-event (~> 1.6)
10
+ bigdecimal (3.1.8)
11
+ concurrent-ruby (1.3.2)
12
+ console (1.25.2)
14
13
  fiber-annotation
15
- fiber-local
16
- down (5.4.1)
14
+ fiber-local (~> 1.1)
15
+ json
16
+ down (5.4.2)
17
17
  addressable (~> 2.8)
18
18
  dry-core (1.0.1)
19
19
  concurrent-ruby (~> 1.0)
@@ -28,31 +28,35 @@ GEM
28
28
  dry-types (>= 1.7, < 2)
29
29
  ice_nine (~> 0.11)
30
30
  zeitwerk (~> 2.6)
31
- dry-types (1.7.1)
31
+ dry-types (1.7.2)
32
+ bigdecimal (~> 3.0)
32
33
  concurrent-ruby (~> 1.0)
33
34
  dry-core (~> 1.0)
34
35
  dry-inflector (~> 1.0)
35
36
  dry-logic (~> 1.4)
36
37
  zeitwerk (~> 2.6)
37
38
  event_stream_parser (0.3.0)
38
- faraday (2.7.12)
39
- base64
40
- faraday-net_http (>= 2.0, < 3.1)
41
- ruby2_keywords (>= 0.0.4)
39
+ faraday (2.9.1)
40
+ faraday-net_http (>= 2.0, < 3.2)
42
41
  faraday-multipart (1.0.4)
43
42
  multipart-post (~> 2)
44
- faraday-net_http (3.0.2)
43
+ faraday-net_http (3.1.0)
44
+ net-http
45
45
  fiber-annotation (0.2.0)
46
- fiber-local (1.0.0)
46
+ fiber-local (1.1.0)
47
+ fiber-storage
48
+ fiber-storage (0.1.1)
47
49
  ice_nine (0.11.2)
48
- io-event (1.3.3)
49
- multipart-post (2.3.0)
50
- public_suffix (5.0.4)
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)
51
56
  ruby-openai (5.2.0)
52
57
  event_stream_parser (>= 0.3.0, < 1.0.0)
53
58
  faraday (>= 1)
54
59
  faraday-multipart (>= 1)
55
- ruby2_keywords (0.0.5)
56
60
  rubydium (0.4.1)
57
61
  async (~> 2.3)
58
62
  telegram-bot-ruby (~> 1.0.0)
@@ -61,10 +65,11 @@ GEM
61
65
  faraday (~> 2.0)
62
66
  faraday-multipart (~> 1.0)
63
67
  zeitwerk (~> 2.6)
64
- timers (4.3.5)
65
- zeitwerk (2.6.12)
68
+ uri (0.13.0)
69
+ zeitwerk (2.6.15)
66
70
 
67
71
  PLATFORMS
72
+ ruby
68
73
  x86_64-linux
69
74
 
70
75
  DEPENDENCIES
@@ -73,4 +78,4 @@ DEPENDENCIES
73
78
  rubydium (>= 0.2.5)
74
79
 
75
80
  BUNDLED WITH
76
- 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`
@@ -8,6 +8,7 @@ module OpenAI
8
8
  end
9
9
 
10
10
  def new_thread(chat_id, model = nil)
11
+ model ||= config.open_ai["chat_gpt_model"].to_sym
11
12
  msgs = config.open_ai["whitelist"].include?(chat_id) ? initial_messages : []
12
13
  new_thread = ChatThread.new(msgs, model)
13
14
  threads[chat_id] = new_thread
@@ -72,16 +73,6 @@ module OpenAI
72
73
  "NULL"
73
74
  end
74
75
 
75
- def base64(file)
76
- return unless config.open_ai["chat_gpt_model"] == "gpt-4-vision-preview"
77
- return unless file
78
-
79
- f = download_file(file)
80
- res = Base64.encode64(f.read)
81
- FileUtils.rm_rf("./#{f.original_filename}")
82
- res
83
- end
84
-
85
76
  def handle_gpt_command
86
77
  return unless bot_mentioned? || bot_replied_to? || private_chat?
87
78
  return if self.class.registered_commands.keys.any? { @text.include? _1 }
@@ -97,7 +88,8 @@ module OpenAI
97
88
  from: username(@user),
98
89
  body: @text_without_bot_mentions,
99
90
  chat_id: @chat.id,
100
- base64_image: base64(@msg.photo&.last)
91
+ chat_thread: current_thread,
92
+ image: Image.from_tg_photo(download_file(@msg.photo&.last), model: current_thread.model)
101
93
  )
102
94
 
103
95
  return unless current_message.valid?
@@ -110,7 +102,8 @@ module OpenAI
110
102
  from: username(@target),
111
103
  body: @replies_to.text.to_s.gsub(/@#{config.bot_username}\b/, ""),
112
104
  chat_id: @chat.id,
113
- base64_image: base64(@replies_to.photo&.last)
105
+ chat_thread: current_thread,
106
+ image: Image.from_tg_photo(download_file(@replies_to.photo&.last), model: current_thread.model)
114
107
  )
115
108
  else
116
109
  nil
@@ -127,7 +120,7 @@ module OpenAI
127
120
 
128
121
  response = open_ai.chat(
129
122
  parameters: {
130
- model: current_thread.model || config.open_ai["chat_gpt_model"],
123
+ model: current_thread.model.to_s,
131
124
  messages: current_thread.as_json
132
125
  }
133
126
  )
@@ -138,9 +131,10 @@ module OpenAI
138
131
  send_chat_gpt_error(error_text.strip)
139
132
  else
140
133
  text = response.dig("choices", 0, "message", "content")
141
- tokens = response.dig("usage", "total_tokens")
142
134
 
143
- send_chat_gpt_response(text, tokens)
135
+ tokens_info = get_tokens_info!(response)
136
+
137
+ send_chat_gpt_response(text, tokens_info)
144
138
  end
145
139
  end
146
140
 
@@ -148,15 +142,28 @@ module OpenAI
148
142
  reply(text, parse_mode: "Markdown")
149
143
  end
150
144
 
151
- def send_chat_gpt_response(text, tokens)
152
- id = reply(text).dig("result", "message_id")
145
+ def get_tokens_info!(response)
146
+ completion_tokens = response.dig("usage", "completion_tokens")
147
+ prompt_tokens = response.dig("usage", "prompt_tokens")
148
+ vision_tokens = current_thread.claim_vision_tokens!
149
+
150
+ result = current_thread.model.request_cost(completion_tokens:, prompt_tokens:, vision_tokens:, current_thread:)
151
+ end
152
+
153
+ def send_chat_gpt_response(text, tokens_info)
154
+ tokens_text = tokens_info[:info]
155
+
156
+ id = reply(text + tokens_text).dig("result", "message_id")
157
+
153
158
  bot_message = BotMessage.new(
154
159
  id: id,
155
160
  replies_to: @message_id,
156
161
  body: text,
157
162
  chat_id: @chat.id,
158
- tokens: tokens
163
+ chat_thread: current_thread,
164
+ cost: tokens_info[:total]
159
165
  )
166
+
160
167
  current_thread.add(bot_message)
161
168
  end
162
169
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  module OpenAI
4
4
  class ChatThread
5
- def initialize(defaults = [], model = nil)
5
+ def initialize(defaults = [], model)
6
6
  @history ||= defaults
7
- @model = model
7
+ @model = model.is_a?(Model) ? model : Model.new(model)
8
8
  puts @history
9
9
  end
10
10
 
@@ -20,6 +20,14 @@ module OpenAI
20
20
  true
21
21
  end
22
22
 
23
+ def total_cost
24
+ @history.map(&:cost).compact.sum
25
+ end
26
+
27
+ def claim_vision_tokens!
28
+ @history.reject(&:vision_tokens_claimed?).map(&:claim_vision_tokens!).compact.sum
29
+ end
30
+
23
31
  def add(message)
24
32
  return false unless message&.valid?
25
33
  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,29 +3,40 @@ 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, :base64_image
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
- [(base64_image || 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
32
  msg = [from, body].compact.join("\n")
22
33
 
23
- if base64_image
34
+ if image
24
35
  {
25
36
  role: role,
26
37
  content: [
27
38
  { type: "text", text: msg },
28
- { type: "image_url", image_url: { url: "data:image/jpeg;base64,#{base64_image}" } }
39
+ { type: "image_url", image_url: { url: "data:image/jpeg;base64,#{image.base64}" } }
29
40
  ]
30
41
  }
31
42
  else
@@ -46,8 +57,8 @@ module OpenAI
46
57
  "From" => from,
47
58
  "To" => replies_to,
48
59
  "Body" => body,
49
- "Tokens used" => tokens,
50
- "Image" => (base64_image ? "Some image" : "None")
60
+ # "Tokens used" => tokens,
61
+ "Image" => (image ? "Some image" : "None")
51
62
  }.reject { |_k, v|
52
63
  v.blank?
53
64
  }.map { |k, v|
@@ -69,7 +80,7 @@ module OpenAI
69
80
  end
70
81
 
71
82
  def valid?
72
- body.present?
83
+ [body, chat_thread].all?(&:present?)
73
84
  end
74
85
  end
75
86
 
@@ -80,7 +91,7 @@ module OpenAI
80
91
  end
81
92
 
82
93
  def valid?
83
- [body, id, chat_id, tokens].all?(&:present?)
94
+ [body, id, chat_id, chat_thread].all?(&:present?)
84
95
  end
85
96
  end
86
97
  end
@@ -0,0 +1,65 @@
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
+ if MODEL_INFO[model].nil?
29
+ raise ArgumentError.new("Unknown model: #{model.inspect}.")
30
+ end
31
+
32
+ @model = model
33
+ end
34
+
35
+ def to_s
36
+ @model
37
+ end
38
+
39
+ def has_vision?
40
+ MODEL_INFO[@model][:vision_price].positive?
41
+ end
42
+
43
+ def request_cost(prompt_tokens:, completion_tokens:, vision_tokens:, current_thread:)
44
+ prompt_cost = prompt_tokens * prompt_price / 1000
45
+ completion_cost = completion_tokens * completion_price / 1000
46
+ vision_cost = vision_tokens * vision_price / 1000
47
+
48
+ total = prompt_cost + completion_cost + vision_cost
49
+ thread_total = current_thread.total_cost
50
+
51
+ info = "\n\n" + {
52
+ prompt: "#{prompt_tokens} tokens (#{prompt_cost.round(5)}$)",
53
+ completion: "#{completion_tokens} tokens (#{completion_cost.round(5)}$)",
54
+ vision: "#{vision_tokens} tokens (#{vision_cost.round(5)}$)",
55
+ total: "#{total.round(5)}$",
56
+ total_for_this_conversation: "#{(thread_total + total).round(5)}$",
57
+ max_context: max_context
58
+ }.map { |k, v|
59
+ "#{k}: #{v}"
60
+ }.join("\n")
61
+
62
+ { info:, total: }
63
+ end
64
+ end
65
+ 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}"
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.3.0"
11
+ spec.version = "0.3.2"
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.3.0
4
+ version: 0.3.2
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-12-09 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: []