@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.
Files changed (54) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +620 -0
  3. package/dist/agent/index.js +230 -0
  4. package/dist/cli.js +640 -0
  5. package/dist/commands/index.js +2 -0
  6. package/dist/commands/px-fetch-more-spans.js +98 -0
  7. package/dist/commands/px-fetch-more-trace.js +110 -0
  8. package/dist/config/index.js +165 -0
  9. package/dist/config/loader.js +141 -0
  10. package/dist/config/schema.js +53 -0
  11. package/dist/index.js +1 -0
  12. package/dist/modes/index.js +17 -0
  13. package/dist/modes/local.js +134 -0
  14. package/dist/modes/sandbox.js +121 -0
  15. package/dist/modes/types.js +1 -0
  16. package/dist/observability/index.js +65 -0
  17. package/dist/progress.js +209 -0
  18. package/dist/prompts/index.js +1 -0
  19. package/dist/prompts/system.js +30 -0
  20. package/dist/snapshot/client.js +74 -0
  21. package/dist/snapshot/context.js +332 -0
  22. package/dist/snapshot/datasets.js +68 -0
  23. package/dist/snapshot/experiments.js +135 -0
  24. package/dist/snapshot/index.js +262 -0
  25. package/dist/snapshot/projects.js +44 -0
  26. package/dist/snapshot/prompts.js +199 -0
  27. package/dist/snapshot/spans.js +80 -0
  28. package/dist/tsconfig.esm.tsbuildinfo +1 -0
  29. package/package.json +75 -0
  30. package/src/agent/index.ts +323 -0
  31. package/src/cli.ts +782 -0
  32. package/src/commands/index.ts +8 -0
  33. package/src/commands/px-fetch-more-spans.ts +174 -0
  34. package/src/commands/px-fetch-more-trace.ts +183 -0
  35. package/src/config/index.ts +225 -0
  36. package/src/config/loader.ts +173 -0
  37. package/src/config/schema.ts +66 -0
  38. package/src/index.ts +1 -0
  39. package/src/modes/index.ts +21 -0
  40. package/src/modes/local.ts +163 -0
  41. package/src/modes/sandbox.ts +144 -0
  42. package/src/modes/types.ts +31 -0
  43. package/src/observability/index.ts +90 -0
  44. package/src/progress.ts +239 -0
  45. package/src/prompts/index.ts +1 -0
  46. package/src/prompts/system.ts +31 -0
  47. package/src/snapshot/client.ts +129 -0
  48. package/src/snapshot/context.ts +462 -0
  49. package/src/snapshot/datasets.ts +132 -0
  50. package/src/snapshot/experiments.ts +246 -0
  51. package/src/snapshot/index.ts +403 -0
  52. package/src/snapshot/projects.ts +58 -0
  53. package/src/snapshot/prompts.ts +267 -0
  54. package/src/snapshot/spans.ts +142 -0
@@ -0,0 +1,8 @@
1
+ export {
2
+ fetchMoreSpans,
3
+ type FetchMoreSpansOptions,
4
+ } from "./px-fetch-more-spans.js";
5
+ export {
6
+ fetchMoreTrace,
7
+ type FetchMoreTraceOptions,
8
+ } from "./px-fetch-more-trace.js";
@@ -0,0 +1,174 @@
1
+ import type { PhoenixClient } from "@arizeai/phoenix-client";
2
+ import type { ExecutionMode } from "../modes/types.js";
3
+ import { withErrorHandling } from "../snapshot/client.js";
4
+
5
+ export interface FetchMoreSpansOptions {
6
+ /** Project name to fetch spans for */
7
+ project: string;
8
+ /** Number of additional spans to fetch */
9
+ limit: number;
10
+ /** Inclusive lower bound time for filtering spans */
11
+ startTime?: Date | string | null;
12
+ /** Exclusive upper bound time for filtering spans */
13
+ endTime?: Date | string | null;
14
+ }
15
+
16
+ interface SpanData {
17
+ id: string;
18
+ name: string;
19
+ context: {
20
+ trace_id: string;
21
+ span_id: string;
22
+ };
23
+ span_kind: string;
24
+ parent_id: string | null;
25
+ start_time: string;
26
+ end_time: string;
27
+ status_code: string;
28
+ status_message: string;
29
+ attributes: Record<string, unknown>;
30
+ events: Array<unknown>;
31
+ }
32
+
33
+ interface SpansMetadata {
34
+ project: string;
35
+ spanCount: number;
36
+ startTime: string | null;
37
+ endTime: string | null;
38
+ snapshotTime: string;
39
+ lastCursor?: string | null;
40
+ }
41
+
42
+ /**
43
+ * Fetches additional spans for a specific project on-demand
44
+ *
45
+ * @param client - Phoenix client instance
46
+ * @param mode - Execution mode for file operations
47
+ * @param options - Options for fetching spans
48
+ */
49
+ export async function fetchMoreSpans(
50
+ client: PhoenixClient,
51
+ mode: ExecutionMode,
52
+ options: FetchMoreSpansOptions
53
+ ): Promise<void> {
54
+ const { project, limit, startTime, endTime } = options;
55
+
56
+ await withErrorHandling(async () => {
57
+ // Try to read existing metadata to get the last cursor
58
+ let existingMetadata: SpansMetadata | null = null;
59
+ let existingSpans: SpanData[] = [];
60
+
61
+ try {
62
+ const metadataResult = await mode.exec(
63
+ `cat /phoenix/projects/${project}/spans/metadata.json`
64
+ );
65
+ if (metadataResult.exitCode === 0 && metadataResult.stdout) {
66
+ existingMetadata = JSON.parse(metadataResult.stdout);
67
+ }
68
+ } catch (error) {
69
+ // Metadata doesn't exist, that's okay
70
+ }
71
+
72
+ // Try to read existing spans
73
+ try {
74
+ const spansResult = await mode.exec(
75
+ `cat /phoenix/projects/${project}/spans/index.jsonl`
76
+ );
77
+ if (spansResult.exitCode === 0 && spansResult.stdout) {
78
+ existingSpans = spansResult.stdout
79
+ .trim()
80
+ .split("\n")
81
+ .filter((line) => line.length > 0)
82
+ .map((line) => JSON.parse(line) as SpanData);
83
+ }
84
+ } catch (error) {
85
+ // Spans file doesn't exist, that's okay
86
+ }
87
+
88
+ // Fetch new spans
89
+ const newSpans: SpanData[] = [];
90
+ let cursor: string | null = existingMetadata?.lastCursor ?? null;
91
+ let totalFetched = 0;
92
+
93
+ while (totalFetched < limit) {
94
+ const query: Record<string, any> = {
95
+ limit: Math.min(100, limit - totalFetched), // Fetch in chunks of 100
96
+ };
97
+
98
+ if (cursor) {
99
+ query.cursor = cursor;
100
+ }
101
+
102
+ if (startTime) {
103
+ query.start_time =
104
+ startTime instanceof Date ? startTime.toISOString() : startTime;
105
+ }
106
+
107
+ if (endTime) {
108
+ query.end_time =
109
+ endTime instanceof Date ? endTime.toISOString() : endTime;
110
+ }
111
+
112
+ const response = await client.GET(
113
+ "/v1/projects/{project_identifier}/spans",
114
+ {
115
+ params: {
116
+ path: {
117
+ project_identifier: project,
118
+ },
119
+ query,
120
+ },
121
+ }
122
+ );
123
+
124
+ if (response.error) throw response.error;
125
+
126
+ const data = response.data?.data ?? [];
127
+ newSpans.push(...(data as SpanData[]));
128
+ totalFetched += data.length;
129
+
130
+ cursor = response.data?.next_cursor ?? null;
131
+
132
+ // Stop if there's no more data
133
+ if (!cursor || data.length === 0) {
134
+ break;
135
+ }
136
+ }
137
+
138
+ // Combine existing and new spans
139
+ const allSpans = [...existingSpans, ...newSpans];
140
+
141
+ // Write updated spans to JSONL file
142
+ const jsonlContent = allSpans
143
+ .map((span) => JSON.stringify(span))
144
+ .join("\n");
145
+ await mode.writeFile(
146
+ `/phoenix/projects/${project}/spans/index.jsonl`,
147
+ jsonlContent
148
+ );
149
+
150
+ // Update metadata
151
+ const metadata: SpansMetadata = {
152
+ project,
153
+ spanCount: allSpans.length,
154
+ startTime:
155
+ startTime instanceof Date
156
+ ? startTime.toISOString()
157
+ : (startTime ?? null),
158
+ endTime:
159
+ endTime instanceof Date ? endTime.toISOString() : (endTime ?? null),
160
+ snapshotTime: new Date().toISOString(),
161
+ lastCursor: cursor,
162
+ };
163
+
164
+ await mode.writeFile(
165
+ `/phoenix/projects/${project}/spans/metadata.json`,
166
+ JSON.stringify(metadata, null, 2)
167
+ );
168
+
169
+ console.log(
170
+ `Fetched ${newSpans.length} additional spans for project "${project}"`
171
+ );
172
+ console.log(`Total spans for project: ${allSpans.length}`);
173
+ }, `fetching more spans for project ${project}`);
174
+ }
@@ -0,0 +1,183 @@
1
+ import type { PhoenixClient } from "@arizeai/phoenix-client";
2
+ import type { ExecutionMode } from "../modes/types.js";
3
+ import { withErrorHandling } from "../snapshot/client.js";
4
+
5
+ export interface FetchMoreTraceOptions {
6
+ /** Trace ID to fetch all spans for */
7
+ traceId: string;
8
+ /** Project name to search in */
9
+ project: string;
10
+ }
11
+
12
+ interface SpanData {
13
+ id: string;
14
+ name: string;
15
+ context: {
16
+ trace_id: string;
17
+ span_id: string;
18
+ };
19
+ span_kind: string;
20
+ parent_id: string | null;
21
+ start_time: string;
22
+ end_time: string;
23
+ status_code: string;
24
+ status_message: string;
25
+ attributes: Record<string, unknown>;
26
+ events: Array<unknown>;
27
+ }
28
+
29
+ /**
30
+ * Fetches all spans for a specific trace ID
31
+ *
32
+ * @param client - Phoenix client instance
33
+ * @param mode - Execution mode for file operations
34
+ * @param options - Options for fetching trace
35
+ */
36
+ export async function fetchMoreTrace(
37
+ client: PhoenixClient,
38
+ mode: ExecutionMode,
39
+ options: FetchMoreTraceOptions
40
+ ): Promise<void> {
41
+ const { traceId, project } = options;
42
+
43
+ await withErrorHandling(async () => {
44
+ // First, check if the project exists in our snapshot
45
+ const projectsResult = await mode.exec("cat /phoenix/projects/index.jsonl");
46
+ if (projectsResult.exitCode !== 0 || !projectsResult.stdout) {
47
+ console.error("No projects found in snapshot. Run a snapshot first.");
48
+ return;
49
+ }
50
+
51
+ const projectNames = projectsResult.stdout
52
+ .trim()
53
+ .split("\n")
54
+ .filter((line) => line.length > 0)
55
+ .map((line) => JSON.parse(line).name);
56
+
57
+ if (!projectNames.includes(project)) {
58
+ console.error(
59
+ `Project "${project}" not found. Available projects: ${projectNames.join(
60
+ ", "
61
+ )}`
62
+ );
63
+ return;
64
+ }
65
+
66
+ // Fetch all spans that belong to this trace
67
+ const traceSpans: SpanData[] = [];
68
+ let cursor: string | null = null;
69
+ let totalFetched = 0;
70
+
71
+ console.log(`Fetching trace ${traceId} from project "${project}"...`);
72
+
73
+ // We need to fetch spans in batches and filter by trace_id
74
+ // Since the API doesn't support direct trace_id filtering
75
+ while (true) {
76
+ const query: Record<string, any> = {
77
+ limit: 100, // Fetch in chunks
78
+ };
79
+
80
+ if (cursor) {
81
+ query.cursor = cursor;
82
+ }
83
+
84
+ const response = await client.GET(
85
+ "/v1/projects/{project_identifier}/spans",
86
+ {
87
+ params: {
88
+ path: {
89
+ project_identifier: project,
90
+ },
91
+ query,
92
+ },
93
+ }
94
+ );
95
+
96
+ if (response.error) throw response.error;
97
+
98
+ const data = response.data?.data ?? [];
99
+ totalFetched += data.length;
100
+
101
+ // Filter spans that belong to our trace
102
+ const matchingSpans = (data as SpanData[]).filter(
103
+ (span) => span.context.trace_id === traceId
104
+ );
105
+ traceSpans.push(...matchingSpans);
106
+
107
+ cursor = response.data?.next_cursor ?? null;
108
+
109
+ // Stop if we found spans for the trace or no more data
110
+ if (traceSpans.length > 0 || !cursor || data.length === 0) {
111
+ // If we found some spans, continue until we have all spans from the trace
112
+ // This is because trace spans might be spread across multiple pages
113
+ if (traceSpans.length > 0 && cursor && data.length > 0) {
114
+ console.log(
115
+ `Found ${traceSpans.length} spans so far, continuing search...`
116
+ );
117
+ continue;
118
+ }
119
+ break;
120
+ }
121
+
122
+ // Show progress for large datasets
123
+ if (totalFetched % 1000 === 0) {
124
+ console.log(`Searched ${totalFetched} spans so far...`);
125
+ }
126
+ }
127
+
128
+ if (traceSpans.length === 0) {
129
+ console.log(
130
+ `No spans found for trace ${traceId} in project "${project}"`
131
+ );
132
+ console.log(
133
+ `Searched through ${totalFetched} spans. The trace might not exist or might be in a different project.`
134
+ );
135
+ return;
136
+ }
137
+
138
+ // Sort spans by start_time to show them in order
139
+ traceSpans.sort(
140
+ (a, b) =>
141
+ new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
142
+ );
143
+
144
+ // Write trace spans to a dedicated file
145
+ const jsonlContent = traceSpans
146
+ .map((span) => JSON.stringify(span))
147
+ .join("\n");
148
+
149
+ const traceDir = `/phoenix/traces/${traceId}`;
150
+ await mode.writeFile(`${traceDir}/spans.jsonl`, jsonlContent);
151
+
152
+ // Create trace metadata
153
+ const rootSpan = traceSpans.find((span) => !span.parent_id);
154
+ const firstSpan = traceSpans[0];
155
+ const lastSpan = traceSpans[traceSpans.length - 1];
156
+ const metadata = {
157
+ traceId,
158
+ project,
159
+ spanCount: traceSpans.length,
160
+ rootSpan: rootSpan ? { id: rootSpan.id, name: rootSpan.name } : null,
161
+ startTime: firstSpan?.start_time || null,
162
+ endTime: lastSpan?.end_time || null,
163
+ duration:
164
+ firstSpan && lastSpan
165
+ ? new Date(lastSpan.end_time).getTime() -
166
+ new Date(firstSpan.start_time).getTime()
167
+ : 0,
168
+ snapshotTime: new Date().toISOString(),
169
+ };
170
+
171
+ await mode.writeFile(
172
+ `${traceDir}/metadata.json`,
173
+ JSON.stringify(metadata, null, 2)
174
+ );
175
+
176
+ console.log(`\nSuccessfully fetched trace ${traceId}:`);
177
+ console.log(`- Project: ${project}`);
178
+ console.log(`- Spans: ${traceSpans.length}`);
179
+ console.log(`- Root span: ${rootSpan?.name || "Unknown"}`);
180
+ console.log(`- Duration: ${(metadata.duration / 1000).toFixed(2)} seconds`);
181
+ console.log(`\nTrace data saved to: ${traceDir}/`);
182
+ }, `fetching trace ${traceId}`);
183
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Config singleton module for Phoenix Insight CLI
3
+ *
4
+ * Provides centralized configuration management with priority-based merging:
5
+ * 1. Config file (lowest priority)
6
+ * 2. Environment variables
7
+ * 3. CLI arguments (highest priority)
8
+ */
9
+
10
+ import { configSchema, type Config, getDefaultConfig } from "./schema.js";
11
+ import {
12
+ getConfigPath,
13
+ loadConfigFile,
14
+ validateConfig,
15
+ createDefaultConfig,
16
+ setCliConfigPath,
17
+ } from "./loader.js";
18
+
19
+ // Re-export Config type for convenience
20
+ export type { Config } from "./schema.js";
21
+
22
+ /**
23
+ * Module-level storage for the initialized config singleton
24
+ */
25
+ let configInstance: Config | null = null;
26
+
27
+ /**
28
+ * CLI arguments that can override config values
29
+ */
30
+ export interface CliArgs {
31
+ /** Custom config file path */
32
+ config?: string;
33
+ /** Phoenix server base URL */
34
+ baseUrl?: string;
35
+ /** Phoenix API key */
36
+ apiKey?: string;
37
+ /** Maximum spans to fetch per project */
38
+ limit?: number;
39
+ /** Enable streaming responses */
40
+ stream?: boolean;
41
+ /** Execution mode: sandbox or local */
42
+ local?: boolean;
43
+ /** Force refresh of snapshot data */
44
+ refresh?: boolean;
45
+ /** Enable tracing */
46
+ trace?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Environment variable mappings to config keys
51
+ */
52
+ const ENV_VAR_MAPPINGS: Record<string, keyof Config> = {
53
+ PHOENIX_BASE_URL: "baseUrl",
54
+ PHOENIX_API_KEY: "apiKey",
55
+ PHOENIX_INSIGHT_LIMIT: "limit",
56
+ PHOENIX_INSIGHT_STREAM: "stream",
57
+ PHOENIX_INSIGHT_MODE: "mode",
58
+ PHOENIX_INSIGHT_REFRESH: "refresh",
59
+ PHOENIX_INSIGHT_TRACE: "trace",
60
+ };
61
+
62
+ /**
63
+ * Parse environment variable value to appropriate type
64
+ */
65
+ function parseEnvValue(
66
+ key: keyof Config,
67
+ value: string
68
+ ): string | number | boolean | undefined {
69
+ switch (key) {
70
+ case "baseUrl":
71
+ case "apiKey":
72
+ return value;
73
+ case "limit":
74
+ const num = parseInt(value, 10);
75
+ return isNaN(num) ? undefined : num;
76
+ case "stream":
77
+ case "refresh":
78
+ case "trace":
79
+ return value.toLowerCase() === "true" || value === "1";
80
+ case "mode":
81
+ if (value === "sandbox" || value === "local") {
82
+ return value;
83
+ }
84
+ return undefined;
85
+ default:
86
+ return value;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Get config values from environment variables
92
+ */
93
+ function getEnvConfig(): Partial<Config> {
94
+ const envConfig: Partial<Config> = {};
95
+
96
+ for (const [envVar, configKey] of Object.entries(ENV_VAR_MAPPINGS)) {
97
+ const value = process.env[envVar];
98
+ if (value !== undefined) {
99
+ const parsed = parseEnvValue(configKey, value);
100
+ if (parsed !== undefined) {
101
+ (envConfig as any)[configKey] = parsed;
102
+ }
103
+ }
104
+ }
105
+
106
+ return envConfig;
107
+ }
108
+
109
+ /**
110
+ * Convert CLI args to config format
111
+ */
112
+ function cliArgsToConfig(cliArgs: CliArgs): Partial<Config> {
113
+ const config: Partial<Config> = {};
114
+
115
+ if (cliArgs.baseUrl !== undefined) {
116
+ config.baseUrl = cliArgs.baseUrl;
117
+ }
118
+ if (cliArgs.apiKey !== undefined) {
119
+ config.apiKey = cliArgs.apiKey;
120
+ }
121
+ if (cliArgs.limit !== undefined) {
122
+ config.limit = cliArgs.limit;
123
+ }
124
+ if (cliArgs.stream !== undefined) {
125
+ config.stream = cliArgs.stream;
126
+ }
127
+ if (cliArgs.local !== undefined) {
128
+ // CLI uses --local flag, config uses mode
129
+ config.mode = cliArgs.local ? "local" : "sandbox";
130
+ }
131
+ if (cliArgs.refresh !== undefined) {
132
+ config.refresh = cliArgs.refresh;
133
+ }
134
+ if (cliArgs.trace !== undefined) {
135
+ config.trace = cliArgs.trace;
136
+ }
137
+
138
+ return config;
139
+ }
140
+
141
+ /**
142
+ * Initialize the configuration singleton
143
+ *
144
+ * Merges configuration from multiple sources with the following priority:
145
+ * 1. Config file (lowest priority)
146
+ * 2. Environment variables
147
+ * 3. CLI arguments (highest priority)
148
+ *
149
+ * @param cliArgs - CLI arguments from Commander
150
+ * @returns The initialized configuration
151
+ */
152
+ export async function initializeConfig(cliArgs: CliArgs = {}): Promise<Config> {
153
+ // Set CLI config path if provided (for getConfigPath to use)
154
+ if (cliArgs.config) {
155
+ setCliConfigPath(cliArgs.config);
156
+ }
157
+
158
+ // Get config file path
159
+ const { path: configPath, isDefault } = getConfigPath();
160
+
161
+ // Try to create default config if it doesn't exist (only for default path)
162
+ if (isDefault) {
163
+ await createDefaultConfig(configPath, isDefault);
164
+ }
165
+
166
+ // Load config file
167
+ const fileConfig = await loadConfigFile(configPath);
168
+
169
+ // Validate file config (returns defaults if null/invalid)
170
+ const validatedFileConfig = validateConfig(fileConfig);
171
+
172
+ // Get environment config
173
+ const envConfig = getEnvConfig();
174
+
175
+ // Get CLI config
176
+ const cliConfig = cliArgsToConfig(cliArgs);
177
+
178
+ // Merge configs: file < env < cli
179
+ const mergedConfig = {
180
+ ...validatedFileConfig,
181
+ ...envConfig,
182
+ ...cliConfig,
183
+ };
184
+
185
+ // Final validation with Zod
186
+ const result = configSchema.safeParse(mergedConfig);
187
+
188
+ if (result.success) {
189
+ configInstance = result.data;
190
+ } else {
191
+ // Log validation issues as warnings
192
+ result.error.issues.forEach((issue) => {
193
+ console.warn(
194
+ `Warning: Config validation error at '${issue.path.join(".")}': ${issue.message}`
195
+ );
196
+ });
197
+ // Fall back to defaults
198
+ configInstance = getDefaultConfig();
199
+ }
200
+
201
+ return configInstance;
202
+ }
203
+
204
+ /**
205
+ * Get the initialized configuration
206
+ *
207
+ * @throws Error if config has not been initialized via initializeConfig()
208
+ * @returns The configuration object
209
+ */
210
+ export function getConfig(): Config {
211
+ if (configInstance === null) {
212
+ throw new Error(
213
+ "Config not initialized. Call initializeConfig() first before using getConfig()."
214
+ );
215
+ }
216
+ return configInstance;
217
+ }
218
+
219
+ /**
220
+ * Reset the config singleton (useful for testing)
221
+ */
222
+ export function resetConfig(): void {
223
+ configInstance = null;
224
+ setCliConfigPath(undefined);
225
+ }