trace.ts•17.3 kB
import { makeApiRequest } from '../utils/api.js';
import { z } from 'zod';
import { logToFile } from '../utils/logging.js';
import {
ProjectResponse,
TraceResponse,
SingleTraceResponse,
TraceStatsResponse,
} from './../types.js';
export const loadTraceTools = (server: any) => {
server.tool(
'list-traces',
'Get a list of traces from a project. Use this for basic trace retrieval and overview',
{
page: z.number().optional().default(1).describe('Page number for pagination (starts at 1)'),
size: z
.number()
.optional()
.default(10)
.describe('Number of traces per page (1-100, default 10)'),
projectId: z
.string()
.optional()
.describe(
'Project ID to filter traces. If not provided, will use the first available project'
),
projectName: z
.string()
.optional()
.describe(
'Project name to filter traces (alternative to projectId). Example: "My AI Assistant"'
),
workspaceName: z
.string()
.optional()
.describe('Workspace name to use instead of the default workspace'),
},
async (args: any) => {
const { page = 1, size = 10, projectId, projectName, workspaceName } = args;
let url = `/v1/private/traces?page=${page}&size=${size}`;
// Add project filtering - API requires either project_id or project_name
if (projectId) {
url += `&project_id=${projectId}`;
} else if (projectName) {
url += `&project_name=${encodeURIComponent(projectName)}`;
} else {
// If no project specified, we need to find one for the API to work
const projectsResponse = await makeApiRequest<ProjectResponse>(
`/v1/private/projects?page=1&size=1`,
{},
workspaceName
);
if (
projectsResponse.data &&
projectsResponse.data.content &&
projectsResponse.data.content.length > 0
) {
const firstProject = projectsResponse.data.content[0];
url += `&project_id=${firstProject.id}`;
logToFile(
`No project specified, using first available: ${firstProject.name} (${firstProject.id})`
);
} else {
return {
content: [
{
type: 'text',
text: 'Error: No project ID or name provided, and no projects found',
},
],
};
}
}
const response = await makeApiRequest<TraceResponse>(url, {}, workspaceName);
if (!response.data) {
return {
content: [{ type: 'text', text: response.error || 'Failed to fetch traces' }],
};
}
return {
content: [
{
type: 'text',
text: `Found ${response.data.total} traces (showing page ${
response.data.page
} of ${Math.ceil(response.data.total / response.data.size)})`,
},
{
type: 'text',
text: JSON.stringify(response.data.content, null, 2),
},
],
};
}
);
server.tool(
'get-trace-by-id',
'Get detailed information about a specific trace including input, output, metadata, and timing information',
{
traceId: z
.string()
.describe(
'ID of the trace to fetch (UUID format, e.g. "123e4567-e89b-12d3-a456-426614174000")'
),
workspaceName: z
.string()
.optional()
.describe('Workspace name to use instead of the default workspace'),
},
async (args: any) => {
const { traceId, workspaceName } = args;
const response = await makeApiRequest<SingleTraceResponse>(
`/v1/private/traces/${traceId}`,
{},
workspaceName
);
if (!response.data) {
return {
content: [{ type: 'text', text: response.error || 'Failed to fetch trace' }],
};
}
// Format the response for better readability
const formattedResponse: any = { ...response.data };
// Format input/output if they're large
if (
formattedResponse.input &&
typeof formattedResponse.input === 'object' &&
Object.keys(formattedResponse.input).length > 0
) {
formattedResponse.input = JSON.stringify(formattedResponse.input, null, 2);
}
if (
formattedResponse.output &&
typeof formattedResponse.output === 'object' &&
Object.keys(formattedResponse.output).length > 0
) {
formattedResponse.output = JSON.stringify(formattedResponse.output, null, 2);
}
return {
content: [
{
type: 'text',
text: `Trace Details for ID: ${traceId}`,
},
{
type: 'text',
text: JSON.stringify(formattedResponse, null, 2),
},
],
};
}
);
server.tool(
'get-trace-stats',
'Get aggregated statistics for traces including counts, costs, token usage, and performance metrics over time',
{
projectId: z
.string()
.optional()
.describe(
'Project ID to filter traces. If not provided, will use the first available project'
),
projectName: z
.string()
.optional()
.describe('Project name to filter traces (alternative to projectId)'),
startDate: z
.string()
.optional()
.describe('Start date in ISO format (YYYY-MM-DD). Example: "2024-01-01"'),
endDate: z
.string()
.optional()
.describe('End date in ISO format (YYYY-MM-DD). Example: "2024-01-31"'),
workspaceName: z
.string()
.optional()
.describe('Workspace name to use instead of the default workspace'),
},
async (args: any) => {
const { projectId, projectName, startDate, endDate, workspaceName } = args;
let url = `/v1/private/traces/stats`;
// Build query parameters
const queryParams = [];
// Add project filtering - API requires either project_id or project_name
if (projectId) {
queryParams.push(`project_id=${projectId}`);
} else if (projectName) {
queryParams.push(`project_name=${encodeURIComponent(projectName)}`);
} else {
// If no project specified, we need to find one for the API to work
const projectsResponse = await makeApiRequest<ProjectResponse>(
`/v1/private/projects?page=1&size=1`,
{},
workspaceName
);
if (
projectsResponse.data &&
projectsResponse.data.content &&
projectsResponse.data.content.length > 0
) {
const firstProject = projectsResponse.data.content[0];
queryParams.push(`project_id=${firstProject.id}`);
logToFile(
`No project specified, using first available: ${firstProject.name} (${firstProject.id})`
);
} else {
return {
content: [
{
type: 'text',
text: 'Error: No project ID or name provided, and no projects found',
},
],
};
}
}
if (startDate) queryParams.push(`start_date=${startDate}`);
if (endDate) queryParams.push(`end_date=${endDate}`);
if (queryParams.length > 0) {
url += `?${queryParams.join('&')}`;
}
const response = await makeApiRequest<TraceStatsResponse>(url, {}, workspaceName);
if (!response.data) {
return {
content: [{ type: 'text', text: response.error || 'Failed to fetch trace statistics' }],
};
}
return {
content: [
{
type: 'text',
text: `Trace Statistics:`,
},
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
}
);
server.tool(
'search-traces',
'Advanced search for traces with complex filtering and query capabilities',
{
projectId: z.string().optional().describe('Project ID to search within'),
projectName: z.string().optional().describe('Project name to search within'),
query: z
.string()
.optional()
.describe(
'Text query to search in trace names, inputs, outputs, and metadata. Example: "error" or "user_query:hello"'
),
filters: z
.record(z.any())
.optional()
.describe(
'Advanced filters as key-value pairs. Examples: {"status": "error"}, {"model": "gpt-4"}, {"duration_ms": {"$gt": 1000}}'
),
page: z.number().optional().default(1).describe('Page number for pagination'),
size: z.number().optional().default(10).describe('Number of traces per page (max 100)'),
sortBy: z
.string()
.optional()
.describe('Field to sort by. Options: "created_at", "duration", "name", "status"'),
sortOrder: z
.enum(['asc', 'desc'])
.optional()
.default('desc')
.describe('Sort order: ascending or descending'),
workspaceName: z.string().optional().describe('Workspace name to use instead of the default'),
},
async (args: any) => {
const {
projectId,
projectName,
query,
filters,
page,
size,
sortBy,
sortOrder,
workspaceName,
} = args;
// Build search request body
const searchBody: any = {
page: page || 1,
size: size || 10,
};
// Add project filtering
if (projectId) {
searchBody.project_id = projectId;
} else if (projectName) {
searchBody.project_name = projectName;
} else {
// If no project specified, we need to find one for the API to work
const projectsResponse = await makeApiRequest<ProjectResponse>(
`/v1/private/projects?page=1&size=1`,
{},
workspaceName
);
if (
projectsResponse.data &&
projectsResponse.data.content &&
projectsResponse.data.content.length > 0
) {
const firstProject = projectsResponse.data.content[0];
searchBody.project_id = firstProject.id;
logToFile(
`No project specified for search, using first available: ${firstProject.name} (${firstProject.id})`
);
} else {
return {
content: [
{
type: 'text',
text: 'Error: No project ID or name provided, and no projects found',
},
],
};
}
}
// Add query if provided
if (query) {
searchBody.query = query;
}
// Add filters if provided
if (filters) {
searchBody.filters = filters;
}
// Add sorting if provided
if (sortBy) {
searchBody.sort_by = sortBy;
if (sortOrder) {
searchBody.sort_order = sortOrder;
}
}
const response = await makeApiRequest<TraceResponse>(
'/v1/private/traces/search',
{
method: 'POST',
body: JSON.stringify(searchBody),
},
workspaceName
);
if (!response.data) {
return {
content: [{ type: 'text', text: response.error || 'Failed to search traces' }],
};
}
return {
content: [
{
type: 'text',
text: `Search found ${response.data.total} traces (page ${response.data.page} of ${Math.ceil(response.data.total / response.data.size)})`,
},
{
type: 'text',
text: JSON.stringify(response.data.content, null, 2),
},
],
};
}
);
server.tool(
'get-trace-threads',
'Get trace threads (conversation groupings) to view related traces that belong to the same conversation or session',
{
projectId: z.string().optional().describe('Project ID to filter threads'),
projectName: z.string().optional().describe('Project name to filter threads'),
page: z.number().optional().default(1).describe('Page number for pagination'),
size: z.number().optional().default(10).describe('Number of threads per page'),
threadId: z
.string()
.optional()
.describe(
'Specific thread ID to retrieve (useful for getting all traces in a conversation)'
),
workspaceName: z.string().optional().describe('Workspace name to use instead of the default'),
},
async (args: any) => {
const { projectId, projectName, page, size, threadId, workspaceName } = args;
let url = `/v1/private/traces/threads?page=${page || 1}&size=${size || 10}`;
// Add project filtering
if (projectId) {
url += `&project_id=${projectId}`;
} else if (projectName) {
url += `&project_name=${encodeURIComponent(projectName)}`;
} else {
// If no project specified, we need to find one for the API to work
const projectsResponse = await makeApiRequest<ProjectResponse>(
`/v1/private/projects?page=1&size=1`,
{},
workspaceName
);
if (
projectsResponse.data &&
projectsResponse.data.content &&
projectsResponse.data.content.length > 0
) {
const firstProject = projectsResponse.data.content[0];
url += `&project_id=${firstProject.id}`;
logToFile(
`No project specified for threads, using first available: ${firstProject.name} (${firstProject.id})`
);
} else {
return {
content: [
{
type: 'text',
text: 'Error: No project ID or name provided, and no projects found',
},
],
};
}
}
// Add thread ID filter if specified
if (threadId) {
url += `&thread_id=${encodeURIComponent(threadId)}`;
}
const response = await makeApiRequest<any>(url, {}, workspaceName);
if (!response.data) {
return {
content: [{ type: 'text', text: response.error || 'Failed to fetch trace threads' }],
};
}
return {
content: [
{
type: 'text',
text: threadId
? `Thread details for ID: ${threadId}`
: `Found ${response.data.total || response.data.length || 0} trace threads`,
},
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
}
);
server.tool(
'add-trace-feedback',
'Add feedback scores to a trace for quality evaluation and monitoring. Useful for rating trace quality, relevance, or custom metrics',
{
traceId: z.string().describe('ID of the trace to add feedback to'),
scores: z
.array(
z.object({
name: z
.string()
.describe(
'Name of the feedback metric (e.g., "relevance", "accuracy", "helpfulness", "quality")'
),
value: z
.number()
.min(0)
.max(1)
.describe('Score value between 0.0 and 1.0 (0.0 = poor, 1.0 = excellent)'),
reason: z.string().optional().describe('Optional explanation for the score'),
})
)
.describe(
'Array of feedback scores to add. Each score should have a name and value between 0-1'
),
workspaceName: z.string().optional().describe('Workspace name to use instead of the default'),
},
async (args: any) => {
const { traceId, scores, workspaceName } = args;
// Validate scores format
if (!scores || !Array.isArray(scores) || scores.length === 0) {
return {
content: [
{
type: 'text',
text: 'Error: At least one feedback score is required. Format: [{"name": "relevance", "value": 0.8}]',
},
],
};
}
// Transform scores to the expected API format
const feedbackScores = scores.map((score: any) => ({
name: score.name,
value: score.value,
...(score.reason && { reason: score.reason }),
}));
const response = await makeApiRequest<any>(
`/v1/private/traces/${traceId}/feedback-scores`,
{
method: 'PUT',
body: JSON.stringify({ scores: feedbackScores }),
},
workspaceName
);
if (response.error) {
return {
content: [
{
type: 'text',
text: `Error adding feedback: ${response.error}`,
},
],
};
}
return {
content: [
{
type: 'text',
text: `Successfully added ${scores.length} feedback score(s) to trace ${traceId}`,
},
{
type: 'text',
text: `Added scores: ${scores.map((s: any) => `${s.name}: ${s.value}`).join(', ')}`,
},
],
};
}
);
return server;
};