summarize-meeting 1.4.0 → 1.5.0

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: 07e3eaa4b04f7b2d9ccb00632ab021a85b87dd8a6798a72319d84ec3c905fa4f
4
- data.tar.gz: 200e57a22c74d4c7c418f2b34ec4fbec6f53119ca1e381266ce16d53ac680e36
3
+ metadata.gz: a1f1e203f7328efbedef8a044434bb8377aac4d8cdf5a3ffc61d903d6155b211
4
+ data.tar.gz: 4b93b413a93e5f2d6cbe1d46c842889a559f32682e19b4b1611115e2b928b5fe
5
5
  SHA512:
6
- metadata.gz: a201d18bd409b281bf8e2e7610d4f98a37cc29dfff8f1a6b1c631d61fe88c00b43977b076bfa887e3f074bc64a6fa03951e2459cdc5baad738e47f5d0255d414
7
- data.tar.gz: d3ea73f7bfbc3f756324d2e72c49ff9cd67694ef7da7647b9e8b0dc08805f745d9d265a906e49ff49c55b08e406b5ddb8b8887d45be0399962a1efd7b1707964
6
+ metadata.gz: 77a9c6ecd2f9fc14c949cc5fab380981d1016ba363332c2c1bc55d2e01433acaeaa9fa3caf5eaee9c17488255da2bfb1ec33487fc624f34dcb5f4da1a97def03
7
+ data.tar.gz: 2cde7420a3e4b5628eaae774ca6c29ca6b5be1409dd42c87ab1bd216ad749557e92b68264059fdc50fe92ec4bd8fbd921b69dc0d156d2f38493916b5e19b0c70
@@ -1,56 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openai"
4
+
1
5
  module SummarizeMeeting
2
6
  module Ai
3
- class OpenAiError < StandardError; end
7
+ module OpenAIException; end
8
+ class OpenAIError < StandardError
9
+ include OpenAIException
10
+ end
11
+ class OpenAiResponseFailure < OpenAIError; end
12
+ class OpenAiNoResponseError < OpenAIError; end
13
+ class OpenAiRetryableError < OpenAIError; end
14
+ extend self
15
+
4
16
  MAX_TOTAL_TOKENS = 8000
5
17
  WORDS_PER_TOKEN = 0.75
6
18
 
7
- @@access_token = ENV["OPENAI_KEY"]
8
- @@organization_id = ENV["OPENAI_ORG"]
9
-
10
- def self.client
19
+ def client
11
20
  @client ||= new_client(access_token: access_token, organization_id: organization_id)
12
21
  end
13
22
 
14
- def self.new_client(access_token:, organization_id:)
23
+ def new_client(access_token:, organization_id:)
15
24
  OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
16
25
  end
17
26
 
18
- def self.access_token
19
- @@access_token
27
+ def access_token
28
+ @access_token ||= ENV.fetch("OPENAI_KEY") do
29
+ fail KeyError, "Set OPENAI_KEY in the the ENV or set access_token directly"
30
+ end
20
31
  end
21
32
 
22
- def self.organization_id
23
- @@organization_id
33
+ def organization_id
34
+ @organization_id ||= ENV.fetch("OPENAI_ORG") do
35
+ fail KeyError, "Set OPENAI_ORG in the the ENV or set organization_id directly"
36
+ end
24
37
  end
25
38
 
26
- def self.access_token=(token)
27
- @@access_token = token
39
+ def access_token=(token)
40
+ @access_token = token
28
41
  end
29
42
 
30
- def self.organization_id=(id)
31
- @@organization_id = id
43
+ def organization_id=(id)
44
+ @organization_id = id
32
45
  end
33
46
 
34
- def self.calculate_token_word_count(token_count)
47
+ def calculate_token_word_count(token_count)
35
48
  (token_count * WORDS_PER_TOKEN.to_f).ceil
36
49
  end
37
50
 
38
- def self.calculate_word_token_count(word_count)
51
+ def calculate_word_token_count(word_count)
39
52
  (word_count / WORDS_PER_TOKEN.to_f).ceil
40
53
  end
