diff --git a/agent/assistant/agent.go b/agent/assistant/agent.go index 85417c31..b5f9383d 100644 --- a/agent/assistant/agent.go +++ b/agent/assistant/agent.go @@ -23,7 +23,7 @@ func (ast *Assistant) Stream(ctx *context.Context, inputMessages []context.Messa } // Request Create hook ( Optional ) - var createResponse *context.ResponseHookCreate + var createResponse *context.HookCreateResponse if ast.Script != nil { var err error createResponse, err = ast.Script.Create(ctx, fullMessages) @@ -90,7 +90,7 @@ func (ast *Assistant) GetConnector(ctx *context.Context) string { } // BuildLLMRequest build the LLM request -func (ast *Assistant) BuildLLMRequest(ctx *context.Context, messages []context.Message, createResponse *context.ResponseHookCreate) ([]context.Message, *llm.CompletionOptions, error) { +func (ast *Assistant) BuildLLMRequest(ctx *context.Context, messages []context.Message, createResponse *context.HookCreateResponse) ([]context.Message, *llm.CompletionOptions, error) { return messages, nil, nil } diff --git a/agent/assistant/hook/create.go b/agent/assistant/hook/create.go index aba8ebcd..770ed8bb 100644 --- a/agent/assistant/hook/create.go +++ b/agent/assistant/hook/create.go @@ -1,14 +1,44 @@ package hook -import "github.com/yaoapp/yao/agent/context" +import ( + "encoding/json" + "fmt" + + "github.com/yaoapp/gou/runtime/v8/bridge" + "github.com/yaoapp/yao/agent/context" +) // Create create a new assistant -func (s *Script) Create(ctx *context.Context, messages []context.Message) (*context.ResponseHookCreate, error) { +func (s *Script) Create(ctx *context.Context, messages []context.Message) (*context.HookCreateResponse, error) { res, err := s.Execute(ctx, "Create", messages) if err != nil { return nil, err } - - _ = res - return &context.ResponseHookCreate{}, nil + return s.getHookCreateResponse(res) +} + +// getHookCreateResponse convert the result to a HookCreateResponse +func (s *Script) getHookCreateResponse(res interface{}) (*context.HookCreateResponse, error) { + // Handle nil result + if res == nil { + return nil, nil + } + + // Handle undefined result (treat as nil) + if _, ok := res.(bridge.UndefinedT); ok { + return nil, nil + } + + // Marshal to JSON and unmarshal to HookCreateResponse + raw, err := json.Marshal(res) + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + var response context.HookCreateResponse + if err := json.Unmarshal(raw, &response); err != nil { + return nil, fmt.Errorf("failed to unmarshal to HookCreateResponse: %w", err) + } + + return &response, nil } diff --git a/agent/assistant/hook/create_test.go b/agent/assistant/hook/create_test.go index 3a11fcfd..be4c681e 100644 --- a/agent/assistant/hook/create_test.go +++ b/agent/assistant/hook/create_test.go @@ -1,11 +1,11 @@ package hook_test import ( - "context" + defaultContext "context" "testing" "github.com/yaoapp/yao/agent/assistant" - agentContext "github.com/yaoapp/yao/agent/context" + "github.com/yaoapp/yao/agent/context" "github.com/yaoapp/yao/agent/testutils" ) @@ -23,12 +23,213 @@ func TestCreate(t *testing.T) { t.Fatalf("The tests.create assistant has no script") } - ctx := &agentContext.Context{ - Context: context.Background(), + ctx := &context.Context{ + Context: defaultContext.Background(), ChatID: "chat-test-create-hook", AssistantID: "tests.create", Sid: "test-session-create-hook", } - agent.Script.Create(ctx, []agentContext.Message{{Role: "user", Content: "Hello, how are you?"}}) + // 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"}}) + if err != nil { + t.Fatalf("Failed to create with null return: %s", err.Error()) + } + if res != nil { + t.Errorf("Expected nil response for null return, got: %v", res) + } + }) + + // 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"}}) + if err != nil { + t.Fatalf("Failed to create with undefined return: %s", err.Error()) + } + if res != nil { + t.Errorf("Expected nil response for undefined return, got: %v", res) + } + }) + + // 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"}}) + if err != nil { + t.Fatalf("Failed to create with empty return: %s", err.Error()) + } + if res == nil { + t.Fatalf("Expected non-nil response for empty object, got nil") + } + if len(res.Messages) != 0 { + t.Errorf("Expected empty messages, got: %d messages", len(res.Messages)) + } + }) + + // 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"}}) + if err != nil { + t.Fatalf("Failed to create with full return: %s", err.Error()) + } + if res == nil { + t.Fatalf("Expected non-nil response, got nil") + } + + // Verify messages + if len(res.Messages) != 2 { + t.Errorf("Expected 2 messages, got: %d", len(res.Messages)) + } else { + if res.Messages[0].Role != context.RoleSystem { + t.Errorf("Expected system role for first message, got: %s", res.Messages[0].Role) + } + if res.Messages[1].Role != context.RoleUser { + t.Errorf("Expected user role for second message, got: %s", res.Messages[1].Role) + } + } + + // Verify audio config + if res.Audio == nil { + t.Error("Expected audio config, got nil") + } else { + if res.Audio.Voice != "alloy" { + t.Errorf("Expected voice 'alloy', got: %s", res.Audio.Voice) + } + if res.Audio.Format != "mp3" { + t.Errorf("Expected format 'mp3', got: %s", res.Audio.Format) + } + } + + // Verify temperature + if res.Temperature == nil { + t.Error("Expected temperature, got nil") + } else if *res.Temperature != 0.7 { + t.Errorf("Expected temperature 0.7, got: %f", *res.Temperature) + } + + // Verify max_tokens + if res.MaxTokens == nil { + t.Error("Expected max_tokens, got nil") + } else if *res.MaxTokens != 2000 { + t.Errorf("Expected max_tokens 2000, got: %d", *res.MaxTokens) + } + + // Verify max_completion_tokens + if res.MaxCompletionTokens == nil { + t.Error("Expected max_completion_tokens, got nil") + } else if *res.MaxCompletionTokens != 1500 { + t.Errorf("Expected max_completion_tokens 1500, got: %d", *res.MaxCompletionTokens) + } + + // Verify metadata + if res.Metadata == nil { + t.Error("Expected metadata, got nil") + } else { + if res.Metadata["test"] != "full_response" { + t.Errorf("Expected metadata['test'] = 'full_response', got: %s", res.Metadata["test"]) + } + if res.Metadata["user_id"] != "test_user_123" { + t.Errorf("Expected metadata['user_id'] = 'test_user_123', got: %s", res.Metadata["user_id"]) + } + } + }) + + // 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"}}) + if err != nil { + t.Fatalf("Failed to create with partial return: %s", err.Error()) + } + if res == nil { + t.Fatalf("Expected non-nil response, got nil") + } + + // Verify messages + if len(res.Messages) != 1 { + t.Errorf("Expected 1 message, got: %d", len(res.Messages)) + } + + // Verify temperature + if res.Temperature == nil { + t.Error("Expected temperature, got nil") + } else if *res.Temperature != 0.5 { + t.Errorf("Expected temperature 0.5, got: %f", *res.Temperature) + } + + // Verify optional fields are nil + if res.Audio != nil { + t.Errorf("Expected audio to be nil, got: %v", res.Audio) + } + if res.MaxTokens != nil { + t.Errorf("Expected max_tokens to be nil, got: %d", *res.MaxTokens) + } + }) + + // 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"}}) + if err != nil { + t.Fatalf("Failed to create with process return: %s", err.Error()) + } + if res == nil { + t.Fatalf("Expected non-nil response, got nil") + } + + // Verify messages - should have at least 1 (system message) + if len(res.Messages) < 1 { + t.Errorf("Expected at least 1 message, got: %d", len(res.Messages)) + } else { + // First message should be system role + if res.Messages[0].Role != context.RoleSystem { + t.Errorf("Expected system role for first message, got: %s", res.Messages[0].Role) + } + // Check system message content + if content, ok := res.Messages[0].Content.(string); ok { + if content != "Here are the available roles in the system:" { + t.Errorf("Unexpected system message content: %s", content) + } + } + } + + // Verify metadata + if res.Metadata == nil { + t.Error("Expected metadata, got nil") + } else { + if res.Metadata["test"] != "process_call" { + t.Errorf("Expected metadata['test'] = 'process_call', got: %s", res.Metadata["test"]) + } + // roles_count should be present + if _, ok := res.Metadata["roles_count"]; !ok { + t.Error("Expected metadata['roles_count'] to be present") + } + } + }) + + // 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}}) + if err != nil { + t.Fatalf("Failed to create with default return: %s", err.Error()) + } + if res == nil { + t.Fatalf("Expected non-nil response, got nil") + } + + // Verify messages + if len(res.Messages) != 1 { + t.Errorf("Expected 1 message, got: %d", len(res.Messages)) + } else { + if res.Messages[0].Role != context.RoleUser { + t.Errorf("Expected user role, got: %s", res.Messages[0].Role) + } + if content, ok := res.Messages[0].Content.(string); ok { + if content != testContent { + t.Errorf("Expected content '%s', got: '%s'", testContent, content) + } + } else { + t.Errorf("Expected string content, got: %T", res.Messages[0].Content) + } + } + }) } diff --git a/agent/context/types.go b/agent/context/types.go index c443c1f2..1e383232 100644 --- a/agent/context/types.go +++ b/agent/context/types.go @@ -184,15 +184,30 @@ type Stack struct { // Response the response // 100% compatible with the OpenAI API type Response struct { - Create *ResponseHookCreate `json:"create,omitempty"` + Create *HookCreateResponse `json:"create,omitempty"` MCP *ResponseHookMCP `json:"mcp,omitempty"` Done *ResponseHookDone `json:"done,omitempty"` Failback *ResponseHookFailback `json:"failback,omitempty"` Completion *ResponseCompletion `json:"completion,omitempty"` } -// ResponseHookCreate the response of the create hook -type ResponseHookCreate struct{} +// HookCreateResponse the response of the create hook +type HookCreateResponse struct { + + // Messages to be sent to the assistant + Messages []Message `json:"messages,omitempty"` + + // Audio configuration (for models that support audio output) + Audio *AudioConfig `json:"audio,omitempty"` + + // Generation parameters + Temperature *float64 `json:"temperature,omitempty"` + MaxTokens *int `json:"max_tokens,omitempty"` + MaxCompletionTokens *int `json:"max_completion_tokens,omitempty"` + + // Request metadata + Metadata map[string]string `json:"metadata,omitempty"` // Optional: developer-defined tags and values for tracking requests +} // ResponseHookDone the response of the done hook type ResponseHookDone struct{}