@cephalization/phoenix-insight 0.1.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/LICENSE +201 -0
- package/README.md +620 -0
- package/dist/agent/index.js +230 -0
- package/dist/cli.js +640 -0
- package/dist/commands/index.js +2 -0
- package/dist/commands/px-fetch-more-spans.js +98 -0
- package/dist/commands/px-fetch-more-trace.js +110 -0
- package/dist/config/index.js +165 -0
- package/dist/config/loader.js +141 -0
- package/dist/config/schema.js +53 -0
- package/dist/index.js +1 -0
- package/dist/modes/index.js +17 -0
- package/dist/modes/local.js +134 -0
- package/dist/modes/sandbox.js +121 -0
- package/dist/modes/types.js +1 -0
- package/dist/observability/index.js +65 -0
- package/dist/progress.js +209 -0
- package/dist/prompts/index.js +1 -0
- package/dist/prompts/system.js +30 -0
- package/dist/snapshot/client.js +74 -0
- package/dist/snapshot/context.js +332 -0
- package/dist/snapshot/datasets.js +68 -0
- package/dist/snapshot/experiments.js +135 -0
- package/dist/snapshot/index.js +262 -0
- package/dist/snapshot/projects.js +44 -0
- package/dist/snapshot/prompts.js +199 -0
- package/dist/snapshot/spans.js +80 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -0
- package/package.json +75 -0
- package/src/agent/index.ts +323 -0
- package/src/cli.ts +782 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/px-fetch-more-spans.ts +174 -0
- package/src/commands/px-fetch-more-trace.ts +183 -0
- package/src/config/index.ts +225 -0
- package/src/config/loader.ts +173 -0
- package/src/config/schema.ts +66 -0
- package/src/index.ts +1 -0
- package/src/modes/index.ts +21 -0
- package/src/modes/local.ts +163 -0
- package/src/modes/sandbox.ts +144 -0
- package/src/modes/types.ts +31 -0
- package/src/observability/index.ts +90 -0
- package/src/progress.ts +239 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/system.ts +31 -0
- package/src/snapshot/client.ts +129 -0
- package/src/snapshot/context.ts +462 -0
- package/src/snapshot/datasets.ts +132 -0
- package/src/snapshot/experiments.ts +246 -0
- package/src/snapshot/index.ts +403 -0
- package/src/snapshot/projects.ts +58 -0
- package/src/snapshot/prompts.ts +267 -0
- package/src/snapshot/spans.ts +142 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Export all snapshot modules
|
|
2
|
+
export { createPhoenixClient, PhoenixClientError, } from "./client.js";
|
|
3
|
+
export { fetchProjects } from "./projects.js";
|
|
4
|
+
export { snapshotSpans } from "./spans.js";
|
|
5
|
+
export { fetchDatasets } from "./datasets.js";
|
|
6
|
+
export { fetchExperiments } from "./experiments.js";
|
|
7
|
+
export { fetchPrompts } from "./prompts.js";
|
|
8
|
+
export { generateContext } from "./context.js";
|
|
9
|
+
import { createPhoenixClient, PhoenixClientError, } from "./client.js";
|
|
10
|
+
import { fetchProjects } from "./projects.js";
|
|
11
|
+
import { snapshotSpans } from "./spans.js";
|
|
12
|
+
import { fetchDatasets } from "./datasets.js";
|
|
13
|
+
import { fetchExperiments } from "./experiments.js";
|
|
14
|
+
import { fetchPrompts } from "./prompts.js";
|
|
15
|
+
import { generateContext } from "./context.js";
|
|
16
|
+
import { SnapshotProgress } from "../progress.js";
|
|
17
|
+
/**
|
|
18
|
+
* Orchestrates all data fetchers to create a complete Phoenix snapshot
|
|
19
|
+
* @param mode - The execution mode (sandbox or local)
|
|
20
|
+
* @param options - Snapshot options including server URL and limits
|
|
21
|
+
*/
|
|
22
|
+
export async function createSnapshot(mode, options) {
|
|
23
|
+
const { baseURL, apiKey, spansPerProject = 1000, startTime, endTime, showProgress = false, } = options;
|
|
24
|
+
// Create progress indicator
|
|
25
|
+
const progress = new SnapshotProgress(showProgress);
|
|
26
|
+
progress.start("Creating Phoenix data snapshot");
|
|
27
|
+
// Create Phoenix client
|
|
28
|
+
const clientConfig = {
|
|
29
|
+
baseURL,
|
|
30
|
+
apiKey,
|
|
31
|
+
};
|
|
32
|
+
const client = createPhoenixClient(clientConfig);
|
|
33
|
+
try {
|
|
34
|
+
// 1. Fetch projects first (required for spans)
|
|
35
|
+
progress.update("Fetching projects");
|
|
36
|
+
try {
|
|
37
|
+
await fetchProjects(client, mode);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
progress.fail("Failed to fetch projects");
|
|
41
|
+
throw new PhoenixClientError(`Failed to fetch projects: ${error instanceof Error ? error.message : String(error)}`, error instanceof PhoenixClientError ? error.code : "UNKNOWN_ERROR", error);
|
|
42
|
+
}
|
|
43
|
+
// 2. Fetch spans and other data in parallel
|
|
44
|
+
progress.update("Fetching all data", "spans, datasets, experiments, prompts");
|
|
45
|
+
const spansOptions = {
|
|
46
|
+
spansPerProject,
|
|
47
|
+
startTime,
|
|
48
|
+
endTime,
|
|
49
|
+
};
|
|
50
|
+
// Fetch all data types in parallel for better performance
|
|
51
|
+
const results = await Promise.allSettled([
|
|
52
|
+
snapshotSpans(client, mode, spansOptions),
|
|
53
|
+
fetchDatasets(client, mode),
|
|
54
|
+
fetchExperiments(client, mode),
|
|
55
|
+
fetchPrompts(client, mode),
|
|
56
|
+
]);
|
|
57
|
+
// Check for failures and collect errors
|
|
58
|
+
const errors = [];
|
|
59
|
+
const dataTypes = ["spans", "datasets", "experiments", "prompts"];
|
|
60
|
+
results.forEach((result, index) => {
|
|
61
|
+
if (result.status === "rejected") {
|
|
62
|
+
errors.push({
|
|
63
|
+
type: dataTypes[index] || "unknown",
|
|
64
|
+
error: result.reason,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (errors.length > 0) {
|
|
69
|
+
// Log individual errors
|
|
70
|
+
errors.forEach(({ type, error }) => {
|
|
71
|
+
console.error(`Warning: Failed to fetch ${type}:`, error instanceof Error ? error.message : String(error));
|
|
72
|
+
});
|
|
73
|
+
// If spans failed, that's critical - throw error
|
|
74
|
+
if (errors.some((e) => e.type === "spans")) {
|
|
75
|
+
progress.fail("Failed to fetch spans");
|
|
76
|
+
throw new PhoenixClientError(`Failed to fetch spans: ${errors.find((e) => e.type === "spans")?.error}`, "UNKNOWN_ERROR", errors);
|
|
77
|
+
}
|
|
78
|
+
// If all other data failed, throw error. If partial success, continue with warning
|
|
79
|
+
if (errors.length === 4) {
|
|
80
|
+
progress.fail("Failed to fetch all data");
|
|
81
|
+
throw new PhoenixClientError("Failed to fetch all data types", "UNKNOWN_ERROR", errors);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 4. Generate context file
|
|
85
|
+
progress.update("Generating context");
|
|
86
|
+
await generateContext(mode, {
|
|
87
|
+
phoenixUrl: baseURL,
|
|
88
|
+
snapshotTime: new Date(),
|
|
89
|
+
spansPerProject,
|
|
90
|
+
});
|
|
91
|
+
// 5. Write metadata file
|
|
92
|
+
progress.update("Writing metadata");
|
|
93
|
+
const metadata = {
|
|
94
|
+
created_at: new Date().toISOString(),
|
|
95
|
+
phoenix_url: baseURL,
|
|
96
|
+
cursors: {
|
|
97
|
+
spans: {}, // TODO: Track span cursors when span fetching supports it
|
|
98
|
+
datasets: { last_fetch: new Date().toISOString() },
|
|
99
|
+
experiments: { last_fetch: new Date().toISOString() },
|
|
100
|
+
prompts: { last_fetch: new Date().toISOString() },
|
|
101
|
+
},
|
|
102
|
+
limits: {
|
|
103
|
+
spans_per_project: spansPerProject,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
await mode.writeFile("/_meta/snapshot.json", JSON.stringify(metadata, null, 2));
|
|
107
|
+
progress.succeed("✅ Snapshot created successfully!");
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// Stop progress if not already stopped
|
|
111
|
+
progress.stop();
|
|
112
|
+
// Enhance error with context before rethrowing
|
|
113
|
+
if (error instanceof PhoenixClientError) {
|
|
114
|
+
throw error; // Already has good context
|
|
115
|
+
}
|
|
116
|
+
throw new PhoenixClientError(`Failed to create snapshot: ${error instanceof Error ? error.message : String(error)}`, "UNKNOWN_ERROR", error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Loads existing snapshot metadata if available
|
|
121
|
+
* @param mode - The execution mode (sandbox or local)
|
|
122
|
+
* @returns The snapshot metadata or null if not found
|
|
123
|
+
*/
|
|
124
|
+
export async function loadSnapshotMetadata(mode) {
|
|
125
|
+
try {
|
|
126
|
+
const result = await mode.exec("cat /phoenix/_meta/snapshot.json 2>/dev/null");
|
|
127
|
+
if (result.exitCode === 0) {
|
|
128
|
+
return JSON.parse(result.stdout);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
// File doesn't exist or parse error
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Creates an incremental snapshot, fetching only new/updated data
|
|
138
|
+
* @param mode - The execution mode (sandbox or local)
|
|
139
|
+
* @param options - Snapshot options including server URL and limits
|
|
140
|
+
*/
|
|
141
|
+
export async function createIncrementalSnapshot(mode, options) {
|
|
142
|
+
// Load existing metadata to get cursors
|
|
143
|
+
const existingMetadata = await loadSnapshotMetadata(mode);
|
|
144
|
+
if (!existingMetadata) {
|
|
145
|
+
// No existing snapshot, create a full one
|
|
146
|
+
await createSnapshot(mode, options);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const { baseURL, apiKey, spansPerProject = 1000, showProgress = false, } = options;
|
|
150
|
+
// Create progress indicator
|
|
151
|
+
const progress = new SnapshotProgress(showProgress);
|
|
152
|
+
progress.start("Updating Phoenix data snapshot");
|
|
153
|
+
// Create Phoenix client
|
|
154
|
+
const clientConfig = {
|
|
155
|
+
baseURL,
|
|
156
|
+
apiKey,
|
|
157
|
+
};
|
|
158
|
+
const client = createPhoenixClient(clientConfig);
|
|
159
|
+
try {
|
|
160
|
+
// Show time since last snapshot
|
|
161
|
+
const lastSnapshotDate = new Date(existingMetadata.created_at);
|
|
162
|
+
const timeSince = formatTimeSince(lastSnapshotDate);
|
|
163
|
+
progress.update("Checking for updates", `last snapshot ${timeSince} ago`);
|
|
164
|
+
// For incremental updates, we'll need to:
|
|
165
|
+
// 1. Fetch projects (always fetch all as they're small)
|
|
166
|
+
progress.update("Updating projects");
|
|
167
|
+
await fetchProjects(client, mode);
|
|
168
|
+
// 2. Fetch spans and other data in parallel for better performance
|
|
169
|
+
progress.update("Fetching updates", "new spans and refreshing other data");
|
|
170
|
+
const spansOptions = {
|
|
171
|
+
spansPerProject,
|
|
172
|
+
// Use the last end time from previous snapshot as start time
|
|
173
|
+
startTime: existingMetadata.cursors.spans
|
|
174
|
+
? Object.values(existingMetadata.cursors.spans)
|
|
175
|
+
.map((cursor) => cursor.last_end_time)
|
|
176
|
+
.filter(Boolean)
|
|
177
|
+
.sort()
|
|
178
|
+
.pop()
|
|
179
|
+
: undefined,
|
|
180
|
+
};
|
|
181
|
+
// For datasets/experiments/prompts, check if they've been updated
|
|
182
|
+
const datasetsLastFetch = existingMetadata.cursors.datasets?.last_fetch;
|
|
183
|
+
const experimentsLastFetch = existingMetadata.cursors.experiments?.last_fetch;
|
|
184
|
+
const promptsLastFetch = existingMetadata.cursors.prompts?.last_fetch;
|
|
185
|
+
// Fetch all data types in parallel
|
|
186
|
+
// For now, we'll refetch all as the API doesn't support filtering by updated_at
|
|
187
|
+
// In a future enhancement, we could check individual items for updates
|
|
188
|
+
const updateResults = await Promise.allSettled([
|
|
189
|
+
snapshotSpans(client, mode, spansOptions),
|
|
190
|
+
fetchDatasets(client, mode),
|
|
191
|
+
fetchExperiments(client, mode),
|
|
192
|
+
fetchPrompts(client, mode),
|
|
193
|
+
]);
|
|
194
|
+
// Check for critical errors
|
|
195
|
+
const updateErrors = [];
|
|
196
|
+
const updateDataTypes = ["spans", "datasets", "experiments", "prompts"];
|
|
197
|
+
updateResults.forEach((result, index) => {
|
|
198
|
+
if (result.status === "rejected") {
|
|
199
|
+
updateErrors.push({
|
|
200
|
+
type: updateDataTypes[index] || "unknown",
|
|
201
|
+
error: result.reason,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
if (updateErrors.length > 0) {
|
|
206
|
+
// Log individual errors
|
|
207
|
+
updateErrors.forEach(({ type, error }) => {
|
|
208
|
+
console.error(`Warning: Failed to update ${type}:`, error instanceof Error ? error.message : String(error));
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
// 4. Regenerate context with updated data
|
|
212
|
+
progress.update("Regenerating context");
|
|
213
|
+
await generateContext(mode, {
|
|
214
|
+
phoenixUrl: baseURL,
|
|
215
|
+
snapshotTime: new Date(),
|
|
216
|
+
spansPerProject,
|
|
217
|
+
});
|
|
218
|
+
// 5. Update metadata
|
|
219
|
+
progress.update("Updating metadata");
|
|
220
|
+
const updatedSpansCursors = existingMetadata.cursors.spans || {};
|
|
221
|
+
const metadata = {
|
|
222
|
+
created_at: new Date().toISOString(),
|
|
223
|
+
phoenix_url: baseURL,
|
|
224
|
+
cursors: {
|
|
225
|
+
spans: updatedSpansCursors,
|
|
226
|
+
datasets: { last_fetch: new Date().toISOString() },
|
|
227
|
+
experiments: { last_fetch: new Date().toISOString() },
|
|
228
|
+
prompts: { last_fetch: new Date().toISOString() },
|
|
229
|
+
},
|
|
230
|
+
limits: {
|
|
231
|
+
spans_per_project: spansPerProject,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
await mode.writeFile("/_meta/snapshot.json", JSON.stringify(metadata, null, 2));
|
|
235
|
+
progress.succeed("✅ Incremental update complete!");
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// Stop progress if not already stopped
|
|
239
|
+
progress.stop();
|
|
240
|
+
// Enhance error with context before rethrowing
|
|
241
|
+
if (error instanceof PhoenixClientError) {
|
|
242
|
+
throw error; // Already has good context
|
|
243
|
+
}
|
|
244
|
+
throw new PhoenixClientError(`Failed to create incremental snapshot: ${error instanceof Error ? error.message : String(error)}`, "UNKNOWN_ERROR", error);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Format time since a date in human-readable format
|
|
249
|
+
*/
|
|
250
|
+
function formatTimeSince(date) {
|
|
251
|
+
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
|
|
252
|
+
if (seconds < 60)
|
|
253
|
+
return `${seconds}s`;
|
|
254
|
+
const minutes = Math.floor(seconds / 60);
|
|
255
|
+
if (minutes < 60)
|
|
256
|
+
return `${minutes}m`;
|
|
257
|
+
const hours = Math.floor(minutes / 60);
|
|
258
|
+
if (hours < 24)
|
|
259
|
+
return `${hours}h`;
|
|
260
|
+
const days = Math.floor(hours / 24);
|
|
261
|
+
return `${days}d`;
|
|
262
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { withErrorHandling } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Converts an array of items to JSONL format (one JSON object per line)
|
|
4
|
+
*/
|
|
5
|
+
function toJSONL(items) {
|
|
6
|
+
return items.map((item) => JSON.stringify(item)).join("\n");
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Fetches all projects and writes them to the filesystem
|
|
10
|
+
* @param client - The Phoenix client instance
|
|
11
|
+
* @param mode - The execution mode (sandbox or local)
|
|
12
|
+
*/
|
|
13
|
+
export async function fetchProjects(client, mode) {
|
|
14
|
+
// Fetch all projects with error handling
|
|
15
|
+
const projectsData = await withErrorHandling(async () => {
|
|
16
|
+
const response = await client.GET("/v1/projects", {
|
|
17
|
+
params: {
|
|
18
|
+
query: {
|
|
19
|
+
include_experiment_projects: false,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
if (!response.data) {
|
|
24
|
+
throw new Error("No data returned from projects endpoint");
|
|
25
|
+
}
|
|
26
|
+
return response.data;
|
|
27
|
+
}, "fetching projects");
|
|
28
|
+
// Extract projects from the response
|
|
29
|
+
const projects = projectsData.data || [];
|
|
30
|
+
// Write projects list as JSONL to /phoenix/projects/index.jsonl
|
|
31
|
+
const projectsPath = "/phoenix/projects/index.jsonl";
|
|
32
|
+
await mode.writeFile(projectsPath, toJSONL(projects));
|
|
33
|
+
// For each project, create a metadata.json file
|
|
34
|
+
for (const project of projects) {
|
|
35
|
+
const projectDir = `/phoenix/projects/${project.name}`;
|
|
36
|
+
const metadataPath = `${projectDir}/metadata.json`;
|
|
37
|
+
// Write project metadata
|
|
38
|
+
await mode.writeFile(metadataPath, JSON.stringify(project, null, 2));
|
|
39
|
+
// Create empty spans directory (will be populated by snapshot-spans task)
|
|
40
|
+
const spansDir = `${projectDir}/spans`;
|
|
41
|
+
// Create directory by writing a placeholder that will be overwritten later
|
|
42
|
+
await mode.writeFile(`${spansDir}/.gitkeep`, "");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { withErrorHandling, extractData } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Converts an array to JSONL format
|
|
4
|
+
*/
|
|
5
|
+
function toJSONL(items) {
|
|
6
|
+
if (items.length === 0) {
|
|
7
|
+
return "";
|
|
8
|
+
}
|
|
9
|
+
return items.map((item) => JSON.stringify(item)).join("\n");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Converts a prompt template to markdown format
|
|
13
|
+
*/
|
|
14
|
+
function templateToMarkdown(template, templateFormat) {
|
|
15
|
+
// Handle string template
|
|
16
|
+
if (templateFormat === "STRING") {
|
|
17
|
+
if (typeof template === "string") {
|
|
18
|
+
return template;
|
|
19
|
+
}
|
|
20
|
+
// It might be wrapped in an object with type
|
|
21
|
+
if (template?.template && typeof template.template === "string") {
|
|
22
|
+
return template.template;
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(template, null, 2);
|
|
25
|
+
}
|
|
26
|
+
// Handle chat template
|
|
27
|
+
const messages = template?.messages || [];
|
|
28
|
+
const lines = ["# Chat Template", ""];
|
|
29
|
+
for (const message of messages) {
|
|
30
|
+
lines.push(`## ${message.role}`);
|
|
31
|
+
lines.push("");
|
|
32
|
+
if (typeof message.content === "string") {
|
|
33
|
+
lines.push(message.content);
|
|
34
|
+
}
|
|
35
|
+
else if (Array.isArray(message.content)) {
|
|
36
|
+
// Handle multi-part content
|
|
37
|
+
for (const part of message.content) {
|
|
38
|
+
if (part.type === "text" && part.text) {
|
|
39
|
+
lines.push(part.text);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// For non-text parts, show as JSON
|
|
43
|
+
lines.push("```json");
|
|
44
|
+
lines.push(JSON.stringify(part, null, 2));
|
|
45
|
+
lines.push("```");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// If content is not string or array, show as JSON
|
|
51
|
+
lines.push("```json");
|
|
52
|
+
lines.push(JSON.stringify(message.content, null, 2));
|
|
53
|
+
lines.push("```");
|
|
54
|
+
}
|
|
55
|
+
lines.push("");
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Creates a markdown representation of a prompt version
|
|
61
|
+
*/
|
|
62
|
+
function createVersionMarkdown(version) {
|
|
63
|
+
const lines = [];
|
|
64
|
+
// Add metadata header
|
|
65
|
+
lines.push("---");
|
|
66
|
+
lines.push(`id: ${version.id}`);
|
|
67
|
+
if (version.model_name)
|
|
68
|
+
lines.push(`model_name: ${version.model_name}`);
|
|
69
|
+
if (version.model_provider)
|
|
70
|
+
lines.push(`model_provider: ${version.model_provider}`);
|
|
71
|
+
if (version.template_format)
|
|
72
|
+
lines.push(`template_format: ${version.template_format}`);
|
|
73
|
+
if (version.description)
|
|
74
|
+
lines.push(`description: ${version.description}`);
|
|
75
|
+
lines.push("---");
|
|
76
|
+
lines.push("");
|
|
77
|
+
// Add template content
|
|
78
|
+
if (version.template) {
|
|
79
|
+
lines.push(templateToMarkdown(version.template, version.template_format || "STRING"));
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
// Add invocation parameters if present
|
|
83
|
+
if (version.invocation_parameters) {
|
|
84
|
+
lines.push("## Invocation Parameters");
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("```json");
|
|
87
|
+
lines.push(JSON.stringify(version.invocation_parameters, null, 2));
|
|
88
|
+
lines.push("```");
|
|
89
|
+
lines.push("");
|
|
90
|
+
}
|
|
91
|
+
// Add tools if present
|
|
92
|
+
if (version.tools) {
|
|
93
|
+
lines.push("## Tools");
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("```json");
|
|
96
|
+
lines.push(JSON.stringify(version.tools, null, 2));
|
|
97
|
+
lines.push("```");
|
|
98
|
+
lines.push("");
|
|
99
|
+
}
|
|
100
|
+
// Add response format if present
|
|
101
|
+
if (version.response_format) {
|
|
102
|
+
lines.push("## Response Format");
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push("```json");
|
|
105
|
+
lines.push(JSON.stringify(version.response_format, null, 2));
|
|
106
|
+
lines.push("```");
|
|
107
|
+
lines.push("");
|
|
108
|
+
}
|
|
109
|
+
return lines.join("\n");
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Fetches all prompts and their versions from Phoenix
|
|
113
|
+
*/
|
|
114
|
+
export async function fetchPrompts(client, mode, options = {}) {
|
|
115
|
+
const { limit = 100 } = options;
|
|
116
|
+
// Fetch all prompts with pagination
|
|
117
|
+
const prompts = [];
|
|
118
|
+
let cursor = null;
|
|
119
|
+
while (prompts.length < limit) {
|
|
120
|
+
const query = {
|
|
121
|
+
limit: Math.min(limit - prompts.length, 100),
|
|
122
|
+
};
|
|
123
|
+
if (cursor) {
|
|
124
|
+
query.cursor = cursor;
|
|
125
|
+
}
|
|
126
|
+
const response = await withErrorHandling(() => client.GET("/v1/prompts", { params: { query } }), "fetching prompts");
|
|
127
|
+
const data = extractData(response);
|
|
128
|
+
prompts.push(...data.data);
|
|
129
|
+
cursor = data.next_cursor;
|
|
130
|
+
// Stop if no more data
|
|
131
|
+
if (!cursor || data.data.length === 0) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Write prompts index
|
|
136
|
+
await mode.writeFile("/phoenix/prompts/index.jsonl", toJSONL(prompts));
|
|
137
|
+
// Fetch versions for each prompt
|
|
138
|
+
for (const prompt of prompts) {
|
|
139
|
+
const safePromptName = prompt.name.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
140
|
+
// Write prompt metadata
|
|
141
|
+
await mode.writeFile(`/phoenix/prompts/${safePromptName}/metadata.json`, JSON.stringify({
|
|
142
|
+
id: prompt.id,
|
|
143
|
+
name: prompt.name,
|
|
144
|
+
description: prompt.description || null,
|
|
145
|
+
metadata: prompt.metadata || {},
|
|
146
|
+
source_prompt_id: prompt.source_prompt_id || null,
|
|
147
|
+
snapshot_timestamp: new Date().toISOString(),
|
|
148
|
+
}, null, 2));
|
|
149
|
+
// Fetch all versions for this prompt
|
|
150
|
+
const versions = [];
|
|
151
|
+
let versionCursor = null;
|
|
152
|
+
while (true) {
|
|
153
|
+
const versionQuery = {
|
|
154
|
+
limit: 100,
|
|
155
|
+
};
|
|
156
|
+
if (versionCursor) {
|
|
157
|
+
versionQuery.cursor = versionCursor;
|
|
158
|
+
}
|
|
159
|
+
const versionsResponse = await withErrorHandling(() => client.GET("/v1/prompts/{prompt_identifier}/versions", {
|
|
160
|
+
params: {
|
|
161
|
+
path: { prompt_identifier: prompt.id },
|
|
162
|
+
query: versionQuery,
|
|
163
|
+
},
|
|
164
|
+
}), `fetching versions for prompt ${prompt.name}`);
|
|
165
|
+
const versionsData = extractData(versionsResponse);
|
|
166
|
+
versions.push(...versionsData.data);
|
|
167
|
+
versionCursor = versionsData.next_cursor;
|
|
168
|
+
// Stop if no more data
|
|
169
|
+
if (!versionCursor || versionsData.data.length === 0) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Write versions index as JSONL
|
|
174
|
+
await mode.writeFile(`/phoenix/prompts/${safePromptName}/versions/index.jsonl`, toJSONL(versions));
|
|
175
|
+
// Write each version as markdown
|
|
176
|
+
for (const version of versions) {
|
|
177
|
+
const versionId = version.id.replace(/[^a-zA-Z0-9-_]/g, "_");
|
|
178
|
+
const markdownContent = createVersionMarkdown(version);
|
|
179
|
+
await mode.writeFile(`/phoenix/prompts/${safePromptName}/versions/${versionId}.md`, markdownContent);
|
|
180
|
+
}
|
|
181
|
+
// Fetch and save the latest version separately for convenience
|
|
182
|
+
try {
|
|
183
|
+
const latestResponse = await withErrorHandling(() => client.GET("/v1/prompts/{prompt_identifier}/latest", {
|
|
184
|
+
params: {
|
|
185
|
+
path: { prompt_identifier: prompt.id },
|
|
186
|
+
},
|
|
187
|
+
}), `fetching latest version for prompt ${prompt.name}`);
|
|
188
|
+
const latestData = extractData(latestResponse);
|
|
189
|
+
if (latestData.data) {
|
|
190
|
+
const latestMarkdownContent = createVersionMarkdown(latestData.data);
|
|
191
|
+
await mode.writeFile(`/phoenix/prompts/${safePromptName}/latest.md`, latestMarkdownContent);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
// If there's no latest version, that's okay - just skip it
|
|
196
|
+
console.warn(`No latest version available for prompt ${prompt.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { withErrorHandling } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetches spans for all projects and writes them to the snapshot
|
|
4
|
+
*
|
|
5
|
+
* @param client - Phoenix client instance
|
|
6
|
+
* @param mode - Execution mode for file operations
|
|
7
|
+
* @param options - Options for filtering and limiting spans
|
|
8
|
+
*/
|
|
9
|
+
export async function snapshotSpans(client, mode, options = {}) {
|
|
10
|
+
const { startTime, endTime, spansPerProject = 1000 } = options;
|
|
11
|
+
// Read projects index to get project names
|
|
12
|
+
const projectsIndexContent = await mode.exec("cat /phoenix/projects/index.jsonl");
|
|
13
|
+
if (!projectsIndexContent.stdout) {
|
|
14
|
+
// No projects, nothing to do
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const projectNames = projectsIndexContent.stdout
|
|
18
|
+
.trim()
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter((line) => line.length > 0)
|
|
21
|
+
.map((line) => {
|
|
22
|
+
const project = JSON.parse(line);
|
|
23
|
+
return project.name;
|
|
24
|
+
});
|
|
25
|
+
// Fetch spans for each project
|
|
26
|
+
for (const projectName of projectNames) {
|
|
27
|
+
await withErrorHandling(async () => {
|
|
28
|
+
const spans = [];
|
|
29
|
+
let cursor = null;
|
|
30
|
+
let totalFetched = 0;
|
|
31
|
+
// Paginate through spans until we reach the limit or no more data
|
|
32
|
+
while (totalFetched < spansPerProject) {
|
|
33
|
+
const query = {
|
|
34
|
+
limit: Math.min(100, spansPerProject - totalFetched), // Fetch in chunks of 100
|
|
35
|
+
};
|
|
36
|
+
if (cursor) {
|
|
37
|
+
query.cursor = cursor;
|
|
38
|
+
}
|
|
39
|
+
if (startTime) {
|
|
40
|
+
query.start_time =
|
|
41
|
+
startTime instanceof Date ? startTime.toISOString() : startTime;
|
|
42
|
+
}
|
|
43
|
+
if (endTime) {
|
|
44
|
+
query.end_time =
|
|
45
|
+
endTime instanceof Date ? endTime.toISOString() : endTime;
|
|
46
|
+
}
|
|
47
|
+
const response = await client.GET("/v1/projects/{project_identifier}/spans", {
|
|
48
|
+
params: {
|
|
49
|
+
path: {
|
|
50
|
+
project_identifier: projectName,
|
|
51
|
+
},
|
|
52
|
+
query,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (response.error)
|
|
56
|
+
throw response.error;
|
|
57
|
+
const data = response.data?.data ?? [];
|
|
58
|
+
spans.push(...data);
|
|
59
|
+
totalFetched += data.length;
|
|
60
|
+
cursor = response.data?.next_cursor ?? null;
|
|
61
|
+
// Stop if there's no more data
|
|
62
|
+
if (!cursor || data.length === 0) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Write spans to JSONL file
|
|
67
|
+
const jsonlContent = spans.map((span) => JSON.stringify(span)).join("\n");
|
|
68
|
+
await mode.writeFile(`/phoenix/projects/${projectName}/spans/index.jsonl`, jsonlContent);
|
|
69
|
+
// Write metadata about the spans snapshot
|
|
70
|
+
const metadata = {
|
|
71
|
+
project: projectName,
|
|
72
|
+
spanCount: spans.length,
|
|
73
|
+
startTime: startTime || null,
|
|
74
|
+
endTime: endTime || null,
|
|
75
|
+
snapshotTime: new Date().toISOString(),
|
|
76
|
+
};
|
|
77
|
+
await mode.writeFile(`/phoenix/projects/${projectName}/spans/metadata.json`, JSON.stringify(metadata, null, 2));
|
|
78
|
+
}, `fetching spans for project ${projectName}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/cli.ts","../src/index.ts","../src/progress.ts","../src/agent/index.ts","../src/commands/index.ts","../src/commands/px-fetch-more-spans.ts","../src/commands/px-fetch-more-trace.ts","../src/config/index.ts","../src/config/loader.ts","../src/config/schema.ts","../src/modes/index.ts","../src/modes/local.ts","../src/modes/sandbox.ts","../src/modes/types.ts","../src/observability/index.ts","../src/prompts/index.ts","../src/prompts/system.ts","../src/snapshot/client.ts","../src/snapshot/context.ts","../src/snapshot/datasets.ts","../src/snapshot/experiments.ts","../src/snapshot/index.ts","../src/snapshot/projects.ts","../src/snapshot/prompts.ts","../src/snapshot/spans.ts"],"version":"5.9.3"}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cephalization/phoenix-insight",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A CLI for Arize AI Phoenix data analysis with AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"phoenix-insight": "./dist/cli.js",
|
|
14
|
+
"pxi": "./dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src",
|
|
22
|
+
"package.json"
|
|
23
|
+
],
|
|
24
|
+
"keywords": [
|
|
25
|
+
"phoenix",
|
|
26
|
+
"cli",
|
|
27
|
+
"insight",
|
|
28
|
+
"arize",
|
|
29
|
+
"llmops",
|
|
30
|
+
"ai"
|
|
31
|
+
],
|
|
32
|
+
"author": "oss@arize.com",
|
|
33
|
+
"license": "Apache-2.0",
|
|
34
|
+
"homepage": "https://github.com/cephalization/phoenix-insight#readme",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/cephalization/phoenix-insight.git"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/cephalization/phoenix-insight/issues"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@ai-sdk/anthropic": "^3.0.0",
|
|
44
|
+
"@arizeai/phoenix-client": "^5.6.1",
|
|
45
|
+
"@arizeai/phoenix-otel": "^0.3.4",
|
|
46
|
+
"@opentelemetry/api": "1.9.0",
|
|
47
|
+
"ai": "^6.0.0",
|
|
48
|
+
"bash-tool": "^1.3.0",
|
|
49
|
+
"commander": "^12.0.0",
|
|
50
|
+
"just-bash": "^2.2.0",
|
|
51
|
+
"ora": "^9.0.0",
|
|
52
|
+
"zod": "^4.3.5"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@changesets/cli": "^2.29.8",
|
|
56
|
+
"@types/node": "^18.19.0",
|
|
57
|
+
"rimraf": "^5.0.10",
|
|
58
|
+
"tsc-alias": "^1.8.11",
|
|
59
|
+
"tsx": "^4.21.0",
|
|
60
|
+
"typescript": "^5.8.2",
|
|
61
|
+
"vitest": "^2.1.9"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"clean": "rimraf dist tsconfig.esm.tsbuildinfo tsconfig.tsbuildinfo",
|
|
68
|
+
"prebuild": "pnpm run clean",
|
|
69
|
+
"build": "tsc --build tsconfig.esm.json",
|
|
70
|
+
"start": "node dist/cli.js",
|
|
71
|
+
"dev": "tsx src/cli.ts",
|
|
72
|
+
"typecheck": "tsc --noEmit",
|
|
73
|
+
"test": "vitest run --typecheck"
|
|
74
|
+
}
|
|
75
|
+
}
|