diff --git a/src/builders/openai_insert.cr b/src/builders/openai_insert.cr index a610b74..49021a2 100644 --- a/src/builders/openai_insert.cr +++ b/src/builders/openai_insert.cr @@ -4,8 +4,7 @@ require "colorize" module Builder class OpenAIInsert - alias Message = NamedTuple(role: String, content: String) - alias Chat = Array(Message) + alias InsertBuffer = NamedTuple(before: String, after: String) getter verbose : Bool def initialize(@verbose) @@ -14,8 +13,8 @@ module Builder # skip prelude_zone # skip future_zone - def build(prompt : Prompt) : Chat - chat = [] of Message + def build(prompt : Prompt) : InsertBuffer + insert_data : InsertBuffer = { before: "", after: "" } token_limit = 2_900 mandatory_token_count = ( @@ -25,7 +24,7 @@ module Builder ## Build mandatory system messages prompt.system_zone.each do |content| - chat << { role: "system", content: content } + insert_data["before"] += content end ## Build mandatory system messages diff --git a/src/builders/prompt_string.cr b/src/builders/prompt_string.cr index f1a7284..d19b2c2 100644 --- a/src/builders/prompt_string.cr +++ b/src/builders/prompt_string.cr @@ -29,7 +29,7 @@ module Builder end if ! prompt.present_zone.content.empty? - str += "@@current".colorize(:yellow).to_s + # str += "@@current".colorize(:yellow).to_s str += prompt.present_zone.content.join(SEPARATOR).colorize(:light_cyan).to_s end diff --git a/src/main.cr b/src/main.cr index 483fa43..251a1f6 100644 --- a/src/main.cr +++ b/src/main.cr @@ -8,199 +8,10 @@ require "./parsers/prompt_string" require "./builders/prompt_string" require "./builders/openai_chat" require "./builders/openai_insert" +require "./storyteller" -class Storyteller - - enum OpenAIMode - Chat - Complete - Insert - - def self.from_s(str_mode) - return OpenAIMode.values.find do |openai_mode| - openai_mode.to_s.downcase == str_mode.downcase - end - end - end - - def initialize() - end - - def self.start(argv) - input_file = STDIN - input_file_path = "" - output_file = STDOUT - output_file_path = "" - past_characters_limit = 1000 - use_color = true - make_request = true - gpt_mode = OpenAIMode::Chat - verbose = false - gpt_temperature = 0.82 - gpt_presence_penalty = 1 - gpt_frequency_penalty = 1 - gpt_max_tokens = 256 - - parser = OptionParser.new do |parser| - parser.banner = "Usage: storyteller [options]" - - parser.separator("Options:") +storyteller = Storyteller.new() +storyteller.prepare() +storyteller.run() - parser.on("-i FILE", "--input=FILE", "Path to input file") do |file| - input_file_path = file - end - - parser.on("-h", "--help", "Show this help") do - puts parser - exit - end - - - parser.on("-n", "--no-color", "Disable color output") do - use_color = false - end - - parser.on("-o FILE", "--output=FILE", "Path to output file") do |file| - use_color = false - output_file_path = file - end - - parser.on("-v", "--verbose", "Be verbose (cumulative)") do - verbose = true - end - - parser.on("--dry-run", "Don't call the API") do - make_request = false - end - - - parser.separator("GPT options") - - parser.on("--gpt-mode MODE", "GPT mode (chat,insert,complete) (default: chat)") do |chosen_mode| - result_mode = OpenAIMode.from_s(chosen_mode.downcase) - if result_mode.nil? - STDERR.puts "ERROR: unknown mode #{chosen_mode}" - exit 1 - end - gpt_mode = result_mode unless result_mode.nil? - end - - parser.on("--gpt-temperature TEMPERATURE", "GPT Temperature") do |temperature| - gpt_temperature = temperature - end - parser.on("--gpt-presence-penalty PENALTY", "GPT Presence Penalty") do |presence_penalty| - gpt_presence_penalty = presence_penalty - end - parser.on("--gpt-frequency-penalty PENALTY", "GPT Frequency Penalty") do |frequency_penalty| - gpt_frequency_penalty = frequency_penalty - end - parser.on("--gpt-max-tokens TOKENS", "GPT Max Tokens") do |max_tokens| - gpt_max_tokens = max_tokens - end - - end - parser.parse(ARGV) - - # Create Storyteller instance - storyteller = Storyteller.new() - - # Read file and initialize zones - if !input_file_path.empty? - # puts "d: Using input file #{input_file_path}" - input_file = File.open(input_file_path) - end - prompt = storyteller.read_file(input_file) - input_file.close - - # Build GPT-3 request - prompt = storyteller.complete(prompt, make_request, verbose) - pp prompt if verbose - exit 0 if !make_request - - if !output_file_path.empty? - # puts "d: Using output file #{input_file_path}" - output_file = File.open(output_file_path, "w") - end - storyteller.write_file(output_file, prompt, use_color) - output_file.close - rescue ex : OptionParser::InvalidOption - STDERR.puts parser - STDERR.puts "\nERROR: #{ex.message}" - exit 1 - end - - def complete(prompt : Prompt, make_request : Bool, verbose : Bool) - builder = Builder::OpenAIChat.new(verbose: verbose) - messages = builder.build(prompt) - - STDERR.puts messages if verbose - - return prompt if !make_request - - channel_ready = Channel(Bool).new - channel_tick = Channel(Bool).new - # sp = Spin.new(0.5, Spinner::Charset[:progress]) - - spawn do - tick = 0 - loop do - tick += 1 - print "." - if tick > (3 * 60) - print "(timeout)" - exit 1 - end - channel_tick.send(true) - sleep 1.seconds - end - end - - spawn do - openai = OpenAI::Client.new(access_token: ENV.fetch("OPENAI_API_KEY")) - result = openai.chat( - "gpt-3.5-turbo", - messages, - { - "temperature" => 0.82, - "presence_penalty" => 1, - "frequency_penalty" => 1, - "max_tokens" => 256 - } - ) - prompt.past_zone.content << "\n" + result.choices.first["message"]["content"] + "\n" - channel_ready.send(true) - end - - # sp.start - channel_ready.receive - channel_ready.close - # sp.stop - - prompt - end - - def read_file(input_file : IO::FileDescriptor) - content = input_file.gets_to_end - - # puts "d: building parser" - parser = Parser::PromptString.new - # puts "d: parsing" - prompt = parser.parse(content) - # pp prompt - end - - def write_file(output_file : IO::FileDescriptor, prompt : Prompt, use_color : Bool) - # STDERR.puts "d: building builder" - builder = Builder::PromptString.new(use_color) - # STDERR.puts "d: building" - text = builder.build(prompt) - output_file.write_string(text.to_slice) - end - - def display_completion(completion : String) - # Code pour afficher la complétion - end -end - -Storyteller.start(ARGV) diff --git a/src/storyteller.cr b/src/storyteller.cr new file mode 100644 index 0000000..1ac2a2a --- /dev/null +++ b/src/storyteller.cr @@ -0,0 +1,216 @@ +enum OpenAIMode + Chat + Complete + Insert + + def self.from_s(str_mode) + return OpenAIMode.values.find do |openai_mode| + openai_mode.to_s.downcase == str_mode.downcase + end + end +end + +class GptConfig + # defaults + property gpt_temperature = 0.82 + property gpt_presence_penalty = 1 + property gpt_frequency_penalty = 1 + property gpt_max_tokens = 256 + property gpt_mode = OpenAIMode::Chat +end + +class AppConfig + property input_file = STDIN + property input_file_path = "" + property output_file = STDOUT + property output_file_path = "" + property past_characters_limit = 1000 + property use_color = true + property make_request = true + property verbose = false + property gpt_config = GptConfig.new +end + +class Storyteller + property config = AppConfig.new + + def initialize() + end + + def prepare() + self._parse_config() + self._parse_command_line(ARGV) + end + + private def _parse_config() + end + + private def _parse_command_line(args) + parser = OptionParser.new do |parser| + parser.banner = "Usage: storyteller [options]" + + parser.separator("Options:") + + parser.on("-i FILE", "--input=FILE", "Path to input file") do |file| + @config.input_file_path = file + end + + parser.on("-h", "--help", "Show this help") do + puts parser + exit + end + + parser.on("-n", "--no-color", "Disable color output") do + @config.use_color = false + end + + parser.on("-o FILE", "--output=FILE", "Path to output file") do |file| + @config.use_color = false + @config.output_file_path = file + end + + parser.on("-v", "--verbose", "Be verbose (cumulative)") do + @config.verbose = true + end + + parser.on("--dry-run", "Don't call the API") do + @config.make_request = false + end + + parser.separator("GPT options") + + parser.on("--gpt-mode MODE", "GPT mode (chat,insert,complete) (default: chat)") do |chosen_mode| + result_mode = OpenAIMode.from_s(chosen_mode.downcase) + if result_mode.nil? + STDERR.puts "ERROR: unknown mode #{chosen_mode}" + exit 1 + end + gpt_mode = result_mode unless result_mode.nil? + end + + parser.on("--gpt-temperature TEMPERATURE", "GPT Temperature") do |temperature| + gpt_temperature = temperature + end + parser.on("--gpt-presence-penalty PENALTY", "GPT Presence Penalty") do |presence_penalty| + gpt_presence_penalty = presence_penalty + end + parser.on("--gpt-frequency-penalty PENALTY", "GPT Frequency Penalty") do |frequency_penalty| + gpt_frequency_penalty = frequency_penalty + end + parser.on("--gpt-max-tokens TOKENS", "GPT Max Tokens") do |max_tokens| + gpt_max_tokens = max_tokens + end + + end + parser.parse(args) + end + + def load_file() + # Read file and initialize zones + if !@config.input_file_path.empty? + # puts "d: Using input file #{input_file_path}" + @config.input_file = File.open(@config.input_file_path) + end + prompt = self.read_file(@config.input_file) + @config.input_file.close + prompt + end + + def run() + original_prompt = self.load_file() + + # Build GPT-3 request + prompt = self.complete(original_prompt, @config.make_request, @config.verbose) + pp prompt if @config.verbose + exit 0 if !@config.make_request + + if !@config.output_file_path.empty? + # puts "d: Using output file #{input_file_path}" + @config.output_file = File.open(@config.output_file_path, "w") + end + self.write_file(@config.output_file, prompt, @config.use_color) + @config.output_file.close + + # rescue ex : OptionParser::InvalidOption + # STDERR.puts @parser + # STDERR.puts "\nERROR: #{ex.message}" + # exit 1 + + # rescue ex: OpenAI::Client::ClientError + # STDERR.puts "ERROR: #{ex.message}" + # exit 1 + end + + def complete(prompt : Prompt, make_request : Bool, verbose : Bool) + builder = Builder::OpenAIChat.new(verbose: verbose) + messages = builder.build(prompt) + + STDERR.puts messages if verbose + + return prompt if !make_request + + channel_ready = Channel(Bool).new + channel_tick = Channel(Bool).new + # sp = Spin.new(0.5, Spinner::Charset[:progress]) + + spawn do + tick = 0 + loop do + tick += 1 + print "." + if tick > (3 * 60) + print "(timeout)" + exit 1 + end + # channel_tick.send(true) + sleep 1.seconds + end + end + + spawn do + openai = OpenAI::Client.new(access_token: ENV.fetch("OPENAI_API_KEY")) + result = openai.chat( + "gpt-3.5-turbo", + messages, + { + "temperature" => 0.82, + "presence_penalty" => 1, + "frequency_penalty" => 1, + "max_tokens" => 256 + } + ) + prompt.present_zone.content << result.choices.first["message"]["content"] + channel_ready.send(true) + end + + # sp.start + channel_ready.receive + channel_ready.close + # sp.stop + + prompt + end + + def read_file(input_file : IO::FileDescriptor) + content = input_file.gets_to_end + + # puts "d: building parser" + parser = Parser::PromptString.new + # puts "d: parsing" + prompt = parser.parse(content) + # pp prompt + end + + def write_file(output_file : IO::FileDescriptor, prompt : Prompt, use_color : Bool) + # STDERR.puts "d: building builder" + builder = Builder::PromptString.new(use_color) + # STDERR.puts "d: building" + text = builder.build(prompt) + output_file.write_string(text.to_slice) + end + + def display_completion(completion : String) + # Code pour afficher la complétion + end +end +