package main import ( "context" "encoding/base64" "fmt" "net/http" "os" "strings" tea "github.com/charmbracelet/bubbletea" llm "gitea.stevedudenhoeffer.com/steve/go-llm" ) // Message types for async operations // ChatResponseMsg contains the response from a chat completion type ChatResponseMsg struct { Response llm.Response Err error } // ToolExecutionMsg contains results from tool execution type ToolExecutionMsg struct { Results []llm.ToolCallResponse Err error } // ImageLoadedMsg contains a loaded image type ImageLoadedMsg struct { Image llm.Image Err error } // sendChatRequest sends a chat completion request func sendChatRequest(chat llm.ChatCompletion, req llm.Request) tea.Cmd { return func() tea.Msg { resp, err := chat.ChatComplete(context.Background(), req) return ChatResponseMsg{Response: resp, Err: err} } } // executeTools executes tool calls and returns results func executeTools(toolbox llm.ToolBox, req llm.Request, resp llm.ResponseChoice) tea.Cmd { return func() tea.Msg { ctx := llm.NewContext(context.Background(), req, &resp, nil) var results []llm.ToolCallResponse for _, call := range resp.Calls { result, err := toolbox.Execute(ctx, call) results = append(results, llm.ToolCallResponse{ ID: call.ID, Result: result, Error: err, }) } return ToolExecutionMsg{Results: results, Err: nil} } } // loadImageFromPath loads an image from a file path func loadImageFromPath(path string) tea.Cmd { return func() tea.Msg { // Clean up the path path = strings.TrimSpace(path) path = strings.Trim(path, "\"'") // Read the file data, err := os.ReadFile(path) if err != nil { return ImageLoadedMsg{Err: fmt.Errorf("failed to read image file: %w", err)} } // Detect content type contentType := http.DetectContentType(data) if !strings.HasPrefix(contentType, "image/") { return ImageLoadedMsg{Err: fmt.Errorf("file is not an image: %s", contentType)} } // Base64 encode encoded := base64.StdEncoding.EncodeToString(data) return ImageLoadedMsg{ Image: llm.Image{ Base64: encoded, ContentType: contentType, }, } } } // loadImageFromURL loads an image from a URL func loadImageFromURL(url string) tea.Cmd { return func() tea.Msg { url = strings.TrimSpace(url) // For URL images, we can just use the URL directly return ImageLoadedMsg{ Image: llm.Image{ Url: url, }, } } } // loadImageFromBase64 loads an image from base64 data func loadImageFromBase64(data string) tea.Cmd { return func() tea.Msg { data = strings.TrimSpace(data) // Check if it's a data URL if strings.HasPrefix(data, "data:") { // Parse data URL: data:image/png;base64,.... parts := strings.SplitN(data, ",", 2) if len(parts) != 2 { return ImageLoadedMsg{Err: fmt.Errorf("invalid data URL format")} } // Extract content type from first part mediaType := strings.TrimPrefix(parts[0], "data:") mediaType = strings.TrimSuffix(mediaType, ";base64") return ImageLoadedMsg{ Image: llm.Image{ Base64: parts[1], ContentType: mediaType, }, } } // Assume it's raw base64, try to detect content type decoded, err := base64.StdEncoding.DecodeString(data) if err != nil { return ImageLoadedMsg{Err: fmt.Errorf("invalid base64 data: %w", err)} } contentType := http.DetectContentType(decoded) if !strings.HasPrefix(contentType, "image/") { return ImageLoadedMsg{Err: fmt.Errorf("data is not an image: %s", contentType)} } return ImageLoadedMsg{ Image: llm.Image{ Base64: data, ContentType: contentType, }, } } } // buildRequest builds a chat request from the current state func buildRequest(m *Model, userText string) llm.Request { // Create the user message with any pending images userMsg := llm.Message{ Role: llm.RoleUser, Text: userText, Images: m.pendingImages, } req := llm.Request{ Conversation: m.conversation, Messages: []llm.Message{ {Role: llm.RoleSystem, Text: m.systemPrompt}, userMsg, }, Temperature: m.temperature, } // Add toolbox if enabled if m.toolsEnabled && len(m.toolbox.Functions()) > 0 { req.Toolbox = m.toolbox.WithRequireTool(false) } return req } // buildFollowUpRequest builds a follow-up request after tool execution func buildFollowUpRequest(m *Model, previousReq llm.Request, resp llm.ResponseChoice, toolResults []llm.ToolCallResponse) llm.Request { return previousReq.NextRequest(resp, toolResults) }