diff --git a/agent/assistant/agent.go b/agent/assistant/agent.go
index 9cebdf5f..2cdc2970 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"
@@ -17,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)
@@ -38,16 +39,24 @@ 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()
// 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
@@ -77,7 +86,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
@@ -106,8 +115,16 @@ 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, opts)
+ 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)
+ completionResponse, err = ast.executeLLMStream(ctx, completionMessages, completionOptions, agentNode, streamHandler, opts)
if err != nil {
ast.traceAgentFail(agentNode, err)
// Send error stream_end for root stack
@@ -186,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)
@@ -219,11 +236,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)
@@ -302,14 +319,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
@@ -345,10 +362,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)
}
@@ -444,16 +462,20 @@ 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
}
+ 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/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 de103b9c..5238bd8b 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, options ...*agentContext.Options) (interface{}, error) {
+ return w.ast.Stream(ctx, messages, options...)
+}
+
// 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..ba254287
--- /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, opts *context.Options) ([]context.Message, error) {
+ // Get connector and capabilities
+ _, capabilities, err := ast.GetConnector(ctx, opts)
+ 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/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/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/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/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 3a059eb0..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
@@ -751,15 +782,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/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/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..fb0e7633
--- /dev/null
+++ b/agent/content/image_test.go
@@ -0,0 +1,264 @@
+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",
+ 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..d1f2e9da
--- /dev/null
+++ b/agent/content/tools.go
@@ -0,0 +1,188 @@
+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, options ...*agentContext.Options) (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}
+
+ // Note: Connector is now in Options (call-level parameter), not Context
+ // 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)
+ }
+
+ // 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/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 d35677dc..8f5f61fd 100644
--- a/agent/context/openapi.go
+++ b/agent/context/openapi.go
@@ -9,26 +9,27 @@ import (
"strings"
"github.com/gin-gonic/gin"
+ "github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/store"
"github.com/yaoapp/yao/openapi/oauth/authorized"
)
// 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)
@@ -46,7 +47,7 @@ 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
@@ -61,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
@@ -76,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..b6aa7952
--- /dev/null
+++ b/agent/context/options.go
@@ -0,0 +1,74 @@
+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
+ }
+ if output, ok := skipMap["output"].(bool); ok {
+ skip.Output = output
+ }
+ 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/output.go b/agent/context/output.go
index e50bdc54..debd39f5 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",
@@ -276,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),
@@ -297,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/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 d62cc1e5..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,12 +233,8 @@ 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
- // 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.
@@ -294,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
@@ -331,12 +352,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 +392,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
@@ -464,6 +484,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 +495,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 +521,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/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/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.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()
}
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/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"
diff --git a/attachment/README.md b/attachment/README.md
index 661dd3ab..4aec9b24 100644
--- a/attachment/README.md
+++ b/attachment/README.md
@@ -659,6 +659,134 @@ 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.
+
+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. It automatically saves both full content and preview:
+
+```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 (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)
+}
+```
+
+### Retrieving Parsed Text Content
+
+Use `GetText` to retrieve text content. By default, it returns the preview for better performance:
+
+```go
+// 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 preview: %w", err)
+}
+
+if preview == "" {
+ fmt.Println("No text content available for this file")
+} else {
+ 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
+// 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
+
+- **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 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)`
Registers a default attachment manager with sensible defaults for common file types.
diff --git a/attachment/manager.go b/attachment/manager.go
index f9db63b3..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
@@ -1322,3 +1332,94 @@ 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
+// 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{}{fieldName},
+ 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][fieldName].(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
+// 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")
+
+ // 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)
+ }
+
+ // 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_preview": preview,
+ }
+
+ _, 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..994a9f1b 100644
--- a/attachment/manager_test.go
+++ b/attachment/manager_test.go
@@ -1477,3 +1477,330 @@ 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)
+ }
+
+ // 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
+ 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 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)
+
+ err := manager.SaveText(context.Background(), file.ID, longText)
+ if err != nil {
+ t.Fatalf("Failed to save long text: %v", err)
+ }
+
+ // 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 preview text: %v", err)
+ }
+
+ // 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))
+ }
+
+ // 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: 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 {
+ 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: 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 8: 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)
+ }
+ })
+
+ // 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 {
+ t.Logf("Warning: Failed to delete test file: %v", err)
+ }
+}
diff --git a/attachment/types.go b/attachment/types.go
index b125174b..b370299e 100644
--- a/attachment/types.go
+++ b/attachment/types.go
@@ -47,6 +47,14 @@ 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
+ // 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
}
// File the file
diff --git a/data/bindata.go b/data/bindata.go
index 365ef74a..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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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\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\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: 4286, mode: os.FileMode(420), modTime: time.Unix(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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(1764673310, 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/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{
diff --git a/yao/models/attachment.mod.yao b/yao/models/attachment.mod.yao
index c1986e9b..b2a78ecb 100644
--- a/yao/models/attachment.mod.yao
+++ b/yao/models/attachment.mod.yao
@@ -45,6 +45,20 @@
"nullable": false,
"index": true
},
+ {
+ "name": "content",
+ "type": "longText",
+ "label": "Content",
+ "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
+ },
{
"name": "name",
"type": "string",