Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,250 changes: 1,242 additions & 8 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
"dependencies": {
"@clerk/clerk-sdk-node": "^4.13.23",
"@clerk/express": "^1.7.13",
"@google/genai": "^1.30.0",
"@google/generative-ai": "^0.24.1",
"@modelcontextprotocol/sdk": "^1.20.2",
"@tiptap/starter-kit": "^3.5.1",
"@types/express": "^5.0.3",
Expand All @@ -27,6 +29,7 @@
"crypto": "^1.0.1",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"google": "^2.1.0",
"jsdom": "^27.0.0",
"jsonwebtoken": "^9.0.2",
"lodash.isequal": "^4.5.0",
Expand Down
2 changes: 1 addition & 1 deletion src/assets/config/jeremy_ai.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"prompt_system": "Tu dois ecrir au format MarkDown en html. Pour faire des todolist tu dois faire une list sous ce format d'example : <ul data-type=\"taskList\"><li data-checked=\"false\"><label contenteditable=\"false\"><input aria-label=\"Task item checkbox for empty task item\" type=\"checkbox\"><span></span></label><div><p>dsq</p></div></li></ul>. Pour integrer un saut de ligne, met ce code décolé avec un espace : '#34'. Pour le markdown, les fonction comme **text** et __text__ doivent être colé au text. Tu es le chatbot de l'application de prise de notes 'silvernote', tu te nommes 'SilverIA'. Tu parles par default français. Ton rôle est d'aider l'utilisateur à organiser ses notes, gerer ses dossiers, répondre aux questions sur l'application et donner des conseils pour mieux gérer ses notes. Tu dois être toujours poli, clair et encourageant. Tu dois repondre avec des reponse breve. Tu ne peux pas inventer d'informations sur l'utilisateur en dehors de ce qu'il te fournit. Si une question est hors sujet ou que tu ne sais pas, tu dois le dire poliment. Les notes et tag de cet utilisateur te seront fournit a chaque message. Ne dit pas de truc inutile dans tes reponses. L'utilisateur sait qui tu est sauf si il te le demande. Tu peut effectué des acction pour agir sur les notes et dossier/tag de l'utilisateur."
"prompt_system": "Tu dois ecrir au format MarkDown en html. Pour faire des todolist tu dois faire une list sous ce format d'example : <ul data-type=\"taskList\"><li data-checked=\"false\"><label contenteditable=\"false\"><input aria-label=\"Task item checkbox for empty task item\" type=\"checkbox\"><span></span></label><div><p>dsq</p></div></li></ul>. Pour le markdown, les fonction comme **text** et __text__ doivent être colé au text. Tu es le chatbot de l'application de prise de notes 'silvernote'. Tu parles par default en français. Ton rôle est d'aider l'utilisateur à organiser ses notes, gerer ses dossiers, répondre aux questions sur l'application et donner des conseils pour mieux gérer ses notes. Tu dois être toujours poli, clair et encourageant. Tu dois repondre avec des reponse breve. Tu ne peux pas inventer d'informations sur l'utilisateur en dehors de ce qu'il te fournit. Si une question est hors sujet ou que tu ne sais pas, tu dois le dire poliment. Les notes et tag de cet utilisateur te seront fournit a chaque message. Ne dit pas de truc inutile dans tes reponses. L'utilisateur sait qui tu est sauf si il te le demande. Tu peut effectué des acction pour agir sur les notes et dossier/tag de l'utilisateur."
}
86 changes: 85 additions & 1 deletion src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,69 @@ export class MCPService {
console.log(`Loaded tools: ${this.tools.map(t => t.name).join(', ')}`);
}

getOpenAITools() {
public getOpenAITools() {
return this.openaiTools.map(tool => ({
...tool,
resources: ["note://*"],
}));
}

public getGeminiTools()
{
if (!this.tools || this.tools.length === 0) {
return [];
}

return this.tools
.filter(tool => tool && (tool.function?.name || tool.name))
.map(tool => {
const isOpenAIFormat = 'function' in tool;

const parameters = isOpenAIFormat
? tool.function.parameters
: (tool.inputSchema || tool.parameters || {});

const cleanedParameters = this.cleanSchemaForGemini(parameters);

return {
name: isOpenAIFormat ? tool.function.name : tool.name,
description: isOpenAIFormat ? tool.function.description : tool.description,
parameters: cleanedParameters
};
});
}

private cleanSchemaForGemini(schema: any): any
{
if (!schema || typeof schema !== 'object') {
return schema;
}

const cleaned = JSON.parse(JSON.stringify(schema));

const removeInvalidFields = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map(removeInvalidFields);
}

if (obj && typeof obj === 'object') {

delete obj.$schema;
delete obj.additionalProperties;
delete obj.$defs;
delete obj.definitions;

for (const key in obj) {
obj[key] = removeInvalidFields(obj[key]);
}
}

return obj;
};

return removeInvalidFields(cleaned);
}

