@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,246 @@
|
|
|
1
|
+
import type { PhoenixClient } from "@arizeai/phoenix-client";
|
|
2
|
+
import type { ExecutionMode } from "../modes/types.js";
|
|
3
|
+
import { withErrorHandling, extractData } from "./client.js";
|
|
4
|
+
|
|
5
|
+
interface Dataset {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Experiment {
|
|
11
|
+
id: string;
|
|
12
|
+
dataset_id: string;
|
|
13
|
+
dataset_version_id: string;
|
|
14
|
+
repetitions: number;
|
|
15
|
+
metadata: Record<string, unknown>;
|
|
16
|
+
project_name: string | null;
|
|
17
|
+
created_at: string;
|
|
18
|
+
updated_at: string;
|
|
19
|
+
example_count: number;
|
|
20
|
+
successful_run_count: number;
|
|
21
|
+
failed_run_count: number;
|
|
22
|
+
missing_run_count: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ExperimentRun {
|
|
26
|
+
id: string;
|
|
27
|
+
experiment_id: string;
|
|
28
|
+
dataset_example_id: string;
|
|
29
|
+
start_time: string;
|
|
30
|
+
end_time: string;
|
|
31
|
+
output: unknown;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
trace_id?: string | null;
|
|
34
|
+
repetition_number?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface FetchExperimentsOptions {
|
|
38
|
+
/**
|
|
39
|
+
* Maximum number of experiments to fetch per dataset
|
|
40
|
+
*/
|
|
41
|
+
limit?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Include experiment runs in the snapshot
|
|
44
|
+
*/
|
|
45
|
+
includeRuns?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Converts an array to JSONL format
|
|
50
|
+
*/
|
|
51
|
+
function toJSONL(items: unknown[]): string {
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
return items.map((item) => JSON.stringify(item)).join("\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fetches all experiments and their runs from Phoenix
|
|
60
|
+
* Note: Experiments are fetched per dataset since there's no direct "all experiments" endpoint
|
|
61
|
+
*/
|
|
62
|
+
export async function fetchExperiments(
|
|
63
|
+
client: PhoenixClient,
|
|
64
|
+
mode: ExecutionMode,
|
|
65
|
+
options: FetchExperimentsOptions = {}
|
|
66
|
+
): Promise<void> {
|
|
67
|
+
const { limit = 100, includeRuns = true } = options;
|
|
68
|
+
|
|
69
|
+
// First, we need to get all datasets to fetch their experiments
|
|
70
|
+
const datasetsResponse = await withErrorHandling(
|
|
71
|
+
() => client.GET("/v1/datasets", { params: { query: { limit: 1000 } } }),
|
|
72
|
+
"fetching datasets for experiments"
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const datasetsData = extractData(datasetsResponse);
|
|
76
|
+
const datasets: Dataset[] = datasetsData.data;
|
|
77
|
+
|
|
78
|
+
// Collect all experiments from all datasets
|
|
79
|
+
const allExperiments: Array<Experiment & { datasetName: string }> = [];
|
|
80
|
+
|
|
81
|
+
for (const dataset of datasets) {
|
|
82
|
+
try {
|
|
83
|
+
// Fetch experiments for this dataset with pagination
|
|
84
|
+
const experiments: Experiment[] = [];
|
|
85
|
+
let cursor: string | null = null;
|
|
86
|
+
|
|
87
|
+
do {
|
|
88
|
+
const response = await withErrorHandling(
|
|
89
|
+
() =>
|
|
90
|
+
client.GET("/v1/datasets/{dataset_id}/experiments", {
|
|
91
|
+
params: {
|
|
92
|
+
path: {
|
|
93
|
+
dataset_id: dataset.id,
|
|
94
|
+
},
|
|
95
|
+
query: {
|
|
96
|
+
cursor,
|
|
97
|
+
limit: 50,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
`fetching experiments for dataset ${dataset.name}`
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const data = extractData(response);
|
|
105
|
+
experiments.push(...(data.data || []));
|
|
106
|
+
cursor = data.next_cursor || null;
|
|
107
|
+
|
|
108
|
+
// Stop if we've reached the overall limit
|
|
109
|
+
if (allExperiments.length + experiments.length >= limit) {
|
|
110
|
+
const remaining = limit - allExperiments.length;
|
|
111
|
+
experiments.splice(remaining);
|
|
112
|
+
cursor = null;
|
|
113
|
+
}
|
|
114
|
+
} while (cursor != null);
|
|
115
|
+
|
|
116
|
+
// Add dataset name to each experiment for context
|
|
117
|
+
const experimentsWithDatasetName = experiments.map((exp) => ({
|
|
118
|
+
...exp,
|
|
119
|
+
datasetName: dataset.name,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
allExperiments.push(...experimentsWithDatasetName);
|
|
123
|
+
|
|
124
|
+
// Apply limit if specified
|
|
125
|
+
if (allExperiments.length >= limit) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// If fetching experiments for a dataset fails, log and continue
|
|
130
|
+
console.warn(
|
|
131
|
+
`Failed to fetch experiments for dataset ${dataset.name}:`,
|
|
132
|
+
error
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Write experiments index
|
|
138
|
+
await mode.writeFile(
|
|
139
|
+
"/phoenix/experiments/index.jsonl",
|
|
140
|
+
toJSONL(allExperiments)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Fetch runs for each experiment if requested
|
|
144
|
+
if (includeRuns) {
|
|
145
|
+
for (const experiment of allExperiments) {
|
|
146
|
+
try {
|
|
147
|
+
// Write experiment metadata
|
|
148
|
+
await mode.writeFile(
|
|
149
|
+
`/phoenix/experiments/${experiment.id}/metadata.json`,
|
|
150
|
+
JSON.stringify(
|
|
151
|
+
{
|
|
152
|
+
id: experiment.id,
|
|
153
|
+
dataset_id: experiment.dataset_id,
|
|
154
|
+
dataset_name: experiment.datasetName,
|
|
155
|
+
dataset_version_id: experiment.dataset_version_id,
|
|
156
|
+
repetitions: experiment.repetitions,
|
|
157
|
+
metadata: experiment.metadata,
|
|
158
|
+
project_name: experiment.project_name,
|
|
159
|
+
created_at: experiment.created_at,
|
|
160
|
+
updated_at: experiment.updated_at,
|
|
161
|
+
example_count: experiment.example_count,
|
|
162
|
+
successful_run_count: experiment.successful_run_count,
|
|
163
|
+
failed_run_count: experiment.failed_run_count,
|
|
164
|
+
missing_run_count: experiment.missing_run_count,
|
|
165
|
+
snapshot_timestamp: new Date().toISOString(),
|
|
166
|
+
},
|
|
167
|
+
null,
|
|
168
|
+
2
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// Fetch runs for this experiment with pagination
|
|
173
|
+
const runs: ExperimentRun[] = [];
|
|
174
|
+
let cursor: string | null = null;
|
|
175
|
+
|
|
176
|
+
do {
|
|
177
|
+
const runsResponse = await withErrorHandling(
|
|
178
|
+
() =>
|
|
179
|
+
client.GET("/v1/experiments/{experiment_id}/runs", {
|
|
180
|
+
params: {
|
|
181
|
+
path: {
|
|
182
|
+
experiment_id: experiment.id,
|
|
183
|
+
},
|
|
184
|
+
query: {
|
|
185
|
+
cursor,
|
|
186
|
+
limit: 100,
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
}),
|
|
190
|
+
`fetching runs for experiment ${experiment.id}`
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const runsData = extractData(runsResponse);
|
|
194
|
+
runs.push(...(runsData.data || []));
|
|
195
|
+
cursor = runsData.next_cursor || null;
|
|
196
|
+
} while (cursor != null);
|
|
197
|
+
|
|
198
|
+
// Write runs as JSONL
|
|
199
|
+
await mode.writeFile(
|
|
200
|
+
`/phoenix/experiments/${experiment.id}/runs.jsonl`,
|
|
201
|
+
toJSONL(runs)
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Write experiment summary with run stats
|
|
205
|
+
await mode.writeFile(
|
|
206
|
+
`/phoenix/experiments/${experiment.id}/summary.json`,
|
|
207
|
+
JSON.stringify(
|
|
208
|
+
{
|
|
209
|
+
experiment_id: experiment.id,
|
|
210
|
+
dataset_name: experiment.datasetName,
|
|
211
|
+
project_name: experiment.project_name,
|
|
212
|
+
total_runs: runs.length,
|
|
213
|
+
successful_runs: experiment.successful_run_count,
|
|
214
|
+
failed_runs: experiment.failed_run_count,
|
|
215
|
+
missing_runs: experiment.missing_run_count,
|
|
216
|
+
created_at: experiment.created_at,
|
|
217
|
+
updated_at: experiment.updated_at,
|
|
218
|
+
},
|
|
219
|
+
null,
|
|
220
|
+
2
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
// If fetching runs for an experiment fails, log and continue
|
|
225
|
+
console.warn(
|
|
226
|
+
`Failed to fetch runs for experiment ${experiment.id}:`,
|
|
227
|
+
error
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Still create the experiment metadata without runs
|
|
231
|
+
await mode.writeFile(
|
|
232
|
+
`/phoenix/experiments/${experiment.id}/metadata.json`,
|
|
233
|
+
JSON.stringify(
|
|
234
|
+
{
|
|
235
|
+
...experiment,
|
|
236
|
+
error: "Failed to fetch runs",
|
|
237
|
+
snapshot_timestamp: new Date().toISOString(),
|
|
238
|
+
},
|
|
239
|
+
null,
|
|
240
|
+
2
|
|
241
|
+
)
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// Export all snapshot modules
|
|
2
|
+
export {
|
|
3
|
+
createPhoenixClient,
|
|
4
|
+
PhoenixClientError,
|
|
5
|
+
type PhoenixClientConfig,
|
|
6
|
+
} from "./client.js";
|
|
7
|
+
export { fetchProjects } from "./projects.js";
|
|
8
|
+
export { snapshotSpans, type SnapshotSpansOptions } from "./spans.js";
|
|
9
|
+
export { fetchDatasets } from "./datasets.js";
|
|
10
|
+
export { fetchExperiments } from "./experiments.js";
|
|
11
|
+
export { fetchPrompts } from "./prompts.js";
|
|
12
|
+
export { generateContext } from "./context.js";
|
|
13
|
+
|
|
14
|
+
// Import necessary types and modules for orchestration
|
|
15
|
+
import type { ExecutionMode } from "../modes/types.js";
|
|
16
|
+
import type { PhoenixClient } from "@arizeai/phoenix-client";
|
|
17
|
+
import {
|
|
18
|
+
createPhoenixClient,
|
|
19
|
+
PhoenixClientError,
|
|
20
|
+
type PhoenixClientConfig,
|
|
21
|
+
} from "./client.js";
|
|
22
|
+
import { fetchProjects } from "./projects.js";
|
|
23
|
+
import { snapshotSpans, type SnapshotSpansOptions } from "./spans.js";
|
|
24
|
+
import { fetchDatasets } from "./datasets.js";
|
|
25
|
+
import { fetchExperiments } from "./experiments.js";
|
|
26
|
+
import { fetchPrompts } from "./prompts.js";
|
|
27
|
+
import { generateContext } from "./context.js";
|
|
28
|
+
import { SnapshotProgress } from "../progress.js";
|
|
29
|
+
|
|
30
|
+
export interface SnapshotOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Phoenix server base URL
|
|
33
|
+
*/
|
|
34
|
+
baseURL: string;
|
|
35
|
+
/**
|
|
36
|
+
* Optional API key for authentication
|
|
37
|
+
*/
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Maximum number of spans per project
|
|
41
|
+
*/
|
|
42
|
+
spansPerProject?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Time range filter for spans (ISO 8601 format)
|
|
45
|
+
*/
|
|
46
|
+
startTime?: string;
|
|
47
|
+
endTime?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Whether to show progress indicators
|
|
50
|
+
*/
|
|
51
|
+
showProgress?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SnapshotMetadata {
|
|
55
|
+
created_at: string;
|
|
56
|
+
phoenix_url: string;
|
|
57
|
+
cursors: {
|
|
58
|
+
spans?: Record<string, { last_end_time?: string; cursor?: string }>;
|
|
59
|
+
datasets?: { last_fetch: string };
|
|
60
|
+
experiments?: { last_fetch: string };
|
|
61
|
+
prompts?: { last_fetch: string };
|
|
62
|
+
};
|
|
63
|
+
limits: {
|
|
64
|
+
spans_per_project: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Orchestrates all data fetchers to create a complete Phoenix snapshot
|
|
70
|
+
* @param mode - The execution mode (sandbox or local)
|
|
71
|
+
* @param options - Snapshot options including server URL and limits
|
|
72
|
+
*/
|
|
73
|
+
export async function createSnapshot(
|
|
74
|
+
mode: ExecutionMode,
|
|
75
|
+
options: SnapshotOptions
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const {
|
|
78
|
+
baseURL,
|
|
79
|
+
apiKey,
|
|
80
|
+
spansPerProject = 1000,
|
|
81
|
+
startTime,
|
|
82
|
+
endTime,
|
|
83
|
+
showProgress = false,
|
|
84
|
+
} = options;
|
|
85
|
+
|
|
86
|
+
// Create progress indicator
|
|
87
|
+
const progress = new SnapshotProgress(showProgress);
|
|
88
|
+
progress.start("Creating Phoenix data snapshot");
|
|
89
|
+
|
|
90
|
+
// Create Phoenix client
|
|
91
|
+
const clientConfig: PhoenixClientConfig = {
|
|
92
|
+
baseURL,
|
|
93
|
+
apiKey,
|
|
94
|
+
};
|
|
95
|
+
const client = createPhoenixClient(clientConfig);
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// 1. Fetch projects first (required for spans)
|
|
99
|
+
progress.update("Fetching projects");
|
|
100
|
+
try {
|
|
101
|
+
await fetchProjects(client, mode);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
progress.fail("Failed to fetch projects");
|
|
104
|
+
throw new PhoenixClientError(
|
|
105
|
+
`Failed to fetch projects: ${error instanceof Error ? error.message : String(error)}`,
|
|
106
|
+
error instanceof PhoenixClientError ? error.code : "UNKNOWN_ERROR",
|
|
107
|
+
error
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 2. Fetch spans and other data in parallel
|
|
112
|
+
progress.update(
|
|
113
|
+
"Fetching all data",
|
|
114
|
+
"spans, datasets, experiments, prompts"
|
|
115
|
+
);
|
|
116
|
+
const spansOptions: SnapshotSpansOptions = {
|
|
117
|
+
spansPerProject,
|
|
118
|
+
startTime,
|
|
119
|
+
endTime,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Fetch all data types in parallel for better performance
|
|
123
|
+
const results = await Promise.allSettled([
|
|
124
|
+
snapshotSpans(client, mode, spansOptions),
|
|
125
|
+
fetchDatasets(client, mode),
|
|
126
|
+
fetchExperiments(client, mode),
|
|
127
|
+
fetchPrompts(client, mode),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Check for failures and collect errors
|
|
131
|
+
const errors: Array<{ type: string; error: unknown }> = [];
|
|
132
|
+
const dataTypes = ["spans", "datasets", "experiments", "prompts"];
|
|
133
|
+
|
|
134
|
+
results.forEach((result, index) => {
|
|
135
|
+
if (result.status === "rejected") {
|
|
136
|
+
errors.push({
|
|
137
|
+
type: dataTypes[index] || "unknown",
|
|
138
|
+
error: result.reason,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (errors.length > 0) {
|
|
144
|
+
// Log individual errors
|
|
145
|
+
errors.forEach(({ type, error }) => {
|
|
146
|
+
console.error(
|
|
147
|
+
`Warning: Failed to fetch ${type}:`,
|
|
148
|
+
error instanceof Error ? error.message : String(error)
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// If spans failed, that's critical - throw error
|
|
153
|
+
if (errors.some((e) => e.type === "spans")) {
|
|
154
|
+
progress.fail("Failed to fetch spans");
|
|
155
|
+
throw new PhoenixClientError(
|
|
156
|
+
`Failed to fetch spans: ${errors.find((e) => e.type === "spans")?.error}`,
|
|
157
|
+
"UNKNOWN_ERROR",
|
|
158
|
+
errors
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// If all other data failed, throw error. If partial success, continue with warning
|
|
163
|
+
if (errors.length === 4) {
|
|
164
|
+
progress.fail("Failed to fetch all data");
|
|
165
|
+
throw new PhoenixClientError(
|
|
166
|
+
"Failed to fetch all data types",
|
|
167
|
+
"UNKNOWN_ERROR",
|
|
168
|
+
errors
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4. Generate context file
|
|
174
|
+
progress.update("Generating context");
|
|
175
|
+
await generateContext(mode, {
|
|
176
|
+
phoenixUrl: baseURL,
|
|
177
|
+
snapshotTime: new Date(),
|
|
178
|
+
spansPerProject,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// 5. Write metadata file
|
|
182
|
+
progress.update("Writing metadata");
|
|
183
|
+
const metadata: SnapshotMetadata = {
|
|
184
|
+
created_at: new Date().toISOString(),
|
|
185
|
+
phoenix_url: baseURL,
|
|
186
|
+
cursors: {
|
|
187
|
+
spans: {}, // TODO: Track span cursors when span fetching supports it
|
|
188
|
+
datasets: { last_fetch: new Date().toISOString() },
|
|
189
|
+
experiments: { last_fetch: new Date().toISOString() },
|
|
190
|
+
prompts: { last_fetch: new Date().toISOString() },
|
|
191
|
+
},
|
|
192
|
+
limits: {
|
|
193
|
+
spans_per_project: spansPerProject,
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
await mode.writeFile(
|
|
198
|
+
"/_meta/snapshot.json",
|
|
199
|
+
JSON.stringify(metadata, null, 2)
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
progress.succeed("✅ Snapshot created successfully!");
|
|
203
|
+
} catch (error) {
|
|
204
|
+
// Stop progress if not already stopped
|
|
205
|
+
progress.stop();
|
|
206
|
+
|
|
207
|
+
// Enhance error with context before rethrowing
|
|
208
|
+
if (error instanceof PhoenixClientError) {
|
|
209
|
+
throw error; // Already has good context
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
throw new PhoenixClientError(
|
|
213
|
+
`Failed to create snapshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
214
|
+
"UNKNOWN_ERROR",
|
|
215
|
+
error
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Loads existing snapshot metadata if available
|
|
222
|
+
* @param mode - The execution mode (sandbox or local)
|
|
223
|
+
* @returns The snapshot metadata or null if not found
|
|
224
|
+
*/
|
|
225
|
+
export async function loadSnapshotMetadata(
|
|
226
|
+
mode: ExecutionMode
|
|
227
|
+
): Promise<SnapshotMetadata | null> {
|
|
228
|
+
try {
|
|
229
|
+
const result = await mode.exec(
|
|
230
|
+
"cat /phoenix/_meta/snapshot.json 2>/dev/null"
|
|
231
|
+
);
|
|
232
|
+
if (result.exitCode === 0) {
|
|
233
|
+
return JSON.parse(result.stdout);
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// File doesn't exist or parse error
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Creates an incremental snapshot, fetching only new/updated data
|
|
243
|
+
* @param mode - The execution mode (sandbox or local)
|
|
244
|
+
* @param options - Snapshot options including server URL and limits
|
|
245
|
+
*/
|
|
246
|
+
export async function createIncrementalSnapshot(
|
|
247
|
+
mode: ExecutionMode,
|
|
248
|
+
options: SnapshotOptions
|
|
249
|
+
): Promise<void> {
|
|
250
|
+
// Load existing metadata to get cursors
|
|
251
|
+
const existingMetadata = await loadSnapshotMetadata(mode);
|
|
252
|
+
|
|
253
|
+
if (!existingMetadata) {
|
|
254
|
+
// No existing snapshot, create a full one
|
|
255
|
+
await createSnapshot(mode, options);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const {
|
|
260
|
+
baseURL,
|
|
261
|
+
apiKey,
|
|
262
|
+
spansPerProject = 1000,
|
|
263
|
+
showProgress = false,
|
|
264
|
+
} = options;
|
|
265
|
+
|
|
266
|
+
// Create progress indicator
|
|
267
|
+
const progress = new SnapshotProgress(showProgress);
|
|
268
|
+
progress.start("Updating Phoenix data snapshot");
|
|
269
|
+
|
|
270
|
+
// Create Phoenix client
|
|
271
|
+
const clientConfig: PhoenixClientConfig = {
|
|
272
|
+
baseURL,
|
|
273
|
+
apiKey,
|
|
274
|
+
};
|
|
275
|
+
const client = createPhoenixClient(clientConfig);
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// Show time since last snapshot
|
|
279
|
+
const lastSnapshotDate = new Date(existingMetadata.created_at);
|
|
280
|
+
const timeSince = formatTimeSince(lastSnapshotDate);
|
|
281
|
+
progress.update("Checking for updates", `last snapshot ${timeSince} ago`);
|
|
282
|
+
|
|
283
|
+
// For incremental updates, we'll need to:
|
|
284
|
+
// 1. Fetch projects (always fetch all as they're small)
|
|
285
|
+
progress.update("Updating projects");
|
|
286
|
+
await fetchProjects(client, mode);
|
|
287
|
+
|
|
288
|
+
// 2. Fetch spans and other data in parallel for better performance
|
|
289
|
+
progress.update("Fetching updates", "new spans and refreshing other data");
|
|
290
|
+
|
|
291
|
+
const spansOptions: SnapshotSpansOptions = {
|
|
292
|
+
spansPerProject,
|
|
293
|
+
// Use the last end time from previous snapshot as start time
|
|
294
|
+
startTime: existingMetadata.cursors.spans
|
|
295
|
+
? Object.values(existingMetadata.cursors.spans)
|
|
296
|
+
.map((cursor) => cursor.last_end_time)
|
|
297
|
+
.filter(Boolean)
|
|
298
|
+
.sort()
|
|
299
|
+
.pop()
|
|
300
|
+
: undefined,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
// For datasets/experiments/prompts, check if they've been updated
|
|
304
|
+
const datasetsLastFetch = existingMetadata.cursors.datasets?.last_fetch;
|
|
305
|
+
const experimentsLastFetch =
|
|
306
|
+
existingMetadata.cursors.experiments?.last_fetch;
|
|
307
|
+
const promptsLastFetch = existingMetadata.cursors.prompts?.last_fetch;
|
|
308
|
+
|
|
309
|
+
// Fetch all data types in parallel
|
|
310
|
+
// For now, we'll refetch all as the API doesn't support filtering by updated_at
|
|
311
|
+
// In a future enhancement, we could check individual items for updates
|
|
312
|
+
const updateResults = await Promise.allSettled([
|
|
313
|
+
snapshotSpans(client, mode, spansOptions),
|
|
314
|
+
fetchDatasets(client, mode),
|
|
315
|
+
fetchExperiments(client, mode),
|
|
316
|
+
fetchPrompts(client, mode),
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
// Check for critical errors
|
|
320
|
+
const updateErrors: Array<{ type: string; error: unknown }> = [];
|
|
321
|
+
const updateDataTypes = ["spans", "datasets", "experiments", "prompts"];
|
|
322
|
+
|
|
323
|
+
updateResults.forEach((result, index) => {
|
|
324
|
+
if (result.status === "rejected") {
|
|
325
|
+
updateErrors.push({
|
|
326
|
+
type: updateDataTypes[index] || "unknown",
|
|
327
|
+
error: result.reason,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (updateErrors.length > 0) {
|
|
333
|
+
// Log individual errors
|
|
334
|
+
updateErrors.forEach(({ type, error }) => {
|
|
335
|
+
console.error(
|
|
336
|
+
`Warning: Failed to update ${type}:`,
|
|
337
|
+
error instanceof Error ? error.message : String(error)
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 4. Regenerate context with updated data
|
|
343
|
+
progress.update("Regenerating context");
|
|
344
|
+
await generateContext(mode, {
|
|
345
|
+
phoenixUrl: baseURL,
|
|
346
|
+
snapshotTime: new Date(),
|
|
347
|
+
spansPerProject,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// 5. Update metadata
|
|
351
|
+
progress.update("Updating metadata");
|
|
352
|
+
const updatedSpansCursors = existingMetadata.cursors.spans || {};
|
|
353
|
+
const metadata: SnapshotMetadata = {
|
|
354
|
+
created_at: new Date().toISOString(),
|
|
355
|
+
phoenix_url: baseURL,
|
|
356
|
+
cursors: {
|
|
357
|
+
spans: updatedSpansCursors,
|
|
358
|
+
datasets: { last_fetch: new Date().toISOString() },
|
|
359
|
+
experiments: { last_fetch: new Date().toISOString() },
|
|
360
|
+
prompts: { last_fetch: new Date().toISOString() },
|
|
361
|
+
},
|
|
362
|
+
limits: {
|
|
363
|
+
spans_per_project: spansPerProject,
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
await mode.writeFile(
|
|
368
|
+
"/_meta/snapshot.json",
|
|
369
|
+
JSON.stringify(metadata, null, 2)
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
progress.succeed("✅ Incremental update complete!");
|
|
373
|
+
} catch (error) {
|
|
374
|
+
// Stop progress if not already stopped
|
|
375
|
+
progress.stop();
|
|
376
|
+
|
|
377
|
+
// Enhance error with context before rethrowing
|
|
378
|
+
if (error instanceof PhoenixClientError) {
|
|
379
|
+
throw error; // Already has good context
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
throw new PhoenixClientError(
|
|
383
|
+
`Failed to create incremental snapshot: ${error instanceof Error ? error.message : String(error)}`,
|
|
384
|
+
"UNKNOWN_ERROR",
|
|
385
|
+
error
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Format time since a date in human-readable format
|
|
392
|
+
*/
|
|
393
|
+
function formatTimeSince(date: Date): string {
|
|
394
|
+
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
|
|
395
|
+
|
|
396
|
+
if (seconds < 60) return `${seconds}s`;
|
|
397
|
+
const minutes = Math.floor(seconds / 60);
|
|
398
|
+
if (minutes < 60) return `${minutes}m`;
|
|
399
|
+
const hours = Math.floor(minutes / 60);
|
|
400
|
+
if (hours < 24) return `${hours}h`;
|
|
401
|
+
const days = Math.floor(hours / 24);
|
|
402
|
+
return `${days}d`;
|
|
403
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PhoenixClient } from "@arizeai/phoenix-client";
|
|
2
|
+
import type { ExecutionMode } from "../modes/types.js";
|
|
3
|
+
import { withErrorHandling } from "./client.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts an array of items to JSONL format (one JSON object per line)
|
|
7
|
+
*/
|
|
8
|
+
function toJSONL(items: unknown[]): string {
|
|
9
|
+
return items.map((item) => JSON.stringify(item)).join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fetches all projects and writes them to the filesystem
|
|
14
|
+
* @param client - The Phoenix client instance
|
|
15
|
+
* @param mode - The execution mode (sandbox or local)
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchProjects(
|
|
18
|
+
client: PhoenixClient,
|
|
19
|
+
mode: ExecutionMode
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
// Fetch all projects with error handling
|
|
22
|
+
const projectsData = await withErrorHandling(async () => {
|
|
23
|
+
const response = await client.GET("/v1/projects", {
|
|
24
|
+
params: {
|
|
25
|
+
query: {
|
|
26
|
+
include_experiment_projects: false,
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!response.data) {
|
|
32
|
+
throw new Error("No data returned from projects endpoint");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return response.data;
|
|
36
|
+
}, "fetching projects");
|
|
37
|
+
|
|
38
|
+
// Extract projects from the response
|
|
39
|
+
const projects = projectsData.data || [];
|
|
40
|
+
|
|
41
|
+
// Write projects list as JSONL to /phoenix/projects/index.jsonl
|
|
42
|
+
const projectsPath = "/phoenix/projects/index.jsonl";
|
|
43
|
+
await mode.writeFile(projectsPath, toJSONL(projects));
|
|
44
|
+
|
|
45
|
+
// For each project, create a metadata.json file
|
|
46
|
+
for (const project of projects) {
|
|
47
|
+
const projectDir = `/phoenix/projects/${project.name}`;
|
|
48
|
+
const metadataPath = `${projectDir}/metadata.json`;
|
|
49
|
+
|
|
50
|
+
// Write project metadata
|
|
51
|
+
await mode.writeFile(metadataPath, JSON.stringify(project, null, 2));
|
|
52
|
+
|
|
53
|
+
// Create empty spans directory (will be populated by snapshot-spans task)
|
|
54
|
+
const spansDir = `${projectDir}/spans`;
|
|
55
|
+
// Create directory by writing a placeholder that will be overwritten later
|
|
56
|
+
await mode.writeFile(`${spansDir}/.gitkeep`, "");
|
|
57
|
+
}
|
|
58
|
+
}
|