import zod from 'https://cdn.jsdelivr.net/npm/zod@4.0.10/+esm' /** * Met en forme le prompt template passé en paramètres avec les arguments * @param {String} template * @param {Object} args */ export function formatTemplate(template, args) { return template.replace(/\{\{(\w+)\}\}/g, (_, key) => { // 'match' est la correspondance complète (ex: "{nom}") // 'key' est le contenu du premier groupe capturé (ex: "nom") if (key in args) return args[key]; // Si la clé n'est pas trouvée dans args, on laisse le placeholder tel quel. return ""; }); } /** * Recupère le prompt pour la tâche spécifiée. * @param {String} task */ export async function retrieveTemplate(task) { const req = await fetch(`/prompt/${task}`) return await req.text(); } /** * Lance un deep search sur le serveur pour les topics donnés. * @param {Array} topics */ export async function performDeepSearch(topics) { const response = await fetch('/solutions/search_prior_art', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ topics: topics }) }); const results = await response.json(); console.log(results); return results.content; } /** * Genère une completion avec le LLM specifié * @param {String} providerUrl - URL du provider du LLM * @param {String} modelName - Nom du modèle à appeler * @param {String} apiKey - API key a utiliser * @param {Array<{role: string, content: string}>} messages - Liste de messages à passer au modèle * @param {Number} temperature - Température à utiliser pour la génération */ export async function generateCompletion(providerUrl, modelName, apiKey, messages, temperature = 0.5) { const genEndpoint = providerUrl + "/chat/completions" try { const response = await fetch(genEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, // OpenAI-like authorization header }, body: JSON.stringify({ model: modelName, messages: messages, temperature: temperature, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(`API request failed with status ${response.status}: ${errorData.error?.message || 'Unknown error'}`); } const data = await response.json(); if (data.choices && data.choices.length > 0 && data.choices[0].message && data.choices[0].message.content) return data.choices[0].message.content; } catch (error) { console.error("Error calling private LLM :", error); throw error; } } /** * Genère une completion structurée avec le LLM specifié * @param {String} providerUrl - URL du provider du LLM * @param {String} modelName - Nom du modèle à appeler * @param {String} apiKey - API key a utiliser * @param {Array<{role: string, content: string}>} messages - Liste de messages à passer au modèle * @param {Object} schema - Zod schema to use for structured generation * @param {Number} temperature - Température à utiliser pour la génération */ //TODO: Find the correct args to constrain the LLM to the json schema instead of enforcing json correct parsing export async function generateStructuredCompletion(providerUrl, modelName, apiKey, messages, schema, temperature = 0.5) { const genEndpoint = providerUrl + "/chat/completions"; try { const response = await fetch(genEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model: modelName, messages: messages, temperature: temperature, response_format: { type: "json_object" } }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(`API request failed with status ${response.status}: ${errorData.error?.message || 'Unknown error'}`); } const data = await response.json(); console.log(data.choices[0].message.content); // parse json output const parsedJSON = JSON.parse(data.choices[0].message.content.replace('```json', '').replace("```", "")); // validate output with zod const validatedSchema = schema.parse(parsedJSON); return validatedSchema; } catch (error) { console.error("Error calling private LLM :", error); throw error; } } /** * Retrieves a list of available models from an OpenAI-compatible API using fetch. * * @param {string} providerUrl The base URL of the OpenAI-compatible API endpoint (e.g., "http://localhost:8000/v1"). * @param {string} apiKey The API key for authentication. * @returns {Promise>} A promise that resolves with an array of model names, or rejects with an error. */ export async function getModelList(providerUrl, apiKey) { try { // Construct the full URL for the models endpoint const modelsUrl = `${providerUrl}/models`; console.log(modelsUrl); // Make a GET request to the models endpoint using fetch const response = await fetch(modelsUrl, { method: 'GET', // Explicitly state the method headers: { 'Authorization': `Bearer ${apiKey}`, // OpenAI-compatible authorization header 'Content-Type': 'application/json', }, }); // Check if the request was successful (status code 200-299) if (!response.ok) { // If the response is not OK, try to get more error details const errorData = await response.json().catch(() => ({})); // Attempt to parse JSON error, fallback to empty object throw new Error(`HTTP error! Status: ${response.status}, Message: ${errorData.message || response.statusText}`); } // Parse the JSON response body const data = await response.json(); // The response data structure for OpenAI-compatible APIs usually contains a 'data' array // where each item represents a model and has an 'id' property. if (data && Array.isArray(data.data)) { const allModelNames = data.data.map(model => model.id); // Filter out models containing "embedding" (case-insensitive) const filteredModelNames = allModelNames.filter(modelName => !modelName.toLowerCase().includes('embedding') ); return filteredModelNames; } else { // Handle cases where the response format is unexpected throw new Error('Unexpected response format from the API. Could not find model list.'); } } catch (error) { console.error('Error fetching model list:', error.message); // Re-throw the error to allow the caller to handle it throw error; } } // # ========================================================================================== Idea assessment logic ================================================================== // JS schema for the assessment output. // keep in sync with contents of "extract" prompt const StructuredAssessmentOutput = zod.object({ final_verdict: zod.string(), summary: zod.string(), insights: zod.array(zod.string()), }); export async function assessSolution(providerUrl, modelName, apiKey, solution, assessment_rules, portfolio_info) { const template = await retrieveTemplate("assess"); const assessment_template = formatTemplate(template, { notation_criterias: assessment_rules, business: portfolio_info, problem_description: solution.problem_description, solution_description: solution.solution_description, }); const assessment_full = await generateCompletion(providerUrl, modelName, apiKey, [ { role: "user", content: assessment_template } ]); const structured_template = await retrieveTemplate("extract"); const structured_filled_template = formatTemplate(structured_template, { "report": assessment_full, "response_schema": zod.toJSONSchema(StructuredAssessmentOutput) }) const extracted_info = await generateStructuredCompletion(providerUrl, modelName, apiKey, [{ role: "user", content: structured_filled_template }], StructuredAssessmentOutput); return { assessment_full, extracted_info }; } export async function refineSolution(providerUrl, modelName, apiKey, solution, insights, user_insights, assessment_rules, portfolio_info) { const template = await retrieveTemplate("refine"); const refine_template = formatTemplate(template, { "problem_description": solution.problem_description, "solution_description": solution.solution_description, "insights": insights.join("\n -"), "user_insights": user_insights, "business_info": portfolio_info, }); console.log(refine_template); const refined_idea = await generateCompletion(providerUrl, modelName, apiKey, [{ role: "user", content: refine_template }]); const newSolution = structuredClone(solution); newSolution.solution_description = refined_idea; return newSolution; } // FTO analysis // JS schema for FTO analysis topic extraction const FTOAnalysisTopicsSchema = zod.object({ topics: zod.array(zod.string()) }); /** * Extract the topics to search for FTO */ async function getFtoAnalysisTopics(providerUrl, modelName, apiKey, idea, count) { const template = await retrieveTemplate("fto_topics"); const structured_template = formatTemplate(template, { "problem_description": idea.problem_description, "solution_description": idea.solution_description, "response_schema": zod.toJSONSchema(FTOAnalysisTopicsSchema), "max_topic_count": count }); const topics = await generateStructuredCompletion(providerUrl, modelName, apiKey, [{ role: "user", content: structured_template }], FTOAnalysisTopicsSchema); return topics; } /* * Assess the infringement of the idea wrt */ async function assessFTOReport(providerUrl, modelName, apiKey, solution, fto_report, portfolio_info) { const template = await retrieveTemplate("fto_assess"); const assessment_template = formatTemplate(template, { business: portfolio_info, fto_report: fto_report, problem_description: solution.problem_description, solution_description: solution.solution_description, }); console.log("FTO Length: " + assessment_template.length); const assessment_full = await generateCompletion(providerUrl, modelName, apiKey, [ { role: "user", content: assessment_template } ]); const structured_template = await retrieveTemplate("extract"); const structured_filled_template = formatTemplate(structured_template, { "report": assessment_full, "response_schema": zod.toJSONSchema(StructuredAssessmentOutput) }) const extracted_info = await generateStructuredCompletion(providerUrl, modelName, apiKey, [{ role: "user", content: structured_filled_template }], StructuredAssessmentOutput); return { assessment_full, extracted_info }; } export async function runFTOAnalysis(providerUrl, providerModel, apiKey, solution, portfolio_info, ftoTopicCount) { const fto_topics = await getFtoAnalysisTopics(providerUrl, providerModel, apiKey, solution, ftoTopicCount); console.log(fto_topics); const fto_report = await performDeepSearch(fto_topics.topics); const assess_results = await assessFTOReport(providerUrl, providerModel, apiKey, solution, fto_report, portfolio_info); console.log(assess_results.extracted_info); return { fto_topics: fto_topics, fto_report: fto_report, assessment_full: assess_results.assessment_full, extracted_info: assess_results.extracted_info, }; }