Accéder au contenu principal

Qwen-Agent : Un guide avec un projet de démonstration

Apprenez à utiliser Qwen-Agent et Qwen3 pour créer une extension de résumé de page Web en temps réel.
Actualisé 7 mai 2025  · 12 min de lecture

Dans cet article de blog, je vous expliquerai comment créer une extension de navigateur basée sur Qwen-Agent qui résume le contenu de n'importe quelle page Web en temps réel. Nous vous expliquons comment :

  • Utilisez localement le modèle Qwen3 d'Alibaba via Ollama
  • Utilisez Qwen-Agent pour des messages-guides structurés et l'utilisation d'outils.
  • Créez une extension Chrome avec des mises à jour d'interface utilisateur en continu
  • Ajout d'un backend FastAPI pour la prise en charge de la synthèse en temps réel

À la fin, vous disposerez d'un agent de synthèse capable de fonctionner hors ligne, alimenté par Qwen3, qui lit et résume les pages web dans votre navigateur en un seul clic.

Nous tenons nos lecteurs informés des dernières nouveautés en matière d'IA en leur envoyant The Median, notre lettre d'information gratuite du vendredi qui analyse les principaux sujets de la semaine. Abonnez-vous et restez à la pointe de la technologie en quelques minutes par semaine :

Qu'est-ce que Qwen-Agent ?

Qwen-Agent est un framework Python léger construit par Alibaba pour développer de puissantes applications LLM en utilisant la technologie Qwen3 de modèles. Il se concentre sur le soutien :

  • Agents qui suivent les instructions
  • Utilisation d'outils et appel de fonctions
  • Mémoire et planification multi-tours
  • Des messages structurés pour vous guider de manière flexible

À la base, Qwen-Agent simplifie le développement d'agents d'intelligence artificielle modulaires dotés de solides capacités de raisonnement et d'exécution de code. Il comprend des composants atomiques tels que des wrappers LLM et des outils, ainsi que des classes de haut niveau Agent pour l'orchestration des flux de travail.

Qwen-Agent fonctionne à la fois avec des API basées sur le cloud (comme Alibaba DashScope) et des runtimes locaux compatibles avec OpenAI, comme vLLM et Ollama. Il facilite la création d'applications LLM puissantes :

  • Intégration rapide via FastAPI
  • Streaming intégré pour les interfaces utilisateur frontales
  • Chaînes de fonctions personnalisables
  • Confidentialité et performance hors ligne

Aperçu du projet : Extension du résumeur Web en temps réel

Dans cette section, nous allons créer une extension de résumé en temps réel basée sur Qwen-Agent. Il s'agit notamment de

  • Un backend FastAPI qui accepte le texte d'une page web et renvoie un résumé.
  • Une extension Chrome pop-up pour capturer le contenu visible de la page
  • Une interface utilisateur en continu est générée sous la forme d'un résumé
  • Backend alimenté par Qwen3:1.7B fonctionnant localement via Ollama

Passons en revue chaque partie de ce projet.

Étape 1 : Configuration du backend Qwen3

Dans cette étape, nous allons couvrir tous les fichiers de code que nous devons mettre en place pour que notre backend fonctionne de manière synchrone avec notre extension.

Étape 1.1 : Dépendances et bibliothèques 

Commencez par installer ollama sur votre système. Ensuite, listez toutes les dépendances Python requises pour le backend dans requirements.txt.. Cela permet d'assurer une configuration cohérente de l'environnement entre les machines et les conteneurs.

fastapi
uvicorn
python-dateutil
python-dotenv
qwen-agent[code_interpreter]
matplotlib

