From 3171f32cecbb30ca4b33e7a4e858dad566641069 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Sun, 29 Mar 2026 19:30:51 +0200 Subject: [PATCH] Fix links and add gemini models --- backend/api/package.json | 2 +- backend/api/src/llm-extract-profile.ts | 89 +++++++++++++++---------- backend/shared/src/parse.ts | 19 +++--- web/components/social-links-section.tsx | 6 +- 4 files changed, 70 insertions(+), 46 deletions(-) diff --git a/backend/api/package.json b/backend/api/package.json index b2ca8845..fdfdcea0 100644 --- a/backend/api/package.json +++ b/backend/api/package.json @@ -1,6 +1,6 @@ { "name": "@compass/api", - "version": "1.29.0", + "version": "1.29.1", "private": true, "description": "Backend API endpoints", "main": "src/serve.ts", diff --git a/backend/api/src/llm-extract-profile.ts b/backend/api/src/llm-extract-profile.ts index d5917c74..1dacc81e 100644 --- a/backend/api/src/llm-extract-profile.ts +++ b/backend/api/src/llm-extract-profile.ts @@ -17,6 +17,7 @@ import { } from 'common/choices' import {debug} from 'common/logger' import {ProfileWithoutUser} from 'common/profiles/profile' +import {SITE_ORDER} from 'common/socials' import {parseJsonContentToText} from 'common/util/parse' import {createHash} from 'crypto' import {promises as fs} from 'fs' @@ -79,42 +80,52 @@ async function callGemini(text: string) { throw APIErrors.internalServerError('Profile extraction service is not configured') } - const response = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=${apiKey}`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - contents: [ - { - parts: [ - { - text: text.slice(0, MAX_CONTEXT_LENGTH), - }, - ], - }, - ], - generationConfig: { - temperature: 0, - topP: 0.95, - topK: 40, - responseMimeType: 'application/json', + const models = [ + 'gemini-2.5-flash', + 'gemini-3-flash-preview', + 'gemini-2.5-flash-lite', + 'gemini-3.1-flash-preview', + ] + + for (const model of models) { + const response = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', }, - }), - }, - ) + body: JSON.stringify({ + contents: [ + { + parts: [ + { + text: text.slice(0, MAX_CONTEXT_LENGTH), + }, + ], + }, + ], + generationConfig: { + temperature: 0, + topP: 0.95, + topK: 40, + responseMimeType: 'application/json', + }, + }), + }, + ) - if (!response.ok) { - const errorText = await response.text() - log('Gemini API error', {status: response.status, error: errorText}) - throw APIErrors.internalServerError('Failed to extract profile data') + if (!response.ok) { + const errorText = await response.text() + log(`Gemini API error with ${model}`, {status: response.status, error: errorText}) + if (model !== models[models.length - 1]) continue + throw APIErrors.internalServerError('Failed to extract profile data') + } + + const data = await response.json() + const outputText = data.candidates?.[0]?.content?.parts?.[0]?.text + return outputText } - - const data = await response.json() - const outputText = data.candidates?.[0]?.content?.parts?.[0]?.text - return outputText } async function _callClaude(text: string) { @@ -224,7 +235,7 @@ async function callLLM(content: string, locale?: string): Promise SITE_ORDER.includes(key as any)) + parsed.links = sites.reduce( + (acc, key) => { + acc[key] = (parsed.links as Record)[key] + return acc + }, + {} as Record, + ) + } await setCachedResult(cacheKey, parsed) diff --git a/backend/shared/src/parse.ts b/backend/shared/src/parse.ts index 1b470a4c..c5aa6a03 100644 --- a/backend/shared/src/parse.ts +++ b/backend/shared/src/parse.ts @@ -1,4 +1,3 @@ -import {Readability} from '@mozilla/readability' import {JSONContent} from '@tiptap/core' import {debug} from 'common/logger' import {JSDOM} from 'jsdom' @@ -8,15 +7,15 @@ export function htmlToJSONContent(html: string, url: string): JSONContent { const originalDom = new JSDOM(html, {url}) const classStyles = extractClassStyles(originalDom.window.document) - const isGoogleDoc = !!extractGoogleDocId(url) - if (!isGoogleDoc) { - const reader = new Readability(originalDom.window.document) - const article = reader.parse() - if (article?.content) { - const cleanDom = new JSDOM(article.content) - return parseHtmlBodyToJSONContent(cleanDom.window.document, classStyles) - } - } + // const isGoogleDoc = !!extractGoogleDocId(url) + // if (!isGoogleDoc) { + // const reader = new Readability(originalDom.window.document) + // const article = reader.parse() + // if (article?.content) { + // const cleanDom = new JSDOM(article.content) + // return parseHtmlBodyToJSONContent(cleanDom.window.document, classStyles) + // } + // } return parseHtmlBodyToJSONContent(originalDom.window.document, classStyles) } diff --git a/web/components/social-links-section.tsx b/web/components/social-links-section.tsx index e9ec9fc4..a731a852 100644 --- a/web/components/social-links-section.tsx +++ b/web/components/social-links-section.tsx @@ -2,6 +2,7 @@ import {PlusIcon, XMarkIcon} from '@heroicons/react/24/solid' import clsx from 'clsx' import {ProfileWithoutUser} from 'common/profiles/profile' import {PLATFORM_LABELS, type Site, SITE_ORDER, Socials} from 'common/socials' +import {removeNullOrUndefinedProps} from 'common/util/object' import {Fragment, useState} from 'react' import {Button, IconButton} from 'web/components/buttons/button' import {Col} from 'web/components/layout/col' @@ -22,7 +23,10 @@ export function SocialLinksSection({profile, setProfile}: SocialLinksSectionProp const [newLinkValue, setNewLinkValue] = useState('') const updateUserLink = (platform: string, value: string | null) => { - setProfile('links', {...((profile.links as Socials) ?? {}), [platform]: value}) + setProfile( + 'links', + removeNullOrUndefinedProps({...((profile.links as Socials) ?? {}), [platform]: value}), + ) } const addNewLink = () => {