summarize-meeting 0.1.1 → 0.2.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: d69584205af88a85d7c42bdcf5964d7e284523eaebc8a851245c6d807af1bc79
4
- data.tar.gz: 6d581baaf28e859a2d78475076c6b131ce25678c56ffa73752803e80eb016075
3
+ metadata.gz: 3c14fc93afabfd456a2c3778dffccf9d2a66e77f2d47ab98b864986ad0e10cd2
4
+ data.tar.gz: 68ba17378b48f84ad029ef1d3d6e1575110739bb4c8f5c40e25a759350722cba
5
5
  SHA512:
6
- metadata.gz: 8c0eb3b83b0b4f2503592c7ed954300622572ce0357dca6c24bb022ee3b6fc84d96647b5cc544732a56628d206f36502e003b7fab4498dd2c92a6e6d4059ab0f
7
- data.tar.gz: 896c4cca235e708b43753edb078eb717cdba0580826a0d5b35382e71d5f59fe70121dedf362035040bbd216bcfc59d764bb3a56ccd8bfe70a9c9bce3456b375a
6
+ metadata.gz: 40a944d8e2289d02368c1f05c98ef3b26612f40c01c0aa761c110e7f46664b8411f11c7881152b32cc4aa3d1c4db20d726906fdc00ef69c5d362779cd5300e40
7
+ data.tar.gz: 6477d716ebf27e2b6b7a8a5e15c4ce2ec205e7f089b277002fb5f1ab5273d1e486f2248625571038475fac55e91d81fa77378094615727d0bccae970251b1f74
data/LICENSE.txt ADDED
@@ -0,0 +1,8 @@
1
+ Copyright 2023 XBE LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ ## Summarize Meeting
2
+
3
+ Automatically generate a meeting summary from a meeting transcript using OpenAI's ChapGPT API.
4
+
5
+ ## Usage
6
+
7
+ Install the gem.
8
+
9
+ `gem install summarize-meeting`
10
+
11
+ ### Commands
12
+
13
+ Input a meeting transcript and output a summary.
14
+
15
+ #### Usage
16
+
17
+ `summarize-meeting transcript.txt`
18
+
19
+ #### Options
20
+
21
+ `-o, --output-file FILE` - Path to the output file. By default, it will be the `${input-file}-summary.txt`
22
+ `-k, --openai-key KEY` - The OpenAI API Key. Can also be set via `ENV["OPENAI_KEY"]`.
23
+ `-g, --openai-org ORG` - The OpenAI org id. It can also be set via `ENV["OPENAI_ORG"]` .
24
+
25
+ ## Example
26
+
27
+ This is an example summary from a 3,700 word transcript from a 30 minute meeting.
28
+
29
+ > Summary of the Meeting:
30
+ >
31
+ > The meeting started with discussions focusing on individual progress reports, where Sean shared updates on profit-related work, consolidating estimates, and improving Hey Kayla by creating a new JSON structure for Chat GPT messages API. Colenso reported finding a bug in the JavaScript Google Maps API and not making progress on push notifications. Meanwhile, Pankaj worked on bulk assign form handling, retaining time zone, and Benjamin faced some issues regarding database connection but eventually resolved the casting and integration issues.
32
+ >
33
+ > In the open discussion session, the team discussed the confusion over parameters and importing across different branches. Benjamin shared about an open discussion necessary for a feature related to job production plans at ACME. Shirish discussed fixing the cycle time summary query, which used to take up to 6-7 seconds and now takes less than 1 second. Anish added a business unit filter feature to the time card and will work on page titles.
34
+ >
35
+ > The meeting concluded with Sean hoping to have success with profit stuff before Monday's call.
36
+ >
37
+ > Attendees:
38
+ >
39
+ > - Sean
40
+ > - Colenso
41
+ > - Pankaj
42
+ > - Benjamin
43
+ > - Shirish
44
+ > - Anish
45
+ >
46
+ > Action Items:
47
+ >
48
+ > - Sean to continue working on profit-related things before the next call.
49
+ > - Colenso to continue working on resolving the bug in the Google Maps API and push notifications.
50
+ > - Pankaj to continue working on bulk assign form handling and to keep the time zone option.
51
+ > - Benjamin to finalize the release notes for the open discussion at and to continue working on resolving the database connection issues.
52
+ > - Shirish to further improve the query for cycle time summary and reduce the processing time.
53
+ > - Anish to improve page titles and add filters for the time card.
54
+ >
55
+ > Detailed notes:
56
+ >
57
+ > Sean:
58
+ >
59
+ > - Reported progress on Prophet-related things
60
+ > - Worked on consolidating all effective estimates
61
+ > - Worked on improving Hey Kayla by creating a new JSON structure for Chat GPT messages API
62
+ >
63
+ > Colenso:
64
+ >
65
+ > - Found a bug in the JavaScript Google Maps API
66
+ > - Has not made progress on push notifications yet
67
+ >
68
+ > Pankaj:
69
+ >
70
+ > - Worked on bulk assign form handling
71
+ > - Allowed the option to retain the time zone
72
+ >
73
+ > Benjamin:
74
+ >
75
+ > - Faced difficulty regarding database connection issues in Heroku
76
+ > - Resolved casting issues by switching to a different cash store
77
+ > - Listed an interesting issue regarding split loads with more than one material transaction during the same trip
78
+ > - Mentioned an integration issue with ACME
79
+ > - Shared that there is an open discussion at ACME and he needs assistance with the release notes for a feature related to job production plans
80
+ >
81
+ > Open Discussion:
82
+ >
83
+ > - The team discussed the confusion over parameters and importing across different branches.
84
+ >
85
+ > Shirish:
86
+ >
87
+ > - Discussed fixing the cycle time summary query, which used to take up to 6-7 seconds and now takes less than 1 second
88
+ >
89
+ > Anish:
90
+ >
91
+ > - Added a business unit filter feature to the time card
92
+ > - Will work on improving page titles
93
+
94
+ ## Credits
95
+
96
+ This gem was created by XBE.
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "optparse"
4
-
5
- require_relative "../lib/meeting"
4
+ require "summarize-meeting"
6
5
 
