Skip to content

Commit b6bec8a

Browse files
committed
feat: Update default model and fix Gemini provider
The default LLM model is now gemma-3n-e4b-it. This commit also includes fixes for the Gemini LLM provider and resolves terminal input issues during cleanup confirmation.
1 parent 76891b8 commit b6bec8a

File tree

3 files changed

+183
-160
lines changed

3 files changed

+183
-160
lines changed

cmd/docker-ai/main.go

Lines changed: 149 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"bufio"
45
"bytes"
56
"docker-ai/pkg/config"
67
"flag"
@@ -73,11 +74,20 @@ func runAIMode() {
7374
fmt.Printf("Warning: could not load config file: %v\n", err)
7475
}
7576

76-
modelPluginAvailable := true
7777
llmProvider := flag.String("llm-provider", "groq", "LLM provider to use (groq, gemini, openai)")
78-
model := flag.String("model", "gemma2-9b-it", "Model to use")
78+
model := flag.String("model", "gemma-3n-e4b-it", "Model to use")
79+
command := flag.String("c", "", "Execute a single command and exit")
7980
flag.Parse()
8081

82+
if *command != "" {
83+
runSingleCommand(&appConfig, *command, *llmProvider, *model)
84+
return
85+
}
86+
87+
runInteractiveMode(&appConfig, *llmProvider, *model)
88+
}
89+
90+
func runInteractiveMode(appConfig *config.Config, llmProvider, model string) {
8191
fmt.Println("Docker AI interactive shell. Type 'exit' or 'quit' to leave.")
8292

8393
historyFile := filepath.Join(os.Getenv("HOME"), ".docker-ai-history")
@@ -91,7 +101,6 @@ func runAIMode() {
91101
f.Close()
92102
}
93103

94-
lastContainerName := ""
95104
for {
96105
input, err := line.Prompt("docker-ai> ")
97106
if err != nil {
@@ -112,87 +121,10 @@ func runAIMode() {
112121
break
113122
}
114123

115-
if input == "reset confirm" {
116-
appConfig.SkipCleanupWarning = false
117-
if err := config.SaveConfig(appConfig); err != nil {
118-
fmt.Println("Failed to save configuration:", err)
119-
} else {
120-
fmt.Println("Cleanup confirmation has been reset. You will be prompted before cleanup commands are run.")
121-
}
122-
continue
123-
}
124-
125-
// Replace 'that container' with lastContainerName if present
126-
userInput := input
127-
if strings.Contains(input, "that container") && lastContainerName != "" {
128-
userInput = strings.ReplaceAll(input, "that container", lastContainerName)
129-
}
130-
131-
// Get running containers to provide context to the LLM
132-
var fullPrompt string
133-
psCmd := exec.Command("sh", "-c", "docker ps -a --format '{{.Names}} ({{.Status}})'")
134-
psOutput, err := psCmd.CombinedOutput()
135-
if err == nil && len(psOutput) > 0 {
136-
containerList := strings.TrimSpace(string(psOutput))
137-
containerList = strings.ReplaceAll(containerList, "\n", ", ")
138-
fullPrompt = fmt.Sprintf("The user wants to perform a Docker command. Here is a list of all containers (running and stopped): %s.\n\nUser's request: %s", containerList, userInput)
139-
} else {
140-
fullPrompt = userInput
141-
}
142-
143-
if !modelPluginAvailable && strings.Contains(userInput, "model") {
144-
fullPrompt += "\n\nNote: The 'docker model' command is not available on this system."
145-
}
146-
147-
response, err := llm.QueryLLM(fullPrompt, *llmProvider, *model)
148-
if err != nil {
149-
fmt.Printf("Error: %v\n", err)
150-
continue
151-
}
152-
153-
if !strings.HasPrefix(response, "docker ") {
154-
fmt.Println(response)
155-
continue
156-
}
157-
158-
// Cleanup command confirmation
159-
if isCleanupCommand(response) && !appConfig.SkipCleanupWarning {
160-
fmt.Printf("WARNING: The generated command is a cleanup command:\n%s\n\n", response)
161-
162-
answer, err := line.Prompt("Are you sure you want to execute? [y]es, [n]o, [d]on't ask again: ")
163-
if err != nil {
164-
fmt.Println("Execution cancelled.")
165-
continue
166-
}
167-
answer = strings.ToLower(strings.TrimSpace(answer))
168-
169-
switch answer {
170-
case "y", "yes":
171-
// continue to execution
172-
case "d", "dont", "don't ask again":
173-
appConfig.SkipCleanupWarning = true
174-
if err := config.SaveConfig(appConfig); err != nil {
175-
fmt.Println("Failed to save configuration:", err)
176-
}
177-
// continue to execution
178-
default:
179-
fmt.Println("Execution cancelled.")
180-
continue
181-
}
182-
}
183-
184-
fmt.Printf("➜ executing: %s\n", response)
185-
186124
// Before running the command, close the liner to restore the terminal
187125
line.Close()
188126

189-
// Execute the Docker command
190-
var stderrBuf bytes.Buffer
191-
cmd := exec.Command("sh", "-c", response)
192-
cmd.Stdin = os.Stdin // Pass the terminal's input to the command
193-
cmd.Stdout = os.Stdout
194-
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
195-
err = cmd.Run()
127+
runSingleCommand(appConfig, input, llmProvider, model)
196128

197129
// After a command that takes over stdin, the terminal can be left in a
198130
// "raw" state. We use `stty` to force it back to a sane mode before
@@ -208,67 +140,152 @@ func runAIMode() {
208140
line.ReadHistory(f)
209141
f.Close()
210142
}
143+
}
211144

212-
if err != nil {
213-
if exitErr, ok := err.(*exec.ExitError); ok {
214-
// The command exited with a non-zero status.
215-
// Stderr is already printed. We can analyze it from the buffer.
216-
stderrString := stderrBuf.String()
217-
if strings.Contains(response, "docker model") {
218-
if strings.Contains(stderrString, "is not a docker command") {
219-
modelPluginAvailable = false
220-
fmt.Println("\nError: The 'docker model' command is not available.")
221-
fmt.Println("Please ensure you have installed the Docker Model Runner plugin.")
222-
fmt.Println("You can try installing it with: 'sudo apt-get update && sudo apt-get install docker-model-plugin'")
223-
}
224-
}
225-
// Check for docker scout update message
226-
if strings.Contains(stderrString, "New version") && strings.Contains(stderrString, "available") {
227-
fmt.Println("\nHint: A new version of Docker Scout is available.")
228-
fmt.Println("To update it, you can run the following command in your terminal:")
229-
fmt.Println("curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --")
230-
}
145+
if f, err := os.Create(historyFile); err != nil {
146+
fmt.Printf("Failed to save history: %v\n", err)
147+
} else {
148+
line.WriteHistory(f)
149+
f.Close()
150+
}
231151

232-
fmt.Printf("\nCommand finished with error: %s\n", exitErr)
233-
} else {
234-
fmt.Printf("Error executing command: %v\n", err)
152+
if err := config.SaveConfig(*appConfig); err != nil {
153+
fmt.Println("Failed to save configuration:", err)
154+
} else {
155+
fmt.Println("Cleanup confirmation has been reset. You will be prompted before cleanup commands are run.")
156+
}
157+
}
158+
159+
func runSingleCommand(appConfig *config.Config, input, llmProvider, model string) {
160+
if input == "reset confirm" {
161+
appConfig.SkipCleanupWarning = false
162+
if err := config.SaveConfig(*appConfig); err != nil {
163+
fmt.Println("Failed to save configuration:", err)
164+
} else {
165+
fmt.Println("Cleanup confirmation has been reset. You will be prompted before cleanup commands are run.")
166+
}
167+
return
168+
}
169+
170+
// Replace 'that container' with lastContainerName if present
171+
userInput := input
172+
if strings.Contains(input, "that container") && appConfig.LastContainerName != "" {
173+
userInput = strings.ReplaceAll(input, "that container", appConfig.LastContainerName)
174+
}
175+
176+
// Get running containers to provide context to the LLM
177+
var fullPrompt string
178+
psCmd := exec.Command("sh", "-c", "docker ps -a --format '{{.Names}} ({{.Status}})'")
179+
psOutput, err := psCmd.CombinedOutput()
180+
if err == nil && len(psOutput) > 0 {
181+
containerList := strings.TrimSpace(string(psOutput))
182+
containerList = strings.ReplaceAll(containerList, "\n", ", ")
183+
fullPrompt = fmt.Sprintf("The user wants to perform a Docker command. Here is a list of all containers (running and stopped): %s.\n\nUser's request: %s", containerList, userInput)
184+
} else {
185+
fullPrompt = userInput
186+
}
187+
188+
if !strings.Contains(userInput, "model") {
189+
fullPrompt += "\n\nNote: The 'docker model' command is not available on this system."
190+
}
191+
192+
response, err := llm.QueryLLM(fullPrompt, llmProvider, model)
193+
if err != nil {
194+
fmt.Printf("Error: %v\n", err)
195+
return
196+
}
197+
198+
if !strings.HasPrefix(response, "docker ") {
199+
fmt.Println(response)
200+
return
201+
}
202+
203+
// Cleanup command confirmation
204+
if isCleanupCommand(response) && !appConfig.SkipCleanupWarning {
205+
fmt.Printf("WARNING: The generated command is a cleanup command:\n%s\n\n", response)
206+
207+
// For single-command mode, we need a way to confirm.
208+
// We'll use a simple prompt here, but this could be improved.
209+
// A liner isn't running, so we use fmt.
210+
fmt.Print("Are you sure you want to execute? [y]es, [n]o, [d]on't ask again: ")
211+
reader := bufio.NewReader(os.Stdin)
212+
answer, _ := reader.ReadString('\n')
213+
214+
answer = strings.ToLower(strings.TrimSpace(answer))
215+
216+
switch answer {
217+
case "y", "yes":
218+
// continue to execution
219+
case "d", "dont", "don't ask again":
220+
appConfig.SkipCleanupWarning = true
221+
if err := config.SaveConfig(*appConfig); err != nil {
222+
fmt.Println("Failed to save configuration:", err)
235223
}
236-
continue
224+
// continue to execution
225+
default:
226+
fmt.Println("Execution cancelled.")
227+
return
237228
}
229+
}
230+
231+
fmt.Printf("➜ executing: %s\n", response)
232+
233+
// Execute the Docker command
234+
var stderrBuf bytes.Buffer
235+
cmd := exec.Command("sh", "-c", response)
236+
cmd.Stdin = os.Stdin
237+
cmd.Stdout = os.Stdout
238+
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf)
239+
err = cmd.Run()
238240

239-
// Since output is streamed, we don't print it again here.
240-
241-
// If the command was a 'docker run', get and store the container name
242-
if strings.HasPrefix(response, "docker run") {
243-
// With streaming, we can't just get the container ID from the output of this command.
244-
// We will need a new way to get the container name.
245-
// A simple approach is to list recent containers and assume the latest one is it.
246-
getLatestContainerCmd := `docker ps -l -q`
247-
getLatestCmd := exec.Command("sh", "-c", getLatestContainerCmd)
248-
latestID, latestErr := getLatestCmd.Output()
249-
if latestErr == nil && len(latestID) > 0 {
250-
containerID := strings.TrimSpace(string(latestID))
251-
inspectCmd := exec.Command("sh", "-c", fmt.Sprintf("docker inspect --format '{{.Name}}' %s | sed 's/^\\///'", containerID))
252-
inspectOut, inspectErr := inspectCmd.CombinedOutput()
253-
if inspectErr == nil {
254-
lastContainerName = strings.TrimSpace(string(inspectOut))
255-
fmt.Printf("\nContainer '%s' started.\n", lastContainerName)
241+
if err != nil {
242+
if exitErr, ok := err.(*exec.ExitError); ok {
243+
// The command exited with a non-zero status.
244+
// Stderr is already printed. We can analyze it from the buffer.
245+
stderrString := stderrBuf.String()
246+
if strings.Contains(response, "docker model") {
247+
if strings.Contains(stderrString, "is not a docker command") {
248+
fmt.Println("\nError: The 'docker model' command is not available.")
249+
fmt.Println("Please ensure you have installed the Docker Model Runner plugin.")
250+
fmt.Println("You can try installing it with: 'sudo apt-get update && sudo apt-get install docker-model-plugin'")
256251
}
257252
}
258-
259-
statusCmd := exec.Command("sh", "-c", "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'")
260-
statusOut, statusErr := statusCmd.CombinedOutput()
261-
if statusErr == nil {
262-
fmt.Println("\nCurrently running containers:")
263-
fmt.Print(string(statusOut))
253+
// Check for docker scout update message
254+
if strings.Contains(stderrString, "New version") && strings.Contains(stderrString, "available") {
255+
fmt.Println("\nHint: A new version of Docker Scout is available.")
256+
fmt.Println("To update it, you can run the following command in your terminal:")
257+
fmt.Println("curl -sSfL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh | sh -s --")
264258
}
259+
fmt.Printf("\nCommand finished with error: %s\n", exitErr)
260+
} else {
261+
fmt.Printf("Error executing command: %v\n", err)
265262
}
263+
return
266264
}
267265

268-
if f, err := os.Create(historyFile); err != nil {
269-
fmt.Fprintln(os.Stderr, "Error writing history file:", err)
270-
} else {
271-
line.WriteHistory(f)
272-
f.Close()
266+
// If the command was a 'docker run', get and store the container name
267+
if strings.HasPrefix(response, "docker run") {
268+
// With streaming, we can't just get the container ID from the output of this command.
269+
// We will need a new way to get the container name.
270+
// A simple approach is to list recent containers and assume the latest one is it.
271+
getLatestContainerCmd := `docker ps -l -q`
272+
getLatestCmd := exec.Command("sh", "-c", getLatestContainerCmd)
273+
latestID, latestErr := getLatestCmd.Output()
274+
if latestErr == nil && len(latestID) > 0 {
275+
containerID := strings.TrimSpace(string(latestID))
276+
inspectCmd := exec.Command("sh", "-c", fmt.Sprintf("docker inspect --format '{{.Name}}' %s | sed 's/^\\///'", containerID))
277+
inspectOut, inspectErr := inspectCmd.CombinedOutput()
278+
if inspectErr == nil {
279+
appConfig.LastContainerName = strings.TrimSpace(string(inspectOut))
280+
fmt.Printf("\nContainer '%s' started.\n", appConfig.LastContainerName)
281+
}
282+
}
283+
284+
statusCmd := exec.Command("sh", "-c", "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'")
285+
statusOut, statusErr := statusCmd.CombinedOutput()
286+
if statusErr == nil {
287+
fmt.Println("\nCurrently running containers:")
288+
fmt.Print(string(statusOut))
289+
}
273290
}
274291
}

pkg/config/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import (
77
)
88

99
type Config struct {
10-
SkipCleanupWarning bool `json:"skip_cleanup_warning"`
10+
SkipCleanupWarning bool `json:"skip_cleanup_warning"`
11+
LastContainerName string `json:"last_container_name"`
1112
}
1213

1314
func GetConfigPath() (string, error) {

0 commit comments

Comments
 (0)