Skip to content

Killian STEIN

4 posts by Killian STEIN

Create a Multi-Agent Discord News Bot with Mastra (Complete Guide 2025)

A Discord news bot

With the rise of LLMs, agents, MCP, and other technologies, building applications has become easier than ever. Today, I will introduce you to an agentic framework in TypeScript that I really enjoy: Mastra. https://mastra.ai/

Mastra

The goal here is not to present all the features of Mastra or go into configuration details, but to show you its potential with workflows and how easy it is to get started.

Normally, there is a playground that makes testing easier, but here we will do everything directly on Discord.

Discord

First, go to: https://discord.com/developers/applications

And create a new application

Discord

In the ‘General Information’ section, retrieve the application ID for later.

Then, in the bot section, reset the token to generate a new one.

Finally, in the installation section, get the installation URL to add it to a server for testing.

CODE!

Now let’s move on to the code and project setup.

Setup

To set up the project in TypeScript, I will follow this guide:

https://medium.com/@rosarioborgesi/setting-up-a-node-js-project-with-pnpm-and-typescript-0b16a512ef24

Only my scripts section is different :

"scripts": {
"dev": "tsx watch src/index.ts",
"start": "tsx src/index.ts",
}

For this, you will also need to add tsx :

Fenêtre de terminal
pnpm add tsx

Now let’s set up our Discord bot, a simple echo message for now.

You will need to install the Discord.js library

Fenêtre de terminal
pnpm add discord.js dotenv

Here is my index.ts :

import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from 'discord.js';
import dotenv from 'dotenv';
// Load environment variables
dotenv.config();
// Create a new instance of the Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
],
});
// Define the /ping command
const commands = [
new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with pong!')
.toJSON(),
];
// Function to register slash commands
async function deployCommands() {
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN!);
try {
console.log('Deploying slash commands...');
await rest.put(
Routes.applicationCommands(process.env.CLIENT_ID!),
{ body: commands },
);
console.log('Slash commands deployed successfully!');
} catch (error) {
console.error('Error deploying commands:', error);
}
}
// Event: Bot ready
client.once('ready', async () => {
console.log(`Bot connected as ${client.user?.tag}!`);
await deployCommands();
});
// Event: Interaction with slash commands
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
if (commandName === 'ping') {
await interaction.reply('Pong!');
}
});
// Error handling
client.on('error', (error) => {
console.error('Discord client error:', error);
});
process.on('unhandledRejection', (error) => {
console.error('Unhandled promise rejection:', error);
});
// Bot connection
client.login(process.env.DISCORD_TOKEN);

Run pnpm run dev and add it to your server, it should respond pong to you !

pong

Mastra

For this tutorial, I will use Google keys and the flash preview model, as it is free !

You could also use them to have a 100% free bot : https://ai.google.dev/gemini-api/docs/api-key?hl=fr

First, we will install all the dependencies :

Fenêtre de terminal
pnpm add @mastra/core @mastra/memory @mastra/libsql @ai-sdk/google zod rss-parser

Now we will create an agents folder and make an agentRss.ts :

