diff --git a/.changeset/soft-ants-slide.md b/.changeset/soft-ants-slide.md new file mode 100644 index 0000000000..b548e6aa3f --- /dev/null +++ b/.changeset/soft-ants-slide.md @@ -0,0 +1,11 @@ +--- +'@mastra/deployer-cloudflare': patch +'@mastra/deployer-netlify': patch +'@mastra/deployer': patch +'@mastra/deployer-vercel': patch +'@mastra/core': patch +'mastra': patch +'create-mastra': patch +--- + +Disable swaggerUI, playground for production builds, mastra instance server build config to enable swaggerUI, apiReqLogs, openAPI documentation for prod builds \ No newline at end of file diff --git a/deployers/cloudflare/src/index.ts b/deployers/cloudflare/src/index.ts index 7399923e3e..cc932faf0a 100644 --- a/deployers/cloudflare/src/index.ts +++ b/deployers/cloudflare/src/index.ts @@ -1,6 +1,5 @@ import { writeFile } from 'fs/promises'; import { join } from 'path'; - import { Deployer, createChildProcessLogger } from '@mastra/deployer'; import type { analyzeBundle } from '@mastra/deployer/analyze'; import virtual from '@rollup/plugin-virtual'; @@ -80,16 +79,63 @@ export class CloudflareDeployer extends Deployer { private getEntry(): string { return ` -import '#polyfills'; -import { mastra } from '#mastra'; -import { createHonoServer } from '#server'; - -export default { - fetch: async (request, env, context) => { - const app = await createHonoServer(mastra) - return app.fetch(request, env, context); - } -} + import '#polyfills'; + import { mastra } from '#mastra'; + import { createHonoServer } from '#server'; + import { evaluate } from '@mastra/core/eval'; + import { AvailableHooks, registerHook } from '@mastra/core/hooks'; + import { TABLE_EVALS } from '@mastra/core/storage'; + import { checkEvalStorageFields } from '@mastra/core/utils'; + + registerHook(AvailableHooks.ON_GENERATION, ({ input, output, metric, runId, agentName, instructions }) => { + evaluate({ + agentName, + input, + metric, + output, + runId, + globalRunId: runId, + instructions, + }); + }); + + if (mastra.getStorage()) { + // start storage init in the background + mastra.getStorage().init(); + } + + registerHook(AvailableHooks.ON_EVALUATION, async traceObject => { + const storage = mastra.getStorage(); + if (storage) { + // Check for required fields + const logger = mastra?.getLogger(); + const areFieldsValid = checkEvalStorageFields(traceObject, logger); + if (!areFieldsValid) return; + + await storage.insert({ + tableName: TABLE_EVALS, + record: { + input: traceObject.input, + output: traceObject.output, + result: JSON.stringify(traceObject.result || {}), + agent_name: traceObject.agentName, + metric_name: traceObject.metricName, + instructions: traceObject.instructions, + test_info: null, + global_run_id: traceObject.globalRunId, + run_id: traceObject.runId, + created_at: new Date().toISOString(), + }, + }); + } + }); + + export default { + fetch: async (request, env, context) => { + const app = await createHonoServer(mastra) + return app.fetch(request, env, context); + } + } `; } async prepare(outputDirectory: string): Promise { diff --git a/deployers/netlify/src/index.ts b/deployers/netlify/src/index.ts index 1dd8b04d7f..7c2aac3c20 100644 --- a/deployers/netlify/src/index.ts +++ b/deployers/netlify/src/index.ts @@ -1,10 +1,8 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs'; import { join } from 'path'; - import { Deployer } from '@mastra/deployer'; import { DepsService } from '@mastra/deployer/services'; import { execa } from 'execa'; - import { getOrCreateSite } from './helpers.js'; export class NetlifyDeployer extends Deployer { @@ -100,13 +98,60 @@ to = "/.netlify/functions/api/:splat" private getEntry(): string { return ` -import { handle } from 'hono/netlify' -import { mastra } from '#mastra'; -import { createHonoServer } from '#server'; + import { handle } from 'hono/netlify' + import { mastra } from '#mastra'; + import { createHonoServer } from '#server'; + import { evaluate } from '@mastra/core/eval'; + import { AvailableHooks, registerHook } from '@mastra/core/hooks'; + import { TABLE_EVALS } from '@mastra/core/storage'; + import { checkEvalStorageFields } from '@mastra/core/utils'; + + registerHook(AvailableHooks.ON_GENERATION, ({ input, output, metric, runId, agentName, instructions }) => { + evaluate({ + agentName, + input, + metric, + output, + runId, + globalRunId: runId, + instructions, + }); + }); + + if (mastra.getStorage()) { + // start storage init in the background + mastra.getStorage().init(); + } + + registerHook(AvailableHooks.ON_EVALUATION, async traceObject => { + const storage = mastra.getStorage(); + if (storage) { + // Check for required fields + const logger = mastra?.getLogger(); + const areFieldsValid = checkEvalStorageFields(traceObject, logger); + if (!areFieldsValid) return; + + await storage.insert({ + tableName: TABLE_EVALS, + record: { + input: traceObject.input, + output: traceObject.output, + result: JSON.stringify(traceObject.result || {}), + agent_name: traceObject.agentName, + metric_name: traceObject.metricName, + instructions: traceObject.instructions, + test_info: null, + global_run_id: traceObject.globalRunId, + run_id: traceObject.runId, + created_at: new Date().toISOString(), + }, + }); + } + }); -const app = await createHonoServer(mastra); + const app = await createHonoServer(mastra); -export default handle(app) + export default handle(app) `; } } diff --git a/deployers/vercel/src/index.ts b/deployers/vercel/src/index.ts index 0ecd8a6330..50cc9cd1b1 100644 --- a/deployers/vercel/src/index.ts +++ b/deployers/vercel/src/index.ts @@ -2,7 +2,6 @@ import * as child_process from 'child_process'; import { readFileSync, writeFileSync } from 'fs'; import { join } from 'path'; import process from 'process'; - import { Deployer } from '@mastra/deployer'; interface EnvVar { @@ -139,6 +138,53 @@ export class VercelDeployer extends Deployer { import { handle } from 'hono/vercel' import { mastra } from '#mastra'; import { createHonoServer } from '#server'; +import { evaluate } from '@mastra/core/eval'; +import { AvailableHooks, registerHook } from '@mastra/core/hooks'; +import { TABLE_EVALS } from '@mastra/core/storage'; +import { checkEvalStorageFields } from '@mastra/core/utils'; + +registerHook(AvailableHooks.ON_GENERATION, ({ input, output, metric, runId, agentName, instructions }) => { + evaluate({ + agentName, + input, + metric, + output, + runId, + globalRunId: runId, + instructions, + }); +}); + +if (mastra.getStorage()) { + // start storage init in the background + mastra.getStorage().init(); +} + +registerHook(AvailableHooks.ON_EVALUATION, async traceObject => { + const storage = mastra.getStorage(); + if (storage) { + // Check for required fields + const logger = mastra?.getLogger(); + const areFieldsValid = checkEvalStorageFields(traceObject, logger); + if (!areFieldsValid) return; + + await storage.insert({ + tableName: TABLE_EVALS, + record: { + input: traceObject.input, + output: traceObject.output, + result: JSON.stringify(traceObject.result || {}), + agent_name: traceObject.agentName, + metric_name: traceObject.metricName, + instructions: traceObject.instructions, + test_info: null, + global_run_id: traceObject.globalRunId, + run_id: traceObject.runId, + created_at: new Date().toISOString(), + }, + }); + } +}); const app = await createHonoServer(mastra); diff --git a/docs/src/components/github-star-count.tsx b/docs/src/components/github-star-count.tsx index 2b0d61b436..22b2478e14 100644 --- a/docs/src/components/github-star-count.tsx +++ b/docs/src/components/github-star-count.tsx @@ -1,4 +1,3 @@ - import React from "react"; function formatToK(number: number) { diff --git a/docs/src/content/en/docs/local-dev/mastra-dev.mdx b/docs/src/content/en/docs/local-dev/mastra-dev.mdx index 54558dcfb4..5f8442498d 100644 --- a/docs/src/content/en/docs/local-dev/mastra-dev.mdx +++ b/docs/src/content/en/docs/local-dev/mastra-dev.mdx @@ -82,6 +82,40 @@ You can then leverage the [Mastra Client](/docs/deployment/client) SDK to intera `mastra dev` provides an OpenAPI spec at http://localhost:4111/openapi.json +To enable OpenAPI documentation in your Mastra instance, add the following configuration: + +```typescript +import { Mastra } from "@mastra/core"; + +export const mastra = new Mastra({ + server: { + build: { + openAPIDocs: true, // Enable OpenAPI documentation + // ... other build config options + } + } +}); +``` + +## Swagger UI + +Swagger UI provides an interactive interface for testing your API endpoints at `mastra dev` provides an OpenAPI spec at http://localhost:4111/swagger-ui. +To enable Swagger UI in your Mastra instance, add the following configuration: + +```typescript +import { Mastra } from "@mastra/core"; + +export const mastra = new Mastra({ + server: { + build: { + openAPIDocs: true, // Enable OpenAPI documentation + swaggerUI: true, // Enable Swagger UI + // ... other build config options + } + } +}); +``` + ## Local Dev Architecture The local development server is designed to run without any external dependencies or containerization. This is achieved through: diff --git a/packages/cli/src/commands/build/BuildBundler.ts b/packages/cli/src/commands/build/BuildBundler.ts index 46eaccd122..fb4dab30ba 100644 --- a/packages/cli/src/commands/build/BuildBundler.ts +++ b/packages/cli/src/commands/build/BuildBundler.ts @@ -1,9 +1,5 @@ import { FileService } from '@mastra/deployer/build'; import { Bundler } from '@mastra/deployer/bundler'; -import * as fsExtra from 'fs-extra'; -import { readFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; export class BuildBundler extends Bundler { constructor() { @@ -29,13 +25,64 @@ export class BuildBundler extends Bundler { await super.prepare(outputDirectory); } - bundle(entryFile: string, outputDirectory: string, toolsPaths: string[]): Promise { + async bundle(entryFile: string, outputDirectory: string, toolsPaths: string[]): Promise { return this._bundle(this.getEntry(), entryFile, outputDirectory, toolsPaths); } protected getEntry(): string { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - return readFileSync(join(__dirname, 'templates', 'dev.entry.js'), 'utf8'); + return ` + // @ts-ignore + import { evaluate } from '@mastra/core/eval'; + import { AvailableHooks, registerHook } from '@mastra/core/hooks'; + import { TABLE_EVALS } from '@mastra/core/storage'; + import { checkEvalStorageFields } from '@mastra/core/utils'; + import { mastra } from '#mastra'; + import { createNodeServer } from '#server'; + // @ts-ignore + await createNodeServer(mastra); + + registerHook(AvailableHooks.ON_GENERATION, ({ input, output, metric, runId, agentName, instructions }) => { + evaluate({ + agentName, + input, + metric, + output, + runId, + globalRunId: runId, + instructions, + }); + }); + + if (mastra.getStorage()) { + // start storage init in the background + mastra.getStorage().init(); + } + + registerHook(AvailableHooks.ON_EVALUATION, async traceObject => { + const storage = mastra.getStorage(); + if (storage) { + // Check for required fields + const logger = mastra?.getLogger(); + const areFieldsValid = checkEvalStorageFields(traceObject, logger); + if (!areFieldsValid) return; + + await storage.insert({ + tableName: TABLE_EVALS, + record: { + input: traceObject.input, + output: traceObject.output, + result: JSON.stringify(traceObject.result || {}), + agent_name: traceObject.agentName, + metric_name: traceObject.metricName, + instructions: traceObject.instructions, + test_info: null, + global_run_id: traceObject.globalRunId, + run_id: traceObject.runId, + created_at: new Date().toISOString(), + }, + }); + } + }); + `; } } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 8ecce91a12..ea184ea5a4 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -169,7 +169,10 @@ program command: 'mastra build', args, execution: async () => { - await build({ dir: args.dir, tools: args.tools ? args.tools.split(',') : [] }); + await build({ + dir: args.dir, + tools: args.tools ? args.tools.split(',') : [], + }); }, origin, }); diff --git a/packages/cli/src/templates/dev.entry.js b/packages/cli/src/templates/dev.entry.js index a91ec15d55..a8161ff70c 100644 --- a/packages/cli/src/templates/dev.entry.js +++ b/packages/cli/src/templates/dev.entry.js @@ -7,7 +7,7 @@ import { checkEvalStorageFields } from '@mastra/core/utils'; import { mastra } from '#mastra'; import { createNodeServer } from '#server'; // @ts-ignore -await createNodeServer(mastra, { playground: true, swaggerUI: true }); +await createNodeServer(mastra, { playground: true, isDev: true }); registerHook(AvailableHooks.ON_GENERATION, ({ input, output, metric, runId, agentName, instructions }) => { evaluate({ diff --git a/packages/core/src/server/types.ts b/packages/core/src/server/types.ts index df948c2a27..ad8e2a0ec0 100644 --- a/packages/core/src/server/types.ts +++ b/packages/core/src/server/types.ts @@ -23,4 +23,24 @@ export type ServerConfig = { * @default { origin: '*', allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], allowHeaders: ['Content-Type', 'Authorization', 'x-mastra-client-type'], exposeHeaders: ['Content-Length', 'X-Requested-With'], credentials: false } */ cors?: Parameters[0] | false; + /** + * Build configuration for the server + */ + build?: { + /** + * Enable Swagger UI + * @default false + */ + swaggerUI?: boolean; + /** + * Enable API request logging + * @default false + */ + apiReqLogs?: boolean; + /** + * Enable OpenAPI documentation + * @default false + */ + openAPIDocs?: boolean; + }; }; diff --git a/packages/deployer/src/index.ts b/packages/deployer/src/index.ts index 72a5c2c0d9..715e9b60ca 100644 --- a/packages/deployer/src/index.ts +++ b/packages/deployer/src/index.ts @@ -1,3 +1,4 @@ export * from './deploy'; +export * from './server/types'; export { Deps, FileService } from './build'; export { getDeployer } from './build/deployer'; diff --git a/packages/deployer/src/server/index.ts b/packages/deployer/src/server/index.ts index a6d7cf7779..acaccb679a 100644 --- a/packages/deployer/src/server/index.ts +++ b/packages/deployer/src/server/index.ts @@ -61,6 +61,7 @@ import { createRunHandler, getWorkflowRunsHandler, } from './handlers/workflows.js'; +import type { ServerBundleOptions } from './types'; import { html } from './welcome.js'; type Bindings = {}; @@ -71,12 +72,10 @@ type Variables = { clients: Set<{ controller: ReadableStreamDefaultController }>; tools: Record; playground: boolean; + isDev: boolean; }; -export async function createHonoServer( - mastra: Mastra, - options: { playground?: boolean; swaggerUI?: boolean; apiReqLogs?: boolean } = {}, -) { +export async function createHonoServer(mastra: Mastra, options: ServerBundleOptions = {}) { // Create typed Hono app const app = new Hono<{ Bindings: Bindings; Variables: Variables }>(); const server = mastra.getServer(); @@ -126,10 +125,6 @@ export async function createHonoServer( } }); - if (options.apiReqLogs) { - app.use(logger()); - } - app.onError(errorHandler); // Add Mastra to context @@ -140,7 +135,7 @@ export async function createHonoServer( c.set('mastra', mastra); c.set('tools', tools); c.set('playground', options.playground === true); - + c.set('isDev', options.isDev === true); return next(); }); @@ -217,6 +212,10 @@ export async function createHonoServer( } } + if (options?.isDev || server?.build?.apiReqLogs) { + app.use(logger()); + } + // API routes app.get( '/api', @@ -2123,18 +2122,18 @@ export async function createHonoServer( deleteIndex, ); - app.get( - '/openapi.json', - openAPISpecs(app, { - documentation: { - info: { title: 'Mastra API', version: '1.0.0', description: 'Mastra API' }, - }, - }), - ); - - app.get('/swagger-ui', swaggerUI({ url: '/openapi.json' })); + if (options?.isDev || server?.build?.openAPIDocs || server?.build?.swaggerUI) { + app.get( + '/openapi.json', + openAPISpecs(app, { + documentation: { + info: { title: 'Mastra API', version: '1.0.0', description: 'Mastra API' }, + }, + }), + ); + } - if (options?.swaggerUI) { + if (options?.isDev || server?.build?.swaggerUI) { app.get('/swagger-ui', swaggerUI({ url: '/openapi.json' })); } @@ -2196,10 +2195,7 @@ export async function createHonoServer( return app; } -export async function createNodeServer( - mastra: Mastra, - options: { playground?: boolean; swaggerUI?: boolean; apiReqLogs?: boolean } = {}, -) { +export async function createNodeServer(mastra: Mastra, options: ServerBundleOptions = {}) { const app = await createHonoServer(mastra, options); const serverOptions = mastra.getServer(); @@ -2212,9 +2208,11 @@ export async function createNodeServer( }, () => { const logger = mastra.getLogger(); - logger.info(`🦄 Mastra API running on port ${port}/api`); - logger.info(`📚 Open API documentation available at http://localhost:${port}/openapi.json`); - if (options?.swaggerUI) { + logger.info(` Mastra API running on port http://localhost:${process.env.PORT || 4111}/api`); + if (options?.isDev) { + logger.info(`� Open API documentation available at http://localhost:${port}/openapi.json`); + } + if (options?.isDev) { logger.info(`🧪 Swagger UI available at http://localhost:${port}/swagger-ui`); } if (options?.playground) { diff --git a/packages/deployer/src/server/types.ts b/packages/deployer/src/server/types.ts index ab0577d03f..1c4a7652dd 100644 --- a/packages/deployer/src/server/types.ts +++ b/packages/deployer/src/server/types.ts @@ -2,3 +2,8 @@ export interface ApiError extends Error { message: string; status?: number; } + +export type ServerBundleOptions = { + playground?: boolean; + isDev?: boolean; +};