SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
8.1 KB · 204 lines typescript
Raw Blame History
1import "server-only";2import { and, desc, eq, ilike, isNull, or, sql, type SQL } from "drizzle-orm";3import { getDb, prompts, projects, type Prompt } from "@/db";4import { ids } from "@/lib/ids";5import { ApiError } from "@/lib/api";6import { mergeVariables, renderTemplate, type PromptVariable } from "./variables";78export const PROMPT_KINDS = ["system", "user", "template", "structured"] as const;9export type PromptKind = (typeof PROMPT_KINDS)[number];1011export function toPublicPrompt(p: Prompt) {12  return {13    id: p.id,14    projectId: p.projectId,15    kind: (PROMPT_KINDS.includes(p.kind as PromptKind) ? p.kind : "user") as PromptKind,16    name: p.name,17    description: p.description,18    content: p.content,19    variables: (p.variables ?? []) as PromptVariable[],20    schema: p.schema ?? null,21    folder: p.folder,22    tags: p.tags ?? [],23    favorite: p.favorite,24    defaultModelKey: p.defaultModelKey,25    uses: p.uses,26    lastUsedAt: p.lastUsedAt?.toISOString() ?? null,27    createdAt: p.createdAt.toISOString(),28    updatedAt: p.updatedAt.toISOString(),29  };30}31export type PublicPrompt = ReturnType<typeof toPublicPrompt>;3233export interface PromptInput {34  kind?: PromptKind;35  name: string;36  description?: string | null;37  content: string;38  variables?: PromptVariable[];39  schema?: Record<string, unknown> | null;40  folder?: string | null;41  tags?: string[];42  favorite?: boolean;43  defaultModelKey?: string | null;44  projectId?: string | null;45}4647export interface ListPromptsFilter {48  q?: string;49  kind?: PromptKind;50  folder?: string | "none";51  tag?: string;52  favorite?: boolean;53  projectId?: string | "none";54  limit?: number;55}5657const normTags = (tags: string[] | undefined) => (tags ? Array.from(new Set(tags.map((t) => t.trim().toLowerCase().slice(0, 32)).filter(Boolean))).slice(0, 12) : undefined);5859async function assertProject(userId: string, projectId: string) {60  const [p] = await getDb()61    .select({ id: projects.id })62    .from(projects)63    .where(and(eq(projects.id, projectId), eq(projects.userId, userId)))64    .limit(1);65  if (!p) throw new ApiError(404, "Project not found", "NOT_FOUND");66}6768export async function listPrompts(userId: string, f: ListPromptsFilter = {}) {69  const conds: SQL[] = [eq(prompts.userId, userId)];70  if (f.q) {71    const term = `%${f.q.replace(/[%_]/g, "\\$&")}%`;72    conds.push(or(ilike(prompts.name, term), ilike(prompts.description, term), ilike(prompts.content, term), sql`exists (select 1 from jsonb_array_elements_text(${prompts.tags}) t where t ilike ${term})`)!);73  }74  if (f.kind) conds.push(eq(prompts.kind, f.kind));75  if (f.folder === "none") conds.push(isNull(prompts.folder));76  else if (f.folder) conds.push(eq(prompts.folder, f.folder));77  if (f.tag) conds.push(sql`${prompts.tags} ? ${f.tag}`);78  if (f.favorite) conds.push(eq(prompts.favorite, true));79  if (f.projectId === "none") conds.push(isNull(prompts.projectId));80  else if (f.projectId) conds.push(eq(prompts.projectId, f.projectId));81  const db = getDb();82  const [rows, meta] = await Promise.all([83    db84      .select()85      .from(prompts)86      .where(and(...conds))87      .orderBy(desc(prompts.favorite), desc(prompts.updatedAt))88      .limit(Math.min(f.limit ?? 300, 500)),89    db.select({ folder: prompts.folder, tags: prompts.tags }).from(prompts).where(eq(prompts.userId, userId)),90  ]);91  const folders = Array.from(new Set(meta.map((m) => m.folder).filter((x): x is string => Boolean(x)))).sort((a, b) => a.localeCompare(b));92  const tagCount = new Map<string, number>();93  for (const m of meta) for (const t of m.tags ?? []) tagCount.set(t, (tagCount.get(t) ?? 0) + 1);94  const tags = [...tagCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).map(([t]) => t);95  return { prompts: rows.map(toPublicPrompt), folders, tags, total: meta.length };96}9798export async function getPrompt(userId: string, id: string): Promise<Prompt> {99  const [p] = await getDb()100    .select()101    .from(prompts)102    .where(and(eq(prompts.id, id), eq(prompts.userId, userId)))103    .limit(1);104  if (!p) throw new ApiError(404, "Prompt not found", "NOT_FOUND");105  return p;106}107108export async function createPrompt(userId: string, input: PromptInput) {109  if (input.projectId) await assertProject(userId, input.projectId);110  const kind: PromptKind = input.kind ?? "user";111  const [p] = await getDb()112    .insert(prompts)113    .values({114      id: ids.prompt(),115      userId,116      projectId: input.projectId ?? null,117      kind,118      name: input.name.trim().slice(0, 120) || "Untitled prompt",119      description: input.description?.trim() || null,120      content: input.content,121      variables: mergeVariables(input.content, input.variables),122      schema: kind === "structured" ? input.schema ?? null : null,123      folder: input.folder?.trim().slice(0, 60) || null,124      tags: normTags(input.tags) ?? [],125      favorite: input.favorite ?? false,126      defaultModelKey: input.defaultModelKey ?? null,127    })128    .returning();129  return toPublicPrompt(p);130}131132export async function updatePrompt(userId: string, id: string, patch: Partial<PromptInput>) {133  const current = await getPrompt(userId, id);134  if (patch.projectId) await assertProject(userId, patch.projectId);135  const set: Partial<typeof prompts.$inferInsert> = { updatedAt: new Date() };136  if (patch.kind !== undefined) set.kind = patch.kind;137  if (patch.name !== undefined) set.name = patch.name.trim().slice(0, 120) || "Untitled prompt";138  if (patch.description !== undefined) set.description = patch.description?.trim() || null;139  if (patch.content !== undefined) set.content = patch.content;140  if (patch.content !== undefined || patch.variables !== undefined) {141    set.variables = mergeVariables(patch.content ?? current.content, patch.variables ?? (current.variables as PromptVariable[]));142  }143  const kind = (patch.kind ?? current.kind) as PromptKind;144  if (patch.schema !== undefined) set.schema = kind === "structured" ? patch.schema : null;145  else if (patch.kind !== undefined && kind !== "structured") set.schema = null;146  if (patch.folder !== undefined) set.folder = patch.folder?.trim().slice(0, 60) || null;147  if (patch.tags !== undefined) set.tags = normTags(patch.tags) ?? [];148  if (patch.favorite !== undefined) set.favorite = patch.favorite;149  if (patch.defaultModelKey !== undefined) set.defaultModelKey = patch.defaultModelKey;150  if (patch.projectId !== undefined) set.projectId = patch.projectId;151  const [p] = await getDb()152    .update(prompts)153    .set(set)154    .where(and(eq(prompts.id, id), eq(prompts.userId, userId)))155    .returning();156  if (!p) throw new ApiError(404, "Prompt not found", "NOT_FOUND");157  return toPublicPrompt(p);158}159160export async function deletePrompt(userId: string, id: string) {161  const res = await getDb()162    .delete(prompts)163    .where(and(eq(prompts.id, id), eq(prompts.userId, userId)))164    .returning({ id: prompts.id });165  if (!res.length) throw new ApiError(404, "Prompt not found", "NOT_FOUND");166}167168export interface UsePromptResult {169  prompt: PublicPrompt;170  kind: PromptKind;171  /** Rendered body. For `system` prompts this is also returned as `systemPrompt` and `text` is empty. */172  rendered: string;173  text: string;174  systemPrompt: string | null;175  schema: Record<string, unknown> | null;176  defaultModelKey: string | null;177  missing: string[];178  defaulted: string[];179}180181/** Renders the prompt with the given variables and records the use (counter + timestamp). */182export async function usePrompt(userId: string, id: string, variables: Record<string, string> = {}): Promise<UsePromptResult> {183  const p = await getPrompt(userId, id);184  const { text, missing, defaulted } = renderTemplate(p.content, variables, (p.variables ?? []) as PromptVariable[]);185  const [updated] = await getDb()186    .update(prompts)187    .set({ uses: sql`${prompts.uses} + 1`, lastUsedAt: new Date() })188    .where(and(eq(prompts.id, id), eq(prompts.userId, userId)))189    .returning();190  const pub = toPublicPrompt(updated ?? p);191  const isSystem = pub.kind === "system";192  return {193    prompt: pub,194    kind: pub.kind,195    rendered: text,196    text: isSystem ? "" : text,197    systemPrompt: isSystem ? text : null,198    schema: pub.kind === "structured" ? pub.schema : null,199    defaultModelKey: pub.defaultModelKey,200    missing,201    defaulted,202  };203}204