7
6
  def main
8
7
  options = {}
@@ -19,8 +18,8 @@ def main
19
18
  options[:output_file] = file
20
19
  end
21
20
 
22
- if ENV["OPENAI_KEY"]
23
- options[:openai_key] = ENV["OPENAI_KEY"]
21
+ if ENV["OPENAI_API_KEY"]
22
+ options[:openai_key] = ENV["OPENAI_API_KEY"]
24
23
  end
25
24
 
26
25
  opts.on("-k", "--openai-key KEY", "The OpenAI API key to use") do |key|
@@ -36,8 +35,8 @@ def main
36
35
  end
37
36
  end.parse!
38
37
 
39
- Ai.access_token = options[:openai_key] if options[:openai_key]
40
- Ai.organization_id = options[:openai_org] if options[:openai_org]
38
+ SummarizeMeeting::Ai.access_token = options[:openai_key] if options[:openai_key]
39
+ SummarizeMeeting::Ai.organization_id = options[:openai_org] if options[:openai_org]
41
40
 
42
41
  if ARGV.length != 1
43
42
  puts "Error: You must specify a transcript file to summarize."
@@ -47,7 +46,7 @@ def main
47
46
  transcript_file = ARGV[0]
48
47
  transcript = File.read(transcript_file)
49
48
 
50
- meeting = Meeting.new(transcript)
49
+ meeting = SummarizeMeeting::Meeting.new(transcript)
51
50
  summary = meeting.summarize
52
51
  summary_file_name = if options[:output_file]
53
52
  options[:output_file]
@@ -59,6 +58,6 @@ def main
59
58
  File.write(summary_file_name, summary)
60
59
  end
61
60
 
62
- if __FILE__ == $0
61
+ if __FILE__.to_s.end_with?("summarize-meeting") && $0.to_s.end_with?("summarize-meeting")
63
62
  main
64
63
  end
