|
| 1 | +import { randomUUID } from "node:crypto"; |
| 2 | +import { mkdtempSync } from "node:fs"; |
| 3 | +import { tmpdir } from "node:os"; |
| 4 | +import { join } from "node:path"; |
| 5 | +import type { AgentPondConfig, S3ObjectStore } from "@agentpond/core"; |
| 6 | + |
| 7 | +export type PerfArgs = { |
| 8 | + traces: number; |
| 9 | + endpoint: string; |
| 10 | + bucket: string; |
| 11 | + accessKeyId: string; |
| 12 | + secretAccessKey: string; |
| 13 | + region: string; |
| 14 | + projectId: string; |
| 15 | + publicKey: string; |
| 16 | + secretKey: string; |
| 17 | + prefix: string; |
| 18 | + dbPath: string; |
| 19 | +}; |
| 20 | + |
| 21 | +const DEFAULT_TRACES = 100_000; |
| 22 | + |
| 23 | +export function parseArgs(argv: string[]): PerfArgs { |
| 24 | + const values = parseFlags(argv.filter((arg) => arg !== "--")); |
| 25 | + const runId = values["run-id"] ?? randomUUID(); |
| 26 | + const dbPath = |
| 27 | + values.db ?? |
| 28 | + join(mkdtempSync(join(tmpdir(), "agentpond-perf-")), "cache.duckdb"); |
| 29 | + |
| 30 | + const traces = integerFlag(values, "traces", DEFAULT_TRACES); |
| 31 | + if (traces < 1) throw new Error("--traces must be at least 1"); |
| 32 | + |
| 33 | + return { |
| 34 | + traces, |
| 35 | + endpoint: values.endpoint ?? "http://localhost:9000", |
| 36 | + bucket: values.bucket ?? "agentpond", |
| 37 | + accessKeyId: values["access-key-id"] ?? "minio", |
| 38 | + secretAccessKey: values["secret-access-key"] ?? "minio123", |
| 39 | + region: values.region ?? "us-east-1", |
| 40 | + projectId: values["project-id"] ?? "default-project", |
| 41 | + publicKey: values["public-key"] ?? "pk-agentpond", |
| 42 | + secretKey: values["secret-key"] ?? "sk-agentpond", |
| 43 | + prefix: normalizePrefix(values.prefix ?? `perf/${runId}`), |
| 44 | + dbPath, |
| 45 | + }; |
| 46 | +} |
| 47 | + |
| 48 | +export function buildConfig(args: PerfArgs): AgentPondConfig { |
| 49 | + return { |
| 50 | + projectId: args.projectId, |
| 51 | + dbPath: args.dbPath, |
| 52 | + s3: { |
| 53 | + bucket: args.bucket, |
| 54 | + prefix: args.prefix, |
| 55 | + endpoint: args.endpoint, |
| 56 | + region: args.region, |
| 57 | + accessKeyId: args.accessKeyId, |
| 58 | + secretAccessKey: args.secretAccessKey, |
| 59 | + forcePathStyle: true, |
| 60 | + }, |
| 61 | + auth: { |
| 62 | + projectId: args.projectId, |
| 63 | + publicKey: args.publicKey, |
| 64 | + secretKey: args.secretKey, |
| 65 | + }, |
| 66 | + }; |
| 67 | +} |
| 68 | + |
| 69 | +export function configureLangfuseEnv(address: string, args: PerfArgs): void { |
| 70 | + process.env.LANGFUSE_BASE_URL = address; |
| 71 | + process.env.LANGFUSE_PUBLIC_KEY = args.publicKey; |
| 72 | + process.env.LANGFUSE_SECRET_KEY = args.secretKey; |
| 73 | + process.env.LANGFUSE_RELEASE = "agentpond-perf"; |
| 74 | + process.env.LANGFUSE_ENVIRONMENT = "performance"; |
| 75 | +} |
| 76 | + |
| 77 | +export async function assertEmptyPrefix( |
| 78 | + store: S3ObjectStore, |
| 79 | + prefix: string, |
| 80 | +): Promise<void> { |
| 81 | + try { |
| 82 | + const keys = await store.listKeys(prefix); |
| 83 | + if (keys.length > 0) { |
| 84 | + throw new Error( |
| 85 | + `S3 prefix ${prefix} is not empty (${keys.length} existing objects)`, |
| 86 | + ); |
| 87 | + } |
| 88 | + } catch (error) { |
| 89 | + if (error instanceof Error && error.message.includes("is not empty")) { |
| 90 | + throw error; |
| 91 | + } |
| 92 | + const message = error instanceof Error ? error.message : String(error); |
| 93 | + throw new Error( |
| 94 | + `Could not access local S3 storage. Start MinIO and create the bucket first: docker compose up -d minio create-bucket. Cause: ${message}`, |
| 95 | + ); |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +export function runIdFromPrefix(prefix: string): string { |
| 100 | + const trimmed = prefix.replace(/\/$/, ""); |
| 101 | + return trimmed.slice(trimmed.lastIndexOf("/") + 1); |
| 102 | +} |
| 103 | + |
| 104 | +function parseFlags(argv: string[]): Record<string, string> { |
| 105 | + const flags: Record<string, string> = {}; |
| 106 | + for (let i = 0; i < argv.length; i += 1) { |
| 107 | + const arg = argv[i]; |
| 108 | + if (!arg.startsWith("--")) throw new Error(`Unexpected argument: ${arg}`); |
| 109 | + const [rawKey, inlineValue] = arg.slice(2).split("=", 2); |
| 110 | + const value = inlineValue ?? argv[i + 1]; |
| 111 | + if (!value || value.startsWith("--")) { |
| 112 | + throw new Error(`Missing value for --${rawKey}`); |
| 113 | + } |
| 114 | + flags[rawKey] = value; |
| 115 | + if (inlineValue === undefined) i += 1; |
| 116 | + } |
| 117 | + return flags; |
| 118 | +} |
| 119 | + |
| 120 | +function integerFlag( |
| 121 | + flags: Record<string, string>, |
| 122 | + name: string, |
| 123 | + defaultValue: number, |
| 124 | +): number { |
| 125 | + const raw = flags[name]; |
| 126 | + if (!raw) return defaultValue; |
| 127 | + const value = Number.parseInt(raw, 10); |
| 128 | + if (!Number.isFinite(value) || String(value) !== raw) { |
| 129 | + throw new Error(`--${name} must be an integer`); |
| 130 | + } |
| 131 | + return value; |
| 132 | +} |
| 133 | + |
| 134 | +function normalizePrefix(prefix: string): string { |
| 135 | + return prefix.endsWith("/") ? prefix : `${prefix}/`; |
| 136 | +} |
0 commit comments