async callTool(name: string, args: any = {}) {
if (!this.client) {
throw new Error('MCP client not connected');
Expand All @@ -104,6 +160,34 @@ export class MCPService {
}
}

async handleToolCallsGemini(toolCalls: any[]) {
const results = [];

for (const toolCall of toolCalls) {
const name = toolCall.function.name;
const args = JSON.parse(toolCall.function.arguments);

try {
const result = await this.callTool(name, args);

results.push({
role: 'function',
name: name,
content: result
});

} catch (error: any) {
results.push({
role: 'function',
name: name,
content: `Error: ${error.message}`
});
}
}

return results;
}

async handleToolCalls(toolCalls: OpenAI.Chat.Completions.ChatCompletionMessageToolCall[]) {
const results = [];

Expand Down
157 changes: 14 additions & 143 deletions src/routes/api.ai.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { UUID, randomUUID } from 'crypto';
import { Router, Request, Response, NextFunction } from 'express';
import OpenAI from "openai";
import fs from 'fs';
import path from 'path';
import __dirname from '../assets/ts/_dirname.js';
Expand All @@ -10,19 +9,14 @@ import db from '../assets/ts/database.js';
import notes_db from '../assets/ts/notes.js';
import tags_db from '../assets/ts/tags.js';
import { getMCPService } from '../mcp.js';
import type { ChatCompletionMessageParam } from 'openai/resources/chat/completions';
import send_to_chatgpt from './api.ai/send_to_chatgpt.js';
import { Chat } from './api.ai/types.js';
import send_to_gemini from './api.ai/send_to_gemini.js';

const AIclient = new OpenAI({ apiKey: process.env.OPENAI_SECRET_KEY });
const router = Router();

type Chat = {
uuid: UUID;
userID: string;
data: { notes: any; tags: any };
messages: ChatCompletionMessageParam[];
}

let chats: Chat[] = [];
let chats: any[] = [];

function verify_auth(req: Request, res: Response, next: NextFunction) {
next();
Expand Down Expand Up @@ -101,141 +95,18 @@ router.post('/close', verify_auth, async (req: Request, res: Response) => {
});

router.post('/send', verify_auth, async (req: Request, res: Response) => {
const { uuid, message } = req.body;
const note = req.body?.note || undefined;

try {
const chat = chats.find(chat => chat.uuid == uuid);

if (!chat) {
res.json({ error: true, message: 'Chat non trouvé' });
return;
}

// Préparer le prompt
let prompt: string = '';

if (!note) {
prompt = `Message de l'utilisateur : ${message}`;
} else {
let db_note = (await notes_db.getNoteByUUID(note)).note;
if (db_note) {
db_note = { ...db_note, content: db_note.content.replace(/<img[^>]*>/g, '') };
}
prompt = `Note ouverte : ${JSON.stringify({ db_note })}\n Message de l'utilisateur : ${message}`;
}

chat.messages.push({
role: 'user',
content: prompt
});

// Obtenir le service MCP
const mcpService = getMCPService();
await mcpService.ensureConnected();

// Obtenir les outils MCP au format OpenAI
const mcpTools = mcpService.getOpenAITools();

// Configuration SSE
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.flushHeaders();
res.socket?.setNoDelay(true);

let conversationMessages = [...chat.messages.slice(-10)];
let iterationCount = 0;
const maxIterations = 5;

// Boucle pour gérer les appels d'outils
while (iterationCount < maxIterations) {
iterationCount++;

// Appel à OpenAI avec les outils MCP
const stream = await AIclient.chat.completions.create({
model: "gpt-5-mini",
messages: conversationMessages,
tools: mcpTools.length > 0 ? mcpTools : undefined,
tool_choice: 'auto',
stream: true
});

let assistantMessage: string = "";
let buffer: string = '';
let toolCalls: any[] = [];
let currentToolCall: any = null;

for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta;

const model = req.query?.model as 'gpt' | 'gemini';

// Gérer le contenu texte
if (delta?.content) {
const token = delta.content;
assistantMessage += token;
buffer += token;
res.write(`data: ${token}\n\n`);
}

// Gérer les appels d'outils
if (delta?.tool_calls) {
for (const toolCallDelta of delta.tool_calls) {
if (toolCallDelta.index !== undefined) {
if (!toolCalls[toolCallDelta.index]) {
toolCalls[toolCallDelta.index] = {
id: toolCallDelta.id || '',
type: 'function',
function: {
name: toolCallDelta.function?.name || '',
arguments: ''
}
};
}

if (toolCallDelta.function?.arguments) {
toolCalls[toolCallDelta.index].function.arguments += toolCallDelta.function.arguments;
}
}
}
}
}

// Ajouter le message de l'assistant
conversationMessages.push({
role: 'assistant',
content: assistantMessage || null,
tool_calls: toolCalls.length > 0 ? toolCalls : undefined
} as any);

chat.messages.push({
role: 'assistant',
content: assistantMessage
});

// Si pas d'appel d'outil, on termine
if (toolCalls.length === 0) {
break;
}

// Exécuter les appels d'outils via MCP
res.write(`data: [TOOLS:${toolCalls.map(tc => tc.function.name).join(',')}]\n\n`);

const toolResults = await mcpService.handleToolCalls(toolCalls);
conversationMessages.push(...toolResults);

// Envoyer les résultats au client
for (const result of toolResults) {
res.write(`data: [TOOL_RESULT:${result.content}]\n\n`);
}
}

res.write("data: [DONE]\n\n");
res.end();

} catch (err: any) {
console.error('Error in /send:', err);
res.status(500).json({ error: true, message: err.message });
if (true || model && model == 'gpt')
{
send_to_chatgpt(req, res, chats);
}
else
{
send_to_gemini(req, res, chats);
}

});

export default router;
Loading