@arizeai/phoenix-mcp 3.1.5 → 4.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 +20 -2
- package/build/annotationConfigTools.js +43 -0
- package/build/client.js +22 -0
- package/build/config.js +47 -0
- package/build/constants.js +61 -0
- package/build/datasetTools.js +123 -102
- package/build/datasetUtils.js +59 -0
- package/build/experimentTools.js +59 -78
- package/build/identifiers.js +77 -0
- package/build/index.js +16 -14
- package/build/pagination.js +25 -0
- package/build/projectTools.js +57 -19
- package/build/projectUtils.js +14 -0
- package/build/promptSchemas.js +26 -14
- package/build/promptTools.js +184 -303
- package/build/responseUtils.js +16 -0
- package/build/sessionTools.js +126 -0
- package/build/spanTools.js +92 -62
- package/build/spanUtils.js +232 -0
- package/build/supportTools.js +12 -9
- package/build/toolResults.js +32 -0
- package/build/traceTools.js +141 -0
- package/build/traceUtils.js +57 -0
- package/package.json +9 -6
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import { DEFAULT_TRACE_PAGE_SIZE, MAX_SESSION_PAGE_SIZE } from "./constants.js";
|
|
3
|
+
import { requireIdentifier } from "./identifiers.js";
|
|
4
|
+
import { fetchAllPages } from "./pagination.js";
|
|
5
|
+
import { resolveProjectIdentifier } from "./projectUtils.js";
|
|
6
|
+
import { getResponseData } from "./responseUtils.js";
|
|
7
|
+
import { jsonResponse } from "./toolResults.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Tool descriptions
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
const LIST_SESSIONS_DESCRIPTION = `List sessions for a project.
|
|
12
|
+
|
|
13
|
+
Sessions represent conversation flows grouped across traces.
|
|
14
|
+
|
|
15
|
+
Example usage:
|
|
16
|
+
Show me the last 10 sessions for project "default"
|
|
17
|
+
|
|
18
|
+
Expected return:
|
|
19
|
+
Array of session objects ordered by the requested sort order.`;
|
|
20
|
+
const GET_SESSION_DESCRIPTION = `Get a single session by GlobalID or user-provided session_id.
|
|
21
|
+
|
|
22
|
+
Example usage:
|
|
23
|
+
Show me session "chat-123"
|
|
24
|
+
|
|
25
|
+
Expected return:
|
|
26
|
+
A session object and, optionally, its annotations.`;
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
/**
|
|
31
|
+
* Fetch all annotations for a single session, paginating internally.
|
|
32
|
+
*/
|
|
33
|
+
async function fetchSessionAnnotations({ client, projectIdentifier, sessionId, }) {
|
|
34
|
+
const normalizedProjectIdentifier = requireIdentifier({
|
|
35
|
+
identifier: projectIdentifier,
|
|
36
|
+
label: "projectIdentifier",
|
|
37
|
+
});
|
|
38
|
+
return fetchAllPages({
|
|
39
|
+
limit: Infinity,
|
|
40
|
+
fetchPage: async (cursor, pageSize) => {
|
|
41
|
+
const query = {
|
|
42
|
+
session_ids: [sessionId],
|
|
43
|
+
limit: pageSize,
|
|
44
|
+
};
|
|
45
|
+
if (cursor) {
|
|
46
|
+
query.cursor = cursor;
|
|
47
|
+
}
|
|
48
|
+
const response = await client.GET("/v1/projects/{project_identifier}/session_annotations", {
|
|
49
|
+
params: {
|
|
50
|
+
path: { project_identifier: normalizedProjectIdentifier },
|
|
51
|
+
query,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const data = getResponseData({
|
|
55
|
+
response,
|
|
56
|
+
errorPrefix: `Failed to fetch annotations for session "${sessionId}"`,
|
|
57
|
+
});
|
|
58
|
+
return { data: data.data, nextCursor: data.next_cursor || undefined };
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Tool registration
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
/**
|
|
66
|
+
* Register session-related MCP tools on the given server.
|
|
67
|
+
*/
|
|
68
|
+
export const initializeSessionTools = ({ client, server, defaultProject, }) => {
|
|
69
|
+
server.tool("list-sessions", LIST_SESSIONS_DESCRIPTION, {
|
|
70
|
+
project_identifier: z.string().optional(),
|
|
71
|
+
limit: z
|
|
72
|
+
.number()
|
|
73
|
+
.min(1)
|
|
74
|
+
.max(MAX_SESSION_PAGE_SIZE)
|
|
75
|
+
.default(DEFAULT_TRACE_PAGE_SIZE),
|
|
76
|
+
order: z.enum(["asc", "desc"]).default("desc").optional(),
|
|
77
|
+
}, async ({ project_identifier, limit, order = "desc" }) => {
|
|
78
|
+
const normalizedProjectIdentifier = resolveProjectIdentifier({
|
|
79
|
+
projectIdentifier: project_identifier,
|
|
80
|
+
defaultProjectIdentifier: defaultProject,
|
|
81
|
+
});
|
|
82
|
+
const sessions = await fetchAllPages({
|
|
83
|
+
limit,
|
|
84
|
+
fetchPage: async (cursor, pageSize) => {
|
|
85
|
+
const response = await client.GET("/v1/projects/{project_identifier}/sessions", {
|
|
86
|
+
params: {
|
|
87
|
+
path: { project_identifier: normalizedProjectIdentifier },
|
|
88
|
+
query: { cursor, limit: pageSize, order },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const data = getResponseData({
|
|
92
|
+
response,
|
|
93
|
+
errorPrefix: `Failed to fetch sessions for project "${normalizedProjectIdentifier}"`,
|
|
94
|
+
});
|
|
95
|
+
return { data: data.data, nextCursor: data.next_cursor || undefined };
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return jsonResponse(sessions);
|
|
99
|
+
});
|
|
100
|
+
server.tool("get-session", GET_SESSION_DESCRIPTION, {
|
|
101
|
+
session_identifier: z.string(),
|
|
102
|
+
include_annotations: z.boolean().default(false).optional(),
|
|
103
|
+
}, async ({ session_identifier, include_annotations = false }) => {
|
|
104
|
+
const response = await client.GET("/v1/sessions/{session_identifier}", {
|
|
105
|
+
params: {
|
|
106
|
+
path: { session_identifier },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const session = getResponseData({
|
|
110
|
+
response,
|
|
111
|
+
errorPrefix: `Failed to fetch session "${session_identifier}"`,
|
|
112
|
+
}).data;
|
|
113
|
+
if (!include_annotations) {
|
|
114
|
+
return jsonResponse(session);
|
|
115
|
+
}
|
|
116
|
+
const annotations = await fetchSessionAnnotations({
|
|
117
|
+
client,
|
|
118
|
+
projectIdentifier: session.project_id,
|
|
119
|
+
sessionId: session.session_id,
|
|
120
|
+
});
|
|
121
|
+
return jsonResponse({
|
|
122
|
+
session,
|
|
123
|
+
annotations,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
};
|
package/build/spanTools.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import z from "zod";
|
|
2
|
+
import { DEFAULT_PAGE_SIZE, MAX_SPAN_QUERY_LIMIT } from "./constants.js";
|
|
3
|
+
import { resolveProjectIdentifier } from "./projectUtils.js";
|
|
4
|
+
import { getResponseData } from "./responseUtils.js";
|
|
5
|
+
import { attachAnnotationsToSpans, extractSpanIds, fetchProjectSpans, fetchSpanAnnotations, } from "./spanUtils.js";
|
|
6
|
+
import { jsonResponse } from "./toolResults.js";
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Tool descriptions
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
2
10
|
const GET_SPANS_DESCRIPTION = `Get spans from a project with filtering criteria.
|
|
3
11
|
|
|
4
12
|
Spans represent individual operations or units of work within a trace. They contain timing information,
|
|
@@ -59,89 +67,111 @@ Expected return:
|
|
|
59
67
|
],
|
|
60
68
|
"nextCursor": "cursor_for_pagination"
|
|
61
69
|
}`;
|
|
62
|
-
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Tool registration
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
/**
|
|
74
|
+
* Register span-related MCP tools on the given server.
|
|
75
|
+
*/
|
|
76
|
+
export const initializeSpanTools = ({ client, server, defaultProject, }) => {
|
|
63
77
|
server.tool("get-spans", GET_SPANS_DESCRIPTION, {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
project_identifier: z.string().optional(),
|
|
79
|
+
start_time: z.string().optional(),
|
|
80
|
+
end_time: z.string().optional(),
|
|
81
|
+
trace_ids: z.array(z.string()).optional(),
|
|
82
|
+
parent_id: z.string().nullable().optional(),
|
|
83
|
+
names: z.array(z.string()).optional(),
|
|
84
|
+
span_kinds: z.array(z.string()).optional(),
|
|
85
|
+
status_codes: z.array(z.enum(["OK", "ERROR", "UNSET"])).optional(),
|
|
68
86
|
cursor: z.string().optional(),
|
|
69
|
-
limit: z
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
limit: z
|
|
88
|
+
.number()
|
|
89
|
+
.min(1)
|
|
90
|
+
.max(MAX_SPAN_QUERY_LIMIT)
|
|
91
|
+
.default(DEFAULT_PAGE_SIZE)
|
|
92
|
+
.optional(),
|
|
93
|
+
include_annotations: z.boolean().default(false).optional(),
|
|
94
|
+
}, async ({ project_identifier, start_time, end_time, trace_ids, parent_id, names, span_kinds, status_codes, cursor, limit = DEFAULT_PAGE_SIZE, include_annotations = false, }) => {
|
|
95
|
+
const resolvedProjectIdentifier = resolveProjectIdentifier({
|
|
96
|
+
projectIdentifier: project_identifier,
|
|
97
|
+
defaultProjectIdentifier: defaultProject,
|
|
98
|
+
});
|
|
99
|
+
const response = await fetchProjectSpans({
|
|
100
|
+
client,
|
|
101
|
+
projectIdentifier: resolvedProjectIdentifier,
|
|
102
|
+
filters: {
|
|
103
|
+
cursor,
|
|
104
|
+
limit,
|
|
105
|
+
startTime: start_time,
|
|
106
|
+
endTime: end_time,
|
|
107
|
+
traceIds: trace_ids,
|
|
108
|
+
parentId: parent_id,
|
|
109
|
+
names,
|
|
110
|
+
spanKinds: span_kinds,
|
|
111
|
+
statusCodes: status_codes,
|
|
92
112
|
},
|
|
113
|
+
totalLimit: limit,
|
|
114
|
+
});
|
|
115
|
+
const spans = include_annotations
|
|
116
|
+
? attachAnnotationsToSpans({
|
|
117
|
+
spans: response.spans,
|
|
118
|
+
annotations: await fetchSpanAnnotations({
|
|
119
|
+
client,
|
|
120
|
+
projectIdentifier: resolvedProjectIdentifier,
|
|
121
|
+
spanIds: extractSpanIds(response.spans),
|
|
122
|
+
}),
|
|
123
|
+
})
|
|
124
|
+
: response.spans;
|
|
125
|
+
return jsonResponse({
|
|
126
|
+
spans,
|
|
127
|
+
nextCursor: response.nextCursor,
|
|
93
128
|
});
|
|
94
|
-
return {
|
|
95
|
-
content: [
|
|
96
|
-
{
|
|
97
|
-
type: "text",
|
|
98
|
-
text: JSON.stringify({
|
|
99
|
-
spans: response.data?.data ?? [],
|
|
100
|
-
nextCursor: response.data?.next_cursor ?? null,
|
|
101
|
-
}, null, 2),
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
129
|
});
|
|
106
130
|
server.tool("get-span-annotations", GET_SPAN_ANNOTATIONS_DESCRIPTION, {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
project_identifier: z.string().optional(),
|
|
132
|
+
span_ids: z.array(z.string()),
|
|
133
|
+
include_annotation_names: z.array(z.string()).optional(),
|
|
134
|
+
exclude_annotation_names: z.array(z.string()).optional(),
|
|
111
135
|
cursor: z.string().optional(),
|
|
112
|
-
limit: z
|
|
113
|
-
|
|
136
|
+
limit: z
|
|
137
|
+
.number()
|
|
138
|
+
.min(1)
|
|
139
|
+
.max(MAX_SPAN_QUERY_LIMIT)
|
|
140
|
+
.default(DEFAULT_PAGE_SIZE)
|
|
141
|
+
.optional(),
|
|
142
|
+
}, async ({ project_identifier, span_ids, include_annotation_names, exclude_annotation_names, cursor, limit = DEFAULT_PAGE_SIZE, }) => {
|
|
143
|
+
const resolvedProjectIdentifier = resolveProjectIdentifier({
|
|
144
|
+
projectIdentifier: project_identifier,
|
|
145
|
+
defaultProjectIdentifier: defaultProject,
|
|
146
|
+
});
|
|
114
147
|
const params = {
|
|
115
|
-
span_ids
|
|
148
|
+
span_ids,
|
|
116
149
|
limit,
|
|
117
150
|
};
|
|
118
151
|
if (cursor) {
|
|
119
152
|
params.cursor = cursor;
|
|
120
153
|
}
|
|
121
|
-
if (
|
|
122
|
-
params.include_annotation_names =
|
|
154
|
+
if (include_annotation_names) {
|
|
155
|
+
params.include_annotation_names = include_annotation_names;
|
|
123
156
|
}
|
|
124
|
-
if (
|
|
125
|
-
params.exclude_annotation_names =
|
|
157
|
+
if (exclude_annotation_names) {
|
|
158
|
+
params.exclude_annotation_names = exclude_annotation_names;
|
|
126
159
|
}
|
|
127
160
|
const response = await client.GET("/v1/projects/{project_identifier}/span_annotations", {
|
|
128
161
|
params: {
|
|
129
162
|
path: {
|
|
130
|
-
project_identifier:
|
|
163
|
+
project_identifier: resolvedProjectIdentifier,
|
|
131
164
|
},
|
|
132
165
|
query: params,
|
|
133
166
|
},
|
|
134
167
|
});
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
},
|
|
144
|
-
],
|
|
145
|
-
};
|
|
168
|
+
const data = getResponseData({
|
|
169
|
+
response,
|
|
170
|
+
errorPrefix: `Failed to fetch span annotations for project "${resolvedProjectIdentifier}"`,
|
|
171
|
+
});
|
|
172
|
+
return jsonResponse({
|
|
173
|
+
annotations: data.data,
|
|
174
|
+
nextCursor: data.next_cursor || null,
|
|
175
|
+
});
|
|
146
176
|
});
|
|
147
177
|
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { getSpans, } from "@arizeai/phoenix-client/spans";
|
|
2
|
+
import { ANNOTATION_CHUNK_SIZE, ANNOTATION_PAGE_SIZE, DEFAULT_PAGE_SIZE, MAX_CONCURRENT_ANNOTATION_REQUESTS, MAX_SPAN_QUERY_LIMIT, MS_PER_MINUTE, } from "./constants.js";
|
|
3
|
+
import { requireIdentifier } from "./identifiers.js";
|
|
4
|
+
import { getResponseData } from "./responseUtils.js";
|
|
5
|
+
/**
|
|
6
|
+
* Extract span IDs from an array of spans, filtering out any without context.
|
|
7
|
+
*/
|
|
8
|
+
export function extractSpanIds(spans) {
|
|
9
|
+
return spans
|
|
10
|
+
.map((span) => span.context?.span_id)
|
|
11
|
+
.filter((spanId) => Boolean(spanId));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Translate MCP span filters into the phoenix-client `getSpans` parameter shape.
|
|
15
|
+
*
|
|
16
|
+
* The MCP layer uses plural field names (`names`, `spanKinds`, `statusCodes`)
|
|
17
|
+
* while phoenix-client uses singular (`name`, `spanKind`, `statusCode`).
|
|
18
|
+
*/
|
|
19
|
+
export function buildProjectSpansRequest({ cursor, limit, startTime, endTime, traceIds, parentId, names, spanKinds, statusCodes, }) {
|
|
20
|
+
const request = {};
|
|
21
|
+
if (cursor) {
|
|
22
|
+
request.cursor = cursor;
|
|
23
|
+
}
|
|
24
|
+
if (limit !== undefined) {
|
|
25
|
+
request.limit = limit;
|
|
26
|
+
}
|
|
27
|
+
if (startTime) {
|
|
28
|
+
request.startTime = startTime;
|
|
29
|
+
}
|
|
30
|
+
if (endTime) {
|
|
31
|
+
request.endTime = endTime;
|
|
32
|
+
}
|
|
33
|
+
if (traceIds && traceIds.length > 0) {
|
|
34
|
+
request.traceIds = traceIds;
|
|
35
|
+
}
|
|
36
|
+
if (parentId !== undefined) {
|
|
37
|
+
request.parentId = parentId;
|
|
38
|
+
}
|
|
39
|
+
if (names && names.length > 0) {
|
|
40
|
+
request.name = names;
|
|
41
|
+
}
|
|
42
|
+
if (spanKinds && spanKinds.length > 0) {
|
|
43
|
+
request.spanKind = spanKinds;
|
|
44
|
+
}
|
|
45
|
+
if (statusCodes && statusCodes.length > 0) {
|
|
46
|
+
request.statusCode = statusCodes;
|
|
47
|
+
}
|
|
48
|
+
return request;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the lower-bound start time for trace and span listing tools.
|
|
52
|
+
*
|
|
53
|
+
* An explicit `since` ISO timestamp takes precedence. When only `lastNMinutes`
|
|
54
|
+
* is provided the start time is computed relative to `now`.
|
|
55
|
+
*
|
|
56
|
+
* @returns An ISO 8601 timestamp string, or `undefined` if no time constraint was given.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveStartTime({ since, lastNMinutes, now = new Date(), }) {
|
|
59
|
+
if (since) {
|
|
60
|
+
return since;
|
|
61
|
+
}
|
|
62
|
+
if (lastNMinutes === undefined) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return new Date(now.getTime() - lastNMinutes * MS_PER_MINUTE).toISOString();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Fetch spans for a project, paginating internally until `totalLimit` is
|
|
69
|
+
* reached or all matching spans have been retrieved.
|
|
70
|
+
*
|
|
71
|
+
* @param options.client - The Phoenix REST client.
|
|
72
|
+
* @param options.projectIdentifier - Project name or Relay GlobalID.
|
|
73
|
+
* @param options.filters - Span filter criteria.
|
|
74
|
+
* @param options.totalLimit - Cap on the total number of spans returned.
|
|
75
|
+
* @returns The collected spans and an optional cursor for further pagination.
|
|
76
|
+
*/
|
|
77
|
+
export async function fetchProjectSpans({ client, projectIdentifier, filters, totalLimit, }) {
|
|
78
|
+
const normalizedProjectIdentifier = requireIdentifier({
|
|
79
|
+
identifier: projectIdentifier,
|
|
80
|
+
label: "projectIdentifier",
|
|
81
|
+
});
|
|
82
|
+
const collectedSpans = [];
|
|
83
|
+
let cursor = filters.cursor;
|
|
84
|
+
const pageLimit = Math.min(totalLimit || filters.limit || DEFAULT_PAGE_SIZE, MAX_SPAN_QUERY_LIMIT);
|
|
85
|
+
do {
|
|
86
|
+
const response = await getSpans({
|
|
87
|
+
client,
|
|
88
|
+
project: { project: normalizedProjectIdentifier },
|
|
89
|
+
...buildProjectSpansRequest({
|
|
90
|
+
...filters,
|
|
91
|
+
cursor,
|
|
92
|
+
limit: pageLimit,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
collectedSpans.push(...response.spans);
|
|
96
|
+
cursor = response.nextCursor || undefined;
|
|
97
|
+
if (totalLimit !== undefined && collectedSpans.length >= totalLimit) {
|
|
98
|
+
return {
|
|
99
|
+
spans: collectedSpans.slice(0, totalLimit),
|
|
100
|
+
nextCursor: cursor || null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
} while (cursor);
|
|
104
|
+
return {
|
|
105
|
+
spans: collectedSpans,
|
|
106
|
+
nextCursor: cursor || null,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Split an array into chunks of at most `size` elements.
|
|
111
|
+
*/
|
|
112
|
+
function chunkArray(values, size) {
|
|
113
|
+
const chunks = [];
|
|
114
|
+
for (let index = 0; index < values.length; index += size) {
|
|
115
|
+
chunks.push(values.slice(index, index + size));
|
|
116
|
+
}
|
|
117
|
+
return chunks;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Fetch all span annotation pages for a single chunk of span IDs.
|
|
121
|
+
*
|
|
122
|
+
* Paginates internally until every annotation for the given span IDs
|
|
123
|
+
* has been collected.
|
|
124
|
+
*/
|
|
125
|
+
async function fetchSpanAnnotationsForChunk({ client, projectIdentifier, spanIds, includeAnnotationNames, excludeAnnotationNames, pageLimit, }) {
|
|
126
|
+
const annotations = [];
|
|
127
|
+
let cursor;
|
|
128
|
+
do {
|
|
129
|
+
const query = {
|
|
130
|
+
span_ids: spanIds,
|
|
131
|
+
limit: pageLimit,
|
|
132
|
+
};
|
|
133
|
+
if (cursor) {
|
|
134
|
+
query.cursor = cursor;
|
|
135
|
+
}
|
|
136
|
+
if (includeAnnotationNames && includeAnnotationNames.length > 0) {
|
|
137
|
+
query.include_annotation_names = includeAnnotationNames;
|
|
138
|
+
}
|
|
139
|
+
if (excludeAnnotationNames && excludeAnnotationNames.length > 0) {
|
|
140
|
+
query.exclude_annotation_names = excludeAnnotationNames;
|
|
141
|
+
}
|
|
142
|
+
const response = await client.GET("/v1/projects/{project_identifier}/span_annotations", {
|
|
143
|
+
params: {
|
|
144
|
+
path: {
|
|
145
|
+
project_identifier: projectIdentifier,
|
|
146
|
+
},
|
|
147
|
+
query,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
const data = getResponseData({
|
|
151
|
+
response,
|
|
152
|
+
errorPrefix: `Failed to fetch span annotations for project "${projectIdentifier}"`,
|
|
153
|
+
});
|
|
154
|
+
annotations.push(...data.data);
|
|
155
|
+
cursor = data.next_cursor || undefined;
|
|
156
|
+
} while (cursor);
|
|
157
|
+
return annotations;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Fetch span annotations in chunks to avoid overwhelming the annotations
|
|
161
|
+
* endpoint with very large span-ID lists.
|
|
162
|
+
*
|
|
163
|
+
* Span IDs are split into chunks of {@link ANNOTATION_CHUNK_SIZE} and
|
|
164
|
+
* fetched with a concurrency cap of {@link MAX_CONCURRENT_ANNOTATION_REQUESTS}.
|
|
165
|
+
*
|
|
166
|
+
* @param options.client - The Phoenix REST client.
|
|
167
|
+
* @param options.projectIdentifier - Project name or Relay GlobalID.
|
|
168
|
+
* @param options.spanIds - Span IDs to fetch annotations for.
|
|
169
|
+
* @param options.includeAnnotationNames - Only return annotations with these names.
|
|
170
|
+
* @param options.excludeAnnotationNames - Exclude annotations with these names.
|
|
171
|
+
* @param options.pageLimit - Per-page limit for annotation pagination.
|
|
172
|
+
* @param options.maxConcurrent - Maximum concurrent chunk requests.
|
|
173
|
+
*/
|
|
174
|
+
export async function fetchSpanAnnotations({ client, projectIdentifier, spanIds, includeAnnotationNames, excludeAnnotationNames, pageLimit = ANNOTATION_PAGE_SIZE, maxConcurrent = MAX_CONCURRENT_ANNOTATION_REQUESTS, }) {
|
|
175
|
+
if (spanIds.length === 0) {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
const normalizedProjectIdentifier = requireIdentifier({
|
|
179
|
+
identifier: projectIdentifier,
|
|
180
|
+
label: "projectIdentifier",
|
|
181
|
+
});
|
|
182
|
+
const uniqueSpanIds = Array.from(new Set(spanIds));
|
|
183
|
+
const chunks = chunkArray(uniqueSpanIds, ANNOTATION_CHUNK_SIZE);
|
|
184
|
+
const allAnnotations = [];
|
|
185
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += maxConcurrent) {
|
|
186
|
+
const batch = chunks.slice(chunkIndex, chunkIndex + maxConcurrent);
|
|
187
|
+
const batchResults = await Promise.all(batch.map((spanIdsChunk) => fetchSpanAnnotationsForChunk({
|
|
188
|
+
client,
|
|
189
|
+
projectIdentifier: normalizedProjectIdentifier,
|
|
190
|
+
spanIds: spanIdsChunk,
|
|
191
|
+
includeAnnotationNames,
|
|
192
|
+
excludeAnnotationNames,
|
|
193
|
+
pageLimit,
|
|
194
|
+
})));
|
|
195
|
+
for (const batchResult of batchResults) {
|
|
196
|
+
allAnnotations.push(...batchResult);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return allAnnotations;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Merge annotations onto their corresponding spans by `span_id`.
|
|
203
|
+
*
|
|
204
|
+
* Spans that have no matching annotations are returned unmodified.
|
|
205
|
+
* Spans with annotations gain an `annotations` property containing
|
|
206
|
+
* the full list.
|
|
207
|
+
*/
|
|
208
|
+
export function attachAnnotationsToSpans({ spans, annotations, }) {
|
|
209
|
+
const annotationsBySpanId = new Map();
|
|
210
|
+
for (const annotation of annotations) {
|
|
211
|
+
const spanAnnotations = annotationsBySpanId.get(annotation.span_id);
|
|
212
|
+
if (spanAnnotations) {
|
|
213
|
+
spanAnnotations.push(annotation);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
annotationsBySpanId.set(annotation.span_id, [annotation]);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return spans.map((span) => {
|
|
220
|
+
const spanId = span.context?.span_id;
|
|
221
|
+
const spanAnnotations = spanId
|
|
222
|
+
? annotationsBySpanId.get(spanId)
|
|
223
|
+
: undefined;
|
|
224
|
+
if (!spanAnnotations || spanAnnotations.length === 0) {
|
|
225
|
+
return span;
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
...span,
|
|
229
|
+
annotations: spanAnnotations,
|
|
230
|
+
};
|
|
231
|
+
});
|
|
232
|
+
}
|
package/build/supportTools.js
CHANGED
|
@@ -12,10 +12,16 @@ or best practices.
|
|
|
12
12
|
|
|
13
13
|
Expected return:
|
|
14
14
|
Expert guidance about how to use and integrate Phoenix`;
|
|
15
|
+
/** Cached RunLLM MCP client, lazily created on first use. */
|
|
16
|
+
let runLLMClient = null;
|
|
15
17
|
/**
|
|
16
|
-
*
|
|
18
|
+
* Return a cached MCP client connected to the RunLLM server.
|
|
19
|
+
* Creates and connects the client on first call; subsequent calls reuse it.
|
|
17
20
|
*/
|
|
18
|
-
async function
|
|
21
|
+
async function getRunLLMClient() {
|
|
22
|
+
if (runLLMClient) {
|
|
23
|
+
return runLLMClient;
|
|
24
|
+
}
|
|
19
25
|
const transport = new StreamableHTTPClientTransport(new URL("https://mcp.runllm.com/mcp"), {
|
|
20
26
|
requestInit: {
|
|
21
27
|
headers: {
|
|
@@ -28,21 +34,18 @@ async function createRunLLMClient() {
|
|
|
28
34
|
version: "1.0.0",
|
|
29
35
|
});
|
|
30
36
|
await client.connect(transport);
|
|
37
|
+
runLLMClient = client;
|
|
31
38
|
return client;
|
|
32
39
|
}
|
|
33
40
|
/**
|
|
34
|
-
* Calls the
|
|
41
|
+
* Calls the search tool on the RunLLM MCP server.
|
|
35
42
|
*/
|
|
36
43
|
export async function callRunLLMQuery({ query, }) {
|
|
37
|
-
const client = await
|
|
38
|
-
// Call the chat tool with the user's question
|
|
44
|
+
const client = await getRunLLMClient();
|
|
39
45
|
const result = await client.callTool({
|
|
40
46
|
name: "search",
|
|
41
|
-
arguments: {
|
|
42
|
-
query: query,
|
|
43
|
-
},
|
|
47
|
+
arguments: { query },
|
|
44
48
|
});
|
|
45
|
-
// There's usually only one content item, but we'll handle multiple for safety
|
|
46
49
|
if (result.content && Array.isArray(result.content)) {
|
|
47
50
|
const textContent = result.content
|
|
48
51
|
.filter((item) => item.type === "text")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wrap an arbitrary payload as a JSON-formatted MCP text content response.
|
|
3
|
+
*
|
|
4
|
+
* @param payload - The value to serialize. Will be pretty-printed with 2-space indentation.
|
|
5
|
+
* @returns An MCP tool result containing one text content block.
|
|
6
|
+
*/
|
|
7
|
+
export function jsonResponse(payload) {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: "text",
|
|
12
|
+
text: JSON.stringify(payload, null, 2),
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a plain string as an MCP text content response.
|
|
19
|
+
*
|
|
20
|
+
* @param text - The plain text to return.
|
|
21
|
+
* @returns An MCP tool result containing one text content block.
|
|
22
|
+
*/
|
|
23
|
+
export function textResponse(text) {
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|