SPB Git forge

spb/spb-cloud

Public
10commits 1branches 0releases
3.2 MBsize
maindefault branch
1 h agolast push
JavaScript 56.7% TypeScript 42.6%
9.6 KB · 206 lines typescript
Raw Blame History
1import Anthropic from "@anthropic-ai/sdk";2import { prisma } from "./prisma";3import { loadFileData } from "./storage";45const anthropic = new Anthropic({6  apiKey: process.env.ANTHROPIC_API_KEY,7});89const MODEL = "claude-opus-4-6";1011export async function aiSearch(query: string, userId?: string): Promise<{12  results: Array<{ fileId: string; name: string; relevance: string; snippet: string; folder?: string; mimeType?: string }>;13  answer: string;14}> {15  // Include files owned by user + legacy files with no owner (userId=null)16  const where: Record<string, unknown> = userId17    ? { OR: [{ userId }, { userId: null }] }18    : {};1920  const files = await prisma.file.findMany({21    where,22    select: { id: true, name: true, mimeType: true, size: true, aiTags: true, aiDescription: true, ocrText: true, createdAt: true, folder: { select: { name: true } } },23    take: 200,24    orderBy: { createdAt: "desc" },25  });2627  const fileList = files.map((f) => {28    const parts = [`- ${f.name} (${f.mimeType}, ID:${f.id})`];29    if (f.folder) parts.push(`  Dossier: ${f.folder.name}`);30    if (f.aiDescription) parts.push(`  Description: ${f.aiDescription}`);31    if (f.aiTags) parts.push(`  Tags: ${f.aiTags}`);32    if (f.ocrText) parts.push(`  OCR: ${f.ocrText.substring(0, 200)}`);33    return parts.join("\n");34  }).join("\n");3536  const response = await anthropic.messages.create({37    model: MODEL,38    max_tokens: 1024,39    messages: [{ role: "user", content: `Tu es un assistant de recherche de fichiers. Voici la liste des fichiers:\n\n${fileList}\n\nRecherche: "${query}"\n\nRéponds en JSON:\n{"results": [{"fileId": "...", "name": "...", "relevance": "high|medium|low", "snippet": "courte explication", "folder": "nom du dossier ou null", "mimeType": "..."}], "answer": "résumé"}` }],40  });4142  try {43    const text = response.content[0].type === "text" ? response.content[0].text : "";44    const jsonMatch = text.match(/\{[\s\S]*\}/);45    if (jsonMatch) return JSON.parse(jsonMatch[0]);46  } catch {}47  return { results: [], answer: "Aucun résultat trouvé." };48}4950export async function aiAutoTag(fileId: string): Promise<string[]> {51  const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, size: true, ocrText: true, storagePath: true, isEncrypted: true } });52  if (!file) return [];5354  let contentHint = `Fichier: ${file.name}\nType: ${file.mimeType}\nTaille: ${Number(file.size)} bytes`;55  if (file.ocrText) contentHint += `\nContenu OCR: ${file.ocrText.substring(0, 500)}`;5657  if (file.mimeType.startsWith("text/") || file.mimeType.includes("json") || file.mimeType.includes("xml")) {58    try {59      contentHint += `\nContenu: ${(await loadFileData(file.storagePath, file.isEncrypted)).toString("utf-8").substring(0, 1000)}`;60    } catch {}61  }6263  const response = await anthropic.messages.create({64    model: MODEL,65    max_tokens: 256,66    messages: [{ role: "user", content: `Analyse ce fichier et suggère 3-5 tags pertinents en français.\n\n${contentHint}\n\nRéponds UNIQUEMENT avec un JSON array de strings. Ex: ["finance", "rapport", "2024"]` }],67  });6869  try {70    const text = response.content[0].type === "text" ? response.content[0].text : "";71    const match = text.match(/\[[\s\S]*\]/);72    if (match) {73      const tags = JSON.parse(match[0]) as string[];74      await prisma.file.update({ where: { id: fileId }, data: { aiTags: JSON.stringify(tags) } });75      return tags;76    }77  } catch {}78  return [];79}8081export async function aiDescribe(fileId: string): Promise<string> {82  const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, storagePath: true, ocrText: true, isEncrypted: true } });83  if (!file) return "";8485  if (file.mimeType.startsWith("image/")) {86    try {87      {88        const base64 = (await loadFileData(file.storagePath, file.isEncrypted)).toString("base64");89        const mediaType = file.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp";90        const response = await anthropic.messages.create({91          model: MODEL,92          max_tokens: 512,93          messages: [{ role: "user", content: [{ type: "image", source: { type: "base64", media_type: mediaType, data: base64 } }, { type: "text", text: "Décris cette image en détail en français. Mentionne le sujet principal, les couleurs, le contexte et tout élément notable." }] }],94        });95        const desc = response.content[0].type === "text" ? response.content[0].text : "";96        await prisma.file.update({ where: { id: fileId }, data: { aiDescription: desc } });97        return desc;98      }99    } catch {}100  }101102  let hint = `Fichier: ${file.name}\nType: ${file.mimeType}`;103  if (file.mimeType.startsWith("text/") || file.mimeType.includes("json")) {104    try {105      hint += `\nContenu: ${(await loadFileData(file.storagePath, file.isEncrypted)).toString("utf-8").substring(0, 2000)}`;106    } catch {}107  }108  if (file.ocrText) hint += `\nOCR: ${file.ocrText.substring(0, 500)}`;109110  const response = await anthropic.messages.create({111    model: MODEL,112    max_tokens: 256,113    messages: [{ role: "user", content: `Décris ce fichier de manière utile en français (2-3 phrases):\n\n${hint}` }],114  });115  const desc = response.content[0].type === "text" ? response.content[0].text : "";116  await prisma.file.update({ where: { id: fileId }, data: { aiDescription: desc } });117  return desc;118}119120export async function aiOcr(fileId: string): Promise<string> {121  const file = await prisma.file.findUnique({ where: { id: fileId }, select: { id: true, name: true, mimeType: true, storagePath: true, isEncrypted: true } });122  if (!file) return "";123124  if (file.mimeType.startsWith("image/")) {125    try {126      const base64 = (await loadFileData(file.storagePath, file.isEncrypted)).toString("base64");127      const mediaType = file.mimeType as "image/jpeg" | "image/png" | "image/gif" | "image/webp";128      const response = await anthropic.messages.create({129        model: MODEL,130        max_tokens: 4096,131        messages: [{ role: "user", content: [{ type: "image", source: { type: "base64", media_type: mediaType, data: base64 } }, { type: "text", text: "Extrais tout le texte visible dans cette image de manière précise. Préserve la structure (titres, listes, tableaux). Retourne uniquement le texte extrait, sans commentaire." }] }],132      });133      const text = response.content[0].type === "text" ? response.content[0].text : "";134      await prisma.file.update({ where: { id: fileId }, data: { ocrText: text } });135      return text;136    } catch {}137  }138139  if (file.mimeType === "application/pdf") {140    try {141      const base64 = (await loadFileData(file.storagePath, file.isEncrypted)).toString("base64");142      const response = await anthropic.messages.create({143        model: MODEL,144        max_tokens: 8192,145        messages: [{ role: "user", content: [{ type: "document", source: { type: "base64", media_type: "application/pdf", data: base64 } }, { type: "text", text: "Extrais tout le texte de ce document PDF en préservant la structure. Retourne uniquement le texte extrait." }] as Anthropic.ContentBlockParam[] }],146      });147      const text = response.content[0].type === "text" ? response.content[0].text : "";148      await prisma.file.update({ where: { id: fileId }, data: { ocrText: text } });149      return text;150    } catch {}151  }152  return "";153}154155export type ChatMessage = { role: "user" | "assistant"; content: string };156157export async function aiChat(message: string, history: ChatMessage[] = [], userId?: string): Promise<string> {158  let fileContext = "";159  if (userId) {160    const where = { OR: [{ userId }, { userId: null }] };161    const recentFiles = await prisma.file.findMany({162      where,163      select: { name: true, mimeType: true, aiTags: true, aiDescription: true, folder: { select: { name: true } } },164      take: 30,165      orderBy: { createdAt: "desc" },166    });167    const totalCount = await prisma.file.count({ where });168    fileContext = `\nL'utilisateur possède ${totalCount} fichier(s) au total. Voici les 30 plus récents:\n` +169      recentFiles.map(f =>170        `- ${f.name} (${f.mimeType})${f.folder ? ` [dossier: ${f.folder.name}]` : ""}${f.aiDescription ? ` — ${f.aiDescription}` : ""}${f.aiTags ? ` [tags: ${f.aiTags}]` : ""}`171      ).join("\n");172  }173174  const response = await anthropic.messages.create({175    model: MODEL,176    max_tokens: 2048,177    system: `Tu es l'assistant IA de SPB Cloud, un cloud personnel privé. Tu aides l'utilisateur avec ses fichiers, réponds en français, et tu es concis et utile.${fileContext}`,178    messages: [179      ...history,180      { role: "user", content: message },181    ],182  });183  return response.content[0].type === "text" ? response.content[0].text : "";184}185186export async function buildFileContext(userId: string): Promise<string> {187  // Include files owned by user + legacy files with no owner (userId=null)188  const where = { OR: [{ userId }, { userId: null }] };189  const recentFiles = await prisma.file.findMany({190    where,191    select: { name: true, mimeType: true, aiTags: true, aiDescription: true, ocrText: true, folder: { select: { name: true } } },192    take: 30,193    orderBy: { createdAt: "desc" },194  });195  const totalCount = await prisma.file.count({ where });196  return `L'utilisateur possède ${totalCount} fichier(s) au total. Voici les 30 plus récents:\n` +197    recentFiles.map(f => {198      const parts = [`- ${f.name} (${f.mimeType})`];199      if (f.folder) parts.push(`[dossier: ${f.folder.name}]`);200      if (f.aiDescription) parts.push(`— ${f.aiDescription}`);201      if (f.aiTags) parts.push(`[tags: ${f.aiTags}]`);202      if (f.ocrText) parts.push(`[OCR dispo: ${f.ocrText.substring(0, 150)}...]`);203      return parts.join(" ");204    }).join("\n");205}206