@@ -0,0 +1,28 @@
1
+ require "openai"
2
+
3
+ module SummarizeMeeting
4
+ module Ai
5
+ @@access_token = ENV["OPENAI_KEY"]
6
+ @@organization_id = ENV["OPENAI_ORG"]
7
+
8
+ def self.client
9
+ OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
10
+ end
11
+
12
+ def self.access_token
13
+ @@access_token
14
+ end
15
+
16
+ def self.organization_id
17
+ @@organization_id
18
+ end
19
+
20
+ def self.access_token=(token)
21
+ @@access_token = token
22
+ end
23
+
24
+ def self.organization_id=(id)
25
+ @@organization_id = id
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,130 @@
1
+ require "json"
2
+ require "mustache"
3
+ require "openai"
4
+
5
+ module SummarizeMeeting
6
+ class Meeting
7
+ LINE_SUMMARY_PROMPT_TEMPLATE = [
8
+ {
9
+ role: "system",
10
+ content: "You are an assistant summarizing a meeting.",
11
+ },
12
+ {
13
+ role: "system",
14
+ content: "The transcript of the meeting is split into {{chunkCount}} chunks. This is the {{chunkIndex}} chunk.",
15
+ },
16
+ {
17
+ role: "assistant",
18
+ content: "Please provide me with the next chunk of the transcript.",
19
+ },
20
+ {
21
+ role: "user",
22
+ content: "{{chunk}}",
23
+ }
24
+ ]
25
+
26
+ CONSOLIDATED_SUMMARY_PROMPT_TEMPLATE = [
27
+ {
28
+ role: "system",
29
+ content: "You are an assistant summarizing a meeting.",
30
+ },
31
+ {
32
+ role: "system",
33
+ content: "Notes about the meeting have been compiled.",
34
+ },
35
+ {
36
+ role: "system",
37
+ content: <<~CONTENT
38
+ Your job is to write a thorough summary of the meeting.
39
+ The summary should start with a brief overview of the meeting.
40
+ The summary should be detailed and should extract any action items that were discussed.
41
+ The summary should be organized into sections with headings and bullet points.
42
+ The summary should include a list of attendees.
43
+ The order of the sections should be overview, attendees, action items, and detailed notes by topic.
44
+ CONTENT
45
+ },
46
+ {
47
+ role: "assistant",
48
+ content: "Please provide me with notes from the meeting.",
49
+ },
50
+ {
51
+ role: "user",
52
+ content: "{{notes}}",
53
+ }
54
+ ]
55
+
56
+ def initialize(transcript)
57
+ @transcript = transcript
58
+ end
59
+
60
+ attr_reader :transcript
61
+
62
+ def summarize
63
+
64
+ # Step 1. Split the transcript into lines.
65
+ lines = transcript.split("\n")
66
+
67
+ # Step 2. Calculate the maximum chunk size in words.
68
+ max_total_tokens = 4000
69
+ response_token_reserve = 500
70
+ template_tokens = LINE_SUMMARY_PROMPT_TEMPLATE.map { |line| line[:content].split.size }.sum
71
+ max_chunk_tokens = max_total_tokens - response_token_reserve - template_tokens
72
+ words_per_token = 0.7
73
+ max_chunk_word_count = max_chunk_tokens * words_per_token
74
+
75
+ # Step 3. Split the transcript into equally sized chunks.
76
+ chunks = split_lines_into_equal_size_chunks(lines, max_chunk_word_count)
77
+
78
+ # Step 4. Summarize each chunk.
79
+ previous_chunks_summary = ""
80
+ chunks.each_with_index do |chunk, chunk_index|
81
+ chunk_summary = summarize_chunk(chunk, chunk_index, chunks.size, previous_chunks_summary)
82
+ previous_chunks_summary += chunk_summary
83
+ end
84
+
85
+ # Step 5. Write a consolidated summary.
86
+ consolidated_template = CONSOLIDATED_SUMMARY_PROMPT_TEMPLATE
87
+ prompt = Mustache.render(consolidated_template.to_json, { notes: previous_chunks_summary.to_json })
88
+ messages = JSON.parse(prompt)
89
+ response = SummarizeMeeting::Ai.client.chat(
90
+ parameters: {
91
+ model: "gpt-3.5-turbo",
92
+ messages: messages,
93
+ }
94
+ )
95
+ response.dig("choices", 0, "message", "content")
96
+ end
97
+
98
+ def summarize_chunk(chunk, chunk_index, chunk_count, previous_chunks_summary)
99
+ template = LINE_SUMMARY_PROMPT_TEMPLATE
100
+ prompt = Mustache.render(template.to_json, { chunkCount: chunk_count, chunkIndex: chunk_index + 1, chunk: chunk.join("\n").to_json })
101
+ messages = JSON.parse(prompt)
102
+
103
+ response = SummarizeMeeting::Ai.client.chat(
104
+ parameters: {
105
+ model: "gpt-3.5-turbo",
106
+ messages: messages,
107
+ }
108
+ )
109
+ response.dig("choices", 0, "message", "content")
110
+ end
111
+
112
+ def split_lines_into_equal_size_chunks(lines, max_chunk_word_count)
113
+ chunks = []
114
+ chunk = []
115
+ chunk_word_count = 0
116
+ lines.each do |line|
117
+ line_word_count = line.split.size
118
+ if chunk_word_count + line_word_count > max_chunk_word_count
119
+ chunks << chunk
120
+ chunk = []
121
+ chunk_word_count = 0
122
+ end
123
+ chunk << line
124
+ chunk_word_count += line_word_count
125
+ end
126
+ chunks << chunk
127
+ chunks
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,3 @@
1
+ module SummarizeMeeting
2
+ VERSION = "0.2.0"
3
+ end
@@ -0,0 +1,6 @@
1
+ require_relative "./summarize-meeting/version"
2
+ require_relative "./summarize-meeting/ai"
3
+ require_relative "./summarize-meeting/meeting"
4
+
5
+ module SummarizeMeeting
6
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: summarize-meeting
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Devine
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
- name: dotenv
28
+ name: ruby-openai
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: ruby-openai
42
+ name: mustache
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -53,13 +53,13 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: mustache
56
+ name: rspec
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
61
  version: '0'
