diff --git a/CLAUDE.md b/CLAUDE.md index 8c1ba97f3..23a2a2cb8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -369,6 +369,29 @@ When changing how messages are constructed: 4. Add tests for new message types 5. Update type definitions in `src/lib/types/conversation.ts` +### Embeddings + +Providers expose `embed()` and `embedMany()` methods for generating vector embeddings. The `AIProvider` type interface includes both methods; unsupported providers throw descriptive errors. + +**Supported providers and defaults:** + +| Provider | Default Model | Env Override | +| ---------------- | ------------------------------ | --------------------------- | +| OpenAI | `text-embedding-3-small` | — | +| Google AI Studio | `gemini-embedding-001` | `GOOGLE_AI_EMBEDDING_MODEL` | +| Google Vertex | `text-embedding-004` | `VERTEX_EMBEDDING_MODEL` | +| Amazon Bedrock | `amazon.titan-embed-text-v2:0` | — | + +**Server routes:** `POST /api/agent/embed` (single) and `POST /api/agent/embed-many` (batch) in `src/lib/server/routes/agentRoutes.ts`. + +**Key files:** + +- `src/lib/core/baseProvider.ts` — Base `embed()` / `embedMany()` stubs +- `src/lib/types/providers.ts` — `AIProvider` type with embedding methods +- `src/lib/server/routes/agentRoutes.ts` — Server embedding endpoints +- `src/lib/server/utils/validation.ts` — `EmbedRequestSchema` / `EmbedManyRequestSchema` +- `src/lib/server/types.ts` — `EmbedRequest`, `EmbedResponse`, `EmbedManyRequest`, `EmbedManyResponse` + ### Working with Multimodal Content **For images:** diff --git a/docs/advanced/api-reference.md b/docs/advanced/api-reference.md index 5b0a7ce8f..dc2534ef2 100644 --- a/docs/advanced/api-reference.md +++ b/docs/advanced/api-reference.md @@ -22,6 +22,63 @@ POST /api/stream GET /api/status ``` +## Embeddings + +### Generate Embedding + +```http +POST /api/agent/embed +``` + +**Request body:** + +```json +{ + "text": "Hello world", + "provider": "googleAiStudio", + "model": "gemini-embedding-001" +} +``` + +**Response:** + +```json +{ + "embedding": [0.123, -0.456, ...], + "provider": "googleAiStudio", + "model": "gemini-embedding-001", + "dimension": 768 +} +``` + +### Generate Batch Embeddings + +```http +POST /api/agent/embed-many +``` + +**Request body:** + +```json +{ + "texts": ["First document", "Second document", "Third document"], + "provider": "openai", + "model": "text-embedding-3-small" +} +``` + +**Response:** + +```json +{ + "embeddings": [[0.123, -0.456, ...], [0.789, -0.012, ...], [0.345, -0.678, ...]], + "provider": "openai", + "model": "text-embedding-3-small", + "count": 3, + "dimension": 1536 +} +``` + ## MCP Integration ### List MCP Tools diff --git a/docs/api/type-aliases/AIProvider.md b/docs/api/type-aliases/AIProvider.md index ee01798e5..31966ba85 100644 --- a/docs/api/type-aliases/AIProvider.md +++ b/docs/api/type-aliases/AIProvider.md @@ -80,6 +80,50 @@ Defined in: [types/providers.ts:308](https://github.com/juspay/neurolink/blob/1b --- +### embed() + +> **embed**(`text`, `modelName?`): `Promise`\<`number[]`\> + +Generate an embedding vector for a single text. Throws if the provider does not support embeddings. + +#### Parameters + +##### text + +`string` + +##### modelName? + +`string` + +#### Returns + +`Promise`\<`number[]`\> + +--- + +### embedMany() + +> **embedMany**(`texts`, `modelName?`): `Promise`\<`number[][]`\> + +Generate embedding vectors for multiple texts in a single batch. The AI SDK automatically handles chunking for models with batch limits. + +#### Parameters + +##### texts + +`string[]` + +##### modelName? + +`string` + +#### Returns + +`Promise`\<`number[][]`\> + +--- + ### setupToolExecutor() > **setupToolExecutor**(`sdk`, `functionTag`): `void` diff --git a/docs/guides/server-adapters/index.md b/docs/guides/server-adapters/index.md index 56a0fe46d..4d9e99ab7 100644 --- a/docs/guides/server-adapters/index.md +++ b/docs/guides/server-adapters/index.md @@ -147,11 +147,13 @@ All server adapters expose the same REST API endpoints: ### Agent Operations -| Endpoint | Method | Description | -| ---------------------- | ------ | -------------------------------------- | -| `/api/agent/execute` | POST | Execute agent and return full response | -| `/api/agent/stream` | POST | Stream agent response via SSE | -| `/api/agent/providers` | GET | List available AI providers | +| Endpoint | Method | Description | +| ----------------------- | ------ | -------------------------------------------- | +| `/api/agent/execute` | POST | Execute agent and return full response | +| `/api/agent/stream` | POST | Stream agent response via SSE | +| `/api/agent/providers` | GET | List available AI providers | +| `/api/agent/embed` | POST | Generate embedding for a single text | +| `/api/agent/embed-many` | POST | Generate embeddings for multiple texts batch | ### Tool Operations @@ -433,6 +435,59 @@ data: {"type":"text-end","timestamp":1706745600100} data: {"type":"finish","usage":{"inputTokens":5,"outputTokens":50,"totalTokens":55}} ``` +### Generate Embedding + +**Request:** + +```json +POST /api/agent/embed +Content-Type: application/json + +{ + "text": "What is the meaning of life?", + "provider": "openai", + "model": "text-embedding-3-small" +} +``` + +**Response:** + +```json +{ + "embedding": [0.123, -0.456, 0.789, ...], + "provider": "openai", + "model": "text-embedding-3-small", + "dimension": 1536 +} +``` + +### Generate Batch Embeddings + +**Request:** + +```json +POST /api/agent/embed-many +Content-Type: application/json + +{ + "texts": ["First document", "Second document"], + "provider": "googleAiStudio", + "model": "gemini-embedding-001" +} +``` + +**Response:** + +```json +{ + "embeddings": [[0.123, -0.456, ...], [0.789, -0.012, ...]], + "provider": "googleAiStudio", + "model": "gemini-embedding-001", + "count": 2, + "dimension": 768 +} +``` + --- ## Production Deployment diff --git a/docs/sdk/api-reference.md b/docs/sdk/api-reference.md index fdd06d7d9..fbe32f49e 100644 --- a/docs/sdk/api-reference.md +++ b/docs/sdk/api-reference.md @@ -454,6 +454,47 @@ const result = await neurolink.gen({ --- +### Embeddings + +Generate embeddings directly via the provider's `embed()` and `embedMany()` methods. + +#### `provider.embed(text, modelName?)` + +Generate an embedding vector for a single text. + +```typescript +import { ProviderFactory } from "@juspay/neurolink"; + +const provider = await ProviderFactory.createProvider("googleAiStudio"); +const embedding = await provider.embed("Hello world"); +// embedding: number[] (e.g., 768 dimensions) +``` + +#### `provider.embedMany(texts, modelName?)` + +Generate embedding vectors for multiple texts in a single batch. The AI SDK automatically handles chunking for models with batch limits. + +```typescript +const provider = await ProviderFactory.createProvider("openai"); +const embeddings = await provider.embedMany([ + "First document", + "Second document", + "Third document", +]); +// embeddings: number[][] (e.g., 3 × 1536 dimensions) +``` + +**Supported providers and default models:** + +| Provider | Default Embedding Model | Env Override | +| ---------------- | ------------------------------ | --------------------------- | +| OpenAI | `text-embedding-3-small` | — | +| Google AI Studio | `gemini-embedding-001` | `GOOGLE_AI_EMBEDDING_MODEL` | +| Google Vertex | `text-embedding-004` | `VERTEX_EMBEDDING_MODEL` | +| Amazon Bedrock | `amazon.titan-embed-text-v2:0` | — | + +--- + ### RAG Integration Pass `rag: { files: [...] }` to `generate()` or `stream()` for automatic RAG pipeline setup: diff --git a/src/lib/core/baseProvider.ts b/src/lib/core/baseProvider.ts index ca80b23fe..f2a28fae4 100644 --- a/src/lib/core/baseProvider.ts +++ b/src/lib/core/baseProvider.ts @@ -1081,6 +1081,33 @@ export abstract class BaseProvider implements AIProvider { ); } + /** + * Generate embeddings for multiple texts in a single batch + * + * This is a default implementation that throws an error. + * Providers that support embeddings should override this method. + * The AI SDK's embedMany automatically handles chunking for models with batch limits. + * + * @param texts - The texts to embed + * @param _modelName - Optional embedding model name (provider-specific) + * @returns Promise resolving to an array of embedding vectors + * @throws Error if the provider does not support embeddings + */ + async embedMany(texts: string[], _modelName?: string): Promise { + logger.warn( + `embedMany() called on ${this.providerName} which does not have a native implementation`, + { + count: texts.length, + }, + ); + throw new Error( + `Batch embedding generation is not supported by the ${this.providerName} provider. ` + + `Supported providers: openai, googleAiStudio, vertex/google, bedrock. ` + + `Use an embedding model like text-embedding-3-small (OpenAI), gemini-embedding-001 (Google AI), ` + + `text-embedding-004 (Vertex), or amazon.titan-embed-text-v2:0 (Bedrock).`, + ); + } + /** * Get the default embedding model for this provider * diff --git a/src/lib/providers/amazonBedrock.ts b/src/lib/providers/amazonBedrock.ts index 529eb5a74..926f7cc96 100644 --- a/src/lib/providers/amazonBedrock.ts +++ b/src/lib/providers/amazonBedrock.ts @@ -2099,4 +2099,43 @@ export class AmazonBedrockProvider extends BaseProvider { throw this.handleProviderError(error); } } + + /** + * Generate embeddings for multiple texts in a single batch + * @param texts - The texts to embed + * @param modelName - The embedding model to use (default: amazon.titan-embed-text-v2:0) + * @returns Promise resolving to an array of embedding vectors + */ + async embedMany(texts: string[], modelName?: string): Promise { + const embeddingModelName = modelName || "amazon.titan-embed-text-v2:0"; + + logger.debug("Generating batch embeddings", { + provider: this.providerName, + model: embeddingModelName, + count: texts.length, + }); + + try { + const embeddings = await Promise.all( + texts.map((text) => this.embed(text, embeddingModelName)), + ); + + logger.debug("Batch embeddings generated successfully", { + provider: this.providerName, + model: embeddingModelName, + count: embeddings.length, + embeddingDimension: embeddings[0]?.length, + }); + + return embeddings; + } catch (error) { + logger.error("Batch embedding generation failed", { + error: error instanceof Error ? error.message : String(error), + model: embeddingModelName, + count: texts.length, + }); + + throw this.handleProviderError(error); + } + } } diff --git a/src/lib/providers/googleAiStudio.ts b/src/lib/providers/googleAiStudio.ts index c9b1bbec4..9d34cf4d7 100644 --- a/src/lib/providers/googleAiStudio.ts +++ b/src/lib/providers/googleAiStudio.ts @@ -1,5 +1,12 @@ import { createGoogleGenerativeAI } from "@ai-sdk/google"; -import { type LanguageModelV1, type Schema, streamText, type Tool } from "ai"; +import { + embed, + embedMany, + type LanguageModelV1, + type Schema, + streamText, + type Tool, +} from "ai"; import { type AIProviderName, ErrorCategory, @@ -1418,6 +1425,105 @@ export class GoogleAIStudioProvider extends BaseProvider { }; } + protected getDefaultEmbeddingModel(): string { + return ( + process.env.GOOGLE_AI_EMBEDDING_MODEL || + process.env.GOOGLE_EMBEDDING_MODEL || + "gemini-embedding-001" + ); + } + + /** + * Generate embeddings for text using Google AI Studio embedding models + * @param text - The text to embed + * @param modelName - The embedding model to use (default: gemini-embedding-001) + * @returns Promise resolving to the embedding vector + */ + async embed(text: string, modelName?: string): Promise { + const embeddingModelName = + modelName || this.getDefaultEmbeddingModel() || "gemini-embedding-001"; + + logger.debug("Generating embedding", { + provider: this.providerName, + model: embeddingModelName, + textLength: text.length, + }); + + try { + const apiKey = this.getApiKey(); + const google = createGoogleGenerativeAI({ apiKey }); + + const embeddingModel = google.textEmbeddingModel(embeddingModelName); + + const result = await embed({ + model: embeddingModel, + value: text, + }); + + logger.debug("Embedding generated successfully", { + provider: this.providerName, + model: embeddingModelName, + embeddingDimension: result.embedding.length, + }); + + return result.embedding; + } catch (error) { + logger.error("Embedding generation failed", { + error: error instanceof Error ? error.message : String(error), + model: embeddingModelName, + textLength: text.length, + }); + + throw this.handleProviderError(error); + } + } + + /** + * Generate embeddings for multiple texts in a single batch + * @param texts - The texts to embed + * @param modelName - The embedding model to use (default: gemini-embedding-001) + * @returns Promise resolving to an array of embedding vectors + */ + async embedMany(texts: string[], modelName?: string): Promise { + const embeddingModelName = + modelName || this.getDefaultEmbeddingModel() || "gemini-embedding-001"; + + logger.debug("Generating batch embeddings", { + provider: this.providerName, + model: embeddingModelName, + count: texts.length, + }); + + try { + const apiKey = this.getApiKey(); + const google = createGoogleGenerativeAI({ apiKey }); + + const embeddingModel = google.textEmbeddingModel(embeddingModelName); + + const result = await embedMany({ + model: embeddingModel, + values: texts, + }); + + logger.debug("Batch embeddings generated successfully", { + provider: this.providerName, + model: embeddingModelName, + count: result.embeddings.length, + embeddingDimension: result.embeddings[0]?.length, + }); + + return result.embeddings; + } catch (error) { + logger.error("Batch embedding generation failed", { + error: error instanceof Error ? error.message : String(error), + model: embeddingModelName, + count: texts.length, + }); + + throw this.handleProviderError(error); + } + } + private getApiKey(): string { const apiKey = process.env.GOOGLE_AI_API_KEY || process.env.GOOGLE_GENERATIVE_AI_API_KEY; diff --git a/src/lib/providers/googleVertex.ts b/src/lib/providers/googleVertex.ts index 86c03bae5..46a177b00 100644 --- a/src/lib/providers/googleVertex.ts +++ b/src/lib/providers/googleVertex.ts @@ -7,6 +7,8 @@ import { type GoogleVertexAnthropicProviderSettings, } from "@ai-sdk/google-vertex/anthropic"; import { + embed, + embedMany, type LanguageModel, type LanguageModelV1, Output, @@ -3778,9 +3780,6 @@ export class GoogleVertexProvider extends BaseProvider { }); try { - // Create embedding model using the AI SDK - const { embed } = await import("ai"); - // Create the Vertex provider with current settings const vertexSettings = await createVertexSettings(this.location); const vertex = createVertex(vertexSettings); @@ -3812,6 +3811,52 @@ export class GoogleVertexProvider extends BaseProvider { } } + /** + * Generate embeddings for multiple texts in a single batch + * @param texts - The texts to embed + * @param modelName - The embedding model to use (default: text-embedding-004) + * @returns Promise resolving to an array of embedding vectors + */ + async embedMany(texts: string[], modelName?: string): Promise { + const embeddingModelName = + modelName || this.getDefaultEmbeddingModel() || "text-embedding-004"; + + logger.debug("Generating batch embeddings", { + provider: this.providerName, + model: embeddingModelName, + count: texts.length, + }); + + try { + const vertexSettings = await createVertexSettings(this.location); + const vertex = createVertex(vertexSettings); + + const embeddingModel = vertex.textEmbeddingModel(embeddingModelName); + + const result = await embedMany({ + model: embeddingModel, + values: texts, + }); + + logger.debug("Batch embeddings generated successfully", { + provider: this.providerName, + model: embeddingModelName, + count: result.embeddings.length, + embeddingDimension: result.embeddings[0]?.length, + }); + + return result.embeddings; + } catch (error) { + logger.error("Batch embedding generation failed", { + error: error instanceof Error ? error.message : String(error), + model: embeddingModelName, + count: texts.length, + }); + + throw this.handleProviderError(error); + } + } + /** * Get model suggestions when a model is not found */ diff --git a/src/lib/providers/openAI.ts b/src/lib/providers/openAI.ts index 8abe41050..ecd464a73 100644 --- a/src/lib/providers/openAI.ts +++ b/src/lib/providers/openAI.ts @@ -1,5 +1,11 @@ import { createOpenAI } from "@ai-sdk/openai"; -import { type LanguageModelV1, streamText, type Tool } from "ai"; +import { + embed, + embedMany, + type LanguageModelV1, + streamText, + type Tool, +} from "ai"; import { trace, SpanKind, SpanStatusCode } from "@opentelemetry/api"; import { AIProviderName } from "../constants/enums.js"; import { BaseProvider } from "../core/baseProvider.js"; @@ -693,8 +699,6 @@ export class OpenAIProvider extends BaseProvider { try { // Create embedding model using the AI SDK - const { embed } = await import("ai"); - // Create the OpenAI provider const openai = createOpenAI({ apiKey: getOpenAIApiKey(), @@ -727,6 +731,53 @@ export class OpenAIProvider extends BaseProvider { throw this.handleProviderError(error); } } + + /** + * Generate embeddings for multiple texts in a single batch + * @param texts - The texts to embed + * @param modelName - The embedding model to use (default: text-embedding-3-small) + * @returns Promise resolving to an array of embedding vectors + */ + async embedMany(texts: string[], modelName?: string): Promise { + const embeddingModelName = modelName || "text-embedding-3-small"; + + logger.debug("Generating batch embeddings", { + provider: this.providerName, + model: embeddingModelName, + count: texts.length, + }); + + try { + const openai = createOpenAI({ + apiKey: getOpenAIApiKey(), + fetch: createProxyFetch(), + }); + + const embeddingModel = openai.textEmbeddingModel(embeddingModelName); + + const result = await embedMany({ + model: embeddingModel, + values: texts, + }); + + logger.debug("Batch embeddings generated successfully", { + provider: this.providerName, + model: embeddingModelName, + count: result.embeddings.length, + embeddingDimension: result.embeddings[0]?.length, + }); + + return result.embeddings; + } catch (error) { + logger.error("Batch embedding generation failed", { + error: error instanceof Error ? error.message : String(error), + model: embeddingModelName, + count: texts.length, + }); + + throw this.handleProviderError(error); + } + } } // Export for factory registration diff --git a/src/lib/server/routes/agentRoutes.ts b/src/lib/server/routes/agentRoutes.ts index 18b8a5f3f..d5a0ec0c2 100644 --- a/src/lib/server/routes/agentRoutes.ts +++ b/src/lib/server/routes/agentRoutes.ts @@ -7,6 +7,10 @@ import { ProviderFactory } from "../../factories/providerFactory.js"; import type { AgentExecuteRequest, AgentExecuteResponse, + EmbedManyRequest, + EmbedManyResponse, + EmbedRequest, + EmbedResponse, RouteGroup, ServerContext, } from "../types.js"; @@ -14,6 +18,9 @@ import { createStreamRedactor } from "../utils/redaction.js"; import { AgentExecuteRequestSchema, type createErrorResponse, + createErrorResponse as createError, + EmbedManyRequestSchema, + EmbedRequestSchema, validateRequest, } from "../utils/validation.js"; @@ -153,6 +160,106 @@ export function createAgentRoutes(basePath: string = "/api"): RouteGroup { description: "List available AI providers", tags: ["agent", "providers"], }, + { + method: "POST", + path: `${basePath}/agent/embed`, + handler: async ( + ctx: ServerContext, + ): Promise> => { + const validation = validateRequest( + EmbedRequestSchema, + ctx.body, + ctx.requestId, + ); + + if (!validation.success) { + return validation.error; + } + + const request = validation.data as EmbedRequest; + + try { + const providerName = request.provider || "openai"; + const provider = await ProviderFactory.createProvider( + providerName, + request.model, + ); + + const embedding = await provider.embed(request.text, request.model); + + return { + embedding, + provider: providerName, + model: request.model || "default", + dimension: embedding.length, + }; + } catch (error) { + return createError( + "EXECUTION_FAILED", + error instanceof Error + ? error.message + : "Embedding generation failed", + undefined, + ctx.requestId, + ); + } + }, + description: "Generate embedding for a single text", + tags: ["agent", "embeddings"], + }, + { + method: "POST", + path: `${basePath}/agent/embed-many`, + handler: async ( + ctx: ServerContext, + ): Promise< + EmbedManyResponse | ReturnType + > => { + const validation = validateRequest( + EmbedManyRequestSchema, + ctx.body, + ctx.requestId, + ); + + if (!validation.success) { + return validation.error; + } + + const request = validation.data as EmbedManyRequest; + + try { + const providerName = request.provider || "openai"; + const provider = await ProviderFactory.createProvider( + providerName, + request.model, + ); + + const embeddings = await provider.embedMany( + request.texts, + request.model, + ); + + return { + embeddings, + provider: providerName, + model: request.model || "default", + count: embeddings.length, + dimension: embeddings[0]?.length ?? 0, + }; + } catch (error) { + return createError( + "EXECUTION_FAILED", + error instanceof Error + ? error.message + : "Batch embedding generation failed", + undefined, + ctx.requestId, + ); + } + }, + description: "Generate embeddings for multiple texts in a batch", + tags: ["agent", "embeddings"], + }, ], }; } diff --git a/src/lib/server/types.ts b/src/lib/server/types.ts index d704ac016..572f17efd 100644 --- a/src/lib/server/types.ts +++ b/src/lib/server/types.ts @@ -614,6 +614,71 @@ export type AgentExecuteResponse = { metadata?: Record; }; +/** + * Embed request (single text) + */ +export type EmbedRequest = { + /** Text to embed */ + text: string; + + /** Provider to use (optional) */ + provider?: string; + + /** Embedding model to use (optional) */ + model?: string; +}; + +/** + * Embed response (single text) + */ +export type EmbedResponse = { + /** The embedding vector */ + embedding: number[]; + + /** Provider used */ + provider: string; + + /** Model used */ + model: string; + + /** Embedding dimension */ + dimension: number; +}; + +/** + * Embed many request (batch texts) + */ +export type EmbedManyRequest = { + /** Texts to embed */ + texts: string[]; + + /** Provider to use (optional) */ + provider?: string; + + /** Embedding model to use (optional) */ + model?: string; +}; + +/** + * Embed many response (batch texts) + */ +export type EmbedManyResponse = { + /** The embedding vectors */ + embeddings: number[][]; + + /** Provider used */ + provider: string; + + /** Model used */ + model: string; + + /** Number of embeddings */ + count: number; + + /** Embedding dimension */ + dimension: number; +}; + /** * Tool execution request */ diff --git a/src/lib/server/utils/validation.ts b/src/lib/server/utils/validation.ts index e44930453..eeaf33307 100644 --- a/src/lib/server/utils/validation.ts +++ b/src/lib/server/utils/validation.ts @@ -121,6 +121,27 @@ export const SessionMessagesQuerySchema = z.object({ .optional(), }); +/** + * Embed request schema (single text) + */ +export const EmbedRequestSchema = z.object({ + text: z.string().min(1, "Text is required"), + provider: z.string().optional(), + model: z.string().optional(), +}); + +/** + * Embed many request schema (batch texts) + */ +export const EmbedManyRequestSchema = z.object({ + texts: z + .array(z.string().min(1)) + .min(1, "At least one text is required") + .max(2048, "Maximum 2048 texts per batch"), + provider: z.string().optional(), + model: z.string().optional(), +}); + // ============================================ // Error Response Types // ============================================ diff --git a/src/lib/types/providers.ts b/src/lib/types/providers.ts index 3c2fe6b0b..ec562cb66 100644 --- a/src/lib/types/providers.ts +++ b/src/lib/types/providers.ts @@ -514,6 +514,10 @@ export type AIProvider = { analysisSchema?: ValidationSchema, ): Promise; + embed(text: string, modelName?: string): Promise; + + embedMany(texts: string[], modelName?: string): Promise; + // Tool execution setup - consolidated from NeuroLink SDK setupToolExecutor( sdk: { diff --git a/test/unit/server/routes/agentRoutes.test.ts b/test/unit/server/routes/agentRoutes.test.ts index 4b4ca4ebf..3e42da604 100644 --- a/test/unit/server/routes/agentRoutes.test.ts +++ b/test/unit/server/routes/agentRoutes.test.ts @@ -86,8 +86,8 @@ describe("Agent Routes", () => { expect(routes.prefix).toBe(`${basePath}/agent`); }); - it("should create three routes", () => { - expect(routes.routes.length).toBe(3); + it("should create five routes", () => { + expect(routes.routes.length).toBe(5); }); it("should create execute route", () => {