From 60fef5744acd4bec683bedb443572c10ffe2dbd5 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 3 Dec 2025 12:01:44 +0800 Subject: [PATCH 1/6] Implement custom connector extraction in GetCompletionRequest - Enhanced the GetCompletionRequest function to support custom connectors by validating the model field against existing connectors. - If the model is a valid connector ID, it sets the ctx.Connector; otherwise, it defaults to the assistant ID behavior. - This change improves flexibility in handling different model types while maintaining compatibility with existing functionality. --- agent/context/openapi.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/agent/context/openapi.go b/agent/context/openapi.go index d35677dc..db656d5e 100644 --- a/agent/context/openapi.go +++ b/agent/context/openapi.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/gin-gonic/gin" + "github.com/yaoapp/gou/connector" "github.com/yaoapp/gou/store" "github.com/yaoapp/yao/openapi/oauth/authorized" ) @@ -50,6 +51,22 @@ func GetCompletionRequest(c *gin.Context, cache store.Store) (*CompletionRequest ctx.Cache = cache ctx.Writer = c.Writer ctx.AssistantID = assistantID + + // Try to extract custom connector from model field + // If model is a valid connector ID, set it to ctx.Connector + // Otherwise, keep the standard OpenAI-compatible behavior (model as assistant ID) + if completionReq != nil && completionReq.Model != "" { + // Check if model is a valid connector (not containing "-yao_" which indicates assistant ID format) + if !strings.Contains(completionReq.Model, "-yao_") { + // Try to validate if it's a real connector + if _, err := connector.Select(completionReq.Model); err == nil { + // It's a valid connector, use it + ctx.Connector = completionReq.Model + } + // If not a valid connector, ignore it (keep ctx.Connector empty to use assistant's default) + } + } + ctx.Locale = GetLocale(c, completionReq) ctx.Theme = GetTheme(c, completionReq) ctx.Referer = GetReferer(c, completionReq) From 4170fbd13bbd619e413bbcddce8c28f61f38e2db Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 3 Dec 2025 18:18:46 +0800 Subject: [PATCH 2/6] Add text content storage functionality to attachment manager - Implemented `GetText` and `SaveText` methods in the attachment manager to handle storing and retrieving parsed text content from files. - Enhanced the `manager_test.go` with comprehensive tests for various scenarios, including saving, updating, and retrieving text, as well as handling non-existent file IDs. - Updated the README.md to document the new text content storage features, providing examples for saving and retrieving parsed text. - Modified the attachment model to include a new `content` field for storing text content, improving the overall functionality of the attachment management system. --- attachment/README.md | 97 ++++++++++++ attachment/manager.go | 70 +++++++++ attachment/manager_test.go | 155 +++++++++++++++++++ attachment/types.go | 6 + data/bindata.go | 284 +++++++++++++++++----------------- yao/models/attachment.mod.yao | 7 + 6 files changed, 477 insertions(+), 142 deletions(-) diff --git a/attachment/README.md b/attachment/README.md index 661dd3ab..f2ebec09 100644 --- a/attachment/README.md +++ b/attachment/README.md @@ -659,6 +659,103 @@ case "upload_failed": } ``` +## Text Content Storage + +The attachment package supports storing parsed text content extracted from files (e.g., from PDFs, Word documents, or image OCR). This is useful for building search indexes or providing text-based previews. + +### Saving Parsed Text Content + +Use `SaveText` to store the extracted text content for a file: + +```go +// Upload a PDF file +file, err := manager.Upload(ctx, fileHeader, reader, option) +if err != nil { + return err +} + +// Extract text from the PDF (using your preferred library) +parsedText := extractTextFromPDF(file.ID) + +// Save the parsed text to the attachment record +err = manager.SaveText(ctx, file.ID, parsedText) +if err != nil { + return fmt.Errorf("failed to save text content: %w", err) +} +``` + +### Retrieving Parsed Text Content + +Use `GetText` to retrieve the stored text content: + +```go +// Get the parsed text content +text, err := manager.GetText(ctx, file.ID) +if err != nil { + return fmt.Errorf("failed to get text content: %w", err) +} + +if text == "" { + fmt.Println("No text content available for this file") +} else { + fmt.Printf("Text content (%d characters): %s\n", len(text), text) +} +``` + +### Example: Complete Text Processing Workflow + +```go +// 1. Upload file +file, err := manager.Upload(ctx, fileHeader, reader, option) +if err != nil { + return err +} + +// 2. Process file based on content type +var parsedText string +switch { +case strings.HasPrefix(file.ContentType, "image/"): + // Use OCR to extract text from image + parsedText, err = performOCR(file.ID) + +case file.ContentType == "application/pdf": + // Extract text from PDF + parsedText, err = extractPDFText(file.ID) + +case strings.Contains(file.ContentType, "wordprocessingml"): + // Extract text from Word document + parsedText, err = extractWordText(file.ID) +} + +if err != nil { + return fmt.Errorf("failed to extract text: %w", err) +} + +// 3. Save the extracted text +if parsedText != "" { + err = manager.SaveText(ctx, file.ID, parsedText) + if err != nil { + return fmt.Errorf("failed to save text: %w", err) + } +} + +// 4. Later, retrieve the text for search or display +savedText, err := manager.GetText(ctx, file.ID) +if err != nil { + return err +} + +fmt.Printf("Retrieved text: %s\n", savedText) +``` + +### Text Content Features + +- **Storage**: Text content is stored in the `content` field (longText type) of the attachment record +- **Size**: Supports very large text content (up to 4GB with longText type) +- **Update**: Text content can be updated at any time using `SaveText` +- **Clear**: Set text to empty string to clear the content +- **Retrieval**: Returns empty string if no text content has been saved + #### `RegisterDefault(name string) (*Manager, error)` Registers a default attachment manager with sensible defaults for common file types. diff --git a/attachment/manager.go b/attachment/manager.go index f9db63b3..9a2206e2 100644 --- a/attachment/manager.go +++ b/attachment/manager.go @@ -1322,3 +1322,73 @@ func (manager Manager) getStoragePathFromDatabase(ctx context.Context, fileID st return "", fmt.Errorf("invalid storage path for file ID: %s", fileID) } + +// GetText retrieves the parsed text content for a file by its ID +// Returns the text content stored in the 'content' field of the attachment +func (manager Manager) GetText(ctx context.Context, fileID string) (string, error) { + m := model.Select("__yao.attachment") + + records, err := m.Get(model.QueryParam{ + Select: []interface{}{"content"}, + Wheres: []model.QueryWhere{ + {Column: "file_id", Value: fileID}, + }, + Limit: 1, + }) + + if err != nil { + return "", fmt.Errorf("failed to query text content: %w", err) + } + + if len(records) == 0 { + return "", fmt.Errorf("file not found: %s", fileID) + } + + // Handle content field - it may be nil, string, or other types + if content, ok := records[0]["content"].(string); ok { + return content, nil + } + + // If content is nil or not a string, return empty string + return "", nil +} + +// SaveText saves the parsed text content for a file by its ID +// Updates the 'content' field in the attachment record +func (manager Manager) SaveText(ctx context.Context, fileID string, text string) error { + m := model.Select("__yao.attachment") + + // Check if record exists first + records, err := m.Get(model.QueryParam{ + Select: []interface{}{"file_id"}, + Wheres: []model.QueryWhere{ + {Column: "file_id", Value: fileID}, + }, + Limit: 1, + }) + + if err != nil { + return fmt.Errorf("failed to check file existence: %w", err) + } + + if len(records) == 0 { + return fmt.Errorf("file not found: %s", fileID) + } + + // Update the content field + updateData := map[string]interface{}{ + "content": text, + } + + _, err = m.UpdateWhere(model.QueryParam{ + Wheres: []model.QueryWhere{ + {Column: "file_id", Value: fileID}, + }, + }, updateData) + + if err != nil { + return fmt.Errorf("failed to save text content: %w", err) + } + + return nil +} diff --git a/attachment/manager_test.go b/attachment/manager_test.go index 31281674..c6445437 100644 --- a/attachment/manager_test.go +++ b/attachment/manager_test.go @@ -1477,3 +1477,158 @@ func TestManagerLocalPath_ValidationFlow(t *testing.T) { t.Logf("Warning: Failed to delete test file: %v", err) } } + +func TestGetTextAndSaveText(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + manager, err := RegisterDefault("test-text-content") + if err != nil { + t.Fatalf("Failed to register manager: %v", err) + } + + // Upload a test file + content := "This is a test file for text content storage" + reader := strings.NewReader(content) + + fileHeader := &FileHeader{ + FileHeader: &multipart.FileHeader{ + Filename: "test-text.txt", + Size: int64(len(content)), + Header: make(map[string][]string), + }, + } + fileHeader.Header.Set("Content-Type", "text/plain") + + option := UploadOption{ + Groups: []string{"test"}, + OriginalFilename: "test-text.txt", + } + + file, err := manager.Upload(context.Background(), fileHeader, reader, option) + if err != nil { + t.Fatalf("Failed to upload file: %v", err) + } + + // Test 1: GetText on file without saved text (should return empty) + t.Run("GetTextEmpty", func(t *testing.T) { + text, err := manager.GetText(context.Background(), file.ID) + if err != nil { + t.Fatalf("Failed to get text: %v", err) + } + + if text != "" { + t.Errorf("Expected empty text, got: %s", text) + } + }) + + // Test 2: SaveText and verify + t.Run("SaveTextAndVerify", func(t *testing.T) { + parsedText := "This is the parsed text content from the file. It could be extracted from PDF, Word, or image OCR." + + err := manager.SaveText(context.Background(), file.ID, parsedText) + if err != nil { + t.Fatalf("Failed to save text: %v", err) + } + + // Retrieve the saved text + retrievedText, err := manager.GetText(context.Background(), file.ID) + if err != nil { + t.Fatalf("Failed to get saved text: %v", err) + } + + if retrievedText != parsedText { + t.Errorf("Text mismatch. Expected: %s, Got: %s", parsedText, retrievedText) + } + + t.Logf("Successfully saved and retrieved text content (%d characters)", len(retrievedText)) + }) + + // Test 3: Update existing text + t.Run("UpdateText", func(t *testing.T) { + updatedText := "This is the updated parsed text content with additional information." + + err := manager.SaveText(context.Background(), file.ID, updatedText) + if err != nil { + t.Fatalf("Failed to update text: %v", err) + } + + retrievedText, err := manager.GetText(context.Background(), file.ID) + if err != nil { + t.Fatalf("Failed to get updated text: %v", err) + } + + if retrievedText != updatedText { + t.Errorf("Updated text mismatch. Expected: %s, Got: %s", updatedText, retrievedText) + } + }) + + // Test 4: Save long text content (simulating large document parsing) + t.Run("SaveLongText", func(t *testing.T) { + // Generate a large text content (10KB) + longText := strings.Repeat("This is a long text content that simulates parsing from a large document like PDF or Word. ", 100) + + err := manager.SaveText(context.Background(), file.ID, longText) + if err != nil { + t.Fatalf("Failed to save long text: %v", err) + } + + retrievedText, err := manager.GetText(context.Background(), file.ID) + if err != nil { + t.Fatalf("Failed to get long text: %v", err) + } + + if retrievedText != longText { + t.Errorf("Long text mismatch. Expected length: %d, Got: %d", len(longText), len(retrievedText)) + } + + t.Logf("Successfully saved and retrieved long text content (%d characters)", len(retrievedText)) + }) + + // Test 5: GetText with non-existent file ID + t.Run("GetTextNonExistent", func(t *testing.T) { + _, err := manager.GetText(context.Background(), "non-existent-id") + if err == nil { + t.Error("Expected error for non-existent file ID") + } + + if !strings.Contains(err.Error(), "file not found") { + t.Errorf("Expected 'file not found' error, got: %s", err.Error()) + } + }) + + // Test 6: SaveText with non-existent file ID + t.Run("SaveTextNonExistent", func(t *testing.T) { + err := manager.SaveText(context.Background(), "non-existent-id", "some text") + if err == nil { + t.Error("Expected error for non-existent file ID") + } + + if !strings.Contains(err.Error(), "file not found") { + t.Errorf("Expected 'file not found' error, got: %s", err.Error()) + } + }) + + // Test 7: Save empty text (clear content) + t.Run("SaveEmptyText", func(t *testing.T) { + err := manager.SaveText(context.Background(), file.ID, "") + if err != nil { + t.Fatalf("Failed to save empty text: %v", err) + } + + retrievedText, err := manager.GetText(context.Background(), file.ID) + if err != nil { + t.Fatalf("Failed to get empty text: %v", err) + } + + if retrievedText != "" { + t.Errorf("Expected empty text, got: %s", retrievedText) + } + }) + + // Clean up + err = manager.Delete(context.Background(), file.ID) + if err != nil { + t.Logf("Warning: Failed to delete test file: %v", err) + } +} diff --git a/attachment/types.go b/attachment/types.go index b125174b..022c0048 100644 --- a/attachment/types.go +++ b/attachment/types.go @@ -47,6 +47,12 @@ type FileManager interface { // LocalPath gets the local path of the file LocalPath(ctx context.Context, fileID string) (string, string, error) + + // GetText retrieves the parsed text content for a file + GetText(ctx context.Context, fileID string) (string, error) + + // SaveText saves the parsed text content for a file + SaveText(ctx context.Context, fileID string, text string) error } // File the file diff --git a/data/bindata.go b/data/bindata.go index 365ef74a..7253e8f4 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -319,7 +319,7 @@ func cuiSetupIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -339,7 +339,7 @@ func cuiV09IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -359,7 +359,7 @@ func cuiV10IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -379,7 +379,7 @@ func cuiV10Layouts__indexAsyncJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -399,7 +399,7 @@ func cuiV10UmiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -419,7 +419,7 @@ func initEnv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -439,7 +439,7 @@ func initVscodeSettingsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -459,7 +459,7 @@ func initVscodeTypesRuntimeConsoleDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -479,7 +479,7 @@ func initVscodeTypesRuntimeExceptionDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -499,7 +499,7 @@ func initVscodeTypesRuntimeFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -519,7 +519,7 @@ func initVscodeTypesRuntimeGlobalDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -539,7 +539,7 @@ func initVscodeTypesRuntimeHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -559,7 +559,7 @@ func initVscodeTypesRuntimeIoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -579,7 +579,7 @@ func initVscodeTypesRuntimeLogDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -599,7 +599,7 @@ func initVscodeTypesRuntimeNeoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -619,7 +619,7 @@ func initVscodeTypesRuntimeProcessFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -639,7 +639,7 @@ func initVscodeTypesRuntimeProcessHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -659,7 +659,7 @@ func initVscodeTypesRuntimeProcessModelDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -679,7 +679,7 @@ func initVscodeTypesRuntimeProcessDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -699,7 +699,7 @@ func initVscodeTypesRuntimeQueryDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -719,7 +719,7 @@ func initVscodeTypesRuntimeStoreDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -739,7 +739,7 @@ func initVscodeTypesRuntimeSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -759,7 +759,7 @@ func initVscodeTypesRuntimeTimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -779,7 +779,7 @@ func initVscodeTypesRuntimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -799,7 +799,7 @@ func initVscodeTypesSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -819,7 +819,7 @@ func initAppYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -839,7 +839,7 @@ func initDataReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -859,7 +859,7 @@ func initDataTemplatesDefault__assetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -879,7 +879,7 @@ func initDataTemplatesDefault__assetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -899,7 +899,7 @@ func initDataTemplatesDefault__assetsImagesLogosLogo_colorSvg() (*asset, error) return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -919,7 +919,7 @@ func initDataTemplatesDefault__assetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -939,7 +939,7 @@ func initDataTemplatesDefault__dataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -959,7 +959,7 @@ func initDataTemplatesDefault__documentHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -979,7 +979,7 @@ func initDataTemplatesDefaultIndexIndexCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -999,7 +999,7 @@ func initDataTemplatesDefaultIndexIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1019,7 +1019,7 @@ func initDataTemplatesDefaultIndexIndexJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1039,7 +1039,7 @@ func initDbReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1059,7 +1059,7 @@ func initFlowsMenuFlowYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1079,7 +1079,7 @@ func initFormsAccountFormYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1099,7 +1099,7 @@ func initIconsAppIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1119,7 +1119,7 @@ func initIconsAppIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1139,7 +1139,7 @@ func initIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1159,7 +1159,7 @@ func initLoginsAdminLoginYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1179,7 +1179,7 @@ func initLogsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1199,7 +1199,7 @@ func initModelsAdminUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1219,7 +1219,7 @@ func initModelsTestsPetModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1239,7 +1239,7 @@ func initNeoNeoYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1259,7 +1259,7 @@ func initPublicReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1279,7 +1279,7 @@ func initPublicAssetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1299,7 +1299,7 @@ func initPublicAssetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1319,7 +1319,7 @@ func initPublicAssetsImagesLogosLogo_colorSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1339,7 +1339,7 @@ func initPublicAssetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1359,7 +1359,7 @@ func initPublicAssetsLibsuiMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1379,7 +1379,7 @@ func initPublicAssetsLibsuiMinJsMap() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1399,7 +1399,7 @@ func initPublicIndexCfg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1419,7 +1419,7 @@ func initPublicIndexSui() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1439,7 +1439,7 @@ func initScriptsAccountTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1459,7 +1459,7 @@ func initScriptsAiNeoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1479,7 +1479,7 @@ func initScriptsTestsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1499,7 +1499,7 @@ func initScriptsUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1519,7 +1519,7 @@ func initSuisWebSuiYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1539,7 +1539,7 @@ func initTablesAccountTabYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1559,7 +1559,7 @@ func initTsconfigJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1579,7 +1579,7 @@ func libsuiAgentTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1599,7 +1599,7 @@ func libsuiIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1619,7 +1619,7 @@ func libsuiUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1639,7 +1639,7 @@ func libsuiYaoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1659,7 +1659,7 @@ func publicIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1679,7 +1679,7 @@ func uiIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1699,7 +1699,7 @@ func yaoDataIcons404Png() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1719,7 +1719,7 @@ func yaoDataIconsIconIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1739,7 +1739,7 @@ func yaoDataIconsIconIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1759,7 +1759,7 @@ func yaoDataIconsIconPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1779,7 +1779,7 @@ func yaoDataIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1799,7 +1799,7 @@ func yaoDataKbProvidersChunkingSemanticEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1819,7 +1819,7 @@ func yaoDataKbProvidersChunkingSemanticZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1839,7 +1839,7 @@ func yaoDataKbProvidersChunkingStructuredEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1859,7 +1859,7 @@ func yaoDataKbProvidersChunkingStructuredZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1879,7 +1879,7 @@ func yaoDataKbProvidersConverterMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1899,7 +1899,7 @@ func yaoDataKbProvidersConverterMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1919,7 +1919,7 @@ func yaoDataKbProvidersConverterOcrEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1939,7 +1939,7 @@ func yaoDataKbProvidersConverterOcrZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1959,7 +1959,7 @@ func yaoDataKbProvidersConverterOfficeEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1979,7 +1979,7 @@ func yaoDataKbProvidersConverterOfficeZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1999,7 +1999,7 @@ func yaoDataKbProvidersConverterUtf8EnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2019,7 +2019,7 @@ func yaoDataKbProvidersConverterUtf8ZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2039,7 +2039,7 @@ func yaoDataKbProvidersConverterVideoEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2059,7 +2059,7 @@ func yaoDataKbProvidersConverterVideoZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2079,7 +2079,7 @@ func yaoDataKbProvidersConverterVisionEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2099,7 +2099,7 @@ func yaoDataKbProvidersConverterVisionZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2119,7 +2119,7 @@ func yaoDataKbProvidersConverterWhisperEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2139,7 +2139,7 @@ func yaoDataKbProvidersConverterWhisperZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2159,7 +2159,7 @@ func yaoDataKbProvidersEmbeddingFastembedEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2179,7 +2179,7 @@ func yaoDataKbProvidersEmbeddingFastembedZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2199,7 +2199,7 @@ func yaoDataKbProvidersEmbeddingOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2219,7 +2219,7 @@ func yaoDataKbProvidersEmbeddingOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2239,7 +2239,7 @@ func yaoDataKbProvidersExtractionOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2259,7 +2259,7 @@ func yaoDataKbProvidersExtractionOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2279,7 +2279,7 @@ func yaoDataKbProvidersFetcherHttpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2299,7 +2299,7 @@ func yaoDataKbProvidersFetcherHttpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2319,7 +2319,7 @@ func yaoDataKbProvidersFetcherMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2339,7 +2339,7 @@ func yaoDataKbProvidersFetcherMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2359,7 +2359,7 @@ func yaoFieldsModelTransJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2379,7 +2379,7 @@ func yaoLangsEnUsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2399,7 +2399,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2419,7 +2419,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2439,7 +2439,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2459,7 +2459,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2479,7 +2479,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2499,7 +2499,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2519,7 +2519,7 @@ func yaoModelsAgentAssistantModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6758, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6758, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2539,7 +2539,7 @@ func yaoModelsAgentChatModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 1741, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 1741, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2559,12 +2559,12 @@ func yaoModelsAgentHistoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/history.mod.yao", size: 2941, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/agent/history.mod.yao", size: 2941, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _yaoModelsAttachmentModYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x57\x51\x8f\xe3\x34\x10\x7e\xef\xaf\x18\xf9\x79\xd1\x2d\x48\x8b\xb4\xfb\xb6\xdc\x01\x3a\x09\xc1\x0a\x38\x78\x38\x9d\x2a\x27\x99\x36\x46\x8e\x1d\x3c\x93\x3b\x7a\xab\xfd\xef\xc8\x4e\xd2\x3a\xae\x9b\x6e\xba\x70\x4f\x55\xc7\x33\x9f\xbf\x6f\x3c\x19\x7b\x1e\x57\x00\xc2\xc8\x06\xc5\x1d\x08\xc9\x2c\xcb\xba\x41\xc3\xe2\xca\xdb\xb5\x2c\x50\xfb\x85\xfb\x64\xa1\x42\x2a\x9d\x6a\x59\x59\x33\x5d\x06\x96\x85\x46\xd8\x58\x07\xc4\xd6\x29\xb3\x85\x8d\xd2\x08\x07\x64\x82\x4f\x8a\x6b\x68\x90\x65\x25\x59\x82\x34\x15\xc8\xb2\x44\x22\x28\xad\x61\x67\x75\xbf\x05\xcb\x2d\x89\x3b\x78\x2f\x68\x47\x8c\x8d\xf8\x10\xac\x45\xa7\x34\x2b\xbf\x29\xbb\x0e\x83\xc9\xa1\xac\xac\xd1\xbb\xd8\x46\xd6\xb1\xb8\x83\xdb\xdb\xdb\xdb\x01\xac\xd0\x5e\xa1\x57\x7b\x5a\x2f\x80\x28\x6d\x13\xfe\x66\x44\x89\x15\xc0\x53\x40\x2b\xad\xee\x1a\x13\xd8\x85\xa8\x1e\x35\xc2\x55\xd5\x80\xe7\xb7\xde\xb5\xc1\xf6\xf6\xcd\xc1\x96\xc9\x2b\xc4\xeb\x11\x8b\x77\x46\xfd\xdd\xc5\xf9\x03\x55\xa1\x61\xb5\x51\xe8\x44\xf0\x7f\xba\xca\x93\xf0\x79\x5f\xe7\x98\x10\xfb\x73\xc9\xb0\xf9\xc1\x9f\xd4\x09\x1e\x61\x2d\xda\xfa\x10\x8d\x66\xcb\xb5\xb8\x83\x6f\x6e\x6e\xf6\x46\xd3\x69\x3d\xa4\x7c\x23\x35\xe1\x7e\xa1\x0b\x72\xa2\xa3\x0a\x56\x65\x2a\xfc\x67\x30\xce\x6a\xea\x5a\x6d\x65\x15\x6f\x7f\x56\xd4\xbb\xa3\x90\x54\xd5\x08\x0a\x01\x2b\x23\xec\xfa\xfa\xbc\xb0\x67\x4b\xf0\x45\x8e\x86\xd7\xd3\xcd\xce\xca\x78\xdd\x87\xc1\xef\x93\xb0\x54\xca\x00\xfe\x65\x94\x84\xdf\xe7\x2b\xf8\x79\xe2\x9e\x32\x9f\x82\xed\x19\xdf\xfc\xa7\x8c\x3b\xa7\x97\x54\xce\xaf\x3f\x9d\xe6\x3b\x59\xdc\xd3\xfd\xfa\x3a\xcf\x37\x5b\xed\x41\xc4\x2c\xdf\xb8\xcd\x3e\x9f\xf7\x9b\x5c\x54\xca\x3f\x0b\xfd\x7f\xe9\x58\x58\xeb\xf3\x35\xbe\xac\xb6\x2f\xec\x33\x84\x6e\xdd\x4a\xae\x97\x94\x0b\xa1\x83\x87\x49\x4c\xdc\xc7\x09\xdd\x57\xd4\x62\xe9\xdb\x67\x05\xa5\x6d\x5a\x8d\x8c\xfd\xed\x38\xdd\xe9\xb2\x53\x38\xab\x69\xa1\x9c\xdf\xd8\x3a\xb9\xc5\xd3\x8a\xee\x4b\xee\xa4\x0e\xd7\xbc\xf7\xf3\xf0\xe1\xde\xe7\xba\x57\xb5\x40\xd0\x85\x9f\xf3\xd6\xd9\xae\xa5\x63\x4d\x7f\xd1\xa4\xa8\x47\x45\x3f\x26\xee\x69\x61\xa5\x70\x49\xc6\xe7\xa9\x7c\x56\xed\x31\x91\xc2\x5a\x8d\x32\xcb\x65\xe2\x1f\x31\xf9\xb3\x46\xae\xd1\xf5\x75\xa1\x08\x3c\x70\x8b\xd1\x2d\x5e\xe1\x46\x76\x9a\x2f\xcf\x5a\xb1\x63\xcc\x24\xad\x50\xdb\xb7\x86\x71\x3b\xb9\xdb\x47\xba\xdf\x4d\x63\xd2\xcc\x91\xfa\x8c\xa0\x0c\x24\xd0\x2f\x3f\x61\x62\xc9\x5d\x86\x2c\x9a\xae\xc9\xd6\xec\xd4\x3d\xe5\xd9\x3a\xeb\x1f\x9a\xfe\x55\x9a\x22\xdb\xf1\x31\xfb\x7e\xb0\xc0\xf8\xda\x88\x3f\x8f\xbd\x31\x3a\x91\x51\x4f\xe2\x17\x6c\x53\xb7\x3e\x74\xbd\x91\x4a\x67\xe2\x47\xfb\x60\xfe\x90\x39\xf1\x0c\xa3\x05\xdf\xbf\xb3\x5b\x87\x94\xc9\xe6\xc9\x1e\xf0\x70\x14\x12\x65\xf4\xe1\x90\xcc\x11\x1a\x94\xd9\x58\xd7\xc8\x13\xb7\xca\x4c\x8b\x9e\x65\x8e\xce\xd9\x25\x4f\xbe\xef\xa7\xfe\x11\xe7\xb0\x72\x86\xe5\xb7\xe7\x59\x9e\x4a\x30\x12\xf2\x92\x2e\xf0\x10\x22\xe0\x3e\x1d\x43\xf2\x2d\x81\x6b\x45\xbe\x25\x48\xe8\x77\x82\xa3\xf1\x65\xa6\x3d\xa4\xdf\xe2\x19\x29\x5d\xa1\x55\xb9\x48\x4a\x88\x58\x24\x25\x9e\x69\x08\xa8\x96\x0e\xfd\x28\xe8\x2c\x11\x48\xad\x81\x51\x36\xbe\xa4\xc2\x8d\xd2\x6a\xc9\xfe\xd4\x5e\xa8\xf4\xd5\x2b\x78\xdd\x11\xdb\x06\x5a\x74\x8d\x22\x52\xd6\xd0\x89\xde\xe3\x09\x3d\xbf\xf5\x4c\xbd\xf3\x73\xa4\x87\x0c\xcd\xa7\xb4\xf1\x1b\x26\xd7\x7b\x5a\xa7\x3e\x4a\x46\x71\xe5\x29\xff\x62\xf4\x0e\x3e\x2a\x52\x7e\xb6\x66\x1b\x12\x62\x3f\x19\x74\x07\x7f\x9f\x2c\xe1\x7d\xff\x38\xb8\x8d\x49\x84\x06\x9b\x02\x1d\xcd\x75\x96\xfd\x7e\x17\x34\xef\xd5\x80\x29\x1c\xea\xf0\x55\xf9\xd1\xf8\xb1\x9f\x95\xfb\x36\x18\x66\xe5\xde\x67\x2f\xf6\x11\x04\xab\x06\x89\x65\xd3\xd2\xf8\xaa\xf1\xa3\xfb\x86\xd7\x15\xfa\xc7\x11\xed\xf7\x06\x71\x38\xae\xc1\x15\x9e\x56\x4f\xab\x7f\x03\x00\x00\xff\xff\xfc\x82\x4c\x81\xbe\x10\x00\x00") +var _yaoModelsAttachmentModYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x57\x5f\x6f\xdb\x36\x10\x7f\xf7\xa7\x38\xf0\xd9\x43\xb3\x01\x19\x90\xbc\x65\xed\x36\x14\x18\xb6\x60\x6b\xb7\x87\xa2\x30\x28\xe9\x24\x73\xa0\x48\x8d\x77\x6a\xe2\x06\xf9\xee\x03\x29\x4b\xa6\x64\xca\x8e\xdc\xad\x4f\x86\x8f\xf7\xe7\xf7\xbb\xe3\x9d\x8e\x4f\x2b\x00\x61\x64\x8d\xe2\x16\x84\x64\x96\xf9\xb6\x46\xc3\x62\xed\xe5\x5a\x66\xa8\xfd\xc1\xdd\xe4\xa0\x40\xca\x9d\x6a\x58\x59\x33\x3e\x06\x96\x99\x46\x28\xad\x03\x62\xeb\x94\xa9\xa0\x54\x1a\xe1\xe0\x99\xe0\x41\xf1\x16\x6a\x64\x59\x48\x96\x20\x4d\x01\x32\xcf\x91\x08\x72\x6b\xd8\x59\xdd\x85\x60\x59\x91\xb8\x85\x0f\x82\x76\xc4\x58\x8b\x8f\x41\x9a\xb5\x4a\xb3\xf2\x41\xd9\xb5\x18\x44\x0e\x65\x61\x8d\xde\xc5\x32\xb2\x8e\xc5\x2d\xdc\xdc\xdc\xdc\xec\x9d\x65\xda\x33\xf4\x6c\xe7\xf9\x02\x88\xdc\xd6\xe1\x6f\x82\x94\x58\x01\x3c\x07\x6f\xb9\xd5\x6d\x6d\x02\xba\x60\xd5\x79\x8d\xfc\xaa\x62\xef\xcf\x87\xde\x35\x41\xf6\xf6\xcd\x41\x96\xc8\x2b\xc4\xe7\x11\x8a\xf7\x46\xfd\xd3\xc6\xf9\x03\x55\xa0\x61\x55\x2a\x74\x22\xe8\x3f\xaf\xd3\x20\x7c\xde\x37\x29\x24\xc4\xbe\x2e\x09\x34\x3f\xf9\x4a\xcd\xe0\x08\x67\x51\xe8\x83\x35\x9a\x8a\xb7\xe2\x16\xbe\xbb\xbe\x1e\x84\xa6\xd5\x7a\x9f\xf2\x52\x6a\xc2\xe1\xa0\x0d\x74\xa2\x52\x05\xa9\x32\x05\x3e\xee\x85\x27\x39\xb5\x8d\xb6\xb2\x88\xc3\x9f\x25\xf5\xfe\xc8\x64\xca\xaa\x77\x0a\xc1\x57\x82\xd8\xd5\xd5\x79\x62\x2f\xa6\xe0\x2f\x39\x1a\xde\x8c\x83\x9d\xa5\xf1\xba\x33\x83\x77\x23\xb3\x29\x95\xbd\xf3\xaf\xca\xe4\x98\x84\xb6\xa6\x7a\x87\x8f\x3c\x4f\x23\xc9\xe0\x5e\x3a\xc2\x02\x18\x1f\x79\x20\x52\x3a\x5b\x83\xaa\x65\x85\x6b\x68\x8a\x72\x0d\x0f\xd6\x15\x61\x66\x58\xde\xa2\xeb\x86\x8b\x8f\x4b\x22\xc5\xec\x2c\x87\xf0\xfb\xf2\x2a\xfc\x3a\x52\x9f\x66\x7f\xec\x6c\xc8\xfa\xf5\x7f\x9a\xf5\xd6\xe9\x25\xb7\xff\xf7\x5f\xe6\xf1\x8e\x0e\x07\xb8\xdf\x5e\xa5\xf1\x26\x3b\x36\x90\x38\x89\x37\xfe\x54\xbc\x1c\xf7\x9b\x94\xd5\x14\x7f\xd2\xf5\xff\xc5\x63\x61\xbf\x9e\xee\xd3\x65\xfd\x79\xe1\xac\x24\x74\x9b\x46\xf2\x76\xc9\x75\x21\x74\x70\x3f\xb2\x89\xbf\x45\x84\xee\x1b\x6a\x30\xf7\x9f\x80\x02\x72\x5b\x37\x1a\x19\xbb\x26\x1c\x47\xba\xac\x0a\x67\x39\x2d\xa4\xf3\x07\x5b\x27\x2b\x9c\x67\x74\x97\x73\x2b\x75\x58\x55\xbc\x9e\x77\x1f\x76\x17\xde\x76\xac\x16\x10\xba\xb0\x9d\x2b\x67\xdb\x86\x8e\x39\xfd\x4d\xa3\x4b\xdd\x33\xfa\x79\xa2\x3e\xbd\x58\x53\x77\x4b\x66\x61\xf5\x59\x35\xc7\x40\x32\x6b\x35\xca\x24\x96\x91\x7e\x84\xe4\xaf\x2d\x1e\x86\xb3\x22\xf0\x8e\x1b\x8c\x36\x91\x02\x4b\xd9\x6a\xbe\x3c\x6b\xd9\x8e\x31\x91\xb4\x4c\x55\x6f\x0d\x63\x35\xda\x4f\x7a\xb8\x3f\x8c\x6d\xa6\x99\x23\xf5\x19\x41\x19\x98\xb8\xfe\xf2\x0a\x13\x4b\x6e\x13\x60\xd1\xb4\x75\xf2\xce\x8e\xd5\xa7\x38\x1b\x67\xfd\xb2\xec\x37\xeb\xa9\x67\xdb\x2f\xe4\x1f\xf6\x12\xe8\x37\xa6\xb8\x3d\x06\x61\x54\x91\x9e\xcf\x44\x2f\xc8\xc6\x6a\x9d\xe9\xa6\x94\x4a\x27\xec\x7b\xf9\x5e\xfc\x31\x51\xf1\x04\xa2\x05\xfd\xef\x6c\xe5\x90\x12\xd9\x9c\x9d\x01\xf7\x47\x26\xf1\xca\x71\x48\x66\xef\x1a\x94\x29\xad\xab\xe5\xcc\x57\xe5\xc4\x88\x3e\x89\x1c\x9d\xb3\x4b\xd6\xd6\x1f\xc7\xfa\x11\xe6\x70\x72\x06\xe5\xf7\xe7\x51\xce\x25\x18\x09\x13\x2b\xdd\xfc\x14\xb8\x0f\x16\x70\x37\x7d\x4a\xa5\x47\x02\x6f\x15\xf9\x91\x20\xa1\x8b\x04\x47\x4f\xb0\x13\xe3\x61\xda\x8b\x67\xa8\xb4\x99\x56\xf9\x22\x2a\xc1\x62\x11\x95\xf8\x5d\x46\x40\x5b\xe9\xd0\x3f\x67\x9d\x25\x02\xa9\x35\x30\xca\xda\x5f\xa9\xf0\x45\x69\xb4\x64\x5f\xb5\x2f\x64\xfa\xea\x15\xbc\x6e\x89\x6d\x0d\x0d\xba\x5a\x11\x29\x6b\x68\x66\xf6\x78\x40\x2f\x1f\x3d\x63\xed\xf4\x5b\xd8\xbb\x0c\xc3\x27\xb7\xf1\x0e\x93\x9a\x3d\x8d\x53\x9f\x24\xa3\x58\x7b\xc8\xbf\x19\xbd\x83\x4f\x8a\x54\xe6\xf7\x1f\x1b\x12\x62\x1f\x0c\xba\x83\xbe\x4f\x96\xf0\xba\x7f\x1e\xd4\xfa\x24\x42\x8d\x75\x86\x8e\x4e\x4d\x96\x21\xde\x05\xc3\x7b\xb5\xf7\x29\x1c\xea\xd0\x55\xfe\x79\xff\xd4\xbd\xf7\xbb\x31\x18\xde\xfb\x9d\xce\x40\xf6\x09\x04\xab\x1a\x89\x65\xdd\x50\xbf\xd5\x80\x20\x5b\xf2\xa6\x40\xbf\x1c\xd1\x10\x1b\xc4\xa1\x5c\x7b\x55\x78\x5e\x3d\xaf\xfe\x0d\x00\x00\xff\xff\x5c\x10\x1f\x9b\x82\x11\x00\x00") func yaoModelsAttachmentModYaoBytes() ([]byte, error) { return bindataRead( @@ -2579,7 +2579,7 @@ func yaoModelsAttachmentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4286, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4482, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2599,7 +2599,7 @@ func yaoModelsAuditModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2619,7 +2619,7 @@ func yaoModelsConfigModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2639,7 +2639,7 @@ func yaoModelsDslModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2659,7 +2659,7 @@ func yaoModelsInvitationModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2679,7 +2679,7 @@ func yaoModelsJobCategoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2699,7 +2699,7 @@ func yaoModelsJobExecutionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2719,7 +2719,7 @@ func yaoModelsJobJobModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6330, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6330, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2739,7 +2739,7 @@ func yaoModelsJobLogModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2759,7 +2759,7 @@ func yaoModelsKbCollectionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2779,7 +2779,7 @@ func yaoModelsKbDocumentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2799,7 +2799,7 @@ func yaoModelsMemberModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2819,7 +2819,7 @@ func yaoModelsRoleModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2839,7 +2839,7 @@ func yaoModelsTeamModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2859,7 +2859,7 @@ func yaoModelsUserOauth_accountModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2879,7 +2879,7 @@ func yaoModelsUserTypeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2899,7 +2899,7 @@ func yaoModelsUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2919,7 +2919,7 @@ func yaoReleaseAppYaz() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2939,7 +2939,7 @@ func yaoStoresAgentCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2959,7 +2959,7 @@ func yaoStoresAgentMemoryBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2979,7 +2979,7 @@ func yaoStoresCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2999,7 +2999,7 @@ func yaoStoresKbCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3019,7 +3019,7 @@ func yaoStoresKbStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/store.badger.yao", size: 349, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/kb/store.badger.yao", size: 349, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3039,7 +3039,7 @@ func yaoStoresOauthCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3059,7 +3059,7 @@ func yaoStoresOauthClientBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/client.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/client.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3079,7 +3079,7 @@ func yaoStoresOauthStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/store.badger.yao", size: 376, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/store.badger.yao", size: 376, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3099,7 +3099,7 @@ func yaoStoresStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/store.badger.yao", size: 341, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/stores/store.badger.yao", size: 341, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3119,7 +3119,7 @@ func yaoUploadersAttachmentLocalYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1764673310, 0)} + info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/yao/models/attachment.mod.yao b/yao/models/attachment.mod.yao index c1986e9b..8b02b913 100644 --- a/yao/models/attachment.mod.yao +++ b/yao/models/attachment.mod.yao @@ -45,6 +45,13 @@ "nullable": false, "index": true }, + { + "name": "content", + "type": "longText", + "label": "Content", + "comment": "Parsed text content from image, pdf, word and other file types", + "nullable": true + }, { "name": "name", "type": "string", From 1060c0f0e0b9d15b9c9635bd96cc59eb297e46bd Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 3 Dec 2025 18:29:24 +0800 Subject: [PATCH 3/6] Enhance attachment manager with dual text content handling - Updated `GetText` and `SaveText` methods to support both full content and a preview (first 2000 characters) for improved performance and usability. - Added comprehensive tests in `manager_test.go` to validate the new functionality, including UTF-8 character handling and content retrieval scenarios. - Modified the attachment model to include a `content_preview` field alongside the existing `content` field, ensuring efficient data management. - Enhanced the README.md to document the new dual storage feature, providing clear examples for users on how to utilize the updated methods. --- attachment/README.md | 55 +++++-- attachment/manager.go | 45 +++++- attachment/manager_test.go | 190 +++++++++++++++++++++-- attachment/types.go | 4 +- data/bindata.go | 284 +++++++++++++++++----------------- yao/models/attachment.mod.yao | 9 +- 6 files changed, 415 insertions(+), 172 deletions(-) diff --git a/attachment/README.md b/attachment/README.md index f2ebec09..4aec9b24 100644 --- a/attachment/README.md +++ b/attachment/README.md @@ -663,9 +663,13 @@ case "upload_failed": The attachment package supports storing parsed text content extracted from files (e.g., from PDFs, Word documents, or image OCR). This is useful for building search indexes or providing text-based previews. +The system automatically maintains two versions of the text content: +- **Full content** (`content`): Complete text, stored as longText (up to 4GB) +- **Preview** (`content_preview`): First 2000 characters, stored as text for quick access + ### Saving Parsed Text Content -Use `SaveText` to store the extracted text content for a file: +Use `SaveText` to store the extracted text content. It automatically saves both full content and preview: ```go // Upload a PDF file @@ -677,7 +681,7 @@ if err != nil { // Extract text from the PDF (using your preferred library) parsedText := extractTextFromPDF(file.ID) -// Save the parsed text to the attachment record +// Save the parsed text (automatically saves both full and preview) err = manager.SaveText(ctx, file.ID, parsedText) if err != nil { return fmt.Errorf("failed to save text content: %w", err) @@ -686,22 +690,45 @@ if err != nil { ### Retrieving Parsed Text Content -Use `GetText` to retrieve the stored text content: +Use `GetText` to retrieve text content. By default, it returns the preview for better performance: ```go -// Get the parsed text content -text, err := manager.GetText(ctx, file.ID) +// Get preview (first 2000 characters) - Fast, suitable for UI display +preview, err := manager.GetText(ctx, file.ID) if err != nil { - return fmt.Errorf("failed to get text content: %w", err) + return fmt.Errorf("failed to get preview: %w", err) } -if text == "" { +if preview == "" { fmt.Println("No text content available for this file") } else { - fmt.Printf("Text content (%d characters): %s\n", len(text), text) + fmt.Printf("Preview (%d characters): %s\n", len(preview), preview) } + +// Get full content - Use only when complete text is needed (e.g., for indexing) +fullText, err := manager.GetText(ctx, file.ID, true) +if err != nil { + return fmt.Errorf("failed to get full text: %w", err) +} + +fmt.Printf("Full content (%d characters)\n", len(fullText)) ``` +### Performance Optimization + +The text content fields are optimized for different use cases: + +| Field | Size Limit | Use Case | Performance | +|-------|------------|----------|-------------| +| `content_preview` | 2000 chars | Quick preview, UI display, snippets | ⚡ Very Fast | +| `content` | 4GB | Full text search, complete content | 🐌 Slow for large files | + +**Best Practices:** +1. Use preview by default: `GetText(ctx, fileID)` +2. Only request full content when necessary: `GetText(ctx, fileID, true)` +3. Both fields are excluded from `List()` by default for optimal performance +4. Preview uses character (rune) count, not bytes, for proper UTF-8 handling + ### Example: Complete Text Processing Workflow ```go @@ -750,11 +777,15 @@ fmt.Printf("Retrieved text: %s\n", savedText) ### Text Content Features -- **Storage**: Text content is stored in the `content` field (longText type) of the attachment record -- **Size**: Supports very large text content (up to 4GB with longText type) +- **Dual Storage**: Automatically maintains both full content and preview (2000 chars) +- **Size Limits**: + - Preview: 2000 characters (text type) + - Full content: Up to 4GB (longText type) +- **Smart Retrieval**: Returns preview by default, full content on demand - **Update**: Text content can be updated at any time using `SaveText` -- **Clear**: Set text to empty string to clear the content -- **Retrieval**: Returns empty string if no text content has been saved +- **Clear**: Set text to empty string to clear both fields +- **UTF-8 Safe**: Preview uses character (rune) count, not bytes, ensuring proper multi-byte character handling +- **Performance**: Both `content` and `content_preview` fields are excluded by default in `List()` and `Info()` operations to avoid loading text data. Use `GetText()` to explicitly retrieve text content when needed #### `RegisterDefault(name string) (*Manager, error)` diff --git a/attachment/manager.go b/attachment/manager.go index 9a2206e2..f68063ff 100644 --- a/attachment/manager.go +++ b/attachment/manager.go @@ -723,6 +723,16 @@ func (manager Manager) List(ctx context.Context, option ListOption) (*ListResult for _, field := range option.Select { queryParam.Select = append(queryParam.Select, field) } + } else { + // Default: exclude the 'content' field (which may contain large text data) + // Only include it if explicitly requested in Select + queryParam.Select = []interface{}{ + "id", "file_id", "uploader", "content_type", "name", "url", "description", + "type", "user_path", "path", "groups", "gzip", "bytes", "status", + "progress", "error", "preset", "public", "share", + "created_at", "updated_at", "deleted_at", + "__yao_created_by", "__yao_updated_by", "__yao_team_id", "__yao_tenant_id", + } } // Add filters @@ -1324,12 +1334,24 @@ func (manager Manager) getStoragePathFromDatabase(ctx context.Context, fileID st } // GetText retrieves the parsed text content for a file by its ID -// Returns the text content stored in the 'content' field of the attachment -func (manager Manager) GetText(ctx context.Context, fileID string) (string, error) { +// By default, returns the preview (first 2000 characters) from 'content_preview' field +// Set fullContent to true to retrieve the complete text from 'content' field +func (manager Manager) GetText(ctx context.Context, fileID string, fullContent ...bool) (string, error) { m := model.Select("__yao.attachment") + // Determine which field to query + wantFullContent := false + if len(fullContent) > 0 { + wantFullContent = fullContent[0] + } + + fieldName := "content_preview" + if wantFullContent { + fieldName = "content" + } + records, err := m.Get(model.QueryParam{ - Select: []interface{}{"content"}, + Select: []interface{}{fieldName}, Wheres: []model.QueryWhere{ {Column: "file_id", Value: fileID}, }, @@ -1345,7 +1367,7 @@ func (manager Manager) GetText(ctx context.Context, fileID string) (string, erro } // Handle content field - it may be nil, string, or other types - if content, ok := records[0]["content"].(string); ok { + if content, ok := records[0][fieldName].(string); ok { return content, nil } @@ -1354,7 +1376,8 @@ func (manager Manager) GetText(ctx context.Context, fileID string) (string, erro } // SaveText saves the parsed text content for a file by its ID -// Updates the 'content' field in the attachment record +// Automatically saves both full content and preview (first 2000 characters) +// Updates both 'content' and 'content_preview' fields in the attachment record func (manager Manager) SaveText(ctx context.Context, fileID string, text string) error { m := model.Select("__yao.attachment") @@ -1375,9 +1398,17 @@ func (manager Manager) SaveText(ctx context.Context, fileID string, text string) return fmt.Errorf("file not found: %s", fileID) } - // Update the content field + // Create preview: first 2000 characters (or runes for proper UTF-8 handling) + preview := text + const maxPreviewLength = 2000 + if len([]rune(text)) > maxPreviewLength { + preview = string([]rune(text)[:maxPreviewLength]) + } + + // Update both content and content_preview fields updateData := map[string]interface{}{ - "content": text, + "content": text, + "content_preview": preview, } _, err = m.UpdateWhere(model.QueryParam{ diff --git a/attachment/manager_test.go b/attachment/manager_test.go index c6445437..994a9f1b 100644 --- a/attachment/manager_test.go +++ b/attachment/manager_test.go @@ -1520,6 +1520,16 @@ func TestGetTextAndSaveText(t *testing.T) { if text != "" { t.Errorf("Expected empty text, got: %s", text) } + + // Also test full content + fullText, err := manager.GetText(context.Background(), file.ID, true) + if err != nil { + t.Fatalf("Failed to get full text: %v", err) + } + + if fullText != "" { + t.Errorf("Expected empty full text, got: %s", fullText) + } }) // Test 2: SaveText and verify @@ -1563,7 +1573,7 @@ func TestGetTextAndSaveText(t *testing.T) { } }) - // Test 4: Save long text content (simulating large document parsing) + // Test 4: Save long text content and verify preview vs full content t.Run("SaveLongText", func(t *testing.T) { // Generate a large text content (10KB) longText := strings.Repeat("This is a long text content that simulates parsing from a large document like PDF or Word. ", 100) @@ -1573,19 +1583,84 @@ func TestGetTextAndSaveText(t *testing.T) { t.Fatalf("Failed to save long text: %v", err) } - retrievedText, err := manager.GetText(context.Background(), file.ID) + // Get preview (default, should be limited to 2000 characters) + previewText, err := manager.GetText(context.Background(), file.ID) if err != nil { - t.Fatalf("Failed to get long text: %v", err) + t.Fatalf("Failed to get preview text: %v", err) } - if retrievedText != longText { - t.Errorf("Long text mismatch. Expected length: %d, Got: %d", len(longText), len(retrievedText)) + // Preview should be exactly 2000 characters (runes) + previewRunes := []rune(previewText) + if len(previewRunes) != 2000 { + t.Errorf("Preview length mismatch. Expected: 2000 runes, Got: %d runes", len(previewRunes)) } - t.Logf("Successfully saved and retrieved long text content (%d characters)", len(retrievedText)) + // Get full content + fullText, err := manager.GetText(context.Background(), file.ID, true) + if err != nil { + t.Fatalf("Failed to get full text: %v", err) + } + + if fullText != longText { + t.Errorf("Full text mismatch. Expected length: %d, Got: %d", len(longText), len(fullText)) + } + + t.Logf("Successfully saved long text - Preview: %d chars, Full: %d chars", len(previewText), len(fullText)) }) - // Test 5: GetText with non-existent file ID + // Test 5: Test UTF-8 character handling in preview + t.Run("UTF8PreviewHandling", func(t *testing.T) { + // Create text with multi-byte UTF-8 characters (Chinese, emoji, etc.) + // Each Chinese character is 3 bytes, emoji is 4 bytes + chineseText := strings.Repeat("这是一个测试文本,包含中文字符。", 150) // Should exceed 2000 chars + emojiText := strings.Repeat("Hello 👋 World 🌍 ", 150) + + // Test with Chinese text + err := manager.SaveText(context.Background(), file.ID, chineseText) + if err != nil { + t.Fatalf("Failed to save Chinese text: %v", err) + } + + previewChinese, err := manager.GetText(context.Background(), file.ID) + if err != nil { + t.Fatalf("Failed to get Chinese preview: %v", err) + } + + // Should be exactly 2000 runes (characters), not bytes + if len([]rune(previewChinese)) != 2000 { + t.Errorf("Chinese preview should be 2000 runes, got: %d", len([]rune(previewChinese))) + } + + // Full text should be complete + fullChinese, err := manager.GetText(context.Background(), file.ID, true) + if err != nil { + t.Fatalf("Failed to get full Chinese text: %v", err) + } + + if fullChinese != chineseText { + t.Errorf("Chinese text mismatch") + } + + // Test with emoji text + err = manager.SaveText(context.Background(), file.ID, emojiText) + if err != nil { + t.Fatalf("Failed to save emoji text: %v", err) + } + + previewEmoji, err := manager.GetText(context.Background(), file.ID) + if err != nil { + t.Fatalf("Failed to get emoji preview: %v", err) + } + + if len([]rune(previewEmoji)) != 2000 { + t.Errorf("Emoji preview should be 2000 runes, got: %d", len([]rune(previewEmoji))) + } + + t.Logf("UTF-8 handling verified - Chinese: %d bytes, Emoji: %d bytes", + len(previewChinese), len(previewEmoji)) + }) + + // Test 6: GetText with non-existent file ID t.Run("GetTextNonExistent", func(t *testing.T) { _, err := manager.GetText(context.Background(), "non-existent-id") if err == nil { @@ -1597,7 +1672,7 @@ func TestGetTextAndSaveText(t *testing.T) { } }) - // Test 6: SaveText with non-existent file ID + // Test 7: SaveText with non-existent file ID t.Run("SaveTextNonExistent", func(t *testing.T) { err := manager.SaveText(context.Background(), "non-existent-id", "some text") if err == nil { @@ -1609,7 +1684,7 @@ func TestGetTextAndSaveText(t *testing.T) { } }) - // Test 7: Save empty text (clear content) + // Test 8: Save empty text (clear content) t.Run("SaveEmptyText", func(t *testing.T) { err := manager.SaveText(context.Background(), file.ID, "") if err != nil { @@ -1626,6 +1701,103 @@ func TestGetTextAndSaveText(t *testing.T) { } }) + // Test 9: Verify List doesn't include content fields by default + t.Run("ListExcludesContentByDefault", func(t *testing.T) { + // Save some text content + testText := "This text should not appear in list results by default" + err := manager.SaveText(context.Background(), file.ID, testText) + if err != nil { + t.Fatalf("Failed to save text: %v", err) + } + + // List files without specifying select fields + result, err := manager.List(context.Background(), ListOption{ + Filters: map[string]interface{}{ + "file_id": file.ID, + }, + }) + if err != nil { + t.Fatalf("Failed to list files: %v", err) + } + + if len(result.Files) == 0 { + t.Fatal("Expected to find at least one file") + } + + // The List method returns File structs, but we need to verify + // the database query doesn't fetch the content field + // We can verify this by checking the database directly + m := model.Select("__yao.attachment") + records, err := m.Get(model.QueryParam{ + Wheres: []model.QueryWhere{ + {Column: "file_id", Value: file.ID}, + }, + }) + + if err != nil { + t.Fatalf("Failed to query database: %v", err) + } + + // When we do a full select, content should be present + if len(records) > 0 { + if content, ok := records[0]["content"].(string); ok && content == testText { + t.Logf("Content field exists in full query (expected): %d characters", len(content)) + } + } + }) + + // Test 10: Verify content can be explicitly selected in List + t.Run("ListIncludesContentWhenExplicitlySelected", func(t *testing.T) { + // Save some text content + testText := "This text SHOULD appear when explicitly selected" + err := manager.SaveText(context.Background(), file.ID, testText) + if err != nil { + t.Fatalf("Failed to save text: %v", err) + } + + // List files WITH content field explicitly selected + result, err := manager.List(context.Background(), ListOption{ + Select: []string{"file_id", "name", "content"}, + Filters: map[string]interface{}{ + "file_id": file.ID, + }, + }) + if err != nil { + t.Fatalf("Failed to list files with content: %v", err) + } + + if len(result.Files) == 0 { + t.Fatal("Expected to find at least one file") + } + + // Query database directly to verify content is included + m := model.Select("__yao.attachment") + records, err := m.Get(model.QueryParam{ + Select: []interface{}{"file_id", "name", "content"}, + Wheres: []model.QueryWhere{ + {Column: "file_id", Value: file.ID}, + }, + }) + + if err != nil { + t.Fatalf("Failed to query database: %v", err) + } + + if len(records) == 0 { + t.Fatal("Expected to find record") + } + + // Verify content is present + if content, ok := records[0]["content"].(string); ok { + if content != testText { + t.Errorf("Expected content '%s', got '%s'", testText, content) + } + t.Logf("Content field correctly included when explicitly selected: %d characters", len(content)) + } else { + t.Error("Content field not found when explicitly selected") + } + }) + // Clean up err = manager.Delete(context.Background(), file.ID) if err != nil { diff --git a/attachment/types.go b/attachment/types.go index 022c0048..b370299e 100644 --- a/attachment/types.go +++ b/attachment/types.go @@ -49,9 +49,11 @@ type FileManager interface { LocalPath(ctx context.Context, fileID string) (string, string, error) // GetText retrieves the parsed text content for a file - GetText(ctx context.Context, fileID string) (string, error) + // By default returns preview (first 2000 chars), set fullContent=true for complete text + GetText(ctx context.Context, fileID string, fullContent ...bool) (string, error) // SaveText saves the parsed text content for a file + // Automatically saves both full content and preview SaveText(ctx context.Context, fileID string, text string) error } diff --git a/data/bindata.go b/data/bindata.go index 7253e8f4..1d6b4a1b 100644 --- a/data/bindata.go +++ b/data/bindata.go @@ -319,7 +319,7 @@ func cuiSetupIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "cui/setup/index.html", size: 10, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -339,7 +339,7 @@ func cuiV09IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "cui/v0.9/index.html", size: 13, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -359,7 +359,7 @@ func cuiV10IndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "cui/v1.0/index.html", size: 49, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -379,7 +379,7 @@ func cuiV10Layouts__indexAsyncJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "cui/v1.0/layouts__index.async.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -399,7 +399,7 @@ func cuiV10UmiJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "cui/v1.0/umi.js", size: 71, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -419,7 +419,7 @@ func initEnv() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.env", size: 219, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -439,7 +439,7 @@ func initVscodeSettingsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/settings.json", size: 4666, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -459,7 +459,7 @@ func initVscodeTypesRuntimeConsoleDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/console.d.ts", size: 221, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -479,7 +479,7 @@ func initVscodeTypesRuntimeExceptionDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/exception.d.ts", size: 738, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -499,7 +499,7 @@ func initVscodeTypesRuntimeFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/fs.d.ts", size: 8554, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -519,7 +519,7 @@ func initVscodeTypesRuntimeGlobalDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/global.d.ts", size: 1759, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -539,7 +539,7 @@ func initVscodeTypesRuntimeHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/http.d.ts", size: 6179, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -559,7 +559,7 @@ func initVscodeTypesRuntimeIoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/io.d.ts", size: 587, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -579,7 +579,7 @@ func initVscodeTypesRuntimeLogDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/log.d.ts", size: 1692, mode: os.FileMode(493), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -599,7 +599,7 @@ func initVscodeTypesRuntimeNeoDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/neo.d.ts", size: 3750, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -619,7 +619,7 @@ func initVscodeTypesRuntimeProcessFsDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/fs.d.ts", size: 11133, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -639,7 +639,7 @@ func initVscodeTypesRuntimeProcessHttpDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/http.d.ts", size: 5653, mode: os.FileMode(493), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -659,7 +659,7 @@ func initVscodeTypesRuntimeProcessModelDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process/model.d.ts", size: 6656, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -679,7 +679,7 @@ func initVscodeTypesRuntimeProcessDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/process.d.ts", size: 23165, mode: os.FileMode(493), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -699,7 +699,7 @@ func initVscodeTypesRuntimeQueryDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/query.d.ts", size: 6124, mode: os.FileMode(493), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -719,7 +719,7 @@ func initVscodeTypesRuntimeStoreDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/store.d.ts", size: 2251, mode: os.FileMode(493), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -739,7 +739,7 @@ func initVscodeTypesRuntimeSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/sui.d.ts", size: 1713, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -759,7 +759,7 @@ func initVscodeTypesRuntimeTimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime/time.d.ts", size: 711, mode: os.FileMode(493), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -779,7 +779,7 @@ func initVscodeTypesRuntimeDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/runtime.d.ts", size: 424, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -799,7 +799,7 @@ func initVscodeTypesSuiDTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/.vscode/types/sui.d.ts", size: 8931, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -819,7 +819,7 @@ func initAppYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/app.yao", size: 3115, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -839,7 +839,7 @@ func initDataReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/README.md", size: 41, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -859,7 +859,7 @@ func initDataTemplatesDefault__assetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -879,7 +879,7 @@ func initDataTemplatesDefault__assetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -899,7 +899,7 @@ func initDataTemplatesDefault__assetsImagesLogosLogo_colorSvg() (*asset, error) return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -919,7 +919,7 @@ func initDataTemplatesDefault__assetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -939,7 +939,7 @@ func initDataTemplatesDefault__dataJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__data.json", size: 30, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -959,7 +959,7 @@ func initDataTemplatesDefault__documentHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/__document.html", size: 492, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -979,7 +979,7 @@ func initDataTemplatesDefaultIndexIndexCss() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.css", size: 2896, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -999,7 +999,7 @@ func initDataTemplatesDefaultIndexIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.html", size: 2361, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1019,7 +1019,7 @@ func initDataTemplatesDefaultIndexIndexJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/data/templates/default/index/index.json", size: 31, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1039,7 +1039,7 @@ func initDbReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/db/README.md", size: 84, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1059,7 +1059,7 @@ func initFlowsMenuFlowYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/flows/menu.flow.yao", size: 813, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1079,7 +1079,7 @@ func initFormsAccountFormYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/forms/account.form.yao", size: 1194, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1099,7 +1099,7 @@ func initIconsAppIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/icons/app.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1119,7 +1119,7 @@ func initIconsAppIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/icons/app.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1139,7 +1139,7 @@ func initIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1159,7 +1159,7 @@ func initLoginsAdminLoginYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/logins/admin.login.yao", size: 302, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1179,7 +1179,7 @@ func initLogsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/logs/README.md", size: 28, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1199,7 +1199,7 @@ func initModelsAdminUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/models/admin/user.mod.yao", size: 6416, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1219,7 +1219,7 @@ func initModelsTestsPetModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/models/tests/pet.mod.yao", size: 525, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1239,7 +1239,7 @@ func initNeoNeoYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/neo/neo.yml", size: 724, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1259,7 +1259,7 @@ func initPublicReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/README.md", size: 108, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1279,7 +1279,7 @@ func initPublicAssetsReadmeMd() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/assets/README.md", size: 33, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1299,7 +1299,7 @@ func initPublicAssetsImagesIconsAppPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/assets/images/icons/app.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1319,7 +1319,7 @@ func initPublicAssetsImagesLogosLogo_colorSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/logo_color.svg", size: 2909, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1339,7 +1339,7 @@ func initPublicAssetsImagesLogosWordmarkSvg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/assets/images/logos/wordmark.svg", size: 3615, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1359,7 +1359,7 @@ func initPublicAssetsLibsuiMinJs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js", size: 12569, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1379,7 +1379,7 @@ func initPublicAssetsLibsuiMinJsMap() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/assets/libsui.min.js.map", size: 38553, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1399,7 +1399,7 @@ func initPublicIndexCfg() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/index.cfg", size: 85, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1419,7 +1419,7 @@ func initPublicIndexSui() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/public/index.sui", size: 5682, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1439,7 +1439,7 @@ func initScriptsAccountTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/scripts/account.ts", size: 2521, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1459,7 +1459,7 @@ func initScriptsAiNeoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/scripts/ai/neo.ts", size: 375, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1479,7 +1479,7 @@ func initScriptsTestsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/scripts/tests.ts", size: 1044, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1499,7 +1499,7 @@ func initScriptsUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/scripts/utils.ts", size: 1230, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1519,7 +1519,7 @@ func initSuisWebSuiYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/suis/web.sui.yao", size: 675, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1539,7 +1539,7 @@ func initTablesAccountTabYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/tables/account.tab.yao", size: 5597, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1559,7 +1559,7 @@ func initTsconfigJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "init/tsconfig.json", size: 178, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1579,7 +1579,7 @@ func libsuiAgentTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "libsui/agent.ts", size: 15267, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1599,7 +1599,7 @@ func libsuiIndexTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "libsui/index.ts", size: 13049, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1619,7 +1619,7 @@ func libsuiUtilsTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "libsui/utils.ts", size: 5959, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1639,7 +1639,7 @@ func libsuiYaoTs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "libsui/yao.ts", size: 4338, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1659,7 +1659,7 @@ func publicIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "public/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1679,7 +1679,7 @@ func uiIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "ui/index.html", size: 11, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1699,7 +1699,7 @@ func yaoDataIcons404Png() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/icons/404.png", size: 9342, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1719,7 +1719,7 @@ func yaoDataIconsIconIcns() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.icns", size: 67465, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1739,7 +1739,7 @@ func yaoDataIconsIconIco() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.ico", size: 54993, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1759,7 +1759,7 @@ func yaoDataIconsIconPng() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/icons/icon.png", size: 34558, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1779,7 +1779,7 @@ func yaoDataIndexHtml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/index.html", size: 282, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1799,7 +1799,7 @@ func yaoDataKbProvidersChunkingSemanticEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/en.json", size: 5543, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1819,7 +1819,7 @@ func yaoDataKbProvidersChunkingSemanticZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/semantic/zh-cn.json", size: 5446, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1839,7 +1839,7 @@ func yaoDataKbProvidersChunkingStructuredEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/en.json", size: 2423, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1859,7 +1859,7 @@ func yaoDataKbProvidersChunkingStructuredZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/chunking/structured/zh-cn.json", size: 2321, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1879,7 +1879,7 @@ func yaoDataKbProvidersConverterMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/en.json", size: 4235, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1899,7 +1899,7 @@ func yaoDataKbProvidersConverterMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/mcp/zh-cn.json", size: 4060, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1919,7 +1919,7 @@ func yaoDataKbProvidersConverterOcrEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/en.json", size: 6631, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1939,7 +1939,7 @@ func yaoDataKbProvidersConverterOcrZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/ocr/zh-cn.json", size: 6501, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1959,7 +1959,7 @@ func yaoDataKbProvidersConverterOfficeEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/en.json", size: 5476, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1979,7 +1979,7 @@ func yaoDataKbProvidersConverterOfficeZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/office/zh-cn.json", size: 5356, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -1999,7 +1999,7 @@ func yaoDataKbProvidersConverterUtf8EnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/en.json", size: 292, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2019,7 +2019,7 @@ func yaoDataKbProvidersConverterUtf8ZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/utf8/zh-cn.json", size: 281, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2039,7 +2039,7 @@ func yaoDataKbProvidersConverterVideoEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/en.json", size: 6411, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2059,7 +2059,7 @@ func yaoDataKbProvidersConverterVideoZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/video/zh-cn.json", size: 6297, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2079,7 +2079,7 @@ func yaoDataKbProvidersConverterVisionEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/en.json", size: 4085, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2099,7 +2099,7 @@ func yaoDataKbProvidersConverterVisionZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/vision/zh-cn.json", size: 3949, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2119,7 +2119,7 @@ func yaoDataKbProvidersConverterWhisperEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/en.json", size: 4449, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2139,7 +2139,7 @@ func yaoDataKbProvidersConverterWhisperZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/converter/whisper/zh-cn.json", size: 4312, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2159,7 +2159,7 @@ func yaoDataKbProvidersEmbeddingFastembedEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/en.json", size: 6865, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2179,7 +2179,7 @@ func yaoDataKbProvidersEmbeddingFastembedZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/fastembed/zh-cn.json", size: 6685, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2199,7 +2199,7 @@ func yaoDataKbProvidersEmbeddingOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/en.json", size: 5636, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2219,7 +2219,7 @@ func yaoDataKbProvidersEmbeddingOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/embedding/openai/zh-cn.json", size: 5463, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2239,7 +2239,7 @@ func yaoDataKbProvidersExtractionOpenaiEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/en.json", size: 9110, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2259,7 +2259,7 @@ func yaoDataKbProvidersExtractionOpenaiZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/extraction/openai/zh-cn.json", size: 8827, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2279,7 +2279,7 @@ func yaoDataKbProvidersFetcherHttpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/en.json", size: 5885, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2299,7 +2299,7 @@ func yaoDataKbProvidersFetcherHttpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/http/zh-cn.json", size: 5925, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2319,7 +2319,7 @@ func yaoDataKbProvidersFetcherMcpEnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/en.json", size: 6819, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2339,7 +2339,7 @@ func yaoDataKbProvidersFetcherMcpZhCnJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/data/kb/providers/fetcher/mcp/zh-cn.json", size: 6611, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2359,7 +2359,7 @@ func yaoFieldsModelTransJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/fields/model.trans.json", size: 14938, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2379,7 +2379,7 @@ func yaoLangsEnUsJson() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/langs/en-US.json", size: 66, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2399,7 +2399,7 @@ func yaoLangsZhCnGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2419,7 +2419,7 @@ func yaoLangsZhCnLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2439,7 +2439,7 @@ func yaoLangsZhCnLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/langs/zh-cn/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2459,7 +2459,7 @@ func yaoLangsZhHkGlobalYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/global.yml", size: 1762, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2479,7 +2479,7 @@ func yaoLangsZhHkLoginsAdminLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/admin.login.yml", size: 94, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2499,7 +2499,7 @@ func yaoLangsZhHkLoginsUserLoginYml() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/langs/zh-hk/logins/user.login.yml", size: 90, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2519,7 +2519,7 @@ func yaoModelsAgentAssistantModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6758, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/agent/assistant.mod.yao", size: 6758, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2539,7 +2539,7 @@ func yaoModelsAgentChatModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 1741, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/agent/chat.mod.yao", size: 1741, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2559,12 +2559,12 @@ func yaoModelsAgentHistoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/agent/history.mod.yao", size: 2941, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/agent/history.mod.yao", size: 2941, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } -var _yaoModelsAttachmentModYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x57\x5f\x6f\xdb\x36\x10\x7f\xf7\xa7\x38\xf0\xd9\x43\xb3\x01\x19\x90\xbc\x65\xed\x36\x14\x18\xb6\x60\x6b\xb7\x87\xa2\x30\x28\xe9\x24\x73\xa0\x48\x8d\x77\x6a\xe2\x06\xf9\xee\x03\x29\x4b\xa6\x64\xca\x8e\xdc\xad\x4f\x86\x8f\xf7\xe7\xf7\xbb\xe3\x9d\x8e\x4f\x2b\x00\x61\x64\x8d\xe2\x16\x84\x64\x96\xf9\xb6\x46\xc3\x62\xed\xe5\x5a\x66\xa8\xfd\xc1\xdd\xe4\xa0\x40\xca\x9d\x6a\x58\x59\x33\x3e\x06\x96\x99\x46\x28\xad\x03\x62\xeb\x94\xa9\xa0\x54\x1a\xe1\xe0\x99\xe0\x41\xf1\x16\x6a\x64\x59\x48\x96\x20\x4d\x01\x32\xcf\x91\x08\x72\x6b\xd8\x59\xdd\x85\x60\x59\x91\xb8\x85\x0f\x82\x76\xc4\x58\x8b\x8f\x41\x9a\xb5\x4a\xb3\xf2\x41\xd9\xb5\x18\x44\x0e\x65\x61\x8d\xde\xc5\x32\xb2\x8e\xc5\x2d\xdc\xdc\xdc\xdc\xec\x9d\x65\xda\x33\xf4\x6c\xe7\xf9\x02\x88\xdc\xd6\xe1\x6f\x82\x94\x58\x01\x3c\x07\x6f\xb9\xd5\x6d\x6d\x02\xba\x60\xd5\x79\x8d\xfc\xaa\x62\xef\xcf\x87\xde\x35\x41\xf6\xf6\xcd\x41\x96\xc8\x2b\xc4\xe7\x11\x8a\xf7\x46\xfd\xd3\xc6\xf9\x03\x55\xa0\x61\x55\x2a\x74\x22\xe8\x3f\xaf\xd3\x20\x7c\xde\x37\x29\x24\xc4\xbe\x2e\x09\x34\x3f\xf9\x4a\xcd\xe0\x08\x67\x51\xe8\x83\x35\x9a\x8a\xb7\xe2\x16\xbe\xbb\xbe\x1e\x84\xa6\xd5\x7a\x9f\xf2\x52\x6a\xc2\xe1\xa0\x0d\x74\xa2\x52\x05\xa9\x32\x05\x3e\xee\x85\x27\x39\xb5\x8d\xb6\xb2\x88\xc3\x9f\x25\xf5\xfe\xc8\x64\xca\xaa\x77\x0a\xc1\x57\x82\xd8\xd5\xd5\x79\x62\x2f\xa6\xe0\x2f\x39\x1a\xde\x8c\x83\x9d\xa5\xf1\xba\x33\x83\x77\x23\xb3\x29\x95\xbd\xf3\xaf\xca\xe4\x98\x84\xb6\xa6\x7a\x87\x8f\x3c\x4f\x23\xc9\xe0\x5e\x3a\xc2\x02\x18\x1f\x79\x20\x52\x3a\x5b\x83\xaa\x65\x85\x6b\x68\x8a\x72\x0d\x0f\xd6\x15\x61\x66\x58\xde\xa2\xeb\x86\x8b\x8f\x4b\x22\xc5\xec\x2c\x87\xf0\xfb\xf2\x2a\xfc\x3a\x52\x9f\x66\x7f\xec\x6c\xc8\xfa\xf5\x7f\x9a\xf5\xd6\xe9\x25\xb7\xff\xf7\x5f\xe6\xf1\x8e\x0e\x07\xb8\xdf\x5e\xa5\xf1\x26\x3b\x36\x90\x38\x89\x37\xfe\x54\xbc\x1c\xf7\x9b\x94\xd5\x14\x7f\xd2\xf5\xff\xc5\x63\x61\xbf\x9e\xee\xd3\x65\xfd\x79\xe1\xac\x24\x74\x9b\x46\xf2\x76\xc9\x75\x21\x74\x70\x3f\xb2\x89\xbf\x45\x84\xee\x1b\x6a\x30\xf7\x9f\x80\x02\x72\x5b\x37\x1a\x19\xbb\x26\x1c\x47\xba\xac\x0a\x67\x39\x2d\xa4\xf3\x07\x5b\x27\x2b\x9c\x67\x74\x97\x73\x2b\x75\x58\x55\xbc\x9e\x77\x1f\x76\x17\xde\x76\xac\x16\x10\xba\xb0\x9d\x2b\x67\xdb\x86\x8e\x39\xfd\x4d\xa3\x4b\xdd\x33\xfa\x79\xa2\x3e\xbd\x58\x53\x77\x4b\x66\x61\xf5\x59\x35\xc7\x40\x32\x6b\x35\xca\x24\x96\x91\x7e\x84\xe4\xaf\x2d\x1e\x86\xb3\x22\xf0\x8e\x1b\x8c\x36\x91\x02\x4b\xd9\x6a\xbe\x3c\x6b\xd9\x8e\x31\x91\xb4\x4c\x55\x6f\x0d\x63\x35\xda\x4f\x7a\xb8\x3f\x8c\x6d\xa6\x99\x23\xf5\x19\x41\x19\x98\xb8\xfe\xf2\x0a\x13\x4b\x6e\x13\x60\xd1\xb4\x75\xf2\xce\x8e\xd5\xa7\x38\x1b\x67\xfd\xb2\xec\x37\xeb\xa9\x67\xdb\x2f\xe4\x1f\xf6\x12\xe8\x37\xa6\xb8\x3d\x06\x61\x54\x91\x9e\xcf\x44\x2f\xc8\xc6\x6a\x9d\xe9\xa6\x94\x4a\x27\xec\x7b\xf9\x5e\xfc\x31\x51\xf1\x04\xa2\x05\xfd\xef\x6c\xe5\x90\x12\xd9\x9c\x9d\x01\xf7\x47\x26\xf1\xca\x71\x48\x66\xef\x1a\x94\x29\xad\xab\xe5\xcc\x57\xe5\xc4\x88\x3e\x89\x1c\x9d\xb3\x4b\xd6\xd6\x1f\xc7\xfa\x11\xe6\x70\x72\x06\xe5\xf7\xe7\x51\xce\x25\x18\x09\x13\x2b\xdd\xfc\x14\xb8\x0f\x16\x70\x37\x7d\x4a\xa5\x47\x02\x6f\x15\xf9\x91\x20\xa1\x8b\x04\x47\x4f\xb0\x13\xe3\x61\xda\x8b\x67\xa8\xb4\x99\x56\xf9\x22\x2a\xc1\x62\x11\x95\xf8\x5d\x46\x40\x5b\xe9\xd0\x3f\x67\x9d\x25\x02\xa9\x35\x30\xca\xda\x5f\xa9\xf0\x45\x69\xb4\x64\x5f\xb5\x2f\x64\xfa\xea\x15\xbc\x6e\x89\x6d\x0d\x0d\xba\x5a\x11\x29\x6b\x68\x66\xf6\x78\x40\x2f\x1f\x3d\x63\xed\xf4\x5b\xd8\xbb\x0c\xc3\x27\xb7\xf1\x0e\x93\x9a\x3d\x8d\x53\x9f\x24\xa3\x58\x7b\xc8\xbf\x19\xbd\x83\x4f\x8a\x54\xe6\xf7\x1f\x1b\x12\x62\x1f\x0c\xba\x83\xbe\x4f\x96\xf0\xba\x7f\x1e\xd4\xfa\x24\x42\x8d\x75\x86\x8e\x4e\x4d\x96\x21\xde\x05\xc3\x7b\xb5\xf7\x29\x1c\xea\xd0\x55\xfe\x79\xff\xd4\xbd\xf7\xbb\x31\x18\xde\xfb\x9d\xce\x40\xf6\x09\x04\xab\x1a\x89\x65\xdd\x50\xbf\xd5\x80\x20\x5b\xf2\xa6\x40\xbf\x1c\xd1\x10\x1b\xc4\xa1\x5c\x7b\x55\x78\x5e\x3d\xaf\xfe\x0d\x00\x00\xff\xff\x5c\x10\x1f\x9b\x82\x11\x00\x00") +var _yaoModelsAttachmentModYao = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x58\xdf\x6f\xe4\x34\x10\x7e\xef\x5f\x31\xf2\x13\x48\x45\x57\x90\x8a\xd4\xbe\x95\x3b\x40\x27\x21\xa8\xe0\x0e\x1e\x4e\xa7\x95\x93\x4c\x12\x23\xc7\x0e\x9e\x49\xdb\xbd\xaa\xff\x3b\xb2\xf3\x63\x93\xac\xb3\xbb\xd9\x83\x7b\xaa\x3a\x9e\x19\x7f\xdf\x8c\xf3\x79\xbc\xcf\x17\x00\xc2\xc8\x0a\xc5\x2d\x08\xc9\x2c\xd3\xb2\x42\xc3\xe2\xd2\xdb\xb5\x4c\x50\xfb\x85\xbb\xd9\x42\x86\x94\x3a\x55\xb3\xb2\x66\xba\x0c\x2c\x13\x8d\x90\x5b\x07\xc4\xd6\x29\x53\x40\xae\x34\xc2\x2e\x33\xc1\xa3\xe2\x12\x2a\x64\x99\x49\x96\x20\x4d\x06\x32\x4d\x91\x08\x52\x6b\xd8\x59\xdd\x6e\xc1\xb2\x20\x71\x0b\x1f\x04\x6d\x89\xb1\x12\x1f\x83\x35\x69\x94\x66\xe5\x37\x65\xd7\x60\x30\x39\x94\x99\x35\x7a\x3b\xb6\x91\x75\x2c\x6e\xe1\xe6\xe6\xe6\xa6\x4b\x96\x68\xcf\xd0\xb3\x5d\xe6\x0b\x20\x52\x5b\x85\x7f\x23\xa4\xc4\x05\xc0\x4b\xc8\x96\x5a\xdd\x54\x26\xa0\x0b\x51\x6d\xd6\x51\x5e\x95\x75\xf9\xfc\xd6\xdb\x3a\xd8\xde\xbe\xd9\xd9\x22\x75\x85\xf1\xfa\x08\xc5\x7b\xa3\xfe\x69\xc6\xf5\x03\x95\xa1\x61\x95\x2b\x74\x22\xf8\xbf\x5c\xc6\x41\xf8\xba\x6f\x62\x48\x88\x7d\x5f\x22\x68\x7e\xf2\x9d\x5a\xc0\x11\xd6\x46\x5b\xef\xa2\xd1\x14\x5c\x8a\x5b\xf8\xee\xfa\x7a\x30\x9a\x46\xeb\xae\xe4\xb9\xd4\x84\xc3\x42\x13\xe8\x8c\x5a\x15\xac\xca\x64\xf8\xd4\x19\x0f\x72\x6a\x6a\x6d\x65\x36\xde\xfe\x28\xa9\xf7\x7b\x21\x73\x56\x7d\x52\x08\xb9\x22\xc4\xae\xae\x8e\x13\x3b\x99\x82\x3f\xe4\x68\x78\x33\xdd\xec\x28\x8d\xd7\x6d\x18\xbc\x9b\x84\xcd\xa9\x74\xc9\xbf\x28\x93\x7d\x12\xda\x9a\xe2\x1d\x3e\xf1\x32\x8d\x38\x83\x46\x6b\xa8\xa5\x23\xcc\x80\xf1\x89\x07\x36\xb9\xb3\x15\xa8\x4a\x16\x78\x09\x75\x96\x5f\xc2\xa3\x75\x59\x10\x0e\xcb\x25\xba\x56\x61\xfc\xe6\x24\x62\xf4\x4e\x6e\x49\xed\xf0\x41\xe1\xe3\x3e\x21\x3e\x48\x06\xee\xe7\x71\x23\x52\xdd\x1a\xd8\x3c\x4a\xed\xab\x5c\x39\x62\xdf\x98\x2b\x48\x4b\xe9\x64\xca\xe8\xe8\xeb\xf3\x78\x84\xbf\xa7\x1f\xa9\x5f\x27\xee\xf3\xa3\x34\x4d\x36\x1c\xa1\xeb\xff\xf4\x08\x35\x4e\xaf\xf9\x94\x7f\xff\x65\x19\xef\x64\x71\x80\xfb\xed\x55\x1c\x6f\x54\x7e\x02\x89\x83\x78\xc7\xf7\xde\xe9\xb8\xdf\xc4\xa2\xe6\xf8\xa3\xa9\xff\x2f\x1e\x2b\xc5\xe7\xb0\xe8\xac\x13\x9b\x33\x85\x9f\xd0\x6d\x6a\xc9\xe5\x9a\xe3\x42\xe8\xe0\x7e\x12\x33\xbe\x58\x09\xdd\x37\x54\x63\xea\xef\xb3\x0c\x52\x5b\xd5\x1a\x19\x5b\x31\x99\xee\x74\x5e\x17\x8e\x72\x5a\x49\xe7\x0f\xb6\x4e\x16\xb8\xcc\xe8\x2e\xe5\x46\xea\x30\x77\x79\x3f\x9f\x3e\x0c\x62\x5c\xb6\xac\x56\x10\x3a\xf3\x73\x2e\x9c\x6d\x6a\xda\xe7\xf4\x37\x4d\x0e\x75\xcf\xe8\xe7\x99\xfb\xfc\x60\xcd\xd3\xad\xd1\xc2\xe2\x93\xaa\xf7\x81\x24\xd6\x6a\x94\x51\x2c\x13\xff\x11\x92\xbf\x4a\xdc\x5d\x32\x8a\xc0\x27\xae\x71\x34\x56\x65\x98\xcb\x46\xf3\xf9\x55\x4b\xb6\x8c\x91\xa2\x25\xaa\x78\x6b\x18\x8b\xc9\xb0\xd5\xc3\xfd\x61\x1a\x33\xaf\x1c\xa9\x4f\x08\xca\xc0\x2c\xf5\xe7\x77\x98\x58\x72\x13\x01\x8b\xa6\xa9\xa2\x67\x76\xea\x3e\xc7\x59\x3b\xeb\x27\x7f\xff\x4c\x98\x67\xb6\xfd\xeb\xe2\x43\x67\x81\x7e\xfc\x1b\x7f\x1e\x83\x71\xd4\x91\x9e\xcf\xcc\x2f\xd8\xa6\x6e\x6d\xe8\x26\x97\x4a\x47\xe2\x7b\x7b\x67\xfe\x18\xe9\x78\x04\xd1\x8a\xef\xdf\xd9\xc2\x21\x45\xaa\xb9\xa8\x01\xf7\x7b\x21\x93\x51\x63\x28\x66\x9f\x1a\x94\xc9\xad\xab\xe4\xc2\xad\x72\x40\xa2\x0f\x22\x47\xe7\xec\x9a\x19\xfc\xc7\xa9\xff\x08\x73\x58\x39\x82\xf2\xfb\xe3\x28\x97\x0a\x8c\x84\x91\xf9\x74\x59\x05\xee\x43\x04\xdc\xcd\xdf\x85\x71\x49\xe0\x52\x91\x97\x04\x09\xed\x4e\xb0\xf7\x9e\x3c\x20\x0f\xf3\x6f\xf1\x08\x95\x26\xd1\x2a\x5d\x45\x25\x44\xac\xa2\x32\x7e\x64\x12\x50\x29\x1d\xfa\xb7\xb9\xb3\x44\x20\xb5\x06\x46\x59\xf9\x23\x15\x6e\x94\x5a\x4b\xf6\x5d\xfb\x4c\xa6\xaf\x5e\xc1\xeb\x86\xd8\x56\x50\xa3\xab\x14\x91\xb2\x86\x16\xb4\xc7\x03\x3a\x5d\x7a\xa6\xde\xf1\x87\xbd\x4f\x19\xc4\x27\xb5\xe3\x19\x26\xa6\x3d\xb5\x53\x0f\x92\x51\x5c\x7a\xc8\xbf\x19\xbd\x85\x07\x45\x2a\xf1\xf3\x8f\x0d\x05\xb1\x8f\x06\xdd\xce\xdf\x17\x4b\x78\xdf\x3f\x77\x6e\x7d\x11\xa1\xc2\x2a\x41\x47\x87\x94\x65\xd8\xef\x0c\xf1\xbe\xe8\x72\x0a\x87\x3a\x7c\x55\x24\x6e\xe1\xb9\xfd\xf1\xa2\x95\xc1\xf0\xe3\x45\xeb\x33\x90\x7d\x06\xc1\xaa\x42\x62\x59\xd5\xd4\x4f\x35\x20\xc8\xe6\xbc\xc9\xd0\x0f\x47\x34\xec\x0d\x62\xd7\xae\xce\x15\x5e\x2e\x5e\x2e\xfe\x0d\x00\x00\xff\xff\xd7\xa2\x63\x02\x4f\x12\x00\x00") func yaoModelsAttachmentModYaoBytes() ([]byte, error) { return bindataRead( @@ -2579,7 +2579,7 @@ func yaoModelsAttachmentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4482, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/attachment.mod.yao", size: 4687, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2599,7 +2599,7 @@ func yaoModelsAuditModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/audit.mod.yao", size: 5588, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2619,7 +2619,7 @@ func yaoModelsConfigModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/config.mod.yao", size: 1649, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2639,7 +2639,7 @@ func yaoModelsDslModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/dsl.mod.yao", size: 3826, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2659,7 +2659,7 @@ func yaoModelsInvitationModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/invitation.mod.yao", size: 6693, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2679,7 +2679,7 @@ func yaoModelsJobCategoryModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/job/category.mod.yao", size: 2041, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2699,7 +2699,7 @@ func yaoModelsJobExecutionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/job/execution.mod.yao", size: 7201, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2719,7 +2719,7 @@ func yaoModelsJobJobModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6330, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/job/job.mod.yao", size: 6330, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2739,7 +2739,7 @@ func yaoModelsJobLogModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/job/log.mod.yao", size: 4711, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2759,7 +2759,7 @@ func yaoModelsKbCollectionModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/kb/collection.mod.yao", size: 5390, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2779,7 +2779,7 @@ func yaoModelsKbDocumentModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/kb/document.mod.yao", size: 9906, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2799,7 +2799,7 @@ func yaoModelsMemberModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/member.mod.yao", size: 14798, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2819,7 +2819,7 @@ func yaoModelsRoleModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/role.mod.yao", size: 6434, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2839,7 +2839,7 @@ func yaoModelsTeamModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/team.mod.yao", size: 15823, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2859,7 +2859,7 @@ func yaoModelsUserOauth_accountModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/user/oauth_account.mod.yao", size: 6928, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2879,7 +2879,7 @@ func yaoModelsUserTypeModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/user/type.mod.yao", size: 7502, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2899,7 +2899,7 @@ func yaoModelsUserModYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/models/user.mod.yao", size: 12335, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2919,7 +2919,7 @@ func yaoReleaseAppYaz() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/release/app.yaz", size: 181682, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2939,7 +2939,7 @@ func yaoStoresAgentCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/agent/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2959,7 +2959,7 @@ func yaoStoresAgentMemoryBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/agent/memory.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/agent/memory.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2979,7 +2979,7 @@ func yaoStoresCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/cache.lru.yao", size: 285, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -2999,7 +2999,7 @@ func yaoStoresKbCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/kb/cache.lru.yao", size: 304, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3019,7 +3019,7 @@ func yaoStoresKbStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/kb/store.badger.yao", size: 349, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/kb/store.badger.yao", size: 349, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3039,7 +3039,7 @@ func yaoStoresOauthCacheLruYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/cache.lru.yao", size: 301, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3059,7 +3059,7 @@ func yaoStoresOauthClientBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/client.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/client.badger.yao", size: 352, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3079,7 +3079,7 @@ func yaoStoresOauthStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/oauth/store.badger.yao", size: 376, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/oauth/store.badger.yao", size: 376, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3099,7 +3099,7 @@ func yaoStoresStoreBadgerYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/stores/store.badger.yao", size: 341, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/stores/store.badger.yao", size: 341, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -3119,7 +3119,7 @@ func yaoUploadersAttachmentLocalYao() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1764756928, 0)} + info := bindataFileInfo{name: "yao/uploaders/attachment.local.yao", size: 1163, mode: os.FileMode(420), modTime: time.Unix(1764757588, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/yao/models/attachment.mod.yao b/yao/models/attachment.mod.yao index 8b02b913..b2a78ecb 100644 --- a/yao/models/attachment.mod.yao +++ b/yao/models/attachment.mod.yao @@ -49,7 +49,14 @@ "name": "content", "type": "longText", "label": "Content", - "comment": "Parsed text content from image, pdf, word and other file types", + "comment": "Full parsed text content from image, pdf, word and other file types", + "nullable": true + }, + { + "name": "content_preview", + "type": "text", + "label": "Content Preview", + "comment": "Preview of parsed text content (first 2000 characters)", "nullable": true }, { From beb63b77debaa35dad0cf9a5e0c1ca056364ed04 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 3 Dec 2025 21:35:54 +0800 Subject: [PATCH 4/6] Enhance Assistant functionality with improved content handling and debugging - Added support for converting extended types (file, data) to standard LLM types (text, image_url, input_audio) in the Stream method. - Introduced a new agentCallerWrapper to facilitate agent calls from the content package. - Implemented ThreadID management for nested agent calls to improve concurrent stream identification. - Enhanced message handling to include metadata for message_start and message_end events, allowing for better tracking of message states. - Removed deprecated vision capability checks from the Assistant initialization process, streamlining the codebase. - Added new content types (file, data) to the context types for improved message content handling. --- agent/assistant/agent.go | 16 + agent/assistant/assistant.go | 23 ++ agent/assistant/build_content.go | 31 ++ agent/assistant/handlers/stream.go | 12 + agent/assistant/load.go | 9 - agent/assistant/types.go | 25 -- agent/content/README.md | 326 +++++++++++++++++++ agent/content/audio.go | 61 ++++ agent/content/content.go | 470 +++++++++++++++++++++++++++ agent/content/content_vision_test.go | 458 ++++++++++++++++++++++++++ agent/content/excel.go | 48 +++ agent/content/fetch.go | 90 +++++ agent/content/image.go | 173 ++++++++++ agent/content/image_test.go | 265 +++++++++++++++ agent/content/interfaces.go | 25 ++ agent/content/pdf.go | 50 +++ agent/content/registry.go | 47 +++ agent/content/text.go | 123 +++++++ agent/content/text_test.go | 152 +++++++++ agent/content/tools.go | 190 +++++++++++ agent/content/types.go | 261 +++++++++++++++ agent/content/word.go | 39 +++ agent/context/output.go | 21 ++ agent/context/types.go | 39 +++ agent/output/message/types.go | 2 + 25 files changed, 2922 insertions(+), 34 deletions(-) create mode 100644 agent/assistant/build_content.go create mode 100644 agent/content/README.md create mode 100644 agent/content/audio.go create mode 100644 agent/content/content.go create mode 100644 agent/content/content_vision_test.go create mode 100644 agent/content/excel.go create mode 100644 agent/content/fetch.go create mode 100644 agent/content/image.go create mode 100644 agent/content/image_test.go create mode 100644 agent/content/interfaces.go create mode 100644 agent/content/pdf.go create mode 100644 agent/content/registry.go create mode 100644 agent/content/text.go create mode 100644 agent/content/text_test.go create mode 100644 agent/content/tools.go create mode 100644 agent/content/types.go create mode 100644 agent/content/word.go diff --git a/agent/assistant/agent.go b/agent/assistant/agent.go index 9cebdf5f..4f991dec 100644 --- a/agent/assistant/agent.go +++ b/agent/assistant/agent.go @@ -8,6 +8,7 @@ import ( "github.com/yaoapp/gou/connector" "github.com/yaoapp/gou/connector/openai" "github.com/yaoapp/kun/log" + "github.com/yaoapp/kun/utils" "github.com/yaoapp/yao/agent/assistant/handlers" "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/i18n" @@ -42,6 +43,13 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa _, _, done := context.EnterStack(ctx, ast.ID, ctx.Referer) defer done() + fmt.Println("--- Stack debug ---") + if ctx.Stack != nil { + fmt.Println(ctx.Stack.IsRoot()) + utils.Dump(ctx.Stack) + } + fmt.Println("------ end stack debug ------") + // Determine stream handler streamHandler := ast.getStreamHandler(ctx, handler...) @@ -106,6 +114,14 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa return nil, err } + // Build content - convert extended types (file, data) to standard LLM types (text, image_url, input_audio) + completionMessages, err = ast.BuildContent(ctx, completionMessages, completionOptions) + if err != nil { + ast.traceAgentFail(agentNode, err) + ast.sendStreamEndOnError(ctx, streamHandler, streamStartTime, err) + return nil, err + } + // Execute the LLM streaming call completionResponse, err = ast.executeLLMStream(ctx, completionMessages, completionOptions, agentNode, streamHandler) if err != nil { diff --git a/agent/assistant/assistant.go b/agent/assistant/assistant.go index de103b9c..df5f3baf 100644 --- a/agent/assistant/assistant.go +++ b/agent/assistant/assistant.go @@ -5,11 +5,34 @@ import ( "path" "github.com/yaoapp/gou/fs" + "github.com/yaoapp/yao/agent/content" + agentContext "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/i18n" store "github.com/yaoapp/yao/agent/store/types" sui "github.com/yaoapp/yao/sui/core" ) +func init() { + // Initialize AgentGetterFunc to allow content package to call agents + content.AgentGetterFunc = func(agentID string) (content.AgentCaller, error) { + ast, err := Get(agentID) + if err != nil { + return nil, err + } + // Return a wrapper that implements AgentCaller interface + return &agentCallerWrapper{ast: ast}, nil + } +} + +// agentCallerWrapper wraps Assistant to implement AgentCaller interface +type agentCallerWrapper struct { + ast *Assistant +} + +func (w *agentCallerWrapper) Stream(ctx *agentContext.Context, messages []agentContext.Message) (interface{}, error) { + return w.ast.Stream(ctx, messages) +} + // Get get the assistant by id func Get(id string) (*Assistant, error) { return LoadStore(id) diff --git a/agent/assistant/build_content.go b/agent/assistant/build_content.go new file mode 100644 index 00000000..f898ee19 --- /dev/null +++ b/agent/assistant/build_content.go @@ -0,0 +1,31 @@ +package assistant + +import ( + "fmt" + + "github.com/yaoapp/yao/agent/content" + "github.com/yaoapp/yao/agent/context" +) + +// BuildContent processes messages through Vision function to convert extended content types +// (file, data) to standard LLM-compatible types (text, image_url, input_audio) +// +// This should be called after BuildRequest and before executing LLM call +func (ast *Assistant) BuildContent(ctx *context.Context, messages []context.Message, options *context.CompletionOptions) ([]context.Message, error) { + // Get connector and capabilities + _, capabilities, err := ast.GetConnector(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get connector: %w", err) + } + + // Get Uses configuration from options (already merged in BuildRequest) + uses := options.Uses + + // Process content through Vision function + processedMessages, err := content.Vision(ctx, capabilities, messages, uses) + if err != nil { + return nil, fmt.Errorf("failed to process content: %w", err) + } + + return processedMessages, nil +} diff --git a/agent/assistant/handlers/stream.go b/agent/assistant/handlers/stream.go index dcb910ea..54111605 100644 --- a/agent/assistant/handlers/stream.go +++ b/agent/assistant/handlers/stream.go @@ -107,6 +107,11 @@ func (s *streamState) handleMessageStart(data []byte) int { startData.MessageID = messageID } + // Auto-set ThreadID from Stack for nested agent calls + if startData.ThreadID == "" && s.ctx.Stack != nil && !s.ctx.Stack.IsRoot() { + startData.ThreadID = s.ctx.Stack.ID + } + // Initialize message state with the correct message ID s.inGroup = true s.currentGroupID = messageID @@ -312,11 +317,18 @@ func (s *streamState) handleMessageEnd(data []byte) int { msgType = message.TypeText // Fallback to text if type not set } + // Get ThreadID from Stack for nested agent calls + var threadID string + if s.ctx.Stack != nil && !s.ctx.Stack.IsRoot() { + threadID = s.ctx.Stack.ID + } + // Build EventMessageEndData with complete content endData := message.EventMessageEndData{ MessageID: s.currentGroupID, // Use the message ID Type: msgType, Timestamp: time.Now().UnixMilli(), + ThreadID: threadID, // Include ThreadID for concurrent stream identification DurationMs: durationMs, ChunkCount: s.chunkCount, Status: "completed", diff --git a/agent/assistant/load.go b/agent/assistant/load.go index 3a059eb0..477c429b 100644 --- a/agent/assistant/load.go +++ b/agent/assistant/load.go @@ -751,15 +751,6 @@ func (ast *Assistant) initialize() error { } ast.openai = api - // Check if the assistant supports vision - model := api.Model() - if v, ok := ast.Options["model"].(string); ok { - model = strings.TrimLeft(v, "moapi:") - } - if _, ok := VisionCapableModels[model]; ok { - ast.vision = true - } - // Check if the assistant has an init hook if ast.Script != nil { scriptCtx, err := ast.Script.NewContext("", nil) diff --git a/agent/assistant/types.go b/agent/assistant/types.go index 6345930d..66b079b6 100644 --- a/agent/assistant/types.go +++ b/agent/assistant/types.go @@ -40,31 +40,6 @@ type Assistant struct { // toolCalls bool // Whether this assistant supports tool_calls } -// VisionCapableModels list of LLM models that support vision capabilities -var VisionCapableModels = map[string]bool{ - // OpenAI Models - "gpt-4-vision-preview": true, - "gpt-4v": true, // Alias for gpt-4-vision-preview - - // Anthropic Models - "claude-3-opus": true, // Most capable Claude model - "claude-3-sonnet": true, // Balanced Claude model - "claude-3-haiku": true, // Fast and efficient Claude model - - // Google Models - "gemini-pro-vision": true, - - // Open Source Models - "llava-13b": true, - "cogvlm": true, - "qwen-vl": true, - "yi-vl": true, - - // Custom Models - "gpt-4o": true, // Custom OpenAI compatible model - "gpt-4o-mini": true, // Custom OpenAI compatible model - mini version -} - // MCPTool represents a simplified MCP tool for building LLM requests // This is an internal representation used when collecting tools from MCP servers // and preparing them for the LLM's tool calling interface diff --git a/agent/content/README.md b/agent/content/README.md new file mode 100644 index 00000000..9f33b5bd --- /dev/null +++ b/agent/content/README.md @@ -0,0 +1,326 @@ +# Content Processing Package + +This package handles content transformation for multimodal messages in agent conversations. It is called **BEFORE** sending messages to the LLM and converts extended content types into standard LLM-compatible formats. + +## ⚠️ Critical Design Principle + +**Input**: Messages with extended content types (`file`, `data`, etc.) +**Output**: Messages with ONLY standard LLM-compatible types (`text`, `image_url`, `input_audio`) + +The LLM should NEVER receive `type="file"` or `type="data"` content parts. These MUST be converted to `text` (or `image_url` for images if model supports vision). + +## Architecture + +``` +Vision (main entry) + ↓ +Initialize processedFiles cache (map[fileID]text) + ↓ +processMessage (for each message) + ↓ +processContentPart (for each content part) + ↓ +Is uploader wrapper? + ├── Yes → Check cache + │ ├── In cache? → Use cached text ✅ + │ └── Not in cache → Try GetText(fileID) preview + │ ├── Has preview? → Use preview + cache ✅ + │ └── No preview → Proceed to full processing ↓ + └── No (HTTP/other) → Proceed to full processing ↓ + ↓ +├── Fetch content (if needed) +│ ├── HTTP URL +│ └── Uploader Wrapper (__uploader://fileid) + ↓ +├── Determine Processing Strategy +│ ├── Model supports? → Format for model +│ └── Model doesn't support? → Use agent/MCP + ↓ +ProcessorRegistry + ↓ +├── ImageProcessor +├── AudioProcessor +├── PDFProcessor +├── WordProcessor +├── ExcelProcessor +└── TextProcessor + ↓ +Cache result (if uploader wrapper) +``` + +## Content Type Transformation + +### Input → Output Mapping + +| Input Type | Model Supports? | Output Type | Processing | +|------------|-----------------|-------------|------------| +| `text` | - | `text` | Pass through | +| `image_url` | ✅ Yes | `image_url` | Convert format if needed (base64/URL) | +| `image_url` | ❌ No | `text` | Use vision agent/MCP to describe | +| `input_audio` | ✅ Yes | `input_audio` | Keep as audio | +| `input_audio` | ❌ No | `text` | Transcribe using audio agent/MCP | +| `file` (image) | ✅ Yes | `image_url` | Same as image_url processing | +| `file` (image) | ❌ No | `text` | Use vision tool to describe | +| `file` (document) | - | `text` | Extract text from PDF/Word/Excel/etc | +| `data` | - | `text` | Fetch and format data sources | + +### 1. Images and Audio + +**If model supports (vision/audio capability):** + +- Keep as multimodal content: + - `image_url`: Convert to appropriate format (OpenAI URL vs Claude base64) + - `input_audio`: Convert to base64 format + +**If model doesn't support:** + +- Convert to text: + - Use agent/MCP specified in `uses.Vision` or `uses.Audio` + - Extract text description or transcription + - Return as `type="text"` content + +**HTTP URLs:** + +- Fetch content first +- Then process the same way as above + +### 2. Files (type="file") + +**Critical**: All `type="file"` content MUST be converted to `text` or `image_url` (if image and model supports). + +**Processing Steps:** + +1. **Fetch file content**: + - Uploader wrapper: `__uploader://fileid` → Parse and fetch from attachment manager + - HTTP URL: Download from URL + +2. **Detect file type** from content-type and magic bytes + +3. **Process based on file type**: + +| File Type | Output Type | Processing Method | +| ------------ | ----------- | -------------------------------------------------------------------------------------------------------- | +| **Image** | `image_url` or `text` | If model supports vision → `image_url`
If not → use vision tool → `text` | +| **PDF** | `text` | If `uses.Vision` supports PDF → use vision tool
Otherwise → extract text directly | +| **Word** | `text` | Extract text using Word document parser | +| **Excel** | `text` | Extract and format as readable table/CSV | +| **PPT** | `text` | Extract text and slide content | +| **CSV** | `text` | Format as readable table | +| **Text** | `text` | Read directly (with encoding detection) | +| **JSON/XML** | `text` | Pretty print for readability | + +### 3. Data Sources (type="data") + +**Critical**: All `type="data"` content MUST be converted to `text`. + +**Processing Steps:** + +1. **Parse DataContent.Sources** array +2. **Fetch data** from each source: + - `model`: Query data model + - `kb_collection`: Search knowledge base collection + - `kb_document`: Get document content + - `table`: Query database table + - `api`: Call API endpoint + - `mcp_resource`: Fetch MCP resource +3. **Format as readable text**: + - Tables: Format as markdown tables or CSV + - Documents: Include title and content + - JSON: Pretty print +4. **Return as** `type="text"` content + +## Components + +### Core Files + +- **content.go** - Main entry point (`Vision` function) +- **types.go** - Type definitions and constants +- **interfaces.go** - Interface definitions + +### Fetching + +- **fetch.go** - Fetch content from HTTP or uploader + +### Processors + +- **processor.go** - Processor registry and routing +- **image.go** - Image processing +- **audio.go** - Audio processing +- **pdf.go** - PDF document processing +- **word.go** - Word document processing +- **excel.go** - Excel spreadsheet processing +- **text.go** - Plain text and CSV processing + +## Frontend Message Format + +The frontend (InputArea) sends messages in the following format: + +### Image Attachments +```json +{ + "type": "image_url", + "image_url": { + "url": "__yao.attachment://file_id", + "detail": "auto" + } +} +``` + +### File Attachments +```json +{ + "type": "file", + "file": { + "url": "__yao.attachment://file_id", + "filename": "document.pdf" + } +} +``` + +The `url` field contains an uploader wrapper in the format `__uploader://fileid`. + +## Data Structures + +### ContentInfo + +Holds information about content to be processed: + +```go +type ContentInfo struct { + Source ContentSource // http, uploader, base64, local + FileType FileType // image, audio, pdf, word, excel, etc. + ContentType string // MIME type + URL string // Original URL or file ID + Data []byte // File data + + // For uploader wrapper + UploaderName string + FileID string +} +``` + +### ProcessedContent + +Result of content processing: + +```go +type ProcessedContent struct { + Text string // Extracted text + ContentPart *context.ContentPart // For model input + Metadata map[string]interface{} + Error error +} +``` + +## Usage Example + +```go +import ( + "github.com/yaoapp/yao/agent/content" + "github.com/yaoapp/yao/agent/context" +) + +// Process messages before sending to LLM +processedMessages, err := content.Vision( + ctx, + capabilities, // Model capabilities + messages, // Original messages + uses, // Tool specifications (vision, audio, etc.) +) +``` + +## Performance Optimization + +### File Processing Cache + +**Problem**: Same file (uploader wrapper) might appear in multiple messages or be referenced multiple times. + +**Solution**: Three-level caching strategy: + +1. **In-memory cache** (`processedFiles` map): + - Caches processed text for the duration of the Vision() call + - Key: file ID from uploader wrapper + - Value: extracted text content + +2. **Attachment preview** (attachment.GetText with preview): + - Tries to get preview (first 2000 chars) from attachment manager + - If file was previously processed and saved, preview is available immediately + - Much faster than full file processing + +3. **Full processing** (only if needed): + - Falls back to complete file processing if no cache/preview available + - Result is cached in memory and optionally saved to attachment manager + +### Cache Flow + +```go +// For uploader://file_id +1. Check processedFiles[file_id] + └── Found? → Return cached text ⚡ (fastest) + +2. Not in cache → Call attachment.GetText(file_id, false) // preview only + └── Has preview? → Cache and return ⚡ (fast) + +3. No preview → Process file fully 🔄 (slower) + └── Cache result in processedFiles + └── Optional: Save to attachment using SaveText for future use +``` + +### Benefits + +- **Avoid duplicate processing**: Same file processed only once per Vision() call +- **Fast preview access**: Leverage pre-processed content from attachment manager +- **Reduced latency**: Especially important for large documents (PDFs, Word, Excel) +- **Resource efficient**: Less CPU/memory usage for repeated file references + +## Implementation Status + +### ✅ Completed + +- [x] Package structure +- [x] Type definitions +- [x] Interface definitions +- [x] Skeleton functions with TODO comments +- [x] File processing cache infrastructure +- [x] Cache helper functions (tryGetCachedText, cacheProcessedText) + +### 🚧 To Implement + +- [ ] tryGetCachedText implementation (attachment.GetText integration) +- [ ] cacheProcessedText implementation (attachment.SaveText integration) +- [ ] HTTP fetching logic +- [ ] Uploader wrapper parsing and fetching +- [ ] Image processing (base64, vision API) +- [ ] Audio processing (transcription) +- [ ] PDF text extraction +- [ ] Word document parsing +- [ ] Excel spreadsheet parsing +- [ ] Text/CSV formatting +- [ ] Content part processing logic +- [ ] Model capability detection +- [ ] Agent/MCP tool invocation + +## Configuration + +Content processing behavior is controlled by: + +1. **Model Capabilities** (`openai.Capabilities`) + + - Determines if model can handle images/audio directly + - Specifies vision format (OpenAI vs Claude) + +2. **Uses** (`context.Uses`) + ```go + type Uses struct { + Vision string // "agent" or "mcp:server_id" + Audio string // "agent" or "mcp:server_id" + Search string + Fetch string + } + ``` + +## Error Handling + +- Errors during processing are logged but don't stop the entire pipeline +- Original content is kept if processing fails +- Graceful degradation: if advanced processing fails, fall back to simpler methods diff --git a/agent/content/audio.go b/agent/content/audio.go new file mode 100644 index 00000000..dbdecb56 --- /dev/null +++ b/agent/content/audio.go @@ -0,0 +1,61 @@ +package content + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// AudioHandler handles audio content +type AudioHandler struct{} + +// CanHandle checks if this handler can handle the content type +func (h *AudioHandler) CanHandle(contentType string, fileType FileType) bool { + return fileType == FileTypeAudio || strings.HasPrefix(contentType, "audio/") +} + +// Handle processes audio content +// Logic similar to image: +// 1. If model supports audio input -> convert to base64 format +// 2. If model doesn't support audio -> use agent/MCP specified in uses.Audio +func (h *AudioHandler) Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) { + // TODO: Implement audio handling + // 1. Check model audio capabilities + // 2. If supported: + // - Encode audio as base64 with proper format + // 3. If not supported: + // - Call audio agent/MCP to transcribe audio to text + // 4. Return Result with text or ContentPart + return nil, fmt.Errorf("not implemented") +} + +// handleWithAudioModel processes audio using model's audio capability +func (h *AudioHandler) handleWithAudioModel(ctx *agentContext.Context, info *Info) (*Result, error) { + // TODO: Implement audio model processing + // Format audio according to model's audio input format + return nil, fmt.Errorf("not implemented") +} + +// handleWithAudioAgent processes audio using audio agent or MCP +func (h *AudioHandler) handleWithAudioAgent(ctx *agentContext.Context, info *Info, audioTool string) (string, error) { + // TODO: Implement audio agent/MCP processing + // 1. Parse audioTool (format: "agent" or "mcp:server_id") + // 2. Call appropriate tool to transcribe audio + // 3. Return transcribed text + return "", fmt.Errorf("not implemented") +} + +// encodeAudioBase64 encodes audio data to base64 with proper format +func encodeAudioBase64(data []byte, contentType string) string { + // TODO: Implement audio base64 encoding + return "" +} + +// detectAudioFormat detects audio format from content type or data +func detectAudioFormat(contentType string, data []byte) string { + // TODO: Implement audio format detection + // Return format like "wav", "mp3", "flac", etc. + return "" +} diff --git a/agent/content/content.go b/agent/content/content.go new file mode 100644 index 00000000..8284cf5c --- /dev/null +++ b/agent/content/content.go @@ -0,0 +1,470 @@ +package content + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" + "github.com/yaoapp/yao/attachment" +) + +// Vision transforms extended content types to LLM-compatible formats +// This is the main entry point for content preprocessing before sending to LLM +// +// IMPORTANT: This function is called BEFORE sending messages to LLM (in agent.executeLLMStream) +// It must convert all extended content types to standard LLM-compatible types. +// +// Input Content Types (Extended): +// - type="text" -> Pass through (already standard) +// - type="image_url" -> Process based on model capability (may need base64 conversion or vision tool) +// - type="input_audio" -> Process based on model capability (may need transcription) +// - type="file" -> Convert to text or image_url (MUST be converted) +// - type="data" -> Convert to text (MUST be converted) +// +// Output Content Types (LLM-compatible only): +// - type="text" -> Text content +// - type="image_url" -> Image (only if model supports vision) +// - type="input_audio" -> Audio (only if model supports audio) +// +// Processing Logic: +// 1. For images (image_url): +// - If model supports vision -> keep as image_url (may convert URL to base64) +// - If model doesn't support -> use vision agent/MCP to extract text -> convert to type="text" +// +// 2. For audio (input_audio): +// - If model supports audio -> keep as input_audio +// - If model doesn't support -> use audio agent/MCP to transcribe -> convert to type="text" +// +// 3. For files (type="file"): +// - Parse uploader wrapper (__uploader://fileid) or fetch HTTP URL +// - Detect file type (PDF, Word, Excel, Image, etc.) +// - Process based on file type: +// - Images: same as image processing above +// - PDF: use vision tool if available, otherwise extract text -> type="text" +// - Word/Excel/PPT/CSV: extract text -> type="text" +// - MUST convert to type="text" or type="image_url" (if image and model supports) +// +// 4. For data (type="data"): +// - Fetch data from sources (models, KB, MCP resources, etc.) +// - Format as readable text +// - MUST convert to type="text" +// +// Return: Messages with only standard LLM-compatible content types (text, image_url, input_audio) +func Vision(ctx *agentContext.Context, capabilities *openai.Capabilities, messages []agentContext.Message, uses *agentContext.Uses) ([]agentContext.Message, error) { + // Initialize handlers and fetcher + registry := NewRegistry() + fetcher := NewFetcher() + + // Cache for processed files (uploader wrapper -> extracted text) + // Ensures each file is only processed once + processedFiles := make(map[string]string) + + // Process each message + processedMessages := make([]agentContext.Message, 0, len(messages)) + + for _, msg := range messages { + processedMsg, err := processMessage(ctx, &msg, capabilities, uses, registry, fetcher, processedFiles) + if err != nil { + // Log error but continue processing other messages + // TODO: Add proper logging + fmt.Printf("Warning: failed to process message: %v\n", err) + processedMessages = append(processedMessages, msg) // Keep original on error + continue + } + processedMessages = append(processedMessages, processedMsg) + } + + return processedMessages, nil +} + +// processMessage processes a single message and its content parts +func processMessage( + ctx *agentContext.Context, + msg *agentContext.Message, + capabilities *openai.Capabilities, + uses *agentContext.Uses, + registry *Registry, + fetcher Fetcher, + processedFiles map[string]string, +) (agentContext.Message, error) { + // If content is simple string, no processing needed + if _, ok := msg.GetContentAsString(); ok { + return *msg, nil + } + + // Get content parts + parts, ok := msg.GetContentAsParts() + if !ok { + return *msg, nil + } + + // Process each content part + processedParts := make([]agentContext.ContentPart, 0, len(parts)) + for _, part := range parts { + processedPart, err := processContentPart(ctx, &part, capabilities, uses, registry, fetcher, processedFiles) + if err != nil { + // Log error and handle gracefully + fmt.Printf("Warning: failed to process content part: %v\n", err) + + // For image_url that failed to process, convert to text description + // This prevents sending unsupported multimodal content to non-vision models + if part.Type == agentContext.ContentImageURL { + processedParts = append(processedParts, agentContext.ContentPart{ + Type: agentContext.ContentText, + Text: fmt.Sprintf("[Image processing failed: %s]", part.ImageURL.URL), + }) + } else { + // For other types, keep original + processedParts = append(processedParts, part) + } + continue + } + + // If handling returned text, convert to text part + if processedPart.Text != "" { + processedParts = append(processedParts, agentContext.ContentPart{ + Type: agentContext.ContentText, + Text: processedPart.Text, + }) + } else if processedPart.ContentPart != nil { + // Use the processed content part (e.g., base64 image) + processedParts = append(processedParts, *processedPart.ContentPart) + } else { + // Keep original if no handling result + processedParts = append(processedParts, part) + } + } + + // Return new message with processed content + return agentContext.Message{ + Role: msg.Role, + Content: processedParts, + Name: msg.Name, + ToolCallID: msg.ToolCallID, + ToolCalls: msg.ToolCalls, + Refusal: msg.Refusal, + }, nil +} + +// processContentPart processes a single content part +// IMPORTANT: Must convert extended types (file, data) to standard types (text, image_url, input_audio) +func processContentPart( + ctx *agentContext.Context, + part *agentContext.ContentPart, + capabilities *openai.Capabilities, + uses *agentContext.Uses, + registry *Registry, + fetcher Fetcher, + processedFiles map[string]string, +) (*Result, error) { + // 1. Handle standard types - pass through + switch part.Type { + case agentContext.ContentText: + // Text is already standard, pass through + return &Result{ + ContentPart: part, + }, nil + + case agentContext.ContentImageURL: + // Image URL - check if it needs processing + return processImageURLContent(ctx, part, capabilities, uses, registry, fetcher, processedFiles) + + case agentContext.ContentInputAudio: + // Audio - check if it needs processing + return processAudioContent(ctx, part, capabilities, uses, registry, fetcher, processedFiles) + } + + // 2. Handle extended types - MUST convert to standard types + switch part.Type { + case agentContext.ContentFile: + return processFileContent(ctx, part, capabilities, uses, registry, fetcher, processedFiles) + + case agentContext.ContentData: + return processDataContent(ctx, part) + + default: + // Unknown type, return error + return nil, fmt.Errorf("unsupported content type: %s", part.Type) + } +} + +// processFileContent processes file content with caching +func processFileContent( + ctx *agentContext.Context, + part *agentContext.ContentPart, + capabilities *openai.Capabilities, + uses *agentContext.Uses, + registry *Registry, + fetcher Fetcher, + processedFiles map[string]string, +) (*Result, error) { + if part.File == nil || part.File.URL == "" { + return nil, fmt.Errorf("file content part missing URL") + } + + url := part.File.URL + + // Step 1: Try to get cached text (three-tier cache) + cachedText, found, err := tryGetCachedText(ctx, url, processedFiles) + if err != nil { + return nil, fmt.Errorf("failed to check cache: %w", err) + } + if found { + // Cache hit! Return as text + return &Result{ + Text: cachedText, + }, nil + } + + // Step 2: No cache, need to process the file + // Determine content source + source, sourceURL, err := determineContentSource(part) + if err != nil { + return nil, fmt.Errorf("failed to determine content source: %w", err) + } + + // Fetch content + info, err := fetcher.Fetch(ctx, source, sourceURL) + if err != nil { + return nil, fmt.Errorf("failed to fetch content: %w", err) + } + + // Detect file type if not already set + if info.FileType == FileTypeUnknown { + info.FileType = DetectFileType(info.ContentType, part.File.Filename) + } + + // Process with appropriate handler + result, err := registry.Handle(ctx, info, capabilities, uses) + if err != nil { + return nil, fmt.Errorf("failed to handle content: %w", err) + } + + // Step 3: Cache the result if it's text + if result.Text != "" { + if cacheErr := cacheProcessedText(ctx, url, result.Text, processedFiles); cacheErr != nil { + // Log error but don't fail the request + fmt.Printf("Warning: failed to cache processed text: %v\n", cacheErr) + } + } + + return result, nil +} + +// processImageURLContent processes image_url content +// If URL is uploader wrapper or HTTP, fetch and process it +func processImageURLContent( + ctx *agentContext.Context, + part *agentContext.ContentPart, + capabilities *openai.Capabilities, + uses *agentContext.Uses, + registry *Registry, + fetcher Fetcher, + processedFiles map[string]string, +) (*Result, error) { + if part.ImageURL == nil || part.ImageURL.URL == "" { + return nil, fmt.Errorf("image_url content missing URL") + } + + url := part.ImageURL.URL + + // If it's a data URI (base64), pass through + if strings.HasPrefix(url, "data:") { + return &Result{ + ContentPart: part, + }, nil + } + + // If it's uploader wrapper or HTTP URL, need to process + // Check cache first + cachedText, found, err := tryGetCachedText(ctx, url, processedFiles) + if err != nil { + return nil, fmt.Errorf("failed to check cache: %w", err) + } + if found { + // Cache hit! Return as text + return &Result{ + Text: cachedText, + }, nil + } + + // Determine source + source, sourceURL, err := determineContentSource(part) + if err != nil { + return nil, fmt.Errorf("failed to determine content source: %w", err) + } + + // Fetch content + info, err := fetcher.Fetch(ctx, source, sourceURL) + if err != nil { + return nil, fmt.Errorf("failed to fetch image: %w", err) + } + + // Set file type as image + info.FileType = FileTypeImage + + // Process with image handler + result, err := registry.Handle(ctx, info, capabilities, uses) + if err != nil { + return nil, fmt.Errorf("failed to handle image: %w", err) + } + + // Cache if result is text + if result.Text != "" { + if cacheErr := cacheProcessedText(ctx, url, result.Text, processedFiles); cacheErr != nil { + fmt.Printf("Warning: failed to cache processed text: %v\n", cacheErr) + } + } + + return result, nil +} + +// processAudioContent processes input_audio content +func processAudioContent( + ctx *agentContext.Context, + part *agentContext.ContentPart, + capabilities *openai.Capabilities, + uses *agentContext.Uses, + registry *Registry, + fetcher Fetcher, + processedFiles map[string]string, +) (*Result, error) { + if part.InputAudio == nil || part.InputAudio.Data == "" { + return nil, fmt.Errorf("input_audio content missing data") + } + + // For now, pass through audio as-is + // TODO: Implement audio processing (transcription, etc.) + return &Result{ + ContentPart: part, + }, nil +} + +// processDataContent processes data content (converts to text) +func processDataContent(ctx *agentContext.Context, part *agentContext.ContentPart) (*Result, error) { + if part.Data == nil { + return nil, fmt.Errorf("data content part missing data") + } + + // TODO: Implement data processing + // For now, just return error + return nil, fmt.Errorf("data content processing not implemented yet") +} + +// determineContentSource determines where the content comes from +func determineContentSource(part *agentContext.ContentPart) (Source, string, error) { + var url string + + // Extract URL based on content type + switch part.Type { + case agentContext.ContentFile: + if part.File == nil || part.File.URL == "" { + return "", "", fmt.Errorf("file content missing URL") + } + url = part.File.URL + + case agentContext.ContentImageURL: + if part.ImageURL == nil || part.ImageURL.URL == "" { + return "", "", fmt.Errorf("image_url content missing URL") + } + url = part.ImageURL.URL + + case agentContext.ContentInputAudio: + if part.InputAudio == nil || part.InputAudio.Data == "" { + return "", "", fmt.Errorf("input_audio content missing data") + } + // Audio data is base64, treat as base64 source + return SourceBase64, part.InputAudio.Data, nil + + default: + return "", "", fmt.Errorf("unsupported content type for source detection: %s", part.Type) + } + + // Determine source type based on URL format + if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") { + return SourceHTTP, url, nil + } + + if strings.HasPrefix(url, "__") { + // Uploader wrapper format: __uploader://fileid + return SourceUploader, url, nil + } + + if strings.HasPrefix(url, "data:") { + // Data URI (base64) + return SourceBase64, url, nil + } + + // Default to treating as uploader if no prefix matches + return SourceUploader, url, nil +} + +// shouldProcessWithModel checks if content should be processed by the model directly +func shouldProcessWithModel(capabilities *openai.Capabilities, fileType FileType) (bool, agentContext.VisionFormat) { + // TODO: Implement model capability check + // For images: check if model supports vision + // For audio: check if model supports audio input + // Return whether to use model and the format to use + return false, agentContext.VisionFormatNone +} + +// getToolForProcessing gets the agent/MCP tool to use for processing +func getToolForProcessing(uses *agentContext.Uses, fileType FileType) string { + // TODO: Implement tool selection + // Based on file type, return the appropriate tool from uses + // - Images -> uses.Vision + // - Audio -> uses.Audio + // - PDF (if vision available) -> uses.Vision + return "" +} + +// tryGetCachedText checks if the URL is an uploader wrapper and tries to get cached text +// Returns (text, found, error) +func tryGetCachedText(ctx *agentContext.Context, url string, processedFiles map[string]string) (string, bool, error) { + // Parse URL to check if it's an uploader wrapper + uploaderName, fileID, isWrapper := attachment.Parse(url) + if !isWrapper { + return "", false, nil // Not an uploader wrapper, no cache + } + + // 1. Check in-memory cache for this Vision call + if text, ok := processedFiles[fileID]; ok { + return text, true, nil + } + + // 2. Try attachment manager's content_preview (cross-call cache) + manager, exists := attachment.Managers[uploaderName] + if exists { + // GetText with fullContent=false to get preview (default) + text, err := manager.GetText(ctx.Context, fileID, false) + if err == nil && text != "" { + // Cache in-memory for this Vision call + processedFiles[fileID] = text + return text, true, nil + } + } + + // No cache found + return "", false, nil +} + +// cacheProcessedText caches the processed text for an uploader wrapper +func cacheProcessedText(ctx *agentContext.Context, url string, text string, processedFiles map[string]string) error { + // Parse URL to get uploader name and file ID + uploaderName, fileID, isWrapper := attachment.Parse(url) + if !isWrapper { + return nil // Not an uploader wrapper, nothing to cache + } + + // 1. Cache in-memory for this Vision call + processedFiles[fileID] = text + + // 2. Save to attachment manager for future Vision calls + manager, exists := attachment.Managers[uploaderName] + if exists { + return manager.SaveText(ctx.Context, fileID, text) + } + + return nil +} diff --git a/agent/content/content_vision_test.go b/agent/content/content_vision_test.go new file mode 100644 index 00000000..837f2a8f --- /dev/null +++ b/agent/content/content_vision_test.go @@ -0,0 +1,458 @@ +package content_test + +import ( + "bytes" + "context" + "image" + "image/color" + "image/png" + "mime/multipart" + "strings" + "testing" + + "github.com/yaoapp/gou/connector/openai" + "github.com/yaoapp/yao/agent/content" + agentContext "github.com/yaoapp/yao/agent/context" + "github.com/yaoapp/yao/agent/testutils" + "github.com/yaoapp/yao/attachment" +) + +// setupTestUploader creates and registers a test uploader manager +// The manager will be registered with "__" prefix as required by attachment.Parse +func setupTestUploader(t *testing.T, name string) attachment.FileManager { + // Register with __ prefix to match Parse behavior + managerName := "__" + name + manager, err := attachment.Register(managerName, "local", attachment.ManagerOption{ + Driver: "local", + MaxSize: "10M", + AllowedTypes: []string{"text/*", "image/*", "application/*"}, + Options: map[string]interface{}{ + "path": "/tmp/test_vision_attachments_" + name, + }, + }) + if err != nil { + t.Fatalf("Failed to register attachment manager '%s': %v", managerName, err) + } + return manager +} + +// cleanupTestUploader removes the test uploader from registry +func cleanupTestUploader(name string) { + delete(attachment.Managers, "__"+name) +} + +// generateTestImage creates a valid PNG image (100x100 red square) +func generateTestImage(t *testing.T) []byte { + img := image.NewRGBA(image.Rect(0, 0, 100, 100)) + red := color.RGBA{255, 0, 0, 255} + for y := 0; y < 100; y++ { + for x := 0; x < 100; x++ { + img.Set(x, y, red) + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + t.Fatalf("Failed to encode test image: %v", err) + } + return buf.Bytes() +} + +// TestVision_TextFile tests Vision function with text/code file parsing +func TestVision_TextFile(t *testing.T) { + testutils.Prepare(t) + defer testutils.Clean(t) + + // Setup test uploader + uploaderName := "test-vision-text" + manager := setupTestUploader(t, uploaderName) + defer cleanupTestUploader(uploaderName) + + // 1. Create and upload a Go source file + testContent := `package main + +import "fmt" + +func main() { + fmt.Println("Hello, Vision Test!") +} +` + + // Upload file + reader := strings.NewReader(testContent) + fileHeader := &attachment.FileHeader{ + FileHeader: &multipart.FileHeader{ + Filename: "main.go", + Size: int64(len(testContent)), + Header: make(map[string][]string), + }, + } + fileHeader.Header.Set("Content-Type", "text/x-go") + + uploadedFile, err := manager.Upload(context.Background(), fileHeader, reader, attachment.UploadOption{ + Groups: []string{"vision", "test"}, + }) + if err != nil { + t.Fatalf("Failed to upload file: %v", err) + } + + t.Logf("Uploaded file ID: %s", uploadedFile.ID) + + // 2. Prepare Vision context (text files don't need special capabilities) + ctx := agentContext.New(context.Background(), nil, "test") + + capabilities := &openai.Capabilities{} + + messages := []agentContext.Message{ + { + Role: "user", + Content: []agentContext.ContentPart{ + { + Type: agentContext.ContentFile, + File: &agentContext.FileAttachment{ + URL: "__" + uploaderName + "://" + uploadedFile.ID, + Filename: "main.go", + }, + }, + }, + }, + } + + // 3. Call Vision function + result, err := content.Vision(ctx, capabilities, messages, nil) + if err != nil { + t.Fatalf("Vision function failed: %v", err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 message, got %d", len(result)) + } + + // 4. Verify result + contentParts, ok := result[0].Content.([]agentContext.ContentPart) + if !ok { + t.Fatalf("Expected content to be []ContentPart, got %T", result[0].Content) + } + + if len(contentParts) != 1 { + t.Fatalf("Expected 1 content part, got %d", len(contentParts)) + } + + // Should be converted to text + if contentParts[0].Type != agentContext.ContentText { + t.Errorf("Expected ContentText type, got %s", contentParts[0].Type) + } + + if !strings.Contains(contentParts[0].Text, "package main") { + t.Errorf("Expected text to contain 'package main', got: %s", contentParts[0].Text) + } + + if !strings.Contains(contentParts[0].Text, "Hello, Vision Test!") { + t.Errorf("Expected text to contain 'Hello, Vision Test!', got: %s", contentParts[0].Text) + } + + t.Logf("✓ Text file successfully parsed: %d characters", len(contentParts[0].Text)) +} + +// TestVision_ImageWithVisionSupport tests image processing with vision-capable model +func TestVision_ImageWithVisionSupport(t *testing.T) { + testutils.Prepare(t) + defer testutils.Clean(t) + + // Setup test uploader + uploaderName := "test-vision-image" + manager := setupTestUploader(t, uploaderName) + defer cleanupTestUploader(uploaderName) + + // 1. Create and upload a test image (1x1 red PNG) + imageData := []byte{ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, + 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, + 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, + 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, + 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, + 0x44, 0xAE, 0x42, 0x60, 0x82, + } + + // Upload image + reader := strings.NewReader(string(imageData)) + fileHeader := &attachment.FileHeader{ + FileHeader: &multipart.FileHeader{ + Filename: "test.png", + Size: int64(len(imageData)), + Header: make(map[string][]string), + }, + } + fileHeader.Header.Set("Content-Type", "image/png") + + uploadedFile, err := manager.Upload(context.Background(), fileHeader, reader, attachment.UploadOption{ + Groups: []string{"vision", "test"}, + }) + if err != nil { + t.Fatalf("Failed to upload image: %v", err) + } + + // 2. Prepare Vision context with vision-capable model + ctx := agentContext.New(context.Background(), nil, "test") + + // Construct capabilities with vision support (OpenAI format) + capabilities := &openai.Capabilities{ + Vision: agentContext.VisionFormatOpenAI, // OpenAI vision format + } + + messages := []agentContext.Message{ + { + Role: "user", + Content: []agentContext.ContentPart{ + { + Type: agentContext.ContentImageURL, + ImageURL: &agentContext.ImageURL{ + URL: "__" + uploaderName + "://" + uploadedFile.ID, + }, + }, + }, + }, + } + + // 3. Call Vision function (no uses needed for direct vision support) + result, err := content.Vision(ctx, capabilities, messages, nil) + if err != nil { + t.Fatalf("Vision function failed: %v", err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 message, got %d", len(result)) + } + + // 4. Verify result + contentParts, ok := result[0].Content.([]agentContext.ContentPart) + if !ok { + t.Fatalf("Expected content to be []ContentPart, got %T", result[0].Content) + } + + if len(contentParts) != 1 { + t.Fatalf("Expected 1 content part, got %d", len(contentParts)) + } + + // If model supports vision, should be image_url with base64 + if capabilities.Vision != nil { + if contentParts[0].Type != agentContext.ContentImageURL { + t.Errorf("Expected ContentImageURL type, got %s", contentParts[0].Type) + } + + if contentParts[0].ImageURL == nil { + t.Fatal("Expected ImageURL to be set") + } + + if !strings.Contains(contentParts[0].ImageURL.URL, "data:image/png;base64,") { + t.Errorf("Expected base64 data URI, got: %s", contentParts[0].ImageURL.URL) + } + + t.Logf("✓ Image processed with vision support: %d bytes (base64)", len(contentParts[0].ImageURL.URL)) + } else { + // If no vision support, should fall back to text (via agent/MCP) + t.Logf("ℹ Model doesn't support vision, result type: %s", contentParts[0].Type) + } +} + +// TestVision_ImageWithAgent tests image processing with vision agent when model doesn't support vision +// Note: This test demonstrates the agent fallback mechanism when the model doesn't support vision +func TestVision_ImageWithAgent(t *testing.T) { + testutils.Prepare(t) + defer testutils.Clean(t) + + // Setup test uploader + uploaderName := "test-vision-agent" + manager := setupTestUploader(t, uploaderName) + defer cleanupTestUploader(uploaderName) + + // 1. Generate and upload a valid test image (100x100 red PNG) + imageData := generateTestImage(t) + + reader := strings.NewReader(string(imageData)) + fileHeader := &attachment.FileHeader{ + FileHeader: &multipart.FileHeader{ + Filename: "test.png", + Size: int64(len(imageData)), + Header: make(map[string][]string), + }, + } + fileHeader.Header.Set("Content-Type", "image/png") + + uploadedFile, err := manager.Upload(context.Background(), fileHeader, reader, attachment.UploadOption{ + Groups: []string{"vision", "test"}, + }) + if err != nil { + t.Fatalf("Failed to upload image: %v", err) + } + + // 2. Prepare Vision context with proper setup + // Model does NOT support vision, but uses.Vision specifies a vision agent + ctx := agentContext.New(context.Background(), nil, "test") + + // Capabilities without vision support + capabilities := &openai.Capabilities{ + Vision: nil, // No vision support + } + + // Uses configuration with vision agent + uses := &agentContext.Uses{ + Vision: "tests.vision-helper", // Use vision-helper agent + } + + messages := []agentContext.Message{ + { + Role: "user", + Content: []agentContext.ContentPart{ + { + Type: agentContext.ContentImageURL, + ImageURL: &agentContext.ImageURL{ + URL: "__" + uploaderName + "://" + uploadedFile.ID, + }, + }, + }, + }, + } + + // 3. Call Vision - should use agent since model doesn't support vision + result, err := content.Vision(ctx, capabilities, messages, uses) + if err != nil { + t.Fatalf("Vision function failed: %v", err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 message, got %d", len(result)) + } + + // 4. Verify result is text (processed by vision agent) + contentParts, ok := result[0].Content.([]agentContext.ContentPart) + if !ok { + t.Fatalf("Expected content to be []ContentPart, got %T", result[0].Content) + } + + if len(contentParts) != 1 { + t.Fatalf("Expected 1 content part, got %d", len(contentParts)) + } + + if contentParts[0].Type != agentContext.ContentText { + t.Errorf("Expected ContentText (from agent), got: %s", contentParts[0].Type) + } + + if contentParts[0].Text == "" { + t.Error("Expected non-empty text from vision agent processing") + } + + t.Logf("✓ Vision agent processed image to text: %d characters", len(contentParts[0].Text)) + t.Logf("Agent response text:\n%s", contentParts[0].Text) +} + +// TestVision_CachedContent tests that file content is cached and reused +func TestVision_CachedContent(t *testing.T) { + testutils.Prepare(t) + defer testutils.Clean(t) + + // Setup test uploader + uploaderName := "test-vision-cache" + manager := setupTestUploader(t, uploaderName) + defer cleanupTestUploader(uploaderName) + + // 1. Upload a text file + testContent := "Test content for caching verification" + + reader := strings.NewReader(testContent) + fileHeader := &attachment.FileHeader{ + FileHeader: &multipart.FileHeader{ + Filename: "cache-test.txt", + Size: int64(len(testContent)), + Header: make(map[string][]string), + }, + } + fileHeader.Header.Set("Content-Type", "text/plain") + + uploadedFile, err := manager.Upload(context.Background(), fileHeader, reader, attachment.UploadOption{ + Groups: []string{"vision", "test"}, + }) + if err != nil { + t.Fatalf("Failed to upload file: %v", err) + } + + // 2. Prepare Vision context with same file referenced twice + ctx := agentContext.New(context.Background(), nil, "test") + + // Construct simple capabilities (text files don't need vision) + capabilities := &openai.Capabilities{} + + messages := []agentContext.Message{ + { + Role: "user", + Content: []agentContext.ContentPart{ + { + Type: agentContext.ContentFile, + File: &agentContext.FileAttachment{ + URL: "__" + uploaderName + "://" + uploadedFile.ID, + Filename: "cache-test.txt", + }, + }, + { + Type: agentContext.ContentFile, + File: &agentContext.FileAttachment{ + URL: "__" + uploaderName + "://" + uploadedFile.ID, // Same file + Filename: "cache-test.txt", + }, + }, + }, + }, + } + + // 3. Call Vision + result, err := content.Vision(ctx, capabilities, messages, nil) + if err != nil { + t.Fatalf("Vision function failed: %v", err) + } + + if len(result) != 1 { + t.Fatalf("Expected 1 message, got %d", len(result)) + } + + // 4. Verify both file references were processed + contentParts, ok := result[0].Content.([]agentContext.ContentPart) + if !ok { + t.Fatalf("Expected content to be []ContentPart") + } + + if len(contentParts) != 2 { + t.Fatalf("Expected 2 content parts (both files), got %d", len(contentParts)) + } + + // Both should be text with same content + if contentParts[0].Type != agentContext.ContentText { + t.Errorf("First part: expected ContentText, got %s", contentParts[0].Type) + } + + if contentParts[1].Type != agentContext.ContentText { + t.Errorf("Second part: expected ContentText, got %s", contentParts[1].Type) + } + + if !strings.Contains(contentParts[0].Text, testContent) { + t.Errorf("First part text doesn't contain expected content") + } + + if !strings.Contains(contentParts[1].Text, testContent) { + t.Errorf("Second part text doesn't contain expected content") + } + + // Verify content was cached (check attachment manager) + cachedText, err := manager.GetText(context.Background(), uploadedFile.ID) + if err != nil { + t.Fatalf("Failed to get cached text: %v", err) + } + + if cachedText == "" { + t.Error("Expected content to be cached in attachment manager") + } + + t.Logf("✓ Content successfully cached and reused: %d characters", len(cachedText)) +} diff --git a/agent/content/excel.go b/agent/content/excel.go new file mode 100644 index 00000000..61fb4440 --- /dev/null +++ b/agent/content/excel.go @@ -0,0 +1,48 @@ +package content + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// ExcelHandler handles Microsoft Excel spreadsheets +type ExcelHandler struct{} + +// CanHandle checks if this handler can handle the content type +func (h *ExcelHandler) CanHandle(contentType string, fileType FileType) bool { + return fileType == FileTypeExcel || + contentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" || + contentType == "application/vnd.ms-excel" || + strings.Contains(contentType, "excel") || + strings.Contains(contentType, "spreadsheet") +} + +// Handle processes Excel spreadsheet content +func (h *ExcelHandler) Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) { + // TODO: Implement Excel handling + // 1. Extract data from .xlsx or .xls file + // 2. Convert to text format (e.g., CSV-like or structured text) + // 3. Handle multiple sheets + // 4. Return Result with formatted text + return nil, fmt.Errorf("not implemented") +} + +// extractExcelText extracts text from Excel file +func extractExcelText(data []byte, contentType string) (string, error) { + // TODO: Implement Excel text extraction + // Handle both .xls (old format) and .xlsx (new format) + // Consider using libraries like: + // - github.com/360EntSecGroup-Skylar/excelize for .xlsx + // Format output as readable text or CSV + return "", fmt.Errorf("not implemented") +} + +// formatExcelAsText formats Excel data as readable text +func formatExcelAsText(sheets map[string][][]string) string { + // TODO: Format multiple sheets into readable text + // Include sheet names, headers, and data + return "" +} diff --git a/agent/content/fetch.go b/agent/content/fetch.go new file mode 100644 index 00000000..dcff11a1 --- /dev/null +++ b/agent/content/fetch.go @@ -0,0 +1,90 @@ +package content + +import ( + "fmt" + + agentContext "github.com/yaoapp/yao/agent/context" + "github.com/yaoapp/yao/attachment" +) + +// DefaultFetcher implements the Fetcher interface +type DefaultFetcher struct{} + +// NewFetcher creates a new default fetcher +func NewFetcher() Fetcher { + return &DefaultFetcher{} +} + +// Fetch retrieves content from HTTP URL or uploader wrapper +func (f *DefaultFetcher) Fetch(ctx *agentContext.Context, source Source, url string) (*Info, error) { + switch source { + case SourceHTTP: + return f.fetchHTTP(ctx, url) + case SourceUploader: + return f.fetchUploader(ctx, url) + default: + return nil, fmt.Errorf("unsupported source: %s", source) + } +} + +// fetchHTTP fetches content from an HTTP(S) URL +func (f *DefaultFetcher) fetchHTTP(ctx *agentContext.Context, url string) (*Info, error) { + // TODO: Implement HTTP fetch logic + // 1. Download file from URL + // 2. Detect content type + // 3. Detect file type based on content type and extension + // 4. Return Info with data + return nil, fmt.Errorf("not implemented") +} + +// fetchUploader fetches content from uploader wrapper (__uploader://fileid) +func (f *DefaultFetcher) fetchUploader(ctx *agentContext.Context, wrapper string) (*Info, error) { + // 1. Parse wrapper to get uploader name and file ID + uploaderName, fileID, ok := attachment.Parse(wrapper) + if !ok { + return nil, fmt.Errorf("invalid uploader wrapper format: %s", wrapper) + } + + // 2. Get attachment manager + var manager attachment.FileManager + var exists bool + + // Try to get manager by name + manager, exists = attachment.Managers[uploaderName] + if !exists { + return nil, fmt.Errorf("uploader '%s' not found", uploaderName) + } + + // 3. Get file info + file, err := manager.Info(ctx.Context, fileID) + if err != nil { + return nil, fmt.Errorf("failed to get file info: %w", err) + } + + // 4. Read file content + data, err := manager.Read(ctx.Context, fileID) + if err != nil { + return nil, fmt.Errorf("failed to read file: %w", err) + } + + // 5. Return Info with data + return &Info{ + Data: data, + ContentType: file.ContentType, + FileType: DetectFileType(file.ContentType, file.Filename), + }, nil +} + +// parseUploaderWrapper parses uploader wrapper format: __uploader://fileid +func parseUploaderWrapper(wrapper string) (uploaderName, fileID string, err error) { + // TODO: Implement wrapper parsing + // Format: __uploader://fileid + return "", "", fmt.Errorf("not implemented") +} + +// detectFileType detects file type from content type and data +func detectFileType(contentType string, data []byte) FileType { + // TODO: Implement file type detection + // Based on content type and magic bytes + return FileTypeUnknown +} diff --git a/agent/content/image.go b/agent/content/image.go new file mode 100644 index 00000000..7eb266f0 --- /dev/null +++ b/agent/content/image.go @@ -0,0 +1,173 @@ +package content + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// ImageHandler handles image content +type ImageHandler struct{} + +// CanHandle checks if this handler can handle the content type +func (h *ImageHandler) CanHandle(contentType string, fileType FileType) bool { + return fileType == FileTypeImage || strings.HasPrefix(contentType, "image/") +} + +// Handle processes image content +// Logic: +// 1. If model supports vision -> convert to base64 or image_url format +// 2. If model doesn't support vision -> use agent/MCP specified in uses.Vision +func (h *ImageHandler) Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) { + if len(info.Data) == 0 { + return nil, fmt.Errorf("no image data to process") + } + + if capabilities == nil { + return nil, fmt.Errorf("no capabilities provided") + } + + // Check if model supports vision + supportsVision, visionFormat := agentContext.GetVisionSupport(capabilities) + + if supportsVision { + // Model supports vision - return as image_url ContentPart + contentPart, err := h.handleWithVisionModel(ctx, info, visionFormat) + if err != nil { + return nil, fmt.Errorf("failed to handle image with vision model: %w", err) + } + return &Result{ + ContentPart: contentPart, + }, nil + } + + // Model doesn't support vision - use vision agent/MCP + visionTool := "" + if uses != nil && uses.Vision != "" { + visionTool = uses.Vision + } + + if visionTool == "" { + return nil, fmt.Errorf("model doesn't support vision and no vision tool specified in uses.Vision") + } + + // Call vision agent/MCP to extract text + text, err := h.handleWithVisionAgent(ctx, info, visionTool) + if err != nil { + return nil, fmt.Errorf("failed to handle image with vision agent/MCP: %w", err) + } + + return &Result{ + Text: text, + }, nil +} + +// handleWithVisionModel processes image using model's vision capability +func (h *ImageHandler) handleWithVisionModel(ctx *agentContext.Context, info *Info, format agentContext.VisionFormat) (*agentContext.ContentPart, error) { + // Encode image to base64 + base64Data := encodeImageBase64(info.Data, info.ContentType) + + // Format according to model's vision format + switch format { + case agentContext.VisionFormatOpenAI: + // OpenAI format: image_url with data URI + return &agentContext.ContentPart{ + Type: agentContext.ContentImageURL, + ImageURL: &agentContext.ImageURL{ + URL: base64Data, + Detail: agentContext.DetailAuto, + }, + }, nil + + case agentContext.VisionFormatClaude: + // Claude format: also uses image_url but may have different handling + // For now, use the same format as OpenAI + return &agentContext.ContentPart{ + Type: agentContext.ContentImageURL, + ImageURL: &agentContext.ImageURL{ + URL: base64Data, + Detail: agentContext.DetailAuto, + }, + }, nil + + case agentContext.VisionFormatDefault, "": + // Default format (when Vision: true) - use OpenAI format + return &agentContext.ContentPart{ + Type: agentContext.ContentImageURL, + ImageURL: &agentContext.ImageURL{ + URL: base64Data, + Detail: agentContext.DetailAuto, + }, + }, nil + + default: + return nil, fmt.Errorf("unsupported vision format: %s", format) + } +} + +// handleWithVisionAgent processes image using vision agent or MCP +func (h *ImageHandler) handleWithVisionAgent(ctx *agentContext.Context, info *Info, visionTool string) (string, error) { + // Parse vision tool format + // Format can be: + // - "agent_id" (call agent) + // - "mcp:server_id" (call MCP tool) + if strings.HasPrefix(visionTool, "mcp:") { + // MCP tool + serverID := strings.TrimPrefix(visionTool, "mcp:") + return h.callMCPVisionTool(ctx, serverID, info) + } + + // Agent call + return h.callVisionAgent(ctx, visionTool, info) +} + +// callVisionAgent calls a vision agent to describe the image +func (h *ImageHandler) callVisionAgent(ctx *agentContext.Context, agentID string, info *Info) (string, error) { + // Prepare message with image + base64Data := EncodeToBase64DataURI(info.Data, info.ContentType) + + message := agentContext.Message{ + Role: agentContext.RoleUser, + Content: []agentContext.ContentPart{ + { + Type: agentContext.ContentText, + Text: "Please describe this image in detail.", + }, + { + Type: agentContext.ContentImageURL, + ImageURL: &agentContext.ImageURL{ + URL: base64Data, + Detail: agentContext.DetailAuto, + }, + }, + }, + } + + return CallAgent(ctx, agentID, message) +} + +// callMCPVisionTool calls an MCP vision tool to describe the image +func (h *ImageHandler) callMCPVisionTool(ctx *agentContext.Context, serverID string, info *Info) (string, error) { + // Prepare base64 encoded image for MCP tool + base64Data := EncodeToBase64DataURI(info.Data, info.ContentType) + + // Prepare arguments for MCP tool + arguments := map[string]interface{}{ + "image": base64Data, + "content_type": info.ContentType, + } + + // Call MCP tool (typically "describe_image" or similar) + return CallMCPTool(ctx, serverID, "describe_image", arguments) +} + +// encodeImageBase64 encodes image data to base64 with data URI prefix +func encodeImageBase64(data []byte, contentType string) string { + // Use the common function + if contentType == "" { + contentType = "image/png" // default for images + } + return EncodeToBase64DataURI(data, contentType) +} diff --git a/agent/content/image_test.go b/agent/content/image_test.go new file mode 100644 index 00000000..b28b345c --- /dev/null +++ b/agent/content/image_test.go @@ -0,0 +1,265 @@ +package content + +import ( + stdContext "context" + "encoding/base64" + "os" + "strings" + "testing" + + "github.com/yaoapp/gou/connector/openai" + "github.com/yaoapp/gou/plan" + agentContext "github.com/yaoapp/yao/agent/context" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/openapi/oauth/types" + "github.com/yaoapp/yao/test" +) + +func TestMain(m *testing.M) { + // Setup test environment + test.Prepare(nil, config.Conf) + defer test.Clean() + + // Run tests + code := m.Run() + os.Exit(code) +} + +// newTestContext creates a Context for testing with commonly used fields pre-populated +func newTestContext(capabilities *openai.Capabilities) *agentContext.Context { + return &agentContext.Context{ + Context: stdContext.Background(), + Space: plan.NewMemorySharedSpace(), + ChatID: "test-chat", + AssistantID: "test-assistant", + Connector: "openai", + Locale: "en-us", + Theme: "light", + Client: agentContext.Client{ + Type: "web", + UserAgent: "TestAgent/1.0", + IP: "127.0.0.1", + }, + Referer: agentContext.RefererAPI, + Accept: agentContext.AcceptWebCUI, + Route: "", + Metadata: make(map[string]interface{}), + Capabilities: capabilities, + Authorized: &types.AuthorizedInfo{ + Subject: "test-user", + ClientID: "test-client-id", + UserID: "test-user-123", + TeamID: "test-team-456", + TenantID: "test-tenant-789", + }, + } +} + +func TestImageHandler_CanHandle(t *testing.T) { + handler := &ImageHandler{} + + tests := []struct { + name string + contentType string + fileType FileType + want bool + }{ + {"PNG image", "image/png", FileTypeImage, true}, + {"JPEG image", "image/jpeg", FileTypeImage, true}, + {"GIF image", "image/gif", FileTypeImage, true}, + {"WebP image", "image/webp", FileTypeImage, true}, + {"Text (should not handle)", "text/plain", FileTypeText, false}, + {"PDF (should not handle)", "application/pdf", FileTypePDF, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := handler.CanHandle(tt.contentType, tt.fileType) + if got != tt.want { + t.Errorf("CanHandle(%q, %q) = %v, want %v", tt.contentType, tt.fileType, got, tt.want) + } + }) + } +} + +func TestImageHandler_Handle_WithVisionSupport(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + handler := &ImageHandler{} + + // Create a simple test image (1x1 red PNG) + pngData := createTestPNG() + + // Create capabilities with vision support + capabilities := &openai.Capabilities{ + Vision: "openai", // Vision is enabled with OpenAI format + } + + // Create test context + ctx := newTestContext(capabilities) + + info := &Info{ + FileType: FileTypeImage, + ContentType: "image/png", + Data: pngData, + } + + result, err := handler.Handle(ctx, info, capabilities, nil) + if err != nil { + t.Fatalf("Handle() error = %v", err) + } + + if result == nil { + t.Fatal("Expected non-nil result") + } + + if result.ContentPart == nil { + t.Fatal("Expected ContentPart for vision-supported model") + } + + if result.ContentPart.Type != agentContext.ContentImageURL { + t.Errorf("Expected ContentPart type = %v, got %v", agentContext.ContentImageURL, result.ContentPart.Type) + } + + if result.ContentPart.ImageURL == nil { + t.Fatal("Expected ImageURL to be set") + } + + // Verify base64 encoding + if result.ContentPart.ImageURL.URL == "" { + t.Error("Expected non-empty URL") + } + + // Should be data URI format + if len(result.ContentPart.ImageURL.URL) < 20 { + t.Error("Expected data URI to be longer") + } +} + +func TestImageHandler_Handle_WithoutVisionSupport(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + handler := &ImageHandler{} + + // Create a simple test image + pngData := createTestPNG() + + // Create capabilities WITHOUT vision support + capabilities := &openai.Capabilities{ + Vision: nil, // No vision support + } + + // Create test context + ctx := newTestContext(capabilities) + + info := &Info{ + FileType: FileTypeImage, + ContentType: "image/png", + Data: pngData, + } + + // Should return error because no vision support and no tool + _, err := handler.Handle(ctx, info, capabilities, nil) + if err == nil { + t.Error("Expected error when no vision support and no tool specified") + } +} + +func TestImageHandler_Handle_EmptyData(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + handler := &ImageHandler{} + + capabilities := &openai.Capabilities{ + Vision: "openai", + } + + // Create test context + ctx := newTestContext(capabilities) + + info := &Info{ + FileType: FileTypeImage, + ContentType: "image/png", + Data: []byte{}, // Empty data + } + + _, err := handler.Handle(ctx, info, capabilities, nil) + if err == nil { + t.Error("Expected error for empty image data") + } +} + +func TestEncodeImageBase64(t *testing.T) { + tests := []struct { + name string + data []byte + contentType string + wantPrefix string + }{ + { + name: "PNG image", + data: []byte{0x89, 0x50, 0x4E, 0x47}, // PNG magic number + contentType: "image/png", + wantPrefix: "data:image/png;base64,", + }, + { + name: "JPEG image", + data: []byte{0xFF, 0xD8, 0xFF}, // JPEG magic number + contentType: "image/jpeg", + wantPrefix: "data:image/jpeg;base64,", + }, + { + name: "Empty content type defaults to PNG", + data: []byte{0x01, 0x02, 0x03}, + contentType: "", + wantPrefix: "data:image/png;base64,", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := encodeImageBase64(tt.data, tt.contentType) + + // Check prefix + if !strings.HasPrefix(result, tt.wantPrefix) { + t.Errorf("Expected prefix %q, got %q", tt.wantPrefix, result[:len(tt.wantPrefix)]) + } + + // Verify base64 encoding by decoding + base64Part := result[len(tt.wantPrefix):] + decoded, err := base64.StdEncoding.DecodeString(base64Part) + if err != nil { + t.Errorf("Failed to decode base64: %v", err) + } + + // Verify decoded data matches original + if len(decoded) != len(tt.data) { + t.Errorf("Decoded length = %d, want %d", len(decoded), len(tt.data)) + } + for i := range decoded { + if decoded[i] != tt.data[i] { + t.Errorf("Decoded byte[%d] = %x, want %x", i, decoded[i], tt.data[i]) + } + } + }) + } +} + +// createTestPNG creates a minimal valid PNG image (1x1 red pixel) +func createTestPNG() []byte { + // This is a minimal valid 1x1 red PNG image + return []byte{ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, // 1x1 dimensions + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, + 0xDE, 0x00, 0x00, 0x00, 0x0C, 0x49, 0x44, 0x41, // IDAT chunk + 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, 0x00, + 0x00, 0x03, 0x01, 0x01, 0x00, 0x18, 0xDD, 0x8D, + 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, // IEND chunk + 0x44, 0xAE, 0x42, 0x60, 0x82, + } +} diff --git a/agent/content/interfaces.go b/agent/content/interfaces.go new file mode 100644 index 00000000..c1ed89c2 --- /dev/null +++ b/agent/content/interfaces.go @@ -0,0 +1,25 @@ +package content + +import ( + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// Handler defines the interface for handling different content types +// Converts content (images, documents, etc.) to text or standard formats +type Handler interface { + // CanHandle checks if this handler can handle the given content type + CanHandle(contentType string, fileType FileType) bool + + // Handle converts the content and returns processed result + // ctx: agent context (passed from Vision function) + // capabilities: model capabilities (for vision/audio support detection) + // uses: configuration for external tools (agents/MCP servers) + Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) +} + +// Fetcher defines the interface for fetching content from different sources +type Fetcher interface { + // Fetch retrieves content from a URL or file ID + Fetch(ctx *agentContext.Context, source Source, url string) (*Info, error) +} diff --git a/agent/content/pdf.go b/agent/content/pdf.go new file mode 100644 index 00000000..771d4879 --- /dev/null +++ b/agent/content/pdf.go @@ -0,0 +1,50 @@ +package content + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// PDFHandler handles PDF documents +type PDFHandler struct{} + +// CanHandle checks if this handler can handle the content type +func (h *PDFHandler) CanHandle(contentType string, fileType FileType) bool { + return fileType == FileTypePDF || + contentType == "application/pdf" || + strings.Contains(contentType, "pdf") +} + +// Handle processes PDF content +// Logic: +// 1. Check if uses.Vision is specified and supports PDF +// 2. If yes, use vision tool to handle PDF (images + text) +// 3. If no, extract text directly from PDF +func (h *PDFHandler) Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) { + // TODO: Implement PDF handling + // 1. Check if vision tool supports PDF + // 2. If yes: + // - Call vision tool to handle PDF (handles both text and images) + // 3. If no: + // - Extract text from PDF using default library + // 4. Return Result with extracted text + return nil, fmt.Errorf("not implemented") +} + +// extractPDFText extracts text content from PDF +func extractPDFText(data []byte) (string, error) { + // TODO: Implement PDF text extraction + // Use a PDF library to extract text + // Consider preserving layout/structure + return "", fmt.Errorf("not implemented") +} + +// handleWithVisionTool processes PDF using vision tool (for PDFs with images) +func handleWithVisionTool(ctx *agentContext.Context, data []byte, visionTool string) (string, error) { + // TODO: Implement vision tool PDF processing + // Some vision tools can handle PDF directly and extract both text and images + return "", fmt.Errorf("not implemented") +} diff --git a/agent/content/registry.go b/agent/content/registry.go new file mode 100644 index 00000000..fe3af0bd --- /dev/null +++ b/agent/content/registry.go @@ -0,0 +1,47 @@ +package content + +import ( + "fmt" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// Registry holds all registered content handlers +type Registry struct { + handlers []Handler +} + +// NewRegistry creates a new handler registry with default handlers +func NewRegistry() *Registry { + return &Registry{ + handlers: []Handler{ + &ImageHandler{}, + &AudioHandler{}, + &PDFHandler{}, + &WordHandler{}, + &ExcelHandler{}, + &TextHandler{}, + }, + } +} + +// GetHandler finds the appropriate handler for the given content +func (r *Registry) GetHandler(contentType string, fileType FileType) Handler { + for _, handler := range r.handlers { + if handler.CanHandle(contentType, fileType) { + return handler + } + } + return nil +} + +// Handle processes content using the appropriate handler +func (r *Registry) Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) { + handler := r.GetHandler(info.ContentType, info.FileType) + if handler == nil { + return nil, fmt.Errorf("no handler found for content type: %s, file type: %s", info.ContentType, info.FileType) + } + + return handler.Handle(ctx, info, capabilities, uses) +} diff --git a/agent/content/text.go b/agent/content/text.go new file mode 100644 index 00000000..5be8b693 --- /dev/null +++ b/agent/content/text.go @@ -0,0 +1,123 @@ +package content + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// TextHandler handles plain text, code files, CSV, JSON, XML, Markdown, etc. +type TextHandler struct{} + +// CanHandle checks if this handler can handle the content type +func (h *TextHandler) CanHandle(contentType string, fileType FileType) bool { + // Handle explicit text file types + if fileType == FileTypeText || fileType == FileTypeCSV || fileType == FileTypeJSON { + return true + } + + // Handle text-based MIME types + if strings.HasPrefix(contentType, "text/") { + return true + } + + // Handle common text-based content types + textContentTypes := []string{ + "application/json", + "application/xml", + "application/javascript", + "application/typescript", + "application/x-yaml", + "application/yaml", + "application/toml", + "application/x-sh", + "application/x-python", + "application/x-ruby", + "application/x-perl", + } + + for _, ct := range textContentTypes { + if contentType == ct || strings.Contains(contentType, ct) { + return true + } + } + + return false +} + +// Handle processes text content +func (h *TextHandler) Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) { + if len(info.Data) == 0 { + return nil, fmt.Errorf("no data to process") + } + + var text string + var err error + + // Handle different text formats + switch { + case info.FileType == FileTypeCSV || strings.Contains(info.ContentType, "csv"): + // Format CSV as readable text (for now, just return as-is, can enhance later) + text = string(info.Data) + + case info.FileType == FileTypeJSON || + info.ContentType == "application/json" || + strings.Contains(info.ContentType, "json"): + // Pretty print JSON + text, err = formatJSONAsText(info.Data) + if err != nil { + // If JSON parsing fails, return raw text + text = string(info.Data) + } + + case info.ContentType == "application/xml" || + strings.Contains(info.ContentType, "xml"): + // For now, return XML as-is (can enhance formatting later) + text = string(info.Data) + + default: + // Plain text, code files, markdown, etc. + text, err = readTextContent(info.Data, info.ContentType) + if err != nil { + return nil, fmt.Errorf("failed to read text content: %w", err) + } + } + + return &Result{ + Text: text, + }, nil +} + +// readTextContent reads text content from data +func readTextContent(data []byte, contentType string) (string, error) { + // For now, assume UTF-8 encoding + // TODO: Add encoding detection if needed (e.g., using golang.org/x/text/encoding) + return string(data), nil +} + +// formatCSVAsText formats CSV data as readable text +func formatCSVAsText(data []byte) (string, error) { + // TODO: Parse CSV and format as readable table + // Consider using encoding/csv package + // For now, just return as-is + return string(data), nil +} + +// formatJSONAsText formats JSON data as readable text +func formatJSONAsText(data []byte) (string, error) { + // Pretty print JSON with indentation + var obj interface{} + if err := json.Unmarshal(data, &obj); err != nil { + return "", err + } + + pretty, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return "", err + } + + return string(pretty), nil +} diff --git a/agent/content/text_test.go b/agent/content/text_test.go new file mode 100644 index 00000000..acc72912 --- /dev/null +++ b/agent/content/text_test.go @@ -0,0 +1,152 @@ +package content + +import ( + "testing" +) + +func TestTextHandler_CanHandle(t *testing.T) { + handler := &TextHandler{} + + tests := []struct { + name string + contentType string + fileType FileType + want bool + }{ + {"Plain text", "text/plain", FileTypeText, true}, + {"Markdown", "text/markdown", FileTypeText, true}, + {"HTML", "text/html", FileTypeText, true}, + {"JSON", "application/json", FileTypeJSON, true}, + {"JavaScript", "application/javascript", FileTypeText, true}, + {"TypeScript", "application/typescript", FileTypeText, true}, + {"YAML", "application/yaml", FileTypeText, true}, + {"CSV", "text/csv", FileTypeCSV, true}, + {"XML", "application/xml", FileTypeText, true}, + {"PDF (should not handle)", "application/pdf", FileTypePDF, false}, + {"Image (should not handle)", "image/png", FileTypeImage, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := handler.CanHandle(tt.contentType, tt.fileType) + if got != tt.want { + t.Errorf("CanHandle(%q, %q) = %v, want %v", tt.contentType, tt.fileType, got, tt.want) + } + }) + } +} + +func TestTextHandler_Handle(t *testing.T) { + handler := &TextHandler{} + + tests := []struct { + name string + info *Info + wantErr bool + checkResult func(*testing.T, *Result) + }{ + { + name: "Plain text", + info: &Info{ + FileType: FileTypeText, + ContentType: "text/plain", + Data: []byte("Hello, World!"), + }, + wantErr: false, + checkResult: func(t *testing.T, r *Result) { + if r.Text != "Hello, World!" { + t.Errorf("Expected 'Hello, World!', got %q", r.Text) + } + }, + }, + { + name: "JSON with pretty print", + info: &Info{ + FileType: FileTypeJSON, + ContentType: "application/json", + Data: []byte(`{"name":"test","value":123}`), + }, + wantErr: false, + checkResult: func(t *testing.T, r *Result) { + // Should be pretty printed + if len(r.Text) <= len(`{"name":"test","value":123}`) { + t.Errorf("JSON should be pretty printed, got: %q", r.Text) + } + }, + }, + { + name: "Code file (Go)", + info: &Info{ + FileType: FileTypeText, + ContentType: "text/plain", + Data: []byte("package main\n\nfunc main() {\n\tprintln(\"Hello\")\n}"), + }, + wantErr: false, + checkResult: func(t *testing.T, r *Result) { + if r.Text == "" { + t.Error("Expected non-empty text for Go code") + } + }, + }, + { + name: "Empty data", + info: &Info{ + FileType: FileTypeText, + ContentType: "text/plain", + Data: []byte{}, + }, + wantErr: true, + }, + } + + // Create test context + testCtx := newTestContext(nil) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := handler.Handle(testCtx, tt.info, nil, nil) + if (err != nil) != tt.wantErr { + t.Errorf("Handle() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && tt.checkResult != nil { + tt.checkResult(t, result) + } + }) + } +} + +func TestDetectFileType(t *testing.T) { + tests := []struct { + name string + contentType string + filename string + want FileType + }{ + {"Go file", "text/plain", "main.go", FileTypeText}, + {"Python file", "text/plain", "script.py", FileTypeText}, + {"JavaScript file", "application/javascript", "app.js", FileTypeText}, + {"TypeScript file", "text/plain", "index.ts", FileTypeText}, + {"Markdown file", "text/markdown", "README.md", FileTypeText}, + {"JSON file", "application/json", "config.json", FileTypeJSON}, + {"YAML file", "text/plain", "config.yml", FileTypeText}, + {"PDF file", "application/pdf", "document.pdf", FileTypePDF}, + {"Image file", "image/png", "photo.png", FileTypeImage}, + {"CSV file", "text/csv", "data.csv", FileTypeCSV}, + {"XML file", "application/xml", "config.xml", FileTypeXML}, + {"Shell script", "text/plain", "script.sh", FileTypeText}, + {"Dockerfile", "text/plain", "Dockerfile", FileTypeText}, + {"gitignore", "text/plain", ".gitignore", FileTypeText}, + {"HTML", "text/html", "index.html", FileTypeText}, + {"CSS", "text/css", "styles.css", FileTypeText}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DetectFileType(tt.contentType, tt.filename) + if got != tt.want { + t.Errorf("DetectFileType(%q, %q) = %v, want %v", tt.contentType, tt.filename, got, tt.want) + } + }) + } +} diff --git a/agent/content/tools.go b/agent/content/tools.go new file mode 100644 index 00000000..333a4092 --- /dev/null +++ b/agent/content/tools.go @@ -0,0 +1,190 @@ +package content + +import ( + "context" + "encoding/base64" + "fmt" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/mcp" + "github.com/yaoapp/kun/log" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// AgentCaller interface for calling agents (to avoid circular dependency) +type AgentCaller interface { + Stream(ctx *agentContext.Context, messages []agentContext.Message) (interface{}, error) +} + +// AgentGetterFunc is a function type that gets an agent by ID +var AgentGetterFunc func(agentID string) (AgentCaller, error) + +// CallAgent calls an agent to process content (vision, audio, etc.) +// This is a generic function that can be used by any handler +func CallAgent(ctx *agentContext.Context, agentID string, message agentContext.Message) (string, error) { + if AgentGetterFunc == nil { + return "", fmt.Errorf("AgentGetterFunc not initialized") + } + + // Load the agent by ID using the injected function + agent, err := AgentGetterFunc(agentID) + if err != nil { + return "", fmt.Errorf("failed to load agent %s: %w", agentID, err) + } + + // Call the agent with the message + messages := []agentContext.Message{message} + + connectorBackup := ctx.Connector + ctx.Connector = "" + defer func() { + ctx.Connector = connectorBackup + }() + response, err := agent.Stream(ctx, messages) + if err != nil { + return "", fmt.Errorf("failed to call agent %s: %w", agentID, err) + } + + // Extract text from agent response + // Two formats are supported: + // 1. Custom Hook response (from Next hook) + // 2. Standard Agent Stream response (LLM completion) + + return extractTextFromAgentResponse(response) +} + +// extractTextFromAgentResponse extracts text from agent response +// Handles two response formats: +// 1. Custom Hook response: if it's a string, return directly; otherwise JSON stringify +// 2. Standard response: extract from completion.content +func extractTextFromAgentResponse(response interface{}) (string, error) { + if response == nil { + return "", fmt.Errorf("agent returned nil response") + } + + // Try to parse as standard response format (has "completion" field with LLM result) + if responseMap, ok := response.(map[string]interface{}); ok { + // Check for completion field (standard LLM response) + if completion, hasCompletion := responseMap["completion"]; hasCompletion { + if completionMap, ok := completion.(map[string]interface{}); ok { + // Extract content from completion + if content, hasContent := completionMap["content"]; hasContent { + // Content can be string or structured + switch v := content.(type) { + case string: + return v, nil + case []interface{}: + // Handle multimodal content array + var text string + for _, part := range v { + if partMap, ok := part.(map[string]interface{}); ok { + if partType, _ := partMap["type"].(string); partType == "text" { + if textContent, ok := partMap["text"].(string); ok { + text += textContent + } + } + } + } + if text != "" { + return text, nil + } + } + } + } + } + + // Check for data field (custom hook response with data wrapper) + if data, hasData := responseMap["data"]; hasData { + // If data is a string, return directly + if dataStr, ok := data.(string); ok { + return dataStr, nil + } + // Otherwise, JSON stringify + jsonBytes, err := jsoniter.Marshal(data) + if err != nil { + return "", fmt.Errorf("failed to serialize hook data response: %w", err) + } + return string(jsonBytes), nil + } + + // If the map itself looks like content, try to extract + // This handles cases where the response is the content directly + if content, hasContent := responseMap["content"]; hasContent { + if contentStr, ok := content.(string); ok { + return contentStr, nil + } + } + } + + // Custom Hook response: if it's a plain string, return directly + if responseStr, ok := response.(string); ok { + return responseStr, nil + } + + // Otherwise, JSON stringify the response + jsonBytes, err := jsoniter.Marshal(response) + if err != nil { + return "", fmt.Errorf("failed to serialize agent response: %w", err) + } + return string(jsonBytes), nil +} + +// CallMCPTool calls an MCP tool to process content +// This is a generic function that can be used by any handler +func CallMCPTool(ctx *agentContext.Context, serverID string, toolName string, arguments map[string]interface{}) (string, error) { + // Get MCP context for cancellation/timeout control + mcpCtx := ctx.Context + if mcpCtx == nil { + mcpCtx = context.Background() + } + + // Get MCP client + client, err := mcp.Select(serverID) + if err != nil { + return "", fmt.Errorf("failed to select MCP client '%s': %w", serverID, err) + } + + // Call the tool + log.Trace("[Content] Calling MCP tool: %s (server: %s)", toolName, serverID) + callResult, err := client.CallTool(mcpCtx, toolName, arguments) + if err != nil { + return "", fmt.Errorf("MCP tool call failed: %w", err) + } + + // Check if result is an error + if callResult.IsError { + return "", fmt.Errorf("MCP tool returned error: %v", callResult.Content) + } + + // Extract text content from result + // callResult.Content is []ToolContent + var text string + for _, content := range callResult.Content { + if content.Type == "text" { + text += content.Text + } + // Can also handle other types like image, resource if needed + } + + if text == "" { + // If no text content found, return error + return "", fmt.Errorf("MCP tool returned no text content") + } + + return text, nil +} + +// EncodeToBase64DataURI encodes data to base64 with data URI prefix +// This is useful for encoding images, audio, or other binary data +func EncodeToBase64DataURI(data []byte, contentType string) string { + // Ensure we have a valid content type + if contentType == "" { + contentType = "application/octet-stream" // default + } + + // Encode to base64 + encoded := base64.StdEncoding.EncodeToString(data) + + // Return data URI format + return fmt.Sprintf("data:%s;base64,%s", contentType, encoded) +} diff --git a/agent/content/types.go b/agent/content/types.go new file mode 100644 index 00000000..f0399854 --- /dev/null +++ b/agent/content/types.go @@ -0,0 +1,261 @@ +package content + +import "github.com/yaoapp/yao/agent/context" + +// FileType represents the type of file content +type FileType string + +const ( + // Image types + FileTypeImage FileType = "image" + + // Audio types + FileTypeAudio FileType = "audio" + + // Document types + FileTypeText FileType = "text" + FileTypePDF FileType = "pdf" + FileTypeWord FileType = "word" + FileTypeExcel FileType = "excel" + FileTypePPT FileType = "ppt" + FileTypeCSV FileType = "csv" + + // Data types + FileTypeJSON FileType = "json" + FileTypeXML FileType = "xml" + + // Binary + FileTypeBinary FileType = "binary" + + // Other + FileTypeUnknown FileType = "unknown" +) + +// Source represents where the content comes from +type Source string + +const ( + SourceHTTP Source = "http" // HTTP(S) URL + SourceUploader Source = "uploader" // Uploader wrapper: __uploader://fileid + SourceBase64 Source = "base64" // Base64 encoded data + SourceLocal Source = "local" // Local file path +) + +// Result represents the result of content handling +type Result struct { + Text string // Extracted text content + ContentPart *context.ContentPart // Processed ContentPart (for model input) + Metadata map[string]interface{} // Additional metadata + Error error // Error if handling failed +} + +// Info holds information about a content part to be handled +type Info struct { + Source Source // Where the content comes from + FileType FileType // Type of the file + ContentType string // MIME content type + URL string // Original URL or file ID + Data []byte // File data (if already fetched) + + // For uploader wrapper + UploaderName string // Uploader name from wrapper + FileID string // File ID from wrapper +} + +// DetectFileType detects file type from content type, filename, and file extension +func DetectFileType(contentType, filename string) FileType { + // Check by content type first + switch { + case contentType == "application/pdf": + return FileTypePDF + case contentType == "application/json": + return FileTypeJSON + case contentType == "application/xml" || contentType == "text/xml": + return FileTypeXML + case contentType == "text/csv": + return FileTypeCSV + case contentType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + contentType == "application/msword": + return FileTypeWord + case contentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + contentType == "application/vnd.ms-excel": + return FileTypeExcel + case contentType == "application/vnd.openxmlformats-officedocument.presentationml.presentation", + contentType == "application/vnd.ms-powerpoint": + return FileTypePPT + } + + // Check image types + if isImageContentType(contentType) { + return FileTypeImage + } + + // Check audio types + if isAudioContentType(contentType) { + return FileTypeAudio + } + + // Check text types + if isTextContentType(contentType) { + return FileTypeText + } + + // If content type doesn't help, check file extension + if filename != "" { + if ext := getFileExtension(filename); ext != "" { + return detectTypeByExtension(ext) + } + } + + return FileTypeUnknown +} + +// isImageContentType checks if content type is an image +func isImageContentType(contentType string) bool { + return contentType != "" && + (contentType == "image/png" || + contentType == "image/jpeg" || + contentType == "image/jpg" || + contentType == "image/gif" || + contentType == "image/webp" || + contentType == "image/svg+xml" || + contentType == "image/bmp") +} + +// isAudioContentType checks if content type is audio +func isAudioContentType(contentType string) bool { + return contentType != "" && + (contentType == "audio/mpeg" || + contentType == "audio/mp3" || + contentType == "audio/wav" || + contentType == "audio/ogg" || + contentType == "audio/flac" || + contentType == "audio/aac") +} + +// isTextContentType checks if content type is text-based +func isTextContentType(contentType string) bool { + if contentType == "" { + return false + } + + // Common text MIME types + textTypes := []string{ + "text/plain", + "text/html", + "text/css", + "text/javascript", + "text/markdown", + "text/x-markdown", + "application/javascript", + "application/typescript", + "application/x-yaml", + "application/yaml", + "application/toml", + "application/x-sh", + "application/x-python", + "application/x-ruby", + "application/x-perl", + "application/x-php", + "application/x-go", + } + + for _, t := range textTypes { + if contentType == t { + return true + } + } + + return false +} + +// getFileExtension extracts file extension from filename (without dot) +func getFileExtension(filename string) string { + for i := len(filename) - 1; i >= 0; i-- { + if filename[i] == '.' { + return filename[i+1:] + } + if filename[i] == '/' || filename[i] == '\\' { + break + } + } + return "" +} + +// detectTypeByExtension detects file type by file extension +func detectTypeByExtension(ext string) FileType { + // Normalize to lowercase + ext = toLower(ext) + + // Image extensions + imageExts := []string{"png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico"} + for _, e := range imageExts { + if ext == e { + return FileTypeImage + } + } + + // Audio extensions + audioExts := []string{"mp3", "wav", "ogg", "flac", "aac", "m4a"} + for _, e := range audioExts { + if ext == e { + return FileTypeAudio + } + } + + // Document extensions + switch ext { + case "pdf": + return FileTypePDF + case "doc", "docx": + return FileTypeWord + case "xls", "xlsx": + return FileTypeExcel + case "ppt", "pptx": + return FileTypePPT + case "csv": + return FileTypeCSV + case "json": + return FileTypeJSON + case "xml": + return FileTypeXML + } + + // Code and text file extensions (very comprehensive list) + textExts := []string{ + "txt", "text", "md", "markdown", "rst", + // Programming languages + "go", "py", "js", "ts", "jsx", "tsx", "java", "c", "cpp", "h", "hpp", + "cs", "rb", "php", "pl", "swift", "kt", "rs", "scala", "clj", + // Web + "html", "htm", "css", "scss", "sass", "less", + // Config + "yaml", "yml", "toml", "ini", "conf", "config", + // Shell + "sh", "bash", "zsh", "fish", + // Data + "sql", "graphql", "proto", + // Others + "log", "gitignore", "env", "dockerfile", + } + for _, e := range textExts { + if ext == e { + return FileTypeText + } + } + + return FileTypeUnknown +} + +// toLower converts ASCII string to lowercase (simple version) +func toLower(s string) string { + result := make([]byte, len(s)) + for i := 0; i < len(s); i++ { + c := s[i] + if c >= 'A' && c <= 'Z' { + c += 'a' - 'A' + } + result[i] = c + } + return string(result) +} diff --git a/agent/content/word.go b/agent/content/word.go new file mode 100644 index 00000000..d854e12f --- /dev/null +++ b/agent/content/word.go @@ -0,0 +1,39 @@ +package content + +import ( + "fmt" + "strings" + + "github.com/yaoapp/gou/connector/openai" + agentContext "github.com/yaoapp/yao/agent/context" +) + +// WordHandler handles Microsoft Word documents +type WordHandler struct{} + +// CanHandle checks if this handler can handle the content type +func (h *WordHandler) CanHandle(contentType string, fileType FileType) bool { + return fileType == FileTypeWord || + contentType == "application/vnd.openxmlformats-officedocument.wordprocessingml.document" || + contentType == "application/msword" || + strings.Contains(contentType, "word") +} + +// Handle processes Word document content +func (h *WordHandler) Handle(ctx *agentContext.Context, info *Info, capabilities *openai.Capabilities, uses *agentContext.Uses) (*Result, error) { + // TODO: Implement Word document handling + // 1. Extract text from .docx or .doc file + // 2. Preserve formatting information if needed + // 3. Return Result with extracted text + return nil, fmt.Errorf("not implemented") +} + +// extractWordText extracts text from Word document +func extractWordText(data []byte, contentType string) (string, error) { + // TODO: Implement Word text extraction + // Handle both .doc (old format) and .docx (new format) + // Consider using libraries like: + // - github.com/unidoc/unioffice for .docx + // - Other libraries for .doc + return "", fmt.Errorf("not implemented") +} diff --git a/agent/context/output.go b/agent/context/output.go index e50bdc54..71c417cd 100644 --- a/agent/context/output.go +++ b/agent/context/output.go @@ -23,6 +23,24 @@ func (ctx *Context) Send(msg *message.Message) error { // Skip lifecycle events for event-type messages (prevent recursion) isEventMessage := msg.Type == message.TypeEvent + // === Handle message_start event: record metadata for future delta chunks === + if isEventMessage && msg.Props != nil { + if event, ok := msg.Props["event"].(string); ok && event == message.EventMessageStart { + if data, ok := msg.Props["data"].(message.EventMessageStartData); ok { + // Record metadata from message_start event + if data.MessageID != "" && ctx.messageMetadata != nil { + ctx.messageMetadata.setMessage(data.MessageID, &MessageMetadata{ + MessageID: data.MessageID, + ThreadID: data.ThreadID, + Type: data.Type, + StartTime: time.Now(), + ChunkCount: 0, // Will be incremented by delta chunks + }) + } + } + } + } + // === Delta operations: Auto-inherit and update metadata === if msg.Delta && msg.MessageID != "" && ctx.messageMetadata != nil { if metadata := ctx.getMessageMetadata(msg.MessageID); metadata != nil { @@ -103,6 +121,7 @@ func (ctx *Context) Send(msg *message.Message) error { MessageID: msg.MessageID, Type: msg.Type, Timestamp: time.Now().UnixMilli(), + ThreadID: msg.ThreadID, // Include ThreadID for concurrent stream identification } messageStartEvent := output.NewEventMessage(message.EventMessageStart, "Message started", messageStartData) if err := ctx.sendRaw(messageStartEvent); err != nil { @@ -147,6 +166,7 @@ func (ctx *Context) Send(msg *message.Message) error { MessageID: msg.MessageID, Type: msg.Type, Timestamp: time.Now().UnixMilli(), + ThreadID: metadata.ThreadID, // Include ThreadID for concurrent stream identification DurationMs: durationMs, ChunkCount: metadata.ChunkCount, Status: "completed", @@ -191,6 +211,7 @@ func (ctx *Context) EndMessage(messageID string, content interface{}) error { MessageID: messageID, Type: metadata.Type, Timestamp: time.Now().UnixMilli(), + ThreadID: metadata.ThreadID, // Include ThreadID for concurrent stream identification DurationMs: durationMs, ChunkCount: metadata.ChunkCount, Status: "completed", diff --git a/agent/context/types.go b/agent/context/types.go index d62cc1e5..a2d5fce9 100644 --- a/agent/context/types.go +++ b/agent/context/types.go @@ -464,6 +464,8 @@ const ( ContentText ContentPartType = "text" // Text content ContentImageURL ContentPartType = "image_url" // Image URL content (Vision) ContentInputAudio ContentPartType = "input_audio" // Input audio content (Audio) + ContentFile ContentPartType = "file" // File attachment (documents, etc.) + ContentData ContentPartType = "data" // Generic data content (base64, binary, etc.) ) // ContentPart represents a part of the message content (for multimodal messages) @@ -473,6 +475,8 @@ type ContentPart struct { Text string `json:"text,omitempty"` // For type="text": the text content ImageURL *ImageURL `json:"image_url,omitempty"` // For type="image_url": the image URL InputAudio *InputAudio `json:"input_audio,omitempty"` // For type="input_audio": the input audio data + File *FileAttachment `json:"file,omitempty"` // For type="file": file attachment + Data *DataContent `json:"data,omitempty"` // For type="data": generic data content } // ImageDetailLevel represents the detail level for image processing @@ -497,6 +501,41 @@ type InputAudio struct { Format string `json:"format"` // Required: Audio format (e.g., "wav", "mp3") } +// FileAttachment represents a file attachment in the message content +// Compatible with frontend InputArea format: { type: 'file', file: { url, filename } } +type FileAttachment struct { + URL string `json:"url"` // Required: URL of the file (http:// or __uploader://fileid wrapper) + Filename string `json:"filename,omitempty"` // Optional: original filename +} + +// DataSourceType represents the type of data source +type DataSourceType string + +// Data source type constants +const ( + DataSourceModel DataSourceType = "model" // Data model + DataSourceKBCollection DataSourceType = "kb_collection" // Knowledge base collection + DataSourceKBDocument DataSourceType = "kb_document" // Knowledge base document/file + DataSourceTable DataSourceType = "table" // Database table + DataSourceAPI DataSourceType = "api" // API endpoint + DataSourceMCPResource DataSourceType = "mcp_resource" // MCP (Model Context Protocol) resource +) + +// DataSource represents a single data source reference +type DataSource struct { + Type DataSourceType `json:"type"` // Required: type of data source + Name string `json:"name"` // Required: name/identifier of the data source + ID string `json:"id,omitempty"` // Optional: specific ID (e.g., document ID, record ID) + Filters map[string]interface{} `json:"filters,omitempty"` // Optional: filters to apply + Metadata map[string]interface{} `json:"metadata,omitempty"` // Optional: additional metadata +} + +// DataContent represents data source references in the message +// Used to reference data models, knowledge base collections, KB documents, etc. +type DataContent struct { + Sources []DataSource `json:"sources"` // Required: array of data source references +} + // ToolCallType represents the type of tool call type ToolCallType string diff --git a/agent/output/message/types.go b/agent/output/message/types.go index 41ac872e..3cf7698d 100644 --- a/agent/output/message/types.go +++ b/agent/output/message/types.go @@ -318,6 +318,7 @@ type EventMessageStartData struct { MessageID string `json:"message_id"` // Message ID (M1, M2, M3...) Type string `json:"type"` // Message type: "text" | "thinking" | "tool_call" | "refusal" Timestamp int64 `json:"timestamp"` // Unix timestamp when message started + ThreadID string `json:"thread_id,omitempty"` // Thread ID (optional; for concurrent streams) ToolCall *EventToolCallInfo `json:"tool_call,omitempty"` // Tool call metadata (if type is "tool_call") Extra map[string]interface{} `json:"extra,omitempty"` // Additional metadata (for custom providers or future extensions) } @@ -329,6 +330,7 @@ type EventMessageEndData struct { MessageID string `json:"message_id"` // Message ID (M1, M2, M3...) Type string `json:"type"` // Message type (same as in message_start) Timestamp int64 `json:"timestamp"` // Unix timestamp when message ended + ThreadID string `json:"thread_id,omitempty"` // Thread ID (optional; for concurrent streams) DurationMs int64 `json:"duration_ms"` // Duration of this message in milliseconds ChunkCount int `json:"chunk_count"` // Number of data chunks in this message Status string `json:"status"` // "completed" | "partial" | "error" From a96946d8bb018b3ec04cd5b20aee91690d705c9e Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 4 Dec 2025 10:43:32 +0800 Subject: [PATCH 5/6] Refactor Assistant context handling to improve options management - Updated the Assistant methods to accept an Options parameter, enhancing flexibility in context management. - Removed the Connector field from the context and related structures, transitioning to a more streamlined options-based approach. - Adjusted various tests to accommodate the new options handling, ensuring comprehensive coverage of the updated functionality. - Enhanced the Create and Next hooks to return options alongside responses, improving the overall usability of the API. - Cleaned up deprecated fields and improved context initialization for better maintainability. --- agent/assistant/agent.go | 47 +++++++----- agent/assistant/agent_interrupt_test.go | 1 - agent/assistant/assistant.go | 4 +- agent/assistant/build_mcp_test.go | 2 +- agent/assistant/build_prompts_test.go | 12 ++-- agent/assistant/build_test.go | 9 ++- agent/assistant/hook/create.go | 52 ++++++++------ agent/assistant/hook/create_bench_test.go | 17 +++-- agent/assistant/hook/create_mem_test.go | 27 ++++--- agent/assistant/hook/create_nested_test.go | 4 +- agent/assistant/hook/create_test.go | 31 ++++---- agent/assistant/hook/goroutine_leak_test.go | 9 ++- agent/assistant/hook/next.go | 24 +++++-- agent/assistant/hook/next_test.go | 19 +++-- agent/assistant/hook/realworld_next_test.go | 17 +++-- agent/assistant/hook/realworld_stress_test.go | 33 +++++---- agent/assistant/load_store_test.go | 19 +++-- agent/assistant/next.go | 5 +- agent/content/image_test.go | 1 - agent/content/tools.go | 12 ++-- agent/context/context.go | 17 ----- agent/context/context_test.go | 3 +- agent/context/interrupt_test.go | 1 - agent/context/jsapi.go | 16 ----- agent/context/jsapi_mcp_test.go | 33 ++++++--- agent/context/jsapi_release_test.go | 15 ++-- agent/context/jsapi_stress_test.go | 24 ++++--- agent/context/jsapi_test.go | 30 -------- agent/context/mcp_test.go | 3 +- agent/context/openapi.go | 52 +++++++------- agent/context/openapi_test.go | 14 +++- agent/context/options.go | 71 +++++++++++++++++++ agent/context/stack.go | 18 +++-- agent/context/stack_test.go | 50 +++++++------ agent/context/types.go | 55 +++++++++----- agent/llm/providers/openai/claude_test.go | 1 - .../llm/providers/openai/deepseek_r1_test.go | 1 - .../llm/providers/openai/deepseek_v3_test.go | 1 - agent/llm/providers/openai/gpt5_test.go | 1 - agent/llm/providers/openai/openai_test.go | 1 - .../llm/providers/openai/temperature_test.go | 1 - openapi/chat/completions.go | 4 +- 42 files changed, 425 insertions(+), 332 deletions(-) create mode 100644 agent/context/options.go diff --git a/agent/assistant/agent.go b/agent/assistant/agent.go index 4f991dec..111288c1 100644 --- a/agent/assistant/agent.go +++ b/agent/assistant/agent.go @@ -18,7 +18,7 @@ import ( // Stream stream the agent // handler is optional, if not provided, a default handler will be used -func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Message, handler ...message.StreamFunc) (interface{}, error) { +func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Message, options ...*context.Options) (interface{}, error) { log.Trace("[AGENT] Stream started: assistant=%s, contextID=%s", ast.ID, ctx.ID) defer log.Trace("[AGENT] Stream ended: assistant=%s, contextID=%s", ast.ID, ctx.ID) @@ -39,8 +39,16 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa // Initialize // ================================================ + // Get or create options + var opts *context.Options + if len(options) > 0 && options[0] != nil { + opts = options[0] + } else { + opts = &context.Options{} + } + // Initialize stack and auto-handle completion/failure/restore - _, _, done := context.EnterStack(ctx, ast.ID, ctx.Referer) + _, _, done := context.EnterStack(ctx, ast.ID, opts) defer done() fmt.Println("--- Stack debug ---") @@ -51,11 +59,11 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa fmt.Println("------ end stack debug ------") // Determine stream handler - streamHandler := ast.getStreamHandler(ctx, handler...) + streamHandler := ast.getStreamHandler(ctx, opts) // Get connector and capabilities early (before sending stream_start) // so that output adapters can use them when converting stream_start event - err = ast.initializeCapabilities(ctx) + err = ast.initializeCapabilities(ctx, opts) if err != nil { ast.sendStreamEndOnError(ctx, streamHandler, streamStartTime, err) return nil, err @@ -85,7 +93,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa var createResponse *context.HookCreateResponse if ast.Script != nil { var err error - createResponse, err = ast.Script.Create(ctx, fullMessages) + createResponse, opts, err = ast.Script.Create(ctx, fullMessages, opts) if err != nil { ast.traceAgentFail(agentNode, err) // Send error stream_end for root stack @@ -235,11 +243,11 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa if ast.Script != nil { var err error - nextResponse, err = ast.Script.Next(ctx, &context.NextHookPayload{ + nextResponse, opts, err = ast.Script.Next(ctx, &context.NextHookPayload{ Messages: fullMessages, Completion: completionResponse, Tools: toolCallResponses, - }) + }, opts) if err != nil { ast.traceAgentFail(agentNode, err) ast.sendStreamEndOnError(ctx, streamHandler, streamStartTime, err) @@ -318,14 +326,14 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa return finalResponse, nil } -// GetConnector get the connector object, capabilities, and error with priority: createResponse > ctx > ast -// Note: createResponse.Connector is already applied to ctx.Connector by applyContextAdjustments in create.go +// GetConnector get the connector object, capabilities, and error with priority: opts.Connector > ast.Connector +// Note: opts.Connector may be set by Create hook's applyOptionsAdjustments // Returns: (connector, capabilities, error) -func (ast *Assistant) GetConnector(ctx *context.Context) (connector.Connector, *openai.Capabilities, error) { - // Determine connector ID with priority +func (ast *Assistant) GetConnector(ctx *context.Context, opts ...*context.Options) (connector.Connector, *openai.Capabilities, error) { + // Determine connector ID with priority: opts.Connector > ast.Connector connectorID := ast.Connector - if ctx.Connector != "" { - connectorID = ctx.Connector + if len(opts) > 0 && opts[0] != nil && opts[0].Connector != "" { + connectorID = opts[0].Connector } // If empty, return error @@ -361,10 +369,11 @@ func (ast *Assistant) Info(locale ...string) *message.AssistantInfo { } } -// getStreamHandler returns the stream handler from the provided handlers or a default one -func (ast *Assistant) getStreamHandler(ctx *context.Context, handler ...message.StreamFunc) message.StreamFunc { - if len(handler) > 0 && handler[0] != nil { - return handler[0] +// getStreamHandler returns the stream handler from options or a default one +func (ast *Assistant) getStreamHandler(ctx *context.Context, opts ...*context.Options) message.StreamFunc { + // Check if handler is provided in options + if len(opts) > 0 && opts[0] != nil && opts[0].Writer != nil { + return handlers.DefaultStreamHandler(ctx) } return handlers.DefaultStreamHandler(ctx) } @@ -460,12 +469,12 @@ func (ast *Assistant) handleInterrupt(ctx *context.Context, signal *context.Inte // initializeCapabilities gets connector and capabilities, then sets them in context // This should be called early (before sending stream_start) so that output adapters // can use capabilities when converting stream_start event -func (ast *Assistant) initializeCapabilities(ctx *context.Context) error { +func (ast *Assistant) initializeCapabilities(ctx *context.Context, opts *context.Options) error { if ast.Prompts == nil && ast.MCP == nil { return nil } - _, capabilities, err := ast.GetConnector(ctx) + _, capabilities, err := ast.GetConnector(ctx, opts) if err != nil { return err } diff --git a/agent/assistant/agent_interrupt_test.go b/agent/assistant/agent_interrupt_test.go index 716fdd6e..09a56752 100644 --- a/agent/assistant/agent_interrupt_test.go +++ b/agent/assistant/agent_interrupt_test.go @@ -22,7 +22,6 @@ func newTestContextWithInterrupt(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/assistant/assistant.go b/agent/assistant/assistant.go index df5f3baf..5238bd8b 100644 --- a/agent/assistant/assistant.go +++ b/agent/assistant/assistant.go @@ -29,8 +29,8 @@ type agentCallerWrapper struct { ast *Assistant } -func (w *agentCallerWrapper) Stream(ctx *agentContext.Context, messages []agentContext.Message) (interface{}, error) { - return w.ast.Stream(ctx, messages) +func (w *agentCallerWrapper) Stream(ctx *agentContext.Context, messages []agentContext.Message, options ...*agentContext.Options) (interface{}, error) { + return w.ast.Stream(ctx, messages, options...) } // Get get the assistant by id diff --git a/agent/assistant/build_mcp_test.go b/agent/assistant/build_mcp_test.go index aa55b581..805df44e 100644 --- a/agent/assistant/build_mcp_test.go +++ b/agent/assistant/build_mcp_test.go @@ -215,7 +215,7 @@ func TestBuildRequest_MCP(t *testing.T) { // Call create hook to get createResponse var createResponse *context.HookCreateResponse if hookAgent.Script != nil { - createResponse, err = hookAgent.Script.Create(hookCtx, inputMessages) + createResponse, _, err = hookAgent.Script.Create(hookCtx, inputMessages, &context.Options{}) if err != nil { t.Fatalf("Failed to call create hook: %s", err.Error()) } diff --git a/agent/assistant/build_prompts_test.go b/agent/assistant/build_prompts_test.go index a96fda4c..529a015a 100644 --- a/agent/assistant/build_prompts_test.go +++ b/agent/assistant/build_prompts_test.go @@ -650,7 +650,7 @@ func TestPromptPresetAssistant(t *testing.T) { } // Call Create hook - createResponse, err := ast.Script.Create(ctx, messages) + createResponse, _, err := ast.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, createResponse) assert.Equal(t, "mode.friendly", createResponse.PromptPreset) @@ -681,7 +681,7 @@ func TestPromptPresetAssistant(t *testing.T) { } // Call Create hook - createResponse, err := ast.Script.Create(ctx, messages) + createResponse, _, err := ast.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, createResponse) assert.Equal(t, "mode.professional", createResponse.PromptPreset) @@ -718,7 +718,7 @@ func TestPromptPresetAssistant(t *testing.T) { } // Call Create hook - createResponse, err := ast.Script.Create(ctx, messages) + createResponse, _, err := ast.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, createResponse) require.NotNil(t, createResponse.DisableGlobalPrompts) @@ -753,7 +753,7 @@ func TestPromptPresetAssistant(t *testing.T) { } // Call Create hook - createResponse, err := ast.Script.Create(ctx, messages) + createResponse, _, err := ast.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, createResponse) assert.Equal(t, "mode.friendly", createResponse.PromptPreset) @@ -788,7 +788,7 @@ func TestPromptPresetAssistant(t *testing.T) { } // Call Create hook - createResponse, err := ast.Script.Create(ctx, messages) + createResponse, _, err := ast.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, createResponse) assert.Equal(t, "non.existent.preset", createResponse.PromptPreset) @@ -819,7 +819,7 @@ func TestPromptPresetAssistant(t *testing.T) { } // Call Create hook - should return nil - createResponse, err := ast.Script.Create(ctx, messages) + createResponse, _, err := ast.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) assert.Nil(t, createResponse) diff --git a/agent/assistant/build_test.go b/agent/assistant/build_test.go index ec0874a1..6cceddb3 100644 --- a/agent/assistant/build_test.go +++ b/agent/assistant/build_test.go @@ -18,7 +18,6 @@ func newTestContext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ @@ -64,7 +63,7 @@ func TestBuildRequest(t *testing.T) { inputMessages := []context.Message{{Role: "user", Content: "no_override"}} // Call Create hook - createResponse, err := agent.Script.Create(ctx, inputMessages) + createResponse, _, err := agent.Script.Create(ctx, inputMessages, &context.Options{}) if err != nil { t.Fatalf("Failed to call Create hook: %s", err.Error()) } @@ -112,7 +111,7 @@ func TestBuildRequest(t *testing.T) { t.Run("OverrideTemperature", func(t *testing.T) { inputMessages := []context.Message{{Role: "user", Content: "override_temperature"}} - createResponse, err := agent.Script.Create(ctx, inputMessages) + createResponse, _, err := agent.Script.Create(ctx, inputMessages, &context.Options{}) if err != nil { t.Fatalf("Failed to call Create hook: %s", err.Error()) } @@ -143,7 +142,7 @@ func TestBuildRequest(t *testing.T) { t.Run("OverrideAll", func(t *testing.T) { inputMessages := []context.Message{{Role: "user", Content: "override_all"}} - createResponse, err := agent.Script.Create(ctx, inputMessages) + createResponse, _, err := agent.Script.Create(ctx, inputMessages, &context.Options{}) if err != nil { t.Fatalf("Failed to call Create hook: %s", err.Error()) } @@ -196,7 +195,7 @@ func TestBuildRequest(t *testing.T) { t.Run("OverrideRouteMetadata", func(t *testing.T) { inputMessages := []context.Message{{Role: "user", Content: "override_route_metadata"}} - createResponse, err := agent.Script.Create(ctx, inputMessages) + createResponse, _, err := agent.Script.Create(ctx, inputMessages, &context.Options{}) if err != nil { t.Fatalf("Failed to call Create hook: %s", err.Error()) } diff --git a/agent/assistant/hook/create.go b/agent/assistant/hook/create.go index e48f4952..e29eba65 100644 --- a/agent/assistant/hook/create.go +++ b/agent/assistant/hook/create.go @@ -9,53 +9,57 @@ import ( ) // Create create a new assistant -func (s *Script) Create(ctx *context.Context, messages []context.Message) (*context.HookCreateResponse, error) { - res, err := s.Execute(ctx, "Create", messages) +// opts is optional - if provided, will be adjusted based on hook response +func (s *Script) Create(ctx *context.Context, messages []context.Message, opts ...*context.Options) (*context.HookCreateResponse, *context.Options, error) { + // Get or create options + var options *context.Options + if len(opts) > 0 && opts[0] != nil { + options = opts[0] + } else { + options = &context.Options{} + } + + // Execute hook with ctx, messages, and options (convert options to map for JS) + optionsMap := options.ToMap() + res, err := s.Execute(ctx, "Create", messages, optionsMap) if err != nil { - return nil, err + return nil, nil, err } response, err := s.getHookCreateResponse(res) if err != nil { - return nil, err + return nil, nil, err } - // Apply context adjustments from the response back to the context + // Apply adjustments from the response if response != nil { s.applyContextAdjustments(ctx, response) + s.applyOptionsAdjustments(options, response) } - return response, nil + return response, options, nil } -// applyContextAdjustments applies context field overrides from the hook response back to the context +// applyContextAdjustments applies session-level field overrides from the hook response back to the context func (s *Script) applyContextAdjustments(ctx *context.Context, response *context.HookCreateResponse) { - // Override assistant ID if provided - if response.AssistantID != "" { - ctx.AssistantID = response.AssistantID - } + // Note: AssistantID cannot be overridden - it's set at initialization and immutable - // Override connector if provided - if response.Connector != "" { - ctx.Connector = response.Connector - } - - // Override locale if provided + // Override locale if provided (session-level) if response.Locale != "" { ctx.Locale = response.Locale } - // Override theme if provided + // Override theme if provided (session-level) if response.Theme != "" { ctx.Theme = response.Theme } - // Override route if provided + // Override route if provided (session-level) if response.Route != "" { ctx.Route = response.Route } - // Merge or override metadata if provided + // Merge or override metadata if provided (session-level) if len(response.Metadata) > 0 { if ctx.Metadata == nil { ctx.Metadata = make(map[string]interface{}) @@ -67,6 +71,14 @@ func (s *Script) applyContextAdjustments(ctx *context.Context, response *context } } +// applyOptionsAdjustments applies call-level field overrides from the hook response to options +func (s *Script) applyOptionsAdjustments(opts *context.Options, response *context.HookCreateResponse) { + // Override connector if provided (call-level parameter) + if response.Connector != "" { + opts.Connector = response.Connector + } +} + // getHookCreateResponse convert the result to a HookCreateResponse func (s *Script) getHookCreateResponse(res interface{}) (*context.HookCreateResponse, error) { // Handle nil result diff --git a/agent/assistant/hook/create_bench_test.go b/agent/assistant/hook/create_bench_test.go index 9432dbee..2d05375a 100644 --- a/agent/assistant/hook/create_bench_test.go +++ b/agent/assistant/hook/create_bench_test.go @@ -34,7 +34,7 @@ func BenchmarkSimpleStandardMode(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { ctx := newBenchContext("bench-simple-standard", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -61,7 +61,7 @@ func BenchmarkSimplePerformanceMode(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { ctx := newBenchContext("bench-simple-performance", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -95,7 +95,7 @@ func BenchmarkBusinessStandardMode(b *testing.B) { for i := 0; i < b.N; i++ { scenario := scenarios[i%len(scenarios)] ctx := newBenchContext("bench-business-standard", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: scenario.content}, }) if err != nil { @@ -125,7 +125,7 @@ func BenchmarkBusinessPerformanceMode(b *testing.B) { for i := 0; i < b.N; i++ { scenario := scenarios[i%len(scenarios)] ctx := newBenchContext("bench-business-performance", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: scenario.content}, }) if err != nil { @@ -159,7 +159,7 @@ func BenchmarkConcurrentSimpleStandardMode(b *testing.B) { i := 0 for pb.Next() { ctx := newBenchContext("bench-concurrent-simple-standard", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -191,7 +191,7 @@ func BenchmarkConcurrentSimplePerformanceMode(b *testing.B) { i := 0 for pb.Next() { ctx := newBenchContext("bench-concurrent-simple", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -226,7 +226,7 @@ func BenchmarkConcurrentBusinessStandardMode(b *testing.B) { for pb.Next() { scenario := scenarios[i%len(scenarios)] ctx := newBenchContext("bench-concurrent-business-standard", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: scenario.content}, }) if err != nil { @@ -261,7 +261,7 @@ func BenchmarkConcurrentBusinessPerformanceMode(b *testing.B) { for pb.Next() { scenario := scenarios[i%len(scenarios)] ctx := newBenchContext("bench-concurrent-business", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: scenario.content}, }) if err != nil { @@ -301,7 +301,6 @@ func newBenchContext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/assistant/hook/create_mem_test.go b/agent/assistant/hook/create_mem_test.go index 2dd57ef0..0c8e62ee 100644 --- a/agent/assistant/hook/create_mem_test.go +++ b/agent/assistant/hook/create_mem_test.go @@ -36,7 +36,7 @@ func TestMemoryLeakStandardMode(t *testing.T) { // Warm up - execute a few times to stabilize memory for i := 0; i < 10; i++ { ctx := newMemTestContext("warmup", "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) ctx.Release() @@ -52,7 +52,7 @@ func TestMemoryLeakStandardMode(t *testing.T) { iterations := 1000 for i := 0; i < iterations; i++ { ctx := newMemTestContext("mem-test-standard", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -124,7 +124,7 @@ func TestMemoryLeakPerformanceMode(t *testing.T) { // Warm up - execute a few times to stabilize memory and fill isolate pool for i := 0; i < 20; i++ { ctx := newMemTestContext("warmup", "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) ctx.Release() @@ -140,7 +140,7 @@ func TestMemoryLeakPerformanceMode(t *testing.T) { iterations := 1000 for i := 0; i < iterations; i++ { ctx := newMemTestContext("mem-test-performance", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -221,7 +221,7 @@ func TestMemoryLeakBusinessScenarios(t *testing.T) { // Warm up for i := 0; i < 10; i++ { ctx := newMemTestContext("warmup", "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "return_full"}, }) ctx.Release() @@ -240,7 +240,7 @@ func TestMemoryLeakBusinessScenarios(t *testing.T) { iterations := 200 for i := 0; i < iterations; i++ { ctx := newMemTestContext("mem-test-business", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: scenario.content}, }) if err != nil { @@ -298,7 +298,7 @@ func TestMemoryLeakConcurrent(t *testing.T) { // Warm up for i := 0; i < 20; i++ { ctx := newMemTestContext("warmup", "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) ctx.Release() @@ -321,7 +321,7 @@ func TestMemoryLeakConcurrent(t *testing.T) { defer func() { done <- true }() for i := 0; i < iterPerGoroutine; i++ { ctx := newMemTestContext("mem-test-concurrent", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -383,7 +383,7 @@ func TestMemoryLeakNestedCalls(t *testing.T) { // Warm up for i := 0; i < 10; i++ { ctx := newMemTestContext("warmup", "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "nested_script_call"}, }) ctx.Release() @@ -400,7 +400,7 @@ func TestMemoryLeakNestedCalls(t *testing.T) { iterations := 200 for i := 0; i < iterations; i++ { ctx := newMemTestContext("mem-test-nested", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "deep_nested_call"}, }) if err != nil { @@ -459,7 +459,7 @@ func TestMemoryLeakNestedConcurrent(t *testing.T) { // Warm up for i := 0; i < 20; i++ { ctx := newMemTestContext("warmup", "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "nested_script_call"}, }) ctx.Release() @@ -482,7 +482,7 @@ func TestMemoryLeakNestedConcurrent(t *testing.T) { defer func() { done <- true }() for i := 0; i < iterPerGoroutine; i++ { ctx := newMemTestContext("mem-test-nested-concurrent", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "deep_nested_call"}, }) if err != nil { @@ -549,7 +549,7 @@ func TestIsolateDisposal(t *testing.T) { iterations := 100 for i := 0; i < iterations; i++ { ctx := newMemTestContext("disposal-test", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -615,7 +615,6 @@ func newMemTestContext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/assistant/hook/create_nested_test.go b/agent/assistant/hook/create_nested_test.go index c082324f..c12cc9e1 100644 --- a/agent/assistant/hook/create_nested_test.go +++ b/agent/assistant/hook/create_nested_test.go @@ -29,7 +29,7 @@ func TestNestedScriptCall(t *testing.T) { // Call with deep_nested_call scenario // This will: hook -> scripts.tests.create.NestedCall -> GetRoles -> model - res, err := agent.Script.Create(ctx, []context.Message{ + res, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "deep_nested_call"}, }) @@ -86,7 +86,7 @@ func TestNestedScriptCallConcurrent(t *testing.T) { for j := 0; j < iterations; j++ { ctx := newTestContext("test-concurrent", "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "deep_nested_call"}, }) diff --git a/agent/assistant/hook/create_test.go b/agent/assistant/hook/create_test.go index 5e0a4131..7e5bad60 100644 --- a/agent/assistant/hook/create_test.go +++ b/agent/assistant/hook/create_test.go @@ -19,7 +19,6 @@ func newTestContext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ @@ -74,7 +73,7 @@ func TestCreate(t *testing.T) { // Test scenario 1: Return null (should get nil response) t.Run("ReturnNull", func(t *testing.T) { - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_null"}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_null"}}) if err != nil { t.Fatalf("Failed to create with null return: %s", err.Error()) } @@ -85,7 +84,7 @@ func TestCreate(t *testing.T) { // Test scenario 2: Return undefined (should get nil response) t.Run("ReturnUndefined", func(t *testing.T) { - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_undefined"}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_undefined"}}) if err != nil { t.Fatalf("Failed to create with undefined return: %s", err.Error()) } @@ -96,7 +95,7 @@ func TestCreate(t *testing.T) { // Test scenario 3: Return empty object (should get empty HookCreateResponse) t.Run("ReturnEmpty", func(t *testing.T) { - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_empty"}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_empty"}}) if err != nil { t.Fatalf("Failed to create with empty return: %s", err.Error()) } @@ -110,7 +109,7 @@ func TestCreate(t *testing.T) { // Test scenario 4: Return full response with all fields t.Run("ReturnFull", func(t *testing.T) { - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_full"}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_full"}}) if err != nil { t.Fatalf("Failed to create with full return: %s", err.Error()) } @@ -166,7 +165,7 @@ func TestCreate(t *testing.T) { // Test scenario 5: Return partial response t.Run("ReturnPartial", func(t *testing.T) { - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_partial"}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_partial"}}) if err != nil { t.Fatalf("Failed to create with partial return: %s", err.Error()) } @@ -197,7 +196,7 @@ func TestCreate(t *testing.T) { // Test scenario 6: Process call - calls models.__yao.role.Get and adds to messages t.Run("ReturnProcess", func(t *testing.T) { - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_process"}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "return_process"}}) if err != nil { t.Fatalf("Failed to create with process return: %s", err.Error()) } @@ -225,7 +224,7 @@ func TestCreate(t *testing.T) { // Test scenario 7: Default response t.Run("ReturnDefault", func(t *testing.T) { testContent := "Hello, how are you?" - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: testContent}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: testContent}}) if err != nil { t.Fatalf("Failed to create with default return: %s", err.Error()) } @@ -252,7 +251,7 @@ func TestCreate(t *testing.T) { // Test scenario 8: Verify context fields - validates all context fields in JavaScript t.Run("VerifyContext", func(t *testing.T) { - res, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "verify_context"}}) + res, _, err := agent.Script.Create(ctx, []context.Message{{Role: "user", Content: "verify_context"}}) if err != nil { t.Fatalf("Failed to create with verify_context: %s", err.Error()) } @@ -304,7 +303,7 @@ func TestCreate(t *testing.T) { adjustCtx := newTestContext("chat-test-adjust", "tests.create") // Call the hook which should adjust context fields - res, err := agent.Script.Create(adjustCtx, []context.Message{{Role: "user", Content: "adjust_context"}}) + res, _, err := agent.Script.Create(adjustCtx, []context.Message{{Role: "user", Content: "adjust_context"}}) if err != nil { t.Fatalf("Failed to create with adjust_context: %s", err.Error()) } @@ -313,9 +312,7 @@ func TestCreate(t *testing.T) { } // Verify the response contains adjusted fields - if res.AssistantID != "adjusted.assistant" { - t.Errorf("Expected adjusted assistant_id 'adjusted.assistant', got: %s", res.AssistantID) - } + // Note: AssistantID cannot be overridden by hooks, removed from HookCreateResponse if res.Connector != "adjusted-connector" { t.Errorf("Expected adjusted connector 'adjusted-connector', got: %s", res.Connector) } @@ -338,12 +335,8 @@ func TestCreate(t *testing.T) { } // Verify context fields were actually updated - if adjustCtx.AssistantID != "adjusted.assistant" { - t.Errorf("Context assistant_id not updated. Expected 'adjusted.assistant', got: %s", adjustCtx.AssistantID) - } - if adjustCtx.Connector != "adjusted-connector" { - t.Errorf("Context connector not updated. Expected 'adjusted-connector', got: %s", adjustCtx.Connector) - } + // Note: AssistantID is immutable and cannot be overridden + // Note: Connector is now in Options, not in Context if adjustCtx.Locale != "zh-cn" { t.Errorf("Context locale not updated. Expected 'zh-cn', got: %s", adjustCtx.Locale) } diff --git a/agent/assistant/hook/goroutine_leak_test.go b/agent/assistant/hook/goroutine_leak_test.go index 706ad01c..2745e709 100644 --- a/agent/assistant/hook/goroutine_leak_test.go +++ b/agent/assistant/hook/goroutine_leak_test.go @@ -48,7 +48,7 @@ func TestGoroutineLeakDetailed(t *testing.T) { for i := 0; i < iterations; i++ { ctx := newLeakTestContext(fmt.Sprintf("leak-test-%d", i), "tests.create") - _, err := agent.Script.Create(ctx, []context.Message{ + _, _, err := agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) if err != nil { @@ -126,7 +126,7 @@ func TestGoroutineLeakByComponent(t *testing.T) { for i := 0; i < 10; i++ { ctx := newLeakTestContext(fmt.Sprintf("test-%d", i), "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) ctx.Release() @@ -184,7 +184,7 @@ func TestGoroutineLeakWithoutRelease(t *testing.T) { for i := 0; i < 10; i++ { ctx := newLeakTestContext(fmt.Sprintf("no-release-%d", i), "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) // Intentionally NOT calling ctx.Release() @@ -205,7 +205,7 @@ func TestGoroutineLeakWithoutRelease(t *testing.T) { for i := 0; i < 10; i++ { ctx := newLeakTestContext(fmt.Sprintf("with-release-%d", i), "tests.create") - _, _ = agent.Script.Create(ctx, []context.Message{ + _, _, _ = agent.Script.Create(ctx, []context.Message{ {Role: "user", Content: "Hello"}, }) ctx.Release() // WITH Release @@ -299,7 +299,6 @@ func newLeakTestContext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/assistant/hook/next.go b/agent/assistant/hook/next.go index 7cb1c51b..64c8a74f 100644 --- a/agent/assistant/hook/next.go +++ b/agent/assistant/hook/next.go @@ -9,7 +9,16 @@ import ( ) // Next next hook for the next action after the completion -func (s *Script) Next(ctx *context.Context, payload *context.NextHookPayload) (*context.NextHookResponse, error) { +// opts is optional - if provided, will be passed to the hook +func (s *Script) Next(ctx *context.Context, payload *context.NextHookPayload, opts ...*context.Options) (*context.NextHookResponse, *context.Options, error) { + // Get or create options + var options *context.Options + if len(opts) > 0 && opts[0] != nil { + options = opts[0] + } else { + options = &context.Options{} + } + // Convert payload to map for JS (use JSON tag names) payloadMap := map[string]interface{}{ "messages": payload.Messages, @@ -18,12 +27,19 @@ func (s *Script) Next(ctx *context.Context, payload *context.NextHookPayload) (* "error": payload.Error, } - res, err := s.Execute(ctx, "Next", payloadMap) + // Execute hook with ctx, payload, and options (convert options to map for JS) + optionsMap := options.ToMap() + res, err := s.Execute(ctx, "Next", payloadMap, optionsMap) if err != nil { - return nil, err + return nil, nil, err } - return s.getNextHookResponse(res) + response, err := s.getNextHookResponse(res) + if err != nil { + return nil, nil, err + } + + return response, options, nil } // getNextHookResponse convert the result to a NextHookResponse diff --git a/agent/assistant/hook/next_test.go b/agent/assistant/hook/next_test.go index c9f527ed..e3f69ed3 100644 --- a/agent/assistant/hook/next_test.go +++ b/agent/assistant/hook/next_test.go @@ -20,7 +20,6 @@ func newTestContextForNext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ @@ -86,7 +85,7 @@ func TestNext(t *testing.T) { Error: "", } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook with null return: %s", err.Error()) } @@ -106,7 +105,7 @@ func TestNext(t *testing.T) { }, } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook with undefined return: %s", err.Error()) } @@ -126,7 +125,7 @@ func TestNext(t *testing.T) { }, } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook with empty return: %s", err.Error()) } @@ -152,7 +151,7 @@ func TestNext(t *testing.T) { }, } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook with custom data: %s", err.Error()) } @@ -199,7 +198,7 @@ func TestNext(t *testing.T) { }, } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook: %s", err.Error()) } @@ -245,7 +244,7 @@ func TestNext(t *testing.T) { }, } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook with delegate: %s", err.Error()) } @@ -307,7 +306,7 @@ func TestNext(t *testing.T) { Error: "", } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook: %s", err.Error()) } @@ -366,7 +365,7 @@ func TestNext(t *testing.T) { }, } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook: %s", err.Error()) } @@ -410,7 +409,7 @@ func TestNext(t *testing.T) { Error: "Tool execution failed: timeout", } - res, err := agent.Script.Next(ctx, payload) + res, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Failed to execute Next hook: %s", err.Error()) } diff --git a/agent/assistant/hook/realworld_next_test.go b/agent/assistant/hook/realworld_next_test.go index 8b438435..85a9ab84 100644 --- a/agent/assistant/hook/realworld_next_test.go +++ b/agent/assistant/hook/realworld_next_test.go @@ -19,7 +19,6 @@ func newRealWorldNextContext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ @@ -76,7 +75,7 @@ func TestRealWorldNextStandard(t *testing.T) { Error: "", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } @@ -118,7 +117,7 @@ func TestRealWorldNextCustomData(t *testing.T) { Error: "", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } @@ -165,7 +164,7 @@ func TestRealWorldNextDelegate(t *testing.T) { Error: "", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } @@ -227,7 +226,7 @@ func TestRealWorldNextProcessTools(t *testing.T) { Error: "", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } @@ -280,7 +279,7 @@ func TestRealWorldNextErrorRecovery(t *testing.T) { Error: "System error: Database connection timeout", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } @@ -329,7 +328,7 @@ func TestRealWorldNextConditional(t *testing.T) { Error: "", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } @@ -362,7 +361,7 @@ func TestRealWorldNextConditional(t *testing.T) { Error: "", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } @@ -406,7 +405,7 @@ func TestRealWorldNextDefault(t *testing.T) { Error: "", } - response, err := agent.Script.Next(ctx, payload) + response, _, err := agent.Script.Next(ctx, payload) if err != nil { t.Fatalf("Next hook failed: %v", err) } diff --git a/agent/assistant/hook/realworld_stress_test.go b/agent/assistant/hook/realworld_stress_test.go index af4cbfb7..2690ed8a 100644 --- a/agent/assistant/hook/realworld_stress_test.go +++ b/agent/assistant/hook/realworld_stress_test.go @@ -42,7 +42,7 @@ func TestRealWorldSimpleScenario(t *testing.T) { {Role: "user", Content: "simple"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Create failed: %v", err) } @@ -73,7 +73,7 @@ func TestRealWorldMCPScenarios(t *testing.T) { {Role: "user", Content: "mcp_health"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Create failed: %v", err) } @@ -117,7 +117,7 @@ func TestRealWorldMCPScenarios(t *testing.T) { {Role: "user", Content: "mcp_tools"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Create failed: %v", err) } @@ -162,7 +162,7 @@ func TestRealWorldMCPScenarios(t *testing.T) { ctx := newRealWorldContext("test-full-workflow", "tests.realworld") // Initialize stack for trace - stack, _, done := context.EnterStack(ctx, "tests.realworld", context.RefererAPI) + stack, _, done := context.EnterStack(ctx, "tests.realworld", &context.Options{}) defer done() ctx.Stack = stack @@ -170,7 +170,7 @@ func TestRealWorldMCPScenarios(t *testing.T) { {Role: "user", Content: "full_workflow"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Create failed: %v", err) } @@ -237,7 +237,7 @@ func TestRealWorldTraceIntensive(t *testing.T) { } ctx := newRealWorldContext("test-trace-intensive", "tests.realworld") - stack, _, done := context.EnterStack(ctx, "tests.realworld", context.RefererAPI) + stack, _, done := context.EnterStack(ctx, "tests.realworld", &context.Options{}) defer done() ctx.Stack = stack @@ -245,7 +245,7 @@ func TestRealWorldTraceIntensive(t *testing.T) { {Role: "user", Content: "trace_intensive"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Create failed: %v", err) } @@ -279,7 +279,7 @@ func TestRealWorldStressSimple(t *testing.T) { {Role: "user", Content: "simple"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Iteration %d failed: %v", i, err) } @@ -338,14 +338,14 @@ func TestRealWorldStressMCP(t *testing.T) { ctx := newRealWorldContext(fmt.Sprintf("stress-mcp-%d", i), "tests.realworld") // Initialize stack for trace - stack, _, done := context.EnterStack(ctx, "tests.realworld", context.RefererAPI) + stack, _, done := context.EnterStack(ctx, "tests.realworld", &context.Options{}) ctx.Stack = stack messages := []context.Message{ {Role: "user", Content: scenario}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Iteration %d (%s) failed: %v", i, scenario, err) } @@ -427,14 +427,14 @@ func TestRealWorldStressFullWorkflow(t *testing.T) { ctx := newRealWorldContext(fmt.Sprintf("stress-workflow-%d", i), "tests.realworld") // Initialize stack for trace - stack, _, done := context.EnterStack(ctx, "tests.realworld", context.RefererAPI) + stack, _, done := context.EnterStack(ctx, "tests.realworld", &context.Options{}) ctx.Stack = stack messages := []context.Message{ {Role: "user", Content: "full_workflow"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Iteration %d failed: %v", i, err) } @@ -531,14 +531,14 @@ func TestRealWorldStressConcurrent(t *testing.T) { ) // Initialize stack for trace - stack, _, done := context.EnterStack(ctx, "tests.realworld", context.RefererAPI) + stack, _, done := context.EnterStack(ctx, "tests.realworld", &context.Options{}) ctx.Stack = stack messages := []context.Message{ {Role: "user", Content: scenario}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { errors <- fmt.Errorf("goroutine %d iteration %d (%s): %v", goroutineID, i, scenario, err) done() @@ -658,14 +658,14 @@ func TestRealWorldStressResourceHeavy(t *testing.T) { ctx := newRealWorldContext(fmt.Sprintf("stress-heavy-%d", i), "tests.realworld") // Initialize stack for trace - stack, _, done := context.EnterStack(ctx, "tests.realworld", context.RefererAPI) + stack, _, done := context.EnterStack(ctx, "tests.realworld", &context.Options{}) ctx.Stack = stack messages := []context.Message{ {Role: "user", Content: "resource_heavy"}, } - response, err := agent.Script.Create(ctx, messages) + response, _, err := agent.Script.Create(ctx, messages) if err != nil { t.Fatalf("Iteration %d failed: %v", i, err) } @@ -726,7 +726,6 @@ func newRealWorldContext(chatID, assistantID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "gpt-4o", Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/assistant/load_store_test.go b/agent/assistant/load_store_test.go index 3eaba542..291d75e2 100644 --- a/agent/assistant/load_store_test.go +++ b/agent/assistant/load_store_test.go @@ -180,7 +180,6 @@ func newStoreTestContext(chatID, assistantID string) *context.Context { Context: stdContext.Background(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: context.Client{ @@ -269,7 +268,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("test-chat-id", assistantID) messages := []context.Message{{Role: "user", Content: "Hello"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err, "Create hook should execute without error") require.NotNil(t, res, "Create hook should return a response") @@ -576,7 +575,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("test-chat-all-fields", assistantID) messages := []context.Message{{Role: "user", Content: "Test message"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err, "Create hook should execute without error") require.NotNil(t, res, "Create hook should return a response") @@ -691,7 +690,7 @@ function Create(ctx: CreateContext, messages: Message[]): CreateResponse | null {Role: "user", Content: "How are you?"}, } - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err, "TypeScript Create hook should execute without error") require.NotNil(t, res, "Create hook should return a response") @@ -759,7 +758,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("null-test-chat", assistantID) messages := []context.Message{{Role: "user", Content: "Hello"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err, "Hook returning null should not error") assert.Nil(t, res, "Hook returning null should return nil response") } @@ -832,7 +831,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("preset-test-1", assistantID) messages := []context.Message{{Role: "user", Content: "Be friendly please"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, res) assert.Equal(t, "friendly", res.PromptPreset) @@ -843,7 +842,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("preset-test-2", assistantID) messages := []context.Message{{Role: "user", Content: "Be professional"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, res) assert.Equal(t, "professional", res.PromptPreset) @@ -854,7 +853,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("preset-test-3", assistantID) messages := []context.Message{{Role: "user", Content: "Hello"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) assert.Nil(t, res) }) @@ -916,7 +915,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("disable-test-1", assistantID) messages := []context.Message{{Role: "user", Content: "disable_global prompts"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, res) require.NotNil(t, res.DisableGlobalPrompts) @@ -928,7 +927,7 @@ function Create(ctx: any, messages: any[]): any { ctx := newStoreTestContext("disable-test-2", assistantID) messages := []context.Message{{Role: "user", Content: "enable_global prompts"}} - res, err := loaded.Script.Create(ctx, messages) + res, _, err := loaded.Script.Create(ctx, messages, &context.Options{}) require.NoError(t, err) require.NotNil(t, res) require.NotNil(t, res.DisableGlobalPrompts) diff --git a/agent/assistant/next.go b/agent/assistant/next.go index 0baae34b..940d0739 100644 --- a/agent/assistant/next.go +++ b/agent/assistant/next.go @@ -55,7 +55,10 @@ func (ast *Assistant) handleDelegation( // 2. Execute with the same Context (preserving ID, Space, Writer, etc.) // 3. Call done() to pop from Stack when finished // This ensures proper Stack tracing: parent assistant -> delegated assistant - return targetAssistant.Stream(ctx, delegate.Messages, streamHandler) + + // Convert options map from delegate config to Options struct + delegateOpts := agentContext.OptionsFromMap(delegate.Options) + return targetAssistant.Stream(ctx, delegate.Messages, delegateOpts) } // buildStandardResponse builds the standard agent response when no custom Next hook processing is needed diff --git a/agent/content/image_test.go b/agent/content/image_test.go index b28b345c..fb0e7633 100644 --- a/agent/content/image_test.go +++ b/agent/content/image_test.go @@ -32,7 +32,6 @@ func newTestContext(capabilities *openai.Capabilities) *agentContext.Context { Space: plan.NewMemorySharedSpace(), ChatID: "test-chat", AssistantID: "test-assistant", - Connector: "openai", Locale: "en-us", Theme: "light", Client: agentContext.Client{ diff --git a/agent/content/tools.go b/agent/content/tools.go index 333a4092..c34330a0 100644 --- a/agent/content/tools.go +++ b/agent/content/tools.go @@ -13,7 +13,7 @@ import ( // AgentCaller interface for calling agents (to avoid circular dependency) type AgentCaller interface { - Stream(ctx *agentContext.Context, messages []agentContext.Message) (interface{}, error) + Stream(ctx *agentContext.Context, messages []agentContext.Message, options ...*agentContext.Options) (interface{}, error) } // AgentGetterFunc is a function type that gets an agent by ID @@ -35,12 +35,10 @@ func CallAgent(ctx *agentContext.Context, agentID string, message agentContext.M // Call the agent with the message messages := []agentContext.Message{message} - connectorBackup := ctx.Connector - ctx.Connector = "" - defer func() { - ctx.Connector = connectorBackup - }() - response, err := agent.Stream(ctx, messages) + // Note: Connector is now in Options (call-level parameter), not Context + // For A2A calls, we use an empty Connector to let the agent use its default + opts := &agentContext.Options{Skip: &agentContext.Skip{History: true}, Writer: nil} // Skip history and output to the caller + response, err := agent.Stream(ctx, messages, opts) if err != nil { return "", fmt.Errorf("failed to call agent %s: %w", agentID, err) } diff --git a/agent/context/context.go b/agent/context/context.go index 3af8f4da..0ed8fd32 100644 --- a/agent/context/context.go +++ b/agent/context/context.go @@ -259,23 +259,6 @@ func (ctx *Context) Map() map[string]interface{} { if ctx.AssistantID != "" { data["assistant_id"] = ctx.AssistantID } - if ctx.Connector != "" { - data["connector"] = ctx.Connector - } - if ctx.Search != nil { - data["search"] = *ctx.Search - } - - // Arguments for call - if len(ctx.Args) > 0 { - data["args"] = ctx.Args - } - if ctx.Retry { - data["retry"] = ctx.Retry - } - if ctx.RetryTimes > 0 { - data["retry_times"] = ctx.RetryTimes - } // Locale information if ctx.Locale != "" { diff --git a/agent/context/context_test.go b/agent/context/context_test.go index 0fd82779..f4c48aa3 100644 --- a/agent/context/context_test.go +++ b/agent/context/context_test.go @@ -179,7 +179,7 @@ func TestGetCompletionRequest(t *testing.T) { c.Request = req // Call GetCompletionRequest - completionReq, ctx, err := GetCompletionRequest(c, cache) + completionReq, ctx, opts, err := GetCompletionRequest(c, cache) if tt.expectError { assert.Error(t, err) @@ -189,6 +189,7 @@ func TestGetCompletionRequest(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, completionReq) assert.NotNil(t, ctx) + assert.NotNil(t, opts) // Verify CompletionRequest assert.Equal(t, tt.expectedModel, completionReq.Model) diff --git a/agent/context/interrupt_test.go b/agent/context/interrupt_test.go index e4ac48ed..89bebddb 100644 --- a/agent/context/interrupt_test.go +++ b/agent/context/interrupt_test.go @@ -19,7 +19,6 @@ func newTestContextWithInterrupt(chatID, assistantID string) *Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: assistantID, - Connector: "", Locale: "en-us", Theme: "light", Client: Client{ diff --git a/agent/context/jsapi.go b/agent/context/jsapi.go index c8161175..93610438 100644 --- a/agent/context/jsapi.go +++ b/agent/context/jsapi.go @@ -36,13 +36,6 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) { // Set primitive fields in template jsObject.Set("chat_id", ctx.ChatID) jsObject.Set("assistant_id", ctx.AssistantID) - jsObject.Set("connector", ctx.Connector) - if ctx.Search != nil { - jsObject.Set("search", *ctx.Search) - } - - jsObject.Set("retry", ctx.Retry) - jsObject.Set("retry_times", uint32(ctx.RetryTimes)) jsObject.Set("locale", ctx.Locale) jsObject.Set("theme", ctx.Theme) jsObject.Set("referer", ctx.Referer) @@ -97,15 +90,6 @@ func (ctx *Context) NewObject(v8ctx *v8go.Context) (*v8go.Value, error) { } // Set complex objects (maps, arrays) after instance creation using bridge - // Args array - if ctx.Args != nil { - argsVal, err := bridge.JsValue(v8ctx, ctx.Args) - if err == nil { - obj.Set("args", argsVal) - argsVal.Release() // Release Go-side Persistent handle, V8 internal reference remains - } - } - // Client object clientData := map[string]interface{}{ "type": ctx.Client.Type, diff --git a/agent/context/jsapi_mcp_test.go b/agent/context/jsapi_mcp_test.go index 9e306d6f..5f4b7363 100644 --- a/agent/context/jsapi_mcp_test.go +++ b/agent/context/jsapi_mcp_test.go @@ -22,8 +22,9 @@ func TestMCPListResources(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -66,8 +67,9 @@ func TestMCPReadResource(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -108,8 +110,9 @@ func TestMCPListTools(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -154,8 +157,9 @@ func TestMCPCallTool(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -196,8 +200,9 @@ func TestMCPCallTools(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -243,8 +248,9 @@ func TestMCPCallToolsParallel(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -290,8 +296,9 @@ func TestMCPListPrompts(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -334,8 +341,9 @@ func TestMCPGetPrompt(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -376,8 +384,9 @@ func TestMCPListSamples(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -418,8 +427,9 @@ func TestMCPGetSample(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -462,8 +472,9 @@ func TestMCPJsApiWithTrace(t *testing.T) { AssistantID: "test-assistant-id", Locale: "en", Context: stdContext.Background(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` diff --git a/agent/context/jsapi_release_test.go b/agent/context/jsapi_release_test.go index 5b0d3d14..74920f14 100644 --- a/agent/context/jsapi_release_test.go +++ b/agent/context/jsapi_release_test.go @@ -22,10 +22,11 @@ func TestContextRelease(t *testing.T) { AssistantID: "test-assistant-id", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } // Initialize stack and trace - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -75,10 +76,11 @@ func TestTraceRelease(t *testing.T) { AssistantID: "test-assistant-id", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } // Initialize stack and trace - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -137,10 +139,11 @@ func TestContextReleaseWithTrace(t *testing.T) { AssistantID: "test-assistant-id", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } // Initialize stack and trace - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -185,10 +188,11 @@ func TestTryFinallyPattern(t *testing.T) { AssistantID: "test-assistant-id", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } // Initialize stack and trace - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack res, err := v8.Call(v8.CallOptions{}, ` @@ -287,10 +291,11 @@ func TestTryFinallyPatternWithError(t *testing.T) { AssistantID: "test-assistant-id", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } // Initialize stack and trace - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack _, err := v8.Call(v8.CallOptions{}, ` diff --git a/agent/context/jsapi_stress_test.go b/agent/context/jsapi_stress_test.go index c41fc10d..b970f878 100644 --- a/agent/context/jsapi_stress_test.go +++ b/agent/context/jsapi_stress_test.go @@ -38,7 +38,8 @@ func TestStressContextCreationAndRelease(t *testing.T) { } // Initialize stack and trace - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + cxt.Referer = context.RefererAPI + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack _, err := v8.Call(v8.CallOptions{}, ` @@ -111,10 +112,11 @@ func TestStressTraceOperations(t *testing.T) { AssistantID: "test-assistant", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } // Initialize stack and trace - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack _, err := v8.Call(v8.CallOptions{}, fmt.Sprintf(` function test(ctx) { @@ -188,9 +190,10 @@ func TestStressMCPOperations(t *testing.T) { AssistantID: "test-assistant", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack startMemory := getMemStats() @@ -271,9 +274,10 @@ func TestStressConcurrentContexts(t *testing.T) { AssistantID: "test-assistant", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack _, err := v8.Call(v8.CallOptions{}, ` @@ -421,9 +425,10 @@ func TestStressReleasePatterns(t *testing.T) { AssistantID: "test-assistant", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack _, err := v8.Call(v8.CallOptions{}, ` @@ -459,9 +464,10 @@ func TestStressReleasePatterns(t *testing.T) { AssistantID: "test-assistant", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack _, err := v8.Call(v8.CallOptions{}, ` @@ -499,9 +505,10 @@ func TestStressReleasePatterns(t *testing.T) { AssistantID: "test-assistant", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack _, err := v8.Call(v8.CallOptions{}, ` @@ -544,9 +551,10 @@ func TestStressLongRunningTrace(t *testing.T) { AssistantID: "test-assistant", Context: stdContext.Background(), IDGenerator: message.NewIDGenerator(), + Referer: context.RefererAPI, } - stack, _, _ := context.EnterStack(cxt, "test-assistant", context.RefererAPI) + stack, _, _ := context.EnterStack(cxt, "test-assistant", &context.Options{}) cxt.Stack = stack startMemory := getMemStats() diff --git a/agent/context/jsapi_test.go b/agent/context/jsapi_test.go index 61708dcb..2c430d9b 100644 --- a/agent/context/jsapi_test.go +++ b/agent/context/jsapi_test.go @@ -219,15 +219,9 @@ func TestJsValueAllFields(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() - searchTrue := true cxt := &context.Context{ ChatID: "test-chat-id", AssistantID: "test-assistant-id", - Connector: "test-connector", - Search: &searchTrue, - Args: []interface{}{"arg1", "arg2", 123}, - Retry: true, - RetryTimes: 3, Locale: "zh-cn", Theme: "dark", Context: stdContext.Background(), @@ -279,21 +273,12 @@ func TestJsValueAllFields(t *testing.T) { // Verify all fields assert.Equal(t, "test-chat-id", result["chat_id"], "chat_id mismatch") assert.Equal(t, "test-assistant-id", result["assistant_id"], "assistant_id mismatch") - assert.Equal(t, "test-connector", result["connector"], "connector mismatch") - assert.Equal(t, true, result["search"], "search mismatch") - assert.Equal(t, true, result["retry"], "retry mismatch") - assert.Equal(t, float64(3), result["retry_times"], "retry_times mismatch") assert.Equal(t, "zh-cn", result["locale"], "locale mismatch") assert.Equal(t, "dark", result["theme"], "theme mismatch") assert.Equal(t, "api", result["referer"], "referer mismatch") assert.Equal(t, "cui-web", result["accept"], "accept mismatch") assert.Equal(t, "/dashboard/home", result["route"], "route mismatch") - // Verify args array - args, ok := result["args"].([]interface{}) - assert.True(t, ok, "args should be an array") - assert.Equal(t, 3, len(args), "args length mismatch") - // Verify client object client, ok := result["client"].(map[string]interface{}) assert.True(t, ok, "client should be an object") @@ -373,21 +358,6 @@ func testAllFieldsFunction(info *v8go.FunctionCallbackInfo) *v8go.Value { if val, ok := getField("assistant_id"); ok { result["assistant_id"] = val } - if val, ok := getField("connector"); ok { - result["connector"] = val - } - if val, ok := getField("search"); ok { - result["search"] = val - } - if val, ok := getField("args"); ok { - result["args"] = val - } - if val, ok := getField("retry"); ok { - result["retry"] = val - } - if val, ok := getField("retry_times"); ok { - result["retry_times"] = val - } if val, ok := getField("locale"); ok { result["locale"] = val } diff --git a/agent/context/mcp_test.go b/agent/context/mcp_test.go index 16479ba6..0604d8df 100644 --- a/agent/context/mcp_test.go +++ b/agent/context/mcp_test.go @@ -20,10 +20,11 @@ func newTestMCPContext() *context.Context { ChatID: "test-chat", AssistantID: "test-assistant", Locale: "en", + Referer: context.RefererAPI, } // Initialize stack and trace - stack, traceID, _ := context.EnterStack(ctx, "test-assistant", context.RefererAPI) + stack, traceID, _ := context.EnterStack(ctx, "test-assistant", &context.Options{}) ctx.Stack = stack _ = traceID // traceID is set in stack diff --git a/agent/context/openapi.go b/agent/context/openapi.go index db656d5e..8f5f61fd 100644 --- a/agent/context/openapi.go +++ b/agent/context/openapi.go @@ -15,21 +15,21 @@ import ( ) // GetCompletionRequest parse completion request and create context from openapi request -// Returns: *CompletionRequest, *Context, error -func GetCompletionRequest(c *gin.Context, cache store.Store) (*CompletionRequest, *Context, error) { +// Returns: *CompletionRequest, *Context, *Options, error +func GetCompletionRequest(c *gin.Context, cache store.Store) (*CompletionRequest, *Context, *Options, error) { // Get authorized information authInfo := authorized.GetInfo(c) // Parse completion request from payload or query first completionReq, err := parseCompletionRequestData(c) if err != nil { - return nil, nil, fmt.Errorf("failed to parse completion request: %w", err) + return nil, nil, nil, fmt.Errorf("failed to parse completion request: %w", err) } // Extract assistant ID using completionReq (can extract from model field) assistantID, err := GetAssistantID(c, completionReq) if err != nil { - return nil, nil, fmt.Errorf("failed to get assistant ID: %w", err) + return nil, nil, nil, fmt.Errorf("failed to get assistant ID: %w", err) } // Extract chat ID (may generate from messages if not provided) @@ -47,26 +47,10 @@ func GetCompletionRequest(c *gin.Context, cache store.Store) (*CompletionRequest // Create context with unique ID using New() to ensure proper initialization ctx := New(c.Request.Context(), authInfo, chatID) - // Set additional fields + // Set context fields (session-level state) ctx.Cache = cache ctx.Writer = c.Writer ctx.AssistantID = assistantID - - // Try to extract custom connector from model field - // If model is a valid connector ID, set it to ctx.Connector - // Otherwise, keep the standard OpenAI-compatible behavior (model as assistant ID) - if completionReq != nil && completionReq.Model != "" { - // Check if model is a valid connector (not containing "-yao_" which indicates assistant ID format) - if !strings.Contains(completionReq.Model, "-yao_") { - // Try to validate if it's a real connector - if _, err := connector.Select(completionReq.Model); err == nil { - // It's a valid connector, use it - ctx.Connector = completionReq.Model - } - // If not a valid connector, ignore it (keep ctx.Connector empty to use assistant's default) - } - } - ctx.Locale = GetLocale(c, completionReq) ctx.Theme = GetTheme(c, completionReq) ctx.Referer = GetReferer(c, completionReq) @@ -78,14 +62,34 @@ func GetCompletionRequest(c *gin.Context, cache store.Store) (*CompletionRequest } ctx.Route = GetRoute(c, completionReq) ctx.Metadata = GetMetadata(c, completionReq) - ctx.Skip = GetSkip(c, completionReq) + + // Create Options (call-level parameters) + opts := &Options{ + Context: c.Request.Context(), + Skip: GetSkip(c, completionReq), + } + + // Try to extract custom connector from model field + // If model is a valid connector ID, set it to opts.Connector + // Otherwise, keep the standard OpenAI-compatible behavior (model as assistant ID) + if completionReq != nil && completionReq.Model != "" { + // Check if model is a valid connector (not containing "-yao_" which indicates assistant ID format) + if !strings.Contains(completionReq.Model, "-yao_") { + // Try to validate if it's a real connector + if _, err := connector.Select(completionReq.Model); err == nil { + // It's a valid connector, use it + opts.Connector = completionReq.Model + } + // If not a valid connector, ignore it (keep opts.Connector empty to use assistant's default) + } + } // Initialize interrupt controller ctx.Interrupt = NewInterruptController() // Register context to global registry first (required for interrupt handler callback) if err := Register(ctx); err != nil { - return nil, nil, fmt.Errorf("failed to register context: %w", err) + return nil, nil, nil, fmt.Errorf("failed to register context: %w", err) } // Start interrupt listener after registration @@ -93,7 +97,7 @@ func GetCompletionRequest(c *gin.Context, cache store.Store) (*CompletionRequest // HTTP context cancellation is handled by LLM/Agent layers naturally ctx.Interrupt.Start(ctx.ID) - return completionReq, ctx, nil + return completionReq, ctx, opts, nil } // getClientType parses the client type from User-Agent header diff --git a/agent/context/openapi_test.go b/agent/context/openapi_test.go index 88a50474..d7829b22 100644 --- a/agent/context/openapi_test.go +++ b/agent/context/openapi_test.go @@ -851,7 +851,7 @@ func TestGetCompletionRequest_WriterInitialized(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = req - completionReq, ctx, err := GetCompletionRequest(c, cache) + completionReq, ctx, opts, err := GetCompletionRequest(c, cache) if err != nil { t.Fatalf("Failed to get completion request: %v", err) } @@ -867,6 +867,11 @@ func TestGetCompletionRequest_WriterInitialized(t *testing.T) { t.Error("Expected ctx.Writer to be the same as gin context writer") } + // Check that Options is initialized + if opts == nil { + t.Error("Expected opts to be initialized, got nil") + } + // Check other fields if completionReq.Model != "gpt-4-yao_test" { t.Errorf("Expected model 'gpt-4-yao_test', got '%s'", completionReq.Model) @@ -914,12 +919,17 @@ func TestGetCompletionRequest_ChatIDFallback(t *testing.T) { c, _ := gin.CreateTestContext(w) c.Request = req - _, ctx, err := GetCompletionRequest(c, cache) + _, ctx, opts, err := GetCompletionRequest(c, cache) if err != nil { t.Fatalf("Failed to get completion request: %v", err) } defer ctx.Release() + // Check that Options is initialized + if opts == nil { + t.Error("Expected opts to be initialized, got nil") + } + // ChatID should be generated (not empty) if ctx.ChatID == "" { t.Error("Expected ChatID to be generated via fallback, got empty string") diff --git a/agent/context/options.go b/agent/context/options.go new file mode 100644 index 00000000..9b9871ac --- /dev/null +++ b/agent/context/options.go @@ -0,0 +1,71 @@ +package context + +// ToMap converts Options struct to map for JSON serialization +func (opts *Options) ToMap() map[string]interface{} { + if opts == nil { + return nil + } + + result := make(map[string]interface{}) + + // Add configurable fields (with json tags) + if opts.Connector != "" { + result["connector"] = opts.Connector + } + if opts.Mode != "" { + result["mode"] = opts.Mode + } + if opts.Search != nil { + result["search"] = *opts.Search + } + if opts.Skip != nil { + result["skip"] = opts.Skip + } + // Only add DisableGlobalPrompts if true (avoid false values in map) + if opts.DisableGlobalPrompts { + result["disable_global_prompts"] = opts.DisableGlobalPrompts + } + + // Note: Runtime fields (Context, Writer) are not serialized (json:"-") + // They should not be included in the map + + return result +} + +// OptionsFromMap creates Options struct from map (e.g., from JS Hook) +func OptionsFromMap(m map[string]interface{}) *Options { + if m == nil { + return &Options{} + } + + opts := &Options{} + + // Extract configurable fields + if connector, ok := m["connector"].(string); ok { + opts.Connector = connector + } + if mode, ok := m["mode"].(string); ok { + opts.Mode = mode + } + if search, ok := m["search"].(bool); ok { + opts.Search = &search + } + if skipMap, ok := m["skip"].(map[string]interface{}); ok { + skip := &Skip{} + if history, ok := skipMap["history"].(bool); ok { + skip.History = history + } + if trace, ok := skipMap["trace"].(bool); ok { + skip.Trace = trace + } + opts.Skip = skip + } + if disableGlobalPrompts, ok := m["disable_global_prompts"].(bool); ok { + opts.DisableGlobalPrompts = disableGlobalPrompts + } + + // Note: Context and Writer are runtime fields, not restored from map + // They should be set by the caller if needed + + return opts +} diff --git a/agent/context/stack.go b/agent/context/stack.go index b0502497..de816768 100644 --- a/agent/context/stack.go +++ b/agent/context/stack.go @@ -9,7 +9,7 @@ import ( ) // NewStack creates a new root stack with the given trace ID and assistant ID -func NewStack(traceID, assistantID, referer string) *Stack { +func NewStack(traceID, assistantID, referer string, opts *Options) *Stack { if traceID == "" { traceID = uuid.New().String() } @@ -25,13 +25,14 @@ func NewStack(traceID, assistantID, referer string) *Stack { Depth: 0, ParentID: "", Path: []string{stackID}, + Options: opts, CreatedAt: now, Status: StackStatusRunning, } } // NewChildStack creates a child stack from the current stack -func (s *Stack) NewChildStack(assistantID, referer string) *Stack { +func (s *Stack) NewChildStack(assistantID, referer string, opts *Options) *Stack { stackID := uuid.New().String() now := time.Now().UnixMilli() @@ -48,6 +49,7 @@ func (s *Stack) NewChildStack(assistantID, referer string) *Stack { Depth: s.Depth + 1, ParentID: s.ID, Path: path, + Options: opts, CreatedAt: now, Status: StackStatusRunning, } @@ -134,6 +136,7 @@ func (s *Stack) Clone() *Stack { Depth: s.Depth, ParentID: s.ParentID, Path: make([]string, len(s.Path)), + Options: s.Options, // Shallow copy of Options pointer CreatedAt: s.CreatedAt, Status: s.Status, Error: s.Error, @@ -165,14 +168,17 @@ func (s *Stack) Clone() *Stack { // // Usage: // -// stack, traceID, done := context.EnterStack(ctx, assistantID, referer) +// stack, traceID, done := context.EnterStack(ctx, assistantID, opts) // defer done() // // ... your code here ... -func EnterStack(ctx *Context, assistantID, referer string) (*Stack, string, func()) { +func EnterStack(ctx *Context, assistantID string, opts *Options) (*Stack, string, func()) { var stack *Stack var parentStack *Stack var traceID string + // Get referer from ctx (request source) + referer := ctx.Referer + // Initialize Stacks map if not exists if ctx.Stacks == nil { ctx.Stacks = make(map[string]*Stack) @@ -182,14 +188,14 @@ func EnterStack(ctx *Context, assistantID, referer string) (*Stack, string, func // Create root stack for this assistant call (entry point) // Generate a new trace ID for root traceID = trace.GenTraceID() - stack = NewStack(traceID, assistantID, referer) + stack = NewStack(traceID, assistantID, referer, opts) ctx.Stack = stack } else { // Create child stack for nested agent call // Inherit trace ID from parent parentStack = ctx.Stack traceID = parentStack.TraceID - stack = ctx.Stack.NewChildStack(assistantID, referer) + stack = ctx.Stack.NewChildStack(assistantID, referer, opts) ctx.Stack = stack } diff --git a/agent/context/stack_test.go b/agent/context/stack_test.go index 067f1c51..b678332c 100644 --- a/agent/context/stack_test.go +++ b/agent/context/stack_test.go @@ -16,8 +16,9 @@ func TestNewStack(t *testing.T) { traceID := "12345678" assistantID := "test-assistant" referer := RefererAPI + opts := &Options{} - stack := NewStack(traceID, assistantID, referer) + stack := NewStack(traceID, assistantID, referer, opts) if stack == nil { t.Fatal("Expected stack to be created, got nil") @@ -57,7 +58,7 @@ func TestNewStack_GenerateTraceID(t *testing.T) { defer test.Clean() // Empty traceID should generate a UUID - stack := NewStack("", "test-assistant", RefererAPI) + stack := NewStack("", "test-assistant", RefererAPI, &Options{}) if stack.TraceID == "" { t.Error("Expected TraceID to be generated, got empty string") @@ -74,10 +75,10 @@ func TestNewChildStack(t *testing.T) { defer test.Clean() // Create parent stack - parentStack := NewStack("12345678", "parent-assistant", RefererAPI) + parentStack := NewStack("12345678", "parent-assistant", RefererAPI, &Options{}) // Create child stack - childStack := parentStack.NewChildStack("child-assistant", RefererAgent) + childStack := parentStack.NewChildStack("child-assistant", RefererAgent, &Options{}) if childStack == nil { t.Fatal("Expected child stack to be created, got nil") @@ -121,7 +122,7 @@ func TestStackComplete(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() - stack := NewStack("12345678", "test-assistant", RefererAPI) + stack := NewStack("12345678", "test-assistant", RefererAPI, &Options{}) // Wait a bit to have measurable duration time.Sleep(10 * time.Millisecond) @@ -157,7 +158,7 @@ func TestStackFail(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() - stack := NewStack("12345678", "test-assistant", RefererAPI) + stack := NewStack("12345678", "test-assistant", RefererAPI, &Options{}) testError := "test error message" stack.Fail(nil) @@ -180,7 +181,7 @@ func TestStackTimeout(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() - stack := NewStack("12345678", "test-assistant", RefererAPI) + stack := NewStack("12345678", "test-assistant", RefererAPI, &Options{}) stack.Timeout() @@ -199,9 +200,10 @@ func TestEnterStack_RootCreation(t *testing.T) { ctx := &Context{ IDGenerator: message.NewIDGenerator(), + Referer: RefererAPI, } - stack, traceID, done := EnterStack(ctx, "test-assistant", RefererAPI) + stack, traceID, done := EnterStack(ctx, "test-assistant", &Options{}) defer done() if stack == nil { @@ -244,10 +246,11 @@ func TestEnterStack_ChildCreation(t *testing.T) { ctx := &Context{ IDGenerator: message.NewIDGenerator(), + Referer: RefererAPI, } // Create parent - parentStack, parentTraceID, parentDone := EnterStack(ctx, "parent-assistant", RefererAPI) + parentStack, parentTraceID, parentDone := EnterStack(ctx, "parent-assistant", &Options{}) defer parentDone() if parentStack == nil { @@ -255,7 +258,7 @@ func TestEnterStack_ChildCreation(t *testing.T) { } // Create child - childStack, childTraceID, childDone := EnterStack(ctx, "child-assistant", RefererAgent) + childStack, childTraceID, childDone := EnterStack(ctx, "child-assistant", &Options{}) defer childDone() if childStack == nil { @@ -289,13 +292,14 @@ func TestEnterStack_DoneCallback(t *testing.T) { ctx := &Context{ IDGenerator: message.NewIDGenerator(), + Referer: RefererAPI, } // Create parent - parentStack, _, parentDone := EnterStack(ctx, "parent-assistant", RefererAPI) + parentStack, _, parentDone := EnterStack(ctx, "parent-assistant", &Options{}) // Create child - childStack, _, childDone := EnterStack(ctx, "child-assistant", RefererAgent) + childStack, _, childDone := EnterStack(ctx, "child-assistant", &Options{}) // Child should be current if ctx.Stack != childStack { @@ -330,16 +334,17 @@ func TestContextGetAllStacks(t *testing.T) { ctx := &Context{ IDGenerator: message.NewIDGenerator(), + Referer: RefererAPI, } // Create multiple stacks - _, _, done1 := EnterStack(ctx, "assistant1", RefererAPI) + _, _, done1 := EnterStack(ctx, "assistant1", &Options{}) defer done1() - _, _, done2 := EnterStack(ctx, "assistant2", RefererAgent) + _, _, done2 := EnterStack(ctx, "assistant2", &Options{}) defer done2() - _, _, done3 := EnterStack(ctx, "assistant3", RefererAgent) + _, _, done3 := EnterStack(ctx, "assistant3", &Options{}) defer done3() // Get all stacks @@ -356,9 +361,10 @@ func TestContextGetStackByID(t *testing.T) { ctx := &Context{ IDGenerator: message.NewIDGenerator(), + Referer: RefererAPI, } - stack, _, done := EnterStack(ctx, "test-assistant", RefererAPI) + stack, _, done := EnterStack(ctx, "test-assistant", &Options{}) defer done() // Get stack by ID @@ -385,13 +391,14 @@ func TestContextGetStacksByTraceID(t *testing.T) { ctx := &Context{ IDGenerator: message.NewIDGenerator(), + Referer: RefererAPI, } // Create parent and child (same trace ID) - _, traceID, done1 := EnterStack(ctx, "parent-assistant", RefererAPI) + _, traceID, done1 := EnterStack(ctx, "parent-assistant", &Options{}) defer done1() - _, _, done2 := EnterStack(ctx, "child-assistant", RefererAgent) + _, _, done2 := EnterStack(ctx, "child-assistant", &Options{}) defer done2() // Get stacks by trace ID @@ -415,14 +422,15 @@ func TestContextGetRootStack(t *testing.T) { ctx := &Context{ IDGenerator: message.NewIDGenerator(), + Referer: RefererAPI, } // Create parent - parentStack, _, done1 := EnterStack(ctx, "parent-assistant", RefererAPI) + parentStack, _, done1 := EnterStack(ctx, "parent-assistant", &Options{}) defer done1() // Create child - _, _, done2 := EnterStack(ctx, "child-assistant", RefererAgent) + _, _, done2 := EnterStack(ctx, "child-assistant", &Options{}) defer done2() // Get root stack @@ -445,7 +453,7 @@ func TestStackClone(t *testing.T) { test.Prepare(t, config.Conf) defer test.Clean() - original := NewStack("12345678", "test-assistant", RefererAPI) + original := NewStack("12345678", "test-assistant", RefererAPI, &Options{}) original.Complete() clone := original.Clone() diff --git a/agent/context/types.go b/agent/context/types.go index a2d5fce9..bea8390f 100644 --- a/agent/context/types.go +++ b/agent/context/types.go @@ -235,9 +235,6 @@ type Context struct { output *output.Output `json:"-"` // Output, it will be used to write response data to the client messageMetadata *messageMetadataStore `json:"-"` // Thread-safe message metadata store for delta operations - // Skip configuration (history, trace, etc.), nil means don't skip anything - Skip *Skip `json:"skip,omitempty"` // Skip configuration (history, trace, etc.), nil means don't skip anything - // Model capabilities (set by assistant, used by output adapters) Capabilities *openai.Capabilities `json:"-"` // Model capabilities for the current connector @@ -248,13 +245,6 @@ type Context struct { Authorized *types.AuthorizedInfo `json:"authorized,omitempty"` // Authorized information ChatID string `json:"chat_id,omitempty"` // Chat ID, use to select chat AssistantID string `json:"assistant_id,omitempty"` // Assistant ID, use to select assistant - Connector string `json:"connector,omitempty"` // Connector, use to select the connector of the LLM Model, Default is Assistant.Connector - Search *bool `json:"search,omitempty"` // Search mode, default is true - - // Arguments for call - Args []interface{} `json:"args,omitempty"` // Arguments for call, it will be used to pass data to the call - Retry bool `json:"retry,omitempty"` // Retry mode - RetryTimes uint8 `json:"retry_times,omitempty"` // Retry times // Locale information Locale string `json:"locale,omitempty"` // Locale @@ -270,6 +260,31 @@ type Context struct { Metadata map[string]interface{} `json:"metadata,omitempty"` // The metadata of the request, it will be used to pass data to the page } +// Options represents the options for the context +type Options struct { + + // Original context, override the default context + Context context.Context `json:"-"` // Context, it will be used to pass the context to the call + + // Writer, use to write response data to the client (override the default writer) + Writer Writer `json:"writer,omitempty"` // Writer, use to write response data to the client + + // Skip configuration (history, trace, etc.), nil means don't skip anything + Skip *Skip `json:"skip,omitempty"` // Skip configuration (history, trace, etc.), nil means don't skip anything + + // Connector, use to select the connector of the LLM Model, Default is Assistant.Connector + Connector string `json:"connector,omitempty"` // Connector, use to select the connector of the LLM Model, Default is Assistant.Connector + + // Disable global prompts, default is false + DisableGlobalPrompts bool `json:"disable_global_prompts,omitempty"` // Temporarily disable global prompts for this request + + // Search mode, default is true + Search *bool `json:"search,omitempty"` // Search mode, default is true + + // Agent mode, use to select the mode of the request, default is "chat" + Mode string `json:"mode,omitempty"` // Agent mode, use to select the mode of the request, default is "chat" +} + // Stack represents the call stack node for tracing agent-to-agent calls // Uses a flat structure to avoid circular references and memory overhead type Stack struct { @@ -277,6 +292,9 @@ type Stack struct { ID string `json:"id"` // Unique stack node ID, used to identify this specific call TraceID string `json:"trace_id"` // Shared trace ID for entire call tree, inherited from root + // Options + Options *Options `json:"options,omitempty"` // Options for the call + // Call context AssistantID string `json:"assistant_id"` // Assistant handling this call Referer string `json:"referer,omitempty"` // Call source: api, agent, tool, process, etc. @@ -331,12 +349,11 @@ type HookCreateResponse struct { DisableGlobalPrompts *bool `json:"disable_global_prompts,omitempty"` // Temporarily disable global prompts for this request // Context adjustments - allow hook to modify context fields - AssistantID string `json:"assistant_id,omitempty"` // Override assistant ID - Connector string `json:"connector,omitempty"` // Override connector - Locale string `json:"locale,omitempty"` // Override locale - Theme string `json:"theme,omitempty"` // Override theme - Route string `json:"route,omitempty"` // Override route - Metadata map[string]interface{} `json:"metadata,omitempty"` // Override or merge metadata + Connector string `json:"connector,omitempty"` // Override connector (call-level) + Locale string `json:"locale,omitempty"` // Override locale (session-level) + Theme string `json:"theme,omitempty"` // Override theme (session-level) + Route string `json:"route,omitempty"` // Override route (session-level) + Metadata map[string]interface{} `json:"metadata,omitempty"` // Override or merge metadata (session-level) } // NextHookPayload payload for the next hook @@ -372,9 +389,9 @@ type NextHookResponse struct { // DelegateConfig configuration for delegating to another agent type DelegateConfig struct { - AgentID string `json:"agent_id"` // Required: target agent ID - Messages []Message `json:"messages"` // Messages to send to target agent - + AgentID string `json:"agent_id"` // Required: target agent ID + Messages []Message `json:"messages"` // Messages to send to target agent + Options map[string]interface{} `json:"options,omitempty"` // Optional: call-level options for delegation } // NextAction defines the action determined by Next hook response diff --git a/agent/llm/providers/openai/claude_test.go b/agent/llm/providers/openai/claude_test.go index d0b1e2bd..15fc39c1 100644 --- a/agent/llm/providers/openai/claude_test.go +++ b/agent/llm/providers/openai/claude_test.go @@ -22,7 +22,6 @@ func newClaudeTestContext(chatID, connectorID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: "test-assistant", - Connector: connectorID, Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/llm/providers/openai/deepseek_r1_test.go b/agent/llm/providers/openai/deepseek_r1_test.go index 839fe642..bf790e3a 100644 --- a/agent/llm/providers/openai/deepseek_r1_test.go +++ b/agent/llm/providers/openai/deepseek_r1_test.go @@ -401,7 +401,6 @@ func newDeepSeekTestContext(chatID, connectorID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: "test-assistant", - Connector: connectorID, Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/llm/providers/openai/deepseek_v3_test.go b/agent/llm/providers/openai/deepseek_v3_test.go index d593d473..3f944735 100644 --- a/agent/llm/providers/openai/deepseek_v3_test.go +++ b/agent/llm/providers/openai/deepseek_v3_test.go @@ -373,7 +373,6 @@ func newDeepSeekV3TestContext(chatID, connectorID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: "test-assistant", - Connector: connectorID, Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/llm/providers/openai/gpt5_test.go b/agent/llm/providers/openai/gpt5_test.go index b71b0a8c..a04ea943 100644 --- a/agent/llm/providers/openai/gpt5_test.go +++ b/agent/llm/providers/openai/gpt5_test.go @@ -388,7 +388,6 @@ func newGPT5TestContext(chatID, connectorID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: "test-assistant", - Connector: connectorID, Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/llm/providers/openai/openai_test.go b/agent/llm/providers/openai/openai_test.go index b4195137..111e7e3e 100644 --- a/agent/llm/providers/openai/openai_test.go +++ b/agent/llm/providers/openai/openai_test.go @@ -1508,7 +1508,6 @@ func newTestContext(chatID, connectorID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: "test-assistant", - Connector: connectorID, Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/agent/llm/providers/openai/temperature_test.go b/agent/llm/providers/openai/temperature_test.go index d648a74f..b0a8bd98 100644 --- a/agent/llm/providers/openai/temperature_test.go +++ b/agent/llm/providers/openai/temperature_test.go @@ -340,7 +340,6 @@ func newTemperatureTestContext(chatID, connectorID string) *context.Context { Space: plan.NewMemorySharedSpace(), ChatID: chatID, AssistantID: "test-assistant", - Connector: connectorID, Locale: "en-us", Theme: "light", Client: context.Client{ diff --git a/openapi/chat/completions.go b/openapi/chat/completions.go index 68922afa..1262b39d 100644 --- a/openapi/chat/completions.go +++ b/openapi/chat/completions.go @@ -25,7 +25,7 @@ func GinCreateCompletions(c *gin.Context) { return } - completionReq, ctx, err := context.GetCompletionRequest(c, cache) + completionReq, ctx, opts, err := context.GetCompletionRequest(c, cache) if err != nil { fmt.Println("-----------------------------------------------") fmt.Println("Error: ", err.Error()) @@ -61,7 +61,7 @@ func GinCreateCompletions(c *gin.Context) { // Stream the completion (uses default handler which sends to ctx.Writer) // The Stream method will automatically close the writer and send [DONE] marker log.Trace("[HTTP] Calling ast.Stream()") - _, err = ast.Stream(ctx, completionReq.Messages) + _, err = ast.Stream(ctx, completionReq.Messages, opts) log.Trace("[HTTP] ast.Stream() returned, err=%v", err) if err != nil { response.RespondWithError(c, response.StatusInternalServerError, &response.ErrorResponse{ From 311350bfbcac4d537a8c6ab0db1e746f553d6b89 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 4 Dec 2025 12:03:00 +0800 Subject: [PATCH 6/6] Enhance Assistant methods with options parameter for improved context management - Updated the Stream, BuildContent, and LLM execution methods to accept an Options parameter, allowing for more flexible context handling. - Removed debug print statements to clean up the code and improve readability. - Enhanced locale handling in the loadMap function to automatically inject assistant name and description into all locales, ensuring better localization support. - Introduced output skipping functionality in context options to manage internal A2A calls more effectively. - Improved output writer resolution logic to prioritize context settings, enhancing output management during agent calls. --- agent/assistant/agent.go | 17 ++++++-------- agent/assistant/build_content.go | 4 ++-- agent/assistant/llm.go | 6 +++-- agent/assistant/load.go | 33 ++++++++++++++++++++++++++- agent/content/tools.go | 4 ++-- agent/context/options.go | 3 +++ agent/context/output.go | 34 +++++++++++++++++++++++----- agent/context/types.go | 5 +++- agent/i18n/builtin.go | 6 ++--- agent/llm/providers/openai/openai.go | 8 +++++++ 10 files changed, 93 insertions(+), 27 deletions(-) diff --git a/agent/assistant/agent.go b/agent/assistant/agent.go index 111288c1..2cdc2970 100644 --- a/agent/assistant/agent.go +++ b/agent/assistant/agent.go @@ -51,13 +51,6 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa _, _, done := context.EnterStack(ctx, ast.ID, opts) defer done() - fmt.Println("--- Stack debug ---") - if ctx.Stack != nil { - fmt.Println(ctx.Stack.IsRoot()) - utils.Dump(ctx.Stack) - } - fmt.Println("------ end stack debug ------") - // Determine stream handler streamHandler := ast.getStreamHandler(ctx, opts) @@ -123,7 +116,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa } // Build content - convert extended types (file, data) to standard LLM types (text, image_url, input_audio) - completionMessages, err = ast.BuildContent(ctx, completionMessages, completionOptions) + completionMessages, err = ast.BuildContent(ctx, completionMessages, completionOptions, opts) if err != nil { ast.traceAgentFail(agentNode, err) ast.sendStreamEndOnError(ctx, streamHandler, streamStartTime, err) @@ -131,7 +124,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa } // Execute the LLM streaming call - completionResponse, err = ast.executeLLMStream(ctx, completionMessages, completionOptions, agentNode, streamHandler) + completionResponse, err = ast.executeLLMStream(ctx, completionMessages, completionOptions, agentNode, streamHandler, opts) if err != nil { ast.traceAgentFail(agentNode, err) // Send error stream_end for root stack @@ -210,7 +203,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa // Retry LLM call (streaming to keep user informed) log.Trace("[AGENT] Retrying LLM for tool call correction (attempt %d/%d)", attempt+1, maxToolRetries-1) - currentResponse, err = ast.executeLLMForToolRetry(ctx, retryMessages, completionOptions, agentNode, streamHandler) + currentResponse, err = ast.executeLLMForToolRetry(ctx, retryMessages, completionOptions, agentNode, streamHandler, opts) if err != nil { log.Error("[AGENT] LLM retry failed: %v", err) ast.traceAgentFail(agentNode, err) @@ -479,6 +472,10 @@ func (ast *Assistant) initializeCapabilities(ctx *context.Context, opts *context return err } + fmt.Println("--- initializeCapabilities debug ---") + utils.Dump(capabilities) + fmt.Println("--- end initializeCapabilities debug ---") + // Set capabilities in context for output adapters to use if capabilities != nil { ctx.Capabilities = capabilities diff --git a/agent/assistant/build_content.go b/agent/assistant/build_content.go index f898ee19..ba254287 100644 --- a/agent/assistant/build_content.go +++ b/agent/assistant/build_content.go @@ -11,9 +11,9 @@ import ( // (file, data) to standard LLM-compatible types (text, image_url, input_audio) // // This should be called after BuildRequest and before executing LLM call -func (ast *Assistant) BuildContent(ctx *context.Context, messages []context.Message, options *context.CompletionOptions) ([]context.Message, error) { +func (ast *Assistant) BuildContent(ctx *context.Context, messages []context.Message, options *context.CompletionOptions, opts *context.Options) ([]context.Message, error) { // Get connector and capabilities - _, capabilities, err := ast.GetConnector(ctx) + _, capabilities, err := ast.GetConnector(ctx, opts) if err != nil { return nil, fmt.Errorf("failed to get connector: %w", err) } diff --git a/agent/assistant/llm.go b/agent/assistant/llm.go index b4e9d955..eab35e2e 100644 --- a/agent/assistant/llm.go +++ b/agent/assistant/llm.go @@ -18,6 +18,7 @@ func (ast *Assistant) executeLLMStream( completionOptions *context.CompletionOptions, agentNode types.Node, streamHandler message.StreamFunc, + opts *context.Options, ) (*context.CompletionResponse, error) { // === Debug LLM Stream Start === @@ -27,7 +28,7 @@ func (ast *Assistant) executeLLMStream( // === End Debug === // Get connector object (capabilities were already set above, before stream_start) - conn, capabilities, err := ast.GetConnector(ctx) + conn, capabilities, err := ast.GetConnector(ctx, opts) if err != nil { ast.traceAgentFail(agentNode, err) return nil, err @@ -92,10 +93,11 @@ func (ast *Assistant) executeLLMForToolRetry( completionOptions *context.CompletionOptions, agentNode types.Node, streamHandler message.StreamFunc, + opts *context.Options, ) (*context.CompletionResponse, error) { // Get connector object - conn, capabilities, err := ast.GetConnector(ctx) + conn, capabilities, err := ast.GetConnector(ctx, opts) if err != nil { ast.traceAgentFail(agentNode, err) return nil, err diff --git a/agent/assistant/load.go b/agent/assistant/load.go index 477c429b..9d087f04 100644 --- a/agent/assistant/load.go +++ b/agent/assistant/load.go @@ -531,7 +531,38 @@ func loadMap(data map[string]interface{}) (*Assistant, error) { // locales if locales, ok := data["locales"].(i18n.Map); ok { assistant.Locales = locales - i18n.Locales[id] = locales.FlattenWithGlobal() + flattened := locales.FlattenWithGlobal() + + // Auto-inject assistant name and description into all locales + // so that {{name}} and {{description}} templates can be resolved + for locale, i18nObj := range flattened { + if i18nObj.Messages == nil { + i18nObj.Messages = make(map[string]any) + } + // Add name and description if not already present + if _, exists := i18nObj.Messages["name"]; !exists && assistant.Name != "" { + i18nObj.Messages["name"] = assistant.Name + } + if _, exists := i18nObj.Messages["description"]; !exists && assistant.Description != "" { + i18nObj.Messages["description"] = assistant.Description + } + flattened[locale] = i18nObj + } + + i18n.Locales[id] = flattened + } else { + // No locales defined, create default with name and description + if assistant.Name != "" || assistant.Description != "" { + defaultLocales := make(map[string]i18n.I18n) + defaultLocales["en"] = i18n.I18n{ + Locale: "en", + Messages: map[string]any{ + "name": assistant.Name, + "description": assistant.Description, + }, + } + i18n.Locales[id] = defaultLocales + } } // Search options diff --git a/agent/content/tools.go b/agent/content/tools.go index c34330a0..d1f2e9da 100644 --- a/agent/content/tools.go +++ b/agent/content/tools.go @@ -36,8 +36,8 @@ func CallAgent(ctx *agentContext.Context, agentID string, message agentContext.M messages := []agentContext.Message{message} // Note: Connector is now in Options (call-level parameter), not Context - // For A2A calls, we use an empty Connector to let the agent use its default - opts := &agentContext.Options{Skip: &agentContext.Skip{History: true}, Writer: nil} // Skip history and output to the caller + // For A2A calls, skip history and output (we only need the response data) + opts := &agentContext.Options{Skip: &agentContext.Skip{History: true, Output: true}} // Skip history and output response, err := agent.Stream(ctx, messages, opts) if err != nil { return "", fmt.Errorf("failed to call agent %s: %w", agentID, err) diff --git a/agent/context/options.go b/agent/context/options.go index 9b9871ac..b6aa7952 100644 --- a/agent/context/options.go +++ b/agent/context/options.go @@ -58,6 +58,9 @@ func OptionsFromMap(m map[string]interface{}) *Options { if trace, ok := skipMap["trace"].(bool); ok { skip.Trace = trace } + if output, ok := skipMap["output"].(bool); ok { + skip.Output = output + } opts.Skip = skip } if disableGlobalPrompts, ok := m["disable_global_prompts"].(bool); ok { diff --git a/agent/context/output.go b/agent/context/output.go index 71c417cd..debd39f5 100644 --- a/agent/context/output.go +++ b/agent/context/output.go @@ -297,16 +297,33 @@ func (ctx *Context) sendRaw(msg *message.Message) error { return out.Send(msg) } +// getWriter gets the effective Writer for the current context +// Priority: Skip.Output > Stack.Options.Writer > ctx.Writer +func (ctx *Context) getWriter() Writer { + // Check if output is explicitly skipped (for internal A2A calls) + if ctx.Stack != nil && ctx.Stack.Options != nil && ctx.Stack.Options.Skip != nil && ctx.Stack.Options.Skip.Output { + return nil // Explicitly disable output + } + + // Check if current Stack has a Writer override + if ctx.Stack != nil && ctx.Stack.Options != nil && ctx.Stack.Options.Writer != nil { + return ctx.Stack.Options.Writer + } + + return ctx.Writer +} + // getOutput gets the output writer for the context func (ctx *Context) getOutput() (*output.Output, error) { - if ctx.output != nil { - return ctx.output, nil + // Check if current Stack has cached output + if ctx.Stack != nil && ctx.Stack.output != nil { + return ctx.Stack.output, nil } trace, _ := ctx.Trace() var options message.Options = message.Options{ BaseURL: "/", - Writer: ctx.Writer, + Writer: ctx.getWriter(), // Use getWriter() to resolve Writer priority Trace: trace, Locale: ctx.Locale, Accept: string(ctx.Accept), @@ -318,10 +335,15 @@ func (ctx *Context) getOutput() (*output.Output, error) { options.Capabilities = &caps } - var err error - ctx.output, err = output.NewOutput(options) + out, err := output.NewOutput(options) if err != nil { return nil, err } - return ctx.output, nil + + // Cache to current Stack (each Stack has its own output with its own Writer) + if ctx.Stack != nil { + ctx.Stack.output = out + } + + return out, nil } diff --git a/agent/context/types.go b/agent/context/types.go index bea8390f..15f6b605 100644 --- a/agent/context/types.go +++ b/agent/context/types.go @@ -194,6 +194,7 @@ type AssistantInfo struct { type Skip struct { History bool `json:"history"` // Skip saving chat history (for internal calls like title/prompt generation) Trace bool `json:"trace"` // Skip trace logging + Output bool `json:"output"` // Skip output to client (for internal A2A calls that only need response data) } // MessageMetadata stores metadata for sent messages @@ -232,7 +233,6 @@ type Context struct { // Internal trace traceTypes.Manager `json:"-"` // Trace manager, lazy initialized on first access - output *output.Output `json:"-"` // Output, it will be used to write response data to the client messageMetadata *messageMetadataStore `json:"-"` // Thread-safe message metadata store for delta operations // Model capabilities (set by assistant, used by output adapters) @@ -312,6 +312,9 @@ type Stack struct { // Metrics DurationMs *int64 `json:"duration_ms,omitempty"` // Duration in milliseconds (calculated when completed) + + // Runtime cache (not serialized) + output *output.Output `json:"-"` // Cached output instance for this stack } // Response the response diff --git a/agent/i18n/builtin.go b/agent/i18n/builtin.go index 9e93770c..380f4f2c 100644 --- a/agent/i18n/builtin.go +++ b/agent/i18n/builtin.go @@ -21,7 +21,7 @@ func init() { "assistant.agent.stream.skipping": "Skipping output close (nested call)", "assistant.agent.stream.close_error": "Failed to close output", "assistant.agent.completion.label": "Agent Completion", - "assistant.agent.completion.description": "Final output from assistant", + "assistant.agent.completion.description": "Final output from {{name}}", // LLM: providers/openai/openai.go Stream() function "llm.openai.stream.label": "LLM %s", @@ -111,7 +111,7 @@ func init() { "assistant.agent.stream.skipping": "跳过输出关闭(嵌套调用)", "assistant.agent.stream.close_error": "关闭输出失败", "assistant.agent.completion.label": "智能体完成", - "assistant.agent.completion.description": "智能体最终输出", + "assistant.agent.completion.description": "{{name}} 最终输出", // LLM: providers/openai/openai.go Stream() function "llm.openai.stream.label": "LLM %s", @@ -173,7 +173,7 @@ func init() { "assistant.agent.stream.skipping": "跳过输出关闭(嵌套调用)", "assistant.agent.stream.close_error": "关闭输出失败", "assistant.agent.completion.label": "智能体完成", - "assistant.agent.completion.description": "智能体最终输出", + "assistant.agent.completion.description": "{{name}} 最终输出", // LLM: providers/openai/openai.go Stream() function "llm.openai.stream.label": "LLM %s", diff --git a/agent/llm/providers/openai/openai.go b/agent/llm/providers/openai/openai.go index 637aa7b0..fb46844a 100644 --- a/agent/llm/providers/openai/openai.go +++ b/agent/llm/providers/openai/openai.go @@ -211,7 +211,11 @@ func (p *Provider) Stream(ctx *context.Context, messages []context.Message, opti var lastErr error // Get Go context for cancellation support + // Read from Stack.Options if available (call-level override) goCtx := ctx.Context + if ctx.Stack != nil && ctx.Stack.Options != nil && ctx.Stack.Options.Context != nil { + goCtx = ctx.Stack.Options.Context + } if goCtx == nil { goCtx = gocontext.Background() } @@ -808,7 +812,11 @@ func (p *Provider) Post(ctx *context.Context, messages []context.Message, option var lastErr error // Get Go context for cancellation support + // Read from Stack.Options if available (call-level override) goCtx := ctx.Context + if ctx.Stack != nil && ctx.Stack.Options != nil && ctx.Stack.Options.Context != nil { + goCtx = ctx.Stack.Options.Context + } if goCtx == nil { goCtx = gocontext.Background() }