open_ai_bot 0.2.11 → 0.3.1

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: 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: []