1
0
mirror of https://github.com/H0llyW00dzZ/GoGenAI-Terminal-Chat.git synced 2025-02-11 14:40:48 +00:00
H0llyW00dzZ 46f7076546
[Go Lint] Remove Unused Parameter (#401)
- [+] refactor(ai_stuff.go, commands.go, genai.go): rename customText to SummaryPrefix in handleAIResponse
- [+] fix(ai_stuff.go): remove session parameter from handleTokenCount
- [+] fix(commands.go): adjust handleTokenCount call to match new parameter list
- [+] fix(genai.go): remove unused client parameter from sendToAIWithoutDisplay
2024-03-02 18:47:07 +07:00

499 lines
20 KiB
Go

// Copyright (c) 2024 H0llyW00dzZ
//
// License: MIT License
package terminal
import (
"fmt"
"os"
"strconv"
"strings"
"github.com/H0llyW00dzZ/GoGenAI-Terminal-Chat/terminal/fun_stuff"
"github.com/H0llyW00dzZ/GoGenAI-Terminal-Chat/terminal/tools"
)
// Execute gracefully terminates the chat session. It sends a shutdown message to the AI,
// prints a farewell message to the user, and signals that the session should end. This method
// is the designated handler for the ":quit" command.
//
// Parameters:
//
// session *Session: The current chat session, which provides context and state for the operation.
// parts []string: The slice containing the command and its arguments.
//
// Returns:
//
// bool: Always returns true to indicate that the session should be terminated.
// error: Returns an error if one occurs during the shutdown message transmission; otherwise, nil.
//
// The method sends a formatted shutdown message to the AI, which includes the entire chat history
// for context. If an error occurs during message transmission, it is logged. The method then prints
// a predefined shutdown message and invokes a session cleanup function.
//
// Note: The function assumes the presence of constants for the shutdown message format (ContextPromptShutdown)
// and a predefined shutdown message (ShutdownMessage). It relies on the session's endSession method to perform
// any necessary cleanup. The method's return value of true indicates to the calling code that the session loop
// should exit and the application should terminate.
func (q *handleQuitCommand) Execute(session *Session, parts []string) (bool, error) {
if err := sendShutdownMessage(session); err != nil {
logger.Error(ErrorFailedToSendShutdownMessage, err)
}
// Proceed with shutdown regardless of the error
fmt.Println(ShutdownMessage)
session.endSession() // End the session and perform cleanup
return true, nil
}
// Execute processes the ":help" command within a chat session. It constructs a help prompt
// that includes a list of available commands and sends it to the generative AI model for a response.
// The AI's response, which contains information on how to use the commands, is then logged.
//
// This method provides the AI with the session's current chat history for context, ensuring
// the help message is relevant to the state of the conversation. If an error occurs during
// message transmission, it is logged.
//
// The method assumes the presence of a HelpCommandPrompt constant that contains the format
// string for the AI's help prompt, as well as constants for the various commands (e.g.,
// QuitCommand, VersionCommand, HelpCommand).
//
// Parameters:
//
// session *Session: the current chat session, which contains state information such as the chat history
// and the generative AI client.
// parts []string: The slice containing the command and its arguments.
//
// Returns:
//
// bool: Indicates whether the command was successfully handled. It returns false to continue the session.
// error: Any error that occurs during the version check or message sending process.
//
// Note: The method does not add the AI's response to the chat history to avoid potential
// loops in the AI's behavior.
func (cmd *handleHelpCommand) Execute(session *Session, parts []string) (bool, error) {
return executeCommand(session, HelpCommand, func(cmd string) string {
// Note: This a better fmt formatting unlike 'C' or 'RUST' hahahaha
return fmt.Sprintf(HelpCommandPrompt,
ApplicationName,
cmd,
QuitCommand,
ShortQuitCommand,
HelpCommand,
ShortHelpCommand,
VersionCommand,
SafetyCommand,
Low, Default, High, Unspecified, None,
AITranslateCommand,
LangArgs,
CryptoRandCommand,
LengthArgs,
SummarizeCommands,
SwitchModelCommands,
GeminiPro, GeminiProTuning, GeminiProLatest,
ChatCommands,
ShowCommands,
ChatHistoryArgs,
StatsCommand,
ChatCommands,
ClearCommand,
SummarizeCommands,
ClearCommand,
ChatCommands,
CheckModelCommands,
TokenCountCommands,
FileCommands)
})
}
// Execute checks if the current version of the software is the latest and informs the user accordingly.
// If the current version is not the latest, it retrieves and provides release notes for the latest version.
// This method uses the session's chat history for context and sends an appropriate message to the generative
// AI model for a response.
//
// Parameters:
//
// session *Session: The current session containing the chat history and other relevant context.
// parts []string: The slice containing the command and its arguments.
//
// Returns:
//
// bool: Indicates whether the command was successfully handled. It returns false to continue the session.
// error: Any error that occurs during the version check or message sending process.
//
// Note: This method does not terminate the session. It is designed to be used with `RenewSession` if needed,
// to ensure that the session state is correctly maintained. The method assumes the presence of constants
// for formatting messages to the AI (YouAreUsingLatest and ReleaseNotesPrompt) and relies on external
// functions (CheckLatestVersion and GetFullReleaseInfo) to determine version information and fetch release details.
func (c *handleCheckVersionCommand) Execute(session *Session, parts []string) (bool, error) {
// Pass ContextPrompt 🤪
// Add messages to the chat history to provide context for the version check.
session.ChatHistory.AddMessage(AiNerd, ContextPrompt, session.ChatConfig)
session.ChatHistory.AddMessage(YouNerd, VersionCommand, session.ChatConfig)
// Check if the current version is the latest and get the prompt for the AI.
aiPrompt, err := c.checkVersionAndGetPrompt()
if err != nil {
// Log errors related to version checking and Google API issues.
logger.Error(ErrorFailedTosendmessagesToAI, err)
logger.HandleGoogleAPIError(err)
return false, err
}
// Sanitize the AI prompt to ensure it is safe to send.
sanitizedMessage := session.ChatHistory.SanitizeMessage(aiPrompt)
// Define a retryable operation with a function that sends the version check message to the AI.
operation := RetryableOperation{
retryFunc: func() (bool, error) {
// Fix Duplicated by using Magic "_" Identifier
// Send the message to the AI, discarding the response since it's not needed here.
_, err := session.SendMessage(session.Ctx, session.Client, sanitizedMessage)
// If there's no error, the operation is successful.
return err == nil, err
},
}
// Execute the retryable operation with an exponential backoff strategy.
success, err := operation.retryWithExponentialBackoff(standardAPIErrorHandler)
if err != nil {
// If an error occurs that is not recoverable by retries, log it and return the error.
logger.Error(ErrorFailedToSendVersionCheckMessage, err)
return false, err
}
if !success {
// If the operation was not successful after retries, return an error.
return false, fmt.Errorf(ErrorFailedToSendVersionCheckMessageAfterReties)
}
// Indicate that the command was handled; return false to continue the session.
return false, nil
}
// Execute performs the ping operation on the provided IP address.
// It uses system utilities to send ICMP packets to the IP and returns the result.
//
// session *Session: The current chat session containing state and context, including the AI client.
// parts []string: The slice containing the command and its arguments.
//
// Returns true if the ping command was executed, and an error if there was an issue executing the command.
func (cmd *handlepingCommand) Execute(session *Session, parts []string, subcommand string) (bool, error) {
// Note: WIP
// Validate the command arguments.
if !cmd.IsValid(parts) {
logger.Error(ErrorWhileTypingCommandArgs, subcommand, parts)
return true, nil
}
ip := parts[1]
_, err := fun_stuff.PingIP(ip)
if err != nil {
logger.Error(ErrorPingFailed, err)
}
return false, nil
}
// Execute clears the chat history if the command is valid.
//
// session *Session: The current chat session containing state and context.
// parts []string: The slice containing the command and its arguments.
//
// Returns true if the clear command was executed, and an error if there was an issue executing the command.
func (cmd *handleClearCommand) Execute(session *Session, parts []string) (bool, error) {
// Heads-Up: The current implementation is sleek and storage-agnostic, but beware of the ever-lurking feature creep!
// Future enhancements might include targeted message purges—think selective user word-bombs or a full-on message-specific snipe hunt.
// But let's cross that bridge when we get to it. For now, we revel in the simplicity of our logic. Stay tuned, fellow code whisperers! 😜
// Note: This place only, for commands doesn't have any subcommands/args, so it will return error hahaha
return cmd.HandleSubcommand("", session, parts) // Continue the session
}
func (cmd *handleClearCommand) HandleSubcommand(subcommand string, session *Session, parts []string) (bool, error) {
// Handle the subcommands of the clear command
switch subcommand {
case ChatCommands:
return cmd.clearChatHistory(session)
case SummarizeCommands:
return cmd.clearSummarizeHistory(session)
default:
// Handle unrecognized subcommand
logger.Error(ErrorWhileTypingCommandArgs, subcommand, parts)
return false, nil
}
}
// Execute processes the ":safety" command within a chat session.
//
// Note: The flexibility demonstrated in this function is quite powerful. In many programming languages,
// changing safety settings would typically require constructing and parsing JSON structures for each request.
// However, Go's type system allows us to elegantly manipulate these settings directly through struct methods,
// bypassing the need for repetitive JSON serialization and deserialization hahaha.
func (cmd *handleSafetyCommand) Execute(session *Session, parts []string) (bool, error) {
// Continue the session after setting safety levels
return cmd.HandleSubcommand("", session, parts) // Continue the session
}
func (cmd *handleSafetyCommand) HandleSubcommand(subcommand string, session *Session, parts []string) (bool, error) {
// Note: The code in "safety_settings.go" employs advanced idiomatic Go practices. 🤪
// Caution is advised: if you're not familiar with these practices, improper handling in this "Execute" could lead to frequent panics 24/7 🤪.
if !cmd.IsValid(parts) {
logger.Error(ErrorWhileTypingCommandArgs, subcommand, parts)
return false, nil
}
// Set the safety level based on the command argument.
cmd.setSafetyLevel(session, parts[1])
// Apply the updated safety settings and notify the user.
// Note: It should be working now. If it still doesn't work, this may indicate a problem with your machine hahaha.
session.SafetySettings.ApplyToModel(session.Client.GenerativeModel(GeminiPro), GeminiPro)
// Pass ContextPrompt
session.ChatHistory.AddMessage(AiNerd, ContextPrompt, session.ChatConfig)
logger.Any(fmt.Sprintf(SystemSafety, parts[1])) // simplify
return false, nil
}
// Execute processes the ":aitranslate" command within a chat session.
func (cmd *handleAITranslateCommand) Execute(session *Session, parts []string) (bool, error) {
// Ensure that the command is valid before proceeding.
if !cmd.IsValid(parts) {
// Log the error with the logger instead of returning fmt.Errorf
logger.Error(ErrorWhileTypingCommandArgs, AITranslateCommand, parts)
return false, nil // Return nil error because the logger already handled it
}
// Find the index of the language flag ":lang" to separate text and target language.
languageFlagIndex := len(parts) - 2
textToTranslate := strings.Join(parts[1:languageFlagIndex], " ")
targetLanguage := parts[languageFlagIndex+1]
aiPrompt := constructAITranslatePrompt(ApplicationName, AITranslateCommand, textToTranslate, targetLanguage)
err := handleAIInteraction(session, aiPrompt, func(session *Session, aiResponse string) error {
// Add a message to the chat history indicating the translation command was invoked
translationCommandMessage := fmt.Sprintf(ContextUserInvokeTranslateCommands, targetLanguage, textToTranslate)
session.ChatHistory.AddMessage(YouNerd, translationCommandMessage, session.ChatConfig)
return postProcessAITranslate(session, aiResponse)
})
if err != nil {
logger.Error(ErrorFailedToSendTranslationMessage, err)
return false, err
}
// Indicate that the command was handled; return false to continue the session.
return false, nil
}
// Execute processes the ":cryptorand" command within a chat session.
func (cmd *handleCryptoRandCommand) Execute(session *Session, parts []string) (bool, error) {
// Continue the session without performing any action.
return cmd.HandleSubcommand("", session, parts) // Continue the session
}
func (cmd *handleCryptoRandCommand) HandleSubcommand(subcommand string, session *Session, parts []string) (bool, error) {
// Check if there are enough parts to contain the length argument.
if len(parts) < 3 {
logger.Error(ErrorWhileTypingCommandArgs, subcommand, parts)
return false, nil
}
lengthStr := parts[2] // The length argument is now the second part of the command
length, err := strconv.Atoi(lengthStr)
if err != nil {
logger.Error(ErrorInvalidLengthArgs, err)
return false, fmt.Errorf(errorinvalidlengthArgs, err)
}
randomString, err := tools.GenerateRandomString(length)
if err != nil {
logger.Error(ErrorFailedtoGenerateRandomString, err)
return false, fmt.Errorf(errorfailedtogeneraterandomstring, err)
}
logger.Any(CryptoRandRes, lengthStr, randomString)
return false, nil
}
// Execute displays the entire chat history.
//
// session *Session: The current chat session containing state and context.
// parts []string: The slice containing the command and its arguments.
//
// Returns false to indicate the session should continue, and an error if there is an issue.
func (cmd *handleShowChatCommand) Execute(session *Session, parts []string) (bool, error) {
// Return false to indicate the session should continue.
return cmd.HandleSubcommand("", session, parts) // Continue the session
}
func (cmd *handleShowChatCommand) HandleSubcommand(subcommand string, session *Session, parts []string) (bool, error) {
// Check if there are enough parts to contain the length argument.
if len(parts) < 3 {
logger.Error(ErrorWhileTypingCommandArgs, subcommand, parts)
return false, nil
}
// Retrieve and log the entire chat history.
history := session.ChatHistory.GetHistory(session.ChatConfig)
logger.Info(ShowChatHistory, history)
return false, nil // Return false to indicate the session should continue.
}
// Execute processes the ":summarize" command within a chat session.
func (h *handleSummarizeCommand) Execute(session *Session, parts []string) (bool, error) {
// Add a message to the chat history indicating the summarize command was invoked
session.ChatHistory.AddMessage(YouNerd, SummarizeCommands, session.ChatConfig)
// Check if there are system messages in the chat history before summarizing.
if session.ChatHistory.HasSystemMessages() {
// Remove system messages from the chat history.
session.ChatHistory.ClearAllSystemMessages()
}
// Define the summarize prompt to be sent to the AI.
aiPrompt := h.constructSummarizePrompt()
// Sanitize the message before sending it to the AI
sanitizedMessage := session.ChatHistory.SanitizeMessage(aiPrompt)
success, err := h.sendSummarizePrompt(session, sanitizedMessage)
if err != nil {
logger.Error(ErrorFailedToSendSummarizeMessage, err)
return false, err
}
if !success {
return false, fmt.Errorf(ErrorFailedToSendSummarizeMessageAfterRetries)
}
// Indicate that the command was handled successfully; return false to continue the session.
return false, nil
}
// Execute processes the main command for handleStatsCommand. Since handleStatsCommand
// is implemented with subcommands, this method does not perform any action and simply
// returns false and nil to indicate that the session should continue without error.
// The actual command logic is delegated to the HandleSubcommand method.
func (cmd *handleStatsCommand) Execute(session *Session, parts []string) (bool, error) {
// Continue the session without performing any action.
return cmd.HandleSubcommand("", session, parts) // Continue the session
}
// HandleSubcommand dispatches the handling of specific subcommands for the stats command.
// It takes a subcommand string, the current session, and the command parts as arguments.
// Based on the subcommand, it calls the appropriate method to handle it.
// If the subcommand is not recognized, it logs an error and continues the session.
func (cmd *handleStatsCommand) HandleSubcommand(subcommand string, session *Session, parts []string) (bool, error) {
// Dispatch handling based on the subcommand.
switch subcommand {
case ChatCommands:
// Handle the ':chat' subcommand to show chat statistics.
return cmd.showChatStats(session)
default:
// Log an error for unrecognized subcommands and continue the session.
logger.Error(ErrorWhileTypingCommandArgs, subcommand, parts)
return false, nil
}
}
func (cmd *handleTokeCountingCommand) Execute(session *Session, parts []string) (bool, error) {
// Continue the session
return cmd.HandleSubcommand("", session, parts)
}
func (cmd *handleTokeCountingCommand) HandleSubcommand(subcommand string, session *Session, parts []string) (bool, error) {
if !cmd.IsValid(parts) {
logger.Error(ErrorWhileTypingCommandArgs, subcommand, parts)
return false, nil
}
// The file paths start from index 2
filePaths := parts[2:]
apiKey := os.Getenv(APIKey) // Retrieve the API_KEY from the environment
switch subcommand {
case FileCommands:
return cmd.handleTokenCount(apiKey, filePaths)
default:
// Log an error for unrecognized subcommands and continue the session.
logger.Error(ErrorUnrecognizedSubcommandForTokenCount, subcommand)
return false, nil
}
}
func (cmd *handleCheckModelCommand) Execute(session *Session, parts []string) (bool, error) {
// Validate the command arguments.
if !cmd.IsValid(parts) {
logger.Error(ErrorWhileTypingCommandArgs, CheckModelCommands, parts)
return false, nil
}
modelName := parts[1] // The model name is the second part.
// Define a retryable operation for retrieving model info.
operation := RetryableOperation{
retryFunc: func() (bool, error) {
// Note: this a magic method
modelInfo, err := session.Client.EmbeddingModel(modelName).Info(session.Ctx)
if err != nil {
// Log the error and decide if it's worth retrying based on the error type.
logger.Error(ErrorFailedToRetriveModelInfo, err)
return false, err
}
// Process and display the model information if retrieval was successful.
DisplayModelInfo(modelInfo)
return true, nil
},
}
// Execute the retryable operation with an exponential backoff strategy.
success, err := operation.retryWithExponentialBackoff(standardAPIErrorHandler)
if err != nil || !success {
logger.Error("%s", err)
}
// Return false to indicate the session should continue.
return false, nil
}
// isValidModelName checks if the provided model name is supported.
func isValidModelName(modelName string) (bool, error) {
if valid, exists := supportedModels[modelName]; exists && valid {
return true, nil
}
// If the model name is not found or not valid, return an error.
return false, fmt.Errorf(ErrorUnsupportedModelName, modelName)
}
// Execute changes the current AI model used in the session to the one specified in the command.
func (cmd *handleSwitchModelCommand) Execute(session *Session, parts []string) (bool, error) {
// Check if the command is valid.
if !cmd.IsValid(parts) {
logger.Error(ErrorWhileTypingCommandArgs, SwitchModelCommands, parts)
return false, nil
}
// Extract the model name from the command parts.
modelName := parts[1]
// Validate the model name.
valid, err := isValidModelName(modelName)
if !valid {
// Log the error using logger.Error and return the error message.
logger.Error(err.Error())
return false, nil // Continue the session
}
// Update the session with the new model name.
session.CurrentModelName = modelName
// Log a confirmation message using the logger.
logger.Any(SwitchedModel, modelName)
return false, nil // Continue the session.
}