62
- type: :runtime
62
+ type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
@@ -67,7 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
69
  - !ruby/object:Gem::Dependency
70
- name: rspec
70
+ name: guard-rspec
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
@@ -81,7 +81,7 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: guard-rspec
84
+ name: vcr
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
@@ -95,7 +95,7 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: vcr
98
+ name: webmock
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -109,7 +109,7 @@ dependencies:
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
111
  - !ruby/object:Gem::Dependency
112
- name: webmock
112
+ name: dotenv
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - ">="
@@ -130,16 +130,21 @@ executables:
130
130
  extensions: []
131
131
  extra_rdoc_files: []
132
132
  files:
133
+ - LICENSE.txt
134
+ - README.md
133
135
  - bin/summarize-meeting
134
- - lib/ai.rb
135
- - lib/meeting.rb
136
+ - lib/summarize-meeting.rb
137
+ - lib/summarize-meeting/ai.rb
138
+ - lib/summarize-meeting/meeting.rb
139
+ - lib/summarize-meeting/version.rb
136
140
  homepage:
137
- licenses: []
141
+ licenses:
142
+ - MIT
138
143
  metadata: {}
139
144
  post_install_message:
140
145
  rdoc_options: []
141
146
  require_paths:
142
- - "."
147
+ - lib
143
148
  required_ruby_version: !ruby/object:Gem::Requirement
144
149
  requirements:
145
150
  - - ">="
