From 3a9e84e538b9253d567aa32bde3bb9d7b67ba274 Mon Sep 17 00:00:00 2001 From: ai16z-demirix Date: Fri, 3 Jan 2025 22:51:17 +0100 Subject: [PATCH 1/4] refactor: (draft) improving API error handling for coinbase integration --- .../advanced-sdk-ts/src/rest/errors.ts | 127 +++++++++++++++--- .../advanced-sdk-ts/src/rest/rest-base.ts | 33 ++++- 2 files changed, 135 insertions(+), 25 deletions(-) diff --git a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts index 5d9695ef67d..3c3d1365f93 100644 --- a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts +++ b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts @@ -1,15 +1,113 @@ import { Response } from 'node-fetch'; -class CoinbaseError extends Error { - statusCode: number; - response: Response; +// Define specific error types for different scenarios +export enum CoinbaseErrorType { + AUTHENTICATION = 'AUTHENTICATION', + PERMISSION = 'PERMISSION', + VALIDATION = 'VALIDATION', + RATE_LIMIT = 'RATE_LIMIT', + SERVER_ERROR = 'SERVER_ERROR', + NETWORK_ERROR = 'NETWORK_ERROR', + UNKNOWN = 'UNKNOWN' +} + +export interface CoinbaseErrorDetails { + type: CoinbaseErrorType; + message: string; + details?: Record; + suggestion?: string; +} - constructor(message: string, statusCode: number, response: Response) { - super(message); +export class CoinbaseError extends Error { + readonly statusCode: number; + readonly response: Response; + readonly type: CoinbaseErrorType; + readonly details?: Record; + readonly suggestion?: string; + + constructor(errorDetails: CoinbaseErrorDetails, statusCode: number, response: Response) { + super(errorDetails.message); this.name = 'CoinbaseError'; this.statusCode = statusCode; this.response = response; + this.type = errorDetails.type; + this.details = errorDetails.details; + this.suggestion = errorDetails.suggestion; + } +} + +function parseErrorResponse(responseText: string): Record { + try { + return JSON.parse(responseText); + } catch { + return {}; + } +} + +function getErrorDetails(response: Response, responseText: string): CoinbaseErrorDetails { + const parsedError = parseErrorResponse(responseText); + const status = response.status; + + // Authentication errors + if (status === 401) { + return { + type: CoinbaseErrorType.AUTHENTICATION, + message: 'Invalid API credentials', + suggestion: 'Please verify your API key and secret are correct and not expired.' + }; + } + + // Permission errors + if (status === 403) { + if (responseText.includes('"error_details":"Missing required scopes"')) { + return { + type: CoinbaseErrorType.PERMISSION, + message: 'Missing required API permissions', + suggestion: 'Please verify your API key has the necessary permissions enabled in your Coinbase account settings.' + }; + } + return { + type: CoinbaseErrorType.PERMISSION, + message: 'Access denied', + suggestion: 'Please check if you have the necessary permissions to perform this action.' + }; + } + + // Validation errors + if (status === 400) { + return { + type: CoinbaseErrorType.VALIDATION, + message: parsedError.message || 'Invalid request parameters', + details: parsedError, + suggestion: 'Please verify all required parameters are provided and have valid values.' + }; } + + // Rate limit errors + if (status === 429) { + return { + type: CoinbaseErrorType.RATE_LIMIT, + message: 'Rate limit exceeded', + suggestion: 'Please reduce your request frequency or wait before trying again.' + }; + } + + // Server errors + if (status >= 500) { + return { + type: CoinbaseErrorType.SERVER_ERROR, + message: 'Coinbase service error', + suggestion: 'This is a temporary issue with Coinbase. Please try again later.' + }; + } + + // Default unknown error + return { + type: CoinbaseErrorType.UNKNOWN, + message: `Unexpected error: ${response.statusText}`, + details: parsedError, + suggestion: 'If this persists, please contact team with the error details.' + }; } export function handleException( @@ -17,20 +115,9 @@ export function handleException( responseText: string, reason: string ) { - let message: string | undefined; - - if ( - (400 <= response.status && response.status <= 499) || - (500 <= response.status && response.status <= 599) - ) { - if ( - response.status == 403 && - responseText.includes('"error_details":"Missing required scopes"') - ) { - message = `${response.status} Coinbase Error: Missing Required Scopes. Please verify your API keys include the necessary permissions.`; - } else - message = `${response.status} Coinbase Error: ${reason} ${responseText}`; - - throw new CoinbaseError(message, response.status, response); + if ((400 <= response.status && response.status <= 499) || + (500 <= response.status && response.status <= 599)) { + const errorDetails = getErrorDetails(response, responseText); + throw new CoinbaseError(errorDetails, response.status, response); } } diff --git a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts index 9084df56123..9a6ec3b49bd 100644 --- a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts +++ b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts @@ -3,6 +3,7 @@ import fetch, { Headers, RequestInit, Response } from 'node-fetch'; import { BASE_URL, USER_AGENT } from '../constants'; import { RequestOptions } from './types/request-types'; import { handleException } from './errors'; +import { CoinbaseError, CoinbaseErrorType } from './errors'; export class RESTBase { private apiKey: string | undefined; @@ -60,11 +61,33 @@ export class RESTBase { requestOptions: RequestInit, url: string ) { - const response: Response = await fetch(url, requestOptions); - const responseText = await response.text(); - handleException(response, responseText, response.statusText); - - return responseText; + try { + const response: Response = await fetch(url, requestOptions); + const responseText = await response.text(); + + // Handle API errors + handleException(response, responseText, response.statusText); + + // Parse successful response + try { + return JSON.parse(responseText); + } catch { + // If response is not JSON, return raw text + return responseText; + } + } catch (error) { + if (error instanceof CoinbaseError) { + // Re-throw Coinbase specific errors + throw error; + } + // Handle network or other errors + throw new CoinbaseError({ + type: CoinbaseErrorType.NETWORK_ERROR, + message: 'Failed to connect to Coinbase', + details: { originalError: error }, + suggestion: 'Please check your internet connection and try again.' + }, 0, new Response()); + } } setHeaders(httpMethod: string, urlPath: string, isPublic?: boolean) { From fb6f3e186fe968f8a78e5ca3c39aae81abddba48 Mon Sep 17 00:00:00 2001 From: ai16z-demirix Date: Fri, 3 Jan 2025 23:23:25 +0100 Subject: [PATCH 2/4] test: adding more improved tests for providers, memory and goals. --- packages/core/src/tests/goals.test.ts | 261 +++++++++++++++++++++- packages/core/src/tests/memory.test.ts | 132 +++++++++++ packages/core/src/tests/providers.test.ts | 64 ++++++ 3 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/tests/memory.test.ts diff --git a/packages/core/src/tests/goals.test.ts b/packages/core/src/tests/goals.test.ts index f66461cc863..180b0aa3377 100644 --- a/packages/core/src/tests/goals.test.ts +++ b/packages/core/src/tests/goals.test.ts @@ -247,8 +247,15 @@ const sampleGoal: Goal = { }; describe("getGoals", () => { + let runtime: IAgentRuntime; + beforeEach(() => { - vi.clearAllMocks(); + runtime = { + agentId: "test-agent-id" as UUID, + databaseAdapter: { + getGoals: vi.fn().mockResolvedValue([]), + } as any, + } as IAgentRuntime; }); it("retrieves goals successfully", async () => { @@ -274,6 +281,26 @@ describe("getGoals", () => { }) ).rejects.toThrow("Failed to retrieve goals"); }); + + it("should handle empty goals list", async () => { + const mockRuntime = { + agentId: "test-agent-id" as UUID, + databaseAdapter: { + getGoals: vi.fn().mockResolvedValue([]), + }, + } as unknown as IAgentRuntime; + + const roomId = "test-room" as UUID; + + await getGoals({ runtime: mockRuntime, roomId }); + + expect(mockRuntime.databaseAdapter.getGoals).toHaveBeenCalledWith({ + agentId: "test-agent-id", + roomId, + onlyInProgress: true, + count: 5, + }); + }); }); describe("formatGoalsAsString", () => { @@ -292,6 +319,53 @@ describe("formatGoalsAsString", () => { const formatted = formatGoalsAsString({ goals: [] }); expect(formatted).toBe(""); }); + + it("should format goals as string correctly", () => { + const goals: Goal[] = [ + { + id: "1" as UUID, + name: "Goal 1", + status: GoalStatus.IN_PROGRESS, + objectives: [ + { + id: "obj1" as UUID, + description: "Objective 1", + completed: true, + }, + { + id: "obj2" as UUID, + description: "Objective 2", + completed: false, + }, + ], + roomId: "test-room" as UUID, + userId: "test-user" as UUID, + }, + { + id: "2" as UUID, + name: "Goal 2", + status: GoalStatus.DONE, + objectives: [ + { + id: "obj3" as UUID, + description: "Objective 3", + completed: true, + }, + ], + roomId: "test-room" as UUID, + userId: "test-user" as UUID, + }, + ]; + + const formattedGoals = formatGoalsAsString({ goals }); + expect(formattedGoals).toContain("Goal: Goal 1"); + expect(formattedGoals).toContain("id: 1"); + expect(formattedGoals).toContain("- [x] Objective 1 (DONE)"); + expect(formattedGoals).toContain("- [ ] Objective 2 (IN PROGRESS)"); + expect(formattedGoals).toContain("Goal: Goal 2"); + expect(formattedGoals).toContain("id: 2"); + expect(formattedGoals).toContain("- [x] Objective 3 (DONE)"); + }); }); describe("updateGoal", () => { @@ -318,6 +392,138 @@ describe("updateGoal", () => { updateGoal({ runtime: mockRuntime, goal: sampleGoal }) ).rejects.toThrow("Failed to update goal"); }); + + it("should update goal status correctly", async () => { + const goalId = "test-goal" as UUID; + const mockRuntime = { + databaseAdapter: { updateGoal: vi.fn() }, + agentId: "test-agent-id" as UUID, + } as unknown as IAgentRuntime; + + const updatedGoal: Goal = { + id: goalId, + name: "Test Goal", + objectives: [ + { + description: "Objective 1", + completed: false, + }, + { + description: "Objective 2", + completed: true, + }, + ], + roomId: "room-id" as UUID, + userId: "user-id" as UUID, + status: GoalStatus.DONE, + }; + + await updateGoal({ + runtime: mockRuntime, + goal: updatedGoal, + }); + + expect(mockRuntime.databaseAdapter.updateGoal).toHaveBeenCalledWith(updatedGoal); + }); + + it("should handle failed goal update", async () => { + const goalId = "test-goal" as UUID; + const mockRuntime = { + databaseAdapter: { updateGoal: vi.fn() }, + agentId: "test-agent-id" as UUID, + } as unknown as IAgentRuntime; + + const updatedGoal: Goal = { + id: goalId, + name: "Test Goal", + objectives: [ + { + description: "Objective 1", + completed: false, + }, + { + description: "Objective 2", + completed: true, + }, + ], + roomId: "room-id" as UUID, + userId: "user-id" as UUID, + status: GoalStatus.FAILED, + }; + + await updateGoal({ + runtime: mockRuntime, + goal: updatedGoal, + }); + + expect(mockRuntime.databaseAdapter.updateGoal).toHaveBeenCalledWith(updatedGoal); + }); + + it("should handle in-progress goal update", async () => { + const goalId = "test-goal" as UUID; + const mockRuntime = { + databaseAdapter: { updateGoal: vi.fn() }, + agentId: "test-agent-id" as UUID, + } as unknown as IAgentRuntime; + + const updatedGoal: Goal = { + id: goalId, + name: "Test Goal", + objectives: [ + { + description: "Objective 1", + completed: false, + }, + { + description: "Objective 2", + completed: true, + }, + ], + roomId: "room-id" as UUID, + userId: "user-id" as UUID, + status: GoalStatus.IN_PROGRESS, + }; + + await updateGoal({ + runtime: mockRuntime, + goal: updatedGoal, + }); + + expect(mockRuntime.databaseAdapter.updateGoal).toHaveBeenCalledWith(updatedGoal); + }); + + it("should handle goal priority updates", async () => { + const goalId = "test-goal" as UUID; + const mockRuntime = { + databaseAdapter: { updateGoal: vi.fn() }, + agentId: "test-agent-id" as UUID, + } as unknown as IAgentRuntime; + + const updatedGoal: Goal = { + id: goalId, + name: "Test Goal", + objectives: [ + { + description: "Objective 1", + completed: false, + }, + { + description: "Objective 2", + completed: true, + }, + ], + roomId: "room-id" as UUID, + userId: "user-id" as UUID, + status: GoalStatus.IN_PROGRESS, + }; + + await updateGoal({ + runtime: mockRuntime, + goal: updatedGoal, + }); + + expect(mockRuntime.databaseAdapter.updateGoal).toHaveBeenCalledWith(updatedGoal); + }); }); describe("createGoal", () => { @@ -344,4 +550,57 @@ describe("createGoal", () => { createGoal({ runtime: mockRuntime, goal: sampleGoal }) ).rejects.toThrow("Failed to create goal"); }); + + it("should create new goal with correct properties", async () => { + const newGoal: Goal = { + name: "New Goal", + roomId: "room-id" as UUID, + userId: "user-id" as UUID, + status: GoalStatus.IN_PROGRESS, + objectives: [] + }; + + const mockRuntime = { + databaseAdapter: { createGoal: vi.fn() }, + agentId: "test-agent-id" as UUID, + } as unknown as IAgentRuntime; + + await createGoal({ + runtime: mockRuntime, + goal: newGoal, + }); + + expect(mockRuntime.databaseAdapter.createGoal).toHaveBeenCalledWith( + expect.objectContaining({ + name: "New Goal", + roomId: "room-id", + userId: "user-id", + status: GoalStatus.IN_PROGRESS, + objectives: [] + }) + ); + }); + + it("should create a new goal", async () => { + const mockRuntime = { + databaseAdapter: { createGoal: vi.fn() }, + agentId: "test-agent-id" as UUID, + } as unknown as IAgentRuntime; + + const newGoal = { + id: "new-goal" as UUID, + name: "New Goal", + objectives: [], + roomId: "test-room" as UUID, + userId: "test-user" as UUID, + status: GoalStatus.IN_PROGRESS, + }; + + await createGoal({ + runtime: mockRuntime, + goal: newGoal, + }); + + expect(mockRuntime.databaseAdapter.createGoal).toHaveBeenCalledWith(newGoal); + }); }); diff --git a/packages/core/src/tests/memory.test.ts b/packages/core/src/tests/memory.test.ts new file mode 100644 index 00000000000..d146cebf7f2 --- /dev/null +++ b/packages/core/src/tests/memory.test.ts @@ -0,0 +1,132 @@ +import { MemoryManager } from "../memory"; +import { CacheManager, MemoryCacheAdapter } from "../cache"; +import { describe, expect, it, vi, beforeEach } from "vitest"; +import { IAgentRuntime, Memory, UUID } from "../types"; + +describe("MemoryManager", () => { + let memoryManager: MemoryManager; + let mockDatabaseAdapter: any; + let mockRuntime: IAgentRuntime; + + beforeEach(() => { + mockDatabaseAdapter = { + getMemories: vi.fn(), + createMemory: vi.fn(), + removeMemory: vi.fn(), + removeAllMemories: vi.fn(), + countMemories: vi.fn(), + getCachedEmbeddings: vi.fn(), + searchMemories: vi.fn(), + getMemoriesByRoomIds: vi.fn(), + getMemoryById: vi.fn(), + }; + + mockRuntime = { + databaseAdapter: mockDatabaseAdapter, + cacheManager: new CacheManager(new MemoryCacheAdapter()), + agentId: "test-agent-id" as UUID, + } as unknown as IAgentRuntime; + + memoryManager = new MemoryManager({ + tableName: "test_memories", + runtime: mockRuntime, + }); + }); + + describe("addEmbeddingToMemory", () => { + it("should preserve existing embedding if present", async () => { + const existingEmbedding = [0.1, 0.2, 0.3]; + const memory: Memory = { + id: "test-id" as UUID, + userId: "user-id" as UUID, + agentId: "agent-id" as UUID, + roomId: "room-id" as UUID, + content: { text: "test content" }, + embedding: existingEmbedding, + }; + + const result = await memoryManager.addEmbeddingToMemory(memory); + expect(result.embedding).toBe(existingEmbedding); + }); + + it("should throw error for empty content", async () => { + const memory: Memory = { + id: "test-id" as UUID, + userId: "user-id" as UUID, + agentId: "agent-id" as UUID, + roomId: "room-id" as UUID, + content: { text: "" }, + }; + + await expect(memoryManager.addEmbeddingToMemory(memory)).rejects.toThrow( + "Cannot generate embedding: Memory content is empty" + ); + }); + }); + + describe("searchMemoriesByEmbedding", () => { + it("should use default threshold and count when not provided", async () => { + const embedding = [0.1, 0.2, 0.3]; + const roomId = "test-room" as UUID; + + mockDatabaseAdapter.searchMemories = vi.fn().mockResolvedValue([]); + + await memoryManager.searchMemoriesByEmbedding(embedding, { roomId }); + + expect(mockDatabaseAdapter.searchMemories).toHaveBeenCalledWith({ + embedding, + match_threshold: 0.1, + match_count: 10, + roomId, + tableName: "test_memories", + agentId: "test-agent-id", + unique: false, + }); + }); + + it("should respect custom threshold and count", async () => { + const embedding = [0.1, 0.2, 0.3]; + const roomId = "test-room" as UUID; + const match_threshold = 0.5; + const count = 5; + + mockDatabaseAdapter.searchMemories = vi.fn().mockResolvedValue([]); + + await memoryManager.searchMemoriesByEmbedding(embedding, { + roomId, + match_threshold, + count, + }); + + expect(mockDatabaseAdapter.searchMemories).toHaveBeenCalledWith({ + embedding, + match_threshold, + match_count: count, + roomId, + tableName: "test_memories", + agentId: "test-agent-id", + unique: false, + }); + }); + }); + + describe("getMemories", () => { + it("should handle pagination parameters", async () => { + const roomId = "test-room" as UUID; + const start = 0; + const end = 5; + + await memoryManager.getMemories({ roomId, start, end }); + + expect(mockDatabaseAdapter.getMemories).toHaveBeenCalledWith({ + roomId, + count: 10, + unique: true, + tableName: "test_memories", + agentId: "test-agent-id", + start: 0, + end: 5, + }); + }); + }); +}); diff --git a/packages/core/src/tests/providers.test.ts b/packages/core/src/tests/providers.test.ts index 62171f631a3..152cac053b0 100644 --- a/packages/core/src/tests/providers.test.ts +++ b/packages/core/src/tests/providers.test.ts @@ -172,4 +172,68 @@ describe("getProviders", () => { getProviders(runtime, message, {} as State) ).rejects.toThrow("Provider error"); }); + + it("should handle empty provider list", async () => { + runtime.providers = []; + const message: Memory = { + userId: "00000000-0000-0000-0000-000000000001", + content: { text: "" }, + roomId, + agentId: "00000000-0000-0000-0000-000000000002", + }; + + const responses = await getProviders(runtime, message); + expect(responses).toBe(""); + }); + + it("should filter out null and undefined responses", async () => { + const MockProviderWithMixedResponses: Provider = { + get: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state?: State + ) => { + return null; + }, + }; + + runtime.providers = [ + MockProvider1, + MockProviderWithMixedResponses, + MockProvider2, + ]; + + const message: Memory = { + userId: "00000000-0000-0000-0000-000000000001", + content: { text: "" }, + roomId, + agentId: "00000000-0000-0000-0000-000000000002", + }; + + const responses = await getProviders(runtime, message); + expect(responses).toBe("Response from Provider 1\nResponse from Provider 2"); + }); + + it("should handle provider throwing an error", async () => { + const MockProviderWithError: Provider = { + get: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state?: State + ) => { + throw new Error("Provider error"); + }, + }; + + runtime.providers = [MockProvider1, MockProviderWithError, MockProvider2]; + + const message: Memory = { + userId: "00000000-0000-0000-0000-000000000001", + content: { text: "" }, + roomId, + agentId: "00000000-0000-0000-0000-000000000002", + }; + + await expect(getProviders(runtime, message)).rejects.toThrow("Provider error"); + }); }); From af5569456bb2950726bd257fb8f244598290ee90 Mon Sep 17 00:00:00 2001 From: ai16z-demirix Date: Sat, 4 Jan 2025 23:48:58 +0100 Subject: [PATCH 3/4] Revert "refactor: (draft) improving API error handling for coinbase integration" This reverts commit 3a9e84e538b9253d567aa32bde3bb9d7b67ba274. --- .../advanced-sdk-ts/src/rest/errors.ts | 127 +++--------------- .../advanced-sdk-ts/src/rest/rest-base.ts | 33 +---- 2 files changed, 25 insertions(+), 135 deletions(-) diff --git a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts index 3c3d1365f93..5d9695ef67d 100644 --- a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts +++ b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/errors.ts @@ -1,113 +1,15 @@ import { Response } from 'node-fetch'; -// Define specific error types for different scenarios -export enum CoinbaseErrorType { - AUTHENTICATION = 'AUTHENTICATION', - PERMISSION = 'PERMISSION', - VALIDATION = 'VALIDATION', - RATE_LIMIT = 'RATE_LIMIT', - SERVER_ERROR = 'SERVER_ERROR', - NETWORK_ERROR = 'NETWORK_ERROR', - UNKNOWN = 'UNKNOWN' -} - -export interface CoinbaseErrorDetails { - type: CoinbaseErrorType; - message: string; - details?: Record; - suggestion?: string; -} +class CoinbaseError extends Error { + statusCode: number; + response: Response; -export class CoinbaseError extends Error { - readonly statusCode: number; - readonly response: Response; - readonly type: CoinbaseErrorType; - readonly details?: Record; - readonly suggestion?: string; - - constructor(errorDetails: CoinbaseErrorDetails, statusCode: number, response: Response) { - super(errorDetails.message); + constructor(message: string, statusCode: number, response: Response) { + super(message); this.name = 'CoinbaseError'; this.statusCode = statusCode; this.response = response; - this.type = errorDetails.type; - this.details = errorDetails.details; - this.suggestion = errorDetails.suggestion; - } -} - -function parseErrorResponse(responseText: string): Record { - try { - return JSON.parse(responseText); - } catch { - return {}; - } -} - -function getErrorDetails(response: Response, responseText: string): CoinbaseErrorDetails { - const parsedError = parseErrorResponse(responseText); - const status = response.status; - - // Authentication errors - if (status === 401) { - return { - type: CoinbaseErrorType.AUTHENTICATION, - message: 'Invalid API credentials', - suggestion: 'Please verify your API key and secret are correct and not expired.' - }; - } - - // Permission errors - if (status === 403) { - if (responseText.includes('"error_details":"Missing required scopes"')) { - return { - type: CoinbaseErrorType.PERMISSION, - message: 'Missing required API permissions', - suggestion: 'Please verify your API key has the necessary permissions enabled in your Coinbase account settings.' - }; - } - return { - type: CoinbaseErrorType.PERMISSION, - message: 'Access denied', - suggestion: 'Please check if you have the necessary permissions to perform this action.' - }; - } - - // Validation errors - if (status === 400) { - return { - type: CoinbaseErrorType.VALIDATION, - message: parsedError.message || 'Invalid request parameters', - details: parsedError, - suggestion: 'Please verify all required parameters are provided and have valid values.' - }; } - - // Rate limit errors - if (status === 429) { - return { - type: CoinbaseErrorType.RATE_LIMIT, - message: 'Rate limit exceeded', - suggestion: 'Please reduce your request frequency or wait before trying again.' - }; - } - - // Server errors - if (status >= 500) { - return { - type: CoinbaseErrorType.SERVER_ERROR, - message: 'Coinbase service error', - suggestion: 'This is a temporary issue with Coinbase. Please try again later.' - }; - } - - // Default unknown error - return { - type: CoinbaseErrorType.UNKNOWN, - message: `Unexpected error: ${response.statusText}`, - details: parsedError, - suggestion: 'If this persists, please contact team with the error details.' - }; } export function handleException( @@ -115,9 +17,20 @@ export function handleException( responseText: string, reason: string ) { - if ((400 <= response.status && response.status <= 499) || - (500 <= response.status && response.status <= 599)) { - const errorDetails = getErrorDetails(response, responseText); - throw new CoinbaseError(errorDetails, response.status, response); + let message: string | undefined; + + if ( + (400 <= response.status && response.status <= 499) || + (500 <= response.status && response.status <= 599) + ) { + if ( + response.status == 403 && + responseText.includes('"error_details":"Missing required scopes"') + ) { + message = `${response.status} Coinbase Error: Missing Required Scopes. Please verify your API keys include the necessary permissions.`; + } else + message = `${response.status} Coinbase Error: ${reason} ${responseText}`; + + throw new CoinbaseError(message, response.status, response); } } diff --git a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts index 9a6ec3b49bd..9084df56123 100644 --- a/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts +++ b/packages/plugin-coinbase/advanced-sdk-ts/src/rest/rest-base.ts @@ -3,7 +3,6 @@ import fetch, { Headers, RequestInit, Response } from 'node-fetch'; import { BASE_URL, USER_AGENT } from '../constants'; import { RequestOptions } from './types/request-types'; import { handleException } from './errors'; -import { CoinbaseError, CoinbaseErrorType } from './errors'; export class RESTBase { private apiKey: string | undefined; @@ -61,33 +60,11 @@ export class RESTBase { requestOptions: RequestInit, url: string ) { - try { - const response: Response = await fetch(url, requestOptions); - const responseText = await response.text(); - - // Handle API errors - handleException(response, responseText, response.statusText); - - // Parse successful response - try { - return JSON.parse(responseText); - } catch { - // If response is not JSON, return raw text - return responseText; - } - } catch (error) { - if (error instanceof CoinbaseError) { - // Re-throw Coinbase specific errors - throw error; - } - // Handle network or other errors - throw new CoinbaseError({ - type: CoinbaseErrorType.NETWORK_ERROR, - message: 'Failed to connect to Coinbase', - details: { originalError: error }, - suggestion: 'Please check your internet connection and try again.' - }, 0, new Response()); - } + const response: Response = await fetch(url, requestOptions); + const responseText = await response.text(); + handleException(response, responseText, response.statusText); + + return responseText; } setHeaders(httpMethod: string, urlPath: string, isPublic?: boolean) { From d4261574fc511fdad8abfdac8dce64daa303e7a5 Mon Sep 17 00:00:00 2001 From: ai16z-demirix Date: Sun, 5 Jan 2025 00:00:08 +0100 Subject: [PATCH 4/4] test: fixing generation.test.ts. Adding tests for memory, goals and provider --- packages/core/src/tests/generation.test.ts | 50 ++++++++++++++-------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/core/src/tests/generation.test.ts b/packages/core/src/tests/generation.test.ts index f1ec8f9bc69..fe8f035913d 100644 --- a/packages/core/src/tests/generation.test.ts +++ b/packages/core/src/tests/generation.test.ts @@ -7,7 +7,6 @@ import { splitChunks, trimTokens, } from "../generation"; -import type { TiktokenModel } from "js-tiktoken"; // Mock the elizaLogger vi.mock("../index.ts", () => ({ @@ -130,30 +129,45 @@ describe("Generation", () => { }); describe("trimTokens", () => { - const model = "gpt-4" as TiktokenModel; + let mockRuntime: IAgentRuntime; + + beforeEach(() => { + mockRuntime = { + getSetting: vi.fn().mockImplementation((key: string) => { + switch (key) { + case "TOKENIZER_MODEL": + return "gpt-4"; + case "TOKENIZER_TYPE": + return "tiktoken"; + default: + return undefined; + } + }), + } as unknown as IAgentRuntime; + }); - it("should return empty string for empty input", () => { - const result = trimTokens("", 100, model); + it("should return empty string for empty input", async () => { + const result = await trimTokens("", 100, mockRuntime); expect(result).toBe(""); }); - it("should throw error for negative maxTokens", () => { - expect(() => trimTokens("test", -1, model)).toThrow( + it("should throw error for negative maxTokens", async () => { + await expect(trimTokens("test", -1, mockRuntime)).rejects.toThrow( "maxTokens must be positive" ); }); - it("should return unchanged text if within token limit", () => { + it("should return unchanged text if within token limit", async () => { const shortText = "This is a short text"; - const result = trimTokens(shortText, 10, model); + const result = await trimTokens(shortText, 10, mockRuntime); expect(result).toBe(shortText); }); - it("should truncate text to specified token limit", () => { + it("should truncate text to specified token limit", async () => { // Using a longer text that we know will exceed the token limit const longText = "This is a much longer text that will definitely exceed our very small token limit and need to be truncated to fit within the specified constraints."; - const result = trimTokens(longText, 5, model); + const result = await trimTokens(longText, 5, mockRuntime); // The exact result will depend on the tokenizer, but we can verify: // 1. Result is shorter than original @@ -164,19 +178,19 @@ describe("Generation", () => { expect(longText.includes(result)).toBe(true); }); - it("should handle non-ASCII characters", () => { + it("should handle non-ASCII characters", async () => { const unicodeText = "Hello 👋 World 🌍"; - const result = trimTokens(unicodeText, 5, model); + const result = await trimTokens(unicodeText, 5, mockRuntime); expect(result.length).toBeGreaterThan(0); }); - it("should handle multiline text", () => { + it("should handle multiline text", async () => { const multilineText = `Line 1 - Line 2 - Line 3 - Line 4 - Line 5`; - const result = trimTokens(multilineText, 5, model); +Line 2 +Line 3 +Line 4 +Line 5`; + const result = await trimTokens(multilineText, 5, mockRuntime); expect(result.length).toBeGreaterThan(0); expect(result.length).toBeLessThan(multilineText.length); });