diff --git a/README.md b/README.md index 74193c9..f5b3096 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,76 @@ # go-ai-cli -go-ai-cli is a command-line interface that provides access to OpenAI's GPT-3 language generation service. With this tool, users can send a prompt to the OpenAI API and receive a generated response, which can then be printed on the command-line or saved to a markdown file. +`go-ai-cli` is a versatile command-line interface that enables users to interact with various AI models for text generation, speech-to-text conversion, image generation, and web scraping. It is designed to be extensible with any AI service. This tool is ideal for developers, content creators, and anyone interested in leveraging AI capabilities directly from their terminal. This tool works well with Ollama. -This project is useful for quickly generating text for various purposes such as creative writing, chatbots, virtual assistants, or content generation for websites. +## Features + +- **Text Generation**: Utilize OpenAI's GPT-3 for generating text based on prompts. +- **Speech to Text**: Convert spoken language into text. +- **Image Generation**: Create images from textual descriptions. +- **Modular Design**: Easily extendable to incorporate additional AI models and services. ## Installation -To install and use the go-ai-cli with golang : +### Using Go ```sh go install github.com/MohammadBnei/go-ai-cli@latest ``` -To install the compiled binaries, go to the the [release page](https://github.com/MohammadBnei/go-ai-cli/releases/) and select the exec matching your operating system. +with portaudio (recommended) : -Lastly, there is an unstable docker image. To run it, here is the code : ```sh -docker run -e OPENAI_KEY= -it mohammaddocker/go-ai-cli prompt +go install -tags portaudio github.com/MohammadBnei/go-ai-cli@latest ``` -## Usage -First, set up your OpenAI API key : +### Pre-compiled Binaries + +Download the appropriate binary for your operating system from the [releases page](https://github.com/MohammadBnei/go-ai-cli/releases/). + +## Configuration + +Before using `go-ai-cli`, configure it with your OpenAI API key: + ```sh go-ai-cli config --OPENAI_KEY= ``` -To send a prompt to OpenAI GPT, run: +You can also specify the AI model to use: + ```sh -go-ai-cli prompt +go-ai-cli config --model= ``` -You will be prompted to enter your text. After submitting your prompt, OpenAI will process your input and generate a response. +To list available models: -### Available command in prompt -``` -q: quit -h: help -s: save the response to a file -f: add a file to the messages (won't send to openAi until you send a prompt) -c: clear message list +```sh +go-ai-cli config -l ``` -## Configuration +## Usage -To store your OpenAI API key and model, run the following command: -```sh -go-ai-cli config --OPENAI_KEY= --model= -``` +To start the interactive prompt: -To get a list of available models, run: ```sh -go-ai-cli config -l +go-ai-cli prompt ``` -### Flags -- `--OPENAI_KEY`: Your OpenAI API key. -- `--model`: The default model to use. -- `-l, --list-model`: List available models. -- `--config`: The config file location. +Within the prompt, you have several commands at your disposal: + +- `ctrl+d`: Quit the application. +- `ctrl+h`: Display help information. +- `ctrl+g`: Open options page. +- `ctrl+f`: Add a file to the messages. The file content won't be sent to the model until you submit a prompt. + +## Advanced Configuration -The configuration file is located in `$HOME/.go-ai-cli.yaml`. +The configuration file is located at `$HOME/.go-ai-cli.yaml`. You can customize various settings, including the default AI model and API keys for different services. ## Contributing -To contribute to this project, fork the repository, make your changes, and submit a pull request. Please also ensure that your code adheres to the accepted [Go style guide](https://golang.org/doc/effective_go.html). +Contributions are welcome! Please fork the repository, make your changes, and submit a pull request. Ensure your code follows the [Go style guide](https://golang.org/doc/effective_go.html). ## License -This project is licensed under the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file +`go-ai-cli` is open-source software licensed under the [MIT License](https://opensource.org/licenses/MIT). \ No newline at end of file diff --git a/api/agent/agent_test.go b/api/agent/agent_test.go index cfb1630..fa7ca16 100644 --- a/api/agent/agent_test.go +++ b/api/agent/agent_test.go @@ -4,12 +4,13 @@ import ( "context" "testing" - "github.com/MohammadBnei/go-ai-cli/api" - "github.com/MohammadBnei/go-ai-cli/api/agent" - "github.com/MohammadBnei/go-ai-cli/config" "github.com/spf13/viper" "github.com/tmc/langchaingo/chains" "github.com/tmc/langchaingo/llms/openai" + + "github.com/MohammadBnei/go-ai-cli/api" + "github.com/MohammadBnei/go-ai-cli/api/agent" + "github.com/MohammadBnei/go-ai-cli/config" ) func TestWebSearchAgent(t *testing.T) { diff --git a/api/agent/system_generator.go b/api/agent/system_generator.go new file mode 100644 index 0000000..62d1083 --- /dev/null +++ b/api/agent/system_generator.go @@ -0,0 +1,102 @@ +package agent + +import ( + "context" + + "github.com/tmc/langchaingo/agents" + "github.com/tmc/langchaingo/prompts" + "github.com/tmc/langchaingo/tools" + "github.com/tmc/langchaingo/tools/wikipedia" + + "github.com/MohammadBnei/go-ai-cli/api" +) + +type UserExchangeChans struct { + Out chan string + In chan string +} + +func NewSystemGeneratorExecutor(sgc *UserExchangeChans) (*agents.OpenAIFunctionsAgent, error) { + llm, err := api.GetLlmModel() + if err != nil { + return nil, err + } + wikiTool := wikipedia.New(RandomUserAgent()) + + t := []tools.Tool{ + wikiTool, + } + + if sgc != nil { + t = append(t, NewExchangeWithUser(sgc)) + defer close(sgc.In) + defer close(sgc.Out) + } + + promptTemplate := prompts.NewPromptTemplate(SystemGeneratorPrompt, []string{ + "input", + }) + + executor := agents.NewOpenAIFunctionsAgent(llm, t, + agents.WithPrompt(promptTemplate), + agents.WithReturnIntermediateSteps(), + ) + return executor, nil +} + +type ExchangeWithUser struct { + exchangeChannels *UserExchangeChans +} + +func NewExchangeWithUser(sgc *UserExchangeChans) *ExchangeWithUser { + return &ExchangeWithUser{ + exchangeChannels: sgc, + } +} + +func (e *ExchangeWithUser) Call(ctx context.Context, input string) (string, error) { + e.exchangeChannels.In <- input + return <-e.exchangeChannels.Out, nil +} + +func (e *ExchangeWithUser) Name() string { + return "Exchange With User" +} + +func (e *ExchangeWithUser) Description() string { + return "Exchange With User is a tool designed to help users exchange with the agent. The model can ask a question or a specification to the user and get his response" +} + +var SystemGeneratorPrompt = ` +Your task is to assist users in crafting detailed and effective system prompts that leverage the full capabilities of large language models like GPT. Follow these guidelines meticulously to ensure each generated prompt is tailored, insightful, and maximizes user engagement: + +1. Interpret User Input with Detail: Begin by analyzing the user's request. Pay close attention to the details provided to ensure a deep understanding of their needs. Encourage users to include specific details in their queries to get more relevant answers. + +2. Persona Adoption: Based on the user's request, adopt a suitable persona for responding. This could range from a scholarly persona for academic inquiries to a more casual tone for creative brainstorming sessions. + +3. Use of Delimiters: In your generated prompts, instruct users on the use of delimiters (like triple quotes or XML tags) to clearly separate different parts of their input. This helps in maintaining clarity, especially in complex requests. + +4. Step-by-Step Instructions: Break down tasks into clear, actionable steps. Provide users with a structured approach to completing their tasks, ensuring each step is concise and directly contributes to the overall goal. + +5. Incorporate Examples: Wherever possible, include examples in your prompts. This could be examples of how to structure their request, or examples of similar queries and their outcomes. + +6. Reference Text Usage: Instruct users to provide reference texts when their queries relate to specific information or topics. Guide them on how to ask the model to use these texts to construct answers, ensuring responses are grounded in relevant content. + +7. Citations from Reference Texts: Encourage users to request citations from reference texts for answers that require factual accuracy. This enhances the reliability of the information provided. + +8. Intent Classification: Utilize intent classification to identify the most relevant instructions or responses to a user's query. This ensures that the generated prompts are highly targeted and effective. + +9. Dialogue Summarization: For long conversations or documents, instruct users on how to ask for summaries or filtered dialogue. This helps in maintaining focus and relevance over extended interactions. + +10. Recursive Summarization: Teach users to request piecewise summarization for long documents, constructing a full summary recursively. This method is particularly useful for digesting large volumes of text. + +11. Solution Development: Encourage users to ask the model to 'think aloud' or work out its own solution before providing a final answer. This process helps in revealing the model's reasoning and ensures more accurate outcomes. + +12. Inner Monologue: Instruct users on how to request the model to use an inner monologue or a sequence of queries for complex problem-solving. This hides the model's reasoning process from the user, making the final response more concise. + +13. Review for Omissions: Finally, remind users they can ask the model if it missed anything on previous passes. This ensures comprehensive coverage of the topic at hand. + +By following these guidelines, you will generate system prompts that are not only highly effective but also enhance the user's ability to engage with the model meaningfully. Remember, the goal is to empower users to craft queries that are detailed, structured, and yield the most insightful responses possible. + +When finished, respond only with the system prompt and nothing else. +` diff --git a/api/agent/system_generator_test.go b/api/agent/system_generator_test.go new file mode 100644 index 0000000..5341ea2 --- /dev/null +++ b/api/agent/system_generator_test.go @@ -0,0 +1,52 @@ +package agent_test + +import ( + "context" + "fmt" + "testing" + + "github.com/spf13/viper" + "github.com/tmc/langchaingo/llms" + "github.com/tmc/langchaingo/schema" + + "github.com/MohammadBnei/go-ai-cli/api" + "github.com/MohammadBnei/go-ai-cli/api/agent" + "github.com/MohammadBnei/go-ai-cli/config" +) + +func TestSystemGenerator(t *testing.T) { + viper.Set(config.AI_API_TYPE, api.API_OPENAI) + viper.Set(config.AI_MODEL_NAME, "gpt-4-turbo-preview") + viper.BindEnv(config.AI_OPENAI_KEY, "OPENAI_API_KEY") + + scg := &agent.UserExchangeChans{ + In: make(chan string), + Out: make(chan string), + } + + go func() { + for input := range scg.In { + res := "" + t.Log("Input: " + input) + fmt.Scanln(res) + scg.Out <- res + } + }() + + t.Log("TestSystemGenerator") + executor, err := agent.NewSystemGeneratorExecutor(scg) + if err != nil { + t.Fatal(err) + } + t.Log("Created executor") + + result, err := executor.LLM.GenerateContent(context.Background(), []llms.MessageContent{ + llms.TextParts(schema.ChatMessageTypeSystem, agent.SystemGeneratorPrompt), + llms.TextParts(schema.ChatMessageTypeHuman, "Create a system prompt for golang code generation."), + }) + if err != nil { + t.Fatal(err) + } + + t.Logf("Result: %s", result.Choices[0].Content) +} diff --git a/api/agent/user_agents.go b/api/agent/user_agents.go new file mode 100644 index 0000000..6ef4f75 --- /dev/null +++ b/api/agent/user_agents.go @@ -0,0 +1,112 @@ +package agent + +import ( + "math/rand" +) + +func RandomUserAgent() string { + return userAgentList[rand.Intn(len(userAgentList)-1)] +} + +var userAgentList = []string{ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_15) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5392.175 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.4.263.6 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5367.208 Safari/537.36", + "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5387.128 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786808", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786817", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786823", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786837", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.5.197.2 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5413.94 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5399.203 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786847", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786468", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786802", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786842", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.172.10 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786831", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786811", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786235", + "Mozilla/5.0 (X11; U; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5412.145 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786229", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5407.108 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786215", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786210", + + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36}}l8xqx", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786130", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36list1675786199", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.184.2 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 618", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36%}w1xn2", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786214", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786222", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786241", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675786202", + "http://abpe79b25ysvufijoog62lkvlmrgfe34rwen8bx.oastify.com/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.75 615", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3627288", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3692221", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3647451", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3623358", + "Mozilla/5.0 (X11; U; Linux x86_64) AppleWebKit/537.36 terteefullbead1988 (KHTML, like Gecko) Chrome/106.0.9406.331 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.364411", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3622633", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3682586", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36 Trailer/97.3.7892.93 623", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3699002", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3611840", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3639608", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3665306", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3698600", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.75 622", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3686140", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3669774", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3616088", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.363777", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.3637534", + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5384.135 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.36.922.499 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; Nexus Player Build/PI; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/109.0.5414.117 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5396.154 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5365.118 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5408.170 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5387.155 Safari/537.36", + + "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5388.218 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787136", + "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5400.147 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.5.20.6 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787145", + "Mozilla/5.0 (Windows NT 11.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5406.108 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5365.98 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.1.27.2 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787141", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 615", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.366yz18aeszp", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 619", + "Mozilla/5.0 (X11; U; Linux x86_64) AppleWebKit/537.36 partplyfsatax1980 (KHTML, like Gecko) Chrome/106.0.4722.586 Safari/537.36", + "http://xtu1pwtpnlaic2066bytk82i3993x0lq9iw9qxf.oastify.com/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5414.120 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.5382.175 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5380.154 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5411.106 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.36.689.861 Safari/537.36", + "Mozilla/5.0 (X11; Linux x86_64; XK03H Build/QX; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/109.0.5414.117 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787702", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787718", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787710", + "Mozilla/5.0 (Windows NT 10.0; WOW64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.5400.194 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787711", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787737", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787747", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787742", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787707", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787725", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.361675787731", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Agency/96.8.7147.48 498", + "Mozilla/5.0 (X11; U; Linux x86_64) AppleWebKit/537.36 cornecaltio1974 (KHTML, like Gecko) Chrome/106.0.5098.200 Safari/537.36", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.5304.68 Safari/537.36 623", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.109 Safari/537.36", + "Mozilla/5.0 (X11; U; Linux x86_64) AppleWebKit/537.36 onswinenas1985 (KHTML, like Gecko) Chrome/106.0.2669.508 Safari/537.36", +} diff --git a/api/agent/webSearch.go b/api/agent/webSearch.go index 9985811..0c63422 100644 --- a/api/agent/webSearch.go +++ b/api/agent/webSearch.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" - "github.com/MohammadBnei/go-ai-cli/api" "github.com/chromedp/chromedp" "github.com/gocolly/colly/v2" "github.com/tmc/langchaingo/chains" @@ -16,6 +15,9 @@ import ( "github.com/tmc/langchaingo/schema" "github.com/tmc/langchaingo/tools/scraper" "golang.org/x/sync/errgroup" + + "github.com/MohammadBnei/go-ai-cli/api" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" ) func NewWebSearchAgent(llm llms.Model, urls []string) (func(ctx context.Context, question string) (map[string]any, error), error) { @@ -34,7 +36,7 @@ func NewWebSearchAgent(llm llms.Model, urls []string) (func(ctx context.Context, return err } - parsedData, err := parseHtml(context.Background(), data) + parsedData, err := parseHtml(godcontext.GodContext, data) if err != nil { return err } @@ -58,7 +60,7 @@ func NewWebSearchAgent(llm llms.Model, urls []string) (func(ctx context.Context, stuffQAChain := chains.LoadStuffQA(llm) callFunc := func(ctx context.Context, question string) (map[string]any, error) { - return chains.Call(context.Background(), stuffQAChain, map[string]any{ + return chains.Call(godcontext.GodContext, stuffQAChain, map[string]any{ "input_documents": docs, "question": question, }) @@ -79,12 +81,12 @@ func getHtmlContent(url string) (string, error) { if err != nil { return "", err } - return scrap.Call(context.Background(), url) + return scrap.Call(godcontext.GodContext, url) } func fetchHTML(url string) (string, error) { // Initialize a new browser context - ctx, cancel := chromedp.NewContext(context.Background()) + ctx, cancel := chromedp.NewContext(godcontext.GodContext) defer cancel() // Navigate to the URL and fetch the rendered HTML diff --git a/api/agent/webSearch_test.go b/api/agent/webSearch_test.go index 8fb29ed..2279b31 100644 --- a/api/agent/webSearch_test.go +++ b/api/agent/webSearch_test.go @@ -4,13 +4,21 @@ import ( "context" "testing" + "github.com/spf13/viper" + "github.com/tmc/langchaingo/llms/ollama" + "go.uber.org/goleak" + "github.com/MohammadBnei/go-ai-cli/api" "github.com/MohammadBnei/go-ai-cli/api/agent" "github.com/MohammadBnei/go-ai-cli/config" - "github.com/spf13/viper" - "github.com/tmc/langchaingo/llms/ollama" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" ) +func TestMain(m *testing.M) { + godcontext.GodContext = context.Background() + goleak.VerifyTestMain(m) +} + func TestWebSearch(t *testing.T) { viper.Set(config.AI_API_TYPE, api.API_OLLAMA) viper.Set(config.AI_MODEL_NAME, "llama2") diff --git a/api/config.go b/api/config.go index dbe3d23..532d41b 100644 --- a/api/config.go +++ b/api/config.go @@ -4,18 +4,18 @@ import ( "context" "encoding/json" "errors" - "io" "net/http" - "github.com/MohammadBnei/go-ai-cli/config" "github.com/samber/lo" + openaiHelper "github.com/sashabaranov/go-openai" "github.com/spf13/viper" "github.com/tmc/langchaingo/llms" "github.com/tmc/langchaingo/llms/huggingface" "github.com/tmc/langchaingo/llms/ollama" "github.com/tmc/langchaingo/llms/openai" - openaiHelper "github.com/sashabaranov/go-openai" + "github.com/MohammadBnei/go-ai-cli/config" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" ) type API_TYPE string @@ -81,13 +81,8 @@ func GetOllamaModelList() ([]string, error) { } defer resp.Body.Close() - jsonDataFromHttp, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var jsonData map[string]any - err = json.Unmarshal(jsonDataFromHttp, &jsonData) + err = json.NewDecoder(resp.Body).Decode(&jsonData) if err != nil { return nil, err } @@ -101,7 +96,7 @@ func GetOllamaModelList() ([]string, error) { func GetOpenAiModelList() ([]string, error) { c := openaiHelper.NewClient(viper.GetString(config.AI_OPENAI_KEY)) - models, err := c.ListModels(context.Background()) + models, err := c.ListModels(godcontext.GodContext) if err != nil { return nil, err } diff --git a/api/config_test.go b/api/config_test.go index c0ba59d..ffdf893 100644 --- a/api/config_test.go +++ b/api/config_test.go @@ -1,21 +1,30 @@ package api_test import ( + "context" "testing" - "github.com/MohammadBnei/go-ai-cli/api" - "github.com/MohammadBnei/go-ai-cli/config" "github.com/spf13/viper" "github.com/stretchr/testify/assert" + "go.uber.org/goleak" + + "github.com/MohammadBnei/go-ai-cli/api" + "github.com/MohammadBnei/go-ai-cli/config" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" ) +func TestMain(m *testing.M) { + godcontext.GodContext = context.Background() + goleak.VerifyTestMain(m) +} + func TestGetOllamaModelList(t *testing.T) { // Set the API type to "OLLAMA" viper.Set(config.AI_API_TYPE, api.API_OLLAMA) // Set the OLLAMA_HOST to your test server URL - viper.Set(config.AI_OLLAMA_HOST, "127.0.0.1:11434") + viper.Set(config.AI_OLLAMA_HOST, "http://127.0.0.1:11434") // Call the function models, err := api.GetOllamaModelList() diff --git a/api/openai.go b/api/openai.go index c52eec6..1e85773 100644 --- a/api/openai.go +++ b/api/openai.go @@ -7,10 +7,11 @@ import ( "io" "strings" - "github.com/MohammadBnei/go-ai-cli/config" "github.com/sashabaranov/go-openai" "github.com/spf13/viper" "golang.org/x/sync/errgroup" + + "github.com/MohammadBnei/go-ai-cli/config" ) func SpeechToText(ctx context.Context, filename string, lang string) (string, error) { diff --git a/cmd/prompt.go b/cmd/prompt.go index 0ae5caa..2ec7073 100644 --- a/cmd/prompt.go +++ b/cmd/prompt.go @@ -8,12 +8,13 @@ import ( "log" "path/filepath" - "github.com/MohammadBnei/go-ai-cli/config" - "github.com/MohammadBnei/go-ai-cli/service" - "github.com/MohammadBnei/go-ai-cli/ui/chat" tea "github.com/charmbracelet/bubbletea" "github.com/spf13/cobra" "github.com/spf13/viper" + + "github.com/MohammadBnei/go-ai-cli/config" + "github.com/MohammadBnei/go-ai-cli/service" + "github.com/MohammadBnei/go-ai-cli/ui/chat" ) // promptCmd represents the prompt command @@ -32,6 +33,13 @@ var promptCmd = &cobra.Command{ FileService: fileService, } + defer func() { + if err := recover(); err != nil { + promptConfig.ChatMessages.SaveToFile(filepath.Dir(viper.ConfigFileUsed()) + "/error-chat.yml") + fmt.Println(err) + } + }() + defaulSystemPrompt := viper.GetStringMapString(config.PR_SYSTEM_DEFAULT) savedSystemPrompt := viper.GetStringMapString(config.PR_SYSTEM) for k := range defaulSystemPrompt { diff --git a/default.pgo b/default.pgo new file mode 100644 index 0000000..67ee03a Binary files /dev/null and b/default.pgo differ diff --git a/go.mod b/go.mod index a26dbbd..b38240f 100644 --- a/go.mod +++ b/go.mod @@ -28,11 +28,12 @@ require ( github.com/muesli/reflow v0.3.0 github.com/pkoukk/tiktoken-go v0.1.6 github.com/samber/lo v1.39.0 - github.com/sashabaranov/go-openai v1.20.0 + github.com/sashabaranov/go-openai v1.20.2 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.8.4 github.com/tmc/langchaingo v0.1.5 + go.uber.org/goleak v1.3.0 golang.org/x/sync v0.6.0 golang.org/x/term v0.17.0 gopkg.in/yaml.v3 v3.0.1 @@ -43,7 +44,7 @@ require ( github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect - github.com/PuerkitoBio/goquery v1.9.0 // indirect + github.com/PuerkitoBio/goquery v1.9.1 // indirect github.com/alecthomas/chroma v0.10.0 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/antchfx/htmlquery v1.3.0 // indirect @@ -53,7 +54,7 @@ require ( github.com/aymerick/douceur v0.2.0 // indirect github.com/catppuccin/go v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect - github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e // indirect + github.com/chromedp/cdproto v0.0.0-20240226204813-532e667d868f // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/containerd/console v1.0.4 // indirect @@ -109,7 +110,7 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -124,7 +125,7 @@ require ( github.com/yuin/goldmark-emoji v1.0.2 // indirect go.starlark.net v0.0.0-20240123142251-f86470692795 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.19.0 // indirect + golang.org/x/crypto v0.20.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/exp/shiny v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/image v0.15.0 // indirect diff --git a/go.sum b/go.sum index 519a629..117693f 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= github.com/PuerkitoBio/goquery v1.9.0 h1:zgjKkdpRY9T97Q5DCtcXwfqkcylSFIVCocZmn2huTp8= github.com/PuerkitoBio/goquery v1.9.0/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= +github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= +github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= github.com/TannerKvarfordt/hfapigo v1.3.1 h1:Q0nGBLI3p6kbY/PHdjWFKsXN9HDulPQPSIlACtiZ0rE= github.com/TannerKvarfordt/hfapigo v1.3.1/go.mod h1:QvdRtU6q3i/VURfTwUu/akYX6HtdIGn25t62zFLoXX4= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= @@ -80,6 +82,8 @@ github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9 github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e h1:kXEolCWQZzuEFcuaTzfqXToX+e29OcvK87BcBiBBJ1c= github.com/chromedp/cdproto v0.0.0-20240214232516-ad4608604e9e/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= +github.com/chromedp/cdproto v0.0.0-20240226204813-532e667d868f h1:jODunjTDQHm0Srs2IsfcS3hOmNLUN7Spag3NJZQra2g= +github.com/chromedp/cdproto v0.0.0-20240226204813-532e667d868f/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg= github.com/chromedp/chromedp v0.9.5/go.mod h1:D4I2qONslauw/C7INoCir1BJkSwBYMyZgx8X276z3+Y= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= @@ -351,6 +355,8 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f h1:MvTmaQdww/z0Q4wrYjDSCcZ78NoftLQyHBSLW/Cx79Y= github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= @@ -358,6 +364,8 @@ github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sashabaranov/go-openai v1.20.0 h1:r9WiwJY6Q2aPDhVyfOSKm83Gs04ogN1yaaBoQOnusS4= github.com/sashabaranov/go-openai v1.20.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= +github.com/sashabaranov/go-openai v1.20.2 h1:nilzF2EKzaHyK4Rk2Dbu/aJEZbtIvskDIXvfS4yx+6M= +github.com/sashabaranov/go-openai v1.20.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= @@ -434,6 +442,8 @@ go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.starlark.net v0.0.0-20240123142251-f86470692795 h1:LmbG8Pq7KDGkglKVn8VpZOZj6vb9b8nKEGcg9l03epM= go.starlark.net v0.0.0-20240123142251-f86470692795/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -445,6 +455,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= diff --git a/main.go b/main.go index d53a77f..a54745e 100644 --- a/main.go +++ b/main.go @@ -22,21 +22,37 @@ THE SOFTWARE. package main import ( + "context" "net/http" _ "net/http/pprof" "os" + "runtime/pprof" "strconv" "github.com/MohammadBnei/go-ai-cli/cmd" + godcontext "github.com/MohammadBnei/go-ai-cli/service/godcontext" ) func main() { + godcontext.GodContext, godcontext.GodCancelFn = context.WithCancel(context.Background()) + defer godcontext.GodCancelFn() + debugStr := os.Getenv("DEBUG") debug, _ := strconv.ParseBool(debugStr) if debug { go func() { - http.ListenAndServe(":1234", nil) + http.ListenAndServe(":6060", nil) }() } + + pgoStr := os.Getenv("PGO") + pgo, _ := strconv.ParseBool(pgoStr) + if pgo { + f, _ := os.Create("default.pgo") + defer f.Close() + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + cmd.Execute() } diff --git a/service/chatMessages.go b/service/chatMessages.go index fa26805..79a1e18 100644 --- a/service/chatMessages.go +++ b/service/chatMessages.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "sort" + "strings" "time" "github.com/bwmarrin/snowflake" @@ -81,12 +82,28 @@ func (c *ChatMessages) SaveToFile(filename string) error { return err } - err = tool.SaveToFile(data, filename, false) - if err != nil { - return err + return tool.SaveToFile(data, filename, false) +} + +func (c *ChatMessages) SaveChatInModelfileFormat(filename string) error { + builder := strings.Builder{} + + builder.WriteString(fmt.Sprintf("FROM %s\n\n", viper.GetString(config.AI_MODEL_NAME))) + + for _, m := range c.Messages { + switch m.Role { + case RoleUser: + builder.WriteString("MESSAGE user ") + case RoleAssistant: + builder.WriteString("MESSAGE assistant ") + default: + continue + } + + builder.WriteString(fmt.Sprintf("%s\n\n", m.Content)) } - return nil + return tool.SaveToFile([]byte(builder.String()), filename, false) } func (c *ChatMessages) LoadFromFile(filename string) (err error) { @@ -126,7 +143,18 @@ func (c *ChatMessages) LoadFromFile(filename string) (err error) { func (c *ChatMessages) FindById(id int64) *ChatMessage { _, index, ok := lo.FindIndexOf(c.Messages, func(item ChatMessage) bool { - return item.Id == snowflake.ParseInt64(int64(id)) + return item.Id == snowflake.ParseInt64(id) + }) + if !ok { + return nil + } + + return &c.Messages[index] +} + +func (c *ChatMessages) FindByOrder(order uint) *ChatMessage { + _, index, ok := lo.FindIndexOf(c.Messages, func(item ChatMessage) bool { + return item.Order == order }) if !ok { return nil @@ -167,7 +195,7 @@ func (c *ChatMessages) AddMessage(content string, role ROLES) (*ChatMessage, err return &exists, ErrAlreadyExist } - msg := ChatMessage{ + msg := &ChatMessage{ Id: c.node.Generate(), Role: role, Content: content, @@ -178,11 +206,12 @@ func (c *ChatMessages) AddMessage(content string, role ROLES) (*ChatMessage, err ApiType: viper.GetString(config.AI_API_TYPE), Model: viper.GetString(config.AI_MODEL_NAME), }, + Order: uint(len(c.Messages)), } msg.Tokens = tokenCount - c.Messages = append(c.Messages, msg) + c.Messages = append(c.Messages, *msg) c.TotalTokens += tokenCount @@ -192,7 +221,7 @@ func (c *ChatMessages) AddMessage(content string, role ROLES) (*ChatMessage, err c.SetMessagesOrder() - return &msg, nil + return msg, nil } // AddMessageFromFile reads the content of a file using os.ReadFile, then adds a new message with the file content and filename to the ChatMessages using the AddMessage method. diff --git a/service/config.go b/service/config.go index 77a0bd2..75bf1b2 100644 --- a/service/config.go +++ b/service/config.go @@ -40,10 +40,18 @@ func (pc *PromptConfig) AddContextWithId(ctx context.Context, cancelFn func(), i pc.Contexts = append(pc.Contexts, ContextHold{Ctx: ctx, CancelFn: cancelFn, UserChatId: snowflake.ParseInt64(id)}) } -func (pc *PromptConfig) DeleteContext(ctx context.Context) { +func (pc *PromptConfig) CloseContext(ctx context.Context) error { + ctxHlod, ok := lo.Find(pc.Contexts, func(item ContextHold) bool { return item.Ctx == ctx }) + if !ok { + return errors.New("context not found") + } + ctxHlod.CancelFn() + pc.Contexts = lo.Filter(pc.Contexts, func(item ContextHold, index int) bool { return item.Ctx != ctx }) + + return nil } func (pc *PromptConfig) FindContextWithId(id int64) *ContextHold { @@ -53,12 +61,6 @@ func (pc *PromptConfig) FindContextWithId(id int64) *ContextHold { return &ctx } -func (pc *PromptConfig) DeleteContextById(id int64) { - pc.Contexts = lo.Filter(pc.Contexts, func(item ContextHold, index int) bool { - return item.UserChatId != snowflake.ParseInt64(id) - }) -} - func (pc *PromptConfig) CloseContextById(id int64) error { ctx, _, ok := lo.FindLastIndexOf(pc.Contexts, func(item ContextHold) bool { return item.UserChatId == snowflake.ParseInt64(id) }) if !ok { diff --git a/service/context.go b/service/context.go deleted file mode 100644 index ca08a3d..0000000 --- a/service/context.go +++ /dev/null @@ -1,37 +0,0 @@ -package service - -import ( - "context" - "os" - "os/signal" - - "github.com/samber/lo" -) - -var StopCh = &[]chan os.Signal{} - -func StopSignalFIFO() { - if len(*StopCh) > 0 { - (*StopCh)[0] <- os.Interrupt - } -} - -func LoadContext(ctx context.Context) (context.Context, func()) { - ctx, cancel := context.WithCancel(ctx) - c := make(chan os.Signal, 1) - *StopCh = append(*StopCh, c) - signal.Notify(c, os.Interrupt) - go func() { - _, ok := <-c - if ok { - cancel() - } - }() - return ctx, func() { - signal.Stop(c) - close(c) - *StopCh = lo.Filter[chan os.Signal](*StopCh, func(item chan os.Signal, index int) bool { - return item != c - }) - } -} diff --git a/service/godcontext/context.go b/service/godcontext/context.go new file mode 100644 index 0000000..aa8e497 --- /dev/null +++ b/service/godcontext/context.go @@ -0,0 +1,6 @@ +package godcontext + +import "context" + +var GodContext context.Context +var GodCancelFn context.CancelFunc diff --git a/service/image.go b/service/image.go index 685e53e..347001c 100644 --- a/service/image.go +++ b/service/image.go @@ -1,7 +1,6 @@ package service import ( - "context" "encoding/base64" "fmt" "image" @@ -11,11 +10,13 @@ import ( "os" "time" - "github.com/MohammadBnei/go-ai-cli/config" "github.com/briandowns/spinner" "github.com/c2h5oh/datasize" "github.com/sashabaranov/go-openai" "github.com/spf13/viper" + + "github.com/MohammadBnei/go-ai-cli/config" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" ) func AskImage(prompt string, size string) ([]byte, error) { @@ -23,7 +24,7 @@ func AskImage(prompt string, size string) ([]byte, error) { s := spinner.New(spinner.CharSets[26], 100*time.Millisecond) s.Start() - resp, err := c.CreateImage(context.Background(), openai.ImageRequest{ + resp, err := c.CreateImage(godcontext.GodContext, openai.ImageRequest{ Prompt: prompt, User: "user", @@ -91,7 +92,7 @@ func EditImage(filePath, prompt, size string) ([]byte, error) { s := spinner.New(spinner.CharSets[26], 100*time.Millisecond) s.Start() - resp, err := c.CreateEditImage(context.Background(), openai.ImageEditRequest{ + resp, err := c.CreateEditImage(godcontext.GodContext, openai.ImageEditRequest{ Prompt: prompt, Image: tmpPng, diff --git a/service/virtualFS_test.go b/service/virtualFS_test.go index 08b05b5..3450165 100644 --- a/service/virtualFS_test.go +++ b/service/virtualFS_test.go @@ -1,14 +1,24 @@ package service_test import ( + "context" "io/ioutil" "os" "path/filepath" "testing" + "go.uber.org/goleak" + "github.com/MohammadBnei/go-ai-cli/service" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" ) +func TestMain(m *testing.M) { + godcontext.GodContext = context.Background() + goleak.VerifyTestMain(m) + +} + func TestAppend(t *testing.T) { fs, _ := service.NewFileService() diff --git a/ui/chat/chat.go b/ui/chat/chat.go index b1fe5f5..472137a 100644 --- a/ui/chat/chat.go +++ b/ui/chat/chat.go @@ -8,15 +8,6 @@ import ( "os" "reflect" - "github.com/MohammadBnei/go-ai-cli/config" - "github.com/MohammadBnei/go-ai-cli/service" - "github.com/MohammadBnei/go-ai-cli/ui/audio" - "github.com/MohammadBnei/go-ai-cli/ui/event" - "github.com/MohammadBnei/go-ai-cli/ui/helper" - "github.com/MohammadBnei/go-ai-cli/ui/list" - "github.com/MohammadBnei/go-ai-cli/ui/style" - "github.com/MohammadBnei/go-ai-cli/ui/transition" - "github.com/charmbracelet/bubbles/cursor" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/spinner" "github.com/charmbracelet/bubbles/textarea" @@ -29,6 +20,15 @@ import ( "github.com/spf13/viper" "github.com/tmc/langchaingo/chains" "golang.org/x/term" + + "github.com/MohammadBnei/go-ai-cli/config" + "github.com/MohammadBnei/go-ai-cli/service" + "github.com/MohammadBnei/go-ai-cli/ui/audio" + "github.com/MohammadBnei/go-ai-cli/ui/event" + "github.com/MohammadBnei/go-ai-cli/ui/helper" + "github.com/MohammadBnei/go-ai-cli/ui/list" + "github.com/MohammadBnei/go-ai-cli/ui/style" + "github.com/MohammadBnei/go-ai-cli/ui/transition" ) var ( @@ -100,14 +100,14 @@ func NewChatModel(pc *service.PromptConfig) (*chatModel, error) { // Remove cursor line styling ta.FocusedStyle.CursorLine = lipgloss.NewStyle() + ta.Cursor.Blink = false + ta.KeyMap.InsertNewline.SetEnabled(false) ta.ShowLineNumbers = false vp := viewport.New(w, 0) vp.MouseWheelDelta = 1 - ta.KeyMap.InsertNewline.SetEnabled(false) - mdRenderer, err := glamour.NewTermRenderer(glamour.WithAutoStyle()) if err != nil { return nil, err @@ -153,11 +153,6 @@ func (m chatModel) Init() tea.Cmd { } func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - defer func() { - if r := recover(); r != nil { - m.err = fmt.Errorf("%v", r) - } - }() m.LoadingTitle() var ( tiCmd tea.Cmd @@ -166,13 +161,6 @@ func (m chatModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds := []tea.Cmd{} switch msg := msg.(type) { - case cursor.BlinkMsg: - if len(m.stack) != 0 { - m.textarea.Cursor.Blink = false - return m, nil - } else { - m.textarea.Cursor.Blink = true - } case tea.WindowSizeMsg: m.size.Height = msg.Height m.size.Width = msg.Width @@ -391,9 +379,8 @@ func (m chatModel) LoadingTitle() { func (m chatModel) GetTitleView() string { userPrompt := m.userPrompt - if m.currentChatMessages.user != nil { - _, index, _ := lo.FindIndexOf[service.ChatMessage](m.promptConfig.ChatMessages.Messages, func(c service.ChatMessage) bool { return c.Id == m.currentChatMessages.user.Id }) - userPrompt = fmt.Sprintf("[%d] %s", index+1, userPrompt) + if m.currentChatMessages.user != nil && m.currentChatMessages.user.Order != 0 { + userPrompt = fmt.Sprintf("[%d] %s", m.currentChatMessages.user.Order, userPrompt) } if userPrompt == "" { userPrompt = "Chat" diff --git a/ui/chat/chatUpdate.go b/ui/chat/chatUpdate.go index 49dba2e..8b061b5 100644 --- a/ui/chat/chatUpdate.go +++ b/ui/chat/chatUpdate.go @@ -6,18 +6,21 @@ import ( "fmt" "io" - "github.com/MohammadBnei/go-ai-cli/api" - "github.com/MohammadBnei/go-ai-cli/config" - "github.com/MohammadBnei/go-ai-cli/service" - "github.com/MohammadBnei/go-ai-cli/ui/event" - "github.com/MohammadBnei/go-ai-cli/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/golang-module/carbon" "github.com/samber/lo" "github.com/spf13/viper" "github.com/tmc/langchaingo/chains" "github.com/tmc/langchaingo/llms" "moul.io/banner" + + "github.com/MohammadBnei/go-ai-cli/api" + "github.com/MohammadBnei/go-ai-cli/config" + "github.com/MohammadBnei/go-ai-cli/service" + godcontext "github.com/MohammadBnei/go-ai-cli/service/godcontext" + "github.com/MohammadBnei/go-ai-cli/ui/event" + "github.com/MohammadBnei/go-ai-cli/ui/style" ) type ChatUpdateFunc func(m *chatModel) (tea.Model, tea.Cmd) @@ -49,82 +52,63 @@ func closeContext(m chatModel) (chatModel, tea.Cmd) { } func changeResponseUp(m chatModel) (chatModel, tea.Cmd) { - defer func() { - if r := recover(); r != nil { - ChatProgram.Send(event.Error(fmt.Errorf("%v", r))) - } - }() if len(m.promptConfig.ChatMessages.Messages) == 0 { return m, nil } - var previous *service.ChatMessage - if m.currentChatMessages.user == nil { - previous = &m.promptConfig.ChatMessages.Messages[len(m.promptConfig.ChatMessages.Messages)-1] - } + var msg *service.ChatMessage + if m.currentChatMessages.user == nil && m.currentChatMessages.assistant == nil { + msg = &m.promptConfig.ChatMessages.Messages[0] + } else { + order := 0 + if m.currentChatMessages.user == nil { + order = int(m.currentChatMessages.assistant.Order) + } else { + order = int(m.currentChatMessages.user.Order) + } + if m.currentChatMessages.assistant != nil && int(m.currentChatMessages.assistant.Order) < order { + order = int(m.currentChatMessages.assistant.Order) + } - if previous == nil { - - _, idx, _ := lo.FindIndexOf(m.promptConfig.ChatMessages.Messages, func(c service.ChatMessage) bool { - return c.Id == m.currentChatMessages.user.Id - }) - switch idx { - case -1: - return m, event.Error(errors.New("current message not found")) - case 0: - previous = &m.promptConfig.ChatMessages.Messages[len(m.promptConfig.ChatMessages.Messages)-1] - default: - previous = &m.promptConfig.ChatMessages.Messages[idx-1] + if order == 1 { + msg = &m.promptConfig.ChatMessages.Messages[len(m.promptConfig.ChatMessages.Messages)-1] + } else { + msg = m.promptConfig.ChatMessages.FindByOrder(uint(order - 1)) } } - m.changeCurrentChatHelper(previous) + + m.changeCurrentChatHelper(msg) m.viewport.GotoTop() return m, tea.Sequence(event.Transition("clear"), event.UpdateChatContent("", ""), event.Transition("")) } func changeResponseDown(m chatModel) (chatModel, tea.Cmd) { - defer func() { - if r := recover(); r != nil { - ChatProgram.Send(event.Error(fmt.Errorf("%v", r))) - } - }() if len(m.promptConfig.ChatMessages.Messages) == 0 { return m, nil } - var previous *service.ChatMessage - currentUserMsg := m.currentChatMessages.user - - if currentUserMsg == nil { - previous = &m.promptConfig.ChatMessages.Messages[0] - } + var msg *service.ChatMessage + if m.currentChatMessages.user == nil && m.currentChatMessages.assistant == nil { + msg = &m.promptConfig.ChatMessages.Messages[0] + } else { + order := 0 + if m.currentChatMessages.user == nil { + order = int(m.currentChatMessages.assistant.Order) + } else { + order = int(m.currentChatMessages.user.Order) + } + if m.currentChatMessages.assistant != nil && int(m.currentChatMessages.assistant.Order) > order { + order = int(m.currentChatMessages.assistant.Order) + } - if previous == nil { - _, idx, _ := lo.FindIndexOf(m.promptConfig.ChatMessages.Messages, func(c service.ChatMessage) bool { - return c.Id == currentUserMsg.Id - }) - - switch idx { - case -1: - return m, event.Error(errors.New("current message not found")) - case len(m.promptConfig.ChatMessages.Messages) - 1: - previous = &m.promptConfig.ChatMessages.Messages[0] - case len(m.promptConfig.ChatMessages.Messages) - 2: - if m.promptConfig.ChatMessages.Messages[idx+1].Id.Int64() == currentUserMsg.AssociatedMessageId { - previous = &m.promptConfig.ChatMessages.Messages[0] - } - default: - if currentUserMsg.Role == service.RoleUser && - m.promptConfig.ChatMessages.Messages[idx+1].Id.Int64() == currentUserMsg.AssociatedMessageId && - idx+2 < len(m.promptConfig.ChatMessages.Messages) { - previous = &m.promptConfig.ChatMessages.Messages[idx+2] - } else { - previous = &m.promptConfig.ChatMessages.Messages[idx+1] - } + if order >= len(m.promptConfig.ChatMessages.Messages) { + msg = &m.promptConfig.ChatMessages.Messages[0] + } else { + msg = m.promptConfig.ChatMessages.FindByOrder(uint(order + 1)) } } - m.changeCurrentChatHelper(previous) + m.changeCurrentChatHelper(msg) m.viewport.GotoTop() return m, tea.Sequence(event.Transition("clear"), event.UpdateChatContent("", ""), event.Transition("")) } @@ -166,32 +150,36 @@ func promptSend(m *chatModel) (tea.Model, tea.Cmd) { return m, tea.Sequence(event.Transition(m.userPrompt), waitForUpdate(m.promptConfig.UpdateChan), event.Transition("")) } -func (m *chatModel) changeCurrentChatHelper(previous *service.ChatMessage) { - if previous.AssociatedMessageId != 0 { - switch previous.Role { +func (m *chatModel) changeCurrentChatHelper(msg *service.ChatMessage) { + if msg == nil { + m.err = errors.New("msg is nil, (changeCurrentChatHelper)") + return + } + if msg.AssociatedMessageId != 0 || msg.Role == service.RoleSystem { + switch msg.Role { case service.RoleUser: - m.currentChatMessages.user = previous - m.currentChatMessages.assistant = m.promptConfig.ChatMessages.FindById(previous.AssociatedMessageId) + m.currentChatMessages.user = msg + m.currentChatMessages.assistant = m.promptConfig.ChatMessages.FindById(msg.AssociatedMessageId) case service.RoleAssistant: - m.currentChatMessages.assistant = previous - m.currentChatMessages.user = m.promptConfig.ChatMessages.FindById(previous.AssociatedMessageId) + m.currentChatMessages.assistant = msg + m.currentChatMessages.user = m.promptConfig.ChatMessages.FindById(msg.AssociatedMessageId) case service.RoleSystem: - m.userPrompt = "System / File | " + previous.Date.String() - m.currentChatMessages.user = previous - m.aiResponse = previous.Content + m.userPrompt = "System / File | " + msg.Date.String() + m.currentChatMessages.user = msg + m.aiResponse = msg.Content m.currentChatMessages.assistant = nil return } } else { - m.currentChatMessages.user = previous + m.currentChatMessages.user = msg } if m.currentChatMessages.assistant != nil && m.currentChatMessages.user != nil { m.aiResponse = m.currentChatMessages.assistant.Content m.userPrompt = m.currentChatMessages.user.Content } else { - m.aiResponse = previous.Content - m.userPrompt = "System / File | " + previous.Date.String() + m.aiResponse = msg.Content + m.userPrompt = carbon.FromStdTime(msg.Date).String() } } @@ -202,14 +190,14 @@ func sendPrompt(pc *service.PromptConfig, currentChatMsgs currentChatMessages) e return err } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(godcontext.GodContext) pc.AddContextWithId(ctx, cancel, currentChatMsgs.user.Id.Int64()) - defer pc.DeleteContextById(currentChatMsgs.user.Id.Int64()) + defer pc.CloseContextById(currentChatMsgs.user.Id.Int64()) options := []llms.CallOption{ llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error { if err := ctx.Err(); err != nil { - pc.DeleteContextById(currentChatMsgs.user.Id.Int64()) + pc.CloseContextById(currentChatMsgs.user.Id.Int64()) if err == io.EOF { return nil } @@ -217,7 +205,7 @@ func sendPrompt(pc *service.PromptConfig, currentChatMsgs currentChatMessages) e } previous := pc.ChatMessages.FindById(currentChatMsgs.assistant.Id.Int64()) if previous == nil { - pc.DeleteContextById(currentChatMsgs.user.Id.Int64()) + pc.CloseContextById(currentChatMsgs.user.Id.Int64()) return errors.New("previous message not found") } previous.Content += string(chunk) @@ -263,9 +251,9 @@ func sendPrompt(pc *service.PromptConfig, currentChatMsgs currentChatMessages) e } func sendAgentPrompt(m chatModel, currentChatMsgs currentChatMessages) error { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(godcontext.GodContext) m.promptConfig.AddContextWithId(ctx, cancel, currentChatMsgs.user.Id.Int64()) - defer m.promptConfig.DeleteContextById(currentChatMsgs.user.Id.Int64()) + defer m.promptConfig.CloseContext(ctx) if m.promptConfig.UpdateChan != nil { m.promptConfig.UpdateChan <- *m.promptConfig.ChatMessages.FindById(currentChatMsgs.assistant.Id.Int64()) @@ -279,7 +267,7 @@ func sendAgentPrompt(m chatModel, currentChatMsgs currentChatMessages) error { output, err := chains.Run(ctx, m.chain, last.Content, chains.WithStreamingFunc(func(ctx context.Context, chunk []byte) error { if err := ctx.Err(); err != nil { - m.promptConfig.DeleteContextById(currentChatMsgs.user.Id.Int64()) + m.promptConfig.CloseContextById(currentChatMsgs.user.Id.Int64()) if err == io.EOF { return nil } @@ -287,7 +275,7 @@ func sendAgentPrompt(m chatModel, currentChatMsgs currentChatMessages) error { } previous := m.promptConfig.ChatMessages.FindById(currentChatMsgs.assistant.Id.Int64()) if previous == nil { - m.promptConfig.DeleteContextById(currentChatMsgs.user.Id.Int64()) + m.promptConfig.CloseContextById(currentChatMsgs.user.Id.Int64()) return errors.New("previous message not found") } previous.Content += string(chunk) diff --git a/ui/chat/keys.go b/ui/chat/keys.go index a026e91..db92dba 100644 --- a/ui/chat/keys.go +++ b/ui/chat/keys.go @@ -5,17 +5,19 @@ import ( "errors" "io" + "github.com/atotto/clipboard" + "github.com/charmbracelet/bubbles/key" + tea "github.com/charmbracelet/bubbletea" + "github.com/MohammadBnei/go-ai-cli/api" "github.com/MohammadBnei/go-ai-cli/service" + godcontext "github.com/MohammadBnei/go-ai-cli/service/godcontext" "github.com/MohammadBnei/go-ai-cli/ui/event" "github.com/MohammadBnei/go-ai-cli/ui/file" "github.com/MohammadBnei/go-ai-cli/ui/info" "github.com/MohammadBnei/go-ai-cli/ui/options" "github.com/MohammadBnei/go-ai-cli/ui/quit" "github.com/MohammadBnei/go-ai-cli/ui/speech" - "github.com/atotto/clipboard" - "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" ) type listKeyMap struct { @@ -158,10 +160,10 @@ func keyMapUpdate(msg tea.Msg, m chatModel) (chatModel, tea.Cmd) { case key.Matches(msg, m.keys.textToSpeech): if m.aiResponse != "" && m.currentChatMessages.assistant != nil && len(m.stack) == 0 { msgID := m.currentChatMessages.assistant.Id.Int64() - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(godcontext.GodContext) m.promptConfig.AddContextWithId(ctx, cancel, msgID) return m, tea.Sequence(func() tea.Msg { - defer m.promptConfig.DeleteContext(ctx) + defer m.promptConfig.CloseContext(ctx) msg := m.promptConfig.ChatMessages.FindById(msgID) if msg == nil { return event.Error(errors.New("message not found")) @@ -183,7 +185,7 @@ func keyMapUpdate(msg tea.Msg, m chatModel) (chatModel, tea.Cmd) { m.promptConfig.ChatMessages.UpdateMessage(*msg) return m.audioPlayer.InitSpeaker(fm.ID) }, func() tea.Msg { - m.promptConfig.DeleteContextById(msgID) + m.promptConfig.CloseContextById(msgID) return event.Transition("") }) } diff --git a/ui/file/filepicker.go b/ui/file/filepicker.go index f201cfa..fec2f51 100644 --- a/ui/file/filepicker.go +++ b/ui/file/filepicker.go @@ -86,12 +86,12 @@ func DefaultKeyMap() KeyMap { return KeyMap{ GoToTop: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "first")), GoToLast: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "last")), - Down: key.NewBinding(key.WithKeys("j", "down", "ctrl+n"), key.WithHelp("j", "down")), - Up: key.NewBinding(key.WithKeys("k", "up", "ctrl+p"), key.WithHelp("k", "up")), - PageUp: key.NewBinding(key.WithKeys("K", "pgup"), key.WithHelp("pgup", "page up")), - PageDown: key.NewBinding(key.WithKeys("J", "pgdown"), key.WithHelp("pgdown", "page down")), - Back: key.NewBinding(key.WithKeys("h", "backspace", "left", "esc"), key.WithHelp("h", "back")), - Open: key.NewBinding(key.WithKeys("l", "right", "enter"), key.WithHelp("l", "open")), + Down: key.NewBinding(key.WithKeys("down", "ctrl+n"), key.WithHelp("down", "down")), + Up: key.NewBinding(key.WithKeys("up", "ctrl+p"), key.WithHelp("up", "up")), + PageUp: key.NewBinding(key.WithKeys("pgup"), key.WithHelp("pgup", "page up")), + PageDown: key.NewBinding(key.WithKeys("pgdown"), key.WithHelp("pgdown", "page down")), + Back: key.NewBinding(key.WithKeys("backspace", "left", "esc"), key.WithHelp("left", "back")), + Open: key.NewBinding(key.WithKeys("right", "enter"), key.WithHelp("right", "open")), Select: key.NewBinding(key.WithKeys("enter"), key.WithHelp("enter", "select")), } } diff --git a/ui/file/pickFile.go b/ui/file/pickFile.go index 6526afe..dbf5cc3 100644 --- a/ui/file/pickFile.go +++ b/ui/file/pickFile.go @@ -4,14 +4,15 @@ import ( "fmt" "os" - "github.com/MohammadBnei/go-ai-cli/ui/event" - "github.com/MohammadBnei/go-ai-cli/ui/style" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/muesli/reflow/wordwrap" "github.com/samber/lo" + + "github.com/MohammadBnei/go-ai-cli/ui/event" + "github.com/MohammadBnei/go-ai-cli/ui/style" ) type PickFileModel struct { @@ -45,6 +46,7 @@ func NewFilePicker(multipleMode bool, allowedTypes []string) PickFileModel { } } + // Init intializes the UI. func (m PickFileModel) Init() tea.Cmd { return m.filepicker.Init() diff --git a/ui/image/image.go b/ui/image/image.go index f43cc3d..734a4e8 100644 --- a/ui/image/image.go +++ b/ui/image/image.go @@ -10,18 +10,20 @@ import ( "runtime" "strings" - "github.com/MohammadBnei/go-ai-cli/api" - "github.com/MohammadBnei/go-ai-cli/config" - "github.com/MohammadBnei/go-ai-cli/service" - "github.com/MohammadBnei/go-ai-cli/ui/event" - "github.com/MohammadBnei/go-ai-cli/ui/file" - "github.com/MohammadBnei/go-ai-cli/ui/style" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/sashabaranov/go-openai" "github.com/spf13/viper" + + "github.com/MohammadBnei/go-ai-cli/api" + "github.com/MohammadBnei/go-ai-cli/config" + "github.com/MohammadBnei/go-ai-cli/service" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" + "github.com/MohammadBnei/go-ai-cli/ui/event" + "github.com/MohammadBnei/go-ai-cli/ui/file" + "github.com/MohammadBnei/go-ai-cli/ui/style" ) const ( @@ -85,9 +87,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.state = GENERATING m.editForm = constructFilepickerForm() return m, tea.Sequence(m.editForm.Init(), m.spinner.Tick, func() tea.Msg { - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(godcontext.GodContext) m.promptConfig.AddContext(ctx, cancel) - defer m.promptConfig.DeleteContext(ctx) + defer m.promptConfig.CloseContext(ctx) data, err := api.GenerateImage(ctx, m.prompt, m.size) if err != nil { return generateError(err)() diff --git a/ui/options/chat.go b/ui/options/chat.go index b6d7487..87e26d5 100644 --- a/ui/options/chat.go +++ b/ui/options/chat.go @@ -3,12 +3,13 @@ package options import ( "errors" + tea "github.com/charmbracelet/bubbletea" + "github.com/MohammadBnei/go-ai-cli/service" "github.com/MohammadBnei/go-ai-cli/ui/event" "github.com/MohammadBnei/go-ai-cli/ui/list" "github.com/MohammadBnei/go-ai-cli/ui/loadchat" "github.com/MohammadBnei/go-ai-cli/ui/savechat" - tea "github.com/charmbracelet/bubbletea" ) type chatModel struct { @@ -18,7 +19,8 @@ type chatModel struct { } const ( - SAVE = "save" + SAVE = "save" + SAVE_MODELFILE = "save as modelfile" LOAD = "load" CLEAR = "clear" ) diff --git a/ui/savechat/save.go b/ui/savechat/save.go index 9681451..0936d08 100644 --- a/ui/savechat/save.go +++ b/ui/savechat/save.go @@ -7,13 +7,14 @@ import ( "regexp" "time" - "github.com/MohammadBnei/go-ai-cli/service" - "github.com/MohammadBnei/go-ai-cli/ui/event" - "github.com/MohammadBnei/go-ai-cli/ui/style" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" "github.com/spf13/viper" + + "github.com/MohammadBnei/go-ai-cli/service" + "github.com/MohammadBnei/go-ai-cli/ui/event" + "github.com/MohammadBnei/go-ai-cli/ui/style" ) type model struct { @@ -23,7 +24,7 @@ type model struct { } func NewSaveChatModel(promptConfig *service.PromptConfig) tea.Model { - return model{promptConfig: promptConfig, form: constructForm(), title: "Saving chat"} + return model{promptConfig: promptConfig, form: constructForm(promptConfig.ChatMessages.Id), title: "Saving chat"} } func (m model) Init() tea.Cmd { @@ -50,7 +51,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if !m.form.GetBool("confirm") { return m, event.RemoveStack(m) } - return m, tea.Sequence(event.Error(saveChat(*m.promptConfig, m.form.GetString("name"))), event.RemoveStack(m)) + filename := m.form.GetString("name") + saveFn := m.saveChat + if m.form.GetBool("modelfile") { + saveFn = m.promptConfig.ChatMessages.SaveChatInModelfileFormat + } + return m, tea.Sequence(event.Error(saveFn(filename)), event.RemoveStack(m)) } return m, tea.Batch(cmds...) @@ -66,10 +72,10 @@ func (m model) GetTitleView() string { var filenamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_. -]*(\.[a-zA-Z]{1,4})?$`) -func constructForm() *huh.Form { +func constructForm(name string) *huh.Form { tRue := true group := huh.NewGroup( - huh.NewInput().Key("name").Title("Saved chat name (leave blank for auto-load)").Validate(func(s string) error { + huh.NewInput().Key("name").Title("Saved chat name (leave blank for auto-load)").Value(&name).Validate(func(s string) error { if s == "" { return nil } @@ -78,16 +84,17 @@ func constructForm() *huh.Form { } return nil }), + huh.NewConfirm().Key("modelfile").Title("Save as modelfile"), huh.NewConfirm().Key("confirm").Title("Confirm").Value(&tRue), ) return huh.NewForm(group) } -func saveChat(pc service.PromptConfig, filename string) error { +func (m model) saveChat(filename string) error { if filename == "" { filename = "last-chat" } - chatMessages := *pc.ChatMessages + chatMessages := *m.promptConfig.ChatMessages chatMessages.Id = filename if chatMessages.Description == "" { chatMessages.Description = "Saved at : " + time.Now().Format("2006-01-02 15:04:05") diff --git a/ui/speech/record.go b/ui/speech/record.go index 2ec33c1..426066f 100644 --- a/ui/speech/record.go +++ b/ui/speech/record.go @@ -10,9 +10,10 @@ import ( "os" "time" - "github.com/MohammadBnei/go-ai-cli/api" "github.com/garlicgarrison/go-recorder/recorder" "github.com/garlicgarrison/go-recorder/stream" + + "github.com/MohammadBnei/go-ai-cli/api" ) type SpeechConfig struct { @@ -74,6 +75,7 @@ func recordAudio(ctx context.Context, filename string, maxDuration time.Duration if err != nil { return err } + defer file.Close() _, err = file.Write(recording.Bytes()) if err != nil { diff --git a/ui/speech/record_test.go b/ui/speech/record_test.go index 744f767..9ae5333 100644 --- a/ui/speech/record_test.go +++ b/ui/speech/record_test.go @@ -5,11 +5,20 @@ import ( "testing" "time" - "github.com/MohammadBnei/go-ai-cli/ui/speech" "github.com/spf13/viper" "github.com/stretchr/testify/assert" + "go.uber.org/goleak" + + "github.com/MohammadBnei/go-ai-cli/service/godcontext" + "github.com/MohammadBnei/go-ai-cli/ui/speech" ) +func TestMain(m *testing.M) { + godcontext.GodContext = context.Background() + goleak.VerifyTestMain(m) + +} + func TestRecordMaxDuration(t *testing.T) { viper.BindEnv("OPENAI_KEY", "OPENAI_API_KEY") res, err := speech.SpeechToText(context.Background(), context.Background(), &speech.SpeechConfig{Duration: 10 * time.Second, Lang: "en"}) diff --git a/ui/speech/speech.go b/ui/speech/speech.go index 53eb65f..991609b 100644 --- a/ui/speech/speech.go +++ b/ui/speech/speech.go @@ -6,9 +6,6 @@ import ( "strings" "time" - "github.com/MohammadBnei/go-ai-cli/service" - "github.com/MohammadBnei/go-ai-cli/ui/event" - "github.com/MohammadBnei/go-ai-cli/ui/style" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/progress" @@ -18,6 +15,11 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/huh" "github.com/charmbracelet/lipgloss" + + "github.com/MohammadBnei/go-ai-cli/service" + "github.com/MohammadBnei/go-ai-cli/service/godcontext" + "github.com/MohammadBnei/go-ai-cli/ui/event" + "github.com/MohammadBnei/go-ai-cli/ui/style" ) const maxDuration = 3 * time.Minute @@ -118,13 +120,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.textarea.SetValue(msg.content) case startRecordingEvent: - ctx, cancelFn := context.WithCancel(context.Background()) + ctx, cancelFn := context.WithCancel(godcontext.GodContext) m.recordCancelCtx = cancelFn - aiCtx, cancelFn := context.WithCancel(context.Background()) + aiCtx, cancelFn := context.WithCancel(godcontext.GodContext) m.aiCancelCtx = cancelFn return m, tea.Batch(func() tea.Msg { + defer m.aiCancelCtx() + defer m.recordCancelCtx() + res, err := SpeechToText(ctx, aiCtx, &SpeechConfig{Duration: m.maxDuration, Lang: m.lang}) if err != nil { return err