44 "bufio"
55 "bytes"
66 "context"
7- "encoding/base64"
87 "fmt"
98 "io"
109 "log/slog"
@@ -21,7 +20,7 @@ import (
2120 "go.opentelemetry.io/otel"
2221
2322 "github.com/docker/cagent/pkg/app"
24- "github.com/docker/cagent/pkg/chat "
23+ "github.com/docker/cagent/pkg/attachment "
2524 "github.com/docker/cagent/pkg/config"
2625 "github.com/docker/cagent/pkg/content"
2726 "github.com/docker/cagent/pkg/evaluation"
@@ -412,15 +411,15 @@ func runWithoutTUI(ctx context.Context, agentFilename string, rt runtime.Runtime
412411 defer signal .Stop (sigCh )
413412
414413 // Parse for /attach commands in the message
415- messageText , attachPath := parseAttachCommand (userInput )
414+ messageText , attachPath := attachment . ParseAttachCommand (userInput )
416415
417416 // Use either the per-message attachment or the global one
418417 finalAttachPath := attachPath
419418 if finalAttachPath == "" {
420419 finalAttachPath = attachmentPath
421420 }
422421
423- sess .AddMessage (createUserMessageWithAttachment (agentFilename , messageText , finalAttachPath ))
422+ sess .AddMessage (attachment . CreateUserMessageWithAttachment (agentFilename , messageText , finalAttachPath ))
424423
425424 firstLoop := true
426425 lastAgent := rt .CurrentAgent ().Name ()
@@ -654,59 +653,7 @@ func runUserCommand(userInput string, sess *session.Session, rt runtime.Runtime,
654653 return false , nil
655654}
656655
657- // parseAttachCommand parses user input for /attach commands
658- // Returns the message text (with /attach commands removed) and the attachment path
659- func parseAttachCommand (input string ) (messageText , attachPath string ) {
660- lines := strings .Split (input , "\n " )
661- var messageLines []string
662-
663- for _ , line := range lines {
664- // Look for /attach anywhere in the line
665- attachIndex := strings .Index (line , "/attach " )
666- if attachIndex != - 1 {
667- // Extract the part before /attach
668- beforeAttach := line [:attachIndex ]
669-
670- // Extract the part after /attach (starting after "/attach ")
671- afterAttachStart := attachIndex + 8 // Length of "/attach "
672- if afterAttachStart < len (line ) {
673- afterAttach := line [afterAttachStart :]
674-
675- // Split on spaces to get the file path (first token) and any remaining text
676- tokens := strings .Fields (afterAttach )
677- if len (tokens ) > 0 {
678- attachPath = tokens [0 ]
679-
680- // Reconstruct the line with /attach and file path removed
681- var remainingText string
682- if len (tokens ) > 1 {
683- remainingText = strings .Join (tokens [1 :], " " )
684- }
685656
686- // Combine the text before /attach and any text after the file path
687- var parts []string
688- if strings .TrimSpace (beforeAttach ) != "" {
689- parts = append (parts , strings .TrimSpace (beforeAttach ))
690- }
691- if remainingText != "" {
692- parts = append (parts , remainingText )
693- }
694- reconstructedLine := strings .Join (parts , " " )
695- if reconstructedLine != "" {
696- messageLines = append (messageLines , reconstructedLine )
697- }
698- }
699- }
700- } else {
701- // Keep lines without /attach commands
702- messageLines = append (messageLines , line )
703- }
704- }
705-
706- // Join the message lines back together
707- messageText = strings .TrimSpace (strings .Join (messageLines , "\n " ))
708- return messageText , attachPath
709- }
710657
711658func fileExists (path string ) bool {
712659 _ , err := os .Stat (path )
@@ -746,92 +693,9 @@ func fromStore(reference string) (string, error) {
746693 return buf .String (), nil
747694}
748695
749- // createUserMessageWithAttachment creates a user message with optional image attachment
750- func createUserMessageWithAttachment (agentFilename , userContent , attachmentPath string ) * session.Message {
751- if attachmentPath == "" {
752- return session .UserMessage (agentFilename , userContent )
753- }
754696
755- // Convert file to data URL
756- dataURL , err := fileToDataURL (attachmentPath )
757- if err != nil {
758- fmt .Printf ("Warning: Failed to attach file %s: %v\n " , attachmentPath , err )
759- return session .UserMessage (agentFilename , userContent )
760- }
761697
762- // Ensure we have some text content when attaching a file
763- textContent := userContent
764- if strings .TrimSpace (textContent ) == "" {
765- textContent = "Please analyze this attached file."
766- }
767698
768- // Create message with multi-content including text and image
769- multiContent := []chat.MessagePart {
770- {
771- Type : chat .MessagePartTypeText ,
772- Text : textContent ,
773- },
774- {
775- Type : chat .MessagePartTypeImageURL ,
776- ImageURL : & chat.MessageImageURL {
777- URL : dataURL ,
778- Detail : chat .ImageURLDetailAuto ,
779- },
780- },
781- }
782-
783- return & session.Message {
784- AgentFilename : agentFilename ,
785- AgentName : "" ,
786- Message : chat.Message {
787- Role : chat .MessageRoleUser ,
788- MultiContent : multiContent ,
789- CreatedAt : time .Now ().Format (time .RFC3339 ),
790- },
791- }
792- }
793-
794- // fileToDataURL converts a file to a data URL
795- func fileToDataURL (filePath string ) (string , error ) {
796- // Check if file exists
797- if _ , err := os .Stat (filePath ); os .IsNotExist (err ) {
798- return "" , fmt .Errorf ("file does not exist: %s" , filePath )
799- }
800-
801- // Read file content
802- fileBytes , err := os .ReadFile (filePath )
803- if err != nil {
804- return "" , fmt .Errorf ("failed to read file: %w" , err )
805- }
806-
807- // Determine MIME type based on file extension
808- ext := strings .ToLower (filepath .Ext (filePath ))
809- var mimeType string
810- switch ext {
811- case ".jpg" , ".jpeg" :
812- mimeType = "image/jpeg"
813- case ".png" :
814- mimeType = "image/png"
815- case ".gif" :
816- mimeType = "image/gif"
817- case ".webp" :
818- mimeType = "image/webp"
819- case ".bmp" :
820- mimeType = "image/bmp"
821- case ".svg" :
822- mimeType = "image/svg+xml"
823- default :
824- return "" , fmt .Errorf ("unsupported image format: %s" , ext )
825- }
826-
827- // Encode to base64
828- encoded := base64 .StdEncoding .EncodeToString (fileBytes )
829-
830- // Create data URL
831- dataURL := fmt .Sprintf ("data:%s;base64,%s" , mimeType , encoded )
832-
833- return dataURL , nil
834- }
835699
836700// getCommandsForAgent returns the commands map for the selected agent,
837701// loading from the in-memory team for local runs or from the YAML file for remote runs.
0 commit comments