opik-mcp 0.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -11
- package/build/cli.js +10 -8
- package/build/config.js +91 -27
- package/build/index.js +56 -76
- package/build/prompts/core-prompts.js +52 -0
- package/build/resources/opik-resources.js +196 -0
- package/build/test-client.js +2 -4
- package/build/tools/capabilities.js +183 -0
- package/build/tools/dataset.js +172 -0
- package/build/tools/integration.js +3 -3
- package/build/tools/metrics.js +51 -47
- package/build/tools/project.js +69 -48
- package/build/tools/prompt.js +88 -49
- package/build/tools/registration.js +110 -0
- package/build/tools/schema.js +13 -0
- package/build/tools/trace.js +319 -378
- package/build/transports/streamable-http-transport.js +298 -0
- package/build/transports/types.js +0 -3
- package/build/utils/capabilities.js +34 -4
- package/build/utils/opik-sdk.js +119 -0
- package/build/utils/remote-auth.js +175 -0
- package/build/utils/request-context.js +32 -0
- package/package.json +25 -20
- package/build/transports/sse-transport.js +0 -169
package/build/tools/trace.js
CHANGED
|
@@ -1,420 +1,361 @@
|
|
|
1
|
-
import { makeApiRequest } from '../utils/api.js';
|
|
2
1
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (projectId) {
|
|
29
|
-
url += `&project_id=${projectId}`;
|
|
30
|
-
}
|
|
31
|
-
else if (projectName) {
|
|
32
|
-
url += `&project_name=${encodeURIComponent(projectName)}`;
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
// If no project specified, we need to find one for the API to work
|
|
36
|
-
const projectsResponse = await makeApiRequest(`/v1/private/projects?page=1&size=1`, {}, workspaceName);
|
|
37
|
-
if (projectsResponse.data &&
|
|
38
|
-
projectsResponse.data.content &&
|
|
39
|
-
projectsResponse.data.content.length > 0) {
|
|
40
|
-
const firstProject = projectsResponse.data.content[0];
|
|
41
|
-
url += `&project_id=${firstProject.id}`;
|
|
42
|
-
logToFile(`No project specified, using first available: ${firstProject.name} (${firstProject.id})`);
|
|
2
|
+
import { buildTraceFilters, callSdk, getOpikApi, getRequestOptions, resolveProjectIdentifier, } from '../utils/opik-sdk.js';
|
|
3
|
+
import { registerTool } from './registration.js';
|
|
4
|
+
import { isoDateSchema, pageSchema, sizeSchema, workspaceNameSchema } from './schema.js';
|
|
5
|
+
export const loadTraceTools = (server, options = {}) => {
|
|
6
|
+
const { includeCoreTools = true, includeExpertActions = true } = options;
|
|
7
|
+
if (includeCoreTools) {
|
|
8
|
+
registerTool(server, 'list-traces', 'List traces for a project for quick inspection and navigation.', {
|
|
9
|
+
page: pageSchema,
|
|
10
|
+
size: sizeSchema(10),
|
|
11
|
+
projectId: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Optional project ID. If omitted, the first available project is used.'),
|
|
15
|
+
projectName: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Optional project name (alternative to projectId).'),
|
|
19
|
+
workspaceName: workspaceNameSchema,
|
|
20
|
+
}, async (args) => {
|
|
21
|
+
const { page = 1, size = 10, projectId, projectName, workspaceName } = args;
|
|
22
|
+
const resolved = await resolveProjectIdentifier(projectId, projectName, workspaceName);
|
|
23
|
+
if (resolved.error) {
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: 'text', text: `Error: ${resolved.error}` }],
|
|
26
|
+
};
|
|
43
27
|
}
|
|
44
|
-
|
|
28
|
+
const api = getOpikApi();
|
|
29
|
+
const response = await callSdk(() => api.traces.getTracesByProject({
|
|
30
|
+
page,
|
|
31
|
+
size,
|
|
32
|
+
...(resolved.projectId && { projectId: resolved.projectId }),
|
|
33
|
+
...(resolved.projectName && { projectName: resolved.projectName }),
|
|
34
|
+
}, getRequestOptions(workspaceName)));
|
|
35
|
+
if (!response.data) {
|
|
45
36
|
return {
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: 'text',
|
|
49
|
-
text: 'Error: No project ID or name provided, and no projects found',
|
|
50
|
-
},
|
|
51
|
-
],
|
|
37
|
+
content: [{ type: 'text', text: response.error || 'Failed to fetch traces' }],
|
|
52
38
|
};
|
|
53
39
|
}
|
|
54
|
-
}
|
|
55
|
-
const response = await makeApiRequest(url, {}, workspaceName);
|
|
56
|
-
if (!response.data) {
|
|
57
40
|
return {
|
|
58
|
-
content: [
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: `Found ${response.data.total} traces (showing page ${response.data.page} of ${Math.ceil(response.data.total / response.data.size)})`,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'text',
|
|
48
|
+
text: JSON.stringify(response.data.content, null, 2),
|
|
49
|
+
},
|
|
50
|
+
],
|
|
59
51
|
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
52
|
+
}, {
|
|
53
|
+
title: 'List Traces',
|
|
54
|
+
annotations: {
|
|
55
|
+
readOnlyHint: true,
|
|
56
|
+
destructiveHint: false,
|
|
57
|
+
idempotentHint: true,
|
|
58
|
+
openWorldHint: false,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
registerTool(server, 'get-trace-by-id', 'Get full details for a trace, including metadata and serialized input/output.', {
|
|
62
|
+
traceId: z.string().min(1).describe('Trace ID.'),
|
|
63
|
+
workspaceName: workspaceNameSchema,
|
|
64
|
+
}, async (args) => {
|
|
65
|
+
const { traceId, workspaceName } = args;
|
|
66
|
+
const api = getOpikApi();
|
|
67
|
+
const response = await callSdk(() => api.traces.getTraceById(traceId, getRequestOptions(workspaceName)));
|
|
68
|
+
if (!response.data) {
|
|
69
|
+
return {
|
|
70
|
+
content: [{ type: 'text', text: response.error || 'Failed to fetch trace' }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const formattedResponse = { ...response.data };
|
|
74
|
+
if (formattedResponse.input &&
|
|
75
|
+
typeof formattedResponse.input === 'object' &&
|
|
76
|
+
Object.keys(formattedResponse.input).length > 0) {
|
|
77
|
+
formattedResponse.input = JSON.stringify(formattedResponse.input, null, 2);
|
|
78
|
+
}
|
|
79
|
+
if (formattedResponse.output &&
|
|
80
|
+
typeof formattedResponse.output === 'object' &&
|
|
81
|
+
Object.keys(formattedResponse.output).length > 0) {
|
|
82
|
+
formattedResponse.output = JSON.stringify(formattedResponse.output, null, 2);
|
|
83
|
+
}
|
|
86
84
|
return {
|
|
87
|
-
content: [
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: `Trace Details for ID: ${traceId}`,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'text',
|
|
92
|
+
text: JSON.stringify(formattedResponse, null, 2),
|
|
93
|
+
},
|
|
94
|
+
],
|
|
88
95
|
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
projectId: z
|
|
118
|
-
.string()
|
|
119
|
-
.optional()
|
|
120
|
-
.describe('Project ID to filter traces. If not provided, will use the first available project'),
|
|
121
|
-
projectName: z
|
|
122
|
-
.string()
|
|
123
|
-
.optional()
|
|
124
|
-
.describe('Project name to filter traces (alternative to projectId)'),
|
|
125
|
-
startDate: z
|
|
126
|
-
.string()
|
|
127
|
-
.optional()
|
|
128
|
-
.describe('Start date in ISO format (YYYY-MM-DD). Example: "2024-01-01"'),
|
|
129
|
-
endDate: z
|
|
130
|
-
.string()
|
|
131
|
-
.optional()
|
|
132
|
-
.describe('End date in ISO format (YYYY-MM-DD). Example: "2024-01-31"'),
|
|
133
|
-
workspaceName: z
|
|
134
|
-
.string()
|
|
135
|
-
.optional()
|
|
136
|
-
.describe('Workspace name to use instead of the default workspace'),
|
|
137
|
-
}, async (args) => {
|
|
138
|
-
const { projectId, projectName, startDate, endDate, workspaceName } = args;
|
|
139
|
-
let url = `/v1/private/traces/stats`;
|
|
140
|
-
// Build query parameters
|
|
141
|
-
const queryParams = [];
|
|
142
|
-
// Add project filtering - API requires either project_id or project_name
|
|
143
|
-
if (projectId) {
|
|
144
|
-
queryParams.push(`project_id=${projectId}`);
|
|
145
|
-
}
|
|
146
|
-
else if (projectName) {
|
|
147
|
-
queryParams.push(`project_name=${encodeURIComponent(projectName)}`);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
// If no project specified, we need to find one for the API to work
|
|
151
|
-
const projectsResponse = await makeApiRequest(`/v1/private/projects?page=1&size=1`, {}, workspaceName);
|
|
152
|
-
if (projectsResponse.data &&
|
|
153
|
-
projectsResponse.data.content &&
|
|
154
|
-
projectsResponse.data.content.length > 0) {
|
|
155
|
-
const firstProject = projectsResponse.data.content[0];
|
|
156
|
-
queryParams.push(`project_id=${firstProject.id}`);
|
|
157
|
-
logToFile(`No project specified, using first available: ${firstProject.name} (${firstProject.id})`);
|
|
96
|
+
}, {
|
|
97
|
+
title: 'Get Trace By ID',
|
|
98
|
+
annotations: {
|
|
99
|
+
readOnlyHint: true,
|
|
100
|
+
destructiveHint: false,
|
|
101
|
+
idempotentHint: true,
|
|
102
|
+
openWorldHint: false,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
registerTool(server, 'get-trace-stats', 'Get aggregated trace statistics (count, tokens, cost, and duration) over time.', {
|
|
106
|
+
projectId: z
|
|
107
|
+
.string()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Optional project ID. If omitted, the first available project is used.'),
|
|
110
|
+
projectName: z
|
|
111
|
+
.string()
|
|
112
|
+
.optional()
|
|
113
|
+
.describe('Optional project name (alternative to projectId).'),
|
|
114
|
+
startDate: isoDateSchema,
|
|
115
|
+
endDate: isoDateSchema,
|
|
116
|
+
workspaceName: workspaceNameSchema,
|
|
117
|
+
}, async (args) => {
|
|
118
|
+
const { projectId, projectName, startDate, endDate, workspaceName } = args;
|
|
119
|
+
const resolved = await resolveProjectIdentifier(projectId, projectName, workspaceName);
|
|
120
|
+
if (resolved.error) {
|
|
121
|
+
return {
|
|
122
|
+
content: [{ type: 'text', text: `Error: ${resolved.error}` }],
|
|
123
|
+
};
|
|
158
124
|
}
|
|
159
|
-
|
|
125
|
+
const filters = buildTraceFilters(undefined, undefined, startDate, endDate);
|
|
126
|
+
const api = getOpikApi();
|
|
127
|
+
const response = await callSdk(() => api.traces.getTraceStats({
|
|
128
|
+
...(resolved.projectId && { projectId: resolved.projectId }),
|
|
129
|
+
...(resolved.projectName && { projectName: resolved.projectName }),
|
|
130
|
+
...(filters && { filters }),
|
|
131
|
+
}, getRequestOptions(workspaceName)));
|
|
132
|
+
if (!response.data) {
|
|
160
133
|
return {
|
|
161
|
-
content: [
|
|
162
|
-
{
|
|
163
|
-
type: 'text',
|
|
164
|
-
text: 'Error: No project ID or name provided, and no projects found',
|
|
165
|
-
},
|
|
166
|
-
],
|
|
134
|
+
content: [{ type: 'text', text: response.error || 'Failed to fetch trace statistics' }],
|
|
167
135
|
};
|
|
168
136
|
}
|
|
169
|
-
}
|
|
170
|
-
if (startDate)
|
|
171
|
-
queryParams.push(`start_date=${startDate}`);
|
|
172
|
-
if (endDate)
|
|
173
|
-
queryParams.push(`end_date=${endDate}`);
|
|
174
|
-
if (queryParams.length > 0) {
|
|
175
|
-
url += `?${queryParams.join('&')}`;
|
|
176
|
-
}
|
|
177
|
-
const response = await makeApiRequest(url, {}, workspaceName);
|
|
178
|
-
if (!response.data) {
|
|
179
137
|
return {
|
|
180
|
-
content: [
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: 'text',
|
|
141
|
+
text: `Trace Statistics:`,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: JSON.stringify(response.data, null, 2),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
181
148
|
};
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.optional()
|
|
206
|
-
.describe('Advanced filters as key-value pairs. Examples: {"status": "error"}, {"model": "gpt-4"}, {"duration_ms": {"$gt": 1000}}'),
|
|
207
|
-
page: z.number().optional().default(1).describe('Page number for pagination'),
|
|
208
|
-
size: z.number().optional().default(10).describe('Number of traces per page (max 100)'),
|
|
209
|
-
sortBy: z
|
|
210
|
-
.string()
|
|
211
|
-
.optional()
|
|
212
|
-
.describe('Field to sort by. Options: "created_at", "duration", "name", "status"'),
|
|
213
|
-
sortOrder: z
|
|
214
|
-
.enum(['asc', 'desc'])
|
|
215
|
-
.optional()
|
|
216
|
-
.default('desc')
|
|
217
|
-
.describe('Sort order: ascending or descending'),
|
|
218
|
-
workspaceName: z.string().optional().describe('Workspace name to use instead of the default'),
|
|
219
|
-
}, async (args) => {
|
|
220
|
-
const { projectId, projectName, query, filters, page, size, sortBy, sortOrder, workspaceName, } = args;
|
|
221
|
-
// Build search request body
|
|
222
|
-
const searchBody = {
|
|
223
|
-
page: page || 1,
|
|
224
|
-
size: size || 10,
|
|
225
|
-
};
|
|
226
|
-
// Add project filtering
|
|
227
|
-
if (projectId) {
|
|
228
|
-
searchBody.project_id = projectId;
|
|
229
|
-
}
|
|
230
|
-
else if (projectName) {
|
|
231
|
-
searchBody.project_name = projectName;
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
// If no project specified, we need to find one for the API to work
|
|
235
|
-
const projectsResponse = await makeApiRequest(`/v1/private/projects?page=1&size=1`, {}, workspaceName);
|
|
236
|
-
if (projectsResponse.data &&
|
|
237
|
-
projectsResponse.data.content &&
|
|
238
|
-
projectsResponse.data.content.length > 0) {
|
|
239
|
-
const firstProject = projectsResponse.data.content[0];
|
|
240
|
-
searchBody.project_id = firstProject.id;
|
|
241
|
-
logToFile(`No project specified for search, using first available: ${firstProject.name} (${firstProject.id})`);
|
|
242
|
-
}
|
|
243
|
-
else {
|
|
149
|
+
}, {
|
|
150
|
+
title: 'Get Trace Stats',
|
|
151
|
+
annotations: {
|
|
152
|
+
readOnlyHint: true,
|
|
153
|
+
destructiveHint: false,
|
|
154
|
+
idempotentHint: true,
|
|
155
|
+
openWorldHint: false,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
registerTool(server, 'get-trace-threads', 'List trace threads (conversation/session groupings) or fetch one thread by ID.', {
|
|
159
|
+
projectId: z.string().optional().describe('Optional project ID filter.'),
|
|
160
|
+
projectName: z.string().optional().describe('Optional project name filter.'),
|
|
161
|
+
page: pageSchema,
|
|
162
|
+
size: sizeSchema(10),
|
|
163
|
+
threadId: z
|
|
164
|
+
.string()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe('Optional thread ID. When set, returns that thread instead of paginated listing.'),
|
|
167
|
+
workspaceName: workspaceNameSchema,
|
|
168
|
+
}, async (args) => {
|
|
169
|
+
const { projectId, projectName, page, size, threadId, workspaceName } = args;
|
|
170
|
+
const resolved = await resolveProjectIdentifier(projectId, projectName, workspaceName);
|
|
171
|
+
if (resolved.error) {
|
|
244
172
|
return {
|
|
245
|
-
content: [
|
|
246
|
-
{
|
|
247
|
-
type: 'text',
|
|
248
|
-
text: 'Error: No project ID or name provided, and no projects found',
|
|
249
|
-
},
|
|
250
|
-
],
|
|
173
|
+
content: [{ type: 'text', text: `Error: ${resolved.error}` }],
|
|
251
174
|
};
|
|
252
175
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
176
|
+
const api = getOpikApi();
|
|
177
|
+
const response = threadId
|
|
178
|
+
? await callSdk(() => api.traces.getTraceThread({
|
|
179
|
+
threadId,
|
|
180
|
+
...(resolved.projectId && { projectId: resolved.projectId }),
|
|
181
|
+
...(resolved.projectName && { projectName: resolved.projectName }),
|
|
182
|
+
}, getRequestOptions(workspaceName)))
|
|
183
|
+
: await callSdk(() => api.traces.getTraceThreads({
|
|
184
|
+
page: page || 1,
|
|
185
|
+
size: size || 10,
|
|
186
|
+
...(resolved.projectId && { projectId: resolved.projectId }),
|
|
187
|
+
...(resolved.projectName && { projectName: resolved.projectName }),
|
|
188
|
+
}, getRequestOptions(workspaceName)));
|
|
189
|
+
if (!response.data) {
|
|
190
|
+
return {
|
|
191
|
+
content: [{ type: 'text', text: response.error || 'Failed to fetch trace threads' }],
|
|
192
|
+
};
|
|
267
193
|
}
|
|
268
|
-
}
|
|
269
|
-
const response = await makeApiRequest('/v1/private/traces/search', {
|
|
270
|
-
method: 'POST',
|
|
271
|
-
body: JSON.stringify(searchBody),
|
|
272
|
-
}, workspaceName);
|
|
273
|
-
if (!response.data) {
|
|
274
194
|
return {
|
|
275
|
-
content: [
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: threadId
|
|
199
|
+
? `Thread details for ID: ${threadId}`
|
|
200
|
+
: `Found ${response.data.total || response.data.length || 0} trace threads`,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
type: 'text',
|
|
204
|
+
text: JSON.stringify(response.data, null, 2),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
276
207
|
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
208
|
+
}, {
|
|
209
|
+
title: 'Get Trace Threads',
|
|
210
|
+
annotations: {
|
|
211
|
+
readOnlyHint: true,
|
|
212
|
+
destructiveHint: false,
|
|
213
|
+
idempotentHint: true,
|
|
214
|
+
openWorldHint: false,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (includeExpertActions) {
|
|
219
|
+
registerTool(server, 'search-traces', 'Search traces with optional text query, structured filters, and sorting.', {
|
|
220
|
+
projectId: z.string().optional().describe('Optional project ID to constrain search.'),
|
|
221
|
+
projectName: z.string().optional().describe('Optional project name to constrain search.'),
|
|
222
|
+
query: z
|
|
223
|
+
.string()
|
|
224
|
+
.optional()
|
|
225
|
+
.describe('Optional free-text query across trace name/input/output/metadata.'),
|
|
226
|
+
filters: z
|
|
227
|
+
.record(z.any())
|
|
228
|
+
.optional()
|
|
229
|
+
.describe('Optional advanced filters, e.g. {"status":"error"} or {"duration_ms":{"$gt":1000}}.'),
|
|
230
|
+
page: pageSchema,
|
|
231
|
+
size: sizeSchema(10),
|
|
232
|
+
sortBy: z
|
|
233
|
+
.enum(['created_at', 'duration', 'name', 'status'])
|
|
234
|
+
.optional()
|
|
235
|
+
.describe('Optional sort field.'),
|
|
236
|
+
sortOrder: z.enum(['asc', 'desc']).optional().default('desc').describe('Sort direction.'),
|
|
237
|
+
workspaceName: workspaceNameSchema,
|
|
238
|
+
}, async (args) => {
|
|
239
|
+
const { projectId, projectName, query, filters, page, size, sortBy, sortOrder, workspaceName, } = args;
|
|
240
|
+
const resolved = await resolveProjectIdentifier(projectId, projectName, workspaceName);
|
|
241
|
+
if (resolved.error) {
|
|
242
|
+
return {
|
|
243
|
+
content: [{ type: 'text', text: `Error: ${resolved.error}` }],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
const sdkFilters = buildTraceFilters(query, filters);
|
|
247
|
+
const sorting = sortBy ? `${sortBy}:${sortOrder || 'desc'}` : undefined;
|
|
248
|
+
const api = getOpikApi();
|
|
249
|
+
const response = await callSdk(() => api.traces.getTracesByProject({
|
|
250
|
+
page: page || 1,
|
|
251
|
+
size: size || 10,
|
|
252
|
+
...(resolved.projectId && { projectId: resolved.projectId }),
|
|
253
|
+
...(resolved.projectName && { projectName: resolved.projectName }),
|
|
254
|
+
...(sdkFilters && { filters: sdkFilters }),
|
|
255
|
+
...(sorting && { sorting }),
|
|
256
|
+
}, getRequestOptions(workspaceName)));
|
|
257
|
+
if (!response.data) {
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: 'text', text: response.error || 'Failed to search traces' }],
|
|
260
|
+
};
|
|
320
261
|
}
|
|
321
|
-
|
|
262
|
+
return {
|
|
263
|
+
content: [
|
|
264
|
+
{
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: `Search found ${response.data.total} traces (page ${response.data.page} of ${Math.ceil(response.data.total / response.data.size)})`,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
type: 'text',
|
|
270
|
+
text: JSON.stringify(response.data.content, null, 2),
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
};
|
|
274
|
+
}, {
|
|
275
|
+
title: 'Search Traces',
|
|
276
|
+
annotations: {
|
|
277
|
+
readOnlyHint: true,
|
|
278
|
+
destructiveHint: false,
|
|
279
|
+
idempotentHint: true,
|
|
280
|
+
openWorldHint: false,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
registerTool(server, 'add-trace-feedback', 'Attach one or more feedback scores to a trace.', {
|
|
284
|
+
traceId: z.string().min(1).describe('Target trace ID.'),
|
|
285
|
+
scores: z
|
|
286
|
+
.array(z.object({
|
|
287
|
+
name: z
|
|
288
|
+
.string()
|
|
289
|
+
.min(1)
|
|
290
|
+
.describe('Feedback metric name, e.g. relevance, accuracy, helpfulness.'),
|
|
291
|
+
value: z.number().finite().describe('Numeric score value.'),
|
|
292
|
+
reason: z.string().optional().describe('Optional reason for this score.'),
|
|
293
|
+
source: z
|
|
294
|
+
.enum(['ui', 'sdk', 'online_scoring'])
|
|
295
|
+
.optional()
|
|
296
|
+
.default('sdk')
|
|
297
|
+
.describe('Feedback source.'),
|
|
298
|
+
categoryName: z
|
|
299
|
+
.string()
|
|
300
|
+
.optional()
|
|
301
|
+
.describe('Optional category for grouped feedback dimensions.'),
|
|
302
|
+
}))
|
|
303
|
+
.min(1)
|
|
304
|
+
.describe('One or more feedback score objects.'),
|
|
305
|
+
workspaceName: workspaceNameSchema,
|
|
306
|
+
}, async (args) => {
|
|
307
|
+
const { traceId, scores, workspaceName } = args;
|
|
308
|
+
if (!scores || !Array.isArray(scores) || scores.length === 0) {
|
|
322
309
|
return {
|
|
323
310
|
content: [
|
|
324
311
|
{
|
|
325
312
|
type: 'text',
|
|
326
|
-
text: 'Error:
|
|
313
|
+
text: 'Error: At least one feedback score is required. Format: [{"name": "relevance", "value": 0.8}]',
|
|
327
314
|
},
|
|
328
315
|
],
|
|
329
316
|
};
|
|
330
317
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
type: 'text',
|
|
352
|
-
text: JSON.stringify(response.data, null, 2),
|
|
353
|
-
},
|
|
354
|
-
],
|
|
355
|
-
};
|
|
356
|
-
});
|
|
357
|
-
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', {
|
|
358
|
-
traceId: z.string().describe('ID of the trace to add feedback to'),
|
|
359
|
-
scores: z
|
|
360
|
-
.array(z.object({
|
|
361
|
-
name: z
|
|
362
|
-
.string()
|
|
363
|
-
.describe('Name of the feedback metric (e.g., "relevance", "accuracy", "helpfulness", "quality")'),
|
|
364
|
-
value: z
|
|
365
|
-
.number()
|
|
366
|
-
.min(0)
|
|
367
|
-
.max(1)
|
|
368
|
-
.describe('Score value between 0.0 and 1.0 (0.0 = poor, 1.0 = excellent)'),
|
|
369
|
-
reason: z.string().optional().describe('Optional explanation for the score'),
|
|
370
|
-
}))
|
|
371
|
-
.describe('Array of feedback scores to add. Each score should have a name and value between 0-1'),
|
|
372
|
-
workspaceName: z.string().optional().describe('Workspace name to use instead of the default'),
|
|
373
|
-
}, async (args) => {
|
|
374
|
-
const { traceId, scores, workspaceName } = args;
|
|
375
|
-
// Validate scores format
|
|
376
|
-
if (!scores || !Array.isArray(scores) || scores.length === 0) {
|
|
318
|
+
const api = getOpikApi();
|
|
319
|
+
for (const score of scores) {
|
|
320
|
+
const response = await callSdk(() => api.traces.addTraceFeedbackScore(traceId, {
|
|
321
|
+
name: score.name,
|
|
322
|
+
value: score.value,
|
|
323
|
+
source: score.source || 'sdk',
|
|
324
|
+
...(score.reason && { reason: score.reason }),
|
|
325
|
+
...(score.categoryName && { categoryName: score.categoryName }),
|
|
326
|
+
}, getRequestOptions(workspaceName)));
|
|
327
|
+
if (response.error) {
|
|
328
|
+
return {
|
|
329
|
+
content: [
|
|
330
|
+
{
|
|
331
|
+
type: 'text',
|
|
332
|
+
text: `Error adding feedback: ${response.error}`,
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
377
338
|
return {
|
|
378
339
|
content: [
|
|
379
340
|
{
|
|
380
341
|
type: 'text',
|
|
381
|
-
text:
|
|
342
|
+
text: `Successfully added ${scores.length} feedback score(s) to trace ${traceId}`,
|
|
382
343
|
},
|
|
383
|
-
],
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
// Transform scores to the expected API format
|
|
387
|
-
const feedbackScores = scores.map((score) => ({
|
|
388
|
-
name: score.name,
|
|
389
|
-
value: score.value,
|
|
390
|
-
...(score.reason && { reason: score.reason }),
|
|
391
|
-
}));
|
|
392
|
-
const response = await makeApiRequest(`/v1/private/traces/${traceId}/feedback-scores`, {
|
|
393
|
-
method: 'PUT',
|
|
394
|
-
body: JSON.stringify({ scores: feedbackScores }),
|
|
395
|
-
}, workspaceName);
|
|
396
|
-
if (response.error) {
|
|
397
|
-
return {
|
|
398
|
-
content: [
|
|
399
344
|
{
|
|
400
345
|
type: 'text',
|
|
401
|
-
text: `
|
|
346
|
+
text: `Added scores: ${scores.map((s) => `${s.name}: ${s.value}`).join(', ')}`,
|
|
402
347
|
},
|
|
403
348
|
],
|
|
404
349
|
};
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
},
|
|
416
|
-
],
|
|
417
|
-
};
|
|
418
|
-
});
|
|
350
|
+
}, {
|
|
351
|
+
title: 'Add Trace Feedback',
|
|
352
|
+
annotations: {
|
|
353
|
+
readOnlyHint: false,
|
|
354
|
+
destructiveHint: false,
|
|
355
|
+
idempotentHint: false,
|
|
356
|
+
openWorldHint: false,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
419
360
|
return server;
|
|
420
361
|
};
|