@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,134 @@
|
|
|
1
|
+
import { exec as execCallback } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import { tool } from "ai";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
const execAsync = promisify(execCallback);
|
|
9
|
+
/**
|
|
10
|
+
* Local execution mode using real bash and persistent filesystem
|
|
11
|
+
* - Real bash execution via child_process
|
|
12
|
+
* - Persistent storage in ~/.phoenix-insight/
|
|
13
|
+
* - Full system access
|
|
14
|
+
*/
|
|
15
|
+
export class LocalMode {
|
|
16
|
+
workDir;
|
|
17
|
+
toolCreated = false;
|
|
18
|
+
bashToolPromise = null;
|
|
19
|
+
constructor() {
|
|
20
|
+
// Create a timestamped directory for this snapshot
|
|
21
|
+
// Add a small random component to ensure uniqueness even if created at the same millisecond
|
|
22
|
+
const timestamp = Date.now().toString() + "-" + Math.random().toString(36).substring(7);
|
|
23
|
+
this.workDir = path.join(os.homedir(), ".phoenix-insight", "snapshots", timestamp, "phoenix");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Initialize the working directory
|
|
27
|
+
*/
|
|
28
|
+
async init() {
|
|
29
|
+
try {
|
|
30
|
+
// Create the directory structure if it doesn't exist
|
|
31
|
+
await fs.mkdir(this.workDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new Error(`Failed to initialize local mode directory at ${this.workDir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async writeFile(filePath, content) {
|
|
38
|
+
await this.init();
|
|
39
|
+
// Ensure the path is relative to phoenix root
|
|
40
|
+
const cleanPath = filePath.startsWith("/phoenix")
|
|
41
|
+
? filePath.substring(8) // Remove /phoenix prefix
|
|
42
|
+
: filePath.startsWith("/")
|
|
43
|
+
? filePath.substring(1) // Remove leading slash
|
|
44
|
+
: filePath;
|
|
45
|
+
const fullPath = path.join(this.workDir, cleanPath);
|
|
46
|
+
// Create parent directories if they don't exist
|
|
47
|
+
const dirname = path.dirname(fullPath);
|
|
48
|
+
await fs.mkdir(dirname, { recursive: true });
|
|
49
|
+
// Write the file
|
|
50
|
+
await fs.writeFile(fullPath, content, "utf-8");
|
|
51
|
+
}
|
|
52
|
+
async exec(command) {
|
|
53
|
+
await this.init();
|
|
54
|
+
try {
|
|
55
|
+
// Execute the command in the phoenix directory with a timeout
|
|
56
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
57
|
+
cwd: this.workDir,
|
|
58
|
+
shell: "/bin/bash",
|
|
59
|
+
encoding: "utf-8",
|
|
60
|
+
timeout: 60000, // 60 second timeout for bash commands
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
stdout: stdout || "",
|
|
64
|
+
stderr: stderr || "",
|
|
65
|
+
exitCode: 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
// Handle command execution errors
|
|
70
|
+
if (error.code !== undefined) {
|
|
71
|
+
return {
|
|
72
|
+
stdout: error.stdout || "",
|
|
73
|
+
stderr: error.stderr || error.message || "Command failed",
|
|
74
|
+
exitCode: error.code || 1,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Handle other errors
|
|
78
|
+
return {
|
|
79
|
+
stdout: "",
|
|
80
|
+
stderr: error.message || "Unknown error",
|
|
81
|
+
exitCode: 1,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async getBashTool() {
|
|
86
|
+
// Only create the tool once and cache it
|
|
87
|
+
if (!this.bashToolPromise) {
|
|
88
|
+
this.bashToolPromise = this.createBashTool();
|
|
89
|
+
}
|
|
90
|
+
return this.bashToolPromise;
|
|
91
|
+
}
|
|
92
|
+
async createBashTool() {
|
|
93
|
+
// We can't use bash-tool directly for local mode since it's designed for just-bash
|
|
94
|
+
// Instead, we'll create a tool using the AI SDK's tool function
|
|
95
|
+
// This ensures compatibility with the AI SDK
|
|
96
|
+
// Return a bash tool that executes real bash commands
|
|
97
|
+
return tool({
|
|
98
|
+
description: "Execute bash commands in the local filesystem",
|
|
99
|
+
inputSchema: z.object({
|
|
100
|
+
command: z.string().describe("The bash command to execute"),
|
|
101
|
+
}),
|
|
102
|
+
execute: async ({ command }) => {
|
|
103
|
+
const result = await this.exec(command);
|
|
104
|
+
// Return result in a format similar to bash-tool
|
|
105
|
+
if (result.exitCode !== 0) {
|
|
106
|
+
// Include error details in the response
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
stdout: result.stdout,
|
|
110
|
+
stderr: result.stderr,
|
|
111
|
+
exitCode: result.exitCode,
|
|
112
|
+
error: `Command failed with exit code ${result.exitCode}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
stdout: result.stdout,
|
|
118
|
+
stderr: result.stderr,
|
|
119
|
+
exitCode: result.exitCode,
|
|
120
|
+
};
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async cleanup() {
|
|
125
|
+
// Optional: Clean up old snapshots
|
|
126
|
+
// For now, we'll keep all snapshots for user reference
|
|
127
|
+
// Users can manually clean ~/.phoenix-insight/ if needed
|
|
128
|
+
// We could implement logic to:
|
|
129
|
+
// 1. Keep only the last N snapshots
|
|
130
|
+
// 2. Delete snapshots older than X days
|
|
131
|
+
// 3. Provide a separate cleanup command
|
|
132
|
+
// For this implementation, we do nothing
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { tool } from "ai";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/**
|
|
4
|
+
* Sandbox execution mode using just-bash for isolated execution
|
|
5
|
+
* - In-memory filesystem
|
|
6
|
+
* - Simulated bash commands
|
|
7
|
+
* - No disk or network access
|
|
8
|
+
*/
|
|
9
|
+
export class SandboxMode {
|
|
10
|
+
bash; // Will be typed as Bash from just-bash
|
|
11
|
+
initialized = false;
|
|
12
|
+
bashToolPromise = null;
|
|
13
|
+
constructor() {
|
|
14
|
+
// We'll initialize in the init method since we need async imports
|
|
15
|
+
}
|
|
16
|
+
async init() {
|
|
17
|
+
if (this.initialized)
|
|
18
|
+
return;
|
|
19
|
+
try {
|
|
20
|
+
// Dynamic imports for ESM modules
|
|
21
|
+
const { Bash } = await import("just-bash");
|
|
22
|
+
// Initialize just-bash with /phoenix as the working directory
|
|
23
|
+
this.bash = new Bash({ cwd: "/phoenix" });
|
|
24
|
+
this.initialized = true;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new Error(`Failed to initialize sandbox mode: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async writeFile(path, content) {
|
|
31
|
+
await this.init();
|
|
32
|
+
try {
|
|
33
|
+
// Ensure the path starts with /phoenix
|
|
34
|
+
const fullPath = path.startsWith("/phoenix")
|
|
35
|
+
? path
|
|
36
|
+
: `/phoenix${path.startsWith("/") ? "" : "/"}${path}`;
|
|
37
|
+
// Create parent directories if they don't exist
|
|
38
|
+
const dirname = fullPath.substring(0, fullPath.lastIndexOf("/"));
|
|
39
|
+
if (dirname) {
|
|
40
|
+
await this.bash.exec(`mkdir -p ${dirname}`);
|
|
41
|
+
}
|
|
42
|
+
// Write the file using just-bash's filesystem
|
|
43
|
+
// We'll use the InMemoryFs directly for better performance
|
|
44
|
+
this.bash.fs.writeFileSync(fullPath, content);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new Error(`Failed to write file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async exec(command) {
|
|
51
|
+
await this.init();
|
|
52
|
+
try {
|
|
53
|
+
const result = await this.bash.exec(command);
|
|
54
|
+
// just-bash returns a different structure, so we need to normalize it
|
|
55
|
+
return {
|
|
56
|
+
stdout: result.stdout || "",
|
|
57
|
+
stderr: result.stderr || "",
|
|
58
|
+
exitCode: result.exitCode || 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
// If the command fails, just-bash throws an error
|
|
63
|
+
// Extract what we can from the error
|
|
64
|
+
if (error && typeof error === "object" && "exitCode" in error) {
|
|
65
|
+
return {
|
|
66
|
+
stdout: error.stdout || "",
|
|
67
|
+
stderr: error.stderr || error.toString(),
|
|
68
|
+
exitCode: error.exitCode || 1,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// Fallback for unexpected errors
|
|
72
|
+
return {
|
|
73
|
+
stdout: "",
|
|
74
|
+
stderr: error?.toString() || "Unknown error",
|
|
75
|
+
exitCode: 1,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async getBashTool() {
|
|
80
|
+
// Only create the tool once and cache it
|
|
81
|
+
if (!this.bashToolPromise) {
|
|
82
|
+
this.bashToolPromise = this.createBashTool();
|
|
83
|
+
}
|
|
84
|
+
return this.bashToolPromise;
|
|
85
|
+
}
|
|
86
|
+
async createBashTool() {
|
|
87
|
+
await this.init();
|
|
88
|
+
// Create a bash tool compatible with the AI SDK
|
|
89
|
+
// Similar to local mode, we'll create it directly using the tool function
|
|
90
|
+
return tool({
|
|
91
|
+
description: "Execute bash commands in the sandbox filesystem",
|
|
92
|
+
inputSchema: z.object({
|
|
93
|
+
command: z.string().describe("The bash command to execute"),
|
|
94
|
+
}),
|
|
95
|
+
execute: async ({ command }) => {
|
|
96
|
+
const result = await this.exec(command);
|
|
97
|
+
// Return result in a format similar to bash-tool
|
|
98
|
+
if (result.exitCode !== 0) {
|
|
99
|
+
// Include error details in the response
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
stdout: result.stdout,
|
|
103
|
+
stderr: result.stderr,
|
|
104
|
+
exitCode: result.exitCode,
|
|
105
|
+
error: `Command failed with exit code ${result.exitCode}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
stdout: result.stdout,
|
|
111
|
+
stderr: result.stderr,
|
|
112
|
+
exitCode: result.exitCode,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async cleanup() {
|
|
118
|
+
// No-op for in-memory mode - garbage collection will handle cleanup
|
|
119
|
+
// We could optionally clear the filesystem here if needed
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phoenix Insight observability configuration
|
|
3
|
+
*/
|
|
4
|
+
import { register } from "@arizeai/phoenix-otel";
|
|
5
|
+
import { DiagLogLevel } from "@opentelemetry/api";
|
|
6
|
+
/**
|
|
7
|
+
* Global tracer provider instance
|
|
8
|
+
*/
|
|
9
|
+
let tracerProvider = null;
|
|
10
|
+
/**
|
|
11
|
+
* Check if observability is enabled
|
|
12
|
+
*/
|
|
13
|
+
export function isObservabilityEnabled() {
|
|
14
|
+
return tracerProvider !== null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Initialize observability for the Phoenix Insight agent
|
|
18
|
+
*/
|
|
19
|
+
export function initializeObservability(options) {
|
|
20
|
+
if (!options.enabled) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// If already initialized, skip
|
|
24
|
+
if (tracerProvider) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
// Configure the tracer provider
|
|
29
|
+
tracerProvider = register({
|
|
30
|
+
projectName: options.projectName || "phoenix-insight",
|
|
31
|
+
url: options.baseUrl,
|
|
32
|
+
apiKey: options.apiKey,
|
|
33
|
+
batch: true,
|
|
34
|
+
global: true,
|
|
35
|
+
diagLogLevel: options.debug ? DiagLogLevel.DEBUG : undefined,
|
|
36
|
+
});
|
|
37
|
+
if (options.debug) {
|
|
38
|
+
console.error("🔭 Observability enabled - traces will be sent to Phoenix");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error("⚠️ Failed to initialize observability:", error);
|
|
43
|
+
// Don't throw - observability should not break the main functionality
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Shutdown observability and cleanup resources
|
|
48
|
+
*/
|
|
49
|
+
export async function shutdownObservability() {
|
|
50
|
+
if (tracerProvider) {
|
|
51
|
+
try {
|
|
52
|
+
await tracerProvider.shutdown();
|
|
53
|
+
tracerProvider = null;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error("⚠️ Error shutting down observability:", error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the current tracer provider
|
|
62
|
+
*/
|
|
63
|
+
export function getTracerProvider() {
|
|
64
|
+
return tracerProvider;
|
|
65
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress indicators for Phoenix Insight CLI
|
|
3
|
+
*/
|
|
4
|
+
import ora, {} from "ora";
|
|
5
|
+
/**
|
|
6
|
+
* Progress indicator for snapshot operations
|
|
7
|
+
*/
|
|
8
|
+
export class SnapshotProgress {
|
|
9
|
+
spinner = null;
|
|
10
|
+
enabled;
|
|
11
|
+
currentPhase = null;
|
|
12
|
+
totalSteps = 6;
|
|
13
|
+
currentStep = 0;
|
|
14
|
+
constructor(enabled = true) {
|
|
15
|
+
this.enabled = enabled;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Start the progress indicator
|
|
19
|
+
*/
|
|
20
|
+
start(message = "Creating Phoenix data snapshot") {
|
|
21
|
+
if (!this.enabled)
|
|
22
|
+
return;
|
|
23
|
+
this.currentStep = 0;
|
|
24
|
+
this.spinner = ora({
|
|
25
|
+
text: message,
|
|
26
|
+
spinner: "dots",
|
|
27
|
+
color: "blue",
|
|
28
|
+
}).start();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Update progress with a new phase
|
|
32
|
+
*/
|
|
33
|
+
update(phase, detail) {
|
|
34
|
+
if (!this.enabled || !this.spinner)
|
|
35
|
+
return;
|
|
36
|
+
this.currentStep++;
|
|
37
|
+
this.currentPhase = phase;
|
|
38
|
+
const progress = Math.round((this.currentStep / this.totalSteps) * 100);
|
|
39
|
+
const progressBar = this.createProgressBar(progress);
|
|
40
|
+
const message = detail
|
|
41
|
+
? `${progressBar} ${phase}: ${detail}`
|
|
42
|
+
: `${progressBar} ${phase}`;
|
|
43
|
+
this.spinner.text = message;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Complete a phase successfully
|
|
47
|
+
*/
|
|
48
|
+
succeed(message) {
|
|
49
|
+
if (!this.enabled || !this.spinner)
|
|
50
|
+
return;
|
|
51
|
+
const finalMessage = message || `✓ ${this.currentPhase || "Snapshot"} complete`;
|
|
52
|
+
this.spinner.succeed(finalMessage);
|
|
53
|
+
this.spinner = null;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fail with an error
|
|
57
|
+
*/
|
|
58
|
+
fail(message) {
|
|
59
|
+
if (!this.enabled || !this.spinner)
|
|
60
|
+
return;
|
|
61
|
+
const finalMessage = message || `✗ ${this.currentPhase || "Snapshot"} failed`;
|
|
62
|
+
this.spinner.fail(finalMessage);
|
|
63
|
+
this.spinner = null;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stop without success/fail status
|
|
67
|
+
*/
|
|
68
|
+
stop() {
|
|
69
|
+
if (!this.enabled || !this.spinner)
|
|
70
|
+
return;
|
|
71
|
+
this.spinner.stop();
|
|
72
|
+
this.spinner = null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create a progress bar string
|
|
76
|
+
*/
|
|
77
|
+
createProgressBar(percentage) {
|
|
78
|
+
const width = 20;
|
|
79
|
+
const filled = Math.round((percentage / 100) * width);
|
|
80
|
+
const empty = width - filled;
|
|
81
|
+
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
82
|
+
return `[${bar}] ${percentage}%`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Progress indicator for agent thinking
|
|
87
|
+
*/
|
|
88
|
+
export class AgentProgress {
|
|
89
|
+
spinner = null;
|
|
90
|
+
enabled;
|
|
91
|
+
stepCount = 0;
|
|
92
|
+
currentTool = null;
|
|
93
|
+
constructor(enabled = true) {
|
|
94
|
+
this.enabled = enabled;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Start thinking indicator
|
|
98
|
+
*/
|
|
99
|
+
startThinking() {
|
|
100
|
+
if (!this.enabled)
|
|
101
|
+
return;
|
|
102
|
+
this.stepCount = 0;
|
|
103
|
+
this.currentTool = null;
|
|
104
|
+
this.spinner = ora({
|
|
105
|
+
text: "🤔 Analyzing...",
|
|
106
|
+
spinner: "dots",
|
|
107
|
+
color: "cyan",
|
|
108
|
+
}).start();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Update with current tool usage
|
|
112
|
+
*/
|
|
113
|
+
updateTool(toolName, detail) {
|
|
114
|
+
if (!this.enabled || !this.spinner)
|
|
115
|
+
return;
|
|
116
|
+
this.stepCount++;
|
|
117
|
+
this.currentTool = toolName;
|
|
118
|
+
// Map tool names to more user-friendly descriptions
|
|
119
|
+
const friendlyNames = {
|
|
120
|
+
bash: "Running command",
|
|
121
|
+
px_fetch_more_spans: "Fetching additional spans",
|
|
122
|
+
px_fetch_more_trace: "Fetching trace details",
|
|
123
|
+
};
|
|
124
|
+
const displayName = friendlyNames[toolName] || toolName;
|
|
125
|
+
const message = detail
|
|
126
|
+
? `🔧 ${displayName}: ${detail}`
|
|
127
|
+
: `🔧 ${displayName} (step ${this.stepCount})`;
|
|
128
|
+
this.spinner.text = message;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Update with tool result
|
|
132
|
+
*/
|
|
133
|
+
updateToolResult(toolName, success = true) {
|
|
134
|
+
if (!this.enabled || !this.spinner)
|
|
135
|
+
return;
|
|
136
|
+
const icon = success ? "✓" : "✗";
|
|
137
|
+
const status = success ? "completed" : "failed";
|
|
138
|
+
// More informative messages for each tool
|
|
139
|
+
const toolMessages = {
|
|
140
|
+
bash: "Command executed",
|
|
141
|
+
px_fetch_more_spans: "Additional spans fetched",
|
|
142
|
+
px_fetch_more_trace: "Trace details fetched",
|
|
143
|
+
};
|
|
144
|
+
const baseMessage = toolMessages[toolName] || `Tool ${toolName}`;
|
|
145
|
+
const message = `${icon} ${baseMessage} ${status}`;
|
|
146
|
+
// Brief flash of the result before moving on
|
|
147
|
+
this.spinner.text = message;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Show progress for a specific action
|
|
151
|
+
*/
|
|
152
|
+
updateAction(action) {
|
|
153
|
+
if (!this.enabled || !this.spinner)
|
|
154
|
+
return;
|
|
155
|
+
this.spinner.text = `🔍 ${action}...`;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Stop the thinking indicator
|
|
159
|
+
*/
|
|
160
|
+
stop() {
|
|
161
|
+
if (!this.enabled || !this.spinner)
|
|
162
|
+
return;
|
|
163
|
+
this.spinner.stop();
|
|
164
|
+
this.spinner = null;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Complete with a success message
|
|
168
|
+
*/
|
|
169
|
+
succeed(message = "✨ Analysis complete") {
|
|
170
|
+
if (!this.enabled || !this.spinner)
|
|
171
|
+
return;
|
|
172
|
+
this.spinner.succeed(message);
|
|
173
|
+
this.spinner = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Simple progress logger for when spinners aren't appropriate
|
|
178
|
+
*/
|
|
179
|
+
export class SimpleProgress {
|
|
180
|
+
enabled;
|
|
181
|
+
constructor(enabled = true) {
|
|
182
|
+
this.enabled = enabled;
|
|
183
|
+
}
|
|
184
|
+
log(message) {
|
|
185
|
+
if (!this.enabled)
|
|
186
|
+
return;
|
|
187
|
+
console.log(`[Phoenix Insight] ${message}`);
|
|
188
|
+
}
|
|
189
|
+
info(message) {
|
|
190
|
+
if (!this.enabled)
|
|
191
|
+
return;
|
|
192
|
+
console.log(`ℹ️ ${message}`);
|
|
193
|
+
}
|
|
194
|
+
success(message) {
|
|
195
|
+
if (!this.enabled)
|
|
196
|
+
return;
|
|
197
|
+
console.log(`✅ ${message}`);
|
|
198
|
+
}
|
|
199
|
+
warning(message) {
|
|
200
|
+
if (!this.enabled)
|
|
201
|
+
return;
|
|
202
|
+
console.log(`⚠️ ${message}`);
|
|
203
|
+
}
|
|
204
|
+
error(message) {
|
|
205
|
+
if (!this.enabled)
|
|
206
|
+
return;
|
|
207
|
+
console.log(`❌ ${message}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { INSIGHT_SYSTEM_PROMPT } from "./system.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* System prompt for the Phoenix Insight AI agent
|
|
3
|
+
* This prompt teaches the agent about the filesystem layout and available commands
|
|
4
|
+
*/
|
|
5
|
+
export const INSIGHT_SYSTEM_PROMPT = `You are an expert at analyzing Phoenix observability data.
|
|
6
|
+
|
|
7
|
+
**START by reading /phoenix/_context.md** - it contains a summary of what's available.
|
|
8
|
+
|
|
9
|
+
You have access to a bash shell with Phoenix data organized as files:
|
|
10
|
+
|
|
11
|
+
/phoenix/
|
|
12
|
+
_context.md - READ THIS FIRST: summary of available data
|
|
13
|
+
/projects/{name}/spans/ - Span data (JSONL format, may be sampled)
|
|
14
|
+
/datasets/ - Datasets and examples
|
|
15
|
+
/experiments/ - Experiment runs and results
|
|
16
|
+
/prompts/ - Prompt templates and versions
|
|
17
|
+
|
|
18
|
+
Use commands like:
|
|
19
|
+
- cat, head, tail: Read file contents
|
|
20
|
+
- grep: Search for patterns
|
|
21
|
+
- jq: Query and transform JSON/JSONL
|
|
22
|
+
- ls, find: Navigate and discover data
|
|
23
|
+
- sort, uniq, wc: Aggregate and count
|
|
24
|
+
- awk: Complex text processing
|
|
25
|
+
|
|
26
|
+
If you need MORE data than what's in the snapshot:
|
|
27
|
+
- px-fetch-more spans --project <name> --limit 500
|
|
28
|
+
- px-fetch-more trace --trace-id <id>
|
|
29
|
+
|
|
30
|
+
This is a READ-ONLY snapshot. Start with _context.md, then explore to answer the question.`;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createClient } from "@arizeai/phoenix-client";
|
|
2
|
+
export class PhoenixClientError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
originalError;
|
|
5
|
+
constructor(message, code, originalError) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "PhoenixClientError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.originalError = originalError;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a wrapped Phoenix client with error handling
|
|
14
|
+
*/
|
|
15
|
+
export function createPhoenixClient(config = {}) {
|
|
16
|
+
const headers = {};
|
|
17
|
+
if (config.apiKey) {
|
|
18
|
+
headers["api_key"] = config.apiKey;
|
|
19
|
+
}
|
|
20
|
+
const clientOptions = {
|
|
21
|
+
options: {
|
|
22
|
+
baseUrl: config.baseURL,
|
|
23
|
+
headers: Object.keys(headers).length > 0 ? headers : undefined,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return createClient(clientOptions);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Wraps an async operation with standardized error handling
|
|
30
|
+
*/
|
|
31
|
+
export async function withErrorHandling(operation, context) {
|
|
32
|
+
try {
|
|
33
|
+
return await operation();
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
// Network errors
|
|
37
|
+
if (error instanceof TypeError && error.message.includes("fetch")) {
|
|
38
|
+
throw new PhoenixClientError(`Network error during ${context}: Unable to connect to Phoenix server`, "NETWORK_ERROR", error);
|
|
39
|
+
}
|
|
40
|
+
// HTTP errors from the middleware
|
|
41
|
+
if (error instanceof Error && error.message.includes(": ")) {
|
|
42
|
+
const parts = error.message.split(": ", 2);
|
|
43
|
+
if (parts.length === 2 && parts[1]) {
|
|
44
|
+
const [url, statusInfo] = parts;
|
|
45
|
+
const statusParts = statusInfo.split(" ");
|
|
46
|
+
const statusCode = statusParts[0];
|
|
47
|
+
const statusText = statusParts.slice(1).join(" ");
|
|
48
|
+
if (statusCode === "401" || statusCode === "403") {
|
|
49
|
+
throw new PhoenixClientError(`Authentication error during ${context}: ${statusText}`, "AUTH_ERROR", error);
|
|
50
|
+
}
|
|
51
|
+
if (statusCode && statusCode.startsWith("4")) {
|
|
52
|
+
throw new PhoenixClientError(`Client error during ${context}: ${statusCode} ${statusText}`, "INVALID_RESPONSE", error);
|
|
53
|
+
}
|
|
54
|
+
if (statusCode && statusCode.startsWith("5")) {
|
|
55
|
+
throw new PhoenixClientError(`Server error during ${context}: ${statusCode} ${statusText}`, "NETWORK_ERROR", error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Unknown errors
|
|
60
|
+
throw new PhoenixClientError(`Unexpected error during ${context}: ${error instanceof Error ? error.message : String(error)}`, "UNKNOWN_ERROR", error);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Helper to safely extract data from API responses
|
|
65
|
+
*/
|
|
66
|
+
export function extractData(response) {
|
|
67
|
+
if (response.error) {
|
|
68
|
+
throw response.error;
|
|
69
|
+
}
|
|
70
|
+
if (!response.data) {
|
|
71
|
+
throw new PhoenixClientError("Invalid API response: missing data", "INVALID_RESPONSE");
|
|
72
|
+
}
|
|
73
|
+
return response.data;
|
|
74
|
+
}
|