chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
import type { ClickHouse } from "@internal/clickhouse";
|
||||
import type { PrismaClientOrTransaction } from "~/db.server";
|
||||
import { BasePresenter } from "./basePresenter.server";
|
||||
import { z } from "zod";
|
||||
|
||||
const GenerationRowSchema = z.object({
|
||||
run_id: z.string(),
|
||||
span_id: z.string(),
|
||||
operation_id: z.string(),
|
||||
task_identifier: z.string(),
|
||||
response_model: z.string(),
|
||||
prompt_version: z.coerce.number(),
|
||||
input_tokens: z.coerce.number(),
|
||||
output_tokens: z.coerce.number(),
|
||||
total_cost: z.coerce.number(),
|
||||
duration_ms: z.coerce.number(),
|
||||
started_at: z.string(),
|
||||
});
|
||||
|
||||
export type GenerationRow = {
|
||||
run_id: string;
|
||||
span_id: string;
|
||||
operation_id: string;
|
||||
task_identifier: string;
|
||||
response_model: string;
|
||||
prompt_version: number;
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
total_cost: number;
|
||||
duration_ms: number;
|
||||
start_time: string;
|
||||
};
|
||||
|
||||
export type GenerationsPagination = {
|
||||
next?: string;
|
||||
};
|
||||
|
||||
export class PromptPresenter extends BasePresenter {
|
||||
private readonly clickhouse: ClickHouse;
|
||||
|
||||
constructor(clickhouse: ClickHouse, replica?: PrismaClientOrTransaction) {
|
||||
super(undefined, replica);
|
||||
this.clickhouse = clickhouse;
|
||||
}
|
||||
|
||||
async listPrompts(projectId: string, environmentId: string) {
|
||||
const prompts = await this._replica.prompt.findMany({
|
||||
where: {
|
||||
projectId,
|
||||
runtimeEnvironmentId: environmentId,
|
||||
archivedAt: null,
|
||||
},
|
||||
include: {
|
||||
versions: {
|
||||
where: {
|
||||
labels: { hasSome: ["current", "override"] },
|
||||
},
|
||||
select: {
|
||||
version: true,
|
||||
labels: true,
|
||||
model: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { updatedAt: "desc" },
|
||||
});
|
||||
|
||||
return prompts.map((p) => {
|
||||
const currentVersion = p.versions.find((v) => v.labels.includes("current"));
|
||||
const overrideVersion = p.versions.find((v) => v.labels.includes("override"));
|
||||
const hasOverride = !!overrideVersion;
|
||||
|
||||
// Effective model: override > current version > prompt default
|
||||
const effectiveModel = overrideVersion?.model ?? currentVersion?.model ?? p.defaultModel;
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
friendlyId: p.friendlyId,
|
||||
slug: p.slug,
|
||||
description: p.description,
|
||||
tags: p.tags,
|
||||
defaultModel: effectiveModel,
|
||||
currentVersion: currentVersion ? { version: currentVersion.version } : null,
|
||||
overrideVersion: overrideVersion ? { version: overrideVersion.version } : null,
|
||||
hasOverride,
|
||||
updatedAt: p.updatedAt,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getUsageSparklines(
|
||||
environmentId: string,
|
||||
promptSlugs: string[]
|
||||
): Promise<Record<string, number[]>> {
|
||||
if (promptSlugs.length === 0) return {};
|
||||
|
||||
const queryFn = this.clickhouse.reader.query({
|
||||
name: "promptUsageSparklines",
|
||||
query: `SELECT
|
||||
prompt_slug,
|
||||
toStartOfHour(start_time) AS bucket,
|
||||
count() AS cnt
|
||||
FROM trigger_dev.llm_metrics_v1
|
||||
WHERE environment_id = {environmentId: String}
|
||||
AND prompt_slug IN {promptSlugs: Array(String)}
|
||||
AND start_time >= now() - INTERVAL 24 HOUR
|
||||
GROUP BY prompt_slug, bucket
|
||||
ORDER BY prompt_slug, bucket`,
|
||||
params: z.object({
|
||||
environmentId: z.string(),
|
||||
promptSlugs: z.array(z.string()),
|
||||
}),
|
||||
schema: z.object({
|
||||
prompt_slug: z.string(),
|
||||
bucket: z.string(),
|
||||
cnt: z.coerce.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
const [error, rows] = await queryFn({ environmentId, promptSlugs });
|
||||
if (error) {
|
||||
console.error("Prompt usage sparkline query failed:", error);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Build a map of slug -> 24 hourly buckets (use UTC to match ClickHouse)
|
||||
const now = new Date();
|
||||
const startHour = new Date(
|
||||
Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate(),
|
||||
now.getUTCHours() - 23,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
const bucketKeys: string[] = [];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const h = new Date(startHour.getTime() + i * 3600_000);
|
||||
// Format to match ClickHouse's toStartOfHour output: "YYYY-MM-DD HH:MM:SS"
|
||||
bucketKeys.push(h.toISOString().slice(0, 13).replace("T", " ") + ":00:00");
|
||||
}
|
||||
|
||||
// Index rows by slug+bucket for fast lookup
|
||||
const rowMap = new Map<string, number>();
|
||||
for (const row of rows) {
|
||||
rowMap.set(`${row.prompt_slug}|${row.bucket}`, row.cnt);
|
||||
}
|
||||
|
||||
const result: Record<string, number[]> = {};
|
||||
for (const slug of promptSlugs) {
|
||||
result[slug] = bucketKeys.map((key) => rowMap.get(`${slug}|${key}`) ?? 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async resolveVersion(promptId: string, options?: { version?: number; label?: string }) {
|
||||
if (options?.version != null) {
|
||||
return this._replica.promptVersion.findUnique({
|
||||
where: {
|
||||
promptId_version: {
|
||||
promptId,
|
||||
version: options.version,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Check for override first — dashboard edits take precedence
|
||||
const override = await this._replica.promptVersion.findFirst({
|
||||
where: {
|
||||
promptId,
|
||||
labels: { has: "override" },
|
||||
},
|
||||
orderBy: { version: "desc" },
|
||||
});
|
||||
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
|
||||
const label = options?.label ?? "current";
|
||||
return this._replica.promptVersion.findFirst({
|
||||
where: {
|
||||
promptId,
|
||||
labels: { has: label },
|
||||
},
|
||||
orderBy: { version: "desc" },
|
||||
});
|
||||
}
|
||||
|
||||
async listVersions(promptId: string, limit: number = 50) {
|
||||
return this._replica.promptVersion.findMany({
|
||||
where: { promptId },
|
||||
orderBy: { version: "desc" },
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
version: true,
|
||||
labels: true,
|
||||
source: true,
|
||||
model: true,
|
||||
textContent: true,
|
||||
commitMessage: true,
|
||||
contentHash: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async listGenerations(options: {
|
||||
environmentId: string;
|
||||
promptSlug: string;
|
||||
promptVersions?: number[];
|
||||
startTime: Date;
|
||||
endTime: Date;
|
||||
cursor?: string;
|
||||
pageSize?: number;
|
||||
responseModels?: string[];
|
||||
operations?: string[];
|
||||
providers?: string[];
|
||||
}): Promise<{ generations: GenerationRow[]; pagination: GenerationsPagination }> {
|
||||
const pageSize = options.pageSize ?? 25;
|
||||
const decodedCursor = options.cursor ? decodeCursor(options.cursor) : null;
|
||||
|
||||
const cursorClause = decodedCursor
|
||||
? `AND (start_time < parseDateTimeBestEffort({cursorStartTime: String})
|
||||
OR (start_time = parseDateTimeBestEffort({cursorStartTime: String})
|
||||
AND span_id < {cursorSpanId: String}))`
|
||||
: "";
|
||||
|
||||
const versionClause = options.promptVersions?.length
|
||||
? `AND prompt_version IN {promptVersions: Array(UInt32)}`
|
||||
: "";
|
||||
const modelClause = options.responseModels?.length
|
||||
? `AND response_model IN {responseModels: Array(String)}`
|
||||
: "";
|
||||
const operationClause = options.operations?.length
|
||||
? `AND operation_id IN {operations: Array(String)}`
|
||||
: "";
|
||||
const providerClause = options.providers?.length
|
||||
? `AND gen_ai_system IN {providers: Array(String)}`
|
||||
: "";
|
||||
|
||||
// Build a unique query name based on which optional filters are active
|
||||
const filterKey = [
|
||||
decodedCursor ? "c" : "",
|
||||
options.promptVersions?.length ? "v" : "",
|
||||
options.responseModels?.length ? "m" : "",
|
||||
options.operations?.length ? "o" : "",
|
||||
options.providers?.length ? "p" : "",
|
||||
].join("");
|
||||
|
||||
const queryFn = this.clickhouse.reader.query({
|
||||
name: `promptGenerationsList${filterKey}`,
|
||||
query: `SELECT
|
||||
run_id, span_id, operation_id, task_identifier, response_model,
|
||||
prompt_version,
|
||||
input_tokens, output_tokens, total_cost,
|
||||
duration / 1000000 AS duration_ms,
|
||||
formatDateTime(start_time, '%Y-%m-%d %H:%i:%S') AS started_at
|
||||
FROM trigger_dev.llm_metrics_v1
|
||||
WHERE environment_id = {environmentId: String}
|
||||
AND prompt_slug = {promptSlug: String}
|
||||
${versionClause}
|
||||
AND start_time >= parseDateTimeBestEffort({startTime: String})
|
||||
AND start_time <= parseDateTimeBestEffort({endTime: String})
|
||||
${cursorClause}
|
||||
${modelClause}
|
||||
${operationClause}
|
||||
${providerClause}
|
||||
ORDER BY start_time DESC, span_id DESC
|
||||
LIMIT {fetchLimit: UInt32}`,
|
||||
params: z.object({
|
||||
environmentId: z.string(),
|
||||
promptSlug: z.string(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
fetchLimit: z.number(),
|
||||
...(decodedCursor
|
||||
? {
|
||||
cursorStartTime: z.string(),
|
||||
cursorSpanId: z.string(),
|
||||
}
|
||||
: {}),
|
||||
...(options.promptVersions?.length ? { promptVersions: z.array(z.number()) } : {}),
|
||||
...(options.responseModels?.length ? { responseModels: z.array(z.string()) } : {}),
|
||||
...(options.operations?.length ? { operations: z.array(z.string()) } : {}),
|
||||
...(options.providers?.length ? { providers: z.array(z.string()) } : {}),
|
||||
}),
|
||||
schema: GenerationRowSchema,
|
||||
});
|
||||
|
||||
const queryParams: Record<string, unknown> = {
|
||||
environmentId: options.environmentId,
|
||||
promptSlug: options.promptSlug,
|
||||
startTime: options.startTime.toISOString(),
|
||||
endTime: options.endTime.toISOString(),
|
||||
fetchLimit: pageSize + 1,
|
||||
};
|
||||
|
||||
if (decodedCursor) {
|
||||
queryParams.cursorStartTime = decodedCursor.startTime;
|
||||
queryParams.cursorSpanId = decodedCursor.spanId;
|
||||
}
|
||||
if (options.promptVersions?.length) {
|
||||
queryParams.promptVersions = options.promptVersions;
|
||||
}
|
||||
if (options.responseModels?.length) {
|
||||
queryParams.responseModels = options.responseModels;
|
||||
}
|
||||
if (options.operations?.length) {
|
||||
queryParams.operations = options.operations;
|
||||
}
|
||||
if (options.providers?.length) {
|
||||
queryParams.providers = options.providers;
|
||||
}
|
||||
|
||||
const [error, rows] = await queryFn(queryParams as any);
|
||||
|
||||
if (error) {
|
||||
console.error("Prompt generations query failed:", error);
|
||||
return { generations: [], pagination: {} };
|
||||
}
|
||||
|
||||
const hasMore = rows.length > pageSize;
|
||||
const pageRows = hasMore ? rows.slice(0, pageSize) : rows;
|
||||
|
||||
const generations: GenerationRow[] = pageRows.map((r) => ({
|
||||
...r,
|
||||
start_time: r.started_at,
|
||||
}));
|
||||
|
||||
const pagination: GenerationsPagination = {};
|
||||
if (hasMore) {
|
||||
const lastRow = pageRows[pageRows.length - 1];
|
||||
pagination.next = encodeCursor(lastRow.started_at, lastRow.span_id);
|
||||
}
|
||||
|
||||
return { generations, pagination };
|
||||
}
|
||||
|
||||
async getDistinctPromptSlugs(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
environmentId: string
|
||||
): Promise<string[]> {
|
||||
const queryFn = this.clickhouse.reader.query({
|
||||
name: "getDistinctPromptSlugs",
|
||||
query: `SELECT DISTINCT prompt_slug FROM trigger_dev.llm_metrics_v1 WHERE organization_id = {organizationId: String} AND project_id = {projectId: String} AND environment_id = {environmentId: String} AND prompt_slug != '' ORDER BY prompt_slug`,
|
||||
params: z.object({
|
||||
organizationId: z.string(),
|
||||
projectId: z.string(),
|
||||
environmentId: z.string(),
|
||||
}),
|
||||
schema: z.object({ prompt_slug: z.string() }),
|
||||
});
|
||||
|
||||
const [error, rows] = await queryFn({ organizationId, projectId, environmentId });
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return rows.map((r) => r.prompt_slug);
|
||||
}
|
||||
|
||||
async getDistinctOperations(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
environmentId: string
|
||||
): Promise<string[]> {
|
||||
const queryFn = this.clickhouse.reader.query({
|
||||
name: "getDistinctOperations",
|
||||
query: `SELECT DISTINCT operation_id FROM trigger_dev.llm_metrics_v1 WHERE organization_id = {organizationId: String} AND project_id = {projectId: String} AND environment_id = {environmentId: String} AND operation_id != '' ORDER BY operation_id`,
|
||||
params: z.object({
|
||||
organizationId: z.string(),
|
||||
projectId: z.string(),
|
||||
environmentId: z.string(),
|
||||
}),
|
||||
schema: z.object({ operation_id: z.string() }),
|
||||
});
|
||||
|
||||
const [error, rows] = await queryFn({ organizationId, projectId, environmentId });
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return rows.map((r) => r.operation_id);
|
||||
}
|
||||
|
||||
async getDistinctProviders(
|
||||
organizationId: string,
|
||||
projectId: string,
|
||||
environmentId: string
|
||||
): Promise<string[]> {
|
||||
const queryFn = this.clickhouse.reader.query({
|
||||
name: "getDistinctProviders",
|
||||
query: `SELECT DISTINCT gen_ai_system FROM trigger_dev.llm_metrics_v1 WHERE organization_id = {organizationId: String} AND project_id = {projectId: String} AND environment_id = {environmentId: String} AND gen_ai_system != '' ORDER BY gen_ai_system`,
|
||||
params: z.object({
|
||||
organizationId: z.string(),
|
||||
projectId: z.string(),
|
||||
environmentId: z.string(),
|
||||
}),
|
||||
schema: z.object({ gen_ai_system: z.string() }),
|
||||
});
|
||||
|
||||
const [error, rows] = await queryFn({ organizationId, projectId, environmentId });
|
||||
if (error) {
|
||||
return [];
|
||||
}
|
||||
return rows.map((r) => r.gen_ai_system);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeCursor(startTime: string, spanId: string): string {
|
||||
return Buffer.from(JSON.stringify({ s: startTime, i: spanId })).toString("base64");
|
||||
}
|
||||
|
||||
function decodeCursor(cursor: string): { startTime: string; spanId: string } | null {
|
||||
try {
|
||||
const parsed = JSON.parse(Buffer.from(cursor, "base64").toString("utf-8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
if (typeof parsed.s === "string" && typeof parsed.i === "string") {
|
||||
return { startTime: parsed.s, spanId: parsed.i };
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user