@cephalization/phoenix-insight 0.2.0 → 0.4.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 +83 -1
- package/dist/cli.js +61 -4
- package/dist/snapshot/context.js +171 -62
- package/dist/snapshot/spans.js +28 -4
- package/dist/snapshot/utils.js +112 -0
- package/dist/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +3 -1
- package/src/cli.ts +97 -29
- package/src/snapshot/context.ts +200 -75
- package/src/snapshot/spans.ts +33 -12
- package/src/snapshot/utils.ts +140 -0
package/README.md
CHANGED
|
@@ -87,7 +87,10 @@ phoenix> exit
|
|
|
87
87
|
Create or update snapshots separately from queries:
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
# Create initial snapshot
|
|
90
|
+
# Create initial snapshot (explicit command)
|
|
91
|
+
phoenix-insight snapshot create
|
|
92
|
+
|
|
93
|
+
# Create initial snapshot (shorthand, same as 'snapshot create')
|
|
91
94
|
phoenix-insight snapshot
|
|
92
95
|
|
|
93
96
|
# Force refresh snapshot (ignore cache)
|
|
@@ -96,6 +99,12 @@ phoenix-insight snapshot --refresh
|
|
|
96
99
|
# Snapshot from a specific Phoenix instance
|
|
97
100
|
phoenix-insight snapshot --base-url https://phoenix.example.com --api-key your-api-key
|
|
98
101
|
|
|
102
|
+
# Get the path to the latest snapshot
|
|
103
|
+
phoenix-insight snapshot latest
|
|
104
|
+
|
|
105
|
+
# List all available snapshots
|
|
106
|
+
phoenix-insight snapshot list
|
|
107
|
+
|
|
99
108
|
# Clean up local snapshots
|
|
100
109
|
phoenix-insight prune
|
|
101
110
|
|
|
@@ -293,8 +302,11 @@ Creates or updates a data snapshot from Phoenix without running a query.
|
|
|
293
302
|
|
|
294
303
|
```bash
|
|
295
304
|
phoenix-insight snapshot [options]
|
|
305
|
+
phoenix-insight snapshot create [options]
|
|
296
306
|
```
|
|
297
307
|
|
|
308
|
+
Note: `phoenix-insight snapshot` (without subcommand) is equivalent to `phoenix-insight snapshot create` for backward compatibility.
|
|
309
|
+
|
|
298
310
|
| Option | Description | Default | Example |
|
|
299
311
|
| ------------------ | --------------------------------------------- | ----------------------- | ----------------------------------------------- |
|
|
300
312
|
| `--config <path>` | Custom config file path | `~/.phoenix-insight/config.json` | `--config ./my-config.json` |
|
|
@@ -304,6 +316,76 @@ phoenix-insight snapshot [options]
|
|
|
304
316
|
| `--limit <n>` | Maximum spans to fetch per project | `1000` | `phoenix-insight snapshot --limit 5000` |
|
|
305
317
|
| `--trace` | Enable tracing of snapshot operations | `false` | `phoenix-insight snapshot --trace` |
|
|
306
318
|
|
|
319
|
+
### Snapshot Create Subcommand
|
|
320
|
+
|
|
321
|
+
Explicitly creates a new snapshot from Phoenix data. This is the preferred way to create snapshots.
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
phoenix-insight snapshot create
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Same options as `phoenix-insight snapshot`. Use `snapshot create` for clarity in scripts and automation.
|
|
328
|
+
|
|
329
|
+
### Snapshot Latest Command
|
|
330
|
+
|
|
331
|
+
Prints the absolute path to the latest snapshot directory.
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
phoenix-insight snapshot latest
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Outputs the path to stdout with no decoration (just the path). Exit code 0 on success, exit code 1 if no snapshots exist.
|
|
338
|
+
|
|
339
|
+
**Example usage:**
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# Get the latest snapshot path
|
|
343
|
+
phoenix-insight snapshot latest
|
|
344
|
+
# Output: /Users/you/.phoenix-insight/snapshots/1704067200000-abc123/phoenix
|
|
345
|
+
|
|
346
|
+
# Use in scripts
|
|
347
|
+
SNAPSHOT_PATH=$(phoenix-insight snapshot latest)
|
|
348
|
+
ls "$SNAPSHOT_PATH"
|
|
349
|
+
|
|
350
|
+
# Check if snapshots exist
|
|
351
|
+
if phoenix-insight snapshot latest > /dev/null 2>&1; then
|
|
352
|
+
echo "Snapshots available"
|
|
353
|
+
else
|
|
354
|
+
echo "No snapshots found"
|
|
355
|
+
fi
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### Snapshot List Command
|
|
359
|
+
|
|
360
|
+
Lists all available snapshots with their timestamps.
|
|
361
|
+
|
|
362
|
+
```bash
|
|
363
|
+
phoenix-insight snapshot list
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Outputs one snapshot per line in the format `<timestamp> <path>` where timestamp is ISO 8601. Most recent first. Exit code 0 even if empty (just prints nothing).
|
|
367
|
+
|
|
368
|
+
**Example usage:**
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
# List all snapshots
|
|
372
|
+
phoenix-insight snapshot list
|
|
373
|
+
# Output:
|
|
374
|
+
# 2024-01-01T12:30:00.000Z /Users/you/.phoenix-insight/snapshots/1704113400000-abc123/phoenix
|
|
375
|
+
# 2024-01-01T10:00:00.000Z /Users/you/.phoenix-insight/snapshots/1704104400000-def456/phoenix
|
|
376
|
+
|
|
377
|
+
# Count snapshots
|
|
378
|
+
phoenix-insight snapshot list | wc -l
|
|
379
|
+
|
|
380
|
+
# Get oldest snapshot path
|
|
381
|
+
phoenix-insight snapshot list | tail -1 | cut -d' ' -f2
|
|
382
|
+
|
|
383
|
+
# Process snapshots in a script
|
|
384
|
+
phoenix-insight snapshot list | while read timestamp path; do
|
|
385
|
+
echo "Snapshot from $timestamp at $path"
|
|
386
|
+
done
|
|
387
|
+
```
|
|
388
|
+
|
|
307
389
|
### Prune Command
|
|
308
390
|
|
|
309
391
|
Deletes the local snapshot directory to free up disk space.
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,7 @@ import * as os from "node:os";
|
|
|
7
7
|
import { createSandboxMode, createLocalMode } from "./modes/index.js";
|
|
8
8
|
import { createInsightAgent, runOneShotQuery } from "./agent/index.js";
|
|
9
9
|
import { createSnapshot, createIncrementalSnapshot, createPhoenixClient, PhoenixClientError, } from "./snapshot/index.js";
|
|
10
|
+
import { getLatestSnapshot, listSnapshots } from "./snapshot/utils.js";
|
|
10
11
|
import { AgentProgress } from "./progress.js";
|
|
11
12
|
import { initializeObservability, shutdownObservability, } from "./observability/index.js";
|
|
12
13
|
import { initializeConfig, getConfig } from "./config/index.js";
|
|
@@ -182,10 +183,11 @@ Examples:
|
|
|
182
183
|
// Initialize config singleton before any command runs
|
|
183
184
|
await initializeConfig(cliArgs);
|
|
184
185
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Shared logic for creating a snapshot.
|
|
188
|
+
* Used by both 'phoenix-insight snapshot' and 'phoenix-insight snapshot create'.
|
|
189
|
+
*/
|
|
190
|
+
async function executeSnapshotCreate() {
|
|
189
191
|
const config = getConfig();
|
|
190
192
|
// Initialize observability if trace is enabled in config
|
|
191
193
|
if (config.trace) {
|
|
@@ -216,6 +218,61 @@ program
|
|
|
216
218
|
catch (error) {
|
|
217
219
|
handleError(error, "creating snapshot");
|
|
218
220
|
}
|
|
221
|
+
}
|
|
222
|
+
// Create snapshot command group
|
|
223
|
+
const snapshotCmd = program
|
|
224
|
+
.command("snapshot")
|
|
225
|
+
.description("Snapshot management commands");
|
|
226
|
+
// Default action for 'phoenix-insight snapshot' (backward compatibility alias for 'snapshot create')
|
|
227
|
+
snapshotCmd.action(async () => {
|
|
228
|
+
await executeSnapshotCreate();
|
|
229
|
+
});
|
|
230
|
+
// Subcommand: snapshot create (explicit create command)
|
|
231
|
+
snapshotCmd
|
|
232
|
+
.command("create")
|
|
233
|
+
.description("Create a new snapshot from Phoenix data")
|
|
234
|
+
.action(async () => {
|
|
235
|
+
await executeSnapshotCreate();
|
|
236
|
+
});
|
|
237
|
+
// Subcommand: snapshot latest
|
|
238
|
+
snapshotCmd
|
|
239
|
+
.command("latest")
|
|
240
|
+
.description("Print the absolute path to the latest snapshot directory")
|
|
241
|
+
.action(async () => {
|
|
242
|
+
try {
|
|
243
|
+
const latestSnapshot = await getLatestSnapshot();
|
|
244
|
+
if (!latestSnapshot) {
|
|
245
|
+
console.error("No snapshots found");
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
// Print only the path to stdout, no decoration
|
|
249
|
+
console.log(latestSnapshot.path);
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
// Subcommand: snapshot list
|
|
257
|
+
snapshotCmd
|
|
258
|
+
.command("list")
|
|
259
|
+
.description("List all available snapshots with their timestamps")
|
|
260
|
+
.action(async () => {
|
|
261
|
+
try {
|
|
262
|
+
const snapshots = await listSnapshots();
|
|
263
|
+
// Print each snapshot: <timestamp> <path>
|
|
264
|
+
// Most recent first (already sorted by listSnapshots)
|
|
265
|
+
for (const snapshot of snapshots) {
|
|
266
|
+
// Format timestamp as ISO 8601
|
|
267
|
+
const isoTimestamp = snapshot.timestamp.toISOString();
|
|
268
|
+
console.log(`${isoTimestamp} ${snapshot.path}`);
|
|
269
|
+
}
|
|
270
|
+
// Exit code 0 even if empty (just print nothing)
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
219
276
|
});
|
|
220
277
|
program
|
|
221
278
|
.command("help")
|
package/dist/snapshot/context.js
CHANGED
|
@@ -1,16 +1,160 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Static Section Templates
|
|
3
|
+
// =============================================================================
|
|
4
|
+
/**
|
|
5
|
+
* Quick Start section for external agents - appears at the top for discoverability
|
|
6
|
+
*/
|
|
7
|
+
const QUICK_START_SECTION = `## Quick Start for External Agents
|
|
8
|
+
|
|
9
|
+
This is a **read-only snapshot** of Phoenix observability data. You cannot modify this data.
|
|
10
|
+
|
|
11
|
+
### Key Files to Start With
|
|
12
|
+
|
|
13
|
+
| File | Description |
|
|
14
|
+
|------|-------------|
|
|
15
|
+
| \`/phoenix/projects/index.jsonl\` | List of all projects with traces |
|
|
16
|
+
| \`/phoenix/datasets/index.jsonl\` | List of all datasets |
|
|
17
|
+
| \`/phoenix/experiments/index.jsonl\` | List of all experiments |
|
|
18
|
+
| \`/phoenix/prompts/index.jsonl\` | List of all prompts |
|
|
19
|
+
|
|
20
|
+
### How to Parse Each File Format
|
|
21
|
+
|
|
22
|
+
**JSONL files** (\`.jsonl\`): One JSON object per line
|
|
23
|
+
\`\`\`bash
|
|
24
|
+
# Read all lines as a JSON array
|
|
25
|
+
cat /phoenix/projects/index.jsonl | jq -s '.'
|
|
26
|
+
|
|
27
|
+
# Process each line individually
|
|
28
|
+
while read -r line; do echo "$line" | jq '.name'; done < /phoenix/projects/index.jsonl
|
|
29
|
+
|
|
30
|
+
# Get first N items
|
|
31
|
+
head -n 5 /phoenix/projects/index.jsonl | jq -s '.'
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
**JSON files** (\`.json\`): Standard JSON format
|
|
35
|
+
\`\`\`bash
|
|
36
|
+
# Read and pretty-print
|
|
37
|
+
cat /phoenix/projects/my-project/metadata.json | jq '.'
|
|
38
|
+
|
|
39
|
+
# Extract specific field
|
|
40
|
+
cat /phoenix/projects/my-project/metadata.json | jq '.name'
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
**Markdown files** (\`.md\`): Plain text prompt templates
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
# Read prompt template
|
|
46
|
+
cat /phoenix/prompts/my-prompt/versions/v1.md
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
### Common Operations
|
|
50
|
+
|
|
51
|
+
\`\`\`bash
|
|
52
|
+
# List all project names
|
|
53
|
+
cat /phoenix/projects/index.jsonl | jq -r '.name'
|
|
54
|
+
|
|
55
|
+
# Count spans in a project
|
|
56
|
+
wc -l < /phoenix/projects/my-project/spans/index.jsonl
|
|
57
|
+
|
|
58
|
+
# Find spans with errors
|
|
59
|
+
cat /phoenix/projects/my-project/spans/index.jsonl | jq 'select(.status_code == "ERROR")'
|
|
60
|
+
|
|
61
|
+
# Get dataset examples
|
|
62
|
+
cat /phoenix/datasets/my-dataset/examples.jsonl | jq -s '.' | head -n 100
|
|
63
|
+
|
|
64
|
+
# Search across all files
|
|
65
|
+
grep -r "error" /phoenix/
|
|
66
|
+
\`\`\``;
|
|
67
|
+
/**
|
|
68
|
+
* Directory Structure section showing the snapshot layout
|
|
69
|
+
*/
|
|
70
|
+
const DIRECTORY_STRUCTURE_SECTION = `## Directory Structure
|
|
71
|
+
|
|
72
|
+
\`\`\`
|
|
73
|
+
/phoenix/
|
|
74
|
+
_context.md # This file - start here!
|
|
75
|
+
/projects/
|
|
76
|
+
index.jsonl # List of all projects
|
|
77
|
+
/{project_name}/
|
|
78
|
+
metadata.json # Project details
|
|
79
|
+
/spans/
|
|
80
|
+
index.jsonl # Span data (may be sampled)
|
|
81
|
+
metadata.json # Span snapshot metadata
|
|
82
|
+
/datasets/
|
|
83
|
+
index.jsonl # List of all datasets
|
|
84
|
+
/{dataset_name}/
|
|
85
|
+
metadata.json # Dataset details
|
|
86
|
+
examples.jsonl # Dataset examples
|
|
87
|
+
/experiments/
|
|
88
|
+
index.jsonl # List of all experiments
|
|
89
|
+
/{experiment_id}/
|
|
90
|
+
metadata.json # Experiment details
|
|
91
|
+
runs.jsonl # Experiment runs
|
|
92
|
+
/prompts/
|
|
93
|
+
index.jsonl # List of all prompts
|
|
94
|
+
/{prompt_name}/
|
|
95
|
+
metadata.json # Prompt details
|
|
96
|
+
/versions/
|
|
97
|
+
index.jsonl # Version list
|
|
98
|
+
/{version_id}.md # Version template
|
|
99
|
+
/_meta/
|
|
100
|
+
snapshot.json # Snapshot metadata
|
|
101
|
+
\`\`\``;
|
|
102
|
+
/**
|
|
103
|
+
* What You Can Do section describing available operations
|
|
104
|
+
*/
|
|
105
|
+
const WHAT_YOU_CAN_DO_SECTION = `## What You Can Do
|
|
106
|
+
|
|
107
|
+
- **Explore**: ls, cat, grep, find, jq, awk, sed
|
|
108
|
+
- **Fetch more data**: \`px-fetch-more spans --project <name> --limit 500\`
|
|
109
|
+
- **Fetch specific trace**: \`px-fetch-more trace --trace-id <id>\``;
|
|
110
|
+
/**
|
|
111
|
+
* Data Freshness section with refresh instructions
|
|
112
|
+
*/
|
|
113
|
+
const DATA_FRESHNESS_SECTION = `## Data Freshness
|
|
114
|
+
|
|
115
|
+
This is a **read-only snapshot**. Data may have changed since capture.
|
|
116
|
+
Run with \`--refresh\` to get latest data.`;
|
|
117
|
+
// =============================================================================
|
|
118
|
+
// Main Context Generation
|
|
119
|
+
// =============================================================================
|
|
1
120
|
/**
|
|
2
121
|
* Generates a _context.md summary file for the Phoenix snapshot
|
|
3
122
|
* This provides human and agent-readable context about what data is available
|
|
4
123
|
*/
|
|
5
124
|
export async function generateContext(mode, metadata) {
|
|
6
|
-
const lines = [];
|
|
7
|
-
// Header
|
|
8
|
-
lines.push("# Phoenix Snapshot Context");
|
|
9
|
-
lines.push("");
|
|
10
125
|
// Collect stats from the snapshot
|
|
11
126
|
const stats = await collectSnapshotStats(mode);
|
|
12
|
-
// What's Here section
|
|
127
|
+
// Build the dynamic "What's Here" section
|
|
128
|
+
const whatsHereSection = buildWhatsHereSection(stats, metadata);
|
|
129
|
+
// Build the dynamic "Recent Activity" section (may be empty)
|
|
130
|
+
const recentActivitySection = buildRecentActivitySection(stats);
|
|
131
|
+
// Compose the full context document
|
|
132
|
+
const content = [
|
|
133
|
+
"# Phoenix Snapshot Context",
|
|
134
|
+
"",
|
|
135
|
+
QUICK_START_SECTION,
|
|
136
|
+
"",
|
|
137
|
+
whatsHereSection,
|
|
138
|
+
recentActivitySection,
|
|
139
|
+
DIRECTORY_STRUCTURE_SECTION,
|
|
140
|
+
"",
|
|
141
|
+
WHAT_YOU_CAN_DO_SECTION,
|
|
142
|
+
"",
|
|
143
|
+
DATA_FRESHNESS_SECTION,
|
|
144
|
+
].join("\n");
|
|
145
|
+
// Write the context file
|
|
146
|
+
await mode.writeFile("/phoenix/_context.md", content);
|
|
147
|
+
}
|
|
148
|
+
// =============================================================================
|
|
149
|
+
// Dynamic Section Builders
|
|
150
|
+
// =============================================================================
|
|
151
|
+
/**
|
|
152
|
+
* Builds the "What's Here" section with project/dataset/experiment/prompt summaries
|
|
153
|
+
*/
|
|
154
|
+
function buildWhatsHereSection(stats, metadata) {
|
|
155
|
+
const lines = [];
|
|
13
156
|
lines.push("## What's Here");
|
|
157
|
+
lines.push("");
|
|
14
158
|
// Projects summary
|
|
15
159
|
if (stats.projects.length > 0) {
|
|
16
160
|
const projectSummary = stats.projects
|
|
@@ -57,67 +201,29 @@ export async function generateContext(mode, metadata) {
|
|
|
57
201
|
// Snapshot metadata
|
|
58
202
|
lines.push(`- **Snapshot**: Created ${formatRelativeTime(metadata.snapshotTime)} from ${metadata.phoenixUrl}`);
|
|
59
203
|
lines.push("");
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
204
|
+
return lines.join("\n");
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Builds the "Recent Activity" section if there are recent updates
|
|
208
|
+
* Returns an empty string if no recent activity
|
|
209
|
+
*/
|
|
210
|
+
function buildRecentActivitySection(stats) {
|
|
211
|
+
const activities = getRecentActivity(stats);
|
|
212
|
+
if (activities.length === 0) {
|
|
213
|
+
return "";
|
|
68
214
|
}
|
|
69
|
-
|
|
70
|
-
lines.push("##
|
|
71
|
-
lines.push("- **Explore**: ls, cat, grep, find, jq, awk, sed");
|
|
72
|
-
lines.push("- **Fetch more data**: `px-fetch-more spans --project <name> --limit 500`");
|
|
73
|
-
lines.push("- **Fetch specific trace**: `px-fetch-more trace --trace-id <id>`");
|
|
74
|
-
lines.push("");
|
|
75
|
-
// Data Freshness section
|
|
76
|
-
lines.push("## Data Freshness");
|
|
77
|
-
lines.push("This is a **read-only snapshot**. Data may have changed since capture.");
|
|
78
|
-
lines.push("Run with `--refresh` to get latest data.");
|
|
215
|
+
const lines = [];
|
|
216
|
+
lines.push("## Recent Activity");
|
|
79
217
|
lines.push("");
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
lines.push("- `.json` files: Standard JSON");
|
|
84
|
-
lines.push("- `.md` files: Markdown (prompt templates)");
|
|
218
|
+
for (const activity of activities) {
|
|
219
|
+
lines.push(`- ${activity}`);
|
|
220
|
+
}
|
|
85
221
|
lines.push("");
|
|
86
|
-
|
|
87
|
-
lines.push("## Directory Structure");
|
|
88
|
-
lines.push("```");
|
|
89
|
-
lines.push("/phoenix/");
|
|
90
|
-
lines.push(" _context.md # This file");
|
|
91
|
-
lines.push(" /projects/");
|
|
92
|
-
lines.push(" index.jsonl # List of all projects");
|
|
93
|
-
lines.push(" /{project_name}/");
|
|
94
|
-
lines.push(" metadata.json # Project details");
|
|
95
|
-
lines.push(" /spans/");
|
|
96
|
-
lines.push(" index.jsonl # Span data (may be sampled)");
|
|
97
|
-
lines.push(" metadata.json # Span snapshot metadata");
|
|
98
|
-
lines.push(" /datasets/");
|
|
99
|
-
lines.push(" index.jsonl # List of all datasets");
|
|
100
|
-
lines.push(" /{dataset_name}/");
|
|
101
|
-
lines.push(" metadata.json # Dataset details");
|
|
102
|
-
lines.push(" examples.jsonl # Dataset examples");
|
|
103
|
-
lines.push(" /experiments/");
|
|
104
|
-
lines.push(" index.jsonl # List of all experiments");
|
|
105
|
-
lines.push(" /{experiment_id}/");
|
|
106
|
-
lines.push(" metadata.json # Experiment details");
|
|
107
|
-
lines.push(" runs.jsonl # Experiment runs");
|
|
108
|
-
lines.push(" /prompts/");
|
|
109
|
-
lines.push(" index.jsonl # List of all prompts");
|
|
110
|
-
lines.push(" /{prompt_name}/");
|
|
111
|
-
lines.push(" metadata.json # Prompt details");
|
|
112
|
-
lines.push(" /versions/");
|
|
113
|
-
lines.push(" index.jsonl # Version list");
|
|
114
|
-
lines.push(" /{version_id}.md # Version template");
|
|
115
|
-
lines.push(" /_meta/");
|
|
116
|
-
lines.push(" snapshot.json # Snapshot metadata");
|
|
117
|
-
lines.push("```");
|
|
118
|
-
// Write the context file
|
|
119
|
-
await mode.writeFile("/phoenix/_context.md", lines.join("\n"));
|
|
222
|
+
return lines.join("\n");
|
|
120
223
|
}
|
|
224
|
+
// =============================================================================
|
|
225
|
+
// Data Collection
|
|
226
|
+
// =============================================================================
|
|
121
227
|
/**
|
|
122
228
|
* Collects statistics from the snapshot filesystem
|
|
123
229
|
*/
|
|
@@ -259,6 +365,9 @@ async function collectSnapshotStats(mode) {
|
|
|
259
365
|
}
|
|
260
366
|
return result;
|
|
261
367
|
}
|
|
368
|
+
// =============================================================================
|
|
369
|
+
// Helper Functions
|
|
370
|
+
// =============================================================================
|
|
262
371
|
/**
|
|
263
372
|
* Determines the status of an experiment based on its run counts
|
|
264
373
|
*/
|
package/dist/snapshot/spans.js
CHANGED
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import { withErrorHandling } from "./client.js";
|
|
2
|
+
/**
|
|
3
|
+
* Debug logger that respects the debug flag or DEBUG environment variable
|
|
4
|
+
*/
|
|
5
|
+
function createDebugLogger(debug) {
|
|
6
|
+
const isDebugEnabled = debug ?? !!process.env.DEBUG;
|
|
7
|
+
return {
|
|
8
|
+
log: (message) => {
|
|
9
|
+
if (isDebugEnabled) {
|
|
10
|
+
console.log(`[snapshotSpans] ${message}`);
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
2
15
|
/**
|
|
3
16
|
* Fetches spans for all projects and writes them to the snapshot
|
|
4
17
|
*
|
|
@@ -7,11 +20,15 @@ import { withErrorHandling } from "./client.js";
|
|
|
7
20
|
* @param options - Options for filtering and limiting spans
|
|
8
21
|
*/
|
|
9
22
|
export async function snapshotSpans(client, mode, options = {}) {
|
|
10
|
-
const { startTime, endTime, spansPerProject = 1000 } = options;
|
|
23
|
+
const { startTime, endTime, spansPerProject = 1000, debug } = options;
|
|
24
|
+
const logger = createDebugLogger(debug);
|
|
11
25
|
// Read projects index to get project names
|
|
12
|
-
|
|
26
|
+
// Use relative path so it works with the cwd set by the execution mode
|
|
27
|
+
logger.log("Reading projects index from projects/index.jsonl");
|
|
28
|
+
const projectsIndexContent = await mode.exec("cat projects/index.jsonl");
|
|
13
29
|
if (!projectsIndexContent.stdout) {
|
|
14
30
|
// No projects, nothing to do
|
|
31
|
+
logger.log("No projects found in index, skipping span fetch");
|
|
15
32
|
return;
|
|
16
33
|
}
|
|
17
34
|
const projectNames = projectsIndexContent.stdout
|
|
@@ -22,9 +39,11 @@ export async function snapshotSpans(client, mode, options = {}) {
|
|
|
22
39
|
const project = JSON.parse(line);
|
|
23
40
|
return project.name;
|
|
24
41
|
});
|
|
42
|
+
logger.log(`Found ${projectNames.length} project(s): ${projectNames.join(", ")}`);
|
|
25
43
|
// Fetch spans for each project
|
|
26
44
|
for (const projectName of projectNames) {
|
|
27
45
|
await withErrorHandling(async () => {
|
|
46
|
+
logger.log(`Starting span fetch for project: ${projectName}`);
|
|
28
47
|
const spans = [];
|
|
29
48
|
let cursor = null;
|
|
30
49
|
let totalFetched = 0;
|
|
@@ -63,10 +82,15 @@ export async function snapshotSpans(client, mode, options = {}) {
|
|
|
63
82
|
break;
|
|
64
83
|
}
|
|
65
84
|
}
|
|
85
|
+
logger.log(`Completed span fetch for project ${projectName}: ${spans.length} span(s) fetched`);
|
|
66
86
|
// Write spans to JSONL file
|
|
87
|
+
const spansFilePath = `/phoenix/projects/${projectName}/spans/index.jsonl`;
|
|
88
|
+
logger.log(`Writing spans to ${spansFilePath}`);
|
|
67
89
|
const jsonlContent = spans.map((span) => JSON.stringify(span)).join("\n");
|
|
68
|
-
await mode.writeFile(
|
|
90
|
+
await mode.writeFile(spansFilePath, jsonlContent);
|
|
69
91
|
// Write metadata about the spans snapshot
|
|
92
|
+
const metadataFilePath = `/phoenix/projects/${projectName}/spans/metadata.json`;
|
|
93
|
+
logger.log(`Writing metadata to ${metadataFilePath}`);
|
|
70
94
|
const metadata = {
|
|
71
95
|
project: projectName,
|
|
72
96
|
spanCount: spans.length,
|
|
@@ -74,7 +98,7 @@ export async function snapshotSpans(client, mode, options = {}) {
|
|
|
74
98
|
endTime: endTime || null,
|
|
75
99
|
snapshotTime: new Date().toISOString(),
|
|
76
100
|
};
|
|
77
|
-
await mode.writeFile(
|
|
101
|
+
await mode.writeFile(metadataFilePath, JSON.stringify(metadata, null, 2));
|
|
78
102
|
}, `fetching spans for project ${projectName}`);
|
|
79
103
|
}
|
|
80
104
|
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot discovery utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for listing and finding snapshots in the local filesystem.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs/promises";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import * as os from "node:os";
|
|
9
|
+
/**
|
|
10
|
+
* Get the base snapshots directory path
|
|
11
|
+
*/
|
|
12
|
+
export function getSnapshotsDir() {
|
|
13
|
+
return path.join(os.homedir(), ".phoenix-insight", "snapshots");
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Parse a snapshot directory name to extract timestamp
|
|
17
|
+
*
|
|
18
|
+
* Directory names are in format: `<timestamp>-<random>` where timestamp is Date.now()
|
|
19
|
+
* Example: "1704067200000-abc123" -> Date(2024-01-01T00:00:00.000Z)
|
|
20
|
+
*
|
|
21
|
+
* @param dirName - The directory name to parse
|
|
22
|
+
* @returns The parsed timestamp as Date, or null if invalid
|
|
23
|
+
*/
|
|
24
|
+
function parseSnapshotDirName(dirName) {
|
|
25
|
+
// Format: <timestamp>-<random>
|
|
26
|
+
const match = dirName.match(/^(\d+)-[\w]+$/);
|
|
27
|
+
if (!match || !match[1]) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const timestamp = parseInt(match[1], 10);
|
|
31
|
+
if (isNaN(timestamp) || timestamp <= 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const date = new Date(timestamp);
|
|
35
|
+
// Validate the date is reasonable (between year 2000 and year 3000)
|
|
36
|
+
// Use UTC year to avoid timezone issues
|
|
37
|
+
const year = date.getUTCFullYear();
|
|
38
|
+
if (year < 2000 || year > 3000) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return date;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* List all available snapshots
|
|
45
|
+
*
|
|
46
|
+
* Scans the snapshots directory and returns information about each valid snapshot.
|
|
47
|
+
* Results are sorted by timestamp descending (most recent first).
|
|
48
|
+
*
|
|
49
|
+
* @returns Array of snapshot info objects, sorted by timestamp descending
|
|
50
|
+
*/
|
|
51
|
+
export async function listSnapshots() {
|
|
52
|
+
const snapshotsDir = getSnapshotsDir();
|
|
53
|
+
// Check if snapshots directory exists
|
|
54
|
+
try {
|
|
55
|
+
await fs.access(snapshotsDir);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Directory doesn't exist - return empty array
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
// Read directory contents
|
|
62
|
+
let entries;
|
|
63
|
+
try {
|
|
64
|
+
entries = await fs.readdir(snapshotsDir);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Cannot read directory - return empty array
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
// Filter and parse valid snapshot directories
|
|
71
|
+
const snapshots = [];
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const timestamp = parseSnapshotDirName(entry);
|
|
74
|
+
if (!timestamp) {
|
|
75
|
+
// Invalid directory name format - skip
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const snapshotPath = path.join(snapshotsDir, entry, "phoenix");
|
|
79
|
+
// Verify the phoenix subdirectory exists
|
|
80
|
+
try {
|
|
81
|
+
const stat = await fs.stat(snapshotPath);
|
|
82
|
+
if (!stat.isDirectory()) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Phoenix subdirectory doesn't exist or can't be accessed - skip
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
snapshots.push({
|
|
91
|
+
path: snapshotPath,
|
|
92
|
+
timestamp,
|
|
93
|
+
id: entry,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// Sort by timestamp descending (most recent first)
|
|
97
|
+
snapshots.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
98
|
+
return snapshots;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get the latest (most recent) snapshot
|
|
102
|
+
*
|
|
103
|
+
* @returns The most recent snapshot info, or null if no snapshots exist
|
|
104
|
+
*/
|
|
105
|
+
export async function getLatestSnapshot() {
|
|
106
|
+
const snapshots = await listSnapshots();
|
|
107
|
+
if (snapshots.length === 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
// First element is the most recent due to descending sort
|
|
111
|
+
return snapshots[0] ?? null;
|
|
112
|
+
}
|
|
@@ -1 +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"}
|
|
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","../src/snapshot/utils.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cephalization/phoenix-insight",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A CLI for Arize AI Phoenix data analysis with AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -53,7 +53,9 @@
|
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@changesets/cli": "^2.29.8",
|
|
56
|
+
"@faker-js/faker": "^10.2.0",
|
|
56
57
|
"@types/node": "^18.19.0",
|
|
58
|
+
"msw": "^2.12.7",
|
|
57
59
|
"rimraf": "^5.0.10",
|
|
58
60
|
"tsc-alias": "^1.8.11",
|
|
59
61
|
"tsx": "^4.21.0",
|
package/src/cli.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
createPhoenixClient,
|
|
14
14
|
PhoenixClientError,
|
|
15
15
|
} from "./snapshot/index.js";
|
|
16
|
+
import { getLatestSnapshot, listSnapshots } from "./snapshot/utils.js";
|
|
16
17
|
import type { ExecutionMode } from "./modes/types.js";
|
|
17
18
|
import type { PhoenixInsightAgentConfig } from "./agent/index.js";
|
|
18
19
|
import { AgentProgress } from "./progress.js";
|
|
@@ -218,44 +219,111 @@ Examples:
|
|
|
218
219
|
await initializeConfig(cliArgs);
|
|
219
220
|
});
|
|
220
221
|
|
|
221
|
-
|
|
222
|
+
/**
|
|
223
|
+
* Shared logic for creating a snapshot.
|
|
224
|
+
* Used by both 'phoenix-insight snapshot' and 'phoenix-insight snapshot create'.
|
|
225
|
+
*/
|
|
226
|
+
async function executeSnapshotCreate(): Promise<void> {
|
|
227
|
+
const config = getConfig();
|
|
228
|
+
|
|
229
|
+
// Initialize observability if trace is enabled in config
|
|
230
|
+
if (config.trace) {
|
|
231
|
+
initializeObservability({
|
|
232
|
+
enabled: true,
|
|
233
|
+
baseUrl: config.baseUrl,
|
|
234
|
+
apiKey: config.apiKey,
|
|
235
|
+
projectName: "phoenix-insight-snapshot",
|
|
236
|
+
debug: !!process.env.DEBUG,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Determine the execution mode
|
|
242
|
+
const mode: ExecutionMode = await createLocalMode();
|
|
243
|
+
|
|
244
|
+
// Create snapshot with config values
|
|
245
|
+
const snapshotOptions = {
|
|
246
|
+
baseURL: config.baseUrl,
|
|
247
|
+
apiKey: config.apiKey,
|
|
248
|
+
spansPerProject: config.limit,
|
|
249
|
+
showProgress: true,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await createSnapshot(mode, snapshotOptions);
|
|
253
|
+
|
|
254
|
+
// Cleanup
|
|
255
|
+
await mode.cleanup();
|
|
256
|
+
|
|
257
|
+
// Shutdown observability if enabled
|
|
258
|
+
await shutdownObservability();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
handleError(error, "creating snapshot");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Create snapshot command group
|
|
265
|
+
const snapshotCmd = program
|
|
222
266
|
.command("snapshot")
|
|
223
|
-
.description("
|
|
224
|
-
.action(async () => {
|
|
225
|
-
const config = getConfig();
|
|
267
|
+
.description("Snapshot management commands");
|
|
226
268
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
baseUrl: config.baseUrl,
|
|
232
|
-
apiKey: config.apiKey,
|
|
233
|
-
projectName: "phoenix-insight-snapshot",
|
|
234
|
-
debug: !!process.env.DEBUG,
|
|
235
|
-
});
|
|
236
|
-
}
|
|
269
|
+
// Default action for 'phoenix-insight snapshot' (backward compatibility alias for 'snapshot create')
|
|
270
|
+
snapshotCmd.action(async () => {
|
|
271
|
+
await executeSnapshotCreate();
|
|
272
|
+
});
|
|
237
273
|
|
|
274
|
+
// Subcommand: snapshot create (explicit create command)
|
|
275
|
+
snapshotCmd
|
|
276
|
+
.command("create")
|
|
277
|
+
.description("Create a new snapshot from Phoenix data")
|
|
278
|
+
.action(async () => {
|
|
279
|
+
await executeSnapshotCreate();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// Subcommand: snapshot latest
|
|
283
|
+
snapshotCmd
|
|
284
|
+
.command("latest")
|
|
285
|
+
.description("Print the absolute path to the latest snapshot directory")
|
|
286
|
+
.action(async () => {
|
|
238
287
|
try {
|
|
239
|
-
|
|
240
|
-
const mode: ExecutionMode = await createLocalMode();
|
|
288
|
+
const latestSnapshot = await getLatestSnapshot();
|
|
241
289
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
spansPerProject: config.limit,
|
|
247
|
-
showProgress: true,
|
|
248
|
-
};
|
|
290
|
+
if (!latestSnapshot) {
|
|
291
|
+
console.error("No snapshots found");
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
249
294
|
|
|
250
|
-
|
|
295
|
+
// Print only the path to stdout, no decoration
|
|
296
|
+
console.log(latestSnapshot.path);
|
|
297
|
+
} catch (error) {
|
|
298
|
+
console.error(
|
|
299
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
300
|
+
);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
251
304
|
|
|
252
|
-
|
|
253
|
-
|
|
305
|
+
// Subcommand: snapshot list
|
|
306
|
+
snapshotCmd
|
|
307
|
+
.command("list")
|
|
308
|
+
.description("List all available snapshots with their timestamps")
|
|
309
|
+
.action(async () => {
|
|
310
|
+
try {
|
|
311
|
+
const snapshots = await listSnapshots();
|
|
312
|
+
|
|
313
|
+
// Print each snapshot: <timestamp> <path>
|
|
314
|
+
// Most recent first (already sorted by listSnapshots)
|
|
315
|
+
for (const snapshot of snapshots) {
|
|
316
|
+
// Format timestamp as ISO 8601
|
|
317
|
+
const isoTimestamp = snapshot.timestamp.toISOString();
|
|
318
|
+
console.log(`${isoTimestamp} ${snapshot.path}`);
|
|
319
|
+
}
|
|
254
320
|
|
|
255
|
-
//
|
|
256
|
-
await shutdownObservability();
|
|
321
|
+
// Exit code 0 even if empty (just print nothing)
|
|
257
322
|
} catch (error) {
|
|
258
|
-
|
|
323
|
+
console.error(
|
|
324
|
+
`Error: ${error instanceof Error ? error.message : String(error)}`
|
|
325
|
+
);
|
|
326
|
+
process.exit(1);
|
|
259
327
|
}
|
|
260
328
|
});
|
|
261
329
|
|
package/src/snapshot/context.ts
CHANGED
|
@@ -39,6 +39,131 @@ interface PromptInfo {
|
|
|
39
39
|
updatedAt?: string;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// =============================================================================
|
|
43
|
+
// Static Section Templates
|
|
44
|
+
// =============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Quick Start section for external agents - appears at the top for discoverability
|
|
48
|
+
*/
|
|
49
|
+
const QUICK_START_SECTION = `## Quick Start for External Agents
|
|
50
|
+
|
|
51
|
+
This is a **read-only snapshot** of Phoenix observability data. You cannot modify this data.
|
|
52
|
+
|
|
53
|
+
### Key Files to Start With
|
|
54
|
+
|
|
55
|
+
| File | Description |
|
|
56
|
+
|------|-------------|
|
|
57
|
+
| \`/phoenix/projects/index.jsonl\` | List of all projects with traces |
|
|
58
|
+
| \`/phoenix/datasets/index.jsonl\` | List of all datasets |
|
|
59
|
+
| \`/phoenix/experiments/index.jsonl\` | List of all experiments |
|
|
60
|
+
| \`/phoenix/prompts/index.jsonl\` | List of all prompts |
|
|
61
|
+
|
|
62
|
+
### How to Parse Each File Format
|
|
63
|
+
|
|
64
|
+
**JSONL files** (\`.jsonl\`): One JSON object per line
|
|
65
|
+
\`\`\`bash
|
|
66
|
+
# Read all lines as a JSON array
|
|
67
|
+
cat /phoenix/projects/index.jsonl | jq -s '.'
|
|
68
|
+
|
|
69
|
+
# Process each line individually
|
|
70
|
+
while read -r line; do echo "$line" | jq '.name'; done < /phoenix/projects/index.jsonl
|
|
71
|
+
|
|
72
|
+
# Get first N items
|
|
73
|
+
head -n 5 /phoenix/projects/index.jsonl | jq -s '.'
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
**JSON files** (\`.json\`): Standard JSON format
|
|
77
|
+
\`\`\`bash
|
|
78
|
+
# Read and pretty-print
|
|
79
|
+
cat /phoenix/projects/my-project/metadata.json | jq '.'
|
|
80
|
+
|
|
81
|
+
# Extract specific field
|
|
82
|
+
cat /phoenix/projects/my-project/metadata.json | jq '.name'
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
**Markdown files** (\`.md\`): Plain text prompt templates
|
|
86
|
+
\`\`\`bash
|
|
87
|
+
# Read prompt template
|
|
88
|
+
cat /phoenix/prompts/my-prompt/versions/v1.md
|
|
89
|
+
\`\`\`
|
|
90
|
+
|
|
91
|
+
### Common Operations
|
|
92
|
+
|
|
93
|
+
\`\`\`bash
|
|
94
|
+
# List all project names
|
|
95
|
+
cat /phoenix/projects/index.jsonl | jq -r '.name'
|
|
96
|
+
|
|
97
|
+
# Count spans in a project
|
|
98
|
+
wc -l < /phoenix/projects/my-project/spans/index.jsonl
|
|
99
|
+
|
|
100
|
+
# Find spans with errors
|
|
101
|
+
cat /phoenix/projects/my-project/spans/index.jsonl | jq 'select(.status_code == "ERROR")'
|
|
102
|
+
|
|
103
|
+
# Get dataset examples
|
|
104
|
+
cat /phoenix/datasets/my-dataset/examples.jsonl | jq -s '.' | head -n 100
|
|
105
|
+
|
|
106
|
+
# Search across all files
|
|
107
|
+
grep -r "error" /phoenix/
|
|
108
|
+
\`\`\``;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Directory Structure section showing the snapshot layout
|
|
112
|
+
*/
|
|
113
|
+
const DIRECTORY_STRUCTURE_SECTION = `## Directory Structure
|
|
114
|
+
|
|
115
|
+
\`\`\`
|
|
116
|
+
/phoenix/
|
|
117
|
+
_context.md # This file - start here!
|
|
118
|
+
/projects/
|
|
119
|
+
index.jsonl # List of all projects
|
|
120
|
+
/{project_name}/
|
|
121
|
+
metadata.json # Project details
|
|
122
|
+
/spans/
|
|
123
|
+
index.jsonl # Span data (may be sampled)
|
|
124
|
+
metadata.json # Span snapshot metadata
|
|
125
|
+
/datasets/
|
|
126
|
+
index.jsonl # List of all datasets
|
|
127
|
+
/{dataset_name}/
|
|
128
|
+
metadata.json # Dataset details
|
|
129
|
+
examples.jsonl # Dataset examples
|
|
130
|
+
/experiments/
|
|
131
|
+
index.jsonl # List of all experiments
|
|
132
|
+
/{experiment_id}/
|
|
133
|
+
metadata.json # Experiment details
|
|
134
|
+
runs.jsonl # Experiment runs
|
|
135
|
+
/prompts/
|
|
136
|
+
index.jsonl # List of all prompts
|
|
137
|
+
/{prompt_name}/
|
|
138
|
+
metadata.json # Prompt details
|
|
139
|
+
/versions/
|
|
140
|
+
index.jsonl # Version list
|
|
141
|
+
/{version_id}.md # Version template
|
|
142
|
+
/_meta/
|
|
143
|
+
snapshot.json # Snapshot metadata
|
|
144
|
+
\`\`\``;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* What You Can Do section describing available operations
|
|
148
|
+
*/
|
|
149
|
+
const WHAT_YOU_CAN_DO_SECTION = `## What You Can Do
|
|
150
|
+
|
|
151
|
+
- **Explore**: ls, cat, grep, find, jq, awk, sed
|
|
152
|
+
- **Fetch more data**: \`px-fetch-more spans --project <name> --limit 500\`
|
|
153
|
+
- **Fetch specific trace**: \`px-fetch-more trace --trace-id <id>\``;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Data Freshness section with refresh instructions
|
|
157
|
+
*/
|
|
158
|
+
const DATA_FRESHNESS_SECTION = `## Data Freshness
|
|
159
|
+
|
|
160
|
+
This is a **read-only snapshot**. Data may have changed since capture.
|
|
161
|
+
Run with \`--refresh\` to get latest data.`;
|
|
162
|
+
|
|
163
|
+
// =============================================================================
|
|
164
|
+
// Main Context Generation
|
|
165
|
+
// =============================================================================
|
|
166
|
+
|
|
42
167
|
/**
|
|
43
168
|
* Generates a _context.md summary file for the Phoenix snapshot
|
|
44
169
|
* This provides human and agent-readable context about what data is available
|
|
@@ -47,17 +172,54 @@ export async function generateContext(
|
|
|
47
172
|
mode: ExecutionMode,
|
|
48
173
|
metadata: ContextMetadata
|
|
49
174
|
): Promise<void> {
|
|
50
|
-
const lines: string[] = [];
|
|
51
|
-
|
|
52
|
-
// Header
|
|
53
|
-
lines.push("# Phoenix Snapshot Context");
|
|
54
|
-
lines.push("");
|
|
55
|
-
|
|
56
175
|
// Collect stats from the snapshot
|
|
57
176
|
const stats = await collectSnapshotStats(mode);
|
|
58
177
|
|
|
59
|
-
// What's Here section
|
|
178
|
+
// Build the dynamic "What's Here" section
|
|
179
|
+
const whatsHereSection = buildWhatsHereSection(stats, metadata);
|
|
180
|
+
|
|
181
|
+
// Build the dynamic "Recent Activity" section (may be empty)
|
|
182
|
+
const recentActivitySection = buildRecentActivitySection(stats);
|
|
183
|
+
|
|
184
|
+
// Compose the full context document
|
|
185
|
+
const content = [
|
|
186
|
+
"# Phoenix Snapshot Context",
|
|
187
|
+
"",
|
|
188
|
+
QUICK_START_SECTION,
|
|
189
|
+
"",
|
|
190
|
+
whatsHereSection,
|
|
191
|
+
recentActivitySection,
|
|
192
|
+
DIRECTORY_STRUCTURE_SECTION,
|
|
193
|
+
"",
|
|
194
|
+
WHAT_YOU_CAN_DO_SECTION,
|
|
195
|
+
"",
|
|
196
|
+
DATA_FRESHNESS_SECTION,
|
|
197
|
+
].join("\n");
|
|
198
|
+
|
|
199
|
+
// Write the context file
|
|
200
|
+
await mode.writeFile("/phoenix/_context.md", content);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// =============================================================================
|
|
204
|
+
// Dynamic Section Builders
|
|
205
|
+
// =============================================================================
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Builds the "What's Here" section with project/dataset/experiment/prompt summaries
|
|
209
|
+
*/
|
|
210
|
+
function buildWhatsHereSection(
|
|
211
|
+
stats: {
|
|
212
|
+
projects: ProjectStats[];
|
|
213
|
+
datasets: DatasetInfo[];
|
|
214
|
+
experiments: ExperimentInfo[];
|
|
215
|
+
prompts: PromptInfo[];
|
|
216
|
+
},
|
|
217
|
+
metadata: ContextMetadata
|
|
218
|
+
): string {
|
|
219
|
+
const lines: string[] = [];
|
|
220
|
+
|
|
60
221
|
lines.push("## What's Here");
|
|
222
|
+
lines.push("");
|
|
61
223
|
|
|
62
224
|
// Projects summary
|
|
63
225
|
if (stats.projects.length > 0) {
|
|
@@ -115,81 +277,40 @@ export async function generateContext(
|
|
|
115
277
|
);
|
|
116
278
|
lines.push("");
|
|
117
279
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (recentActivity.length > 0) {
|
|
121
|
-
lines.push("## Recent Activity");
|
|
122
|
-
for (const activity of recentActivity) {
|
|
123
|
-
lines.push(`- ${activity}`);
|
|
124
|
-
}
|
|
125
|
-
lines.push("");
|
|
126
|
-
}
|
|
280
|
+
return lines.join("\n");
|
|
281
|
+
}
|
|
127
282
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
283
|
+
/**
|
|
284
|
+
* Builds the "Recent Activity" section if there are recent updates
|
|
285
|
+
* Returns an empty string if no recent activity
|
|
286
|
+
*/
|
|
287
|
+
function buildRecentActivitySection(stats: {
|
|
288
|
+
projects: ProjectStats[];
|
|
289
|
+
datasets: DatasetInfo[];
|
|
290
|
+
experiments: ExperimentInfo[];
|
|
291
|
+
prompts: PromptInfo[];
|
|
292
|
+
}): string {
|
|
293
|
+
const activities = getRecentActivity(stats);
|
|
138
294
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
"This is a **read-only snapshot**. Data may have changed since capture."
|
|
143
|
-
);
|
|
144
|
-
lines.push("Run with `--refresh` to get latest data.");
|
|
145
|
-
lines.push("");
|
|
295
|
+
if (activities.length === 0) {
|
|
296
|
+
return "";
|
|
297
|
+
}
|
|
146
298
|
|
|
147
|
-
|
|
148
|
-
lines.push("##
|
|
149
|
-
lines.push(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
lines.push("- `.md` files: Markdown (prompt templates)");
|
|
299
|
+
const lines: string[] = [];
|
|
300
|
+
lines.push("## Recent Activity");
|
|
301
|
+
lines.push("");
|
|
302
|
+
for (const activity of activities) {
|
|
303
|
+
lines.push(`- ${activity}`);
|
|
304
|
+
}
|
|
154
305
|
lines.push("");
|
|
155
306
|
|
|
156
|
-
|
|
157
|
-
lines.push("## Directory Structure");
|
|
158
|
-
lines.push("```");
|
|
159
|
-
lines.push("/phoenix/");
|
|
160
|
-
lines.push(" _context.md # This file");
|
|
161
|
-
lines.push(" /projects/");
|
|
162
|
-
lines.push(" index.jsonl # List of all projects");
|
|
163
|
-
lines.push(" /{project_name}/");
|
|
164
|
-
lines.push(" metadata.json # Project details");
|
|
165
|
-
lines.push(" /spans/");
|
|
166
|
-
lines.push(" index.jsonl # Span data (may be sampled)");
|
|
167
|
-
lines.push(" metadata.json # Span snapshot metadata");
|
|
168
|
-
lines.push(" /datasets/");
|
|
169
|
-
lines.push(" index.jsonl # List of all datasets");
|
|
170
|
-
lines.push(" /{dataset_name}/");
|
|
171
|
-
lines.push(" metadata.json # Dataset details");
|
|
172
|
-
lines.push(" examples.jsonl # Dataset examples");
|
|
173
|
-
lines.push(" /experiments/");
|
|
174
|
-
lines.push(" index.jsonl # List of all experiments");
|
|
175
|
-
lines.push(" /{experiment_id}/");
|
|
176
|
-
lines.push(" metadata.json # Experiment details");
|
|
177
|
-
lines.push(" runs.jsonl # Experiment runs");
|
|
178
|
-
lines.push(" /prompts/");
|
|
179
|
-
lines.push(" index.jsonl # List of all prompts");
|
|
180
|
-
lines.push(" /{prompt_name}/");
|
|
181
|
-
lines.push(" metadata.json # Prompt details");
|
|
182
|
-
lines.push(" /versions/");
|
|
183
|
-
lines.push(" index.jsonl # Version list");
|
|
184
|
-
lines.push(" /{version_id}.md # Version template");
|
|
185
|
-
lines.push(" /_meta/");
|
|
186
|
-
lines.push(" snapshot.json # Snapshot metadata");
|
|
187
|
-
lines.push("```");
|
|
188
|
-
|
|
189
|
-
// Write the context file
|
|
190
|
-
await mode.writeFile("/phoenix/_context.md", lines.join("\n"));
|
|
307
|
+
return lines.join("\n");
|
|
191
308
|
}
|
|
192
309
|
|
|
310
|
+
// =============================================================================
|
|
311
|
+
// Data Collection
|
|
312
|
+
// =============================================================================
|
|
313
|
+
|
|
193
314
|
/**
|
|
194
315
|
* Collects statistics from the snapshot filesystem
|
|
195
316
|
*/
|
|
@@ -358,6 +479,10 @@ async function collectSnapshotStats(mode: ExecutionMode): Promise<{
|
|
|
358
479
|
return result;
|
|
359
480
|
}
|
|
360
481
|
|
|
482
|
+
// =============================================================================
|
|
483
|
+
// Helper Functions
|
|
484
|
+
// =============================================================================
|
|
485
|
+
|
|
361
486
|
/**
|
|
362
487
|
* Determines the status of an experiment based on its run counts
|
|
363
488
|
*/
|
package/src/snapshot/spans.ts
CHANGED
|
@@ -9,6 +9,22 @@ export interface SnapshotSpansOptions {
|
|
|
9
9
|
endTime?: Date | string | null;
|
|
10
10
|
/** Maximum number of spans to fetch per project (default: 1000) */
|
|
11
11
|
spansPerProject?: number;
|
|
12
|
+
/** Enable debug logging (default: uses DEBUG env var) */
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Debug logger that respects the debug flag or DEBUG environment variable
|
|
18
|
+
*/
|
|
19
|
+
function createDebugLogger(debug?: boolean) {
|
|
20
|
+
const isDebugEnabled = debug ?? !!process.env.DEBUG;
|
|
21
|
+
return {
|
|
22
|
+
log: (message: string) => {
|
|
23
|
+
if (isDebugEnabled) {
|
|
24
|
+
console.log(`[snapshotSpans] ${message}`);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
12
28
|
}
|
|
13
29
|
|
|
14
30
|
interface SpanData {
|
|
@@ -44,14 +60,16 @@ export async function snapshotSpans(
|
|
|
44
60
|
mode: ExecutionMode,
|
|
45
61
|
options: SnapshotSpansOptions = {}
|
|
46
62
|
): Promise<void> {
|
|
47
|
-
const { startTime, endTime, spansPerProject = 1000 } = options;
|
|
63
|
+
const { startTime, endTime, spansPerProject = 1000, debug } = options;
|
|
64
|
+
const logger = createDebugLogger(debug);
|
|
48
65
|
|
|
49
66
|
// Read projects index to get project names
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
);
|
|
67
|
+
// Use relative path so it works with the cwd set by the execution mode
|
|
68
|
+
logger.log("Reading projects index from projects/index.jsonl");
|
|
69
|
+
const projectsIndexContent = await mode.exec("cat projects/index.jsonl");
|
|
53
70
|
if (!projectsIndexContent.stdout) {
|
|
54
71
|
// No projects, nothing to do
|
|
72
|
+
logger.log("No projects found in index, skipping span fetch");
|
|
55
73
|
return;
|
|
56
74
|
}
|
|
57
75
|
|
|
@@ -64,9 +82,12 @@ export async function snapshotSpans(
|
|
|
64
82
|
return project.name;
|
|
65
83
|
});
|
|
66
84
|
|
|
85
|
+
logger.log(`Found ${projectNames.length} project(s): ${projectNames.join(", ")}`);
|
|
86
|
+
|
|
67
87
|
// Fetch spans for each project
|
|
68
88
|
for (const projectName of projectNames) {
|
|
69
89
|
await withErrorHandling(async () => {
|
|
90
|
+
logger.log(`Starting span fetch for project: ${projectName}`);
|
|
70
91
|
const spans: SpanData[] = [];
|
|
71
92
|
let cursor: string | null = null;
|
|
72
93
|
let totalFetched = 0;
|
|
@@ -117,14 +138,17 @@ export async function snapshotSpans(
|
|
|
117
138
|
}
|
|
118
139
|
}
|
|
119
140
|
|
|
141
|
+
logger.log(`Completed span fetch for project ${projectName}: ${spans.length} span(s) fetched`);
|
|
142
|
+
|
|
120
143
|
// Write spans to JSONL file
|
|
144
|
+
const spansFilePath = `/phoenix/projects/${projectName}/spans/index.jsonl`;
|
|
145
|
+
logger.log(`Writing spans to ${spansFilePath}`);
|
|
121
146
|
const jsonlContent = spans.map((span) => JSON.stringify(span)).join("\n");
|
|
122
|
-
await mode.writeFile(
|
|
123
|
-
`/phoenix/projects/${projectName}/spans/index.jsonl`,
|
|
124
|
-
jsonlContent
|
|
125
|
-
);
|
|
147
|
+
await mode.writeFile(spansFilePath, jsonlContent);
|
|
126
148
|
|
|
127
149
|
// Write metadata about the spans snapshot
|
|
150
|
+
const metadataFilePath = `/phoenix/projects/${projectName}/spans/metadata.json`;
|
|
151
|
+
logger.log(`Writing metadata to ${metadataFilePath}`);
|
|
128
152
|
const metadata = {
|
|
129
153
|
project: projectName,
|
|
130
154
|
spanCount: spans.length,
|
|
@@ -133,10 +157,7 @@ export async function snapshotSpans(
|
|
|
133
157
|
snapshotTime: new Date().toISOString(),
|
|
134
158
|
};
|
|
135
159
|
|
|
136
|
-
await mode.writeFile(
|
|
137
|
-
`/phoenix/projects/${projectName}/spans/metadata.json`,
|
|
138
|
-
JSON.stringify(metadata, null, 2)
|
|
139
|
-
);
|
|
160
|
+
await mode.writeFile(metadataFilePath, JSON.stringify(metadata, null, 2));
|
|
140
161
|
}, `fetching spans for project ${projectName}`);
|
|
141
162
|
}
|
|
142
163
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot discovery utilities
|
|
3
|
+
*
|
|
4
|
+
* Functions for listing and finding snapshots in the local filesystem.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Information about a single snapshot
|
|
13
|
+
*/
|
|
14
|
+
export interface SnapshotInfo {
|
|
15
|
+
/** Absolute path to the snapshot directory (the 'phoenix' subdirectory) */
|
|
16
|
+
path: string;
|
|
17
|
+
/** Timestamp when the snapshot was created (from directory name) */
|
|
18
|
+
timestamp: Date;
|
|
19
|
+
/** Unique identifier for the snapshot (directory name) */
|
|
20
|
+
id: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the base snapshots directory path
|
|
25
|
+
*/
|
|
26
|
+
export function getSnapshotsDir(): string {
|
|
27
|
+
return path.join(os.homedir(), ".phoenix-insight", "snapshots");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a snapshot directory name to extract timestamp
|
|
32
|
+
*
|
|
33
|
+
* Directory names are in format: `<timestamp>-<random>` where timestamp is Date.now()
|
|
34
|
+
* Example: "1704067200000-abc123" -> Date(2024-01-01T00:00:00.000Z)
|
|
35
|
+
*
|
|
36
|
+
* @param dirName - The directory name to parse
|
|
37
|
+
* @returns The parsed timestamp as Date, or null if invalid
|
|
38
|
+
*/
|
|
39
|
+
function parseSnapshotDirName(dirName: string): Date | null {
|
|
40
|
+
// Format: <timestamp>-<random>
|
|
41
|
+
const match = dirName.match(/^(\d+)-[\w]+$/);
|
|
42
|
+
if (!match || !match[1]) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const timestamp = parseInt(match[1], 10);
|
|
47
|
+
if (isNaN(timestamp) || timestamp <= 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const date = new Date(timestamp);
|
|
52
|
+
// Validate the date is reasonable (between year 2000 and year 3000)
|
|
53
|
+
// Use UTC year to avoid timezone issues
|
|
54
|
+
const year = date.getUTCFullYear();
|
|
55
|
+
if (year < 2000 || year > 3000) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return date;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* List all available snapshots
|
|
64
|
+
*
|
|
65
|
+
* Scans the snapshots directory and returns information about each valid snapshot.
|
|
66
|
+
* Results are sorted by timestamp descending (most recent first).
|
|
67
|
+
*
|
|
68
|
+
* @returns Array of snapshot info objects, sorted by timestamp descending
|
|
69
|
+
*/
|
|
70
|
+
export async function listSnapshots(): Promise<SnapshotInfo[]> {
|
|
71
|
+
const snapshotsDir = getSnapshotsDir();
|
|
72
|
+
|
|
73
|
+
// Check if snapshots directory exists
|
|
74
|
+
try {
|
|
75
|
+
await fs.access(snapshotsDir);
|
|
76
|
+
} catch {
|
|
77
|
+
// Directory doesn't exist - return empty array
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Read directory contents
|
|
82
|
+
let entries: string[];
|
|
83
|
+
try {
|
|
84
|
+
entries = await fs.readdir(snapshotsDir);
|
|
85
|
+
} catch {
|
|
86
|
+
// Cannot read directory - return empty array
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Filter and parse valid snapshot directories
|
|
91
|
+
const snapshots: SnapshotInfo[] = [];
|
|
92
|
+
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
const timestamp = parseSnapshotDirName(entry);
|
|
95
|
+
if (!timestamp) {
|
|
96
|
+
// Invalid directory name format - skip
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const snapshotPath = path.join(snapshotsDir, entry, "phoenix");
|
|
101
|
+
|
|
102
|
+
// Verify the phoenix subdirectory exists
|
|
103
|
+
try {
|
|
104
|
+
const stat = await fs.stat(snapshotPath);
|
|
105
|
+
if (!stat.isDirectory()) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Phoenix subdirectory doesn't exist or can't be accessed - skip
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
snapshots.push({
|
|
114
|
+
path: snapshotPath,
|
|
115
|
+
timestamp,
|
|
116
|
+
id: entry,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Sort by timestamp descending (most recent first)
|
|
121
|
+
snapshots.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
122
|
+
|
|
123
|
+
return snapshots;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get the latest (most recent) snapshot
|
|
128
|
+
*
|
|
129
|
+
* @returns The most recent snapshot info, or null if no snapshots exist
|
|
130
|
+
*/
|
|
131
|
+
export async function getLatestSnapshot(): Promise<SnapshotInfo | null> {
|
|
132
|
+
const snapshots = await listSnapshots();
|
|
133
|
+
|
|
134
|
+
if (snapshots.length === 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// First element is the most recent due to descending sort
|
|
139
|
+
return snapshots[0] ?? null;
|
|
140
|
+
}
|