@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
package/src/cli.ts
ADDED
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import * as readline from "node:readline";
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import { createSandboxMode, createLocalMode } from "./modes/index.js";
|
|
9
|
+
import { createInsightAgent, runOneShotQuery } from "./agent/index.js";
|
|
10
|
+
import {
|
|
11
|
+
createSnapshot,
|
|
12
|
+
createIncrementalSnapshot,
|
|
13
|
+
createPhoenixClient,
|
|
14
|
+
PhoenixClientError,
|
|
15
|
+
} from "./snapshot/index.js";
|
|
16
|
+
import type { ExecutionMode } from "./modes/types.js";
|
|
17
|
+
import type { PhoenixInsightAgentConfig } from "./agent/index.js";
|
|
18
|
+
import { AgentProgress } from "./progress.js";
|
|
19
|
+
import {
|
|
20
|
+
initializeObservability,
|
|
21
|
+
shutdownObservability,
|
|
22
|
+
} from "./observability/index.js";
|
|
23
|
+
import { initializeConfig, getConfig, type CliArgs } from "./config/index.js";
|
|
24
|
+
|
|
25
|
+
// Version will be read from package.json during build
|
|
26
|
+
const VERSION = "0.0.1";
|
|
27
|
+
|
|
28
|
+
const program = new Command();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format bash command for display in progress indicator
|
|
32
|
+
*/
|
|
33
|
+
function formatBashCommand(command: string): string {
|
|
34
|
+
if (!command) return "";
|
|
35
|
+
|
|
36
|
+
// Split by newline and get first line
|
|
37
|
+
const lines = command.split("\n");
|
|
38
|
+
const firstLine = lines[0]?.trim() || "";
|
|
39
|
+
|
|
40
|
+
// Check for pipeline first (3+ commands)
|
|
41
|
+
if (firstLine.includes(" | ") && firstLine.split(" | ").length > 2) {
|
|
42
|
+
const parts = firstLine.split(" | ");
|
|
43
|
+
const firstCmd = parts[0]?.split(" ")[0] || "";
|
|
44
|
+
const lastCmd = parts[parts.length - 1]?.split(" ")[0] || "";
|
|
45
|
+
return `${firstCmd} | ... | ${lastCmd}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Common command patterns to display nicely
|
|
49
|
+
if (firstLine.startsWith("cat ")) {
|
|
50
|
+
const file = firstLine.substring(4).trim();
|
|
51
|
+
return `cat ${file}`;
|
|
52
|
+
} else if (firstLine.startsWith("grep ")) {
|
|
53
|
+
// Extract pattern and file/directory
|
|
54
|
+
const match = firstLine.match(
|
|
55
|
+
/grep\s+(?:-[^\s]+\s+)*['"]?([^'"]+)['"]?\s+(.+)/
|
|
56
|
+
);
|
|
57
|
+
if (match && match[1] && match[2]) {
|
|
58
|
+
return `grep "${match[1]}" in ${match[2]}`;
|
|
59
|
+
}
|
|
60
|
+
return firstLine.substring(0, 60) + (firstLine.length > 60 ? "..." : "");
|
|
61
|
+
} else if (firstLine.startsWith("find ")) {
|
|
62
|
+
const match = firstLine.match(
|
|
63
|
+
/find\s+([^\s]+)(?:\s+-name\s+['"]?([^'"]+)['"]?)?/
|
|
64
|
+
);
|
|
65
|
+
if (match && match[1]) {
|
|
66
|
+
return match[2]
|
|
67
|
+
? `find "${match[2]}" in ${match[1]}`
|
|
68
|
+
: `find in ${match[1]}`;
|
|
69
|
+
}
|
|
70
|
+
return firstLine.substring(0, 60) + (firstLine.length > 60 ? "..." : "");
|
|
71
|
+
} else if (firstLine.startsWith("ls ")) {
|
|
72
|
+
const path = firstLine.substring(3).trim();
|
|
73
|
+
return path ? `ls ${path}` : "ls";
|
|
74
|
+
} else if (firstLine.startsWith("ls")) {
|
|
75
|
+
return "ls";
|
|
76
|
+
} else if (firstLine.startsWith("jq ")) {
|
|
77
|
+
return `jq processing JSON data`;
|
|
78
|
+
} else if (firstLine.startsWith("head ") || firstLine.startsWith("tail ")) {
|
|
79
|
+
const cmd = firstLine.split(" ")[0];
|
|
80
|
+
const fileMatch = firstLine.match(/(?:head|tail)\s+(?:-[^\s]+\s+)*(.+)/);
|
|
81
|
+
if (fileMatch && fileMatch[1]) {
|
|
82
|
+
return `${cmd} ${fileMatch[1]}`;
|
|
83
|
+
}
|
|
84
|
+
return firstLine.substring(0, 60) + (firstLine.length > 60 ? "..." : "");
|
|
85
|
+
} else {
|
|
86
|
+
// For other commands, show up to 80 characters
|
|
87
|
+
return firstLine.substring(0, 80) + (firstLine.length > 80 ? "..." : "");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle errors with appropriate exit codes and user-friendly messages
|
|
93
|
+
*/
|
|
94
|
+
function handleError(error: unknown, context: string): never {
|
|
95
|
+
console.error(`\n❌ Error ${context}:`);
|
|
96
|
+
|
|
97
|
+
if (error instanceof PhoenixClientError) {
|
|
98
|
+
switch (error.code) {
|
|
99
|
+
case "NETWORK_ERROR":
|
|
100
|
+
console.error(
|
|
101
|
+
"\n🌐 Network Error: Unable to connect to Phoenix server"
|
|
102
|
+
);
|
|
103
|
+
console.error(` Make sure Phoenix is running and accessible`);
|
|
104
|
+
console.error(` You can specify a different URL with --base-url`);
|
|
105
|
+
break;
|
|
106
|
+
case "AUTH_ERROR":
|
|
107
|
+
console.error("\n🔒 Authentication Error: Invalid or missing API key");
|
|
108
|
+
console.error(
|
|
109
|
+
` Set the PHOENIX_API_KEY environment variable or use --api-key`
|
|
110
|
+
);
|
|
111
|
+
break;
|
|
112
|
+
case "INVALID_RESPONSE":
|
|
113
|
+
console.error(
|
|
114
|
+
"\n⚠️ Invalid Response: Phoenix returned unexpected data"
|
|
115
|
+
);
|
|
116
|
+
console.error(` This might be a version compatibility issue`);
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
console.error("\n❓ Phoenix Client Error:", error.message);
|
|
120
|
+
}
|
|
121
|
+
if (error.originalError && process.env.DEBUG) {
|
|
122
|
+
console.error("\nOriginal error:", error.originalError);
|
|
123
|
+
}
|
|
124
|
+
} else if (error instanceof Error) {
|
|
125
|
+
// Check for specific error patterns
|
|
126
|
+
if (error.message.includes("ENOENT")) {
|
|
127
|
+
console.error(
|
|
128
|
+
"\n📁 File System Error: Required file or directory not found"
|
|
129
|
+
);
|
|
130
|
+
console.error(` ${error.message}`);
|
|
131
|
+
} else if (
|
|
132
|
+
error.message.includes("EACCES") ||
|
|
133
|
+
error.message.includes("EPERM")
|
|
134
|
+
) {
|
|
135
|
+
console.error("\n🚫 Permission Error: Insufficient permissions");
|
|
136
|
+
console.error(` ${error.message}`);
|
|
137
|
+
if (error.message.includes(".phoenix-insight")) {
|
|
138
|
+
console.error(
|
|
139
|
+
` Try running with appropriate permissions or check ~/.phoenix-insight/`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
} else if (
|
|
143
|
+
error.message.includes("rate limit") ||
|
|
144
|
+
error.message.includes("429")
|
|
145
|
+
) {
|
|
146
|
+
console.error("\n⏱️ Rate Limit Error: Too many requests to Phoenix");
|
|
147
|
+
console.error(` Please wait a moment and try again`);
|
|
148
|
+
} else if (error.message.includes("timeout")) {
|
|
149
|
+
console.error("\n⏰ Timeout Error: Request took too long");
|
|
150
|
+
console.error(` The Phoenix server might be slow or unresponsive`);
|
|
151
|
+
} else {
|
|
152
|
+
console.error(`\n${error.message}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (error.stack && process.env.DEBUG) {
|
|
156
|
+
console.error("\nStack trace:", error.stack);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
console.error("\nUnexpected error:", error);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.error("\n💡 Tips:");
|
|
163
|
+
console.error(" • Run with DEBUG=1 for more detailed error information");
|
|
164
|
+
console.error(
|
|
165
|
+
" • Check your Phoenix connection with: phoenix-insight snapshot --base-url <url>"
|
|
166
|
+
);
|
|
167
|
+
console.error(" • Use --help to see all available options");
|
|
168
|
+
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
program
|
|
173
|
+
.name("phoenix-insight")
|
|
174
|
+
.description("A CLI for Phoenix data analysis with AI agents")
|
|
175
|
+
.version(VERSION)
|
|
176
|
+
.usage("[options] [query]")
|
|
177
|
+
.option(
|
|
178
|
+
"--config <path>",
|
|
179
|
+
"Path to config file (default: ~/.phoenix-insight/config.json, or set PHOENIX_INSIGHT_CONFIG env var)"
|
|
180
|
+
)
|
|
181
|
+
.addHelpText(
|
|
182
|
+
"after",
|
|
183
|
+
`
|
|
184
|
+
Configuration:
|
|
185
|
+
Config values are loaded with the following priority (highest to lowest):
|
|
186
|
+
1. CLI arguments (e.g., --base-url)
|
|
187
|
+
2. Environment variables (e.g., PHOENIX_BASE_URL)
|
|
188
|
+
3. Config file (~/.phoenix-insight/config.json)
|
|
189
|
+
|
|
190
|
+
Use --config to specify a custom config file path.
|
|
191
|
+
Set PHOENIX_INSIGHT_CONFIG env var to override the default config location.
|
|
192
|
+
|
|
193
|
+
Examples:
|
|
194
|
+
$ phoenix-insight # Start interactive mode
|
|
195
|
+
$ phoenix-insight "What are the slowest traces?" # Single query (sandbox mode)
|
|
196
|
+
$ phoenix-insight --interactive # Explicitly start interactive mode
|
|
197
|
+
$ phoenix-insight --local "Show me error patterns" # Local mode with persistence
|
|
198
|
+
$ phoenix-insight --local --stream "Analyze recent experiments" # Local mode with streaming
|
|
199
|
+
$ phoenix-insight --config ./my-config.json "Analyze traces" # Use custom config file
|
|
200
|
+
$ phoenix-insight help # Show this help message
|
|
201
|
+
`
|
|
202
|
+
)
|
|
203
|
+
.hook("preAction", async (thisCommand) => {
|
|
204
|
+
// Get all options from the root command
|
|
205
|
+
const opts = thisCommand.opts();
|
|
206
|
+
// Build CLI args from commander options
|
|
207
|
+
const cliArgs: CliArgs = {
|
|
208
|
+
config: opts.config,
|
|
209
|
+
baseUrl: opts.baseUrl,
|
|
210
|
+
apiKey: opts.apiKey,
|
|
211
|
+
limit: opts.limit,
|
|
212
|
+
stream: opts.stream,
|
|
213
|
+
local: opts.local,
|
|
214
|
+
refresh: opts.refresh,
|
|
215
|
+
trace: opts.trace,
|
|
216
|
+
};
|
|
217
|
+
// Initialize config singleton before any command runs
|
|
218
|
+
await initializeConfig(cliArgs);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
program
|
|
222
|
+
.command("snapshot")
|
|
223
|
+
.description("Create a snapshot of Phoenix data")
|
|
224
|
+
.action(async () => {
|
|
225
|
+
const config = getConfig();
|
|
226
|
+
|
|
227
|
+
// Initialize observability if trace is enabled in config
|
|
228
|
+
if (config.trace) {
|
|
229
|
+
initializeObservability({
|
|
230
|
+
enabled: true,
|
|
231
|
+
baseUrl: config.baseUrl,
|
|
232
|
+
apiKey: config.apiKey,
|
|
233
|
+
projectName: "phoenix-insight-snapshot",
|
|
234
|
+
debug: !!process.env.DEBUG,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
// Determine the execution mode
|
|
240
|
+
const mode: ExecutionMode = await createLocalMode();
|
|
241
|
+
|
|
242
|
+
// Create snapshot with config values
|
|
243
|
+
const snapshotOptions = {
|
|
244
|
+
baseURL: config.baseUrl,
|
|
245
|
+
apiKey: config.apiKey,
|
|
246
|
+
spansPerProject: config.limit,
|
|
247
|
+
showProgress: true,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
await createSnapshot(mode, snapshotOptions);
|
|
251
|
+
|
|
252
|
+
// Cleanup
|
|
253
|
+
await mode.cleanup();
|
|
254
|
+
|
|
255
|
+
// Shutdown observability if enabled
|
|
256
|
+
await shutdownObservability();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
handleError(error, "creating snapshot");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
program
|
|
263
|
+
.command("help")
|
|
264
|
+
.description("Show help information")
|
|
265
|
+
.action(() => {
|
|
266
|
+
program.outputHelp();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
program
|
|
270
|
+
.command("prune")
|
|
271
|
+
.description("Delete the local snapshot directory (~/.phoenix-insight/)")
|
|
272
|
+
.option("--dry-run", "Show what would be deleted without actually deleting")
|
|
273
|
+
.action(async (options) => {
|
|
274
|
+
const snapshotDir = path.join(os.homedir(), ".phoenix-insight");
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Check if the directory exists
|
|
278
|
+
const stats = await fs.stat(snapshotDir).catch(() => null);
|
|
279
|
+
|
|
280
|
+
if (!stats) {
|
|
281
|
+
console.log("📁 No local snapshot directory found. Nothing to prune.");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (options.dryRun) {
|
|
286
|
+
console.log("🔍 Dry run mode - would delete:");
|
|
287
|
+
console.log(` ${snapshotDir}`);
|
|
288
|
+
|
|
289
|
+
// Show size and count of snapshots
|
|
290
|
+
const snapshots = await fs
|
|
291
|
+
.readdir(path.join(snapshotDir, "snapshots"))
|
|
292
|
+
.catch(() => []);
|
|
293
|
+
console.log(` 📊 Contains ${snapshots.length} snapshot(s)`);
|
|
294
|
+
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Ask for confirmation
|
|
299
|
+
const rl = readline.createInterface({
|
|
300
|
+
input: process.stdin,
|
|
301
|
+
output: process.stdout,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const answer = await new Promise<string>((resolve) => {
|
|
305
|
+
rl.question(
|
|
306
|
+
`⚠️ This will delete all local snapshots at:\n ${snapshotDir}\n\n Are you sure? (yes/no): `,
|
|
307
|
+
resolve
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
rl.close();
|
|
312
|
+
|
|
313
|
+
if (answer.toLowerCase() !== "yes" && answer.toLowerCase() !== "y") {
|
|
314
|
+
console.log("❌ Prune cancelled.");
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Delete the directory
|
|
319
|
+
await fs.rm(snapshotDir, { recursive: true, force: true });
|
|
320
|
+
console.log("✅ Local snapshot directory deleted successfully!");
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error("❌ Error pruning snapshots:");
|
|
323
|
+
console.error(
|
|
324
|
+
` ${error instanceof Error ? error.message : String(error)}`
|
|
325
|
+
);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
program
|
|
331
|
+
.argument("[query]", "Query to run against Phoenix data")
|
|
332
|
+
.option(
|
|
333
|
+
"--sandbox",
|
|
334
|
+
"Run in sandbox mode with in-memory filesystem (default)"
|
|
335
|
+
)
|
|
336
|
+
.option("--local", "Run in local mode with real filesystem")
|
|
337
|
+
.option("--base-url <url>", "Phoenix base URL")
|
|
338
|
+
.option("--api-key <key>", "Phoenix API key")
|
|
339
|
+
.option("--refresh", "Force refresh of snapshot data")
|
|
340
|
+
.option("--limit <number>", "Limit number of spans to fetch", parseInt)
|
|
341
|
+
.option("--stream [true|false]", "Stream agent responses", (v) =>
|
|
342
|
+
["f", "false"].includes(v.toLowerCase()) ? false : true
|
|
343
|
+
)
|
|
344
|
+
.option("-i, --interactive", "Run in interactive mode (REPL)")
|
|
345
|
+
.option("--trace", "Enable tracing of the agent to Phoenix")
|
|
346
|
+
.action(async (query, options) => {
|
|
347
|
+
const config = getConfig();
|
|
348
|
+
// If interactive mode is requested, ignore query argument
|
|
349
|
+
if (options.interactive) {
|
|
350
|
+
await runInteractiveMode();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// If no query is provided and no specific flag, start interactive mode
|
|
355
|
+
if (!query && !options.help) {
|
|
356
|
+
await runInteractiveMode();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Initialize observability if trace is enabled in config
|
|
361
|
+
if (config.trace) {
|
|
362
|
+
initializeObservability({
|
|
363
|
+
enabled: true,
|
|
364
|
+
baseUrl: config.baseUrl,
|
|
365
|
+
apiKey: config.apiKey,
|
|
366
|
+
projectName: "phoenix-insight",
|
|
367
|
+
debug: !!process.env.DEBUG,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
// Determine the execution mode
|
|
373
|
+
const mode: ExecutionMode =
|
|
374
|
+
config.mode === "local" ? await createLocalMode() : createSandboxMode();
|
|
375
|
+
|
|
376
|
+
// Create Phoenix client
|
|
377
|
+
const client = createPhoenixClient({
|
|
378
|
+
baseURL: config.baseUrl,
|
|
379
|
+
apiKey: config.apiKey,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Create or update snapshot
|
|
383
|
+
const snapshotOptions = {
|
|
384
|
+
baseURL: config.baseUrl,
|
|
385
|
+
apiKey: config.apiKey,
|
|
386
|
+
spansPerProject: config.limit,
|
|
387
|
+
showProgress: true,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (config.refresh || config.mode !== "local") {
|
|
391
|
+
// For sandbox mode (default) or when refresh is requested, always create a fresh snapshot
|
|
392
|
+
await createSnapshot(mode, snapshotOptions);
|
|
393
|
+
} else {
|
|
394
|
+
// For local mode without refresh, try incremental update
|
|
395
|
+
await createIncrementalSnapshot(mode, snapshotOptions);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Create agent configuration
|
|
399
|
+
const agentConfig: PhoenixInsightAgentConfig = {
|
|
400
|
+
mode,
|
|
401
|
+
client,
|
|
402
|
+
maxSteps: 25,
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
// Execute the query
|
|
406
|
+
const agentProgress = new AgentProgress(!config.stream);
|
|
407
|
+
agentProgress.startThinking();
|
|
408
|
+
|
|
409
|
+
if (config.stream) {
|
|
410
|
+
// Stream mode
|
|
411
|
+
const result = (await runOneShotQuery(agentConfig, query, {
|
|
412
|
+
stream: true,
|
|
413
|
+
onStepFinish: (step) => {
|
|
414
|
+
// Show tool usage even in stream mode
|
|
415
|
+
if (step.toolCalls?.length) {
|
|
416
|
+
step.toolCalls.forEach((toolCall: any) => {
|
|
417
|
+
const toolName = toolCall.toolName;
|
|
418
|
+
if (toolName === "bash") {
|
|
419
|
+
// Extract bash command for better visibility
|
|
420
|
+
const command = toolCall.args?.command || "";
|
|
421
|
+
const formattedCmd = formatBashCommand(command);
|
|
422
|
+
agentProgress.updateTool(toolName, formattedCmd);
|
|
423
|
+
} else {
|
|
424
|
+
agentProgress.updateTool(toolName);
|
|
425
|
+
}
|
|
426
|
+
console.log();
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Show tool results
|
|
431
|
+
if (step.toolResults?.length) {
|
|
432
|
+
step.toolResults.forEach((toolResult: any) => {
|
|
433
|
+
agentProgress.updateToolResult(
|
|
434
|
+
toolResult.toolName,
|
|
435
|
+
!toolResult.isError
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
console.log();
|
|
439
|
+
}
|
|
440
|
+
},
|
|
441
|
+
})) as any; // Type assertion needed due to union type
|
|
442
|
+
|
|
443
|
+
// Stop progress before streaming
|
|
444
|
+
agentProgress.stop();
|
|
445
|
+
|
|
446
|
+
// Handle streaming response
|
|
447
|
+
console.log("\n✨ Answer:\n");
|
|
448
|
+
for await (const chunk of result.textStream) {
|
|
449
|
+
process.stdout.write(chunk);
|
|
450
|
+
}
|
|
451
|
+
console.log(); // Final newline
|
|
452
|
+
|
|
453
|
+
// Wait for full response to complete
|
|
454
|
+
await result.response;
|
|
455
|
+
} else {
|
|
456
|
+
// Non-streaming mode
|
|
457
|
+
const result = (await runOneShotQuery(agentConfig, query, {
|
|
458
|
+
onStepFinish: (step) => {
|
|
459
|
+
// Show tool usage
|
|
460
|
+
if (step.toolCalls?.length) {
|
|
461
|
+
step.toolCalls.forEach((toolCall: any) => {
|
|
462
|
+
const toolName = toolCall.toolName;
|
|
463
|
+
if (toolName === "bash") {
|
|
464
|
+
// Extract bash command for better visibility
|
|
465
|
+
const command = toolCall.args?.command || "";
|
|
466
|
+
const formattedCmd = formatBashCommand(command);
|
|
467
|
+
agentProgress.updateTool(toolName, formattedCmd);
|
|
468
|
+
} else {
|
|
469
|
+
agentProgress.updateTool(toolName);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Show tool results
|
|
475
|
+
if (step.toolResults?.length) {
|
|
476
|
+
step.toolResults.forEach((toolResult: any) => {
|
|
477
|
+
agentProgress.updateToolResult(
|
|
478
|
+
toolResult.toolName,
|
|
479
|
+
!toolResult.isError
|
|
480
|
+
);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
})) as any; // Type assertion needed due to union type
|
|
485
|
+
|
|
486
|
+
// Stop progress and display the final answer
|
|
487
|
+
agentProgress.succeed();
|
|
488
|
+
console.log("\n✨ Answer:\n");
|
|
489
|
+
console.log(result.text);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Cleanup
|
|
493
|
+
await mode.cleanup();
|
|
494
|
+
|
|
495
|
+
console.log("\n✅ Done!");
|
|
496
|
+
} catch (error) {
|
|
497
|
+
handleError(error, "executing query");
|
|
498
|
+
} finally {
|
|
499
|
+
// Shutdown observability if enabled
|
|
500
|
+
await shutdownObservability();
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
async function runInteractiveMode(): Promise<void> {
|
|
505
|
+
const config = getConfig();
|
|
506
|
+
|
|
507
|
+
console.log("🚀 Phoenix Insight Interactive Mode");
|
|
508
|
+
console.log(
|
|
509
|
+
"Type your queries below. Type 'help' for available commands or 'exit' to quit.\n"
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Prevent the process from exiting on unhandled promise rejections
|
|
513
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
514
|
+
console.error("\n⚠️ Unhandled promise rejection:", reason);
|
|
515
|
+
console.error(
|
|
516
|
+
"The interactive mode will continue. You can try another query."
|
|
517
|
+
);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// Initialize observability if trace is enabled in config
|
|
521
|
+
if (config.trace) {
|
|
522
|
+
initializeObservability({
|
|
523
|
+
enabled: true,
|
|
524
|
+
baseUrl: config.baseUrl,
|
|
525
|
+
apiKey: config.apiKey,
|
|
526
|
+
projectName: "phoenix-insight",
|
|
527
|
+
debug: !!process.env.DEBUG,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Setup mode and snapshot once for the session
|
|
532
|
+
let mode: ExecutionMode;
|
|
533
|
+
let agent: any;
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
// Determine the execution mode
|
|
537
|
+
mode =
|
|
538
|
+
config.mode === "local" ? await createLocalMode() : createSandboxMode();
|
|
539
|
+
|
|
540
|
+
// Create Phoenix client
|
|
541
|
+
const client = createPhoenixClient({
|
|
542
|
+
baseURL: config.baseUrl,
|
|
543
|
+
apiKey: config.apiKey,
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Create or update snapshot
|
|
547
|
+
const snapshotOptions = {
|
|
548
|
+
baseURL: config.baseUrl,
|
|
549
|
+
apiKey: config.apiKey,
|
|
550
|
+
spansPerProject: config.limit,
|
|
551
|
+
showProgress: true,
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
if (config.refresh || config.mode !== "local") {
|
|
555
|
+
await createSnapshot(mode, snapshotOptions);
|
|
556
|
+
} else {
|
|
557
|
+
await createIncrementalSnapshot(mode, snapshotOptions);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log(
|
|
561
|
+
"\n✅ Snapshot ready. You can now ask questions about your Phoenix data.\n"
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// Create agent configuration
|
|
565
|
+
const agentConfig: PhoenixInsightAgentConfig = {
|
|
566
|
+
mode,
|
|
567
|
+
client,
|
|
568
|
+
maxSteps: 25,
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// Create reusable agent
|
|
572
|
+
agent = await createInsightAgent(agentConfig);
|
|
573
|
+
|
|
574
|
+
// Setup readline interface
|
|
575
|
+
const rl = readline.createInterface({
|
|
576
|
+
input: process.stdin,
|
|
577
|
+
output: process.stdout,
|
|
578
|
+
prompt: "phoenix> ",
|
|
579
|
+
terminal: true, // Ensure terminal mode for better compatibility
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
let userExited = false;
|
|
583
|
+
|
|
584
|
+
// Handle SIGINT (Ctrl+C) gracefully
|
|
585
|
+
rl.on("SIGINT", () => {
|
|
586
|
+
if (userExited) {
|
|
587
|
+
process.exit(0);
|
|
588
|
+
}
|
|
589
|
+
console.log(
|
|
590
|
+
'\n\nUse "exit" to quit or press Ctrl+C again to force exit.'
|
|
591
|
+
);
|
|
592
|
+
userExited = true;
|
|
593
|
+
rl.prompt();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Helper function to process a single query
|
|
597
|
+
const processQuery = async (query: string): Promise<boolean> => {
|
|
598
|
+
if (query === "exit" || query === "quit") {
|
|
599
|
+
return true; // Signal to exit
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (query === "help") {
|
|
603
|
+
console.log("\n📖 Interactive Mode Commands:");
|
|
604
|
+
console.log(" help - Show this help message");
|
|
605
|
+
console.log(" exit, quit - Exit interactive mode");
|
|
606
|
+
console.log(
|
|
607
|
+
" px-fetch-more - Fetch additional data (e.g., px-fetch-more spans --project <name> --limit <n>)"
|
|
608
|
+
);
|
|
609
|
+
console.log("\n💡 Usage Tips:");
|
|
610
|
+
console.log(
|
|
611
|
+
" • Ask natural language questions about your Phoenix data"
|
|
612
|
+
);
|
|
613
|
+
console.log(
|
|
614
|
+
" • The agent has access to bash commands to analyze the data"
|
|
615
|
+
);
|
|
616
|
+
console.log(
|
|
617
|
+
" • Use px-fetch-more commands to get additional data on-demand"
|
|
618
|
+
);
|
|
619
|
+
console.log("\n🔧 Options (set when starting phoenix-insight):");
|
|
620
|
+
console.log(
|
|
621
|
+
" --local - Use local mode with persistent storage"
|
|
622
|
+
);
|
|
623
|
+
console.log(
|
|
624
|
+
" --stream - Stream agent responses in real-time"
|
|
625
|
+
);
|
|
626
|
+
console.log(" --refresh - Force fresh snapshot data");
|
|
627
|
+
console.log(" --limit <n> - Set max spans per project");
|
|
628
|
+
console.log(" --trace - Enable observability tracing");
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (query === "") {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const agentProgress = new AgentProgress(!config.stream);
|
|
638
|
+
agentProgress.startThinking();
|
|
639
|
+
|
|
640
|
+
if (config.stream) {
|
|
641
|
+
// Stream mode
|
|
642
|
+
const result = await agent.stream(query, {
|
|
643
|
+
onStepFinish: (step: any) => {
|
|
644
|
+
// Show tool usage even in stream mode
|
|
645
|
+
if (step.toolCalls?.length) {
|
|
646
|
+
step.toolCalls.forEach((toolCall: any) => {
|
|
647
|
+
const toolName = toolCall.toolName;
|
|
648
|
+
if (toolName === "bash") {
|
|
649
|
+
// Extract bash command for better visibility
|
|
650
|
+
const command = toolCall.args?.command || "";
|
|
651
|
+
const shortCmd = command.split("\n")[0].substring(0, 50);
|
|
652
|
+
agentProgress.updateTool(
|
|
653
|
+
toolName,
|
|
654
|
+
shortCmd + (command.length > 50 ? "..." : "")
|
|
655
|
+
);
|
|
656
|
+
} else {
|
|
657
|
+
agentProgress.updateTool(toolName);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Show tool results
|
|
663
|
+
if (step.toolResults?.length) {
|
|
664
|
+
step.toolResults.forEach((toolResult: any) => {
|
|
665
|
+
agentProgress.updateToolResult(
|
|
666
|
+
toolResult.toolName,
|
|
667
|
+
!toolResult.isError
|
|
668
|
+
);
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Stop progress before streaming
|
|
675
|
+
agentProgress.stop();
|
|
676
|
+
|
|
677
|
+
// Handle streaming response
|
|
678
|
+
console.log("\n✨ Answer:\n");
|
|
679
|
+
for await (const chunk of result.textStream) {
|
|
680
|
+
process.stdout.write(chunk);
|
|
681
|
+
}
|
|
682
|
+
console.log(); // Final newline
|
|
683
|
+
|
|
684
|
+
// Wait for full response to complete
|
|
685
|
+
await result.response;
|
|
686
|
+
} else {
|
|
687
|
+
// Non-streaming mode
|
|
688
|
+
const result = await agent.generate(query, {
|
|
689
|
+
onStepFinish: (step: any) => {
|
|
690
|
+
// Show tool usage
|
|
691
|
+
if (step.toolCalls?.length) {
|
|
692
|
+
step.toolCalls.forEach((toolCall: any) => {
|
|
693
|
+
const toolName = toolCall.toolName;
|
|
694
|
+
if (toolName === "bash") {
|
|
695
|
+
// Extract bash command for better visibility
|
|
696
|
+
const command = toolCall.args?.command || "";
|
|
697
|
+
const shortCmd = command.split("\n")[0].substring(0, 50);
|
|
698
|
+
agentProgress.updateTool(
|
|
699
|
+
toolName,
|
|
700
|
+
shortCmd + (command.length > 50 ? "..." : "")
|
|
701
|
+
);
|
|
702
|
+
} else {
|
|
703
|
+
agentProgress.updateTool(toolName);
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Show tool results
|
|
709
|
+
if (step.toolResults?.length) {
|
|
710
|
+
step.toolResults.forEach((toolResult: any) => {
|
|
711
|
+
agentProgress.updateToolResult(
|
|
712
|
+
toolResult.toolName,
|
|
713
|
+
!toolResult.isError
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
// Stop progress and display the final answer
|
|
721
|
+
agentProgress.succeed();
|
|
722
|
+
console.log("\n✨ Answer:\n");
|
|
723
|
+
console.log(result.text);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
console.log("\n" + "─".repeat(50) + "\n");
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error("\n❌ Query Error:");
|
|
729
|
+
if (error instanceof PhoenixClientError) {
|
|
730
|
+
console.error(` ${error.message}`);
|
|
731
|
+
} else if (error instanceof Error) {
|
|
732
|
+
console.error(` ${error.message}`);
|
|
733
|
+
} else {
|
|
734
|
+
console.error(` ${String(error)}`);
|
|
735
|
+
}
|
|
736
|
+
console.error(" You can try again with a different query\n");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
return false;
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
// Use event-based approach instead of async iterator to prevent
|
|
743
|
+
// premature exit when ora/spinners interact with stdin
|
|
744
|
+
await new Promise<void>((resolve) => {
|
|
745
|
+
rl.on("line", async (line) => {
|
|
746
|
+
const query = line.trim();
|
|
747
|
+
|
|
748
|
+
// Pause readline while processing to prevent queuing
|
|
749
|
+
rl.pause();
|
|
750
|
+
|
|
751
|
+
const shouldExit = await processQuery(query);
|
|
752
|
+
|
|
753
|
+
if (shouldExit) {
|
|
754
|
+
rl.close();
|
|
755
|
+
} else {
|
|
756
|
+
// Resume and show prompt for next input
|
|
757
|
+
rl.resume();
|
|
758
|
+
rl.prompt();
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
rl.on("close", () => {
|
|
763
|
+
resolve();
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
// Show initial prompt
|
|
767
|
+
rl.prompt();
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
console.log("\n👋 Goodbye!");
|
|
771
|
+
|
|
772
|
+
// Cleanup
|
|
773
|
+
await mode.cleanup();
|
|
774
|
+
|
|
775
|
+
// Shutdown observability if enabled
|
|
776
|
+
await shutdownObservability();
|
|
777
|
+
} catch (error) {
|
|
778
|
+
handleError(error, "setting up interactive mode");
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
program.parse();
|