summaryrefslogtreecommitdiff
path: root/models
diff options
context:
space:
mode:
authorGrail Finder <wohilas@gmail.com>2026-02-28 07:57:49 +0300
committerGrail Finder <wohilas@gmail.com>2026-02-28 07:57:49 +0300
commite52143407367e54f5b04177957f5f0436e28718b (patch)
tree9a1abc47cb7f07a924179e66925d3bfc7c1d9ac3 /models
parent916c5d3904b366e9f7f6f12867f7f0b71791bb6f (diff)
Refactor: move msg totext method to main package
logic requires reference to config
Diffstat (limited to 'models')
-rw-r--r--models/models.go134
-rw-r--r--models/models_test.go161
2 files changed, 16 insertions, 279 deletions
diff --git a/models/models.go b/models/models.go
index 7ac9ec9..200d898 100644
--- a/models/models.go
+++ b/models/models.go
@@ -5,22 +5,9 @@ import (
"encoding/json"
"fmt"
"os"
- "path/filepath"
"strings"
)
-var (
- // imageBaseDir is the base directory for displaying image paths.
- // If set, image paths will be shown relative to this directory.
- imageBaseDir = ""
-)
-
-// SetImageBaseDir sets the base directory for displaying image paths.
-// If dir is empty, full paths will be shown.
-func SetImageBaseDir(dir string) {
- imageBaseDir = dir
-}
-
type FuncCall struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
@@ -119,14 +106,14 @@ type RoleMsg struct {
IsShellCommand bool `json:"is_shell_command,omitempty"` // True for shell command outputs (always shown)
KnownTo []string `json:"known_to,omitempty"`
Stats *ResponseStats `json:"stats"`
- hasContentParts bool // Flag to indicate which content type to marshal
+ HasContentParts bool // Flag to indicate which content type to marshal
}
// MarshalJSON implements custom JSON marshaling for RoleMsg
//
//nolint:gocritic
func (m RoleMsg) MarshalJSON() ([]byte, error) {
- if m.hasContentParts {
+ if m.HasContentParts {
// Use structured content format
aux := struct {
Role string `json:"role"`
@@ -189,7 +176,7 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
m.IsShellCommand = structured.IsShellCommand
m.KnownTo = structured.KnownTo
m.Stats = structured.Stats
- m.hasContentParts = true
+ m.HasContentParts = true
return nil
}
@@ -213,77 +200,13 @@ func (m *RoleMsg) UnmarshalJSON(data []byte) error {
m.IsShellCommand = simple.IsShellCommand
m.KnownTo = simple.KnownTo
m.Stats = simple.Stats
- m.hasContentParts = false
+ m.HasContentParts = false
return nil
}
-func (m *RoleMsg) ToText(i int) string {
- var contentStr string
- var imageIndicators []string
- if !m.hasContentParts {
- contentStr = m.Content
- } else {
- var textParts []string
- for _, part := range m.ContentParts {
- switch p := part.(type) {
- case TextContentPart:
- if p.Type == "text" {
- textParts = append(textParts, p.Text)
- }
- case ImageContentPart:
- displayPath := p.Path
- if displayPath == "" {
- displayPath = "image"
- } else {
- displayPath = extractDisplayPath(displayPath)
- }
- imageIndicators = append(imageIndicators, fmt.Sprintf("[orange::i][image: %s][-:-:-]", displayPath))
- case map[string]any:
- if partType, exists := p["type"]; exists {
- switch partType {
- case "text":
- if textVal, textExists := p["text"]; textExists {
- if textStr, isStr := textVal.(string); isStr {
- textParts = append(textParts, textStr)
- }
- }
- case "image_url":
- var displayPath string
- if pathVal, pathExists := p["path"]; pathExists {
- if pathStr, isStr := pathVal.(string); isStr && pathStr != "" {
- displayPath = extractDisplayPath(pathStr)
- }
- }
- if displayPath == "" {
- displayPath = "image"
- }
- imageIndicators = append(imageIndicators, fmt.Sprintf("[orange::i][image: %s][-:-:-]", displayPath))
- }
- }
- }
- }
- contentStr = strings.Join(textParts, " ") + " "
- }
- contentStr, _ = strings.CutPrefix(contentStr, m.Role+":")
- icon := fmt.Sprintf("(%d) <%s>: ", i, m.Role)
- var finalContent strings.Builder
- if len(imageIndicators) > 0 {
- for _, indicator := range imageIndicators {
- finalContent.WriteString(indicator)
- finalContent.WriteString("\n")
- }
- }
- finalContent.WriteString(contentStr)
- if m.Stats != nil {
- fmt.Fprintf(&finalContent, "\n[gray::i][%d tok, %.1fs, %.1f t/s][-:-:-]", m.Stats.Tokens, m.Stats.Duration, m.Stats.TokensPerSec)
- }
- textMsg := fmt.Sprintf("[-:-:b]%s[-:-:-]\n%s\n", icon, finalContent.String())
- return strings.ReplaceAll(textMsg, "\n\n", "\n")
-}
-
func (m *RoleMsg) ToPrompt() string {
var contentStr string
- if !m.hasContentParts {
+ if !m.HasContentParts {
contentStr = m.Content
} else {
// For structured content, just take the text parts
@@ -316,7 +239,7 @@ func NewRoleMsg(role, content string) RoleMsg {
return RoleMsg{
Role: role,
Content: content,
- hasContentParts: false,
+ HasContentParts: false,
}
}
@@ -325,7 +248,7 @@ func NewMultimodalMsg(role string, contentParts []any) RoleMsg {
return RoleMsg{
Role: role,
ContentParts: contentParts,
- hasContentParts: true,
+ HasContentParts: true,
}
}
@@ -334,7 +257,7 @@ func (m *RoleMsg) HasContent() bool {
if m.Content != "" {
return true
}
- if m.hasContentParts && len(m.ContentParts) > 0 {
+ if m.HasContentParts && len(m.ContentParts) > 0 {
return true
}
return false
@@ -342,7 +265,7 @@ func (m *RoleMsg) HasContent() bool {
// IsContentParts returns true if the message uses structured content parts
func (m *RoleMsg) IsContentParts() bool {
- return m.hasContentParts
+ return m.HasContentParts
}
// GetContentParts returns the content parts of the message
@@ -359,14 +282,14 @@ func (m *RoleMsg) Copy() RoleMsg {
ToolCallID: m.ToolCallID,
KnownTo: m.KnownTo,
Stats: m.Stats,
- hasContentParts: m.hasContentParts,
+ HasContentParts: m.HasContentParts,
}
}
// GetText returns the text content of the message, handling both
// simple Content and multimodal ContentParts formats.
func (m *RoleMsg) GetText() string {
- if !m.hasContentParts {
+ if !m.HasContentParts {
return m.Content
}
var textParts []string
@@ -395,7 +318,7 @@ func (m *RoleMsg) GetText() string {
// ContentParts (multimodal), it updates the text parts while preserving
// images. If not, it sets the simple Content field.
func (m *RoleMsg) SetText(text string) {
- if !m.hasContentParts {
+ if !m.HasContentParts {
m.Content = text
return
}
@@ -425,14 +348,14 @@ func (m *RoleMsg) SetText(text string) {
// AddTextPart adds a text content part to the message
func (m *RoleMsg) AddTextPart(text string) {
- if !m.hasContentParts {
+ if !m.HasContentParts {
// Convert to content parts format
if m.Content != "" {
m.ContentParts = []any{TextContentPart{Type: "text", Text: m.Content}}
} else {
m.ContentParts = []any{}
}
- m.hasContentParts = true
+ m.HasContentParts = true
}
textPart := TextContentPart{Type: "text", Text: text}
m.ContentParts = append(m.ContentParts, textPart)
@@ -440,14 +363,14 @@ func (m *RoleMsg) AddTextPart(text string) {
// AddImagePart adds an image content part to the message
func (m *RoleMsg) AddImagePart(imageURL, imagePath string) {
- if !m.hasContentParts {
+ if !m.HasContentParts {
// Convert to content parts format
if m.Content != "" {
m.ContentParts = []any{TextContentPart{Type: "text", Text: m.Content}}
} else {
m.ContentParts = []any{}
}
- m.hasContentParts = true
+ m.HasContentParts = true
}
imagePart := ImageContentPart{
Type: "image_url",
@@ -491,31 +414,6 @@ func CreateImageURLFromPath(imagePath string) (string, error) {
return fmt.Sprintf("data:%s;base64,%s", mimeType, encoded), nil
}
-// extractDisplayPath returns a path suitable for display, potentially relative to imageBaseDir
-func extractDisplayPath(p string) string {
- if p == "" {
- return ""
- }
-
- // If base directory is set, try to make path relative to it
- if imageBaseDir != "" {
- if rel, err := filepath.Rel(imageBaseDir, p); err == nil {
- // Check if relative path doesn't start with ".." (meaning it's within base dir)
- // If it starts with "..", we might still want to show it as relative
- // but for now we show full path if it goes outside base dir
- if !strings.HasPrefix(rel, "..") {
- p = rel
- }
- }
- }
-
- // Truncate long paths to last 60 characters if needed
- if len(p) > 60 {
- return "..." + p[len(p)-60:]
- }
- return p
-}
-
type ChatBody struct {
Model string `json:"model"`
Stream bool `json:"stream"`
diff --git a/models/models_test.go b/models/models_test.go
deleted file mode 100644
index 3b6476a..0000000
--- a/models/models_test.go
+++ /dev/null
@@ -1,161 +0,0 @@
-package models
-import (
- "strings"
- "testing"
-)
-func TestRoleMsgToTextWithImages(t *testing.T) {
- tests := []struct {
- name string
- msg RoleMsg
- index int
- expected string // substring to check
- }{
- {
- name: "text and image",
- index: 0,
- msg: func() RoleMsg {
- msg := NewMultimodalMsg("user", []interface{}{})
- msg.AddTextPart("Look at this picture")
- msg.AddImagePart("data:image/jpeg;base64,abc123", "/home/user/Pictures/cat.jpg")
- return msg
- }(),
- expected: "[orange::i][image: /home/user/Pictures/cat.jpg][-:-:-]",
- },
- {
- name: "image only",
- index: 1,
- msg: func() RoleMsg {
- msg := NewMultimodalMsg("user", []interface{}{})
- msg.AddImagePart("data:image/png;base64,xyz789", "/tmp/screenshot_20250217_123456.png")
- return msg
- }(),
- expected: "[orange::i][image: /tmp/screenshot_20250217_123456.png][-:-:-]",
- },
- {
- name: "long filename truncated",
- index: 2,
- msg: func() RoleMsg {
- msg := NewMultimodalMsg("user", []interface{}{})
- msg.AddTextPart("Check this")
- msg.AddImagePart("data:image/jpeg;base64,foo", "/very/long/path/to/a/really_long_filename_that_exceeds_forty_characters.jpg")
- return msg
- }(),
- expected: "[orange::i][image: .../to/a/really_long_filename_that_exceeds_forty_characters.jpg][-:-:-]",
- },
- {
- name: "multiple images",
- index: 3,
- msg: func() RoleMsg {
- msg := NewMultimodalMsg("user", []interface{}{})
- msg.AddTextPart("Multiple images")
- msg.AddImagePart("data:image/jpeg;base64,a", "/path/img1.jpg")
- msg.AddImagePart("data:image/png;base64,b", "/path/img2.png")
- return msg
- }(),
- expected: "[orange::i][image: /path/img1.jpg][-:-:-]\n[orange::i][image: /path/img2.png][-:-:-]",
- },
- {
- name: "old format without path",
- index: 4,
- msg: RoleMsg{
- Role: "user",
- hasContentParts: true,
- ContentParts: []interface{}{
- map[string]interface{}{
- "type": "image_url",
- "image_url": map[string]interface{}{
- "url": "data:image/jpeg;base64,old",
- },
- },
- },
- },
- expected: "[orange::i][image: image][-:-:-]",
- },
- {
- name: "old format with path",
- index: 5,
- msg: RoleMsg{
- Role: "user",
- hasContentParts: true,
- ContentParts: []interface{}{
- map[string]interface{}{
- "type": "image_url",
- "path": "/old/path/photo.jpg",
- "image_url": map[string]interface{}{
- "url": "data:image/jpeg;base64,old",
- },
- },
- },
- },
- expected: "[orange::i][image: /old/path/photo.jpg][-:-:-]",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := tt.msg.ToText(tt.index)
- if !strings.Contains(result, tt.expected) {
- t.Errorf("ToText() result does not contain expected indicator\ngot: %s\nwant substring: %s", result, tt.expected)
- }
- // Ensure the indicator appears before text content
- if strings.Contains(tt.expected, "cat.jpg") && strings.Contains(result, "Look at this picture") {
- indicatorPos := strings.Index(result, "[orange::i][image: /home/user/Pictures/cat.jpg][-:-:-]")
- textPos := strings.Index(result, "Look at this picture")
- if indicatorPos == -1 || textPos == -1 || indicatorPos >= textPos {
- t.Errorf("image indicator should appear before text")
- }
- }
- })
- }
-}
-func TestExtractDisplayPath(t *testing.T) {
- // Save original base dir
- originalBaseDir := imageBaseDir
- defer func() { imageBaseDir = originalBaseDir }()
- tests := []struct {
- name string
- baseDir string
- path string
- expected string
- }{
- {
- name: "no base dir shows full path",
- baseDir: "",
- path: "/home/user/images/cat.jpg",
- expected: "/home/user/images/cat.jpg",
- },
- {
- name: "relative path within base dir",
- baseDir: "/home/user",
- path: "/home/user/images/cat.jpg",
- expected: "images/cat.jpg",
- },
- {
- name: "path outside base dir shows full path",
- baseDir: "/home/user",
- path: "/tmp/test.jpg",
- expected: "/tmp/test.jpg",
- },
- {
- name: "same directory",
- baseDir: "/home/user/images",
- path: "/home/user/images/cat.jpg",
- expected: "cat.jpg",
- },
- {
- name: "long path truncated",
- baseDir: "",
- path: "/very/long/path/to/a/really_long_filename_that_exceeds_sixty_characters_limit_yes_it_is_very_long.jpg",
- expected: "..._that_exceeds_sixty_characters_limit_yes_it_is_very_long.jpg",
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- imageBaseDir = tt.baseDir
- result := extractDisplayPath(tt.path)
- if result != tt.expected {
- t.Errorf("extractDisplayPath(%q) with baseDir=%q = %q, want %q",
- tt.path, tt.baseDir, result, tt.expected)
- }
- })
- }
-}