Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,4 @@ jobs:
with:
go-version: '1.24'
- name: Build the code
run: go build -o gromitai .
- name: Run cli with no commands and check the usage output
run: |
output=$(./gromitai || true)
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The empty argument also calls the AI to generate some text for introduction. Therefore removed this check to not call AI on each build.

echo "$output"
if [[ "$output" != *"⚡️🤖 Please run ./gromit --help to see usage"* ]]; then
echo "❌ Usage message does not match the expected value"
exit 1
fi
run: go build -o gromitai .
69 changes: 45 additions & 24 deletions gromit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io"
"os"
"os/exec"
"regexp"
"runtime"
"sort"
"strings"
Expand All @@ -18,8 +19,9 @@ import (
const systemPrompt = `You are an assistant providing terminal commands based on user's questions.
You will be given a question about how to do something in the CLI environment.
You will then find out what command to execute and provide the command.
Do not provide any additional information, explanation or context, just the linux command.
For example, if question is about listing all files in a directory for linux, respond with "ls".`
Make sure to enclose the actual command inside *** marker.
For example, if question is about listing all files in a directory for linux, respond with "***ls***".
If no question is asked by user, continue the conversation. If they want to exit, respond with "***exit***".`
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively the prompt could be to return a JSON with specific key values. That way Gromit could infer how the AI interpreted the original question, etc.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that is a great point! I will refactor to make it that way.


type Gromit struct {
cli.Command
Expand Down Expand Up @@ -131,10 +133,6 @@ func WithAskForConfirmation(confirm bool) ConfigurationModifier {
func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error {
commandArgs := command.Args().Slice()
query := strings.Join(commandArgs, " ")
if query == "" {
g.print("Please run ./gromit --help to see usage")
return nil
}
prompt := g.String("systemPrompt")
if prompt == "" {
prompt = systemPrompt
Expand All @@ -147,51 +145,74 @@ func (g *Gromit) actionGromit(ctx context.Context, command *cli.Command) error {
model: g.String("model"),
systemPrompt: prompt,
}
err := g.handleUserQuery(ctx, query)
terminalCommand, err := g.extractCommandForQuery(ctx, query)
if err != nil {
return err
}
if terminalCommand != "" {
err = g.handleTerminalCommand(ctx, terminalCommand)
if err != nil {
return err
}
}
for ctx.Err() == nil {
confirmation, err := g.askConfirmation("Can I help you with anything else?")
//read the user input, pass it to AI
reader := bufio.NewReader(g.Reader)
query, err := reader.ReadString('\n')
if err != nil {
return err
}
if confirmation.confirmed {
g.print("How can I help?")
reader := bufio.NewReader(os.Stdin)
query, err := reader.ReadString('\n')
terminalCommand, err := g.extractCommandForQuery(ctx, query)
if err != nil {
return err
}
if terminalCommand == "exit" {
break
}
if terminalCommand != "" {
err = g.handleTerminalCommand(ctx, terminalCommand)
if err != nil {
return err
}
if err = g.handleUserQuery(ctx, query); err != nil {
return err
}
} else {
break
}
}
return nil
}

func (g *Gromit) handleUserQuery(ctx context.Context, query string) error {
func (g *Gromit) extractCommandForQuery(ctx context.Context, query string) (string, error) {
assister, err := g.AssisterCreator.GetAssister(g.configuration.AiParameters)
if err != nil {
return err
return "", err
}
exeCommand, err := assister.GetTerminalCommand(ctx, query)
if query == "" {
query = "Can you please introduce yourself or continue the conversation?"
}
response, err := assister.GetTerminalCommand(ctx, query)
if err != nil {
return err
return "", err
}
g.print("In order to do that, you need to run:")
g.print(exeCommand)
//command is enclosed in *** marker
regexp := regexp.MustCompile(`\*\*\*(.*?)\*\*\*`)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could compile this once as a package level var and reuse it. (also see my earlier suggestion about asking AI to return structured response. That way regex would be redundant I think.

commands := regexp.FindStringSubmatch(response)
var command string
if len(commands) > 0 {
command = commands[1]
} else {
g.print(response)
}
return command, nil
}

func (g *Gromit) handleTerminalCommand(ctx context.Context, terminalCommand string) error {
g.print("In order to do that, you need to run:")
g.print(terminalCommand)
confirmation, err := g.askConfirmation("Would you like to run this command?")
if err != nil {
g.print("Error reading your response")
return err
}
if confirmation.confirmed {
err = g.executeCommand(exeCommand)
err = g.executeCommand(terminalCommand)
if err != nil {
return err
}
Expand Down
19 changes: 5 additions & 14 deletions gromit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,13 @@ func TestMessagePrinter(t *testing.T) {
require.Equal(t, "✌️ hello \r\n", buff.String())
}

func TestConfigurationPromptPrefix(t *testing.T) {
var buff bytes.Buffer
g, err := NewGromit(&mockAIProvider{}, WithPromptPrefix("🏝️"), WithWriter(&buff))
require.NoError(t, err)
g.Run(t.Context(), []string{})
require.Contains(t, buff.String(), "🏝️ Please run ./gromit --help to see usage")
}

func TestWhenAIProviderFailsToCreateAssister(t *testing.T) {
m := &mockAIProvider{
assisterError: errors.New("Unable to create assister"),
}
g, err := NewGromit(m)
require.NoError(t, err)
err = g.handleUserQuery(t.Context(), "some query")
_, err = g.extractCommandForQuery(t.Context(), "some query")
require.EqualError(t, err, "Unable to create assister")
}

Expand All @@ -85,19 +77,18 @@ func TestWhenAIProviderFailsToFindTheCommand(t *testing.T) {
func TestAIAssisterFindingCorrectCommand(t *testing.T) {
var buff bytes.Buffer
m := &mockAIProvider{
commandResult: "ls",
commandResult: "***ls***",
}
g, err := NewGromit(m, WithWriter(&buff), WithPromptPrefix("🐶"), WithAskForConfirmation(false))
require.NoError(t, err)

g.Reader = strings.NewReader("I want to list all files in current directory\n")
g.Run(t.Context(), []string{"gromit", "--model", "myModel", "--agent", "myAgent",
"--apiKey=key1234", "--maxTokens=2000",
"--systemPrompt", "myPrompt", "I", "want", "to", "list", "all", "files", "in", "current", "directory"})
"--systemPrompt", "myPrompt", "hello", "my", "ai", "friend!"})
result := buff.String()
require.Contains(t, result, "🐶 In order to do that, you need to run")
require.Contains(t, result, "🐶 ls")
require.Contains(t, result, "README.md")
require.Contains(t, result, "🐶 How can I help?")

require.Equal(t, "myAgent", m.actualAiParameters.agent)
require.Equal(t, "myModel", m.actualAiParameters.model)
Expand All @@ -107,7 +98,7 @@ func TestAIAssisterFindingCorrectCommand(t *testing.T) {
require.Contains(t, m.actualAiParameters.systemPrompt, "User's operating system is")
require.Contains(t, m.actualAiParameters.systemPrompt, "User's current shell is")
require.Contains(t, m.actualAiParameters.systemPrompt, "User's available path commands are")
require.Equal(t, "I want to list all files in current directory", m.actualUserMessage)
require.Contains(t, m.actualUserMessage, "I want to list all files in current directory")
}

type mockAIProvider struct {
Expand Down