word_smith 0.1.4

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7dc1f6956a9ee72a61859869da8f0413fe26fdc42ad306e68dbce28f184cbb55
4
+ data.tar.gz: 1c8563f63c8ed9febfb1a332d4fbf69400642590ed86b658675c233a58c4c4c9
5
+ SHA512:
6
+ metadata.gz: 4888cd0c6ebabdd9ba9416aa6d4a8decc4647b89db7d2c5edcbb404b0670e773ed51717821c597d648a058d8b01bb3b53006f30f87405d9e3a9ba7cd12f48de9
7
+ data.tar.gz: 490683d03531c59041cecd173bf0b1b03c69136debd215e6b6bf2f022df3c2e87b8cddeeffbf508b0e8449994b6c9eaf706a5fb9786a888d2f5e5d7a8447da01
data/bin/ws ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../lib/index.rb"
4
+
5
+ WordSmith::run(ARGV)
@@ -0,0 +1,156 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require_relative '../../version'
6
+ require_relative '../services/open_a_i'
7
+ require_relative '../helpers/str'
8
+ require_relative 'translation'
9
+ require 'sorbet-runtime'
10
+
11
+ module WordSmith
12
+ module CommandRunner
13
+ class ArgumentError < StandardError; end
14
+ class BadPermutationOfArgumentsError < ArgumentError; end
15
+
16
+ class << self
17
+ extend T::Sig
18
+
19
+ EXECUTABLE_NAME = 'ws'
20
+ OPENAI_API_KEY_COMMAND = '--set-openai-api-key'
21
+ OPENAI_ORG_ID_COMMAND = '--set-openai-org-id'
22
+
23
+ class Options < T::Struct
24
+ prop :no_cache, T::Boolean
25
+ prop :file_path, T.nilable(String)
26
+ prop :target_language, T.nilable(String)
27
+ end
28
+
29
+ sig { params(args: T::Array[String]).void }
30
+ def run(args)
31
+ args_clone = args.clone
32
+ options = Options.new(no_cache: false, file_path: nil, target_language: nil)
33
+
34
+ parser = OptionParser.new do |opts|
35
+ opts.banner = "Usage: #{EXECUTABLE_NAME} [word] [options...]"
36
+
37
+ opts.on('-f', '--file [FILE_PATH]', 'Read words from a file') do |file_path|
38
+ options.file_path = file_path
39
+ end
40
+
41
+ opts.on('--target-language [LANGUAGE_CODE]',
42
+ 'If you want to translate the word to a specific language, specify the language. e.g: Spanish') do |target_language|
43
+ options.target_language = target_language
44
+ end
45
+
46
+ opts.on("#{OPENAI_API_KEY_COMMAND} [key]", 'Set the OpenAI API key') do |key|
47
+ store_open_a_i_api_key(key)
48
+
49
+ exit
50
+ end
51
+
52
+ opts.on("#{OPENAI_ORG_ID_COMMAND} [key]", 'Set the OpenAI Org ID') do |key|
53
+ store_open_a_i_org_id(key)
54
+
55
+ exit
56
+ end
57
+
58
+ opts.on('--no-cache', 'Translate word without using cache') do
59
+ options.no_cache = true
60
+ end
61
+
62
+ opts.on_tail('-h', '--help', 'Show help') do
63
+ print_help(opts)
64
+
65
+ exit
66
+ end
67
+
68
+ opts.on_tail('-v', '--version', 'Show version') do
69
+ print_version
70
+
71
+ exit
72
+ end
73
+ end
74
+
75
+ parser.parse!(args_clone)
76
+
77
+ input_text = args_clone.join(' ').chomp
78
+
79
+ if !input_text.empty? && !options.file_path.nil?
80
+ raise BadPermutationOfArgumentsError, 'Both word and file path cannot be provided'
81
+ end
82
+
83
+ raise ArgumentError, 'No word or file path provided' if input_text.empty? && options.file_path.nil?
84
+
85
+ translation_options = {
86
+ no_cache: options.no_cache,
87
+ target_language: options.target_language
88
+ }
89
+
90
+ unless options.file_path.nil?
91
+ raise ArgumentError, "File not found: #{options.file_path}" unless File.exist?(options.file_path)
92
+
93
+ File.readlines(T.must(options.file_path)).each_with_index do |line, index|
94
+ puts Rainbow('-' * 60).yellow.bright unless index.zero?
95
+
96
+ Translation.run(line, translation_options)
97
+ end
98
+
99
+ return
100
+ end
101
+
102
+ Translation.run(input_text, translation_options)
103
+ end
104
+
105
+ private
106
+
107
+ sig { params(opts: OptionParser).void }
108
+ def print_help(opts)
109
+ puts Helpers::Str.lstr_every_line("
110
+ Translate words, phrases and words in context of a sentence.
111
+
112
+ -> To translate a word or a phrase, just run:
113
+ #{Rainbow(EXECUTABLE_NAME + ' [word/phrase]').blue.bold}
114
+
115
+ -> To translate a word in context of a sentence, you should write the sentence and wrap the word in slashes:
116
+ #{Rainbow(EXECUTABLE_NAME + ' a /random/ sentence').blue.bold}
117
+ In this example, the word 'random' will be translated in context of the sentence.
118
+
119
+ #{Rainbow('----------------------------------------').yellow.bright}
120
+ ")
121
+
122
+ puts '', opts
123
+
124
+ return unless WordSmith::Services::OpenAI.api_key.nil?
125
+
126
+ return unless WordSmith::Services::OpenAI.api_key.nil?
127
+
128
+ open_a_i_message = T.let("
129
+ To use OpenAI, you need to set an API key and Org ID.
130
+ You can set the API key using '#{EXECUTABLE_NAME} #{OPENAI_API_KEY_COMMAND} <key>'
131
+ You can set the Org ID using '#{EXECUTABLE_NAME} #{OPENAI_ORG_ID_COMMAND} <key>'
132
+ ", String)
133
+ puts '', WordSmith::Helpers::Str.lstr_every_line(open_a_i_message)
134
+ end
135
+
136
+ sig { void }
137
+ def print_version
138
+ puts "Version #{WordSmith::VERSION}"
139
+ end
140
+
141
+ sig { params(key: String).void }
142
+ def store_open_a_i_api_key(key)
143
+ WordSmith::Services::OpenAI.store_api_key(key)
144
+
145
+ puts 'OpenAI API key set!'
146
+ end
147
+
148
+ sig { params(key: String).void }
149
+ def store_open_a_i_org_id(key)
150
+ WordSmith::Services::OpenAI.store_org_id(key)
151
+
152
+ puts 'OpenAI Org ID set!'
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,123 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require_relative '../services/open_a_i'
6
+ require_relative '../models/word'
7
+ require_relative '../services/logger'
8
+
9
+ module WordSmith
10
+ module CommandRunner
11
+ module Translation
12
+ class << self
13
+ extend T::Sig
14
+
15
+ sig { params(input_text: String, options: { no_cache: T::Boolean, target_language: T.nilable(String) }).void }
16
+ def run(input_text, options)
17
+ is_contextual_translation = input_text.match?(%r{/[a-zA-Z]+/})
18
+
19
+ if is_contextual_translation
20
+ sentence = input_text.gsub(%r{/([a-zA-Z]+)/}, '\1')
21
+
22
+ literal_word_match = input_text.match(%r{/([a-zA-Z]+)/})
23
+ raise "Invalid word: #{input_text}" if literal_word_match.nil?
24
+
25
+ word = T.cast(literal_word_match.captures[0], String)
26
+
27
+ result = find_or_create_contextual_translation(word, sentence, **options)
28
+
29
+ puts '', "In context of \"#{Rainbow(sentence).blue.bold}\":"
30
+
31
+ print_common_parts(result)
32
+
33
+ return
34
+ end
35
+
36
+ word = input_text.chomp
37
+
38
+ result = find_or_create_word_translation(word, **options)
39
+
40
+ print_common_parts(result)
41
+ end
42
+
43
+ private
44
+
45
+ sig do
46
+ params(word: String, no_cache: T::Boolean, target_language: T.nilable(String)).returns(Models::Word)
47
+ end
48
+ def find_or_create_word_translation(word, no_cache: false, target_language: nil)
49
+ existing_word = Models::Word.find_by_word(word)
50
+
51
+ unless no_cache || existing_word.nil?
52
+ Services::Logger.debug_log("Found existing word: #{existing_word.word}")
53
+
54
+ return existing_word
55
+ end
56
+
57
+ Services::Logger.debug_log("Translating the word: #{word}")
58
+
59
+ result = Services::OpenAI.new.translate(text: word, target_language: target_language)
60
+
61
+ if existing_word.nil?
62
+ Services::Logger.debug_log("Creating new word: #{word}")
63
+
64
+ return Models::Word.create(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
65
+ example: result[:example], target_language: target_language,
66
+ translation_to_target_language: result[:translation_to_target_language])
67
+ end
68
+
69
+ Services::Logger.debug_log("Updating existing word: #{word}")
70
+
71
+ existing_word.update(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
72
+ example: result[:example], target_language: target_language,
73
+ translation_to_target_language: result[:translation_to_target_language])
74
+ end
75
+
76
+ sig do
77
+ params(word: String, sentence: String, no_cache: T::Boolean,
78
+ target_language: T.nilable(String)).returns(Models::Word)
79
+ end
80
+ def find_or_create_contextual_translation(word, sentence, no_cache: false, target_language: nil)
81
+ existing_word = Models::Word.find_by_word(word)
82
+
83
+ unless no_cache || existing_word.nil?
84
+ Services::Logger.debug_log("Found existing word: #{existing_word.word}")
85
+
86
+ return existing_word
87
+ end
88
+
89
+ Services::Logger.debug_log("Translating the word: #{word}")
90
+
91
+ result = Services::OpenAI.new.translate_in_context_of_sentence(word: word, sentence: sentence,
92
+ target_language: target_language)
93
+
94
+ if existing_word.nil?
95
+ Services::Logger.debug_log("Creating new word: #{word}")
96
+
97
+ return Models::Word.create(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
98
+ example: result[:example], target_language: target_language,
99
+ translation_to_target_language: result[:translation_to_target_language])
100
+ end
101
+
102
+ Services::Logger.debug_log("Updating existing word: #{word}")
103
+
104
+ existing_word.update(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
105
+ example: result[:example], target_language: target_language,
106
+ translation_to_target_language: result[:translation_to_target_language])
107
+ end
108
+
109
+ sig { params(word: Models::Word).void }
110
+ def print_common_parts(word)
111
+ puts '', "#{Rainbow(word.word).green.bold}#{Rainbow(" (#{word.pronunciation})").blue}", "\n"
112
+ puts " #{Rainbow('Meaning:').bold}#{Rainbow(" #{word.meaning}").blue}", "\n"
113
+ puts " #{Rainbow('Example:').bold}#{Rainbow(" #{word.example}").blue}", "\n"
114
+
115
+ return if word.translation_to_target_language.nil?
116
+
117
+ puts " #{Rainbow('Translation to target language:').bold}#{Rainbow(" #{word.translation_to_target_language}").blue}",
118
+ "\n"
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
data/lib/config.rb ADDED
@@ -0,0 +1,5 @@
1
+ module WordSmith
2
+ module Config
3
+ DEBUG_MODE = ENV['DEBUG'] == 'true'
4
+ end
5
+ end
@@ -0,0 +1,14 @@
1
+ module WordSmith
2
+ module Helpers
3
+ module LogoVisualizer
4
+ def self.draw
5
+ puts Rainbow("
6
+ ▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▄▄▄ ▗▄▄▖▗▖ ▗▖▗▄▄▄▖▗▄▄▄▖▗▖ ▗▖
7
+ ▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ █ ▐▌ ▐▛▚▞▜▌ █ █ ▐▌ ▐▌
8
+ ▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▌ █ ▝▀▚▖▐▌ ▐▌ █ █ ▐▛▀▜▌
9
+ ▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▙▄▄▀ ▗▄▄▞▘▐▌ ▐▌▗▄█▄▖ █ ▐▌ ▐▌
10
+ ").blue.bold
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module WordSmith
7
+ module Helpers
8
+ module Str
9
+ extend T::Sig
10
+
11
+ sig { params(string: String).returns(String) }
12
+ def self.lstr_every_line(string)
13
+ string.split("\n").map(&:lstrip).join("\n")
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/index.rb ADDED
@@ -0,0 +1,34 @@
1
+ # typed: true
2
+
3
+ require_relative 'command_runner/index'
4
+ require_relative 'services/logger'
5
+ require_relative 'migrations/index'
6
+ require_relative 'helpers/logo_visualizer'
7
+ module WordSmith
8
+ class << self
9
+ extend T::Sig
10
+
11
+ sig { params(args: T::Array[String]).void }
12
+ def run(args)
13
+ Helpers::LogoVisualizer.draw
14
+
15
+ run_migrations
16
+
17
+ CommandRunner.run(args)
18
+ rescue CommandRunner::ArgumentError => e
19
+ puts e.message
20
+
21
+ exit 1
22
+ end
23
+
24
+ private
25
+
26
+ def run_migrations
27
+ Services::Logger.debug_log('Running migrations...')
28
+
29
+ Migrations.run
30
+
31
+ Services::Logger.debug_log('Migrations complete.')
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../services/db'
5
+
6
+ module WordSmith
7
+ module Migrations
8
+ class Words
9
+ def self.up
10
+ Services::DB.instance.execute <<-SQL
11
+ CREATE TABLE IF NOT EXISTS words (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ word TEXT NOT NULL,
14
+ pronunciation TEXT NOT NULL,
15
+ meaning TEXT NOT NULL,
16
+ example TEXT NOT NULL,
17
+ context TEXT DEFAULT NULL,
18
+ target_language TEXT DEFAULT NULL,
19
+ translation_to_target_language TEXT DEFAULT NULL,
20
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
21
+ )
22
+ SQL
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+
6
+ module WordSmith
7
+ module Migrations
8
+ class << self
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def run
13
+ Dir.glob(File.join(__dir__, './*.rb')).each do |file| # rubocop:disable Lint/NonDeterministicRequireOrder
14
+ next if file == __FILE__
15
+
16
+ require_relative file
17
+ end
18
+
19
+ WordSmith::Migrations.constants.each do |constant|
20
+ WordSmith::Migrations.const_get(constant).up
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,134 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../services/db'
5
+ require 'sorbet-runtime'
6
+
7
+ module WordSmith
8
+ module Models
9
+ class Word
10
+ extend T::Sig
11
+
12
+ sig { returns(Integer) }
13
+ attr_reader :id
14
+
15
+ sig { returns(String) }
16
+ attr_reader :word
17
+
18
+ sig { returns(String) }
19
+ attr_reader :pronunciation
20
+
21
+ sig { returns(String) }
22
+ attr_reader :meaning
23
+
24
+ sig { returns(String) }
25
+ attr_reader :example
26
+
27
+ sig { returns(T.nilable(String)) }
28
+ attr_reader :context
29
+
30
+ sig { returns(T.nilable(String)) }
31
+ attr_reader :target_language
32
+
33
+ sig { returns(T.nilable(String)) }
34
+ attr_reader :translation_to_target_language
35
+
36
+ sig do
37
+ params(id: Integer, word: String, pronunciation: String, meaning: String, example: String,
38
+ context: T.nilable(String), target_language: T.nilable(String),
39
+ translation_to_target_language: T.nilable(String)).void
40
+ end
41
+ def initialize(
42
+ id:,
43
+ word:,
44
+ pronunciation:,
45
+ meaning:,
46
+ example:,
47
+ context: nil,
48
+ target_language: nil,
49
+ translation_to_target_language: nil
50
+ )
51
+ @id = id
52
+ @word = word
53
+ @pronunciation = pronunciation
54
+ @meaning = meaning
55
+ @example = example
56
+ @context = context
57
+ @target_language = target_language
58
+ @translation_to_target_language = translation_to_target_language
59
+ end
60
+
61
+ sig { void }
62
+ def delete
63
+ Services::DB.instance.execute('DELETE FROM words WHERE id = ?', [@id])
64
+ end
65
+
66
+ sig do
67
+ params(word: String, pronunciation: String, meaning: String, example: String,
68
+ context: T.nilable(String), target_language: T.nilable(String),
69
+ translation_to_target_language: T.nilable(String)).returns(Word)
70
+ end
71
+ def update(word:, pronunciation:, meaning:, example:, context: nil, target_language: nil,
72
+ translation_to_target_language: nil)
73
+ result = Services::DB.instance.execute('UPDATE words SET word = ?, pronunciation = ?, meaning = ?, example = ?, context = ?, target_language = ?, translation_to_target_language = ? WHERE id = ? RETURNING *',
74
+ [word, pronunciation, meaning, example, context, target_language,
75
+ translation_to_target_language, @id]).first
76
+
77
+ @word = result[1]
78
+ @pronunciation = result[2]
79
+ @meaning = result[3]
80
+ @example = result[4]
81
+ @context = result[5]
82
+ @target_language = result[6]
83
+ @translation_to_target_language = result[7]
84
+
85
+ self
86
+ end
87
+
88
+ class << self
89
+ extend T::Sig
90
+
91
+ sig { returns(T::Array[Word]) }
92
+ def all
93
+ Services::DB.instance.execute('SELECT id, word, pronunciation, meaning, example, context, target_language, translation_to_target_language FROM words').map do |row|
94
+ new(id: row[0], word: row[1], pronunciation: row[2], meaning: row[3], example: row[4], context: row[5],
95
+ target_language: row[6], translation_to_target_language: row[7])
96
+ end
97
+ end
98
+
99
+ sig do
100
+ params(word: String, pronunciation: String, meaning: String, example: String,
101
+ context: T.nilable(String), target_language: T.nilable(String),
102
+ translation_to_target_language: T.nilable(String)).returns(Word)
103
+ end
104
+ def create(word:, pronunciation:, meaning:, example:, context: nil, target_language: nil,
105
+ translation_to_target_language: nil)
106
+ result = Services::DB.instance.execute('INSERT INTO words (word, pronunciation, meaning, example, context, target_language, translation_to_target_language) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *',
107
+ [word, pronunciation, meaning, example, context, target_language,
108
+ translation_to_target_language]).first
109
+
110
+ new(id: result[0], word: result[1], pronunciation: result[2], meaning: result[3], example: result[4],
111
+ context: result[5], target_language: result[6], translation_to_target_language: result[7])
112
+ end
113
+
114
+ sig { params(id: Integer).returns(Word) }
115
+ def find(id)
116
+ Services::DB.instance.execute('SELECT id, word, pronunciation, meaning, example, context, target_language, translation_to_target_language FROM words WHERE id = ?',
117
+ [id]).map do |row|
118
+ new(id: row[0], word: row[1], pronunciation: row[2], meaning: row[3], example: row[4], context: row[5],
119
+ target_language: row[6], translation_to_target_language: row[7])
120
+ end.first
121
+ end
122
+
123
+ sig { params(word: String).returns(T.nilable(Word)) }
124
+ def find_by_word(word)
125
+ Services::DB.instance.execute('SELECT id, word, pronunciation, meaning, example, context, target_language, translation_to_target_language FROM words WHERE word = ?',
126
+ [word]).map do |row|
127
+ new(id: row[0], word: row[1], pronunciation: row[2], meaning: row[3], example: row[4], context: row[5],
128
+ target_language: row[6], translation_to_target_language: row[7])
129
+ end.first
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sqlite3'
5
+ require 'sorbet-runtime'
6
+ require 'singleton'
7
+
8
+ module WordSmith
9
+ module Services
10
+ class DB < SQLite3::Database
11
+ extend T::Sig
12
+ include Singleton
13
+
14
+ DB_FILE = File.join(File.dirname(__FILE__), '../..', 'db', 'storage.db')
15
+
16
+ sig { void }
17
+ def initialize
18
+ Dir.mkdir(File.dirname(DB_FILE)) unless Dir.exist?(File.dirname(DB_FILE))
19
+
20
+ @db = super(DB_FILE)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'rainbow'
6
+
7
+ module WordSmith
8
+ module Services
9
+ module Logger
10
+ class << self
11
+ extend T::Sig
12
+
13
+ sig { params(message: String).void }
14
+ def debug_log(message)
15
+ return unless Config::DEBUG_MODE
16
+
17
+ puts Rainbow('DEBUG:').white.bg(:yellow) + " #{message}"
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,173 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'sorbet-runtime'
5
+ require 'openai'
6
+ require 'json'
7
+
8
+ require_relative '../config'
9
+
10
+ module WordSmith
11
+ module Services
12
+ class OpenAI
13
+ class << self
14
+ extend T::Sig
15
+
16
+ # Storage methods
17
+
18
+ OPEN_AI_API_KEY_FILE = File.join(File.dirname(__FILE__), '../../', '.openai_api_key')
19
+ OPEN_AI_ORG_ID_FILE = File.join(File.dirname(__FILE__), '../../', '.openai_org_id')
20
+
21
+ sig { params(key: String).void }
22
+ def store_api_key(key)
23
+ File.write(OPEN_AI_API_KEY_FILE, key)
24
+ end
25
+
26
+ sig { returns(T.nilable(String)) }
27
+ def api_key
28
+ return nil unless File.exist?(OPEN_AI_API_KEY_FILE)
29
+
30
+ File.read(OPEN_AI_API_KEY_FILE)
31
+ end
32
+
33
+ sig { params(key: String).void }
34
+ def store_org_id(key)
35
+ File.write(OPEN_AI_ORG_ID_FILE, key)
36
+ end
37
+
38
+ sig { returns(T.nilable(String)) }
39
+ def org_id
40
+ return nil unless File.exist?(OPEN_AI_ORG_ID_FILE)
41
+
42
+ File.read(OPEN_AI_ORG_ID_FILE)
43
+ end
44
+ end
45
+
46
+ extend T::Sig
47
+
48
+ class OpenAIKeyNotSetError < StandardError; end
49
+ class OpenAIOrgIDNotSetError < StandardError; end
50
+
51
+ def initialize
52
+ raise OpenAIKeyNotSetError if OpenAI.api_key.nil?
53
+ raise OpenAIOrgIDNotSetError if OpenAI.org_id.nil?
54
+
55
+ ::OpenAI.configure do |config|
56
+ config.access_token = OpenAI.api_key
57
+ config.organization_id = OpenAI.org_id
58
+ config.log_errors = Config::DEBUG_MODE
59
+ end
60
+
61
+ @client = ::OpenAI::Client.new
62
+ end
63
+
64
+ sig do
65
+ params(text: String, target_language: T.nilable(String)).returns({ word: String, pronunciation: String,
66
+ meaning: String, example: String,
67
+ translation_to_target_language: T.nilable(String) })
68
+ end
69
+ def translate(text:, target_language:)
70
+ response = @client.chat(
71
+ parameters: {
72
+ model: 'gpt-4o-mini',
73
+ response_format: { type: 'json_schema',
74
+ json_schema: {
75
+ name: 'Translation',
76
+ strict: true,
77
+ schema: {
78
+ type: 'object',
79
+ properties: {
80
+ word: { type: 'string' },
81
+ pronunciation: { type: 'string' },
82
+ meaning: { type: 'string' },
83
+ example: { type: 'string' },
84
+ translation_to_target_language: { type: %w[string null] }
85
+ },
86
+ additionalProperties: false,
87
+ required: %w[word pronunciation meaning example translation_to_target_language]
88
+ }
89
+ } },
90
+ messages: [
91
+ {
92
+ role: 'system', content: Helpers::Str.lstr_every_line("
93
+ You are a great translator. Give the user the meaning of the word in English.
94
+ Also give the user an example of the sentence using the word.
95
+ #{get_target_language_prompt(target_language)}
96
+ Do not hallucinate.
97
+ Be to the point and concise without an
98
+ ")
99
+ },
100
+ {
101
+ role: 'user', content: text
102
+ }
103
+ ],
104
+ temperature: 0.7
105
+ }
106
+ )
107
+
108
+ JSON.parse(response.dig('choices', 0, 'message', 'content'), { symbolize_names: true })
109
+ end
110
+
111
+ sig do
112
+ params(sentence: String,
113
+ word: String,
114
+ target_language: T.nilable(String)).returns({ word: String, pronunciation: String, meaning: String,
115
+ example: String })
116
+ end
117
+ def translate_in_context_of_sentence(sentence:, word:, target_language:)
118
+ response = @client.chat(
119
+ parameters: {
120
+ model: 'gpt-4o-mini',
121
+ response_format: { type: 'json_schema',
122
+ json_schema: {
123
+ name: 'Translation',
124
+ strict: true,
125
+ schema: {
126
+ type: 'object',
127
+ properties: {
128
+ word: { type: 'string' },
129
+ pronunciation: { type: 'string' },
130
+ meaning: { type: 'string' },
131
+ example: { type: 'string' },
132
+ translation_to_target_language: { type: %w[string null] }
133
+ },
134
+ additionalProperties: false,
135
+ required: %w[word pronunciation meaning example]
136
+ }
137
+ } },
138
+ messages: [
139
+ {
140
+ role: 'system', content: Helpers::Str.lstr_every_line("
141
+ You are a great translator. Give the user the meaning of the word in context of the sentence in English.
142
+ Also give the user an example of the sentence using the meaning of the word in that context.
143
+ #{get_target_language_prompt(target_language)}
144
+ Do not hallucinate.
145
+ Be to the point and concise without an
146
+ ")
147
+ },
148
+ {
149
+ role: 'user', content: Helpers::Str.lstr_every_line("
150
+ Sentence: #{sentence}
151
+ Word: #{word}
152
+ ")
153
+ }
154
+ ],
155
+ temperature: 0.7
156
+ }
157
+ )
158
+
159
+ JSON.parse(response.dig('choices', 0, 'message', 'content'), { symbolize_names: true })
160
+ end
161
+
162
+ private
163
+
164
+ def get_target_language_prompt(target_language)
165
+ return '' if target_language.nil?
166
+
167
+ Helpers::Str.lstr_every_line("
168
+ Also, please give me the literal translation of the word in #{target_language} language.
169
+ ")
170
+ end
171
+ end
172
+ end
173
+ end
data/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module WordSmith
2
+ VERSION = '0.1.4'
3
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: word_smith
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.4
5
+ platform: ruby
6
+ authors:
7
+ - Pouya Mozaffar Magham
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-09-13 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Command-line tool for quick and easy English word lookup.
14
+ email: pouya.mozafar@gmail.com
15
+ executables:
16
+ - ws
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/ws
21
+ - lib/command_runner/index.rb
22
+ - lib/command_runner/translation.rb
23
+ - lib/config.rb
24
+ - lib/helpers/logo_visualizer.rb
25
+ - lib/helpers/str.rb
26
+ - lib/index.rb
27
+ - lib/migrations/1_words.rb
28
+ - lib/migrations/index.rb
29
+ - lib/models/word.rb
30
+ - lib/services/db.rb
31
+ - lib/services/logger.rb
32
+ - lib/services/open_a_i.rb
33
+ - version.rb
34
+ homepage: https://github.com/pmzi/WordSmith
35
+ licenses:
36
+ - MIT
37
+ metadata: {}
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.5.11
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: MR Word Smith!
57
+ test_files: []