diff --git a/agent/assistant/agent.go b/agent/assistant/agent.go index 2f515fc0..eca73f0a 100644 --- a/agent/assistant/agent.go +++ b/agent/assistant/agent.go @@ -8,7 +8,6 @@ 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" @@ -472,10 +471,6 @@ func (ast *Assistant) initializeCapabilities(ctx *context.Context, opts *context return err } - fmt.Println("--- initializeCapabilities debug ---") - utils.Dump(capabilities) - fmt.Println("--- end initializeCapabilities debug ---") - // Set capabilities in context for output adapters to use if capabilities != nil { ctx.Capabilities = capabilities diff --git a/agent/context/jsapi.go b/agent/context/jsapi.go index 4f6b5ea0..1dbda994 100644 --- a/agent/context/jsapi.go +++ b/agent/context/jsapi.go @@ -694,6 +694,40 @@ func (ctx *Context) createSpaceObject(v8ctx *v8go.Context) *v8go.Value { delFuncVal := delFunc.GetFunction(v8ctx) obj.Set("Delete", delFuncVal.Value) + // GetDel method: space.GetDel(key) - Get value and delete immediately + // Convenient for one-time use data (e.g., file metadata passed between agents) + getDelFunc := v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { + if ctx.Space == nil { + return v8go.Null(iso) + } + + if len(info.Args()) < 1 { + return bridge.JsException(info.Context(), "GetDel requires a key argument") + } + + key := info.Args()[0].String() + + // Get value first + value, err := ctx.Space.Get(key) + if err != nil { + return v8go.Null(iso) + } + + // Delete immediately after getting + // Ignore delete errors (key might not exist) + ctx.Space.Delete(key) + + // Convert to JavaScript value + jsValue, err := bridge.JsValue(info.Context(), value) + if err != nil { + return v8go.Null(iso) + } + + return jsValue + }) + getDelFuncVal := getDelFunc.GetFunction(v8ctx) + obj.Set("GetDel", getDelFuncVal.Value) + return spaceObj } diff --git a/agent/context/jsapi_space_test.go b/agent/context/jsapi_space_test.go index 6452c5da..50148894 100644 --- a/agent/context/jsapi_space_test.go +++ b/agent/context/jsapi_space_test.go @@ -570,3 +570,248 @@ func TestSpaceErrorHandling(t *testing.T) { assert.Equal(t, true, result["success"], "Should catch Delete error") } + +// TestSpaceGetDel tests ctx.space.GetDel method +func TestSpaceGetDel(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + ctx := &context.Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Locale: "en", + Context: stdContext.Background(), + Space: plan.NewMemorySharedSpace(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Set a one-time use value + ctx.space.Set("one_time_token", "secret_token_12345"); + + // Verify it exists before GetDel + const before = ctx.space.Get("one_time_token"); + if (before !== "secret_token_12345") throw new Error("Value not set"); + + // Use GetDel - should get value and delete automatically + const value = ctx.space.GetDel("one_time_token"); + + // Verify value was retrieved + if (value !== "secret_token_12345") throw new Error("GetDel returned wrong value"); + + // Verify key was deleted + const after = ctx.space.Get("one_time_token"); + if (after !== null && after !== undefined) throw new Error("Key should be deleted after GetDel"); + + return { + success: true, + value: value, + is_deleted: after === null || after === undefined + }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, ctx) + + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + + if !result["success"].(bool) { + t.Fatalf("Test failed: %v", result["error"]) + } + + assert.Equal(t, true, result["success"], "GetDel should succeed") + assert.Equal(t, "secret_token_12345", result["value"], "GetDel should return correct value") + assert.Equal(t, true, result["is_deleted"], "Key should be deleted after GetDel") +} + +// TestSpaceGetDelNonExistentKey tests GetDel on non-existent key +func TestSpaceGetDelNonExistentKey(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + ctx := &context.Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Locale: "en", + Context: stdContext.Background(), + Space: plan.NewMemorySharedSpace(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // GetDel on non-existent key should return null/undefined + const value = ctx.space.GetDel("non_existent_key"); + + return { + success: true, + value: value, + is_null: value === null || value === undefined + }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, ctx) + + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + + assert.Equal(t, true, result["success"], "GetDel on non-existent key should not throw") + assert.Equal(t, true, result["is_null"], "GetDel on non-existent key should return null") +} + +// TestSpaceGetDelComplexData tests GetDel with complex data structures +func TestSpaceGetDelComplexData(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + ctx := &context.Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Locale: "en", + Context: stdContext.Background(), + Space: plan.NewMemorySharedSpace(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Set complex file info data (like voucher assistant use case) + const filesInfo = [ + { + file_id: "file123", + filename: "invoice.pdf", + content_type: "application/pdf", + file_type: "pdf", + source: "uploader", + uploader_name: "__yao.attachment" + }, + { + file_id: "file456", + filename: "receipt.png", + content_type: "image/png", + file_type: "image", + source: "uploader", + uploader_name: "__yao.attachment" + } + ]; + + ctx.space.Set("workers.voucher:files_info", filesInfo); + + // Use GetDel to retrieve and clean up + const retrieved = ctx.space.GetDel("workers.voucher:files_info"); + + // Verify data integrity + if (!Array.isArray(retrieved)) throw new Error("Should be array"); + if (retrieved.length !== 2) throw new Error("Length mismatch"); + if (retrieved[0].file_id !== "file123") throw new Error("First file_id mismatch"); + if (retrieved[1].filename !== "receipt.png") throw new Error("Second filename mismatch"); + + // Verify it's deleted + const after = ctx.space.Get("workers.voucher:files_info"); + if (after !== null && after !== undefined) throw new Error("Should be deleted"); + + return { + success: true, + files_count: retrieved.length, + first_file_id: retrieved[0].file_id, + is_deleted: after === null || after === undefined + }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, ctx) + + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + + if !result["success"].(bool) { + t.Fatalf("Test failed: %v", result["error"]) + } + + assert.Equal(t, true, result["success"], "GetDel with complex data should succeed") + assert.Equal(t, float64(2), result["files_count"], "Should have 2 files") + assert.Equal(t, "file123", result["first_file_id"], "File ID should match") + assert.Equal(t, true, result["is_deleted"], "Should be deleted after GetDel") +} + +// TestSpaceGetDelMultipleCalls tests that GetDel only works once +func TestSpaceGetDelMultipleCalls(t *testing.T) { + test.Prepare(t, config.Conf) + defer test.Clean() + + ctx := &context.Context{ + ChatID: "test-chat-id", + AssistantID: "test-assistant-id", + Locale: "en", + Context: stdContext.Background(), + Space: plan.NewMemorySharedSpace(), + } + + res, err := v8.Call(v8.CallOptions{}, ` + function test(ctx) { + try { + // Set a value + ctx.space.Set("single_use", "use_me_once"); + + // First GetDel should work + const first = ctx.space.GetDel("single_use"); + if (first !== "use_me_once") throw new Error("First GetDel failed"); + + // Second GetDel should return null (already deleted) + const second = ctx.space.GetDel("single_use"); + if (second !== null && second !== undefined) throw new Error("Second GetDel should return null"); + + // Third GetDel should also return null + const third = ctx.space.GetDel("single_use"); + if (third !== null && third !== undefined) throw new Error("Third GetDel should return null"); + + return { + success: true, + first: first, + second_is_null: second === null || second === undefined, + third_is_null: third === null || third === undefined + }; + } catch (error) { + return { success: false, error: error.message }; + } + }`, ctx) + + if err != nil { + t.Fatalf("Call failed: %v", err) + } + + result, ok := res.(map[string]interface{}) + if !ok { + t.Fatalf("Expected map result, got %T", res) + } + + if !result["success"].(bool) { + t.Fatalf("Test failed: %v", result["error"]) + } + + assert.Equal(t, true, result["success"], "Multiple GetDel calls should work correctly") + assert.Equal(t, "use_me_once", result["first"], "First GetDel should return value") + assert.Equal(t, true, result["second_is_null"], "Second GetDel should return null") + assert.Equal(t, true, result["third_is_null"], "Third GetDel should return null") +}