Voici un aperçu de l'utilisation de chaque bibliothèque :

  • qwen-agent[code_interpreter]: Il s'agit d'un cadre d'agent de base avec un support d'utilisation d'outils, permettant l'exécution de code et le raisonnement structuré.
  • fastapi et uvicorn: Ces bibliothèques alimentent ensemble le backend de l'API asynchrone et servent votre logique de résumé.
  • python-dotenv et python-dateutil: Ces bibliothèques utilitaires gèrent les variables d'environnement et le traitement de la date et de l'heure (utile pour la mise à l'échelle future).
  • matplotlib: Il est utilisé pour le graphique ou l'affichage de sorties visuelles (facultatif). 

Si vous souhaitez exécuter ce projet localement, exécutez la commande suivante pour installer toutes les dépendances :

pip install -r requirements.txt

Étape 1.2 : Configuration de DockerFile (optionnel)

Maintenant, configurons un Dockerfile qui crée un environnement Python minimal avec notre app et expose le port 7864, permettant à l'extension Chrome d'envoyer des requêtes au backend FastAPI.

FROM python:3.10

WORKDIR /app
COPY . .

RUN pip install --no-cache-dir -r requirements.txt

EXPOSE 7864

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7864"]

Cette configuration est optionnelle si vous choisissez d'exécuter le projet localement en utilisant Uvicorn au lieu de Docker.

Étape 1.3 : Servir Qwen avec Ollama

Ensuite, vous devrez extraire et servir le modèle Qwen3 localement à l'aide d'Ollama. Cette étape permet l'inférence hors ligne en hébergeant le service LLM sur votre machine.

ollama pull qwen3:1.7b
ollama serve 

Étape 1.4 : Construction du serveur de compression FastAPI

Le fichier app.py définit un serveur FastAPI qui diffuse en temps réel des résumés du contenu des pages web à l'aide d'un modèle Qwen3:1.7B exécuté localement via Qwen-Agent.

Étape 1.4.1 : Importations

Commencez par importer FastAPI, les utilitaires de diffusion en continu et les composants Qwen-Agent tels que Assistant, qui englobe le modèle et ses capacités d'outil.

import re
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from typing import List, Dict, Any
from qwen_agent.agents import Assistant
import logging

Étape 1.4.2 : Initialisation de l'application FastAPI

Ensuite, nous définissons un schéma simple RequestData pour recevoir du contenu, et nous créons une instance de bot Assistant

app = FastAPI()
class RequestData(BaseModel):
    content: str
bot = Assistant(
    llm={
        "model": "qwen3:1.7b",
        "model_server": "http://localhost:11434/v1",
        "api_key": "EMPTY"
    },
    function_list=["code_interpreter"],
    system_message="You are a summarization assistant. Directly output the cleaned summary of the given text without any reasoning, self-talk, thoughts, or internal planning steps. Do not include phrases like 'I think', 'maybe', 'let's', 'the user wants', or anything not part of the final summary. Your output must look like it was written by an editor, not a model."
)

Dans le code ci-dessus, bot est une instance de la classe Assistant de Qwen-Agent, configurée pour interagir avec le modèle Qwen3:1.7B desservi localement via Ollama. Il utilise l'outil intégré code_interpreter pour analyser le contenu et exécuter le code Python si nécessaire. Un site system_message personnalisé guide son comportement pour générer des résumés concis de pages web.

Note : Vous pouvez utiliser des modèles plus petits comme qwen3:0.6b ou qwen3:1.7b pour réduire considérablement le temps de réponse et l'utilisation de la mémoire. Ceci est particulièrement utile pour les tâches de résumé plus rapides sans trop de frais de raisonnement.

Étape 1.4.3 : Itinéraire de synthèse

Enfin, un point de terminaison lit le contenu entrant et produit un résumé en direct au fur et à mesure qu'il est généré par le modèle. Il utilise une sortie en continu afin que l'utilisateur n'ait pas à attendre que la réponse complète soit générée.

@app.post("/summarize_stream_status")
async def summarize_stream_status(data: RequestData):
    user_input = data.content
    def stream():
        try:
            yield "🔍 Reading content on website...\n"
            print(" Received text:", user_input[:200])
            messages = [
            {"role": "system", "content": "You are a summarization assistant. Directly output the cleaned summary of the given text without any reasoning, self-talk, thoughts, or internal planning steps. Do not include phrases like 'I think', 'maybe', 'let's', 'the user wants', or anything not part of the final summary. Your output must look like it was written by an editor, not a model."},
            {"role": "user", "content": "<nothink>\nSummarize the following text clearly and concisely. Do not include any internal thoughts, planning, or reasoning. Just return the final summary:\n\n" + user_input + "\n</nothink>"}
            ]
            yield "🧠 Generating summary...\n"
            result = bot.run(messages)
            result_list = list(result)
            print(" Raw result:", result_list)
            # Extract summary
            last_content = None
            for item in reversed(result_list):
                if isinstance(item, list):
                    for subitem in reversed(item):
                        if isinstance(subitem, dict) and "content" in subitem:
                            last_content = subitem["content"]
                            break
                if last_content:
                    break
            if not last_content:
                yield " No valid summary found.\n"
                return
            summary = re.sub(r"</?think>", "", last_content)
            summary = re.sub(
                r"(?s)^.*?(Summary:|Here's a summary|The key points are|Your tutorial|This tutorial|To summarize|Final summary:)", 
                "", 
                summary, 
                flags=re.IGNORECASE
            )
            summary = re.sub(r"\n{3,}", "\n\n", summary)
            summary = summary.strip()
            yield "\n📄 Summary:\n" + summary + "\n"
        except Exception as e:
            print(" Error:", e)
            yield f"\n Error: {str(e)}\n"
    return StreamingResponse(stream(), media_type="text/plain")

Voici comment fonctionne notre résumé :

  • La fonction reçoit l'instruction de données POST en utilisant le schéma RequestData et attend un champ nommé content qui contient le texte brut de la page web.
  • Une fonction génératrice imbriquée stream() est définie pour produire progressivement des messages. Cela permet de renvoyer un retour d'information en temps réel au frontend avant que le résumé final ne soit terminé.
  • La liste des messages inclut explicitement une balise dans le message de l'utilisateur pour supprimer le raisonnement interne, et une invite détaillée du système renforce ce comportement.
  • Le bot Qwen-Agent est invoqué à l'aide de bot.run(messages), où messages est une liste contenant un seul message d'entrée de l'utilisateur. 
  • Les réponses du modèle sont diffusées et collectées sur le site result_list. Le code parcourt ensuite la liste en sens inverse pour extraire la chaîne de contenu la plus récente renvoyée par le LLM.
  • Les marqueurs de raisonnement interne de Qwen tels que et sont supprimés de la sortie à l'aide d'une expression rationnelle afin de nettoyer le résumé final.
  • Si aucun contenu valide n'est trouvé, un message de repli est envoyé. Dans le cas contraire, le résumé nettoyé est transmis au frontend.

Étape 2 : Création de l'extension Chrome

Cette section passe en revue tous les fichiers nécessaires à la configuration de l'extension, y compris l'interface utilisateur frontale, la logique d'extraction du contenu des pages, les scripts de communication en arrière-plan et les métadonnées de configuration.

Avant d'examiner les différents scripts, voici une description simple de ce que fait chaque fichier :

  • manifest.json: Il s'agit d'un fichier de configuration qui définit les métadonnées, les autorisations et les scripts de l'extension.
  • popup.html: Ce script définit l'interface utilisateur visible pour la fenêtre contextuelle de l'extension Chrome avec un bouton et un panneau de sortie.
  • popup.js: Il gère la logique frontale, capture le texte de l'onglet en cours et transmet la réponse du résumé à l'interface utilisateur.
  • content.js: Il extrait le contenu visible de la page web lorsqu'il est demandé, agissant comme un script de contenu.
  • background.js: Ce script coordonne la communication avec le backend et transmet les résumés en continu à la fenêtre contextuelle.
  • icon.png: Il s'agit d'une icône d'extension affichée dans la barre d'outils et le gestionnaire d'extensions de Chrome.

Ensemble, ces fichiers rendent l'extension interactive, réactive et capable de communiquer avec un serveur de modèle local.

Étape 1 : Métadonnées d'extension

Le script manifest.json contient des métadonnées pour Chrome. Il définit le comportement de l'extension, les autorisations, le service d'arrière-plan et l'interface utilisateur.

{
    "manifest_version": 3,
    "name": "Web Summarizer",
    "version": "1.0",
    "description": "Summarize the content of the current page using a local LLM agent.",
    "permissions": [
      "scripting",
      "activeTab",
      "storage"
    ],
    "host_permissions": [
      "<all_urls>"
    ],
    "action": {
      "default_popup": "popup.html",
      "default_icon": "icon.png"
    },
    "background": {
      "service_worker": "background.js"
    },
    "content_scripts": [
      {
        "matches": ["<all_urls>"],
        "js": ["content.js"],
        "run_at": "document_idle"
      }
    ]
}  

Ce fichier définit la configuration et les capacités de votre extension Chrome. Il accorde des autorisations essentielles telles que scripting (pour injecter du JavaScript), activeTab (pour accéder à l'onglet actuel) et storage (pour enregistrer les préférences de l'utilisateur).

Le champ host_permissions avec "<all_urls>" permet à l'extension de fonctionner sur n'importe quelle page web. Il met également en place l'interface utilisateur de l'extension par le biais de popup.html et icon.png, et enregistre le comportement en arrière-plan à l'aide d'un agent de service (background.js). Enfin, il définit un script de contenu (content.js) à exécuter sur toutes les pages chargées, ce qui permet d'interagir avec le contenu de la page web en cas de besoin.

Étape 2 : Construire une interface visible 

Le site popup.html définit l'interface visible de l'extension. Il se connecte à l'onglet actif qui capture le texte visible et l'envoie à votre serveur dorsal. 

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Web Summarizer Assistant</title>
  <style>
    body {
      width: 500px;
      height: 600px;
      background-color: aliceblue;
      
      
      
    }
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 15px;
    }
    button {
      border: none;
      border-radius: 5px;
      background-
      color: white;
      
      
      cursor: pointer;
    }
    button:hover {
      background-
    }
    pre {
      background-
      border: 1px solid #ccc;
      border-radius: 5px;
      
      width: 90%;
      height: 350px;
      overflow: auto;
    }
    input[type="text"] {
      
      width: 90%;
      border-radius: 5px;
      border: 1px solid #ccc;
    }
  </style>