import 'dotenv/config';
import { Agent } from "@mastra/core/agent";
import { Memory } from '@mastra/memory';
import { LibSQLStore } from '@mastra/libsql';
import { createTool } from '@mastra/core';
import { z } from 'zod';
import Parser from 'rss-parser';
import { google } from '@ai-sdk/google';
// Initialize the RSS parser
const parser = new Parser();
const memory = new Memory({
storage: new LibSQLStore({
url: "file:./memory.db",
}),
});
const today = new Date().toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
// Tool to read RSS feeds
const rssFeedTool = createTool({
id: 'rss-feed-reader',
description: 'Reads and analyzes an RSS feed from a URL',
inputSchema: z.object({
url: z.string().url().describe('The URL of the RSS feed to read'),
limit: z.number().optional().default(10).describe('Maximum number of articles to retrieve (default: 10)')
}),
outputSchema: z.object({
title: z.string().describe('Title of the RSS feed'),
description: z.string().optional().describe('Description of the feed'),
link: z.string().optional().describe('Link to the website'),
articles: z.array(z.object({
title: z.string().describe('Title of the article'),
link: z.string().optional().describe('Link to the article'),
pubDate: z.string().optional().describe('Publication date'),
contentSnippet: z.string().optional().describe('Content snippet'),
creator: z.string().optional().describe('Author of the article')
})).describe('List of articles in the feed')
}),
execute: async ({ context }) => {
try {
const { url, limit } = context;
// Parse the RSS feed
const feed = await parser.parseURL(url);
// Limit the number of articles
const articles = feed.items.slice(0, limit).map(item => ({
title: item.title || 'Untitled',
link: item.link || undefined,
pubDate: item.pubDate || undefined,
contentSnippet: item.contentSnippet || item.content || undefined,
creator: item.creator || item.author || undefined
}));
return {
title: feed.title || 'RSS Feed',
description: feed.description || undefined,
link: feed.link || undefined,
articles
};
} catch (error) {
throw new Error(`Error reading RSS feed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
});
export const rssAgent = new Agent({
name: 'RSS Feed Agent',
instructions:
`You are a specialized RSS feed reader assistant. ` +
`Today is ${today}. ` +
`Your capabilities include: ` +
`- Reading and parsing RSS feeds from URLs ` +
`- Summarizing RSS feed content and articles ` +
`- Providing information about the latest articles from various sources ` +
`- Helping users stay updated with news and content from their favorite websites ` +
`When users provide RSS feed URLs or ask about specific feeds: ` +
`- Use the rssFeedTool to fetch and parse the RSS content ` +
`- Provide clear summaries of the articles ` +
`- Highlight the most important or recent articles ` +
`- Format the information in an easy-to-read way ` +
`You can help with popular RSS feeds from tech blogs, news sites, ` +
`podcasts, and other content sources. Always be helpful and provide ` +
`organized information about the RSS content.`,
model: google('gemini-2.5-flash-preview-04-17'),
memory,
tools: { rssFeedTool }
});

Here we have created an agent with the current date and it has a tool that allows it to retrieve RSS feeds using a URL. In the .env file, you must also add GOOGLE_GENERATIVE_AI_API_KEY=your-key, for it to work.

Now we will create an agent that will rewrite the news received.

agentWriter.ts

import 'dotenv/config';
import { Agent } from "@mastra/core/agent";
import { Memory } from '@mastra/memory';
import { google } from '@ai-sdk/google';
import { LibSQLStore } from '@mastra/libsql';
const memory = new Memory({
storage: new LibSQLStore({
url: "file:./memory.db",
}),
});
export const writerAgent = new Agent({
name: 'Content Writer',
instructions: `You are an expert content rewriter specialized in tech news and research aggregation. Your goal is to:
1. Take multiple pieces of content (RSS feeds, web research, etc.) and create a comprehensive digest
2. Identify common trends and important topics across sources
3. Organize the information with clear sections
4. Maintain accuracy while improving readability and flow
5. Respond in plain text with no Markdown formatting
When rewriting:
- Prioritize the most important and current information
- Group related topics together
- Highlight major trends and innovations
- Use a professional yet accessible tone
- Include dates and sources when available
- Do NOT use Markdown; answer as if sending a simple text message
`,
memory,
model: google('gemini-2.5-flash-preview-04-17')
});

And now let’s move on to the workflow ! Where we will specify which URLs to check. We could go further and create a command for the user to add sources and during the workflow we would check the sources in the database.

but for now let’s keep it simple.

Create a workflows folder and inside create an index.ts

import 'dotenv/config';
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";
import { rssAgent } from "../agents/agentRss";
import { writerAgent } from "../agents/agentWriter";
// Step 1: Read Blog du Modérateur Tech
const blogModeratorStep = createStep({
id: "blog-moderator-step",
description: "Reads the RSS feed from Blog du Modérateur Tech",
inputSchema: z.object({}),
outputSchema: z.object({
blogModeratorContent: z.string()
}),
execute: async () => {
const message1 = "📡 Step 1: Reading RSS feed from Blog du Modérateur Tech...";
console.log(message1);
const response = await rssAgent.generate(
"Read the RSS feed from https://www.blogdumoderateur.com/tech/feed/ and give me a CONCISE summary of the 3 most recent articles only. Limit yourself to 2-3 sentences per article.",
{
resourceId: "rss_workflow",
threadId: "rss_workflow_blog_moderator"
}
);
const message2 = "✅ Step 1: Blog du Modérateur Tech completed";
console.log(message2);
return {
blogModeratorContent: response.text
};
}
});
// Step 2: Read Le Monde Informatique Cloud
const mondeInformatiqueStep = createStep({
id: "monde-informatique-step",
description: "Reads the RSS feed from Le Monde Informatique Cloud",
inputSchema: z.object({}),
outputSchema: z.object({
mondeInformatiqueContent: z.string()
}),
execute: async () => {
const message1 = "📡 Step 2: Reading RSS feed from Le Monde Informatique Cloud...";
console.log(message1);
const response = await rssAgent.generate(
"Read the RSS feed from https://www.lemondeinformatique.fr/flux-rss/thematique/le-monde-du-cloud-computing/rss.xml and give me a CONCISE summary of the 3 most recent articles on cloud computing. Limit yourself to 2-3 sentences per article.",
{
resourceId: "rss_workflow",
threadId: "rss_workflow_monde_informatique"
}
);
const message2 = "✅ Step 2: Le Monde Informatique Cloud completed";
console.log(message2);
return {
mondeInformatiqueContent: response.text
};
}
});
// Step 3: Read Forbes Innovation
const forbesStep = createStep({
id: "forbes-step",
description: "Reads the RSS feed from Forbes Innovation",
inputSchema: z.object({}),
outputSchema: z.object({
forbesContent: z.string()
}),
execute: async () => {
const message1 = "📡 Step 3: Reading RSS feed from Forbes Innovation...";
console.log(message1);
const response = await rssAgent.generate(
"Read the RSS feed from https://www.forbes.com/innovation/feed and give me a CONCISE summary of the 3 most recent articles on innovation. Limit yourself to 2-3 sentences per article.",
{
resourceId: "rss_workflow",
threadId: "rss_workflow_forbes"
}
);
const message2 = "✅ Step 3: Forbes Innovation completed";
console.log(message2);
return {
forbesContent: response.text
};
}
});
// Step 4: Rewrite and final synthesis
const rewriteStep = createStep({
id: "rewrite-synthesis-step",
description: "Rewrites and synthesizes all RSS content",
inputSchema: z.object({
"blog-moderator-step": z.object({ blogModeratorContent: z.string() }),
"monde-informatique-step": z.object({ mondeInformatiqueContent: z.string() }),
"forbes-step": z.object({ forbesContent: z.string() })
}),
outputSchema: z.object({
finalDigest: z.string()
}),
execute: async ({ inputData }) => {
const message1 = "🔄 Step 4: Synthesis and rewriting...";
console.log(message1);
const blogModeratorContent = inputData["blog-moderator-step"].blogModeratorContent;
const mondeInformatiqueContent = inputData["monde-informatique-step"].mondeInformatiqueContent;
const forbesContent = inputData["forbes-step"].forbesContent;
const synthesisPrompt = `
Create a professional daily tech digest from the following RSS content:
Source 1 - Blog du Modérateur Tech:
${blogModeratorContent}
Source 2 - Le Monde Informatique (Cloud):
${mondeInformatiqueContent}
Source 3 - Forbes Innovation:
${forbesContent}
Instructions:
1. Create an organized synthesis by technological themes
2. Identify common trends and important topics
3. Prioritize information by importance
4. Use plain text without Markdown, like a WhatsApp message
5. Always indicate the source and date for each article
6. Keep an informative and engaging tone with clear sections
`;
const response = await writerAgent.generate([
{ role: "user", content: synthesisPrompt }
]);
const message2 = "✅ Step 4: Final synthesis completed, coming soon!";
console.log(message2);
return {
finalDigest: response.text
};
}
});
// Create the complete workflow
export const rssWorkflow = createWorkflow({
id: "rss-aggregator-workflow",
description: "Aggregates and rewrites content from multiple tech RSS sources",
inputSchema: z.object({}),
outputSchema: z.object({
finalDigest: z.string()
})
})
.parallel([
blogModeratorStep,
mondeInformatiqueStep,
forbesStep
])
.then(rewriteStep)
.commit();

Very well we have a workflow now ! Which has three steps that are done in parallel (fetching RSS and synthesizing by LLM) and then the ‘writer’ agent summarizes the news.

You might say very well but I want to chat with me, no problem !

Master Agent

We will soon test our code, but before that, we have a few steps left.

Create a agentMaster.ts in the agents folder :

import 'dotenv/config';
import { Agent } from "@mastra/core/agent";
import { Memory } from '@mastra/memory';
import { rssWorkflow } from '../workflows';
import { google } from '@ai-sdk/google';
import { LibSQLStore } from '@mastra/libsql';
const memory = new Memory({
storage: new LibSQLStore({
url: "file:./memory.db",
}),
});
const today = new Date().toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
export const chefAgent = new Agent({
name: 'News Assistant',
instructions:
`You are a specialized news assistant focused on providing tech news and current events. ` +
`Today is ${today}. ` +
`Always reply in plain text without using Markdown formatting. ` +
`Do not use asterisks or any other symbols for bold or italic text. ` +
`MANDATORY RULE: For ANY request related to news, actualités, current events, or tech updates, ` +
`you MUST use the rssWorkflow immediately. This includes: ` +
`- "actualités" ` +
`- "news" ` +
`- "dernières nouvelles" ` +
`- "infos du jour" ` +
`- "tech news" ` +
`- "what's happening" ` +
`- Any request for current information ` +
`The rssWorkflow provides real-time tech news from Blog du Modérateur, Le Monde Informatique, and Forbes Innovation. ` +
`Always use rssWorkflow first for news requests - do not attempt to provide news from your knowledge base. `,
model: google('gemini-2.5-flash-preview-04-17'),
memory,
workflows: { rssWorkflow },
});

And in this agent, we have given the workflow as a tool, so it can execute the workflow to retrieve the news. Great right ?!

Discord Test

Let’s modify the index.ts to add the /chat command and generate a conversation with the master agent :

import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from 'discord.js';
import dotenv from 'dotenv';
import { chefAgent } from './agents/agentMaster';
// Load environment variables
dotenv.config();
// Create a new instance of the Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
],
});
// Define the /ping and /chat commands
const commands = [
new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with pong!')
.toJSON(),
new SlashCommandBuilder()
.setName('chat')
.setDescription('Chat with the AI assistant')
.addStringOption(option =>
option.setName('message')
.setDescription('Your message to the assistant')
.setRequired(true))
.toJSON(),
];
// Function to register slash commands
async function deployCommands() {
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN!);
try {
console.log('Deploying slash commands...');
await rest.put(
Routes.applicationCommands(process.env.CLIENT_ID!),
{ body: commands },
);
console.log('Slash commands deployed successfully!');
} catch (error) {
console.error('Error deploying commands:', error);
}
}
// Event: Bot ready
client.once('ready', async () => {
console.log(`Bot connected as ${client.user?.tag}!`);
await deployCommands();
});
// Event: Interaction with slash commands
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const { commandName } = interaction;
if (commandName === 'ping') {
await interaction.reply('Pong!');
} else if (commandName === 'chat') {
const message = interaction.options.getString('message', true);
try {
// Respond immediately to avoid timeout
await interaction.reply('🤖 Searching for information...');
// Create a unique threadId based on user and date
const uniqueThreadId = `discord_chat_${interaction.user.id}_${new Date().toDateString().replace(/\s/g, '_')}`;
// Send the message to the agent
const response = await chefAgent.generate(message, {
resourceId: `discord_user_${interaction.user.id}`,
threadId: uniqueThreadId,
});
// Function to split text into chunks of max 2000 characters
const splitMessage = (text: string, maxLength: number = 2000): string[] => {
if (text.length <= maxLength) {
return [text];
}
const chunks: string[] = [];
let currentChunk = '';
// Split by paragraphs first
const paragraphs = text.split('\n\n');
for (const paragraph of paragraphs) {
// If the paragraph alone exceeds the limit, split by sentences
if (paragraph.length > maxLength) {
const sentences = paragraph.split('. ');
for (const sentence of sentences) {
const potentialChunk = currentChunk + (currentChunk ? '\n\n' : '') + sentence + (sentence.endsWith('.') ? '' : '.');
if (potentialChunk.length > maxLength) {
if (currentChunk) {
chunks.push(currentChunk.trim());
currentChunk = sentence + (sentence.endsWith('.') ? '' : '.');
} else {
// If even a sentence exceeds, cut it brutally
chunks.push(sentence.substring(0, maxLength - 3) + '...');
}
} else {
currentChunk = potentialChunk;
}
}
} else {
const potentialChunk = currentChunk + (currentChunk ? '\n\n' : '') + paragraph;
if (potentialChunk.length > maxLength) {
chunks.push(currentChunk.trim());
currentChunk = paragraph;
} else {
currentChunk = potentialChunk;
}
}
}
if (currentChunk) {
chunks.push(currentChunk.trim());
}
return chunks;
};
// Split the response if necessary
const messageChunks = splitMessage(response.text);
// Send the first chunk by modifying the initial response
await interaction.editReply(messageChunks[0]);
// Send the following chunks as new messages
for (let i = 1; i < messageChunks.length; i++) {
await interaction.followUp(messageChunks[i]);
}
} catch (error) {
console.error('Error generating response:', error);
await interaction.editReply('Sorry, an error occurred while processing your request. 😔');
}
}
});
// Error handling
client.on('error', (error) => {
console.error('Discord client error:', error);
});
process.on('unhandledRejection', (error) => {
console.error('Unhandled promise rejection:', error);
});
// Bot connection
client.login(process.env.DISCORD_TOKEN);

Now do /chat and ask “What are the news of the day ? ”

actuResponse

Isn’t it great ?

Let’s add a web workflow to ask for more information about a news !

Web

To avoid recreating an agent, you can use https://tsai-registry.dev.

I will take this opportunity to add the web agent, before that, go to create a firecrawl.dev account and retrieve your free API key.

Run the following command to add your web agent :

Fenêtre de terminal
npx tsai-registry add firecrawl-agent

The utility will ask you for your firecrawl API key to feed the .env directly.

Once installed, you should have a mastra/registry/agent/firecrawl… folder, I will take the firecrawl folder and put it in my src/agents.

I will not go into detail about the agent configuration, all you have to do is replace openai with google like the other agents.

Then, you can create a web.ts in the workflows folder

import 'dotenv/config';
import { createWorkflow, createStep } from "@mastra/core/workflows";
import { z } from "zod";
import { firecrawlAgent } from "../agents/firecrawl-agent/index";
import { writerAgent } from "../agents/agentWriter";
// Step 1: Web search
const webSearchStep = createStep({
id: "web-search-step",
description: "Performs a web search on the given topic",
inputSchema: z.object({
query: z.string().describe("The topic or search query")
}),
outputSchema: z.object({
searchResults: z.string().describe("The compiled search results")
}),
execute: async ({ inputData }) => {
const { query } = inputData;
const message1 = "🔎 Step 1: Web search in progress...";
console.log(message1);
const prompt = `Search for detailed information on: ${query}`;
const { text } = await firecrawlAgent.generate([
{ role: "user", content: prompt }
]);
const message2 = "✅ Step 1: Web search completed.";
console.log(message2);
return {
searchResults: text
};
}
});
// Step 2: Rewrite content
const rewriteStep = createStep({
id: "rewrite-step",
description: "Rewrites and improves the search content",
inputSchema: z.object({
searchResults: z.string().describe("The search results to rewrite")
}),
outputSchema: z.object({
rewrittenContent: z.string().describe("The rewritten and improved content")
}),
execute: async ({ inputData }) => {
const { searchResults } = inputData;
const message1 = "✍️ Step 2: Rewriting content in progress...";
console.log(message1);
const prompt = `
Rewrite and improve the following content in a clear and engaging way:
${searchResults}
Instructions:
- Organize the content with appropriate headings
- Improve readability and structure
- Keep all important information
- Make the tone professional but accessible
`;
const { text } = await writerAgent.generate([
{ role: "user", content: prompt }
]);
const message2 = "✅ Step 2: Rewriting completed.";
console.log(message2);
return {
rewrittenContent: text
};
}
});
// Create the complete workflow
export const webResearchWorkflow = createWorkflow({
id: "web-research-workflow",
description: "Web research and content rewriting workflow",
inputSchema: z.object({
query: z.string().describe("The research topic")
}),
outputSchema: z.object({
rewrittenContent: z.string().describe("The final rewritten and improved content")
})
})
.then(webSearchStep)
.then(rewriteStep)
.commit();

Here is my new master agent with the workflow and additional instructions in the prompt :

export const chefAgent = new Agent({
name: 'Chef Assistant',
instructions:
`You are a helpful global assistant that can help with various tasks and questions. ` +
`Today is ${today}. ` +
`Always reply in plain text without using Markdown formatting. ` +
`Do not use asterisks or any other symbols for bold or italic text. ` +
`You have access to different workflows that can help you provide better answers: ` +
`- rssWorkflow: **MANDATORY** - Use this IMMEDIATELY when users ask for news, actualités, current events, or tech updates. This provides a comprehensive tech digest from multiple sources. ` +
`- webResearchWorkflow: Use this when you need other current information, research, or web search ` +
`IMPORTANT RULES: ` +
`- When users ask for "actualités", "news", "dernières nouvelles", "infos du jour", or any news-related request: YOU MUST use rssWorkflow first ` +
`- Do not attempt to provide news from your knowledge - always use rssWorkflow for current news ` +
`- The rssWorkflow provides real-time tech news aggregated from Blog du Modérateur, Le Monde Informatique, and Forbes Innovation ` +
`- Keep conversations focused and concise to avoid token overflow ` +
`You can assist with: ` +
`- General questions and information ` +
`- News and current events (use rssWorkflow MANDATORY) ` +
`- Research and web search (use webResearchWorkflow) ` +
`- Technical help and explanations ` +
`- Creative tasks and brainstorming ` +
`Always try to be helpful, accurate, and use the available workflows when they can ` +
`improve your response quality.`,
model: google('gemini-2.5-flash-preview-04-17'),
memory,
workflows: { rssWorkflow, webResearchWorkflow },
});

And now let’s ask for more information about a news :

finalResponse

Conclusion

I hope this tutorial will make you want to use Mastra, it is a pleasant TypeScript framework to use with an active community and quick support from the founders.

The repo for this tutorial : https://github.com/Killian-Aidalinfo/discord-bot-actu

Also, if you want to participate in the community building of agents and workflows you can participate here : https://github.com/aidalinfo/tsai-registry

That’s all for me !

feel free to follow me on social networks :

Utiliser les function calling en IA : guide pratique Mistral/OpenAI

Mistral Function callings TypeScript

Vous avez déjà utilisé des API d’intelligence artificielle ? Si vous avez déjà intégré OpenAI API ou Mistral API, vous savez que les réponses de l’IA sont très imprévisibles, même si notre prompt demande un format JSON, cela ne fonctionne pas à tous les coups.

Pour résoudre ce problème, nous allons utiliser le concept de function calling, qui est disponible dans la plupart des API d’IA. Cela va plutôt dépendre du modèle.

Les function calling, c’est quoi ?

Les function calling sont des fonctions qui sont appelées par l’IA. On va donc pouvoir exécuter des fonctions de notre choix pendant la réponse ou alors … Formater la réponse avec des clés prédéfinies.

Dans ce tutoriel, je ne vais pas explorer l’exécution de fonctions externes, mais plutôt je vais utiliser les fonctions callings pour formater la réponse, ce qui va nous permettre de récupérer des données structurées de la même façon à tous les coups.

Documentation function calling Mistral

Mistral

Si vous ne le savez pas, Mistral est une IA open source, développée par des français ! Mistral

Elle dispose de plusieurs modèles, qui permettent différentes utilisations. Cela dépend donc de vos besoins ! Il existe même un modèle de vision qui permet d’extraire les données d’une image !

Est-ce qu’on ne ferait pas un petit workflow avec deux modèles ?

Projet

Bon, pour avoir un petit exemple, je vais coder rapidement une API qui va nous permettre l’upload d’une image d’un excel de commande de pièce. Avec cette image nous allons utiliser le modèle de vision de Mistral pour extraire les données de la pièce et ensuite le modèle de texte avec une function calling pour formater la réponse.

J’utiliserai l’image suivante pour mes tests :

Excel de commande de pièce

Start

Pour le développement rapidement de cette API, je vais utiliser Bun avec Hono pour avoir une API rapidement et simplement.

Si vous voulez découvrir comment débuter avec Bun et Hono, j’ai réalisé récemment un article sur le sujet : Découverte de Bun

Upload de fichier

Voici mes deux fichiers qui me permettent d’uploader une image et de récupérer le base64 de l’image qui permettra l’envoi à l’API de Mistral.

src/index.ts
import { Hono } from 'hono';
import { ExtractorFunction } from './controllers/ExtractorImage';
const app = new Hono();
app.get('/status', (c) => {
return c.text('Ready 🔥🔥🔥')
});
app.post('/upload', ...ExtractorFunction);
export default {
port: 4000,
fetch: app.fetch
}
src/controllers/ExtractorImage.ts
import { createFactory } from "hono/factory";
import { HTTPException } from "hono/http-exception";
const factory = createFactory();
interface UploadedFile {
name: string;
size: number;
type: string;
}
export async function extractFile(request: any){
const formData = await request.req.formData();
const file = formData.get("file") as UploadedFile;
if (!(file instanceof File)) {
throw new HTTPException(400, { message: "No file uploaded" });
}
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
const base64Image = buffer.toString("base64");
return base64Image;
}
export const ExtractorFunction = factory.createHandlers(async (c) => {
const base64Image = await extractFile(c);
return c.text(`File uploaded : ${base64Image}`);
});

Configuration Postman :

Configuration Postman

Mistral API

Bien, maintenant que nous avons l’upload, rajoutons une étape d’analyse qui nous permettras de récupérer le texte d’une image.

Pour voir les models disponibles, voici un lien très utile : https://docs.mistral.ai/getting-started/models/models_overview/

Et le lien de la documentation de Mistral : Documentation

En premier, nous allons installer le SDK de mistral AI pour TypeScript. SDK

Fenêtre de terminal
bun add @mistralai/mistralai

Ensuite dans mon fichier ExtractorImage.ts, je vais ajouter le client Mistral et utiliser le model de vision pixtral-12b-2409.

src/controllers/ExtractorImage.ts
import { Mistral } from '@mistralai/mistralai';
const apiKey = Bun.env.MISTRAL_AI_API_KEY;
const client = new Mistral({ apiKey: apiKey });
interface ImageResponse {
choices?: {
message: {
content: string;
};
}[];
}
async function analyzeImage(base64Image: string) {
try {
const imageResponse = await client.chat.complete({
model: "pixtral-12b-2409",
messages: [
{
role: "user",
content: [
{ type: "text", text: "Please analyze the provided image. If it contains a table, extract each row and convert it into a structured JSON format. For each row, use the exact values without filling in missing fields with data from other rows. If a field is empty, leave it empty. Ensure that each JSON object represents exactly one row of the table. Thank you!" },
{
type: "image_url",
imageUrl: `data:image/jpeg;base64,${base64Image}`,
},
],
},
],
}) as ImageResponse;
return imageResponse?.choices?.[0]?.message?.content;
} catch (error) {
throw new HTTPException(500, { message: `Error analyzing image : ${error}` });
}
}

Et du coup, on ajoute cette fonction dans notre controller HTTP.

export const ExtractorFunction = factory.createHandlers(async (c) => {
const base64Image = await extractFile(c);
const analyzeResult = await analyzeImage(base64Image);
return c.text(`File uploaded and analyzed : ${analyzeResult}`);
});

Maintenant, si on reteste notre POST d’image dans postman, nous devrions avoir le texte de l’image grâce à l’IA !

Mistral AI analyse d&#x27;image

Nous voyons que l’IA nous donne le texte de l’image, mais la structure n’est pas parfaite, il y a du texte, JSON etc. Ce qui n’est pas idéal pour traiter notre sortie de façon automatique, pour intéragir avec d’autres partie de notre code.

Function calling

Nous arrivons enfin au sujet principal de cet article, les function calling, nous allons structurer la réponse d’analyse d’IA pour que nous n’ayons que un JSON avec des clés prédéfinies.

Pour cela, nous devrons utiliser un autre modèle qui prend en charge les function calling : mistral-small-latest.

Toujours dans le ExtractorImage.ts, nous allons ajouter du code :

src/controllers/ExtractorImage.ts
const tools: any[] = [
{
"type": "function",
"function": {
"name": "retrieve_product_information",
"description": "Retrieve detailed information about a product including manufacturer, description, quantity, and supplier.",
"parameters": {
"type": "object",
"properties": {
"product_name": {
"type": "string",
"description": "The name of the product to retrieve information about.",
},
"manufacture": {
"type": "string",
"description": "The name of the manufacturer or brand of the product.",
},
"description": {
"type": "string",
"description": "A brief description of the product, including its features and specifications.",
},
"fabricant": {
"type": "string",
"description": "The company responsible for producing the product.",
},
"qty": {
"type": "integer",
"description": "The quantity of the product available in stock.",
}
},
"required": ["product_name","manufacture", "description", "fabricant", "qty"],
},
},
}
];
async function resultFunctionCalling(text: string){
const result = await client.chat.complete({
model: "mistral-large-latest",
messages: [{ role: "user", content: `Extract all information from the provided text and format the response using the tools in JSON format. Do not reply with any text; use only the provided tools to format the response. The text: ${text}` }],
tools: tools,
tool_choice: "auto",
} as any);
console.log("(1)", result);
console.log("(2)", result?.choices?.[0]?.message);
console.log("(3)", result?.choices?.[0]?.message?.content);
const results = [];
//Execute les functions calling si elles existent
if (result?.choices?.[0]?.message?.toolCalls) {
for (const call of result.choices[0].message.toolCalls) {
// Récupère le nom de la fonction et les arguments
const functionName = call.function.name;
const resultFunction = JSON.parse(call.function.arguments as string);
console.log("The function name :", functionName);
console.log("The function result :", resultFunction);
// Push du résultat de la fonction dans les résultats
results.push(resultFunction)
}
console.log("results", results)
return results;
}
else {
return result?.choices?.[0]?.message?.content;
}
}

Puis dans le même fichier, je remodifie le controller HTTP pour appeler la nouvelle fonction.

export const ExtractorFunction = factory.createHandlers(async (c) => {
const base64Image = await extractFile(c);
const analyzeResult = await analyzeImage(base64Image);
const result = await resultFunctionCalling(analyzeResult as string) as Array<JSON>;
return c.json(result);
});

On va refaire un test postman !

Résultat :

[
{
"product_name": "STMAV340TTR",
"manufacture": "",
"description": "SiC - MOSFET Silicon Carbide (SiC) MOSFET - ElseSiC_13 mohm, 1200 V, MJS, TO-247-4L Silicon Carbide (SiC) MOSFET - ElseSiC_13 mohm, 1200 V, MJS, TO-247-4L",
"fabricant": "MULTICOMP PRO",
"qty": 254
},
{
"product_name": "ERA3AR8203V",
"manufacture": "",
"description": "Résistance à puce CMS, 47 kohm, ± 1%, 125 mW, 0805 [2012 Metric], Couche épaisse",
"fabricant": "",
"qty": 570
}
]

Résultat formaté par la fonction calling

À présent vous savez utiliser l’IA pour retourner des données structurées, faites plusieurs upload, vous verrez que vous aurez toujours votre JSON structuré de la même façon ! 🚀

Conclusion

Lien du projet sur Github

Maintenant que vous avez appris à utiliser les function calling, vous pouvez intégrer facilement l’IA en contrôlant son output dans votre code, j’espère que ce tutoriel vous a aidé pour comprendre ce concept et vous donnera des idées pour vos projets 🙃

Réseaux sociaux

Comme d’habitude, n’hésitez pas à me rejoindre sur les réseaux sociaux pour plus de contenu !

Sources

Mistral AI

Mistral AI Documentation

Mistral AI Function calling

Mistral AI Models

Mistral AI SDK

Bun

Hono

Découverte de Bun : le nouveau runtime JavaScript rapide et moderne

À la découverte de bun

Bonjour à tous ! Aujourd’hui on se retrouve pour découvrir Bun !

Bun

C’est quoi Bun ?

Bun est un framework de développement qui a pour but de ‘remplacer’ Node.js pour le développement en TypeScript/JavaScript. Bun contient plusieurs modules natifs, il va aussi nous permettre de gérer les packages NPM mais aussi de réaliser des tests unitaires très facilement.

Avant de commencer

Avant d’aller dans notre éditeur de code préféré, je tiens à souligner que je n’ai jamais fait de TypeScript, donc ce sera de la découverte pour moi.

L’idée, c’est de créer un projet qui sera une petite API, une base de données SQLite et de créer les tests unitaires.

Le projet

Mais on va coder quoi ?

Pour créer cet article, je profite d’un projet qui m’a été donné à mon école (CESI). Le projet est assez grand pour le peu de temps accordé donc nous devons prioriser les tâches, je ne vais pas vous expliquer tout le sujet, donc je vais juste vous énoncer quelques user stories que nous avons choisies.

User stories

User storie 1

En tant qu’utilisateur je souhaite m’inscrire pour parier sur un match.

Remarque : L’utilisateur ne peut pas s’inscrire s’il a moins de 18 ans. Il faut également connaître le pays de l’utilisateur.

User storie 2

En tant qu’utilisateur je veux pouvoir me connecter à mon compte.

Remarque : Un token JWT de 1H devra être fourni.

User storie 3

En tant qu’utilisateur je peux visualiser les matchs disponibles ainsi que leurs côtes.

Remarque : Pas besoins de compte pour visualiser les matchs. Remarque : Les matchs seront fictifs dans un JSON.

User storie 4

(optionnel) En tant qu’utilisateur connecté je peux parier sur un match une somme.

Remarque : Le match ne doit pas être en cours de jeu.

Installation de bun

Maintenant que nous avons nos user stories qui déterminent nos tâches, on va pouvoir commencer à coder !

Pour commencer, on va installer bun.

Sous linux et MacOS nous utiliserons cette commande :

Fenêtre de terminal
curl -fsSL https://bun.sh/install | bash

Voici la sortie de la commande :

Fenêtre de terminal
Added "~/.bun/bin" to $PATH in "~/.zshrc"
To get started, run:
exec /usr/bin/zsh
bun --help

Nous allons donc réaliser la commence exec et tester bun —help.

Screen de bun —help :

Screen de bun --help

Parfait tout va bien !

Je vais créer un nouveau dossier que je vais appeler “bun-trd”

Fenêtre de terminal
mkdir bun-trd

Et je vais aller dans le dossier :

Fenêtre de terminal
cd bun-trd

Création du projet

Maintenant nous allons créer le projet avec bun init.

Fenêtre de terminal
bun init

Screen de bun init

Nous voyons que nous avons un dossier avec tous les fichiers nécessaires pour notre projet. (Pas comme NodeJS, créer ne crée pas le fichier index.js)

Dans le package.json je vais ajouter la partie scripts qui va nous permettre de lancer le fichier index.ts qui contient un console.log.

"scripts": {
"dev": "bun --hot index.ts"
},

C’est quoi —hot ? Magie ! C’est une option qui permet de lancer le programme avec hot reload. Pas besoin d’ajouter nodemon ou autres. :)

Il nous reste plus qu’à lancer le projet avec bun run dev

Screen de bun run dev

Et voilà notre projet est lancé !

Avant de bien structurer notre projet, nous allons ajouter Hono, u module qui permet la création d’API Rest très facilement.

Ajout de Hono

Normalement nous pouvons faire bun create hono projetname, mais comme nous avons déjà initialisé notre projet, nous allons utiliser bun add hono

Screen de bun add hono

On va commencer les choses sérieuse, donc je vais structurer le projet.

Voici ma structure choisis :

.
├── bun.lockb
├── nodes_modules
├── package.json
├── README.md
├── data
├── .env
├── tests
├── src
│ ├── controllers
│ ├── index.ts
│ ├── models
│ └── utils
└── tsconfig.json

Persistance des données

Avant de commencer notre api nous allons explorer la persistance des données, pour cela nous allons utiliser une base de données SQLite.

Bun à une gestion native de SQLite ?! Trop bien ! https://bun.sh/docs/api/sqlite

Bon bah on va créer un fichier db.ts dans src/utils.

Voici pour l’instant mon fichier db.ts :

import { Database } from "bun:sqlite";
//Connexion à la base de données (La création est faite si elle n'existe pas)
export const db = new Database("data/trd.sqlite", { create: true });
//Création de la table utilisateur par défaut
db.run(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
prenom TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
age INTEGER,
pays TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`);

Je n’ai pas encore testé mais dans l’idée, nous pourrons utiliser db dans d’autre fichier pour créer des query,transaction et autres.

La partie db.run nous permet de créer la table utilisateur si elle n’existe pas, nous en ajouterons d’autres plus tard.

dans mon fichier index.ts je vais juste ajouter l’import de db.ts pour initialiser la base de données.

import './utils/db';

Création de l’API

Maintenant que nous avons notre base de données, nous allons créer les différents endpoints ainsi que les controllers.

Pour commencer, je vais créer un fichier dans le dossier controllers nommé usersController.ts.

UsersController

Création d’utilisateur

Voici mon fichier usersController.ts avec la fonction qui permet la création d’un utilisateur ainsi que son helper qui servira à notre futur route :

// Import du helper create Factory de Hono
// https://hono.dev/docs/helpers/factory
import { createFactory } from "hono/factory";
// Import de l'exception HTTP
import { HTTPException } from 'hono/http-exception'
// Import du connecteur SQLite
import { db } from "../utils/db";
// Création d'un type utilisateur
type user = {
id: number;
nom: string;
prenom: string;
email: string;
password: string;
age: number;
pays: string;
created_at: string;
};
// Création d'un type d'input utilisateur sans id et created_at
type userInput = Omit<user, "id" | "created_at">;
//Instanciation du Factory
const factory = createFactory();
// Création d'une fonction qui permet de créer un utilisateur
// la fonction est en dehors du helper pour faciliter la création des test
export async function fonctionUserCreate(userInput: userInput){
//Vérification des champs obligatoires
if (!userInput.email || !userInput.password || !userInput.nom || !userInput.prenom || !userInput.age || !userInput.pays) {
throw new HTTPException(400, {message: "Tous les champs sont obligatoires"});
}
//Vérification de l'age
if(userInput.age < 18){
throw new HTTPException(400, {message: "L'age doit être supérieur à 18"});
}
//Hashage du mot de passe
//https://bun.sh/guides/util/hash-a-password
const hashedPassword = await Bun.password.hash(userInput.password);
// Insertion de l'utilisateur dans la base de données
try {
db.run(
"INSERT INTO users (nom, prenom, email, password, age, pays) VALUES (?, ?, ?, ?, ?, ?)",
[userInput.nom,
userInput.prenom,
userInput.email,
hashedPassword,
userInput.age,
userInput.pays]
);
return 'Utilisateur créé avec succès';
}
catch (err) {
//Envoie d'une erreur si la création de l'utilisateur a échoué
throw new HTTPException(500, {message: `Erreur lors de la création de l'utilisateur: ${err}`});
}
}
export const userCreate = factory.createHandlers(async (c) => {
// Récupéreration des donnée de la requête
const userInput = await c.req.json<userInput>();
const message = await fonctionUserCreate(userInput);
return c.text(message);
})

J’ai commenté au maximum les différentes parties de mon code pour que vous compreniez mieux l’utilité de chaque ligne.

D’ailleurs pour faire un retour rapide, je suis assez choqué du nombre de documentation pour Bun alors qu’il est très récent.

Par exemple pour le hashage de mot passe, il y a un module intégré et même bcrypt. https://bun.sh/guides/util/hash-a-password

Maintenant, modifions l’index.ts pour ajouter hono et notre route.

import './utils/db';
import { Hono } from "hono";
import { userCreate} from './controllers/usersController';
const app = new Hono();
// Route de status
app.get("/", (c) => {
return c.text("Ready");
});
//Partie utilisateur
app.post("/user", ...userCreate);
//https://hono.dev/docs/getting-started/bun
//https://bun.sh/guides/runtime/read-env
export default {
port: Bun.env.API_PORT,
fetch: app.fetch
}

Pour essayer les variables d’environnement, j’ai rajouter un fichier .env à la racine du projet.

Pour l’instant elle contient seulement ceci :

Fenêtre de terminal
API_PORT=3000

On teste la création de l’utilisateur ?

Voici la requête que je vais utiliser avec mon terminal directement :

Fenêtre de terminal
curl -X POST http://localhost:3000/user \
-H "Content-Type: application/json" \
-d '{
"nom": "STEIN",
"prenom": "Killian",
"email": "killian.stein@civilisation-it.fr",
"password": "SuperAzerty57",
"age": 23,
"pays": "France"
}'

Screen de curl

Et voilà notre utilisateur est créé ! On va vérifier en base ?

DB Browser utilisateur créé

Parfait, maintenant je vais essayer de créer un test unitaire pour notre fonction userCreate.

Test unitaire

L’avantage c’est que bun embarque directement Jest et une cli, donc l’intégration de test est très simple. https://bun.sh/docs/cli/test

Pour commencer, je vais créer un fichier dans le dossier tests qui sera nommé userCreate.test.ts.

Voici mon fichier userCreate.test.ts :

//Import des class de test via bun:test
import { describe, expect, test, jest } from "bun:test";
//Récupération de la fonction à tester
import { fonctionUserCreate } from "../src/controllers/usersController";
// Import de l'exception HTTP
import { HTTPException } from "hono/http-exception";
// Import de la db pour mock la méthode db.run
import { db } from "../src/utils/db";
// Mock de la méthode db.run pour éviter les écritures en base réelle
// https://jestjs.io/docs/mock-functions
db.run = jest.fn();
//Input standard pour la création d'un utilisateur
let userInput = {
nom: "Test",
prenom: "Killian",
email: "killian.stein@civilisation.fr",
password: "SuperAzerty57",
age: 23,
pays: "France",
};
//Création d'un bloc de test pour fonctionUserCreate
describe("Tests de la fonction fonctionUserCreate", () => {
// Test de la création d'un utilisateur
test("Création d'un utilisateur", async () => {
//Message de retour dans une variable
const resultUserCreation = await fonctionUserCreate(userInput);
//Vérification que ça retourne la valeur attendu
expect(resultUserCreation).toBe("Utilisateur créé avec succès");
});
// Test de la création d'un utilisateur avec un age inférieur à 18
test("Erreur de création de l'utilisateur avec un age inférieur à 18", async () => {
userInput.age = 17;
//Ici on met pas la fonction dans une variable sinon nous n'avons pas l'erreur Throw
expect(fonctionUserCreate(userInput)).rejects.toThrow(
new HTTPException(400, { message: "L'age doit être supérieur à 18" })
);
});
// Test de la création d'un utilisateur avec des champs manquants
test("Erreur de création de l'utilisateur avec des champs manquants", async () => {
userInput.age = 18;
userInput.email = "";
expect(fonctionUserCreate(userInput)).rejects.toThrow(
new HTTPException(400, { message: "Tous les champs sont obligatoires" })
);
});
});

Encore une fois, j’ai commenté au maximum les différentes parties de mon code pour que vous compreniez mieux l’utilité, et pour moi ici quand je regarderai mon tutoriel plus tard ^^”

Maintenant on va lancer notre test avec la commande bun test

Screen de bun test

Connexion d’un utilisateur

Je ne vais pas vous détailler tout le projet, mais je trouve ça intéressant d’avoir plusieurs outils à notre disposition directement, comme le hash de mot de passe ou le connecteur de base de données.

Pour continuer sur cette lancé je vais utiliser une autre intégration, la génération de token JWT ! Ce n’est pas vraiment bun qui l’intégre mais Hono embarque directement le module https://hono.dev/docs/helpers/jwt

Dans userController je vais donc ajouter le code suivant :

// Login utilisateur
import { sign } from "hono/jwt";
//Fonction de login de l'utilisateur
export async function fonctionUserLogin(userInput: Pick<userInput, "email" | "password">) {
//Vérification des champs obligatoires
if (!userInput.email || !userInput.password) {
throw new HTTPException(400, {
message: "Tous les champs sont obligatoires",
});
}
//Requete dans la base de données pour récupérer l'utilisateur
const user = db
.query("SELECT * FROM users WHERE email = ?")
.get(userInput.email) as user; //Nécessite as user en TS pour typé le résultat de la query
//Erreur si l'utilisateur n'existe pas
if(!user){
throw new HTTPException(400, { message: "Utilisateur inexistant" });
}
//Vérification du password
//https://bun.sh/guides/util/hash-a-password
const isMatch = await Bun.password.verify(userInput.password, user.password);
//Vérification si le password signé correspond
if (!isMatch) {
throw new HTTPException(400, { message: "Mot de passe incorrect" });
}
//Création du payload pour le token
const payload = {
id: user.id,
exp: Math.floor(Date.now() / 1000) + 60 * 60, // Expiration dans 60 minutes
};
// Création du token
const token = await sign(payload, Bun.env.JWT_SECRET as string);
return token;
}
export const userLogin = factory.createHandlers(async (c) => {
// Récupéreration des donnée de la requête
const userInput = await c.req.json<userInput>();
const result = await fonctionUserLogin(userInput);
return c.text(result);
});

Dans l’index.ts on va rajouter une route /login

app.post("/login", ...userLogin);

Résultat :

Login

Parfait tout ça ! Mais maintenant, je me demande comment faire les tests de login, sachant qu’on mock la fonction db donc on insère par l’utilisateur dedans.

En demandant à GPTo il m’a indiqué qu’en mode test notre NODE_ENV est égal à test. Donc nous pourrions modifier db.ts pour créer une database de test.

Et spoiler alerte, il a raison !

J’ai testé sans créer la base de données de test, c’est une galère de mock toutes les commandes SQLite.

Donc je vais faire un if à la création de la base de données pour créer une base de données de test si la variable d’environnement NODE_ENV est égale à test.

//Connexion à la base de données (La création est faite si elle n'existe pas)
export const db =
Bun.env.NODE_ENV === "test"
? new Database("data/trd_test.sqlite", { create: true })
: new Database("data/trd.sqlite", { create: true });
Test unitaire

Avec le changement de base de données, j’ai du intégré la suppression des données dans la tables, mais j’ai pu rajouter aussi le test d’erreur lorsque l’email existe déjà !

Voici mon fichier complet de test à présent :

//Import des class de test via bun:test
import { describe, expect, test, jest } from "bun:test";
//Récupération de la fonction à tester
import { fonctionUserCreate, fonctionUserLogin } from "../src/controllers/usersController";
// Import de l'exception HTTP
import { HTTPException } from "hono/http-exception";
// Import de la db vider les données de la base de données
import { db } from "../src/utils/db";
import { verify } from "hono/jwt";
//Récupération des tables de la base de données
const tables = db.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';").all() as { name: string }[];
// as name string[], permet de dire que le résultat de la query est un tableau
//Suppression des données de chaque table
for (const table of tables) {
db.run(`DELETE FROM ${table.name};`);
}
//Input standard pour la création d'un utilisateur
let userInput = {
nom: "Test",
prenom: "Killian",
email: "killian.stein@civilisation.fr",
password: "SuperAzerty57",
age: 23,
pays: "France",
};
//Création d'un bloc de test pour fonctionUserCreate
describe("Tests de la fonction fonctionUserCreate", () => {
// Test de la création d'un utilisateur
test("Création d'un utilisateur", async () => {
//Message de retour dans une variable
const resultUserCreation = await fonctionUserCreate(userInput);
//Vérification que ça retourne la valeur attendu
expect(resultUserCreation).toBe("Utilisateur créé avec succès");
});
test("Vérification d'un essaie de création avec une email déjà existant", async () => {
expect(fonctionUserCreate(userInput)).rejects.toThrow(
new HTTPException(500, { message: "Erreur lors de la création de l'utilisateur: SQLiteError: UNIQUE constraint failed: users.email" })
);
});
// Test de la création d'un utilisateur avec un age inférieur à 18
test("Erreur de création de l'utilisateur avec un age inférieur à 18", async () => {
userInput.age = 17;
//Ici on met pas la fonction dans une variable sinon nous n'avons pas l'erreur Throw
expect(fonctionUserCreate(userInput)).rejects.toThrow(
new HTTPException(400, { message: "L'age doit être supérieur à 18" })
);
});
// Test de la création d'un utilisateur avec des champs manquants
test("Erreur de création de l'utilisateur avec des champs manquants", async () => {
userInput.age = 18;
userInput.pays = "";
expect(fonctionUserCreate(userInput)).rejects.toThrow(
new HTTPException(400, { message: "Tous les champs sont obligatoires" })
);
});
});
//Input de login
let loginInput = {
email: userInput.email,
password: userInput.password,
}
// Tests de la fonction fonctionUserLogin
describe("Tests de la fonction fonctionUserLogin", () => {
test("Connexion réussie", async () => {
const token = await fonctionUserLogin(loginInput);
expect(token).toBeDefined();
// Décodage et vérification du token
const payload = await verify(token, Bun.env.JWT_SECRET as string);
expect(payload).toHaveProperty("id" && "exp");
//https://jestjs.io/docs/expect#tohavepropertykeypath-value
});
test("Échec de connexion avec un mot de passe incorrect", async () => {
loginInput.password = "Azerty57";
expect(fonctionUserLogin(loginInput)).rejects.toThrow(
new HTTPException(400, { message: "Mot de passe incorrect" })
);
});
test("Addresse email inexistante", async () => {
loginInput.email = "test@gmail.com";
expect(fonctionUserLogin(loginInput)).rejects.toThrow(
new HTTPException(400, { message: "Utilisateur inexistant" })
);
});
test("Échec de connexion avec des champs manquants", async () => {
loginInput.email = "";
loginInput.password = "";
expect(fonctionUserLogin(loginInput)).rejects.toThrow(
new HTTPException(400, { message: "Tous les champs sont obligatoires" })
);
});
});

On peut voir un résultat satisfaisant !

Nouveau test

MatchController

Maintenant que la partie utilisateur est fonctionnelle, on va passer aux matchs.

Pour travailler sur des données fictives, j’ai demandé à GPTo de créer un tableau de matchs.

const championsLeagueMatches = [
{
equipe1: "Paris Saint-Germain",
equipe2: "Manchester City",
date: new Date("2023-11-06T20:00:00"), // 6 novembre 2023, 20h00
odds: {
"1": 2.10, // Victoire de PSG
"N": 3.60, // Match nul
"2": 2.30, // Victoire de Manchester City
},
},
{
equipe1: "Real Madrid",
equipe2: "Inter Milan",
date: new Date("2023-11-07T20:00:00"), // 7 novembre 2023, 20h00
odds: {
"1": 1.85, // Victoire de Real Madrid
"N": 3.40, // Match nul
"2": 4.00, // Victoire de Inter Milan
},
},
{
equipe1: "Liverpool",
equipe2: "Bayern Munich",
date: new Date("2023-11-08T20:00:00"), // 8 novembre 2023, 20h00
odds: {
"1": 2.50, // Victoire de Liverpool
"N": 3.50, // Match nul
"2": 2.20, // Victoire de Bayern Munich
},
},
{
equipe1: "Chelsea",
equipe2: "Barcelona",
date: new Date("2023-11-09T20:00:00"), // 9 novembre 2023, 20h00
odds: {
"1": 2.25, // Victoire de Chelsea
"N": 3.20, // Match nul
"2": 2.40, // Victoire de Barcelona
},
},
{
equipe1: "Juventus",
equipe2: "Manchester United",
date: new Date("2023-11-10T20:00:00"), // 10 novembre 2023, 20h00
odds: {
"1": 2.40, // Victoire de Juventus
"N": 3.30, // Match nul
"2": 2.30, // Victoire de Manchester United
},
},
{
equipe1: "Atletico Madrid",
equipe2: "AC Milan",
date: new Date("2023-11-11T20:00:00"), // 11 novembre 2023, 20h00
odds: {
"1": 1.95, // Victoire de Atletico Madrid
"N": 3.30, // Match nul
"2": 3.90, // Victoire de AC Milan
},
},
{
equipe1: "Dortmund",
equipe2: "Porto",
date: new Date("2023-11-12T20:00:00"), // 12 novembre 2023, 20h00
odds: {
"1": 1.75, // Victoire de Dortmund
"N": 3.80, // Match nul
"2": 4.20, // Victoire de Porto
},
},
{
equipe1: "Benfica",
equipe2: "RB Leipzig",
date: new Date("2023-11-13T20:00:00"), // 13 novembre 2023, 20h00
odds: {
"1": 2.60, // Victoire de Benfica
"N": 3.40, // Match nul
"2": 2.10, // Victoire de RB Leipzig
},
},
];

Je vais partir de cette base, mais en modifiant quelques éléments pour que ce soit plus intéressant.

Je vais créer un fichier matchController.ts dans src/controllers :

// Import du helper create Factory de Hono
// https://hono.dev/docs/helpers/factory
import { createFactory } from "hono/factory";
// Import de l'exception HTTP
import { HTTPException } from "hono/http-exception";
// Import du connecteur SQLite
import { db } from "../utils/db";
const championsLeagueMatches = [
{
equipe1: "Paris Saint-Germain",
equipe2: "Manchester City",
date: new Date("2024-10-27T20:00:00"),
cote: {
"1": 2.10, // Victoire de PSG
"N": 3.60, // Match nul
"2": 2.30, // Victoire de Manchester City
},
},
{
equipe1: "Real Madrid",
equipe2: "Inter Milan",
date: new Date("2024-10-27T18:00:00"),
cote: {
"1": 1.85, // Victoire de Real Madrid
"N": 3.40, // Match nul
"2": 4.00, // Victoire de Inter Milan
},
},
{
equipe1: "Liverpool",
equipe2: "Bayern Munich",
date: new Date("2024-10-27T15:00:00"),
cote: {
"1": 2.50, // Victoire de Liverpool
"N": 3.50, // Match nul
"2": 2.20, // Victoire de Bayern Munich
},
},
{
equipe1: "Chelsea",
equipe2: "Barcelona",
date: new Date("2024-10-28T18:00:00"),
cote: {
"1": 2.25, // Victoire de Chelsea
"N": 3.20, // Match nul
"2": 2.40, // Victoire de Barcelona
},
},
{
equipe1: "Juventus",
equipe2: "Manchester United",
date: new Date("2024-10-28T18:00:00"),
cote: {
"1": 2.40, // Victoire de Juventus
"N": 3.30, // Match nul
"2": 2.30, // Victoire de Manchester United
},
},
{
equipe1: "Atletico Madrid",
equipe2: "AC Milan",
date: new Date("2024-10-28T18:00:00"),
cote: {
"1": 1.95, // Victoire de Atletico Madrid
"N": 3.30, // Match nul
"2": 3.90, // Victoire de AC Milan
},
},
{
equipe1: "Dortmund",
equipe2: "Porto",
date: new Date("2024-10-28T18:00:00"),
cote: {
"1": 1.75, // Victoire de Dortmund
"N": 3.80, // Match nul
"2": 4.20, // Victoire de Porto
},
},
{
equipe1: "Benfica",
equipe2: "RB Leipzig",
date: new Date("2024-10-28T18:00:00"),
cote: {
"1": 2.60, // Victoire de Benfica
"N": 3.40, // Match nul
"2": 2.10, // Victoire de RB Leipzig
},
},
];
type match = {
equipe1: string;
equipe2: string;
date: Date;
cote: {
"1": number;
"2": number;
"N": number;
};
};
export async function fonctionViewMatch(){
const today = new Date();
//Permet de récupérer juste les matchs avec la date du jour dans le tableau
const match = championsLeagueMatches.filter(match => match.date.getDate() === today.getDate()) as match[];
if(!match){
throw new HTTPException(400, { message: "Aucun match du jour" });
}
return match;
}
export const viewMatch = createFactory().createHandlers(async (c) => {
const match = await fonctionViewMatch();
return c.json(match);
});

Rajouter la route dans l’index :

app.get("/matchs", ...viewMatch);

Un petit curl pour tester curl http://localhost:3000/matchs

Et voici le retour :

[
{"equipe1":"Paris Saint-Germain","equipe2":"Manchester City","date":"2024-10-27T19:00:00.000Z","cote":{"1":2.1,"2":2.3,"N":3.6}},
{"equipe1":"Real Madrid","equipe2":"Inter Milan","date":"2024-10-27T17:00:00.000Z","cote":{"1":1.85,"2":4,"N":3.4}},
{"equipe1":"Liverpool","equipe2":"Bayern Munich","date":"2024-10-27T14:00:00.000Z","cote":{"1":2.5,"2":2.2,"N":3.5}}
]

Tout va bien ! C’est très simple mais mon but encore une fois est de faire des tests unitaires, donc c’est parti !

test unitaire

J’ai décidé de faire un seul test unitaire, car pour les matchs il faudrait implémenter une forte logique pour une gestion parfaite, mais je pense avoir assez détaillé les tests dans la partie utilisateur, ce qui m’a permis d’acquérir de nouvelles connaissances sur jest et bun.

Voici mon fichier matchController.test.ts :

import { describe, expect, test } from "bun:test";
import { fonctionViewMatch } from "../src/controllers/matchController";
describe("Recupéation des matchs seulement du jour", () => {
test("Recupération des matchs du jour", async () => {
const match = await fonctionViewMatch();
const today = new Date();
expect(match.length).toBeGreaterThan(0);
match.forEach((m) => {
expect(m.date.getDate()).toBe(today.getDate());
})
});
})

Ce fichier permet de vérifier que la fonction fonctionViewMatch retourne bien des données et que c’est donnée soit uniquement des matchs du jour.

Conclusion

J’espère que cet article vous a plu et vous donnera envie de tester Bun !

Etant une personne qui n’est jamais fait de TypeScript, je n’ai pas ressenti d’énorme difficulté, je suis même étonné de la facilité d’utilisation. Cela ne change pas énormément de JavaScript, à part l’ajout d’un typage qui permet une sécurité accrue sur l’input/output des données.

Bien sûr, je n’ai pas exploré toutes les possibilités de TypeScript/Bun, mais je pense que c’est une première expérience très intéressante !

Pour ceux qui sont intéressés par le code de cet article voici le repos github : https://github.com/Killian-Aidalinfo/bun-trd

Liens utiles

Réseaux sociaux

Comme d’habitude, n’hésitez pas à me rejoindre sur les réseaux sociaux pour plus de contenu !

plugins vite pour vue 3 : auto-imports et productivité

Des librairies gain de temps pour VueJS

En utilisant VueJS dans son état le plus pur possible, il nous arrive d’importer plusieurs fois des fonctions/méthodes, ici je veux parler de Ref, Watch, onMounted ou encore d’autres comme route et router de vue-router.

img post.png

Si vous avez déjà utilisé des frameworks comme Nuxt, vous avez remarqué que vous n’avez pas besoin de cela, même pour les components, de l’auto import qui nous rend plus efficaces et rend le code plus propre.

Cependant, tous les frameworks ne sont pas dotés de cette technologie, ni Vue dans son état brut. Dans cet article de blog, je vais donc vous montrer comment y remédier 🙂 Il suffit de 2 librairies très puissantes qui vont vous faire gagner beaucoup de lignes de code.

Librairie pour l’auto import des fonctions :

https://www.npmjs.com/package/unplugin-auto-import/v/0.17.1

Librairie pour l’auto import de components :

https://www.npmjs.com/package/unplugin-vue-components/v/0.14.0-beta.1

Pour commencer, je vais créer un projet Vue avec Vite :

Need to install the following packages.png

On va maintenant suivre la documentation et faire un premier npm install.

npm i -D unplugin-auto-import

Dans le fichier vite.config.js, nous allons ajouter l’import et indiquer le plugin pour qu’il soit chargé au build de l’application. Ensuite, en suivant la documentation, nous allons pré-importer des librairies dans un tableau.

CleanShot 2024-08-19 at 21.45.19@2x.png

À présent, dans mon component TheWelcome, je peux utiliser ref sans l’importer.

CleanShot 2024-08-19 at 21.46.52@2x.png

CleanShot 2024-08-19 at 21.47.46@2x.png

Je vais supprimer aussi les imports de vue-router dans mon app.vue

CleanShot 2024-08-19 at 21.48.48@2x.png

Et ça fonctionne :D

CleanShot 2024-08-19 at 21.49.59.gif

Maintenant, on va supprimer toute la partie script setup, avec unplugin-vue-components, fini l’import de components !

Un petit npm install :

npm i unplugin-vue-components -D

On retourne dans vite.config.js et on ajoute notre plugin dans vite.

CleanShot 2024-08-19 at 21.55.55@2x.png

J’ai supprimé l’import de components dans App.vue

CleanShot 2024-08-19 at 21.56.55@2x.png

Et cela fonctionne parfaitement !

CleanShot 2024-08-19 at 21.58.40@2x.png

J’espère que ces librairies vous plairont mais aussi qu’elles vous seront utiles 😁

Pour finir, n’hésitez pas à me rejoindre sur les réseaux sociaux pour plus de contenu !

Youtube ➡️ https://www.youtube.com/@civilisationit

X (ancien Twitter) ➡️ https://x.com/Ninapepite_

LinkedIn ➡️ https://www.linkedin.com/in/killian-stein-4465b81a2/