In deze blogpost laat ik je stap voor stap zien hoe je een browserextensie bouwt op basis van Qwen-Agent die de inhoud van elke webpagina realtime samenvat. We behandelen hoe je:
Alibaba’s Qwen3-model lokaal gebruikt via Ollama
Qwen-Agent inzet voor gestructureerde prompts en toolgebruik
Een Chrome-extensie bouwt met streaming UI-updates
Een FastAPI-backend toevoegt voor realtime samenvattingen
Aan het einde heb je een offline-capabele samenvattingsagent, aangedreven door Qwen3, die met één klik webpagina’s in je browser leest en samenvat.
We houden onze lezers op de hoogte van het laatste AI-nieuws met The Median, onze gratis vrijdagse nieuwsbrief die de belangrijkste verhalen van de week uitlegt. Abonneer je en blijf scherp in een paar minuten per week:
Wat is Qwen-Agent?
Qwen-Agent is een lichtgewicht Python-framework van Alibaba voor het ontwikkelen van krachtige LLM-toepassingen met de Qwen3-modelreeks. Het richt zich op ondersteuning van:
Agents die instructies opvolgen
Toolgebruik en functieaanroepen
Geheugen en meerstapsplanning
Flexibel prompten via gestructureerde berichten
In de kern vereenvoudigt Qwen-Agent de ontwikkeling van modulaire AI-agents met robuuste redeneer- en code-uitvoeringsmogelijkheden. Het bevat elementaire componenten zoals LLM-wrappers en tools, en hoog-niveau Agent-klassen voor het orkestreren van workflows.
Qwen-Agent werkt met zowel cloud-API’s (zoals Alibaba DashScope) als lokale OpenAI-compatibele runtimes zoals vLLM en Ollama. Het maakt het eenvoudig om krachtige LLM-apps te bouwen met:
In dit deel bouwen we een realtime samenvattingsextensie op basis van Qwen-Agent. Dit omvat:
Een FastAPI-backend die webpaginatekst accepteert en een samenvatting terugstuurt
Een Chrome-extensiepop-up om zichtbare paginainhoud vast te leggen
Een streaming-UI terwijl de samenvatting wordt gegenereerd
Backend aangedreven door Qwen3:1.7B die lokaal draait via Ollama
Laten we elk onderdeel van dit project doorlopen.
Stap 1: De Qwen3-backend instellen
In deze stap behandelen we alle codebestanden die we moeten opzetten zodat onze backend synchroon werkt met onze extensie.
Stap 1.1: Afhankelijkheden en libraries
Begin met het installeren van ollama op je systeem. Noteer vervolgens alle Python-afhankelijkheden die voor de backend nodig zijn in requirements.txt. Dit helpt om een consistente omgeving op verschillende machines en containers te waarborgen.
fastapi
uvicorn
python-dateutil
python-dotenv
qwen-agent[code_interpreter]
matplotlibMogelijk gemaakt door
Hier is een overzicht waar elke library voor wordt gebruikt:
qwen-agent[code_interpreter]: Dit is een kernframework voor agents met ondersteuning voor toolgebruik, waarmee je code kunt uitvoeren en gestructureerd kunt redeneren.
fastapi en uvicorn: Deze libraries verzorgen samen de async API-backend en dienen je samenvattingslogica.
python-dotenv en python-dateutil: Deze hulplibraries behandelen omgevingsvariabelen en datum-/tijdverwerking (handig voor toekomstige opschaling).
matplotlib: Wordt gebruikt voor plotten of het tonen van visuele outputs (optioneel)
Als je dit project lokaal wilt draaien, voer dan de volgende opdracht uit om alle afhankelijkheden te installeren:
Laten we nu een Dockerfile opzetten die een minimale Python-omgeving met onze app creëert en poort 7864 openstelt, zodat de Chrome-extensie verzoeken kan sturen naar de FastAPI-backend.
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"]Mogelijk gemaakt door
Deze setup is optioneel als je ervoor kiest om het project lokaal met Uvicorn te draaien in plaats van met Docker.
Stap 1.3: Qwen serveren met Ollama
Vervolgens moet je het Qwen3-model lokaal ophalen en serveren met Ollama. Deze stap maakt offline-inferentie mogelijk door de LLM-service op je machine te hosten.
Het bestand app.py definieert een FastAPI-server die realtime samenvattingen van webpaginas inhoud streamt met een lokaal draaiend Qwen3:1.7B-model via Qwen-Agent.
Stap 1.4.1: Imports
Begin met het importeren van FastAPI, streamingutilities en Qwen-Agent-componenten zoals Assistant, die het model en zijn toolmogelijkheden verpakt.
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 loggingMogelijk gemaakt door
Stap 1.4.2: Initialisatie van de FastAPI-app
Vervolgens definiëren we een eenvoudig RequestData-schema om content te ontvangen, en maken we een botinstantie van Assistant.
app = FastAPI()classRequestData(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.")Mogelijk gemaakt door
In de bovenstaande code is bot een instantie van de Assistant-klasse uit Qwen-Agent, geconfigureerd om te communiceren met het lokaal geserveerde Qwen3:1.7B-model via Ollama. Het gebruikt de ingebouwde code_interpreter-tool om door content te redeneren en indien nodig Python-code uit te voeren. Een aangepaste system_message stuurt zijn gedrag om beknopte webpaginasamenvattingen te genereren.
Opmerking: je kunt kleinere modellen zoals qwen3:0.6b of qwen3:1.7b gebruiken om de responstijd en het geheugengebruik aanzienlijk te verlagen. Dit is vooral handig voor snellere samenvattingstaken zonder veel redeneerwerk.
Stap 1.4.3: Samenvattingsroute
Tot slot leest een endpoint de binnenkomende content en levert live samenvattingstekst terwijl die door het model wordt gegenereerd. Het gebruikt streamingoutput zodat de gebruiker niet hoeft te wachten tot de volledige respons is gegenereerd.
@app.post("/summarize_stream_status")asyncdefsummarize_stream_status(data: RequestData):
user_input = data.content
defstream():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 =Nonefor item inreversed(result_list):ifisinstance(item,list):for subitem inreversed(item):ifisinstance(subitem,dict)and"content"in subitem:
last_content = subitem["content"]breakif last_content:breakifnot 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)yieldf"\n Error: {str(e)}\n"return StreamingResponse(stream(), media_type="text/plain")Mogelijk gemaakt door
Zo werkt onze samenvatter:
De functie ontvangt POST-data-instructie met het RequestData-schema en verwacht een veld content met de ruwe webpaginatekst.
Er is een geneste stream()-generatorfunctie gedefinieerd die stapsgewijs berichten oplevert. Hierdoor kan er realtime feedback naar de frontend worden gestuurd voordat de uiteindelijke samenvatting klaar is.
De berichtenlijst bevat expliciet een </nothink>-tagwrapper in het gebruikersbericht om interne redeneringen te onderdrukken, en een gedetailleerde systeemprompt versterkt dit gedrag.
De Qwen-Agent-bot wordt aangeroepen met bot.run(messages) waarbij messages een lijst is met één gebruikersbericht.
Modelantwoorden worden gestreamd en verzameld in result_list. De code loopt vervolgens omgekeerd door de lijst om de meest recente contentstring die door de LLM is geretourneerd te extraheren.
Qwen’s interne redeneermarkeringen zoals <think> en </think> worden met een regex uit de output verwijderd om de uiteindelijke samenvatting op te schonen.
Als er geen geldige content wordt gevonden, wordt een terugvalbericht verzonden. Anders wordt de opgeschoonde samenvatting naar de frontend gestreamd.
Stap 2: De Chrome-extensie bouwen
Dit deel loopt door alle bestanden die nodig zijn om de extensie op te zetten, waaronder de frontend-UI, logica om paginacontent te extraheren, achtergrondcommunicatiescripts en configuratiemetagegevens.
Voordat we de afzonderlijke scripts bekijken, volgt hier een eenvoudige uitleg van wat elk bestand doet:
manifest.json: Dit is een configuratiebestand dat extensiemetagegevens, machtigingen en scripts definieert.
popup.html: Dit script definieert de zichtbare UI voor de Chrome-extensiepop-up met een knop en een uitvoerpaneel.
popup.js: Dit handelt de frontendlogica af, legt de tekst van het huidige tabblad vast en streamt de samenvattingsrespons naar de UI.
content.js: Extraheert zichtbare webpaginas inhoud op aanvraag en fungeert als content script.
background.js: Coördineert backendcommunicatie en geeft gestreamde samenvattingen door aan de pop-up.
icon.png: Het extensiepictogram dat wordt weergegeven in de Chrome-werkbalk en extensiebeheer.
Samen maken deze bestanden de extensie interactief, reactief en in staat om met een lokale modelserver te communiceren.
Stap 1: Extensiemetagegevens
Het script manifest.json bevat metagegevens voor Chrome. Het definieert het gedrag van de extensie, machtigingen, de achtergrondserviceworker en de pop-up-UI.
{"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"}]}Mogelijk gemaakt door
Dit bestand definieert de configuratie en mogelijkheden van je Chrome-extensie. Het verleent essentiële machtigingen zoals scripting (om JavaScript te injecteren), activeTab (om toegang te krijgen tot het huidige tabblad) en storage (voor het opslaan van gebruikersvoorkeuren).
Het veld host_permissions met "<all_urls>" laat de extensie op elke webpagina draaien. Het stelt ook de UI van de extensie in via popup.html en icon.png, en registreert achtergrondgedrag met een serviceworker (background.js). Tot slot definieert het een content script (content.js) dat op alle geladen pagina’s draait, zodat wanneer nodig met webpaginas inhoud kan worden gecommuniceerd.
Stap 2: Een zichtbare interface bouwen
De popup.html definieert de zichtbare interface van de extensie. Hij maakt verbinding met het actieve tabblad, legt de zichtbare tekst vast en stuurt die naar je backendserver.
<!DOCTYPEhtml><html><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="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><divclass="container"><h2>Summarize Current Page</h2><buttonid="summarizeBtn">Summarize</button><preid="output">Summary will appear here...</pre></div><scriptsrc="popup.js"></script></body></html>Mogelijk gemaakt door
Laten we de bovenstaande code stap voor stap begrijpen:
Eerst zetten we een basis-HTML-structuur op met een <head>-tag met metatags voor tekencodering, viewportinstellingen en een <title>-tag.
Daarna bevat de container binnen de <body>-tag alle UI-elementen, waaronder:
Een <h2>-kop die de gebruiker vraagt de huidige pagina samen te vatten.
Een Summarize-knop (“#summarizeBtn”) die de gebruiker klikt om contentextractie en samenvatten te starten.
Een <pre>-element (“#output”) dat fungeert als scrollbaar paneel om de samenvatting te tonen terwijl deze binnenstroomt.
De styling is gedefinieerd met ingesloten CSS om de pop-up visueel strak en leesbaar te maken. Het pre-blok maakt de output scrollbaar en visueel onderscheidend.
Het script popup.js luistert naar klikken op de knop, extraheert de paginacontent, stuurt die naar de FastAPI-backend en werkt het uitvoerpaneel bij met gestreamde samenvattingstekst.
Stap 3: Een frontend-UI bouwen
De popup.js fungeert als frontendlogica-controller die reageert op knopklikken, tekst uit het huidige tabblad extraheert, die naar de backend stuurt en de samenvatting realtime terugstreamt naar het uitvoergebied.
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 =awaitfetch(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 =newTextDecoder();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;}});});});Mogelijk gemaakt door
Dit JavaScript-bestand verzorgt de frontend-UI-logica van de Chrome-extensie, zodat gebruikers met één klik webpaginas inhoud kunnen extraheren en samenvatten.
Zo werkt het:
Wanneer de pop-up is geladen (DOMContentLoaded), wordt een eventlistener gekoppeld aan de "Summarize"-knop.
Bij een klik vraagt de extensie het huidige actieve tabblad op met chrome.tabs.query.
Vervolgens voert hij een content script uit in dat tabblad met chrome.scripting.executeScript, dat probeert de innerText van de pagina-body te extraheren.
Als het script faalt of lege content teruggeeft, worden passende fallbackberichten in de UI getoond.
Als er succesvol tekst is geëxtraheerd, wordt die als POST-verzoek verstuurd naar de lokale FastAPI-backend op http://127.0.0.1:7864/summarize_stream_status.
De respons wordt gestreamd met res.body.getReader(), zodat delen van de samenvatting realtime in het uitvoerpaneel verschijnen.
Een TextDecoder decodeert elk gestreamd tekstfragment en voegt het live toe aan de weergave.
Eventuele netwerk- of backendfouten worden opgevangen en getoond in de pop-up voor debugging of feedback.
Stap 4: Webpaginas inhoud extraheren
Voordat we webpaginas inhoud naar onze backend kunnen sturen, hebben we een manier nodig om deze uit het huidige tabblad te halen. Het script content.js luistert naar berichten van de achtergrond- of pop-up-scripts en retourneert de zichtbare tekstinhoud van de webpagina.
chrome.runtime.onMessage.addListener((request, sender, sendResponse)=>{if(request.type ==="GET_PAGE_TEXT"){const bodyText = document.body.innerText.trim();sendResponse({text: bodyText ||null});}});Mogelijk gemaakt door
Dit script luistert naar berichten van het type "GET_PAGE_TEXT" en reageert door de zichtbare bodytekst (document.body.innerText) te extraheren en terug te sturen. Zo kan de achtergrondworker eenvoudig om webpaginas inhoud vragen zonder telkens code te injecteren, waardoor de communicatie netjes en asynchroon blijft.
Stap 5: Backendcoördinatie
Het achtergrondscript (background.js) vormt de brug tussen de pop-up-UI en content scripts. Het zorgt ervoor dat berichtuitwisseling en samenvattingscoördinatie achter de schermen correct verlopen.
Bovenstaande code zorgt voor een soepele stroom van contentextractie, samenvatten en realtime updates. Zo werkt het stap voor stap:
Hij luistert naar een bericht van het type "SUMMARIZE_PAGE" dat wordt geactiveerd door een knopklik in de pop-up.
Eenmaal geactiveerd identificeert hij het actieve tabblad met sender.tab.id en voert een geïnjecteerd script uit via chrome.scripting.executeScript om de zichtbare tekst van de webpagina te extraheren.
Als de content niet toegankelijk is of leeg blijkt, wordt een fallbackbericht teruggestuurd.
Anders haalt hij de database_host (meestal 127.0.0.1) op uit Chrome’s lokale opslag en gebruikt die om een POST-verzoek naar de FastAPI-backend te sturen met de geëxtraheerde content.
Vervolgens opent hij een streamingverbinding en leest hij de respons stukje bij beetje met een ReadableStreamDefaultReader.
Elk fragment wordt gedecodeerd en als tussentijdse update naar de pop-up gestuurd met chrome.runtime.sendMessage met type 'SUMMARY_PROGRESS'.
Zodra de stream eindigt, wordt een laatste bericht met de volledige samenvatting verzonden. Als er tijdens dit proces een fout optreedt, wordt in plaats daarvan een fallbackbericht verstuurd.
Dit achtergrondscript is cruciaal om realtime, gestreamde samenvattingen in de Chrome-extensie mogelijk te maken zonder de UI te blokkeren of de pagina te herladen.
Stap 6: Een extensiepictogram toevoegen
Kies of teken een pictogramafbeelding voor de extensie en sla die op als icon.png in de extensiemap. Dit is wat we gebruiken:
Dit is ideaal voor lokale ontwikkeling of debugging zonder Docker. Om in Docker te draaien, voer je de volgende opdrachten één voor één uit in je terminal:
docker build -t qwen-agent-backend .docker run -p7864:7864 qwen-agent-backendMogelijk gemaakt door
De Chrome-extensie communiceert met poort 7864. Volg nu deze stappen om je extensie in Chrome te laten draaien:
Open Chrome en klik op “Extensions”(aan de rechterkant van de zoekbalk).
Zet “Developer mode” aan en klik op “Load unpacked” (linksboven).
Laad vervolgens onze extensiemap met alle bestanden en wacht op de bevestiging.
Zodra de pop-up de extensie bevestigt, klik je op het extensiepictogram op een willekeurige webpagina en krijg je een streamende samenvatting via je lokale Qwen3-model.
Laten we de resultaten bekijken:
Conclusie
In dit project hebben we een realtime assistent voor webpaginasamenvatting gebouwd met het Qwen-Agent-framework en het Qwen3:1.7B-model dat lokaal draait via Ollama. We hebben een FastAPI-backend ontwikkeld voor LLM-inferentie en die geïntegreerd met een Chrome-extensie die zichtbare paginacontent vastlegt en een live gestreamde samenvatting toont.
Deze demo laat zien hoe Qwen-Agent volledig offline, tool-aangevulde redeneerworkflows in de browser mogelijk maakt. Het vormt de basis voor het bouwen van geavanceerdere lokale agents, zoals browserautomatiseringstools, researchcopilots of documentgebaseerde chatbots.
Wil je meer leren over AI-agents? Bekijk dan deze blogs:
Ik ben een Google Developers Expert in ML (Gen AI), een Kaggle 3x Expert en een Women Techmakers Ambassador met meer dan 3 jaar ervaring in tech. In 2020 heb ik een healthtech-startup mee opgericht en ik volg een master computer science aan Georgia Tech, met als specialisatie machine learning.
Ontdek de basis van AI, leer hoe je AI slim kunt gebruiken voor je werk en duik in modellen zoals ChatGPT om je weg te vinden in de dynamische wereld van AI.
In deze blogpost laat ik je stap voor stap zien hoe je een browserextensie bouwt op basis van Qwen-Agent die de inhoud van elke webpagina realtime samenvat. We behandelen hoe je:
Aan het einde heb je een offline-capabele samenvattingsagent, aangedreven door Qwen3, die met één klik webpagina’s in je browser leest en samenvat.
We houden onze lezers op de hoogte van het laatste AI-nieuws met The Median, onze gratis vrijdagse nieuwsbrief die de belangrijkste verhalen van de week uitlegt. Abonneer je en blijf scherp in een paar minuten per week:
Wat is Qwen-Agent?
Qwen-Agent is een lichtgewicht Python-framework van Alibaba voor het ontwikkelen van krachtige LLM-toepassingen met de Qwen3-modelreeks. Het richt zich op ondersteuning van:
In de kern vereenvoudigt Qwen-Agent de ontwikkeling van modulaire AI-agents met robuuste redeneer- en code-uitvoeringsmogelijkheden. Het bevat elementaire componenten zoals LLM-wrappers en tools, en hoog-niveau
Agent-klassen voor het orkestreren van workflows.Qwen-Agent werkt met zowel cloud-API’s (zoals Alibaba DashScope) als lokale OpenAI-compatibele runtimes zoals vLLM en Ollama. Het maakt het eenvoudig om krachtige LLM-apps te bouwen met:
Projectoverzicht: Realtime web-samenvattingsextensie
In dit deel bouwen we een realtime samenvattingsextensie op basis van Qwen-Agent. Dit omvat:
Laten we elk onderdeel van dit project doorlopen.
Stap 1: De Qwen3-backend instellen
In deze stap behandelen we alle codebestanden die we moeten opzetten zodat onze backend synchroon werkt met onze extensie.
Stap 1.1: Afhankelijkheden en libraries
Begin met het installeren van ollama op je systeem. Noteer vervolgens alle Python-afhankelijkheden die voor de backend nodig zijn in
requirements.txt.Dit helpt om een consistente omgeving op verschillende machines en containers te waarborgen.Hier is een overzicht waar elke library voor wordt gebruikt:
qwen-agent[code_interpreter]: Dit is een kernframework voor agents met ondersteuning voor toolgebruik, waarmee je code kunt uitvoeren en gestructureerd kunt redeneren.fastapienuvicorn: Deze libraries verzorgen samen de async API-backend en dienen je samenvattingslogica.python-dotenvenpython-dateutil: Deze hulplibraries behandelen omgevingsvariabelen en datum-/tijdverwerking (handig voor toekomstige opschaling).matplotlib: Wordt gebruikt voor plotten of het tonen van visuele outputs (optioneel)Als je dit project lokaal wilt draaien, voer dan de volgende opdracht uit om alle afhankelijkheden te installeren:
Stap 1.2: DockerFile-setup (optioneel)
Laten we nu een Dockerfile opzetten die een minimale Python-omgeving met onze app creëert en poort 7864 openstelt, zodat de Chrome-extensie verzoeken kan sturen naar de FastAPI-backend.
Deze setup is optioneel als je ervoor kiest om het project lokaal met Uvicorn te draaien in plaats van met Docker.
Stap 1.3: Qwen serveren met Ollama
Vervolgens moet je het Qwen3-model lokaal ophalen en serveren met Ollama. Deze stap maakt offline-inferentie mogelijk door de LLM-service op je machine te hosten.
Stap 1.4: De FastAPI-samenvattingsserver bouwen
Het bestand
app.pydefinieert een FastAPI-server die realtime samenvattingen van webpaginas inhoud streamt met een lokaal draaiend Qwen3:1.7B-model via Qwen-Agent.Stap 1.4.1: Imports
Begin met het importeren van FastAPI, streamingutilities en Qwen-Agent-componenten zoals
Assistant, die het model en zijn toolmogelijkheden verpakt.Stap 1.4.2: Initialisatie van de FastAPI-app
Vervolgens definiëren we een eenvoudig
RequestData-schema om content te ontvangen, en maken we een botinstantie vanAssistant.In de bovenstaande code is
boteen instantie van deAssistant-klasse uit Qwen-Agent, geconfigureerd om te communiceren met het lokaal geserveerde Qwen3:1.7B-model via Ollama. Het gebruikt de ingebouwdecode_interpreter-tool om door content te redeneren en indien nodig Python-code uit te voeren. Een aangepastesystem_messagestuurt zijn gedrag om beknopte webpaginasamenvattingen te genereren.Opmerking: je kunt kleinere modellen zoals
qwen3:0.6bofqwen3:1.7bgebruiken om de responstijd en het geheugengebruik aanzienlijk te verlagen. Dit is vooral handig voor snellere samenvattingstaken zonder veel redeneerwerk.Stap 1.4.3: Samenvattingsroute
Tot slot leest een endpoint de binnenkomende content en levert live samenvattingstekst terwijl die door het model wordt gegenereerd. Het gebruikt streamingoutput zodat de gebruiker niet hoeft te wachten tot de volledige respons is gegenereerd.
Zo werkt onze samenvatter:
POST-data-instructie met hetRequestData-schema en verwacht een veldcontentmet de ruwe webpaginatekst.stream()-generatorfunctie gedefinieerd die stapsgewijs berichten oplevert. Hierdoor kan er realtime feedback naar de frontend worden gestuurd voordat de uiteindelijke samenvatting klaar is.</nothink>-tagwrapper in het gebruikersbericht om interne redeneringen te onderdrukken, en een gedetailleerde systeemprompt versterkt dit gedrag.bot.run(messages)waarbijmessageseen lijst is met één gebruikersbericht.result_list. De code loopt vervolgens omgekeerd door de lijst om de meest recente contentstring die door de LLM is geretourneerd te extraheren.<think>en</think>worden met een regex uit de output verwijderd om de uiteindelijke samenvatting op te schonen.Stap 2: De Chrome-extensie bouwen
Dit deel loopt door alle bestanden die nodig zijn om de extensie op te zetten, waaronder de frontend-UI, logica om paginacontent te extraheren, achtergrondcommunicatiescripts en configuratiemetagegevens.
Voordat we de afzonderlijke scripts bekijken, volgt hier een eenvoudige uitleg van wat elk bestand doet:
manifest.json: Dit is een configuratiebestand dat extensiemetagegevens, machtigingen en scripts definieert.popup.html: Dit script definieert de zichtbare UI voor de Chrome-extensiepop-up met een knop en een uitvoerpaneel.popup.js: Dit handelt de frontendlogica af, legt de tekst van het huidige tabblad vast en streamt de samenvattingsrespons naar de UI.content.js: Extraheert zichtbare webpaginas inhoud op aanvraag en fungeert als content script.background.js: Coördineert backendcommunicatie en geeft gestreamde samenvattingen door aan de pop-up.icon.png: Het extensiepictogram dat wordt weergegeven in de Chrome-werkbalk en extensiebeheer.Samen maken deze bestanden de extensie interactief, reactief en in staat om met een lokale modelserver te communiceren.
Stap 1: Extensiemetagegevens
Het script
manifest.jsonbevat metagegevens voor Chrome. Het definieert het gedrag van de extensie, machtigingen, de achtergrondserviceworker en de pop-up-UI.Dit bestand definieert de configuratie en mogelijkheden van je Chrome-extensie. Het verleent essentiële machtigingen zoals
scripting(om JavaScript te injecteren),activeTab(om toegang te krijgen tot het huidige tabblad) enstorage(voor het opslaan van gebruikersvoorkeuren).Het veld
host_permissionsmet "<all_urls>" laat de extensie op elke webpagina draaien. Het stelt ook de UI van de extensie in viapopup.htmlenicon.png, en registreert achtergrondgedrag met een serviceworker (background.js). Tot slot definieert het een content script (content.js) dat op alle geladen pagina’s draait, zodat wanneer nodig met webpaginas inhoud kan worden gecommuniceerd.Stap 2: Een zichtbare interface bouwen
De
popup.htmldefinieert de zichtbare interface van de extensie. Hij maakt verbinding met het actieve tabblad, legt de zichtbare tekst vast en stuurt die naar je backendserver.Laten we de bovenstaande code stap voor stap begrijpen:
<head>-tag met metatags voor tekencodering, viewportinstellingen en een<title>-tag.<body>-tag alle UI-elementen, waaronder:<h2>-kop die de gebruiker vraagt de huidige pagina samen te vatten.Summarize-knop (“#summarizeBtn”) die de gebruiker klikt om contentextractie en samenvatten te starten.<pre>-element (“#output”) dat fungeert als scrollbaar paneel om de samenvatting te tonen terwijl deze binnenstroomt.pre-blok maakt de output scrollbaar en visueel onderscheidend.popup.jsluistert naar klikken op de knop, extraheert de paginacontent, stuurt die naar de FastAPI-backend en werkt het uitvoerpaneel bij met gestreamde samenvattingstekst.Stap 3: Een frontend-UI bouwen
De
popup.jsfungeert als frontendlogica-controller die reageert op knopklikken, tekst uit het huidige tabblad extraheert, die naar de backend stuurt en de samenvatting realtime terugstreamt naar het uitvoergebied.Dit JavaScript-bestand verzorgt de frontend-UI-logica van de Chrome-extensie, zodat gebruikers met één klik webpaginas inhoud kunnen extraheren en samenvatten.
Zo werkt het:
DOMContentLoaded), wordt een eventlistener gekoppeld aan de "Summarize"-knop.chrome.tabs.query.chrome.scripting.executeScript, dat probeert deinnerTextvan de pagina-body te extraheren.http://127.0.0.1:7864/summarize_stream_status.res.body.getReader(), zodat delen van de samenvatting realtime in het uitvoerpaneel verschijnen.TextDecoderdecodeert elk gestreamd tekstfragment en voegt het live toe aan de weergave.Stap 4: Webpaginas inhoud extraheren
Voordat we webpaginas inhoud naar onze backend kunnen sturen, hebben we een manier nodig om deze uit het huidige tabblad te halen. Het script
content.jsluistert naar berichten van de achtergrond- of pop-up-scripts en retourneert de zichtbare tekstinhoud van de webpagina.Dit script luistert naar berichten van het type "GET_PAGE_TEXT" en reageert door de zichtbare bodytekst (
document.body.innerText) te extraheren en terug te sturen. Zo kan de achtergrondworker eenvoudig om webpaginas inhoud vragen zonder telkens code te injecteren, waardoor de communicatie netjes en asynchroon blijft.Stap 5: Backendcoördinatie
Het achtergrondscript (
background.js) vormt de brug tussen de pop-up-UI en content scripts. Het zorgt ervoor dat berichtuitwisseling en samenvattingscoördinatie achter de schermen correct verlopen.Bovenstaande code zorgt voor een soepele stroom van contentextractie, samenvatten en realtime updates. Zo werkt het stap voor stap:
sender.tab.iden voert een geïnjecteerd script uit viachrome.scripting.executeScriptom de zichtbare tekst van de webpagina te extraheren.database_host(meestal127.0.0.1) op uit Chrome’s lokale opslag en gebruikt die om eenPOST-verzoek naar de FastAPI-backend te sturen met de geëxtraheerde content.ReadableStreamDefaultReader.chrome.runtime.sendMessagemet type 'SUMMARY_PROGRESS'.Dit achtergrondscript is cruciaal om realtime, gestreamde samenvattingen in de Chrome-extensie mogelijk te maken zonder de UI te blokkeren of de pagina te herladen.
Stap 6: Een extensiepictogram toevoegen
Kies of teken een pictogramafbeelding voor de extensie en sla die op als
icon.pngin de extensiemap. Dit is wat we gebruiken:Afbeelding gegenereerd met FireFly
Je totale mapstructuur zou er zo uit moeten zien:
Stap 3: De app draaien
Om alles lokaal te draaien, voer je de volgende opdracht uit in de terminal:
Dit is ideaal voor lokale ontwikkeling of debugging zonder Docker. Om in Docker te draaien, voer je de volgende opdrachten één voor één uit in je terminal:
De Chrome-extensie communiceert met poort 7864. Volg nu deze stappen om je extensie in Chrome te laten draaien:
Laten we de resultaten bekijken:
Conclusie
In dit project hebben we een realtime assistent voor webpaginasamenvatting gebouwd met het Qwen-Agent-framework en het Qwen3:1.7B-model dat lokaal draait via Ollama. We hebben een FastAPI-backend ontwikkeld voor LLM-inferentie en die geïntegreerd met een Chrome-extensie die zichtbare paginacontent vastlegt en een live gestreamde samenvatting toont.
Deze demo laat zien hoe Qwen-Agent volledig offline, tool-aangevulde redeneerworkflows in de browser mogelijk maakt. Het vormt de basis voor het bouwen van geavanceerdere lokale agents, zoals browserautomatiseringstools, researchcopilots of documentgebaseerde chatbots.
Wil je meer leren over AI-agents? Bekijk dan deze blogs: