@opensearch-project/agent-health 0.1.0 → 0.2.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 +108 -72
- package/cli/dist/index.js +1132 -351
- package/dist/assets/index-4BAkkFzo.js +267 -0
- package/dist/assets/index-C3K5cBQr.css +1 -0
- package/dist/index.html +2 -2
- package/dist/opensearch-logo-dark.svg +5 -0
- package/dist/opensearch-logo-light.svg +5 -0
- package/dist/test-first-run-improved.html +469 -0
- package/lib/dist/config/index.js +107 -107
- package/lib/dist/index.js +151 -13
- package/package.json +12 -4
- package/server/dist/app.js +12726 -7693
- package/server/dist/index.js +12728 -7697
- package/dist/assets/index-BheBjDce.css +0 -1
- package/dist/assets/index-aT-TLVpA.js +0 -244
package/cli/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/index.ts
|
|
4
|
-
import { Command as
|
|
5
|
-
import
|
|
4
|
+
import { Command as Command10 } from "commander";
|
|
5
|
+
import chalk10 from "chalk";
|
|
6
6
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
7
|
import { dirname as dirname3, join as join4, resolve as resolve4 } from "path";
|
|
8
8
|
import { readFileSync as readFileSync3, existsSync as existsSync5 } from "fs";
|
|
9
9
|
import { config as loadDotenv } from "dotenv";
|
|
10
10
|
import open from "open";
|
|
11
|
-
import
|
|
11
|
+
import ora5 from "ora";
|
|
12
12
|
|
|
13
13
|
// cli/utils/startServer.ts
|
|
14
14
|
import { fileURLToPath } from "url";
|
|
@@ -49,25 +49,6 @@ import { existsSync as existsSync2 } from "fs";
|
|
|
49
49
|
import { resolve } from "path";
|
|
50
50
|
import { pathToFileURL } from "url";
|
|
51
51
|
|
|
52
|
-
// lib/debug.ts
|
|
53
|
-
var isBrowser = typeof window !== "undefined";
|
|
54
|
-
var serverDebugEnabled = !isBrowser && typeof process !== "undefined" && process.env?.DEBUG === "true";
|
|
55
|
-
function isDebugEnabled() {
|
|
56
|
-
if (isBrowser) {
|
|
57
|
-
try {
|
|
58
|
-
return localStorage.getItem("agenteval_debug") === "true";
|
|
59
|
-
} catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return serverDebugEnabled;
|
|
64
|
-
}
|
|
65
|
-
function debug(module, ...args) {
|
|
66
|
-
if (isDebugEnabled()) {
|
|
67
|
-
console.debug(`[${module}]`, ...args);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
52
|
// lib/config.ts
|
|
72
53
|
var isServerSide = typeof window === "undefined";
|
|
73
54
|
var SERVER_PORT = isServerSide ? process.env?.VITE_BACKEND_PORT || process.env?.PORT || "4001" : "4001";
|
|
@@ -103,10 +84,8 @@ var ENV_CONFIG = {
|
|
|
103
84
|
openSearchLogsPassword: getEnvVar("OPENSEARCH_LOGS_PASSWORD", ""),
|
|
104
85
|
openSearchLogsTracesIndex: getEnvVar("OPENSEARCH_LOGS_TRACES_INDEX", "otel-v1-apm-span-*"),
|
|
105
86
|
openSearchLogsIndex: getEnvVar("OPENSEARCH_LOGS_INDEX", "ml-commons-logs-*"),
|
|
106
|
-
//
|
|
107
|
-
langgraphEndpoint: getEnvVar("LANGGRAPH_ENDPOINT", "http://localhost:3000"),
|
|
87
|
+
// ML-Commons agent endpoint
|
|
108
88
|
mlcommonsEndpoint: getEnvVar("MLCOMMONS_ENDPOINT", "http://localhost:9200/_plugins/_ml/agents/{agent_id}/_execute/stream"),
|
|
109
|
-
holmesGptEndpoint: getEnvVar("HOLMESGPT_ENDPOINT", "http://localhost:5050/api/agui/chat"),
|
|
110
89
|
// ML-Commons agent headers
|
|
111
90
|
mlcommonsHeaderOpenSearchUrl: getEnvVar("MLCOMMONS_HEADER_OPENSEARCH_URL", ""),
|
|
112
91
|
mlcommonsHeaderAuthorization: getEnvVar("MLCOMMONS_HEADER_AUTHORIZATION", ""),
|
|
@@ -115,6 +94,11 @@ var ENV_CONFIG = {
|
|
|
115
94
|
mlcommonsHeaderAwsAccessKeyId: getEnvVar("MLCOMMONS_HEADER_AWS_ACCESS_KEY_ID", ""),
|
|
116
95
|
mlcommonsHeaderAwsSecretAccessKey: getEnvVar("MLCOMMONS_HEADER_AWS_SECRET_ACCESS_KEY", ""),
|
|
117
96
|
mlcommonsHeaderAwsSessionToken: getEnvVar("MLCOMMONS_HEADER_AWS_SESSION_TOKEN", ""),
|
|
97
|
+
// Travel Planner multi-agent endpoint (OTel Demo in Docker)
|
|
98
|
+
travelPlannerEndpoint: getEnvVar("TRAVEL_PLANNER_ENDPOINT", "http://localhost:3000"),
|
|
99
|
+
// OpenAI-compatible (optional - for OpenAI-compatible judge/agent endpoints)
|
|
100
|
+
openaiCompatibleApiKey: getEnvVar("OPENAI_COMPATIBLE_API_KEY", ""),
|
|
101
|
+
openaiCompatibleEndpoint: getEnvVar("OPENAI_COMPATIBLE_ENDPOINT", "http://localhost:4000/v1/chat/completions"),
|
|
118
102
|
// Claude Code Telemetry (optional - for OTEL traces from Claude Code)
|
|
119
103
|
claudeCodeTelemetryEnabled: getEnvVar("CLAUDE_CODE_TELEMETRY_ENABLED", "false") === "true",
|
|
120
104
|
otelExporterEndpoint: getEnvVar("OTEL_EXPORTER_OTLP_ENDPOINT", ""),
|
|
@@ -122,41 +106,47 @@ var ENV_CONFIG = {
|
|
|
122
106
|
otelExporterProtocol: getEnvVar("OTEL_EXPORTER_OTLP_PROTOCOL", ""),
|
|
123
107
|
otelExporterHeaders: getEnvVar("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
124
108
|
};
|
|
125
|
-
function buildMLCommonsHeaders() {
|
|
126
|
-
debug("Config", "Building ML-Commons headers");
|
|
127
|
-
const headers = {};
|
|
128
|
-
if (ENV_CONFIG.mlcommonsHeaderOpenSearchUrl) {
|
|
129
|
-
headers["opensearch-url"] = ENV_CONFIG.mlcommonsHeaderOpenSearchUrl;
|
|
130
|
-
}
|
|
131
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsRegion) {
|
|
132
|
-
headers["aws-region"] = ENV_CONFIG.mlcommonsHeaderAwsRegion;
|
|
133
|
-
}
|
|
134
|
-
if (ENV_CONFIG.mlcommonsHeaderAuthorization) {
|
|
135
|
-
headers["Authorization"] = ENV_CONFIG.mlcommonsHeaderAuthorization;
|
|
136
|
-
} else {
|
|
137
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsServiceName) {
|
|
138
|
-
headers["aws-service-name"] = ENV_CONFIG.mlcommonsHeaderAwsServiceName;
|
|
139
|
-
}
|
|
140
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId) {
|
|
141
|
-
headers["aws-access-key-id"] = ENV_CONFIG.mlcommonsHeaderAwsAccessKeyId;
|
|
142
|
-
}
|
|
143
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey) {
|
|
144
|
-
headers["aws-secret-access-key"] = ENV_CONFIG.mlcommonsHeaderAwsSecretAccessKey;
|
|
145
|
-
}
|
|
146
|
-
if (ENV_CONFIG.mlcommonsHeaderAwsSessionToken) {
|
|
147
|
-
headers["aws-session-token"] = ENV_CONFIG.mlcommonsHeaderAwsSessionToken;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
debug("Config", "ML-Commons headers built, keys:", Object.keys(headers));
|
|
151
|
-
return headers;
|
|
152
|
-
}
|
|
153
109
|
|
|
154
110
|
// lib/constants.ts
|
|
111
|
+
var CONNECTOR_TYPE_INFO = {
|
|
112
|
+
"agui-streaming": {
|
|
113
|
+
label: "AG-UI Streaming",
|
|
114
|
+
description: "AG-UI protocol over SSE. Use for ML-Commons and AG-UI compatible agents.",
|
|
115
|
+
serverOnly: false
|
|
116
|
+
},
|
|
117
|
+
"rest": {
|
|
118
|
+
label: "REST",
|
|
119
|
+
description: "Standard HTTP POST. Agent receives JSON, returns JSON. No streaming.",
|
|
120
|
+
serverOnly: false
|
|
121
|
+
},
|
|
122
|
+
"openai-compatible": {
|
|
123
|
+
label: "OpenAI Compatible",
|
|
124
|
+
description: "OpenAI chat completions format (POST /v1/chat/completions). Works with LiteLLM, Ollama, vLLM.",
|
|
125
|
+
serverOnly: false
|
|
126
|
+
},
|
|
127
|
+
"subprocess": {
|
|
128
|
+
label: "Subprocess",
|
|
129
|
+
description: "Runs a CLI command as a child process. Server-only \u2014 use the CLI or benchmark runner.",
|
|
130
|
+
serverOnly: true
|
|
131
|
+
},
|
|
132
|
+
"claude-code": {
|
|
133
|
+
label: "Claude Code",
|
|
134
|
+
description: "Invokes the Claude Code CLI. Server-only \u2014 use the CLI or benchmark runner.",
|
|
135
|
+
serverOnly: true
|
|
136
|
+
},
|
|
137
|
+
"mock": {
|
|
138
|
+
label: "Mock",
|
|
139
|
+
description: "Built-in demo agent for testing. No real endpoint needed.",
|
|
140
|
+
serverOnly: false
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var VALID_CONNECTOR_TYPES = Object.keys(CONNECTOR_TYPE_INFO);
|
|
144
|
+
var BROWSER_SAFE_CONNECTORS = Object.entries(CONNECTOR_TYPE_INFO).filter(([, info]) => !info.serverOnly).map(([type]) => type);
|
|
155
145
|
function getClaudeCodeConnectorEnv() {
|
|
156
146
|
const env = {
|
|
157
|
-
AWS_PROFILE:
|
|
147
|
+
AWS_PROFILE: ENV_CONFIG.awsProfile || "Bedrock",
|
|
158
148
|
CLAUDE_CODE_USE_BEDROCK: "1",
|
|
159
|
-
AWS_REGION:
|
|
149
|
+
AWS_REGION: ENV_CONFIG.awsRegion || "us-west-2",
|
|
160
150
|
DISABLE_PROMPT_CACHING: "1",
|
|
161
151
|
DISABLE_ERROR_REPORTING: "1"
|
|
162
152
|
};
|
|
@@ -183,70 +173,18 @@ var DEFAULT_CONFIG = {
|
|
|
183
173
|
endpoint: "mock://demo",
|
|
184
174
|
description: "Mock agent for testing (simulated responses)",
|
|
185
175
|
connectorType: "mock",
|
|
186
|
-
models: ["demo-model"],
|
|
187
176
|
headers: {},
|
|
188
177
|
useTraces: false
|
|
189
178
|
},
|
|
190
|
-
{
|
|
191
|
-
key: "langgraph",
|
|
192
|
-
name: "Langgraph",
|
|
193
|
-
endpoint: ENV_CONFIG.langgraphEndpoint,
|
|
194
|
-
description: "Langgraph AG-UI agent server",
|
|
195
|
-
connectorType: "agui-streaming",
|
|
196
|
-
models: [
|
|
197
|
-
"claude-sonnet-4.5",
|
|
198
|
-
"claude-sonnet-4",
|
|
199
|
-
"claude-haiku-3.5"
|
|
200
|
-
],
|
|
201
|
-
headers: {},
|
|
202
|
-
useTraces: true
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
key: "mlcommons-local",
|
|
206
|
-
name: "ML-Commons (Localhost)",
|
|
207
|
-
endpoint: ENV_CONFIG.mlcommonsEndpoint,
|
|
208
|
-
description: "Local OpenSearch ML-Commons conversational agent",
|
|
209
|
-
connectorType: "agui-streaming",
|
|
210
|
-
models: [
|
|
211
|
-
"claude-sonnet-4.5",
|
|
212
|
-
"claude-sonnet-4",
|
|
213
|
-
"claude-haiku-3.5"
|
|
214
|
-
],
|
|
215
|
-
headers: buildMLCommonsHeaders(),
|
|
216
|
-
useTraces: true
|
|
217
|
-
},
|
|
218
|
-
{
|
|
219
|
-
key: "holmesgpt",
|
|
220
|
-
name: "HolmesGPT",
|
|
221
|
-
endpoint: ENV_CONFIG.holmesGptEndpoint,
|
|
222
|
-
description: "HolmesGPT AI-powered RCA agent (AG-UI)",
|
|
223
|
-
connectorType: "agui-streaming",
|
|
224
|
-
models: [
|
|
225
|
-
"claude-sonnet-4.5",
|
|
226
|
-
"claude-sonnet-4",
|
|
227
|
-
"claude-haiku-3.5"
|
|
228
|
-
],
|
|
229
|
-
headers: {},
|
|
230
|
-
useTraces: true
|
|
231
|
-
},
|
|
232
179
|
{
|
|
233
180
|
key: "claude-code",
|
|
234
181
|
name: "Claude Code",
|
|
235
182
|
endpoint: "claude",
|
|
236
|
-
// Command name, not URL
|
|
237
183
|
description: "Claude Code CLI agent (requires claude command installed)",
|
|
238
184
|
connectorType: "claude-code",
|
|
239
|
-
models: ["claude-sonnet-4"],
|
|
240
185
|
headers: {},
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
},
|
|
244
|
-
// connectorConfig env vars are evaluated at runtime by getClaudeCodeConnectorEnv()
|
|
245
|
-
get connectorConfig() {
|
|
246
|
-
return {
|
|
247
|
-
env: getClaudeCodeConnectorEnv()
|
|
248
|
-
};
|
|
249
|
-
}
|
|
186
|
+
useTraces: ENV_CONFIG.claudeCodeTelemetryEnabled && !!ENV_CONFIG.otelExporterEndpoint,
|
|
187
|
+
connectorConfig: { env: getClaudeCodeConnectorEnv() }
|
|
250
188
|
}
|
|
251
189
|
],
|
|
252
190
|
models: {
|
|
@@ -257,6 +195,48 @@ var DEFAULT_CONFIG = {
|
|
|
257
195
|
context_window: 2e5,
|
|
258
196
|
max_output_tokens: 4096
|
|
259
197
|
},
|
|
198
|
+
"claude-opus-4.6": {
|
|
199
|
+
model_id: "us.anthropic.claude-opus-4-6-v1",
|
|
200
|
+
display_name: "Claude Opus 4.6",
|
|
201
|
+
provider: "bedrock",
|
|
202
|
+
context_window: 2e5,
|
|
203
|
+
max_output_tokens: 128e3
|
|
204
|
+
},
|
|
205
|
+
"claude-sonnet-4.6": {
|
|
206
|
+
model_id: "us.anthropic.claude-sonnet-4-6",
|
|
207
|
+
display_name: "Claude Sonnet 4.6",
|
|
208
|
+
provider: "bedrock",
|
|
209
|
+
context_window: 2e5,
|
|
210
|
+
max_output_tokens: 64e3
|
|
211
|
+
},
|
|
212
|
+
"claude-haiku-4.5": {
|
|
213
|
+
model_id: "us.anthropic.claude-haiku-4-5-20251001-v1:0",
|
|
214
|
+
display_name: "Claude Haiku 4.5",
|
|
215
|
+
provider: "bedrock",
|
|
216
|
+
context_window: 2e5,
|
|
217
|
+
max_output_tokens: 64e3
|
|
218
|
+
},
|
|
219
|
+
"claude-opus-4.5": {
|
|
220
|
+
model_id: "us.anthropic.claude-opus-4-5-20251101-v1:0",
|
|
221
|
+
display_name: "Claude Opus 4.5",
|
|
222
|
+
provider: "bedrock",
|
|
223
|
+
context_window: 2e5,
|
|
224
|
+
max_output_tokens: 64e3
|
|
225
|
+
},
|
|
226
|
+
"claude-opus-4.1": {
|
|
227
|
+
model_id: "us.anthropic.claude-opus-4-1-20250805-v1:0",
|
|
228
|
+
display_name: "Claude Opus 4.1",
|
|
229
|
+
provider: "bedrock",
|
|
230
|
+
context_window: 2e5,
|
|
231
|
+
max_output_tokens: 32e3
|
|
232
|
+
},
|
|
233
|
+
"claude-opus-4": {
|
|
234
|
+
model_id: "us.anthropic.claude-opus-4-20250514-v1:0",
|
|
235
|
+
display_name: "Claude Opus 4",
|
|
236
|
+
provider: "bedrock",
|
|
237
|
+
context_window: 2e5,
|
|
238
|
+
max_output_tokens: 32e3
|
|
239
|
+
},
|
|
260
240
|
"claude-sonnet-4.5": {
|
|
261
241
|
model_id: "us.anthropic.claude-sonnet-4-5-20250929-v1:0",
|
|
262
242
|
display_name: "Claude Sonnet 4.5",
|
|
@@ -277,6 +257,27 @@ var DEFAULT_CONFIG = {
|
|
|
277
257
|
provider: "bedrock",
|
|
278
258
|
context_window: 2e5,
|
|
279
259
|
max_output_tokens: 4096
|
|
260
|
+
},
|
|
261
|
+
"gpt-4o": {
|
|
262
|
+
model_id: "gpt-4o",
|
|
263
|
+
display_name: "GPT-4o",
|
|
264
|
+
provider: "openai-compatible",
|
|
265
|
+
context_window: 128e3,
|
|
266
|
+
max_output_tokens: 4096
|
|
267
|
+
},
|
|
268
|
+
"deepseek-r1:8b": {
|
|
269
|
+
model_id: "deepseek-r1:8b",
|
|
270
|
+
display_name: "DeepSeek R1 8B (Ollama)",
|
|
271
|
+
provider: "openai-compatible",
|
|
272
|
+
context_window: 128e3,
|
|
273
|
+
max_output_tokens: 8192
|
|
274
|
+
},
|
|
275
|
+
"gemma3:12b": {
|
|
276
|
+
model_id: "gemma3:12b",
|
|
277
|
+
display_name: "Gemma 3 12B (Ollama)",
|
|
278
|
+
provider: "openai-compatible",
|
|
279
|
+
context_window: 128e3,
|
|
280
|
+
max_output_tokens: 8192
|
|
280
281
|
}
|
|
281
282
|
},
|
|
282
283
|
defaults: {
|
|
@@ -313,7 +314,6 @@ function toAgentConfig(userAgent) {
|
|
|
313
314
|
endpoint: userAgent.endpoint,
|
|
314
315
|
description: userAgent.description,
|
|
315
316
|
enabled: userAgent.enabled ?? true,
|
|
316
|
-
models: userAgent.models,
|
|
317
317
|
headers: userAgent.headers ?? {},
|
|
318
318
|
useTraces: userAgent.useTraces ?? false,
|
|
319
319
|
connectorType: userAgent.connectorType,
|
|
@@ -501,6 +501,44 @@ var ConnectorRegistryImpl = class {
|
|
|
501
501
|
};
|
|
502
502
|
var connectorRegistry = new ConnectorRegistryImpl();
|
|
503
503
|
|
|
504
|
+
// lib/debug.ts
|
|
505
|
+
import fs from "fs";
|
|
506
|
+
import path from "path";
|
|
507
|
+
var isBrowser = typeof window !== "undefined";
|
|
508
|
+
var CONFIG_FILENAME = "agent-health.config.json";
|
|
509
|
+
var serverDebugEnabled = false;
|
|
510
|
+
if (!isBrowser) {
|
|
511
|
+
try {
|
|
512
|
+
const configPath = path.join(process.cwd(), CONFIG_FILENAME);
|
|
513
|
+
if (fs.existsSync(configPath)) {
|
|
514
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
515
|
+
const config = JSON.parse(content) || {};
|
|
516
|
+
serverDebugEnabled = config.debug === true;
|
|
517
|
+
} else if (process.env?.DEBUG === "true") {
|
|
518
|
+
serverDebugEnabled = true;
|
|
519
|
+
}
|
|
520
|
+
} catch (err) {
|
|
521
|
+
if (process.env?.DEBUG === "true") {
|
|
522
|
+
serverDebugEnabled = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function isDebugEnabled() {
|
|
527
|
+
if (isBrowser) {
|
|
528
|
+
try {
|
|
529
|
+
return localStorage.getItem("agenteval_debug") === "true";
|
|
530
|
+
} catch {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return serverDebugEnabled;
|
|
535
|
+
}
|
|
536
|
+
function debug(module, ...args) {
|
|
537
|
+
if (isDebugEnabled()) {
|
|
538
|
+
console.debug(`[${module}]`, ...args);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
504
542
|
// services/connectors/base/BaseConnector.ts
|
|
505
543
|
var BaseConnector = class {
|
|
506
544
|
/**
|
|
@@ -631,9 +669,11 @@ var SSEClient = class {
|
|
|
631
669
|
this.abortController = new AbortController();
|
|
632
670
|
debug("SSE", "Connecting to", url);
|
|
633
671
|
debug("SSE", "Method:", method);
|
|
634
|
-
debug("SSE", "
|
|
672
|
+
debug("SSE", "Headers:", headers);
|
|
673
|
+
debug("SSE", "Payload:", body ? JSON.stringify(body, null, 2).substring(0, 500) : "none");
|
|
674
|
+
debug("SSE", "Timeout:", idleTimeoutMs, "ms");
|
|
635
675
|
try {
|
|
636
|
-
const
|
|
676
|
+
const requestConfig = {
|
|
637
677
|
method,
|
|
638
678
|
headers: {
|
|
639
679
|
"Content-Type": "application/json",
|
|
@@ -642,17 +682,28 @@ var SSEClient = class {
|
|
|
642
682
|
},
|
|
643
683
|
body: body ? JSON.stringify(body) : void 0,
|
|
644
684
|
signal: this.abortController.signal
|
|
645
|
-
}
|
|
685
|
+
};
|
|
686
|
+
debug("SSE", "Request config:", JSON.stringify(requestConfig, null, 2).substring(0, 500));
|
|
687
|
+
const response = await fetch(url, requestConfig);
|
|
688
|
+
debug("SSE", "Response received:", response.status, response.statusText);
|
|
646
689
|
if (!response.ok) {
|
|
647
|
-
|
|
690
|
+
let errorBody = "";
|
|
691
|
+
try {
|
|
692
|
+
errorBody = await response.text();
|
|
693
|
+
debug("SSE", "Error response body:", errorBody.substring(0, 500));
|
|
694
|
+
} catch {
|
|
695
|
+
debug("SSE", "Could not read error response body");
|
|
696
|
+
}
|
|
697
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}${errorBody ? ` - ${errorBody}` : ""}`);
|
|
648
698
|
}
|
|
649
699
|
if (!response.body) {
|
|
650
700
|
throw new Error("Response body is null");
|
|
651
701
|
}
|
|
652
|
-
|
|
702
|
+
console.info("[SSE] Connected to agent endpoint, streaming events...");
|
|
653
703
|
debug("SSE", "Response status:", response.status);
|
|
654
704
|
debug("SSE", "Content-Type:", response.headers.get("content-type"));
|
|
655
705
|
const completionReason = await this.processStream(response.body, onEvent, completeOnRunEnd, idleTimeoutMs);
|
|
706
|
+
console.info(`[SSE] Stream completed: ${completionReason}`);
|
|
656
707
|
debug("SSE", `Stream completed: ${completionReason}`);
|
|
657
708
|
onComplete?.();
|
|
658
709
|
} catch (error) {
|
|
@@ -662,9 +713,36 @@ var SSEClient = class {
|
|
|
662
713
|
onComplete?.();
|
|
663
714
|
} else {
|
|
664
715
|
console.error("[SSE] Stream error:", error.message);
|
|
716
|
+
console.error("[SSE] Debug mode is:", isDebugEnabled() ? "ENABLED \u2705" : "DISABLED \u274C");
|
|
717
|
+
const errorDetails = {
|
|
718
|
+
name: error.name,
|
|
719
|
+
message: error.message,
|
|
720
|
+
stack: error.stack,
|
|
721
|
+
cause: error.cause,
|
|
722
|
+
url,
|
|
723
|
+
method,
|
|
724
|
+
headers
|
|
725
|
+
};
|
|
726
|
+
debug("SSE", "Error details:", errorDetails);
|
|
727
|
+
if (error.message.includes("fetch failed")) {
|
|
728
|
+
debug("SSE", '\u{1F4A1} Diagnostic: "fetch failed" typically means:');
|
|
729
|
+
debug("SSE", " - Connection refused (endpoint not running)");
|
|
730
|
+
debug("SSE", " - DNS resolution failed (invalid hostname)");
|
|
731
|
+
debug("SSE", " - Network unreachable (firewall/VPN issues)");
|
|
732
|
+
debug("SSE", " - SSL/TLS certificate issues (self-signed cert)");
|
|
733
|
+
debug("SSE", ` Check if ${url} is accessible`);
|
|
734
|
+
} else if (error.message.includes("timeout")) {
|
|
735
|
+
debug("SSE", "\u{1F4A1} Diagnostic: Request timed out - endpoint may be slow or unresponsive");
|
|
736
|
+
} else if (error.message.includes("ENOTFOUND")) {
|
|
737
|
+
debug("SSE", "\u{1F4A1} Diagnostic: DNS lookup failed - hostname not found");
|
|
738
|
+
} else if (error.message.includes("ECONNREFUSED")) {
|
|
739
|
+
debug("SSE", "\u{1F4A1} Diagnostic: Connection refused - service not listening on this port");
|
|
740
|
+
}
|
|
665
741
|
onError?.(error);
|
|
666
742
|
}
|
|
667
743
|
} else {
|
|
744
|
+
console.error("[SSE] Unknown error:", error);
|
|
745
|
+
debug("SSE", "Unknown error details:", error);
|
|
668
746
|
onError?.(new Error("Unknown error occurred"));
|
|
669
747
|
}
|
|
670
748
|
}
|
|
@@ -708,8 +786,11 @@ var SSEClient = class {
|
|
|
708
786
|
debug("SSE", "Raw event:", data.substring(0, 200) + (data.length > 200 ? "..." : ""));
|
|
709
787
|
try {
|
|
710
788
|
const event = JSON.parse(data);
|
|
711
|
-
debug("SSE", "Parsed:", event.type);
|
|
789
|
+
debug("SSE", "Parsed event:", event.type);
|
|
712
790
|
eventCount++;
|
|
791
|
+
if (eventCount % 10 === 0) {
|
|
792
|
+
console.info(`[SSE] Processed ${eventCount} events...`);
|
|
793
|
+
}
|
|
713
794
|
onEvent(event);
|
|
714
795
|
if (completeOnRunEnd && (event.type === AGUIEventType.RUN_FINISHED || event.type === AGUIEventType.RUN_ERROR)) {
|
|
715
796
|
debug("SSE", `Received ${event.type}, completing stream`);
|
|
@@ -1520,10 +1601,123 @@ var RESTConnector = class extends BaseConnector {
|
|
|
1520
1601
|
};
|
|
1521
1602
|
var restConnector = new RESTConnector();
|
|
1522
1603
|
|
|
1604
|
+
// services/connectors/openai-compatible/OpenAICompatibleConnector.ts
|
|
1605
|
+
var OpenAICompatibleConnector = class extends BaseConnector {
|
|
1606
|
+
constructor() {
|
|
1607
|
+
super(...arguments);
|
|
1608
|
+
this.type = "openai-compatible";
|
|
1609
|
+
this.name = "OpenAI-compatible";
|
|
1610
|
+
this.supportsStreaming = false;
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Build OpenAI Chat Completion payload from test case
|
|
1614
|
+
*/
|
|
1615
|
+
buildPayload(request) {
|
|
1616
|
+
const messages = [];
|
|
1617
|
+
if (request.testCase.context && request.testCase.context.length > 0) {
|
|
1618
|
+
const contextText = request.testCase.context.map((c) => typeof c === "string" ? c : JSON.stringify(c)).join("\n");
|
|
1619
|
+
messages.push({
|
|
1620
|
+
role: "system",
|
|
1621
|
+
content: contextText
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
messages.push({
|
|
1625
|
+
role: "user",
|
|
1626
|
+
content: request.testCase.initialPrompt
|
|
1627
|
+
});
|
|
1628
|
+
const payload = {
|
|
1629
|
+
model: request.modelId,
|
|
1630
|
+
messages
|
|
1631
|
+
};
|
|
1632
|
+
if (request.testCase.tools && request.testCase.tools.length > 0) {
|
|
1633
|
+
payload.tools = request.testCase.tools.map((tool) => ({
|
|
1634
|
+
type: "function",
|
|
1635
|
+
function: {
|
|
1636
|
+
name: tool.name,
|
|
1637
|
+
description: tool.description || "",
|
|
1638
|
+
parameters: tool.parameters || {}
|
|
1639
|
+
}
|
|
1640
|
+
}));
|
|
1641
|
+
}
|
|
1642
|
+
return payload;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Execute OpenAI-compatible Chat Completion request
|
|
1646
|
+
*/
|
|
1647
|
+
async execute(endpoint, request, auth, onProgress, onRawEvent) {
|
|
1648
|
+
const payload = request.payload || this.buildPayload(request);
|
|
1649
|
+
const headers = this.buildAuthHeaders(auth);
|
|
1650
|
+
this.debug("Executing OpenAI-compatible request");
|
|
1651
|
+
this.debug("Endpoint:", endpoint);
|
|
1652
|
+
this.debug("Model:", payload.model);
|
|
1653
|
+
const response = await fetch(endpoint, {
|
|
1654
|
+
method: "POST",
|
|
1655
|
+
headers: {
|
|
1656
|
+
"Content-Type": "application/json",
|
|
1657
|
+
...headers
|
|
1658
|
+
},
|
|
1659
|
+
body: JSON.stringify(payload)
|
|
1660
|
+
});
|
|
1661
|
+
if (!response.ok) {
|
|
1662
|
+
const errorText = await response.text();
|
|
1663
|
+
throw new Error(`OpenAI-compatible request failed: ${response.status} - ${errorText}`);
|
|
1664
|
+
}
|
|
1665
|
+
const data = await response.json();
|
|
1666
|
+
onRawEvent?.(data);
|
|
1667
|
+
const trajectory = this.parseResponse(data);
|
|
1668
|
+
trajectory.forEach((step) => onProgress?.(step));
|
|
1669
|
+
return {
|
|
1670
|
+
trajectory,
|
|
1671
|
+
runId: data.id || null,
|
|
1672
|
+
rawEvents: [data],
|
|
1673
|
+
metadata: {
|
|
1674
|
+
model: data.model,
|
|
1675
|
+
usage: data.usage,
|
|
1676
|
+
finishReason: data.choices?.[0]?.finish_reason
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Parse OpenAI Chat Completion response into trajectory steps
|
|
1682
|
+
*/
|
|
1683
|
+
parseResponse(data) {
|
|
1684
|
+
const steps = [];
|
|
1685
|
+
const choice = data.choices?.[0];
|
|
1686
|
+
if (!choice) {
|
|
1687
|
+
steps.push(this.createStep("response", JSON.stringify(data, null, 2)));
|
|
1688
|
+
return steps;
|
|
1689
|
+
}
|
|
1690
|
+
const message = choice.message;
|
|
1691
|
+
if (message.tool_calls && message.tool_calls.length > 0) {
|
|
1692
|
+
for (const toolCall of message.tool_calls) {
|
|
1693
|
+
let toolArgs;
|
|
1694
|
+
try {
|
|
1695
|
+
toolArgs = JSON.parse(toolCall.function.arguments);
|
|
1696
|
+
} catch {
|
|
1697
|
+
toolArgs = toolCall.function.arguments;
|
|
1698
|
+
}
|
|
1699
|
+
steps.push(this.createStep("action", `Calling ${toolCall.function.name}...`, {
|
|
1700
|
+
toolName: toolCall.function.name,
|
|
1701
|
+
toolArgs
|
|
1702
|
+
}));
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (message.content) {
|
|
1706
|
+
steps.push(this.createStep("response", message.content));
|
|
1707
|
+
}
|
|
1708
|
+
if (steps.length === 0) {
|
|
1709
|
+
steps.push(this.createStep("response", "(empty response)"));
|
|
1710
|
+
}
|
|
1711
|
+
return steps;
|
|
1712
|
+
}
|
|
1713
|
+
};
|
|
1714
|
+
var openaiCompatibleConnector = new OpenAICompatibleConnector();
|
|
1715
|
+
|
|
1523
1716
|
// services/connectors/index.ts
|
|
1524
1717
|
connectorRegistry.register(aguiStreamingConnector);
|
|
1525
1718
|
connectorRegistry.register(mockConnector);
|
|
1526
1719
|
connectorRegistry.register(restConnector);
|
|
1720
|
+
connectorRegistry.register(openaiCompatibleConnector);
|
|
1527
1721
|
console.log("[Connectors] Browser-safe connectors registered:", connectorRegistry.getRegisteredTypes().join(", "));
|
|
1528
1722
|
|
|
1529
1723
|
// services/connectors/subprocess/SubprocessConnector.ts
|
|
@@ -1890,7 +2084,39 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
|
|
|
1890
2084
|
this.isInThinking = false;
|
|
1891
2085
|
}
|
|
1892
2086
|
/**
|
|
1893
|
-
*
|
|
2087
|
+
* Build CLI args from ClaudeCodeConnectorConfig
|
|
2088
|
+
*/
|
|
2089
|
+
buildConfigArgs(config) {
|
|
2090
|
+
const args = [];
|
|
2091
|
+
if (config.dangerouslySkipPermissions) {
|
|
2092
|
+
args.push("--dangerously-skip-permissions");
|
|
2093
|
+
}
|
|
2094
|
+
if (config.systemPrompt) {
|
|
2095
|
+
args.push("--system-prompt", config.systemPrompt);
|
|
2096
|
+
} else if (config.appendSystemPrompt) {
|
|
2097
|
+
args.push("--append-system-prompt", config.appendSystemPrompt);
|
|
2098
|
+
}
|
|
2099
|
+
if (config.allowedTools?.length) {
|
|
2100
|
+
args.push("--allowed-tools", ...config.allowedTools);
|
|
2101
|
+
}
|
|
2102
|
+
if (config.disallowedTools?.length) {
|
|
2103
|
+
args.push("--disallowed-tools", ...config.disallowedTools);
|
|
2104
|
+
}
|
|
2105
|
+
if (config.mcpConfigPath) {
|
|
2106
|
+
args.push("--mcp-config", config.mcpConfigPath);
|
|
2107
|
+
} else if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
2108
|
+
args.push("--mcp-config", JSON.stringify({ mcpServers: config.mcpServers }));
|
|
2109
|
+
}
|
|
2110
|
+
if (config.strictMcpConfig) {
|
|
2111
|
+
args.push("--strict-mcp-config");
|
|
2112
|
+
}
|
|
2113
|
+
if (config.additionalArgs) {
|
|
2114
|
+
args.push(...config.additionalArgs);
|
|
2115
|
+
}
|
|
2116
|
+
return args;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Override execute to reset state and apply connectorConfig
|
|
1894
2120
|
*/
|
|
1895
2121
|
async execute(endpoint, request, auth, onProgress, onRawEvent) {
|
|
1896
2122
|
this.debug("========== execute() STARTED ==========");
|
|
@@ -1898,11 +2124,55 @@ var ClaudeCodeConnector = class extends SubprocessConnector {
|
|
|
1898
2124
|
this.debug("Test case:", request.testCase.name);
|
|
1899
2125
|
this.debug("Config:", this["config"]);
|
|
1900
2126
|
this.resetState();
|
|
1901
|
-
this.
|
|
1902
|
-
const
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
2127
|
+
const originalEnv = this.config.env ? structuredClone(this.config.env) : {};
|
|
2128
|
+
const originalArgs = this.config.args ? [...this.config.args] : [];
|
|
2129
|
+
const originalInputMode = this.config.inputMode;
|
|
2130
|
+
const originalTimeout = this.config.timeout;
|
|
2131
|
+
const originalWorkingDir = this.config.workingDir;
|
|
2132
|
+
const ccConfig = request.connectorConfig;
|
|
2133
|
+
if (ccConfig) {
|
|
2134
|
+
this.debug("Applying connectorConfig:", Object.keys(ccConfig));
|
|
2135
|
+
if (ccConfig.env) {
|
|
2136
|
+
this.config.env = { ...this.config.env, ...ccConfig.env };
|
|
2137
|
+
}
|
|
2138
|
+
if (ccConfig.usePromptArg) {
|
|
2139
|
+
this.config.inputMode = "arg";
|
|
2140
|
+
}
|
|
2141
|
+
if (ccConfig.timeout !== void 0) {
|
|
2142
|
+
this.config.timeout = ccConfig.timeout;
|
|
2143
|
+
}
|
|
2144
|
+
if (ccConfig.workingDir) {
|
|
2145
|
+
this.config.workingDir = ccConfig.workingDir;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
if (this.config.env?.CLAUDE_CODE_USE_BEDROCK === "1") {
|
|
2149
|
+
this.config.env = { ...this.config.env, ANTHROPIC_API_KEY: "" };
|
|
2150
|
+
this.debug("Bedrock mode: cleared ANTHROPIC_API_KEY to bypass credit check");
|
|
2151
|
+
}
|
|
2152
|
+
if (request.modelId) {
|
|
2153
|
+
this.config.args = [...this.config.args || [], "--model", request.modelId];
|
|
2154
|
+
this.debug("Model flag added:", request.modelId);
|
|
2155
|
+
}
|
|
2156
|
+
if (ccConfig) {
|
|
2157
|
+
const configArgs = this.buildConfigArgs(ccConfig);
|
|
2158
|
+
if (configArgs.length > 0) {
|
|
2159
|
+
this.config.args = [...this.config.args || [], ...configArgs];
|
|
2160
|
+
this.debug("Config args added:", configArgs);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
try {
|
|
2164
|
+
this.debug("State reset, calling super.execute()...");
|
|
2165
|
+
const result = await super.execute(endpoint, request, auth, onProgress, onRawEvent);
|
|
2166
|
+
this.debug("super.execute() returned with", result.trajectory.length, "steps");
|
|
2167
|
+
this.debug("========== execute() COMPLETED ==========");
|
|
2168
|
+
return result;
|
|
2169
|
+
} finally {
|
|
2170
|
+
this.config.env = originalEnv;
|
|
2171
|
+
this.config.args = originalArgs;
|
|
2172
|
+
this.config.inputMode = originalInputMode;
|
|
2173
|
+
this.config.timeout = originalTimeout;
|
|
2174
|
+
this.config.workingDir = originalWorkingDir;
|
|
2175
|
+
}
|
|
1906
2176
|
}
|
|
1907
2177
|
/**
|
|
1908
2178
|
* Health check - verify claude command exists
|
|
@@ -2156,6 +2426,12 @@ function createServerCleanup(result, isCI) {
|
|
|
2156
2426
|
}
|
|
2157
2427
|
|
|
2158
2428
|
// cli/utils/apiClient.ts
|
|
2429
|
+
var ServerError = class extends Error {
|
|
2430
|
+
constructor(message) {
|
|
2431
|
+
super(message);
|
|
2432
|
+
this.name = "ServerError";
|
|
2433
|
+
}
|
|
2434
|
+
};
|
|
2159
2435
|
var ApiClient = class {
|
|
2160
2436
|
constructor(baseUrl) {
|
|
2161
2437
|
this.baseUrl = baseUrl;
|
|
@@ -2271,7 +2547,7 @@ var ApiClient = class {
|
|
|
2271
2547
|
} else if (event.type === "completed" || event.type === "cancelled") {
|
|
2272
2548
|
finalRun = event.run;
|
|
2273
2549
|
} else if (event.type === "error") {
|
|
2274
|
-
throw new
|
|
2550
|
+
throw new ServerError(event.error);
|
|
2275
2551
|
}
|
|
2276
2552
|
} catch (e) {
|
|
2277
2553
|
if (e instanceof SyntaxError) continue;
|
|
@@ -2281,6 +2557,9 @@ var ApiClient = class {
|
|
|
2281
2557
|
}
|
|
2282
2558
|
}
|
|
2283
2559
|
} catch (streamError) {
|
|
2560
|
+
if (streamError instanceof ServerError) {
|
|
2561
|
+
throw streamError;
|
|
2562
|
+
}
|
|
2284
2563
|
if (runId) {
|
|
2285
2564
|
console.warn(`[ApiClient] SSE stream disconnected: ${streamError instanceof Error ? streamError.message : streamError}`);
|
|
2286
2565
|
console.warn(`[ApiClient] Falling back to polling for run ${runId}...`);
|
|
@@ -2518,7 +2797,7 @@ var ApiClient = class {
|
|
|
2518
2797
|
}
|
|
2519
2798
|
const response = await this.listTestCasesWithMeta();
|
|
2520
2799
|
return response.data.find(
|
|
2521
|
-
(tc) => tc.name
|
|
2800
|
+
(tc) => tc.name?.toLowerCase() === identifier.toLowerCase()
|
|
2522
2801
|
) || null;
|
|
2523
2802
|
}
|
|
2524
2803
|
/**
|
|
@@ -2566,7 +2845,7 @@ var ApiClient = class {
|
|
|
2566
2845
|
if (event.type === "completed") {
|
|
2567
2846
|
result = event.report;
|
|
2568
2847
|
} else if (event.type === "error") {
|
|
2569
|
-
throw new
|
|
2848
|
+
throw new ServerError(event.error);
|
|
2570
2849
|
}
|
|
2571
2850
|
} catch (e) {
|
|
2572
2851
|
if (e instanceof SyntaxError) continue;
|
|
@@ -2616,12 +2895,53 @@ var ApiClient = class {
|
|
|
2616
2895
|
}
|
|
2617
2896
|
return res.json();
|
|
2618
2897
|
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Fetch traces from OpenSearch with optional filters
|
|
2900
|
+
*/
|
|
2901
|
+
async fetchTraces(params) {
|
|
2902
|
+
const res = await fetch(`${this.baseUrl}/api/traces`, {
|
|
2903
|
+
method: "POST",
|
|
2904
|
+
headers: { "Content-Type": "application/json" },
|
|
2905
|
+
body: JSON.stringify(params)
|
|
2906
|
+
});
|
|
2907
|
+
if (!res.ok) {
|
|
2908
|
+
const errorBody = await res.text();
|
|
2909
|
+
let errorMessage;
|
|
2910
|
+
try {
|
|
2911
|
+
const parsed = JSON.parse(errorBody);
|
|
2912
|
+
errorMessage = parsed.error || errorBody;
|
|
2913
|
+
} catch {
|
|
2914
|
+
errorMessage = errorBody;
|
|
2915
|
+
}
|
|
2916
|
+
throw new Error(`Failed to fetch traces: ${errorMessage}`);
|
|
2917
|
+
}
|
|
2918
|
+
return res.json();
|
|
2919
|
+
}
|
|
2619
2920
|
};
|
|
2620
2921
|
|
|
2621
|
-
// cli/
|
|
2922
|
+
// cli/utils/formatOutput.ts
|
|
2923
|
+
var OUTPUT_FORMAT_DESCRIPTION = "Output format: table, json, markdown";
|
|
2924
|
+
function formatMarkdownTable(headers, rows) {
|
|
2925
|
+
const separator = headers.map(() => "---");
|
|
2926
|
+
const lines = [
|
|
2927
|
+
`| ${headers.join(" | ")} |`,
|
|
2928
|
+
`| ${separator.join(" | ")} |`,
|
|
2929
|
+
...rows.map((row) => `| ${row.join(" | ")} |`)
|
|
2930
|
+
];
|
|
2931
|
+
return lines.join("\n");
|
|
2932
|
+
}
|
|
2622
2933
|
function formatJson(data) {
|
|
2623
2934
|
return JSON.stringify(data, null, 2);
|
|
2624
2935
|
}
|
|
2936
|
+
function parseOutputFormat(format) {
|
|
2937
|
+
const normalized = format.toLowerCase();
|
|
2938
|
+
if (normalized === "table" || normalized === "json" || normalized === "markdown" || normalized === "md") {
|
|
2939
|
+
return normalized === "md" ? "markdown" : normalized;
|
|
2940
|
+
}
|
|
2941
|
+
return "table";
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
// cli/commands/list.ts
|
|
2625
2945
|
function displayStorageWarnings(meta) {
|
|
2626
2946
|
if (!meta.storageConfigured) {
|
|
2627
2947
|
console.log(chalk.yellow("\n \u26A0 Storage not configured"));
|
|
@@ -2642,25 +2962,24 @@ async function listAgents(format, config) {
|
|
|
2642
2962
|
console.log(formatJson(agents));
|
|
2643
2963
|
return;
|
|
2644
2964
|
}
|
|
2965
|
+
const headers = ["Key", "Name", "Connector", "Endpoint"];
|
|
2966
|
+
const rows = agents.map((agent) => [
|
|
2967
|
+
agent.key,
|
|
2968
|
+
agent.name,
|
|
2969
|
+
agent.connectorType || "agui-streaming",
|
|
2970
|
+
agent.endpoint.substring(0, 47) + (agent.endpoint.length > 47 ? "..." : "")
|
|
2971
|
+
]);
|
|
2972
|
+
if (format === "markdown") {
|
|
2973
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
2974
|
+
return;
|
|
2975
|
+
}
|
|
2645
2976
|
const table = new Table({
|
|
2646
|
-
head:
|
|
2647
|
-
|
|
2648
|
-
chalk.cyan("Name"),
|
|
2649
|
-
chalk.cyan("Connector"),
|
|
2650
|
-
chalk.cyan("Models"),
|
|
2651
|
-
chalk.cyan("Endpoint")
|
|
2652
|
-
],
|
|
2653
|
-
colWidths: [15, 20, 15, 25, 40],
|
|
2977
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2978
|
+
colWidths: [15, 20, 15, 50],
|
|
2654
2979
|
wordWrap: true
|
|
2655
2980
|
});
|
|
2656
|
-
for (const
|
|
2657
|
-
table.push(
|
|
2658
|
-
agent.key,
|
|
2659
|
-
agent.name,
|
|
2660
|
-
agent.connectorType || "agui-streaming",
|
|
2661
|
-
agent.models.slice(0, 3).join(", ") + (agent.models.length > 3 ? "..." : ""),
|
|
2662
|
-
agent.endpoint.substring(0, 37) + (agent.endpoint.length > 37 ? "..." : "")
|
|
2663
|
-
]);
|
|
2981
|
+
for (const row of rows) {
|
|
2982
|
+
table.push(row);
|
|
2664
2983
|
}
|
|
2665
2984
|
console.log(chalk.bold("\nAvailable Agents:\n"));
|
|
2666
2985
|
console.log(table.toString());
|
|
@@ -2682,31 +3001,35 @@ async function listTestCases(format, config) {
|
|
|
2682
3001
|
try {
|
|
2683
3002
|
const client = new ApiClient(serverResult.baseUrl);
|
|
2684
3003
|
const response = await client.listTestCasesWithMeta();
|
|
2685
|
-
|
|
3004
|
+
if (format !== "markdown") {
|
|
3005
|
+
displayStorageWarnings(response.meta);
|
|
3006
|
+
}
|
|
2686
3007
|
if (format === "json") {
|
|
2687
3008
|
console.log(formatJson(response));
|
|
2688
3009
|
return;
|
|
2689
3010
|
}
|
|
2690
|
-
const
|
|
2691
|
-
|
|
2692
|
-
chalk.cyan("ID"),
|
|
2693
|
-
chalk.cyan("Name"),
|
|
2694
|
-
chalk.cyan("Labels"),
|
|
2695
|
-
chalk.cyan("Version"),
|
|
2696
|
-
chalk.cyan("Source")
|
|
2697
|
-
],
|
|
2698
|
-
colWidths: [25, 28, 28, 10, 10],
|
|
2699
|
-
wordWrap: true
|
|
2700
|
-
});
|
|
2701
|
-
for (const tc of response.data) {
|
|
3011
|
+
const headers = ["ID", "Name", "Labels", "Version", "Source"];
|
|
3012
|
+
const rows = response.data.map((tc) => {
|
|
2702
3013
|
const isDemo = tc.id.startsWith("demo-");
|
|
2703
|
-
|
|
3014
|
+
return [
|
|
2704
3015
|
tc.id,
|
|
2705
3016
|
tc.name,
|
|
2706
3017
|
tc.labels?.slice(0, 3).join(", ") || "",
|
|
2707
3018
|
`v${tc.currentVersion || 1}`,
|
|
2708
|
-
isDemo ?
|
|
2709
|
-
]
|
|
3019
|
+
isDemo ? "Sample" : "Stored"
|
|
3020
|
+
];
|
|
3021
|
+
});
|
|
3022
|
+
if (format === "markdown") {
|
|
3023
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3024
|
+
return;
|
|
3025
|
+
}
|
|
3026
|
+
const table = new Table({
|
|
3027
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
3028
|
+
colWidths: [25, 28, 28, 10, 10],
|
|
3029
|
+
wordWrap: true
|
|
3030
|
+
});
|
|
3031
|
+
for (const row of rows) {
|
|
3032
|
+
table.push(row);
|
|
2710
3033
|
}
|
|
2711
3034
|
console.log(chalk.bold("\nAvailable Test Cases:\n"));
|
|
2712
3035
|
console.log(table.toString());
|
|
@@ -2735,31 +3058,35 @@ async function listBenchmarks(format, config) {
|
|
|
2735
3058
|
try {
|
|
2736
3059
|
const client = new ApiClient(serverResult.baseUrl);
|
|
2737
3060
|
const response = await client.listBenchmarksWithMeta();
|
|
2738
|
-
|
|
3061
|
+
if (format !== "markdown") {
|
|
3062
|
+
displayStorageWarnings(response.meta);
|
|
3063
|
+
}
|
|
2739
3064
|
if (format === "json") {
|
|
2740
3065
|
console.log(formatJson(response));
|
|
2741
3066
|
return;
|
|
2742
3067
|
}
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
chalk.cyan("ID"),
|
|
2746
|
-
chalk.cyan("Name"),
|
|
2747
|
-
chalk.cyan("Test Cases"),
|
|
2748
|
-
chalk.cyan("Created"),
|
|
2749
|
-
chalk.cyan("Source")
|
|
2750
|
-
],
|
|
2751
|
-
colWidths: [28, 28, 12, 22, 10],
|
|
2752
|
-
wordWrap: true
|
|
2753
|
-
});
|
|
2754
|
-
for (const b of response.data) {
|
|
3068
|
+
const headers = ["ID", "Name", "Test Cases", "Created", "Source"];
|
|
3069
|
+
const rows = response.data.map((b) => {
|
|
2755
3070
|
const isDemo = b.id.startsWith("demo-");
|
|
2756
|
-
|
|
3071
|
+
return [
|
|
2757
3072
|
b.id,
|
|
2758
3073
|
b.name,
|
|
2759
3074
|
b.testCaseIds.length.toString(),
|
|
2760
3075
|
new Date(b.createdAt).toLocaleDateString(),
|
|
2761
|
-
isDemo ?
|
|
2762
|
-
]
|
|
3076
|
+
isDemo ? "Sample" : "Stored"
|
|
3077
|
+
];
|
|
3078
|
+
});
|
|
3079
|
+
if (format === "markdown") {
|
|
3080
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
const table = new Table({
|
|
3084
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
3085
|
+
colWidths: [28, 28, 12, 22, 10],
|
|
3086
|
+
wordWrap: true
|
|
3087
|
+
});
|
|
3088
|
+
for (const row of rows) {
|
|
3089
|
+
table.push(row);
|
|
2763
3090
|
}
|
|
2764
3091
|
console.log(chalk.bold("\nAvailable Benchmarks:\n"));
|
|
2765
3092
|
console.log(table.toString());
|
|
@@ -2796,19 +3123,25 @@ function listConnectors(format) {
|
|
|
2796
3123
|
console.log(formatJson(connectors));
|
|
2797
3124
|
return;
|
|
2798
3125
|
}
|
|
3126
|
+
const headers = ["Type", "Name", "Streaming"];
|
|
3127
|
+
const rows = connectors.map((c) => [
|
|
3128
|
+
c.type,
|
|
3129
|
+
c.name,
|
|
3130
|
+
c.streaming ? "Yes" : "No"
|
|
3131
|
+
]);
|
|
3132
|
+
if (format === "markdown") {
|
|
3133
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
2799
3136
|
const table = new Table({
|
|
2800
|
-
head:
|
|
2801
|
-
chalk.cyan("Type"),
|
|
2802
|
-
chalk.cyan("Name"),
|
|
2803
|
-
chalk.cyan("Streaming")
|
|
2804
|
-
],
|
|
3137
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2805
3138
|
colWidths: [20, 25, 12]
|
|
2806
3139
|
});
|
|
2807
|
-
for (const
|
|
3140
|
+
for (const row of rows) {
|
|
2808
3141
|
table.push([
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
3142
|
+
row[0],
|
|
3143
|
+
row[1],
|
|
3144
|
+
row[2] === "Yes" ? chalk.green("Yes") : chalk.gray("No")
|
|
2812
3145
|
]);
|
|
2813
3146
|
}
|
|
2814
3147
|
console.log(chalk.bold("\nRegistered Connectors:\n"));
|
|
@@ -2827,23 +3160,24 @@ async function listModels(format, config) {
|
|
|
2827
3160
|
console.log(formatJson(models));
|
|
2828
3161
|
return;
|
|
2829
3162
|
}
|
|
3163
|
+
const headers = ["Key", "Display Name", "Provider", "Context"];
|
|
3164
|
+
const rows = models.map((m) => [
|
|
3165
|
+
m.key,
|
|
3166
|
+
m.display_name || m.key,
|
|
3167
|
+
m.provider || "bedrock",
|
|
3168
|
+
m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
|
|
3169
|
+
]);
|
|
3170
|
+
if (format === "markdown") {
|
|
3171
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
2830
3174
|
const table = new Table({
|
|
2831
|
-
head:
|
|
2832
|
-
chalk.cyan("Key"),
|
|
2833
|
-
chalk.cyan("Display Name"),
|
|
2834
|
-
chalk.cyan("Provider"),
|
|
2835
|
-
chalk.cyan("Context")
|
|
2836
|
-
],
|
|
3175
|
+
head: headers.map((h) => chalk.cyan(h)),
|
|
2837
3176
|
colWidths: [25, 30, 12, 12],
|
|
2838
3177
|
wordWrap: true
|
|
2839
3178
|
});
|
|
2840
|
-
for (const
|
|
2841
|
-
table.push(
|
|
2842
|
-
m.key,
|
|
2843
|
-
m.display_name || m.key,
|
|
2844
|
-
m.provider || "bedrock",
|
|
2845
|
-
m.context_window ? `${Math.round(m.context_window / 1e3)}k` : "-"
|
|
2846
|
-
]);
|
|
3179
|
+
for (const row of rows) {
|
|
3180
|
+
table.push(row);
|
|
2847
3181
|
}
|
|
2848
3182
|
console.log(chalk.bold("\nAvailable Models:\n"));
|
|
2849
3183
|
console.log(table.toString());
|
|
@@ -2860,8 +3194,8 @@ async function listModels(format, config) {
|
|
|
2860
3194
|
}
|
|
2861
3195
|
}
|
|
2862
3196
|
function createListCommand() {
|
|
2863
|
-
const command = new Command("list").description("List available resources").argument("<resource>", "Resource type: agents, test-cases, benchmarks, connectors, models").option("-o, --output <format>",
|
|
2864
|
-
const format = options.output;
|
|
3197
|
+
const command = new Command("list").description("List available resources").argument("<resource>", "Resource type: agents, test-cases, benchmarks, connectors, models").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").action(async (resource, options) => {
|
|
3198
|
+
const format = parseOutputFormat(options.output);
|
|
2865
3199
|
const config = await loadConfig();
|
|
2866
3200
|
for (const connector of config.connectors) {
|
|
2867
3201
|
connectorRegistry.register(connector);
|
|
@@ -2905,8 +3239,8 @@ function findAgent(identifier, config) {
|
|
|
2905
3239
|
(a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
|
|
2906
3240
|
);
|
|
2907
3241
|
}
|
|
2908
|
-
function getDefaultModel(
|
|
2909
|
-
return
|
|
3242
|
+
function getDefaultModel(config) {
|
|
3243
|
+
return Object.keys(config.models)[0] || "claude-sonnet";
|
|
2910
3244
|
}
|
|
2911
3245
|
async function commandExists(command) {
|
|
2912
3246
|
const { execSync: execSync2 } = await import("child_process");
|
|
@@ -2960,26 +3294,36 @@ async function runForAgent(client, testCaseId, agent, modelId, verbose) {
|
|
|
2960
3294
|
throw error;
|
|
2961
3295
|
}
|
|
2962
3296
|
}
|
|
2963
|
-
function
|
|
3297
|
+
function buildResultRows(results) {
|
|
3298
|
+
return results.map((r) => {
|
|
3299
|
+
if (!r.report) {
|
|
3300
|
+
return [r.agent.name, "ERROR", "-", "-", "-"];
|
|
3301
|
+
}
|
|
3302
|
+
const status = r.report.passFailStatus === "passed" ? "PASSED" : r.report.passFailStatus === "failed" ? "FAILED" : r.report.status;
|
|
3303
|
+
return [
|
|
3304
|
+
r.agent.name,
|
|
3305
|
+
status,
|
|
3306
|
+
r.report.metrics?.accuracy ? `${Math.round(r.report.metrics.accuracy)}%` : "-",
|
|
3307
|
+
r.report.trajectorySteps.toString(),
|
|
3308
|
+
r.report.id?.substring(0, 27) + "..." || "-"
|
|
3309
|
+
];
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
function displayResults(results, format) {
|
|
3313
|
+
const headers = ["Agent", "Status", "Accuracy", "Steps", "Report ID"];
|
|
3314
|
+
const rows = buildResultRows(results);
|
|
3315
|
+
if (format === "markdown") {
|
|
3316
|
+
console.log("\n");
|
|
3317
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3318
|
+
return;
|
|
3319
|
+
}
|
|
2964
3320
|
const table = new Table2({
|
|
2965
|
-
head:
|
|
2966
|
-
chalk2.cyan("Agent"),
|
|
2967
|
-
chalk2.cyan("Status"),
|
|
2968
|
-
chalk2.cyan("Accuracy"),
|
|
2969
|
-
chalk2.cyan("Steps"),
|
|
2970
|
-
chalk2.cyan("Report ID")
|
|
2971
|
-
],
|
|
3321
|
+
head: headers.map((h) => chalk2.cyan(h)),
|
|
2972
3322
|
colWidths: [20, 12, 12, 10, 30]
|
|
2973
3323
|
});
|
|
2974
3324
|
for (const r of results) {
|
|
2975
3325
|
if (!r.report) {
|
|
2976
|
-
table.push([
|
|
2977
|
-
r.agent.name,
|
|
2978
|
-
chalk2.red("ERROR"),
|
|
2979
|
-
"-",
|
|
2980
|
-
"-",
|
|
2981
|
-
"-"
|
|
2982
|
-
]);
|
|
3326
|
+
table.push([r.agent.name, chalk2.red("ERROR"), "-", "-", "-"]);
|
|
2983
3327
|
continue;
|
|
2984
3328
|
}
|
|
2985
3329
|
const statusStr = r.report.passFailStatus === "passed" ? chalk2.green("PASSED") : r.report.passFailStatus === "failed" ? chalk2.red("FAILED") : chalk2.yellow(r.report.status);
|
|
@@ -2995,7 +3339,7 @@ function displayTableResults(results) {
|
|
|
2995
3339
|
console.log(table.toString());
|
|
2996
3340
|
}
|
|
2997
3341
|
function createRunCommand() {
|
|
2998
|
-
const command = new Command2("run").description("Run a test case against agents").requiredOption("-t, --test-case <id>", "Test case ID or name").option("-a, --agent <key>", "Agent key (can be specified multiple times)", (val, arr) => [...arr, val], []).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>",
|
|
3342
|
+
const command = new Command2("run").description("Run a test case against agents").requiredOption("-t, --test-case <id>", "Test case ID or name").option("-a, --agent <key>", "Agent key (can be specified multiple times)", (val, arr) => [...arr, val], []).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").option("-v, --verbose", "Show detailed trajectory output").action(async (options) => {
|
|
2999
3343
|
console.log(chalk2.bold("\nAgent Health - Test Case Runner\n"));
|
|
3000
3344
|
const config = await loadConfig();
|
|
3001
3345
|
for (const connector of config.connectors) {
|
|
@@ -3033,7 +3377,7 @@ function createRunCommand() {
|
|
|
3033
3377
|
console.log("");
|
|
3034
3378
|
const results = [];
|
|
3035
3379
|
for (const agent of agents) {
|
|
3036
|
-
const modelId = options.model || getDefaultModel(
|
|
3380
|
+
const modelId = options.model || getDefaultModel(config);
|
|
3037
3381
|
const validationError = await validateAgentRequirements(agent);
|
|
3038
3382
|
if (validationError) {
|
|
3039
3383
|
console.error(chalk2.red(` Error: ${validationError}`));
|
|
@@ -3044,17 +3388,27 @@ function createRunCommand() {
|
|
|
3044
3388
|
const report = await runForAgent(client, testCase.id, agent, modelId, options.verbose || false);
|
|
3045
3389
|
results.push({ agent, report });
|
|
3046
3390
|
} catch (error) {
|
|
3047
|
-
|
|
3391
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
3392
|
+
console.error(chalk2.red(` Error running ${agent.name}: ${errorMsg}`));
|
|
3393
|
+
const lowerError = errorMsg.toLowerCase();
|
|
3394
|
+
if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
|
|
3395
|
+
console.log(chalk2.gray(` Hint: Authentication issue. Check agent-health.config.ts (headers, hooks.beforeRequest, or credentials).`));
|
|
3396
|
+
} else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
|
|
3397
|
+
console.log(chalk2.gray(` Hint: Cannot reach agent endpoint. Run: npx @opensearch-project/agent-health doctor`));
|
|
3398
|
+
} else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
|
|
3399
|
+
console.log(chalk2.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
|
|
3400
|
+
}
|
|
3048
3401
|
results.push({ agent, report: null });
|
|
3049
3402
|
}
|
|
3050
3403
|
}
|
|
3051
|
-
|
|
3052
|
-
|
|
3404
|
+
const outputFormat = parseOutputFormat(options.output);
|
|
3405
|
+
if (outputFormat === "json") {
|
|
3406
|
+
console.log(formatJson(results.map((r) => ({
|
|
3053
3407
|
agent: { key: r.agent.key, name: r.agent.name },
|
|
3054
3408
|
report: r.report
|
|
3055
|
-
}))
|
|
3409
|
+
}))));
|
|
3056
3410
|
} else {
|
|
3057
|
-
|
|
3411
|
+
displayResults(results, outputFormat);
|
|
3058
3412
|
}
|
|
3059
3413
|
} catch (error) {
|
|
3060
3414
|
console.error(chalk2.red(`
|
|
@@ -3201,8 +3555,8 @@ function findAgent2(identifier, config) {
|
|
|
3201
3555
|
(a) => a.key === identifier || a.name.toLowerCase() === identifier.toLowerCase()
|
|
3202
3556
|
);
|
|
3203
3557
|
}
|
|
3204
|
-
function getDefaultModel2(
|
|
3205
|
-
return
|
|
3558
|
+
function getDefaultModel2(config) {
|
|
3559
|
+
return Object.keys(config.models)[0] || "claude-sonnet";
|
|
3206
3560
|
}
|
|
3207
3561
|
function isFilePath(value) {
|
|
3208
3562
|
return value.toLowerCase().endsWith(".json");
|
|
@@ -3238,7 +3592,7 @@ async function fetchReportsForRun(api, run) {
|
|
|
3238
3592
|
);
|
|
3239
3593
|
return reportsMap;
|
|
3240
3594
|
}
|
|
3241
|
-
async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
3595
|
+
async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose, concurrency) {
|
|
3242
3596
|
const results = {
|
|
3243
3597
|
agent,
|
|
3244
3598
|
passed: 0,
|
|
@@ -3253,7 +3607,8 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3253
3607
|
{
|
|
3254
3608
|
name: `CLI Run - ${agent.name}`,
|
|
3255
3609
|
agentKey: agent.key,
|
|
3256
|
-
modelId
|
|
3610
|
+
modelId,
|
|
3611
|
+
...concurrency && concurrency > 1 ? { concurrency } : {}
|
|
3257
3612
|
},
|
|
3258
3613
|
(event) => {
|
|
3259
3614
|
if (event.type === "started") {
|
|
@@ -3262,9 +3617,13 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3262
3617
|
const current = event.currentTestCaseIndex + 1;
|
|
3263
3618
|
const testCaseName = event.currentTestCase?.name || `Test ${current}`;
|
|
3264
3619
|
spinner.text = `${agent.name}: ${testCaseName} (${current}/${totalTestCases})`;
|
|
3265
|
-
if (
|
|
3620
|
+
if (event.result) {
|
|
3266
3621
|
const status = event.result.status === "completed" ? chalk3.green("\u2713") : chalk3.red("\u2717");
|
|
3267
3622
|
spinner.text = `${agent.name}: ${testCaseName} ${status} (${current}/${totalTestCases})`;
|
|
3623
|
+
if (verbose && event.result.status === "failed" && event.result.error) {
|
|
3624
|
+
spinner.info(`${agent.name}: ${testCaseName} ${chalk3.red("\u2717")} - ${event.result.error}`);
|
|
3625
|
+
spinner.start(`${agent.name}: (${current}/${totalTestCases})`);
|
|
3626
|
+
}
|
|
3268
3627
|
}
|
|
3269
3628
|
}
|
|
3270
3629
|
}
|
|
@@ -3291,6 +3650,7 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3291
3650
|
}
|
|
3292
3651
|
} catch (error) {
|
|
3293
3652
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3653
|
+
const isServerError = error instanceof ServerError;
|
|
3294
3654
|
if (startedRunId) {
|
|
3295
3655
|
results.runId = startedRunId;
|
|
3296
3656
|
try {
|
|
@@ -3319,29 +3679,71 @@ async function runBenchmarkForAgent(api, agent, modelId, benchmark, verbose) {
|
|
|
3319
3679
|
}
|
|
3320
3680
|
return results;
|
|
3321
3681
|
}
|
|
3682
|
+
if (run.status === "failed") {
|
|
3683
|
+
const runError = run.error || errorMessage;
|
|
3684
|
+
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${runError}`);
|
|
3685
|
+
if (stats.passed > 0 || stats.failed > 0) {
|
|
3686
|
+
console.log(chalk3.gray(` Partial results: ${stats.passed} passed, ${stats.failed} failed out of ${stats.total}`));
|
|
3687
|
+
}
|
|
3688
|
+
return results;
|
|
3689
|
+
}
|
|
3322
3690
|
}
|
|
3323
3691
|
} catch {
|
|
3324
3692
|
}
|
|
3325
3693
|
}
|
|
3326
|
-
|
|
3327
|
-
if (isStreamError && startedRunId) {
|
|
3328
|
-
spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
|
|
3329
|
-
console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
|
|
3330
|
-
} else {
|
|
3694
|
+
if (isServerError) {
|
|
3331
3695
|
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
|
|
3696
|
+
} else {
|
|
3697
|
+
const isStreamError = errorMessage.includes("terminated") || errorMessage.includes("network") || errorMessage.includes("stream") || errorMessage.includes("aborted");
|
|
3698
|
+
if (isStreamError && startedRunId) {
|
|
3699
|
+
spinner.warn(`${agent.name}: ${chalk3.yellow("Stream disconnected")} - server may still be processing`);
|
|
3700
|
+
console.log(chalk3.gray(` Check status: Use the UI to monitor progress`));
|
|
3701
|
+
} else {
|
|
3702
|
+
spinner.fail(`${agent.name}: ${chalk3.red("Failed")} - ${errorMessage}`);
|
|
3703
|
+
}
|
|
3704
|
+
}
|
|
3705
|
+
const lowerError = errorMessage.toLowerCase();
|
|
3706
|
+
if (lowerError.includes("401") || lowerError.includes("403") || lowerError.includes("unauthorized") || lowerError.includes("forbidden") || lowerError.includes("token") || lowerError.includes("auth")) {
|
|
3707
|
+
console.log(chalk3.gray(` Hint: This looks like an authentication issue. Check your agent-health.config.ts`));
|
|
3708
|
+
console.log(chalk3.gray(` (headers, hooks.beforeRequest, or credentials) and re-run.`));
|
|
3709
|
+
} else if (lowerError.includes("econnrefused") || lowerError.includes("enotfound") || lowerError.includes("connect")) {
|
|
3710
|
+
console.log(chalk3.gray(` Hint: Could not connect to the agent endpoint. Verify the endpoint in agent-health.config.ts`));
|
|
3711
|
+
console.log(chalk3.gray(` is reachable: npx @opensearch-project/agent-health doctor`));
|
|
3712
|
+
} else if (lowerError.includes("not found") || lowerError.includes("agent not found")) {
|
|
3713
|
+
console.log(chalk3.gray(` Hint: Agent key not found. List available agents: npx @opensearch-project/agent-health list agents`));
|
|
3714
|
+
} else if (lowerError.includes("hook") || lowerError.includes("beforerequest")) {
|
|
3715
|
+
console.log(chalk3.gray(` Hint: The beforeRequest hook in agent-health.config.ts threw an error.`));
|
|
3716
|
+
console.log(chalk3.gray(` Check the hook logic and any external services it calls.`));
|
|
3717
|
+
}
|
|
3718
|
+
if (errorMessage !== "terminated") {
|
|
3719
|
+
console.log(chalk3.gray(` Debug: Run with DEBUG=true for verbose server logs`));
|
|
3332
3720
|
}
|
|
3333
3721
|
}
|
|
3334
3722
|
return results;
|
|
3335
3723
|
}
|
|
3336
|
-
function
|
|
3724
|
+
function buildSummaryRows(allResults, totalTestCases) {
|
|
3725
|
+
return allResults.map((results) => {
|
|
3726
|
+
const passRate = totalTestCases > 0 ? results.passed / totalTestCases * 100 : 0;
|
|
3727
|
+
return [
|
|
3728
|
+
results.agent.name,
|
|
3729
|
+
results.passed.toString(),
|
|
3730
|
+
results.failed.toString(),
|
|
3731
|
+
`${passRate.toFixed(0)}%`,
|
|
3732
|
+
results.run?.id || results.runId || "N/A"
|
|
3733
|
+
];
|
|
3734
|
+
});
|
|
3735
|
+
}
|
|
3736
|
+
function displaySummary(allResults, totalTestCases, format) {
|
|
3737
|
+
const headers = ["Agent", "Passed", "Failed", "Pass Rate", "Run ID"];
|
|
3738
|
+
const rows = buildSummaryRows(allResults, totalTestCases);
|
|
3739
|
+
if (format === "markdown") {
|
|
3740
|
+
console.log("\n");
|
|
3741
|
+
console.log("## Benchmark Summary\n");
|
|
3742
|
+
console.log(formatMarkdownTable(headers, rows));
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3337
3745
|
const table = new Table3({
|
|
3338
|
-
head:
|
|
3339
|
-
chalk3.cyan("Agent"),
|
|
3340
|
-
chalk3.cyan("Passed"),
|
|
3341
|
-
chalk3.cyan("Failed"),
|
|
3342
|
-
chalk3.cyan("Pass Rate"),
|
|
3343
|
-
chalk3.cyan("Run ID")
|
|
3344
|
-
],
|
|
3746
|
+
head: headers.map((h) => chalk3.cyan(h)),
|
|
3345
3747
|
colWidths: [25, 10, 10, 12, 35]
|
|
3346
3748
|
});
|
|
3347
3749
|
for (const results of allResults) {
|
|
@@ -3359,26 +3761,50 @@ function displaySummaryTable(allResults, totalTestCases) {
|
|
|
3359
3761
|
console.log(chalk3.bold("Benchmark Summary"));
|
|
3360
3762
|
console.log(table.toString());
|
|
3361
3763
|
}
|
|
3362
|
-
function exportResults(benchmark, allResults, exportPath) {
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
}
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3764
|
+
async function exportResults(benchmark, allResults, exportPath, format, serverBaseUrl) {
|
|
3765
|
+
if (format !== "json") {
|
|
3766
|
+
const runIds = allResults.map((r) => r.run?.id || r.runId).filter((id) => !!id);
|
|
3767
|
+
const params = new URLSearchParams({ format });
|
|
3768
|
+
if (runIds.length > 0) {
|
|
3769
|
+
params.set("runIds", runIds.join(","));
|
|
3770
|
+
}
|
|
3771
|
+
const url = `${serverBaseUrl}/api/storage/benchmarks/${encodeURIComponent(benchmark.id)}/report?${params.toString()}`;
|
|
3772
|
+
const response = await fetch(url);
|
|
3773
|
+
if (!response.ok) {
|
|
3774
|
+
const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
3775
|
+
console.error(chalk3.red(`
|
|
3776
|
+
Export failed: ${errorBody.error}`));
|
|
3777
|
+
return;
|
|
3778
|
+
}
|
|
3779
|
+
const contentType = response.headers.get("content-type") || "";
|
|
3780
|
+
if (contentType.includes("application/pdf")) {
|
|
3781
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
3782
|
+
writeFileSync(exportPath, buffer);
|
|
3783
|
+
} else {
|
|
3784
|
+
const text = await response.text();
|
|
3785
|
+
writeFileSync(exportPath, text);
|
|
3786
|
+
}
|
|
3787
|
+
} else {
|
|
3788
|
+
const exportData = {
|
|
3789
|
+
benchmark: {
|
|
3790
|
+
id: benchmark.id,
|
|
3791
|
+
name: benchmark.name,
|
|
3792
|
+
testCaseCount: benchmark.testCaseIds.length
|
|
3793
|
+
},
|
|
3794
|
+
runs: allResults.map((r) => ({
|
|
3795
|
+
agent: { key: r.agent.key, name: r.agent.name },
|
|
3796
|
+
runId: r.run?.id || r.runId,
|
|
3797
|
+
status: r.run?.status,
|
|
3798
|
+
passed: r.passed,
|
|
3799
|
+
failed: r.failed,
|
|
3800
|
+
passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
|
|
3801
|
+
results: r.run?.results,
|
|
3802
|
+
reports: r.reports
|
|
3803
|
+
})),
|
|
3804
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3805
|
+
};
|
|
3806
|
+
writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
|
|
3807
|
+
}
|
|
3382
3808
|
console.log(chalk3.green(`
|
|
3383
3809
|
Results exported to: ${exportPath}`));
|
|
3384
3810
|
}
|
|
@@ -3388,7 +3814,7 @@ function createBenchmarkCommand() {
|
|
|
3388
3814
|
"Agent key (can be specified multiple times)",
|
|
3389
3815
|
(val, arr) => [...arr, val],
|
|
3390
3816
|
[]
|
|
3391
|
-
).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", "
|
|
3817
|
+
).option("-m, --model <id>", "Model ID (uses agent default if not specified)").option("-o, --output <format>", OUTPUT_FORMAT_DESCRIPTION, "table").option("--export <path>", "Export results to file").option("--format <type>", "Report format for --export: json (default), html, pdf", "json").option("-c, --concurrency <n>", "Number of test cases to run in parallel (default: 1)", "1").option("-v, --verbose", "Show detailed output").option("--stop-server", "Stop the server after benchmark completes (default: keep running)").action(async (options) => {
|
|
3392
3818
|
console.log(chalk3.bold("\nAgent Health - Benchmark Runner\n"));
|
|
3393
3819
|
const config = await loadConfig();
|
|
3394
3820
|
const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
|
|
@@ -3522,6 +3948,9 @@ function createBenchmarkCommand() {
|
|
|
3522
3948
|
console.log(chalk3.gray(` - ${a.name} (${a.key})`));
|
|
3523
3949
|
}
|
|
3524
3950
|
console.log("");
|
|
3951
|
+
console.log(chalk3.gray(" To add a custom agent, configure it in agent-health.config.ts"));
|
|
3952
|
+
console.log(chalk3.gray(" Generate one with: npx @opensearch-project/agent-health init"));
|
|
3953
|
+
console.log("");
|
|
3525
3954
|
process.exit(1);
|
|
3526
3955
|
}
|
|
3527
3956
|
agents.push(agent);
|
|
@@ -3529,19 +3958,25 @@ function createBenchmarkCommand() {
|
|
|
3529
3958
|
console.log(chalk3.gray(` Agents: ${agents.map((a) => a.name).join(", ")}`));
|
|
3530
3959
|
}
|
|
3531
3960
|
console.log("");
|
|
3961
|
+
const concurrency = Math.max(1, Math.min(20, parseInt(options.concurrency, 10) || 1));
|
|
3962
|
+
if (concurrency > 1) {
|
|
3963
|
+
console.log(chalk3.gray(` Concurrency: ${concurrency}`));
|
|
3964
|
+
}
|
|
3532
3965
|
const allResults = [];
|
|
3533
3966
|
for (const agent of agents) {
|
|
3534
|
-
const modelId = options.model || getDefaultModel2(
|
|
3967
|
+
const modelId = options.model || getDefaultModel2(config);
|
|
3535
3968
|
const results = await runBenchmarkForAgent(
|
|
3536
3969
|
api,
|
|
3537
3970
|
agent,
|
|
3538
3971
|
modelId,
|
|
3539
3972
|
benchmark,
|
|
3540
|
-
options.verbose || false
|
|
3973
|
+
options.verbose || false,
|
|
3974
|
+
concurrency
|
|
3541
3975
|
);
|
|
3542
3976
|
allResults.push(results);
|
|
3543
3977
|
}
|
|
3544
|
-
|
|
3978
|
+
const outputFormat = parseOutputFormat(options.output);
|
|
3979
|
+
if (outputFormat === "json") {
|
|
3545
3980
|
const jsonOutput = allResults.map((r) => ({
|
|
3546
3981
|
agent: { key: r.agent.key, name: r.agent.name },
|
|
3547
3982
|
runId: r.run?.id || r.runId,
|
|
@@ -3550,12 +3985,12 @@ function createBenchmarkCommand() {
|
|
|
3550
3985
|
passRate: benchmark.testCaseIds.length > 0 ? r.passed / benchmark.testCaseIds.length * 100 : 0,
|
|
3551
3986
|
results: r.run?.results
|
|
3552
3987
|
}));
|
|
3553
|
-
console.log(
|
|
3988
|
+
console.log(formatJson(jsonOutput));
|
|
3554
3989
|
} else {
|
|
3555
|
-
|
|
3990
|
+
displaySummary(allResults, benchmark.testCaseIds.length, outputFormat);
|
|
3556
3991
|
}
|
|
3557
3992
|
if (options.export) {
|
|
3558
|
-
exportResults(benchmark, allResults, options.export);
|
|
3993
|
+
await exportResults(benchmark, allResults, options.export, options.format, serverResult.baseUrl);
|
|
3559
3994
|
}
|
|
3560
3995
|
console.log("");
|
|
3561
3996
|
console.log(chalk3.cyan("View results:"));
|
|
@@ -3630,9 +4065,86 @@ function createExportCommand() {
|
|
|
3630
4065
|
return command;
|
|
3631
4066
|
}
|
|
3632
4067
|
|
|
3633
|
-
// cli/commands/
|
|
4068
|
+
// cli/commands/report.ts
|
|
3634
4069
|
import { Command as Command5 } from "commander";
|
|
3635
4070
|
import chalk5 from "chalk";
|
|
4071
|
+
import ora3 from "ora";
|
|
4072
|
+
import { writeFileSync as writeFileSync3 } from "fs";
|
|
4073
|
+
function createReportCommand() {
|
|
4074
|
+
const command = new Command5("report").description("Generate a report for a benchmark").requiredOption("-b, --benchmark <id>", "Benchmark name or ID").option("-r, --runs <ids>", "Comma-separated run IDs (default: all runs)").option("-f, --format <type>", "Report format: json, html, pdf", "html").option("-o, --output <file>", "Output file path (auto-generates filename if omitted)").option("--stdout", "Write to stdout (JSON format only)").action(async (options) => {
|
|
4075
|
+
const config = await loadConfig();
|
|
4076
|
+
const serverConfig = { ...DEFAULT_SERVER_CONFIG, ...config.server };
|
|
4077
|
+
const connectSpinner = ora3("Connecting to server...").start();
|
|
4078
|
+
let serverResult;
|
|
4079
|
+
let cleanup;
|
|
4080
|
+
try {
|
|
4081
|
+
serverResult = await ensureServer(serverConfig);
|
|
4082
|
+
cleanup = createServerCleanup(serverResult, false);
|
|
4083
|
+
if (serverResult.wasStarted) {
|
|
4084
|
+
connectSpinner.succeed(`Started server on port ${serverConfig.port}`);
|
|
4085
|
+
} else {
|
|
4086
|
+
connectSpinner.succeed(`Connected to existing server on port ${serverConfig.port}`);
|
|
4087
|
+
}
|
|
4088
|
+
} catch (error) {
|
|
4089
|
+
connectSpinner.fail(
|
|
4090
|
+
`Failed to connect to server: ${error instanceof Error ? error.message : error}`
|
|
4091
|
+
);
|
|
4092
|
+
process.exit(1);
|
|
4093
|
+
}
|
|
4094
|
+
const api = new ApiClient(serverResult.baseUrl);
|
|
4095
|
+
try {
|
|
4096
|
+
const spinner = ora3("Finding benchmark...").start();
|
|
4097
|
+
const benchmark = await api.findBenchmark(options.benchmark);
|
|
4098
|
+
if (!benchmark) {
|
|
4099
|
+
spinner.fail(`Benchmark not found: "${options.benchmark}"`);
|
|
4100
|
+
console.log("");
|
|
4101
|
+
console.log(chalk5.cyan(" Available benchmarks:"));
|
|
4102
|
+
console.log(chalk5.gray(" npx agent-health list benchmarks"));
|
|
4103
|
+
console.log("");
|
|
4104
|
+
process.exit(1);
|
|
4105
|
+
}
|
|
4106
|
+
spinner.succeed(`Found benchmark: ${benchmark.name} (${benchmark.id})`);
|
|
4107
|
+
const params = new URLSearchParams({ format: options.format });
|
|
4108
|
+
if (options.runs) {
|
|
4109
|
+
params.set("runIds", options.runs);
|
|
4110
|
+
}
|
|
4111
|
+
const reportSpinner = ora3(`Generating ${options.format.toUpperCase()} report...`).start();
|
|
4112
|
+
const url = `${serverResult.baseUrl}/api/storage/benchmarks/${encodeURIComponent(benchmark.id)}/report?${params.toString()}`;
|
|
4113
|
+
const response = await fetch(url);
|
|
4114
|
+
if (!response.ok) {
|
|
4115
|
+
const errorBody = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
4116
|
+
reportSpinner.fail(`Report generation failed: ${errorBody.error}`);
|
|
4117
|
+
process.exit(1);
|
|
4118
|
+
}
|
|
4119
|
+
const contentDisposition = response.headers.get("content-disposition") || "";
|
|
4120
|
+
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
|
|
4121
|
+
const defaultFilename = filenameMatch?.[1] || `report.${options.format}`;
|
|
4122
|
+
if (options.stdout) {
|
|
4123
|
+
const text = await response.text();
|
|
4124
|
+
reportSpinner.stop();
|
|
4125
|
+
process.stdout.write(text);
|
|
4126
|
+
} else {
|
|
4127
|
+
const outputPath = options.output || defaultFilename;
|
|
4128
|
+
const contentType = response.headers.get("content-type") || "";
|
|
4129
|
+
if (contentType.includes("application/pdf")) {
|
|
4130
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
4131
|
+
writeFileSync3(outputPath, buffer);
|
|
4132
|
+
} else {
|
|
4133
|
+
const text = await response.text();
|
|
4134
|
+
writeFileSync3(outputPath, text);
|
|
4135
|
+
}
|
|
4136
|
+
reportSpinner.succeed(`Report saved to: ${outputPath}`);
|
|
4137
|
+
}
|
|
4138
|
+
} finally {
|
|
4139
|
+
cleanup();
|
|
4140
|
+
}
|
|
4141
|
+
});
|
|
4142
|
+
return command;
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
// cli/commands/doctor.ts
|
|
4146
|
+
import { Command as Command6 } from "commander";
|
|
4147
|
+
import chalk6 from "chalk";
|
|
3636
4148
|
import { existsSync as existsSync3 } from "fs";
|
|
3637
4149
|
import { resolve as resolve2 } from "path";
|
|
3638
4150
|
function checkConfigFile() {
|
|
@@ -3791,23 +4303,23 @@ function checkOpenSearchObservability() {
|
|
|
3791
4303
|
]
|
|
3792
4304
|
};
|
|
3793
4305
|
}
|
|
3794
|
-
function
|
|
3795
|
-
console.log(
|
|
4306
|
+
function displayResults2(results) {
|
|
4307
|
+
console.log(chalk6.bold("\n Configuration Check\n"));
|
|
3796
4308
|
for (const result of results) {
|
|
3797
4309
|
const icon = {
|
|
3798
|
-
ok:
|
|
3799
|
-
warning:
|
|
3800
|
-
error:
|
|
4310
|
+
ok: chalk6.green("\u2713"),
|
|
4311
|
+
warning: chalk6.yellow("\u26A0"),
|
|
4312
|
+
error: chalk6.red("\u2717")
|
|
3801
4313
|
}[result.status];
|
|
3802
4314
|
const messageColor = {
|
|
3803
|
-
ok:
|
|
3804
|
-
warning:
|
|
3805
|
-
error:
|
|
4315
|
+
ok: chalk6.green,
|
|
4316
|
+
warning: chalk6.yellow,
|
|
4317
|
+
error: chalk6.red
|
|
3806
4318
|
}[result.status];
|
|
3807
|
-
console.log(` ${icon} ${
|
|
4319
|
+
console.log(` ${icon} ${chalk6.bold(result.name)}: ${messageColor(result.message)}`);
|
|
3808
4320
|
if (result.details) {
|
|
3809
4321
|
for (const detail of result.details) {
|
|
3810
|
-
console.log(
|
|
4322
|
+
console.log(chalk6.gray(` ${detail}`));
|
|
3811
4323
|
}
|
|
3812
4324
|
}
|
|
3813
4325
|
}
|
|
@@ -3815,17 +4327,17 @@ function displayResults(results) {
|
|
|
3815
4327
|
const errors = results.filter((r) => r.status === "error").length;
|
|
3816
4328
|
const warnings = results.filter((r) => r.status === "warning").length;
|
|
3817
4329
|
if (errors > 0) {
|
|
3818
|
-
console.log(
|
|
4330
|
+
console.log(chalk6.red(` ${errors} error(s) found. Fix these before running evaluations.
|
|
3819
4331
|
`));
|
|
3820
4332
|
} else if (warnings > 0) {
|
|
3821
|
-
console.log(
|
|
4333
|
+
console.log(chalk6.yellow(` ${warnings} warning(s). Some features may be limited.
|
|
3822
4334
|
`));
|
|
3823
4335
|
} else {
|
|
3824
|
-
console.log(
|
|
4336
|
+
console.log(chalk6.green(" All checks passed!\n"));
|
|
3825
4337
|
}
|
|
3826
4338
|
}
|
|
3827
4339
|
function createDoctorCommand() {
|
|
3828
|
-
const command = new
|
|
4340
|
+
const command = new Command6("doctor").description("Check configuration and system requirements").option("-o, --output <format>", "Output format: text, json", "text").action(async (options) => {
|
|
3829
4341
|
const results = [];
|
|
3830
4342
|
const config = await loadConfig();
|
|
3831
4343
|
for (const connector of config.connectors) {
|
|
@@ -3842,16 +4354,16 @@ function createDoctorCommand() {
|
|
|
3842
4354
|
if (options.output === "json") {
|
|
3843
4355
|
console.log(JSON.stringify(results, null, 2));
|
|
3844
4356
|
} else {
|
|
3845
|
-
|
|
4357
|
+
displayResults2(results);
|
|
3846
4358
|
}
|
|
3847
4359
|
});
|
|
3848
4360
|
return command;
|
|
3849
4361
|
}
|
|
3850
4362
|
|
|
3851
4363
|
// cli/commands/init.ts
|
|
3852
|
-
import { Command as
|
|
3853
|
-
import
|
|
3854
|
-
import { writeFileSync as
|
|
4364
|
+
import { Command as Command7 } from "commander";
|
|
4365
|
+
import chalk7 from "chalk";
|
|
4366
|
+
import { writeFileSync as writeFileSync4, existsSync as existsSync4 } from "fs";
|
|
3855
4367
|
import { resolve as resolve3 } from "path";
|
|
3856
4368
|
var TYPESCRIPT_CONFIG = `/*
|
|
3857
4369
|
* Agent Health Configuration
|
|
@@ -3873,7 +4385,6 @@ export default defineConfig({
|
|
|
3873
4385
|
username: process.env.OPENSEARCH_USER || 'admin',
|
|
3874
4386
|
password: process.env.OPENSEARCH_PASS || 'admin',
|
|
3875
4387
|
},
|
|
3876
|
-
models: ['claude-sonnet'],
|
|
3877
4388
|
},
|
|
3878
4389
|
|
|
3879
4390
|
// Claude Code CLI agent (optional)
|
|
@@ -3890,7 +4401,6 @@ export default defineConfig({
|
|
|
3890
4401
|
},
|
|
3891
4402
|
}),
|
|
3892
4403
|
endpoint: 'claude', // Command name
|
|
3893
|
-
models: ['claude-sonnet-4'],
|
|
3894
4404
|
},
|
|
3895
4405
|
*/
|
|
3896
4406
|
],
|
|
@@ -3970,8 +4480,8 @@ expectedOutcomes:
|
|
|
3970
4480
|
- The agent should suggest investigating network connectivity
|
|
3971
4481
|
`;
|
|
3972
4482
|
function createInitCommand() {
|
|
3973
|
-
const command = new
|
|
3974
|
-
console.log(
|
|
4483
|
+
const command = new Command7("init").description("Initialize configuration files").option("--force", "Overwrite existing files").option("--with-examples", "Include example test case").action(async (options) => {
|
|
4484
|
+
console.log(chalk7.bold("\n Agent Health - Initialize Configuration\n"));
|
|
3975
4485
|
const cwd = process.cwd();
|
|
3976
4486
|
const files = [];
|
|
3977
4487
|
files.push({
|
|
@@ -4000,24 +4510,24 @@ function createInitCommand() {
|
|
|
4000
4510
|
let skipped = 0;
|
|
4001
4511
|
for (const file of files) {
|
|
4002
4512
|
if (existsSync4(file.path) && !options.force) {
|
|
4003
|
-
console.log(
|
|
4513
|
+
console.log(chalk7.yellow(` \u26A0 Skipped: ${file.name} (already exists, use --force to overwrite)`));
|
|
4004
4514
|
skipped++;
|
|
4005
4515
|
} else {
|
|
4006
|
-
|
|
4007
|
-
console.log(
|
|
4516
|
+
writeFileSync4(file.path, file.content);
|
|
4517
|
+
console.log(chalk7.green(` \u2713 Created: ${file.name}`));
|
|
4008
4518
|
created++;
|
|
4009
4519
|
}
|
|
4010
4520
|
}
|
|
4011
4521
|
console.log("");
|
|
4012
4522
|
if (created > 0) {
|
|
4013
|
-
console.log(
|
|
4014
|
-
console.log(
|
|
4015
|
-
console.log(
|
|
4016
|
-
console.log(
|
|
4017
|
-
console.log(
|
|
4523
|
+
console.log(chalk7.gray(" Next steps:"));
|
|
4524
|
+
console.log(chalk7.gray(" 1. Copy .env.example to .env and fill in your values"));
|
|
4525
|
+
console.log(chalk7.gray(" 2. Update the config file with your agent endpoint"));
|
|
4526
|
+
console.log(chalk7.gray(" 3. Run `agent-health doctor` to verify configuration"));
|
|
4527
|
+
console.log(chalk7.gray(" 4. Run `agent-health run -t sample-rca-001` to test\n"));
|
|
4018
4528
|
}
|
|
4019
4529
|
if (skipped > 0) {
|
|
4020
|
-
console.log(
|
|
4530
|
+
console.log(chalk7.yellow(` ${skipped} file(s) skipped. Use --force to overwrite.
|
|
4021
4531
|
`));
|
|
4022
4532
|
}
|
|
4023
4533
|
});
|
|
@@ -4025,9 +4535,9 @@ function createInitCommand() {
|
|
|
4025
4535
|
}
|
|
4026
4536
|
|
|
4027
4537
|
// cli/commands/migrate.ts
|
|
4028
|
-
import { Command as
|
|
4029
|
-
import
|
|
4030
|
-
import
|
|
4538
|
+
import { Command as Command8 } from "commander";
|
|
4539
|
+
import chalk8 from "chalk";
|
|
4540
|
+
import ora4 from "ora";
|
|
4031
4541
|
function computeStatsFromReports(run, reports) {
|
|
4032
4542
|
const reportsMap = new Map(reports.map((r) => [r.id, r]));
|
|
4033
4543
|
let passed = 0;
|
|
@@ -4065,26 +4575,26 @@ function computeStatsFromReports(run, reports) {
|
|
|
4065
4575
|
return { passed, failed, pending, total };
|
|
4066
4576
|
}
|
|
4067
4577
|
function createMigrateCommand() {
|
|
4068
|
-
const command = new
|
|
4069
|
-
console.log(
|
|
4578
|
+
const command = new Command8("migrate").description("One-time migration to add stats to existing benchmark runs").option("--dry-run", "Show what would be migrated without making changes").option("-v, --verbose", "Show detailed progress").action(async (options) => {
|
|
4579
|
+
console.log(chalk8.cyan.bold("\n Benchmark Stats Migration\n"));
|
|
4070
4580
|
const config = await loadConfig();
|
|
4071
4581
|
const serverResult = await ensureServer(config.server);
|
|
4072
4582
|
const cleanup = createServerCleanup(serverResult, config.server.reuseExistingServer === false);
|
|
4073
4583
|
try {
|
|
4074
4584
|
const client = new ApiClient(serverResult.baseUrl);
|
|
4075
|
-
const spinner =
|
|
4585
|
+
const spinner = ora4("Fetching benchmarks...").start();
|
|
4076
4586
|
const benchmarks = await client.listBenchmarks();
|
|
4077
4587
|
spinner.succeed(`Found ${benchmarks.length} benchmarks`);
|
|
4078
4588
|
const migratable = benchmarks.filter(
|
|
4079
4589
|
(b) => !b.id.startsWith("demo-") && (b.runs?.length ?? 0) > 0
|
|
4080
4590
|
);
|
|
4081
4591
|
if (migratable.length === 0) {
|
|
4082
|
-
console.log(
|
|
4083
|
-
console.log(
|
|
4084
|
-
console.log(
|
|
4592
|
+
console.log(chalk8.yellow("\n No benchmarks to migrate.\n"));
|
|
4593
|
+
console.log(chalk8.gray(" Only user-created benchmarks with runs can be migrated."));
|
|
4594
|
+
console.log(chalk8.gray(" Sample data (demo-*) already has stats computed.\n"));
|
|
4085
4595
|
return;
|
|
4086
4596
|
}
|
|
4087
|
-
console.log(
|
|
4597
|
+
console.log(chalk8.gray(`
|
|
4088
4598
|
Migrating ${migratable.length} benchmarks with runs...
|
|
4089
4599
|
`));
|
|
4090
4600
|
let totalRuns = 0;
|
|
@@ -4095,13 +4605,13 @@ function createMigrateCommand() {
|
|
|
4095
4605
|
const runs = benchmark.runs || [];
|
|
4096
4606
|
totalRuns += runs.length;
|
|
4097
4607
|
if (options.verbose) {
|
|
4098
|
-
console.log(
|
|
4608
|
+
console.log(chalk8.gray(` Processing: ${benchmark.name} (${runs.length} runs)`));
|
|
4099
4609
|
}
|
|
4100
4610
|
for (const run of runs) {
|
|
4101
4611
|
if (run.stats && typeof run.stats.passed === "number") {
|
|
4102
4612
|
skippedRuns++;
|
|
4103
4613
|
if (options.verbose) {
|
|
4104
|
-
console.log(
|
|
4614
|
+
console.log(chalk8.gray(` \u2713 ${run.name} - already has stats`));
|
|
4105
4615
|
}
|
|
4106
4616
|
continue;
|
|
4107
4617
|
}
|
|
@@ -4115,7 +4625,7 @@ function createMigrateCommand() {
|
|
|
4115
4625
|
const { runs: reports } = await reportsRes.json();
|
|
4116
4626
|
const stats = computeStatsFromReports(run, reports || []);
|
|
4117
4627
|
if (options.verbose) {
|
|
4118
|
-
console.log(
|
|
4628
|
+
console.log(chalk8.gray(
|
|
4119
4629
|
` \u2192 ${run.name}: passed=${stats.passed}, failed=${stats.failed}, pending=${stats.pending}`
|
|
4120
4630
|
));
|
|
4121
4631
|
}
|
|
@@ -4138,30 +4648,30 @@ function createMigrateCommand() {
|
|
|
4138
4648
|
errors++;
|
|
4139
4649
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
4140
4650
|
if (options.verbose) {
|
|
4141
|
-
console.log(
|
|
4651
|
+
console.log(chalk8.red(` \u2717 ${run.name} - ${msg}`));
|
|
4142
4652
|
}
|
|
4143
4653
|
}
|
|
4144
4654
|
}
|
|
4145
4655
|
console.log(
|
|
4146
|
-
options.dryRun ?
|
|
4656
|
+
options.dryRun ? chalk8.blue(` [DRY RUN] ${benchmark.name} - ${runs.length} runs would be processed`) : chalk8.green(` \u2713 ${benchmark.name} - ${runs.length} runs`)
|
|
4147
4657
|
);
|
|
4148
4658
|
}
|
|
4149
|
-
console.log(
|
|
4150
|
-
console.log(
|
|
4151
|
-
console.log(
|
|
4152
|
-
console.log(
|
|
4659
|
+
console.log(chalk8.bold("\n Migration Summary\n"));
|
|
4660
|
+
console.log(chalk8.gray(` Total runs: ${totalRuns}`));
|
|
4661
|
+
console.log(chalk8.green(` Migrated: ${migratedRuns}`));
|
|
4662
|
+
console.log(chalk8.yellow(` Already done: ${skippedRuns}`));
|
|
4153
4663
|
if (errors > 0) {
|
|
4154
|
-
console.log(
|
|
4664
|
+
console.log(chalk8.red(` Errors: ${errors}`));
|
|
4155
4665
|
}
|
|
4156
4666
|
if (options.dryRun) {
|
|
4157
|
-
console.log(
|
|
4158
|
-
console.log(
|
|
4667
|
+
console.log(chalk8.blue("\n This was a dry run. No changes were made."));
|
|
4668
|
+
console.log(chalk8.blue(" Run without --dry-run to apply changes.\n"));
|
|
4159
4669
|
} else {
|
|
4160
|
-
console.log(
|
|
4670
|
+
console.log(chalk8.green("\n Migration complete!\n"));
|
|
4161
4671
|
}
|
|
4162
4672
|
} catch (error) {
|
|
4163
4673
|
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
4164
|
-
console.error(
|
|
4674
|
+
console.error(chalk8.red(`
|
|
4165
4675
|
Error: ${msg}
|
|
4166
4676
|
`));
|
|
4167
4677
|
process.exit(1);
|
|
@@ -4172,6 +4682,222 @@ function createMigrateCommand() {
|
|
|
4172
4682
|
return command;
|
|
4173
4683
|
}
|
|
4174
4684
|
|
|
4685
|
+
// cli/commands/compare-services.ts
|
|
4686
|
+
import { Command as Command9 } from "commander";
|
|
4687
|
+
import chalk9 from "chalk";
|
|
4688
|
+
function analyzeErrorPatterns(spans) {
|
|
4689
|
+
const errorSpans = spans.filter((s) => s.status === "ERROR");
|
|
4690
|
+
if (errorSpans.length === 0) {
|
|
4691
|
+
return { errorSpans, patterns: [], avgDurationMs: 0 };
|
|
4692
|
+
}
|
|
4693
|
+
const patternMap = /* @__PURE__ */ new Map();
|
|
4694
|
+
let totalDuration = 0;
|
|
4695
|
+
for (const span of errorSpans) {
|
|
4696
|
+
const errorType = extractErrorType(span);
|
|
4697
|
+
const spanName = span.name || "Unknown";
|
|
4698
|
+
const duration = span.duration || 0;
|
|
4699
|
+
const errorMsg = extractErrorMessage(span);
|
|
4700
|
+
totalDuration += duration;
|
|
4701
|
+
if (!patternMap.has(errorType)) {
|
|
4702
|
+
patternMap.set(errorType, {
|
|
4703
|
+
count: 0,
|
|
4704
|
+
spanNames: /* @__PURE__ */ new Set(),
|
|
4705
|
+
durations: [],
|
|
4706
|
+
messages: /* @__PURE__ */ new Set()
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
const pattern = patternMap.get(errorType);
|
|
4710
|
+
pattern.count++;
|
|
4711
|
+
pattern.spanNames.add(spanName);
|
|
4712
|
+
pattern.durations.push(duration);
|
|
4713
|
+
if (errorMsg) pattern.messages.add(errorMsg);
|
|
4714
|
+
}
|
|
4715
|
+
const patterns = Array.from(patternMap.entries()).map(([errorType, data]) => ({
|
|
4716
|
+
errorType,
|
|
4717
|
+
count: data.count,
|
|
4718
|
+
spanNames: Array.from(data.spanNames),
|
|
4719
|
+
avgDurationMs: data.durations.reduce((a, b) => a + b, 0) / data.durations.length,
|
|
4720
|
+
exampleMessages: Array.from(data.messages).slice(0, 3)
|
|
4721
|
+
// Top 3 examples
|
|
4722
|
+
})).sort((a, b) => b.count - a.count);
|
|
4723
|
+
const avgDurationMs = totalDuration / errorSpans.length;
|
|
4724
|
+
return { errorSpans, patterns, avgDurationMs };
|
|
4725
|
+
}
|
|
4726
|
+
function extractErrorType(span) {
|
|
4727
|
+
const attrs = span.attributes || {};
|
|
4728
|
+
if (attrs["error.type"]) return attrs["error.type"];
|
|
4729
|
+
if (attrs["exception.type"]) return attrs["exception.type"];
|
|
4730
|
+
if (attrs["http.status_code"] >= 400) return `HTTP ${attrs["http.status_code"]}`;
|
|
4731
|
+
const name = span.name || "";
|
|
4732
|
+
if (name.includes("timeout")) return "Timeout";
|
|
4733
|
+
if (name.includes("connection")) return "Connection Error";
|
|
4734
|
+
if (name.includes("auth")) return "Authentication Error";
|
|
4735
|
+
return "Unknown Error";
|
|
4736
|
+
}
|
|
4737
|
+
function extractErrorMessage(span) {
|
|
4738
|
+
const attrs = span.attributes || {};
|
|
4739
|
+
if (attrs["error.message"]) return attrs["error.message"];
|
|
4740
|
+
if (attrs["exception.message"]) return attrs["exception.message"];
|
|
4741
|
+
if (span.events && Array.isArray(span.events)) {
|
|
4742
|
+
for (const event of span.events) {
|
|
4743
|
+
if (event.name === "exception" && event.attributes?.["exception.message"]) {
|
|
4744
|
+
return event.attributes["exception.message"];
|
|
4745
|
+
}
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
return null;
|
|
4749
|
+
}
|
|
4750
|
+
async function analyzeServiceErrors(client, serviceName, startTime, endTime, limit = 1e3) {
|
|
4751
|
+
console.log(chalk9.gray(`
|
|
4752
|
+
Fetching traces for service: ${serviceName}...`));
|
|
4753
|
+
const response = await client.fetchTraces({
|
|
4754
|
+
serviceName,
|
|
4755
|
+
startTime,
|
|
4756
|
+
endTime,
|
|
4757
|
+
size: limit
|
|
4758
|
+
});
|
|
4759
|
+
const spans = response.spans || [];
|
|
4760
|
+
console.log(chalk9.gray(` Found ${spans.length} spans`));
|
|
4761
|
+
const traceMap = /* @__PURE__ */ new Map();
|
|
4762
|
+
for (const span of spans) {
|
|
4763
|
+
if (!traceMap.has(span.traceId)) {
|
|
4764
|
+
traceMap.set(span.traceId, []);
|
|
4765
|
+
}
|
|
4766
|
+
traceMap.get(span.traceId).push(span);
|
|
4767
|
+
}
|
|
4768
|
+
const totalTraces = traceMap.size;
|
|
4769
|
+
let tracesWithErrors = 0;
|
|
4770
|
+
for (const traceSpans of traceMap.values()) {
|
|
4771
|
+
if (traceSpans.some((s) => s.status === "ERROR")) {
|
|
4772
|
+
tracesWithErrors++;
|
|
4773
|
+
}
|
|
4774
|
+
}
|
|
4775
|
+
const errorRate = totalTraces > 0 ? tracesWithErrors / totalTraces * 100 : 0;
|
|
4776
|
+
const { errorSpans, patterns, avgDurationMs } = analyzeErrorPatterns(spans);
|
|
4777
|
+
return {
|
|
4778
|
+
serviceName,
|
|
4779
|
+
totalTraces,
|
|
4780
|
+
tracesWithErrors,
|
|
4781
|
+
errorRate,
|
|
4782
|
+
totalErrorSpans: errorSpans.length,
|
|
4783
|
+
errorPatterns: patterns,
|
|
4784
|
+
avgErrorDurationMs: avgDurationMs
|
|
4785
|
+
};
|
|
4786
|
+
}
|
|
4787
|
+
function printServiceAnalysis(analysis) {
|
|
4788
|
+
console.log(chalk9.bold.cyan(`
|
|
4789
|
+
${"=".repeat(60)}`));
|
|
4790
|
+
console.log(chalk9.bold.cyan(`Service: ${analysis.serviceName}`));
|
|
4791
|
+
console.log(chalk9.bold.cyan("=".repeat(60)));
|
|
4792
|
+
console.log(chalk9.white(`Total Traces: ${analysis.totalTraces}`));
|
|
4793
|
+
console.log(chalk9.white(`Traces with Errors: ${analysis.tracesWithErrors}`));
|
|
4794
|
+
const errorRateColor = analysis.errorRate > 10 ? chalk9.red : analysis.errorRate > 5 ? chalk9.yellow : chalk9.green;
|
|
4795
|
+
console.log(errorRateColor(`Error Rate: ${analysis.errorRate.toFixed(2)}%`));
|
|
4796
|
+
console.log(chalk9.white(`Total Error Spans: ${analysis.totalErrorSpans}`));
|
|
4797
|
+
console.log(chalk9.white(`Avg Error Span Duration: ${analysis.avgErrorDurationMs.toFixed(2)}ms`));
|
|
4798
|
+
if (analysis.errorPatterns.length > 0) {
|
|
4799
|
+
console.log(chalk9.bold.white("\nError Patterns:"));
|
|
4800
|
+
for (const pattern of analysis.errorPatterns) {
|
|
4801
|
+
console.log(chalk9.yellow(`
|
|
4802
|
+
\u2022 ${pattern.errorType}`));
|
|
4803
|
+
console.log(chalk9.gray(` Count: ${pattern.count}`));
|
|
4804
|
+
console.log(chalk9.gray(` Avg Duration: ${pattern.avgDurationMs.toFixed(2)}ms`));
|
|
4805
|
+
console.log(chalk9.gray(` Affected Spans: ${pattern.spanNames.join(", ")}`));
|
|
4806
|
+
if (pattern.exampleMessages.length > 0) {
|
|
4807
|
+
console.log(chalk9.gray(` Example Messages:`));
|
|
4808
|
+
pattern.exampleMessages.forEach((msg) => {
|
|
4809
|
+
console.log(chalk9.gray(` - ${msg.substring(0, 80)}${msg.length > 80 ? "..." : ""}`));
|
|
4810
|
+
});
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
} else {
|
|
4814
|
+
console.log(chalk9.green("\n\u2713 No error patterns detected"));
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
function printComparison(service1, service2) {
|
|
4818
|
+
console.log(chalk9.bold.magenta(`
|
|
4819
|
+
${"=".repeat(60)}`));
|
|
4820
|
+
console.log(chalk9.bold.magenta("COMPARISON SUMMARY"));
|
|
4821
|
+
console.log(chalk9.bold.magenta("=".repeat(60)));
|
|
4822
|
+
const errorRateDiff = service1.errorRate - service2.errorRate;
|
|
4823
|
+
const diffColor = Math.abs(errorRateDiff) < 1 ? chalk9.white : errorRateDiff > 0 ? chalk9.red : chalk9.green;
|
|
4824
|
+
const diffSymbol = errorRateDiff > 0 ? "\u2191" : errorRateDiff < 0 ? "\u2193" : "=";
|
|
4825
|
+
console.log(chalk9.bold.white("\nError Rate:"));
|
|
4826
|
+
console.log(` ${service1.serviceName}: ${service1.errorRate.toFixed(2)}%`);
|
|
4827
|
+
console.log(` ${service2.serviceName}: ${service2.errorRate.toFixed(2)}%`);
|
|
4828
|
+
console.log(diffColor(` Difference: ${diffSymbol} ${Math.abs(errorRateDiff).toFixed(2)}%`));
|
|
4829
|
+
console.log(chalk9.bold.white("\nUnique Error Patterns:"));
|
|
4830
|
+
const patterns1 = new Set(service1.errorPatterns.map((p) => p.errorType));
|
|
4831
|
+
const patterns2 = new Set(service2.errorPatterns.map((p) => p.errorType));
|
|
4832
|
+
const onlyIn1 = Array.from(patterns1).filter((p) => !patterns2.has(p));
|
|
4833
|
+
const onlyIn2 = Array.from(patterns2).filter((p) => !patterns1.has(p));
|
|
4834
|
+
const inBoth = Array.from(patterns1).filter((p) => patterns2.has(p));
|
|
4835
|
+
if (onlyIn1.length > 0) {
|
|
4836
|
+
console.log(chalk9.cyan(`
|
|
4837
|
+
Only in ${service1.serviceName}:`));
|
|
4838
|
+
onlyIn1.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
|
|
4839
|
+
}
|
|
4840
|
+
if (onlyIn2.length > 0) {
|
|
4841
|
+
console.log(chalk9.cyan(`
|
|
4842
|
+
Only in ${service2.serviceName}:`));
|
|
4843
|
+
onlyIn2.forEach((p) => console.log(chalk9.gray(` \u2022 ${p}`)));
|
|
4844
|
+
}
|
|
4845
|
+
if (inBoth.length > 0) {
|
|
4846
|
+
console.log(chalk9.cyan(`
|
|
4847
|
+
Common error patterns:`));
|
|
4848
|
+
inBoth.forEach((p) => {
|
|
4849
|
+
const count1 = service1.errorPatterns.find((x) => x.errorType === p)?.count || 0;
|
|
4850
|
+
const count2 = service2.errorPatterns.find((x) => x.errorType === p)?.count || 0;
|
|
4851
|
+
console.log(chalk9.gray(` \u2022 ${p}: ${count1} vs ${count2}`));
|
|
4852
|
+
});
|
|
4853
|
+
}
|
|
4854
|
+
console.log(chalk9.bold.yellow("\nRecommendations:"));
|
|
4855
|
+
if (service1.errorRate > service2.errorRate * 1.5) {
|
|
4856
|
+
console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has significantly higher error rate - investigate urgently`));
|
|
4857
|
+
} else if (service2.errorRate > service1.errorRate * 1.5) {
|
|
4858
|
+
console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has significantly higher error rate - investigate urgently`));
|
|
4859
|
+
} else {
|
|
4860
|
+
console.log(chalk9.green(` \u2713 Error rates are comparable`));
|
|
4861
|
+
}
|
|
4862
|
+
if (onlyIn1.length > 2) {
|
|
4863
|
+
console.log(chalk9.yellow(` \u26A0 ${service1.serviceName} has ${onlyIn1.length} unique error types - review configuration`));
|
|
4864
|
+
}
|
|
4865
|
+
if (onlyIn2.length > 2) {
|
|
4866
|
+
console.log(chalk9.yellow(` \u26A0 ${service2.serviceName} has ${onlyIn2.length} unique error types - review configuration`));
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
function createCompareServicesCommand() {
|
|
4870
|
+
const cmd = new Command9("compare-services");
|
|
4871
|
+
cmd.description("Compare error patterns between two services from trace data").requiredOption("-s, --services <service1,service2>", 'Comma-separated service names (e.g., "lambda-api,eks-api")').option("--start <time>", 'Start time (ISO 8601 format or relative like "1h", "24h")').option("--end <time>", "End time (ISO 8601 format)").option("--limit <number>", "Maximum number of spans to fetch per service", "1000").action(async (options) => {
|
|
4872
|
+
try {
|
|
4873
|
+
const config = await loadConfig();
|
|
4874
|
+
const serverResult = await ensureServer(config.server);
|
|
4875
|
+
const client = new ApiClient(serverResult.baseUrl);
|
|
4876
|
+
const serviceNames = options.services.split(",").map((s) => s.trim());
|
|
4877
|
+
if (serviceNames.length !== 2) {
|
|
4878
|
+
console.error(chalk9.red("Error: Please provide exactly two service names"));
|
|
4879
|
+
process.exit(1);
|
|
4880
|
+
}
|
|
4881
|
+
const [service1Name, service2Name] = serviceNames;
|
|
4882
|
+
const limit = parseInt(options.limit, 10);
|
|
4883
|
+
console.log(chalk9.bold.cyan("\nComparing Error Patterns Between Services"));
|
|
4884
|
+
console.log(chalk9.gray(`Service 1: ${service1Name}`));
|
|
4885
|
+
console.log(chalk9.gray(`Service 2: ${service2Name}`));
|
|
4886
|
+
if (options.start) console.log(chalk9.gray(`Time Range: ${options.start} to ${options.end || "now"}`));
|
|
4887
|
+
const analysis1 = await analyzeServiceErrors(client, service1Name, options.start, options.end, limit);
|
|
4888
|
+
const analysis2 = await analyzeServiceErrors(client, service2Name, options.start, options.end, limit);
|
|
4889
|
+
printServiceAnalysis(analysis1);
|
|
4890
|
+
printServiceAnalysis(analysis2);
|
|
4891
|
+
printComparison(analysis1, analysis2);
|
|
4892
|
+
console.log();
|
|
4893
|
+
} catch (error) {
|
|
4894
|
+
console.error(chalk9.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
|
|
4895
|
+
process.exit(1);
|
|
4896
|
+
}
|
|
4897
|
+
});
|
|
4898
|
+
return cmd;
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4175
4901
|
// cli/index.ts
|
|
4176
4902
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
4177
4903
|
var __dirname3 = dirname3(__filename3);
|
|
@@ -4185,59 +4911,112 @@ try {
|
|
|
4185
4911
|
function loadEnvFile(envPath) {
|
|
4186
4912
|
const absolutePath = resolve4(process.cwd(), envPath);
|
|
4187
4913
|
if (!existsSync5(absolutePath)) {
|
|
4188
|
-
console.error(
|
|
4914
|
+
console.error(chalk10.red(`
|
|
4189
4915
|
Error: Environment file not found: ${absolutePath}
|
|
4190
4916
|
`));
|
|
4191
4917
|
process.exit(1);
|
|
4192
4918
|
}
|
|
4193
4919
|
const result = loadDotenv({ path: absolutePath });
|
|
4194
4920
|
if (result.error) {
|
|
4195
|
-
console.error(
|
|
4921
|
+
console.error(chalk10.red(`
|
|
4196
4922
|
Error loading environment file: ${result.error.message}
|
|
4197
4923
|
`));
|
|
4198
4924
|
process.exit(1);
|
|
4199
4925
|
}
|
|
4200
|
-
console.log(
|
|
4926
|
+
console.log(chalk10.gray(` Loaded environment from: ${envPath}`));
|
|
4201
4927
|
}
|
|
4202
4928
|
var defaultEnvPath = resolve4(process.cwd(), ".env");
|
|
4203
4929
|
if (existsSync5(defaultEnvPath)) {
|
|
4204
4930
|
loadDotenv({ path: defaultEnvPath });
|
|
4205
4931
|
}
|
|
4206
|
-
var program = new
|
|
4207
|
-
program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions()
|
|
4932
|
+
var program = new Command10();
|
|
4933
|
+
program.name("agent-health").description("Agent Health Evaluation Framework - Evaluate and monitor AI agent performance").version(version).enablePositionalOptions().passThroughOptions().configureHelp({
|
|
4934
|
+
sortSubcommands: false,
|
|
4935
|
+
// Hide default command list — replaced by grouped custom help below
|
|
4936
|
+
subcommandTerm: () => "",
|
|
4937
|
+
formatHelp: (cmd, helper) => {
|
|
4938
|
+
const termWidth = helper.padWidth(cmd, helper);
|
|
4939
|
+
const helpWidth = helper.helpWidth || 80;
|
|
4940
|
+
const output = [];
|
|
4941
|
+
const desc = helper.commandDescription(cmd);
|
|
4942
|
+
if (desc) {
|
|
4943
|
+
output.push(desc, "");
|
|
4944
|
+
}
|
|
4945
|
+
output.push(`${chalk10.cyan.bold("Usage:")} ${helper.commandUsage(cmd)}`, "");
|
|
4946
|
+
const optionList = helper.visibleOptions(cmd).map((opt) => {
|
|
4947
|
+
const term = helper.optionTerm(opt);
|
|
4948
|
+
const desc2 = helper.optionDescription(opt);
|
|
4949
|
+
return ` ${term.padEnd(termWidth)} ${desc2}`;
|
|
4950
|
+
}).join("\n");
|
|
4951
|
+
if (optionList) {
|
|
4952
|
+
output.push(`${chalk10.cyan.bold("Options:")}`, optionList, "");
|
|
4953
|
+
}
|
|
4954
|
+
return output.join("\n");
|
|
4955
|
+
}
|
|
4956
|
+
});
|
|
4957
|
+
program.addHelpText("after", `
|
|
4958
|
+
${chalk10.cyan.bold("Getting Started:")}
|
|
4959
|
+
${chalk10.yellow("agent-health")} Launch the web UI and evaluation server
|
|
4960
|
+
${chalk10.yellow("agent-health init")} Generate an agent-health.config.ts file
|
|
4961
|
+
${chalk10.yellow("agent-health doctor")} Verify your setup (AWS creds, OpenSearch, agents)
|
|
4962
|
+
|
|
4963
|
+
${chalk10.cyan.bold("Running Evaluations:")}
|
|
4964
|
+
${chalk10.yellow("agent-health run")} ${chalk10.gray("-t <case> -a <agent>")} Run a single test case against an agent
|
|
4965
|
+
${chalk10.yellow("agent-health benchmark")} ${chalk10.gray("-f <file>")} Run a full benchmark from a test cases JSON file
|
|
4966
|
+
${chalk10.yellow("agent-health benchmark")} ${chalk10.gray("-b <id>")} Re-run an existing benchmark
|
|
4967
|
+
|
|
4968
|
+
${chalk10.cyan.bold("Viewing Results:")}
|
|
4969
|
+
${chalk10.yellow("agent-health list")} ${chalk10.gray("agents|benchmarks|...")} List agents, connectors, test cases, or benchmarks
|
|
4970
|
+
${chalk10.yellow("agent-health report")} ${chalk10.gray("-b <benchmark>")} Generate an HTML/PDF/JSON report
|
|
4971
|
+
${chalk10.yellow("agent-health export")} ${chalk10.gray("-b <benchmark>")} Export test cases as re-importable JSON
|
|
4972
|
+
${chalk10.yellow("agent-health compare-services")} ${chalk10.gray("-s A B")} Compare error patterns between services
|
|
4973
|
+
|
|
4974
|
+
${chalk10.cyan.bold("Maintenance:")}
|
|
4975
|
+
${chalk10.yellow("agent-health migrate")} Migrate legacy benchmark data to current format
|
|
4976
|
+
${chalk10.yellow("agent-health serve")} Start the server (same as default, explicit command)
|
|
4977
|
+
|
|
4978
|
+
${chalk10.cyan.bold("Examples:")}
|
|
4979
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health
|
|
4980
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health --port 8080 --no-browser
|
|
4981
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health run -t "RCA for 500 errors" -a langgraph
|
|
4982
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health benchmark -f ./test-cases.json -a my-agent
|
|
4983
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health list agents
|
|
4984
|
+
${chalk10.gray("$")} npx @opensearch-project/agent-health report -b bench-123 -f pdf -o report.pdf
|
|
4985
|
+
`);
|
|
4208
4986
|
program.option("-p, --port <number>", "Server port", "4001").option("-e, --env-file <path>", "Load environment variables from file (e.g., .env)").option("--no-browser", "Do not open browser automatically");
|
|
4209
4987
|
program.action(async (options) => {
|
|
4210
|
-
console.log(
|
|
4988
|
+
console.log(chalk10.cyan.bold(`
|
|
4211
4989
|
Agent Health v${version} - AI Agent Evaluation Framework
|
|
4212
4990
|
`));
|
|
4213
|
-
console.log(
|
|
4214
|
-
console.log(
|
|
4991
|
+
console.log(chalk10.gray(` Working directory: ${process.cwd()}`));
|
|
4992
|
+
console.log(chalk10.gray(` Package directory: ${__dirname3}`));
|
|
4215
4993
|
if (options.envFile) {
|
|
4216
4994
|
loadEnvFile(options.envFile);
|
|
4217
4995
|
} else if (existsSync5(defaultEnvPath)) {
|
|
4218
|
-
console.log(
|
|
4996
|
+
console.log(chalk10.gray(" Auto-loaded .env from current directory"));
|
|
4219
4997
|
}
|
|
4220
4998
|
const port = parseInt(options.port, 10);
|
|
4221
|
-
const spinner =
|
|
4999
|
+
const spinner = ora5("Starting server...").start();
|
|
4222
5000
|
try {
|
|
4223
5001
|
await startServer({ port });
|
|
4224
5002
|
spinner.succeed("Server started");
|
|
4225
|
-
console.log(
|
|
4226
|
-
console.log(
|
|
4227
|
-
console.log(
|
|
4228
|
-
console.log(
|
|
5003
|
+
console.log(chalk10.gray("\n Configuration:"));
|
|
5004
|
+
console.log(chalk10.gray(` Storage: Sample data (configure OpenSearch for persistence)`));
|
|
5005
|
+
console.log(chalk10.gray(` Agent: Select in UI (Demo Agent for mock, real agents require endpoints)`));
|
|
5006
|
+
console.log(chalk10.gray(` Judge: Select in UI (Demo Judge for mock, Bedrock requires AWS creds)
|
|
4229
5007
|
`));
|
|
4230
5008
|
const url = `http://localhost:${port}`;
|
|
4231
|
-
console.log(
|
|
5009
|
+
console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
|
|
4232
5010
|
`));
|
|
5011
|
+
console.log(chalk10.green(` Demo data loaded`));
|
|
4233
5012
|
if (options.browser !== false) {
|
|
4234
|
-
console.log(
|
|
5013
|
+
console.log(chalk10.gray(" Opening browser..."));
|
|
4235
5014
|
await open(url);
|
|
4236
5015
|
}
|
|
4237
|
-
console.log(
|
|
5016
|
+
console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
|
|
4238
5017
|
} catch (error) {
|
|
4239
5018
|
spinner.fail("Failed to start server");
|
|
4240
|
-
console.error(
|
|
5019
|
+
console.error(chalk10.red(`
|
|
4241
5020
|
Error: ${error instanceof Error ? error.message : error}
|
|
4242
5021
|
`));
|
|
4243
5022
|
process.exit(1);
|
|
@@ -4247,29 +5026,31 @@ program.addCommand(createListCommand());
|
|
|
4247
5026
|
program.addCommand(createRunCommand());
|
|
4248
5027
|
program.addCommand(createBenchmarkCommand());
|
|
4249
5028
|
program.addCommand(createExportCommand());
|
|
5029
|
+
program.addCommand(createReportCommand());
|
|
4250
5030
|
program.addCommand(createDoctorCommand());
|
|
4251
5031
|
program.addCommand(createInitCommand());
|
|
4252
5032
|
program.addCommand(createMigrateCommand());
|
|
5033
|
+
program.addCommand(createCompareServicesCommand());
|
|
4253
5034
|
program.command("serve").description("Start the Agent Health server (same as default action)").option("-p, --port <number>", "Server port", "4001").option("--no-browser", "Do not open browser automatically").action(async (options) => {
|
|
4254
|
-
console.log(
|
|
5035
|
+
console.log(chalk10.cyan.bold(`
|
|
4255
5036
|
Agent Health v${version} - AI Agent Evaluation Framework
|
|
4256
5037
|
`));
|
|
4257
5038
|
const port = parseInt(options.port, 10);
|
|
4258
|
-
const spinner =
|
|
5039
|
+
const spinner = ora5("Starting server...").start();
|
|
4259
5040
|
try {
|
|
4260
5041
|
await startServer({ port });
|
|
4261
5042
|
spinner.succeed("Server started");
|
|
4262
5043
|
const url = `http://localhost:${port}`;
|
|
4263
|
-
console.log(
|
|
5044
|
+
console.log(chalk10.green(` Server running at ${chalk10.bold(url)}
|
|
4264
5045
|
`));
|
|
4265
5046
|
if (options.browser !== false) {
|
|
4266
|
-
console.log(
|
|
5047
|
+
console.log(chalk10.gray(" Opening browser..."));
|
|
4267
5048
|
await open(url);
|
|
4268
5049
|
}
|
|
4269
|
-
console.log(
|
|
5050
|
+
console.log(chalk10.gray(" Press Ctrl+C to stop\n"));
|
|
4270
5051
|
} catch (error) {
|
|
4271
5052
|
spinner.fail("Failed to start server");
|
|
4272
|
-
console.error(
|
|
5053
|
+
console.error(chalk10.red(`
|
|
4273
5054
|
Error: ${error instanceof Error ? error.message : error}
|
|
4274
5055
|
`));
|
|
4275
5056
|
process.exit(1);
|
|
@@ -4278,15 +5059,15 @@ program.command("serve").description("Start the Agent Health server (same as def
|
|
|
4278
5059
|
program.on("command:*", (operands) => {
|
|
4279
5060
|
const unknownCommand = operands[0];
|
|
4280
5061
|
const availableCommands = program.commands.map((cmd) => cmd.name());
|
|
4281
|
-
console.error(
|
|
5062
|
+
console.error(chalk10.red(`
|
|
4282
5063
|
Error: Unknown command '${unknownCommand}'`));
|
|
4283
5064
|
console.log("");
|
|
4284
|
-
console.log(
|
|
5065
|
+
console.log(chalk10.cyan(" Available commands:"));
|
|
4285
5066
|
for (const cmd of availableCommands) {
|
|
4286
|
-
console.log(
|
|
5067
|
+
console.log(chalk10.gray(` - ${cmd}`));
|
|
4287
5068
|
}
|
|
4288
5069
|
console.log("");
|
|
4289
|
-
console.log(
|
|
5070
|
+
console.log(chalk10.gray(` Run ${chalk10.cyan("agent-health --help")} for usage information.
|
|
4290
5071
|
`));
|
|
4291
5072
|
process.exitCode = 1;
|
|
4292
5073
|
});
|