Repository navigation
test(e2e): Add Mastra Jev classifier test to node-mastra #25115
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import { Classifier } from '@mastra/core/classifier'; | ||
| import { openrouter } from '../openrouter.js'; | ||
|
|
||
| export const TICKET_CLASSIFIER = 'ticketClassifier'; | ||
|
|
||
| // A Jev evaluation through OpenRouter's Decisions API. `Classifier` calls the model's `doEvaluate()` | ||
| // directly, so its span comes from the Mastra exporter, not from the Vercel AI integration. | ||
| export const ticketClassifier = new Classifier({ | ||
| id: TICKET_CLASSIFIER, | ||
| model: openrouter.evaluationModel('typesafe/jev-1.13'), | ||
| questions: { | ||
| is_bug: { | ||
| type: 'boolean', | ||
| instructions: 'Is the customer reporting a software defect?', | ||
| }, | ||
| }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| import { createOpenRouter } from '@openrouter/ai-sdk-provider'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. l: I think other scenarios also use openrouter. could this be used there as well?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They do now in this pr, I moved the weather agent to the same shared helper.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant for instance
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, I'll do that in follow-up tasks as it doesn't quite fit into here.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure sounds good
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Filed JS-3894 for this. |
||
|
|
||
| const apiKey = process.env.E2E_OPENROUTER_API_KEY; | ||
| if (!apiKey) { | ||
| throw new Error('E2E_OPENROUTER_API_KEY is not set'); | ||
| } | ||
|
|
||
| // Call OpenRouter directly (rather than the default Vercel AI Gateway) so the | ||
| // e2e test needs only a single OpenRouter key, reusing `E2E_OPENROUTER_API_KEY`. | ||
| export const openrouter = createOpenRouter({ apiKey }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { expect, test } from '@playwright/test'; | ||
| import { | ||
| GEN_AI_INPUT_MESSAGES, | ||
| GEN_AI_OPERATION_NAME, | ||
| GEN_AI_OUTPUT_MESSAGES, | ||
| GEN_AI_PROVIDER_NAME, | ||
| GEN_AI_REQUEST_MODEL, | ||
| GEN_AI_USAGE_INPUT_TOKENS, | ||
| SENTRY_ORIGIN, | ||
| URL_FULL, | ||
| } from '@sentry/conventions/attributes'; | ||
| import { GEN_AI_EVALUATE, HTTP_SERVER } from '@sentry/conventions/op'; | ||
| import { collectStreamedSpans, getSpanOp, SerializedStreamedSpan } from '@sentry-internal/test-utils'; | ||
|
|
||
| const APP = 'node-mastra'; | ||
| const TICKET = 'My checkout page shows a blank screen after I click Pay.'; | ||
|
|
||
| const isEvaluateSpan = (span: SerializedStreamedSpan): boolean => getSpanOp(span) === GEN_AI_EVALUATE; | ||
| const isClassifyServerSpan = (span: SerializedStreamedSpan): boolean => | ||
| getSpanOp(span) === HTTP_SERVER && String(span.attributes?.[URL_FULL]?.value ?? '').includes('/classify'); | ||
|
|
||
| test('captures a Mastra classifier (Jev) evaluation as a gen_ai.evaluate span', async ({ baseURL }) => { | ||
| const spansPromise = collectStreamedSpans( | ||
| APP, | ||
| spansOfTrace => spansOfTrace.some(isEvaluateSpan) && spansOfTrace.some(isClassifyServerSpan), | ||
| ); | ||
|
|
||
| const res = await fetch(`${baseURL}/classify`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ ticket: TICKET }), | ||
| }); | ||
| expect(res.status).toBe(200); | ||
| await res.json(); | ||
|
|
||
| const spans = await spansPromise; | ||
| const serverSpan = spans.find(isClassifyServerSpan)!; | ||
|
|
||
| // Only the Mastra exporter reports the evaluation: `Classifier` calls `doEvaluate()` directly, so the | ||
| // Vercel AI integration does not see it. | ||
| const evaluateSpans = spans.filter(isEvaluateSpan); | ||
| expect(evaluateSpans.map(span => ({ name: span.name, origin: span.attributes?.[SENTRY_ORIGIN]?.value }))).toEqual([ | ||
| { name: 'evaluate typesafe/jev-1.13', origin: 'auto.ai.mastra' }, | ||
| ]); | ||
|
|
||
| const evaluateSpan = evaluateSpans[0]!; | ||
| expect(evaluateSpan.parent_span_id).toBe(serverSpan.span_id); | ||
| expect(evaluateSpan.attributes?.[GEN_AI_OPERATION_NAME]?.value).toBe('evaluate'); | ||
| expect(evaluateSpan.attributes?.[GEN_AI_REQUEST_MODEL]?.value).toBe('typesafe/jev-1.13'); | ||
| expect(evaluateSpan.attributes?.[GEN_AI_PROVIDER_NAME]?.value).toBe('openrouter'); | ||
| expect(evaluateSpan.attributes?.[GEN_AI_USAGE_INPUT_TOKENS]?.value).toBeGreaterThan(0); | ||
| expect(JSON.parse(String(evaluateSpan.attributes?.[GEN_AI_INPUT_MESSAGES]?.value))).toEqual([ | ||
| { | ||
| type: 'evaluation', | ||
| state: { ticket: TICKET }, | ||
| questions: { | ||
| is_bug: { type: 'boolean', instructions: 'Is the customer reporting a software defect?' }, | ||
| }, | ||
| }, | ||
| ]); | ||
| // The probability comes from the live model, so only its shape is stable. | ||
| expect(JSON.parse(String(evaluateSpan.attributes?.[GEN_AI_OUTPUT_MESSAGES]?.value))).toEqual([ | ||
| { type: 'evaluation', answers: { is_bug: { type: 'boolean', probability: expect.any(Number) } } }, | ||
| ]); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.