41
54
 
42
- def self.chat(messages, client: self.client)
55
+ # @raise on response error or no expected response content
56
+ def chat(messages, client: self.client)
43
57
  parameters = {
44
58
  model: "gpt-4",
45
59
  messages: messages,
46
60
  }
47
- response = client.chat(parameters: parameters)
48
- content = response.dig("choices", 0, "message", "content")
49
- if !content
50
- raise OpenAiError, "No response from OpenAI"
61
+ response = nil
62
+ begin
63
+ response = client.chat(parameters: parameters)
64
+ rescue => exception
65
+ handle_request_error(exception)
66
+ else
67
+ is_failure = !response.success?
68
+ if is_failure
69
+ log_response_failure(response)
70
+ raise OpenAiResponseFailure, "Failed response from OpenAI"
71
+ else
72
+ content = response.dig("choices", 0, "message", "content")
73
+ if !content
74
+ raise OpenAiNoResponseError, "No response from OpenAI in #{response.body}"
75
+ else
76
+ content
77
+ end
78
+ end
79
+ end
80
+ end
81
+
82
+ def notify_error(exception, message, **options)
83
+ if exception_notifier
84
+ exception_notifier.call(exception, message, **options)
51
85
  else
52
- content
86
+ warn "#{exception.inspect}: #{message}. options=#{options.inspect}"
87
+ end
88
+ end
89
+
90
+ # responds to call(exception, message, **options)
91
+ def exception_notifier=(exception_notifier)
92
+ @exception_notifier = exception_notifier
93
+ end
94
+
95
+ def exception_notifier
96
+ return @exception_notifier if defined?(@exception_notifier)
97
+ nil
98
+ end
99
+
100
+ def handle_request_error(exception)
101
+ is_openai_error =
102
+ case exception.inspect
103
+ when /Connection refused/i then true
104
+ when /Connection reset/i then true
105
+ when /Connection timed out/i then true
106
+ when /Could not connect/i then true
107
+ when /EOFError/i then true
108
+ when /Errno::ECONNREFUSED/i then true
109
+ when /Errno::ECONNRESET/i then true
110
+ when /Net::OpenTimeout/i then true
111
+ when /Net::ReadTimeout/i then true
112
+ when /OpenSSL::SSL::SSLError/i then false
113
+ when /Server is unavailable or does not exist/i then true
114
+ when /Unable to connect/i then true
115
+ when /closed stream/i then true
116
+ when /connect timed out/i then true
117
+ when /end of file reached/i then true
118
+ when /execution expired/i then true
119
+ else false
120
+ end
121
+ if is_openai_error
122
+ exception.extend OpenAIException
123
+ end
124
+ raise
125
+ end
126
+
127
+ def log_response_failure(response)
128
+ response_details = {
129
+ # https://github.com/jnunemaker/httparty/blob/v0.21.0/lib/httparty/response.rb#L53-L56
130
+ response: response.to_s,
131
+ }
132
+ if response.class.name.match?(/HTTParty::Response/)
133
+ response_details.merge!(
134
+ raw_request: response.request.inspect,
135
+ raw_response: response.response.inspect,
136
+ response_body: response.body,
137
+ response_code: response.code,
138
+ response_headers: response.headers,
139
+ # marshal_dump_base_64: Base64.encode64(response._dump(1)), # so it can be encoded as JSON
140
+ )
53
141
  end
142
+ notify_error(
143
+ nil, # there's no exception object
144
+ "Logging OpenAI Error",
145
+ **response_details
146
+ )
147
+ rescue => exception
148
+ notify_error(
149
+ exception,
150
+ "Unhandled error in #{name}##{__callee__}",
151
+ response: response.inspect,
152
+ )
54
153
  end
55
154
  end
56
- end
155
+ end
@@ -1,3 +1,3 @@
1
1
  module SummarizeMeeting
2
- VERSION = "1.4.0"
3
- end
2
+ VERSION = "1.5.0"
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: summarize-meeting
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Devine
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-03-17 00:00:00.000000000 Z
11
+ date: 2023-03-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: optparse