</head>
<body>
  <div class="container">
    <h2>Summarize Current Page</h2>
    <button id="summarizeBtn">Summarize</button>
    <pre id="output">Summary will appear here...</pre>
  </div>
  <script src="popup.js"></script>
</body>
</html>

Comprenons le code ci-dessus étape par étape :

  1. Tout d'abord, nous mettons en place une structure HTML de base avec une balise qui comprend des balises méta pour l'encodage des caractères, les paramètres d'affichage et une balise </code>.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ensuite, à l'intérieur de la balise <code><body></code>, un conteneur contient tous les éléments de l'interface utilisateur, notamment :</span></li> </ol> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Un titre <code><h2></code> invitant l'utilisateur à résumer la page en cours.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Un bouton <code>Summarize</code> ("#summarizeBtn") sur lequel l'utilisateur clique pour déclencher l'extraction et le résumé du contenu.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Un élément <code><pre></code> ("#output") qui sert de panneau déroulant pour afficher le résumé au fur et à mesure qu'il afflue.</span></li> </ul> <ol style="padding-inline-start: 48px;" start="3"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Le style est défini à l'aide d'une feuille de style CSS intégrée afin de rendre la fenêtre contextuelle visuellement propre et lisible. Le bloc <code>pre</code> est utilisé pour faire défiler les résultats et les distinguer visuellement.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Le script <code>popup.js</code> écoute les clics sur les boutons, extrait le contenu de la page, l'envoie au backend FastAPI et met à jour le panneau de sortie avec un texte récapitulatif en continu.</span></li> </ol> <h3 dir="ltr">Étape 3 : Construire une interface utilisateur frontale </h3> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Le site <code>popup.js</code> fait office de contrôleur logique frontal qui réagit aux clics sur les boutons, extrait le texte de l'onglet en cours, l'envoie au backend et renvoie le résumé dans la zone de sortie en temps réel.</span></p> <pre class="language-javascript"><code>document.addEventListener('DOMContentLoaded', function () { let serverAddress = '127.0.0.1'; document.getElementById('summarizeBtn').addEventListener('click', async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); chrome.scripting.executeScript( { target: { tabId: tab.id }, func: () => { try { return document.body?.innerText || 'EMPTY'; } catch (e) { return 'SCRIPT_ERROR'; } } }, async (results) => { const pageText = results?.[0]?.result || ''; console.log(' Extracted text:', pageText.slice(0, 500)); if (pageText === 'SCRIPT_ERROR') { document.getElementById('output').innerText = 'Could not access page content (script error).'; return; } if (!pageText || pageText.trim() === 'EMPTY') { document.getElementById('output').innerText = ' No page text found.'; return; } try { console.log( Sending streaming POST to http://${serverAddress}:7864/summarize_stream_status); const res = await fetch(http://${serverAddress}:7864/summarize_stream_status, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: pageText }), }); const reader = res.body.getReader(); const decoder = new TextDecoder(); let resultText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); resultText += chunk; document.getElementById('output').innerText = resultText; } } catch (err) { console.error(' Fetch error:', err); document.getElementById('output').innerText = ' Failed to get summary.\n' + err.message; } } ); }); });</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ce fichier JavaScript alimente la logique de l'interface utilisateur frontale de l'extension Chrome, permettant aux utilisateurs d'extraire et de résumer le contenu d'une page web en un clic.</span></p> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Voici comment cela fonctionne :</span></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Lorsque la fenêtre pop-up est chargée (<code>DOMContentLoaded</code>), un écouteur d'événements est attaché à la fonction </span><span style="background-color: transparent; font-weight: bold; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">"Résumer</span><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;"> "Résumer".</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">En cliquant sur le bouton, l'extension interroge l'onglet actuellement actif à l'aide de <code>chrome.tabs.query</code>.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Il exécute ensuite un script de contenu dans cet onglet à l'aide de <code>chrome.scripting.executeScript</code>, qui tente d'extraire le <code>innerText</code> du corps de la page.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Si le script échoue ou renvoie un contenu vide, des messages de repli appropriés sont affichés dans l'interface utilisateur.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Si le texte est extrait avec succès, il est envoyé sous la forme d'une requête POST au backend FastAPI local à l'adresse <code>http://127.0.0.1:7864/summarize_stream_status</code>.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">La réponse est diffusée à l'aide de <code>res.body.getReader()</code>, ce qui permet d'afficher des parties du résumé en temps réel dans le panneau de sortie.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Un site <code>TextDecoder</code> décode chaque morceau de texte transmis en continu et l'ajoute en direct à l'écran.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Toute erreur de réseau ou de backend est détectée et affichée dans l'interface pop-up pour le débogage ou le retour d'information de l'utilisateur.</span></li> </ul> <h3 dir="ltr">Étape 4 : Extraction du contenu d'une page web</h3> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Avant d'envoyer le contenu d'une page web à notre backend, nous avons besoin d'un moyen de l'extraire de l'onglet en cours. Le script <code>content.js</code> écoute les messages des scripts d'arrière-plan ou des scripts popup et renvoie le contenu textuel visible de la page web. </span></p> <pre class="language-javascript"><code>chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { if (request.type === "GET_PAGE_TEXT") { const bodyText = document.body.innerText.trim(); sendResponse({ text: bodyText || null }); } });</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ce script est à l'écoute des messages de type "GET_PAGE_TEXT" et répond en extrayant et en renvoyant le corps de texte visible (<code>document.body.innerText</code>). Cela permet au travailleur en arrière-plan de demander facilement le contenu d'une page web sans injecter de code à chaque fois, ce qui permet de maintenir une communication propre et asynchrone.</span></p> <h3 dir="ltr">Étape 5 : Coordination du backend</h3> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Le script d'arrière-plan (<code>background.js</code>) fait le lien entre l'interface utilisateur de la fenêtre contextuelle et les scripts de contenu. Il garantit que le passage des messages et la coordination de la synthèse se déroulent correctement dans les coulisses.</span></p> <pre class="language-javascript"><code>chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => { if (request.type === "SUMMARIZE_PAGE") { const tabId = sender.tab.id; chrome.scripting.executeScript( { target: { tabId }, func: () => { try { return document.body?.innerText || 'EMPTY'; } catch (e) { return 'SCRIPT_ERROR'; } } }, async (results) => { const pageText = results?.[0]?.result || ''; if (!pageText || pageText === 'SCRIPT_ERROR') { chrome.runtime.sendMessage({ type: 'SUMMARY_RESULT', summary: ' Failed to read page text.', }); return; } chrome.storage.local.get(['database_host'], async function (result) { const host = result.database_host || '127.0.0.1'; try { const response = await fetch(http://${host}:7864/summarize_stream_status, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content: pageText }), }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullSummary = ''; while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); fullSummary += chunk; chrome.runtime.sendMessage({ type: 'SUMMARY_PROGRESS', chunk }); } chrome.runtime.sendMessage({ type: 'SUMMARY_DONE', summary: fullSummary }); } catch (err) { chrome.runtime.sendMessage({ type: 'SUMMARY_RESULT', summary: ' Error during summarization: ' + err.message }); } }); } ); return true; } }); </code></pre> <p dir="ltr"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Le code ci-dessus garantit un flux fluide d'extraction de contenu, de résumé et de mises à jour en temps réel. Voici une description détaillée de son fonctionnement :</span></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Il est à l'écoute d'un message de type "SUMMARIZE_PAGE" déclenché par un clic sur un bouton de la fenêtre contextuelle.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Une fois déclenché, il identifie l'onglet actif à l'aide de <code>sender.tab.id</code> et exécute un script injecté via <code>chrome.scripting.executeScript</code> pour extraire le texte visible de la page web.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Si le contenu n'est pas accessible ou s'il est vide, il renvoie un message de repli.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Sinon, il récupère l'adresse <code>database_host</code> (généralement <code>127.0.0.1</code>) dans le stockage local de Chrome et l'utilise pour envoyer une requête <code>POST</code> au backend FastAPI avec le contenu extrait.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ensuite, il ouvre une connexion en continu et lit la réponse morceau par morceau à l'aide d'une adresse <code>ReadableStreamDefaultReader</code>.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Au fur et à mesure que chaque morceau est décodé, il envoie des mises à jour intermédiaires à la fenêtre contextuelle à l'aide de <code>chrome.runtime.sendMessage</code> avec le type "SUMMARY_PROGRESS".</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Lorsque le flux se termine, il envoie un message final contenant le résumé complet. Si une erreur survient au cours de ce processus, un message de repli est envoyé à la place.</span></li> </ul> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ce script d'arrière-plan est essentiel pour permettre des résumés en temps réel et en continu dans l'extension Chrome sans bloquer l'interface utilisateur ou recharger la page.</span></p> <h3 dir="ltr">Étape 6 : Ajouter une icône d'extension</h3> <p dir="ltr"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Choisissez ou dessinez une image d'icône pour l'extension et enregistrez-la sous <code>icon.png</code> dans le dossier de l'extension. C'est ce que nous allons utiliser :</span></p> <p dir="ltr"><img style="display: block; margin-left: auto; margin-right: auto;" src="https://media.datacamp.com/cms/ad_4nxenz-ec-m6n1ovwrqkq_ya8gybch-wok3r1ildciiyiunz9kusu_-uucfrkzqwfl1w-jjgrvcoz_erdhjcufxweuwpchh-0kqthmmw1xlxhozmlr7hor5hkf9rnpykvvyzp2p7t4g.png" alt="Icône d'extension" width="400" height="264" /></p> <p dir="ltr" style="text-align: center;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Image générée avec FireFly</span></p> <p dir="ltr"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">La structure générale de vos dossiers devrait ressembler à ceci :</span></p> <pre class="language-markdown"><code>QWEN-WEB-SUMMARIZER/ ├── backend/ │ ├── app.py │ ├── Dockerfile │ └── requirements.txt ├── extension/ │ ├── background.js │ ├── content.js │ ├── icon.png │ ├── manifest.json │ ├── popup.html │ └── popup.js</code></pre> <h2 dir="ltr">Étape 3 : Exécution de l'application</h2> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Pour tout exécuter localement, lancez la commande suivante dans le terminal :</span></p> <pre class="language-bash"><code>uvicorn backend.app:app --host 0.0.0.0 --port 7864</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">C'est idéal pour le développement local ou le débogage sans Docker. Pour l'exécuter dans Docker, exécutez les commandes suivantes une à la fois dans votre terminal :</span></p> <pre class="language-bash"><code>docker build -t qwen-agent-backend . docker run -p 7864:7864 qwen-agent-backend</code></pre> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">L'extension Chrome communique avec le port 7864.  Maintenant, suivez les étapes suivantes pour faire fonctionner votre extension sur Chrome :</span></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ouvrez Chrome et cliquez sur "Extensions"</span><span style="background-color: transparent; font-weight: bold; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;"> </span><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">(à droite de la barre de recherche).</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Activez le "Developer mode" et cliquez sur "Load unpacked" (en haut à gauche). </span></li> </ul> <p dir="ltr"><img loading="lazy" style="display: block; margin-left: auto; margin-right: auto;" src="https://media.datacamp.com/cms/ad_4nxeqediol3e_2camavptkettnqaz0dwk2biqs72uwhqpjjhnqgnzqbuhlyrx7ve4ropcgvz4czhzjc0lc496kwq7pie_epl-gqo04bdagklllw_5asyyo-_k0ny9ggqapx4rs_5v6q.png" alt="Onglet Chrome Extension pour l'activation d'une extension propulsée par Qwen-Agent" width="800" height="222" /></p> <ul style="padding-inline-start: 48px;"> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Ensuite, chargez notre dossier d'extensions contenant tous les fichiers et attendez la confirmation.</span></li> <li><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Une fois que la fenêtre contextuelle confirme la configuration de l'extension, cliquez sur l'icône de l'extension sur n'importe quelle page Web et obtenez un résumé en continu à l'aide de votre modèle Qwen3 local.</span></li> </ul> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Voyons les résultats :</span></p> <p dir="ltr"><img loading="lazy" style="display: block; margin-left: auto; margin-right: auto;" src="https://media.datacamp.com/cms/ad_4nxen-zef7jtzs6nuqx9tscg2ggzh79ftzn4rwghacrjz4ztiz9qy-bvbr1879yiyy6y9woe_dzsak4cap42tu2qr5oc-wquz8x9k7wvvbkeprq5l8tsuntnnckm8my0pvnsqw0heza.png" alt="Exemple d'extension Qwen-Agent" width="800" height="512" /></p> <h2 dir="ltr">Conclusion</h2> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Dans ce projet, nous avons construit un assistant de résumé de pages web en temps réel en utilisant le cadre Qwen-Agent et le modèle Qwen3:1.7B fonctionnant localement via Ollama. Nous avons développé un backend FastAPI pour gérer l'inférence LLM et l'avons intégré à une extension Chrome qui capture le contenu visible de la page et affiche un résumé en direct.</span></p> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Cette démonstration montre comment Qwen-Agent peut permettre des flux de travail de raisonnement entièrement hors ligne et augmentés d'outils dans le navigateur. Il jette les bases de la création d'agents locaux plus avancés, tels que des outils d'automatisation des navigateurs, des copilotes de recherche ou des chatbots basés sur des documents.</span></p> <p dir="ltr" style="text-align: justify;"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Pour en savoir plus sur les agents d'intelligence artificielle, consultez ces blogs :</span></p> <ul> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/fr/tutorial/n8n-ai"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">n8n : Un guide avec des exemples</span></a></li> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/fr/blog/types-of-ai-agents"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Types d'agents d'intelligence artificielle</span></a></li> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/fr/blog/agentic-ai"><span style="background-color: transparent; font-weight: 400; font-style: normal; font-variant: normal; text-decoration: none; vertical-align: baseline; white-space: pre-wrap;">Qu'est-ce que l'IA agentique ?</span></a></li> </ul>

Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Je suis un expert Google Developers en ML (Gen AI), un expert Kaggle 3x, et un ambassadeur Women Techmakers avec plus de 3 ans d'expérience dans la technologie. J'ai cofondé une startup dans le domaine de la santé en 2020 et je poursuis un master en informatique à Georgia Tech, avec une spécialisation dans l'apprentissage automatique.

Sujets
Apparenté

blog

Les 50 meilleures questions et réponses d'entretien sur AWS pour 2025

Un guide complet pour explorer les questions d'entretien AWS de base, intermédiaires et avancées, ainsi que des questions basées sur des situations réelles.
Zoumana Keita 's photo

Zoumana Keita

15 min

blog

Architecture de l'entrepôt de données : Tendances, outils et techniques

Apprenez l'essentiel de l'architecture d'un entrepôt de données, des composants clés aux meilleures pratiques, pour construire un système de données évolutif et efficace !
Kurtis Pykes 's photo

Kurtis Pykes

15 min

blog

Les 20 meilleures questions d'entretien pour les flocons de neige, à tous les niveaux

Vous êtes actuellement à la recherche d'un emploi qui utilise Snowflake ? Préparez-vous à répondre à ces 20 questions d'entretien sur le flocon de neige pour décrocher le poste !
Nisha Arya Ahmed's photo

Nisha Arya Ahmed

15 min

blog

Q2 2023 DataCamp Donates Digest

DataCamp Donates a offert plus de 20k bourses d'études à nos partenaires à but non lucratif au deuxième trimestre 2023. Découvrez comment des apprenants défavorisés et assidus ont transformé ces opportunités en réussites professionnelles qui ont changé leur vie.
Nathaniel Taylor-Leach's photo

Nathaniel Taylor-Leach

blog

2022-2023 Rapport annuel DataCamp Classrooms

À l'aube de la nouvelle année scolaire, DataCamp Classrooms est plus motivé que jamais pour démocratiser l'apprentissage des données, avec plus de 7 650 nouveaux Classrooms ajoutés au cours des 12 derniers mois.
Nathaniel Taylor-Leach's photo

Nathaniel Taylor-Leach

8 min

blog

Célébration de Saghar Hazinyar : Une boursière de DataCamp Donates et une diplômée de Code to Inspire

Découvrez le parcours inspirant de Saghar Hazinyar, diplômée de Code to Inspire, qui a surmonté les défis en Afghanistan et s'est épanouie grâce à une bourse de DataCamp Donates.
Fereshteh Forough's photo

Fereshteh Forough

4 min

Voir plusVoir plus