data/lib/ai.rb DELETED
@@ -1,26 +0,0 @@
1
- require "openai"
2
-
3
- module Ai
4
- @@access_token = ENV["OPENAI_KEY"]
5
- @@organization_id = ENV["OPENAI_ORG"]
6
-
7
- def self.client
8
- OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
9
- end
10
-
11
- def self.access_token
12
- @@access_token
13
- end
14
-
15
- def self.organization_id
16
- @@organization_id
17
- end
18
-
19
- def self.access_token=(token)
20
- @@access_token = token
21
- end
22
-
23
- def self.organization_id=(id)
24
- @@organization_id = id
25
- end
26
- end
data/lib/meeting.rb DELETED
@@ -1,130 +0,0 @@
1
- require "json"
2
- require "mustache"
3
- require "openai"
4
-
5
- require_relative "./ai"
6
-
7
- class Meeting
8
- LINE_SUMMARY_PROMPT_TEMPLATE = [
9
- {
10
- role: "system",
11
- content: "You are an assistant summarizing a meeting.",
12
- },
13
- {
14
- role: "system",
15
- content: "The transcript of the meeting is split into {{chunkCount}} chunks. This is the {{chunkIndex}} chunk.",
16
- },
17
- {
18
- role: "assistant",
19
- content: "Please provide me with the next chunk of the transcript.",
20
- },
21
- {
22
- role: "user",
23
- content: "{{chunk}}",
24
- }
25
- ]
26
-
27
- CONSOLIDATED_SUMMARY_PROMPT_TEMPLATE = [
28
- {
29
- role: "system",
30
- content: "You are an assistant summarizing a meeting.",
31
- },
32
- {
33
- role: "system",
34
- content: "Notes about the meeting have been compiled.",
35
- },
36
- {
37
- role: "system",
38
- content: <<~CONTENT
39
- Your job is to write a thorough summary of the meeting.
40
- The summary should start with a brief overview of the meeting.
41
- The summary should be detailed and should extract any action items that were discussed.
42
- The summary should be organized into sections with headings and bullet points.
43
- The summary should include a list of attendees.
44
- The order of the sections should be overview, attendees, action items, and detailed notes by topic.
45
- CONTENT
46
- },
47
- {
48
- role: "assistant",
49
- content: "Please provide me with notes from the meeting.",
50
- },
51
- {
52
- role: "user",
53
- content: "{{notes}}",
54
- }
55
- ]
56
-
57
- def initialize(transcript)
58
- @transcript = transcript
59
- end
60
-
61
- attr_reader :transcript
62
-
63
- def summarize
64
-
65
- # Step 1. Split the transcript into lines.
66
- lines = transcript.split("\n")
67
-
68
- # Step 2. Calculate the maximum chunk size in words.
69
- max_total_tokens = 4000
70
- response_token_reserve = 500
71
- template_tokens = LINE_SUMMARY_PROMPT_TEMPLATE.map { |line| line[:content].split.size }.sum
72
- max_chunk_tokens = max_total_tokens - response_token_reserve - template_tokens
73
- words_per_token = 0.7
74
- max_chunk_word_count = max_chunk_tokens * words_per_token
75
-
76
- # Step 3. Split the transcript into equally sized chunks.
77
- chunks = split_lines_into_equal_size_chunks(lines, max_chunk_word_count)
78
-
79
- # Step 4. Summarize each chunk.
80
- previous_chunks_summary = ""
81
- chunks.each_with_index do |chunk, chunk_index|
82
- chunk_summary = summarize_chunk(chunk, chunk_index, chunks.size, previous_chunks_summary)
83
- previous_chunks_summary += chunk_summary
84
- end
85
-
86
- # Step 5. Write a consolidated summary.
87
- consolidated_template = CONSOLIDATED_SUMMARY_PROMPT_TEMPLATE
88
- prompt = Mustache.render(consolidated_template.to_json, { notes: previous_chunks_summary.to_json })
89
- messages = JSON.parse(prompt)
90
- response = Ai.client.chat(
91
- parameters: {
92
- model: "gpt-3.5-turbo",
93
- messages: messages,
94
- }
95
- )
96
- response.dig("choices", 0, "message", "content")
97
- end
98
-
99
- def summarize_chunk(chunk, chunk_index, chunk_count, previous_chunks_summary)
100
- template = LINE_SUMMARY_PROMPT_TEMPLATE
101
- prompt = Mustache.render(template.to_json, { chunkCount: chunk_count, chunkIndex: chunk_index + 1, chunk: chunk.join("\n").to_json })
102
- messages = JSON.parse(prompt)
103
-
104
- response = Ai.client.chat(
105
- parameters: {
106
- model: "gpt-3.5-turbo",
107
- messages: messages,
108
- }
109
- )
110
- response.dig("choices", 0, "message", "content")
111
- end
112
-
113
- def split_lines_into_equal_size_chunks(lines, max_chunk_word_count)
114
- chunks = []
115
- chunk = []
116
- chunk_word_count = 0
117
- lines.each do |line|
118
- line_word_count = line.split.size
119
- if chunk_word_count + line_word_count > max_chunk_word_count
120
- chunks << chunk
121
- chunk = []
122
- chunk_word_count = 0
123
- end
124
- chunk << line
125
- chunk_word_count += line_word_count
126
- end
127
- chunks << chunk
128
- chunks
129
- end
130
- end