Corso
In questo post ti mostro come creare un'estensione per browser basata su Qwen-Agent che riassume in tempo reale i contenuti di qualsiasi pagina web. Vedremo come:
- Usare il modello Qwen3 di Alibaba in locale tramite Ollama
- Usare Qwen-Agent per prompting strutturato e uso di tool
- Creare un'estensione Chrome con aggiornamenti UI in streaming
- Aggiungere un backend FastAPI per supportare il riassunto in tempo reale
Alla fine avrai un agente di sintesi utilizzabile offline, basato su Qwen3, che legge e riassume le pagine nel tuo browser con un solo clic.
Manteniamo i nostri lettori aggiornati sulle ultime novità dell'AI con The Median, la nostra newsletter gratuita del venerdì che spiega le notizie chiave della settimana. Iscriviti e resta sul pezzo in pochi minuti a settimana:
Cos'è Qwen-Agent?
Qwen-Agent è un framework Python leggero creato da Alibaba per sviluppare applicazioni LLM potenti usando la famiglia di modelli Qwen3. Si concentra sul supporto di:
- Agenti che seguono istruzioni
- Uso di tool e function calling
- Memoria e pianificazione multi-turno
- Prompting flessibile tramite messaggi strutturati
Alla base, Qwen-Agent semplifica lo sviluppo di agenti AI modulari con solide capacità di ragionamento ed esecuzione di codice. Include componenti atomici come wrapper LLM e tool, oltre a classi Agent di alto livello per orchestrare i flussi di lavoro.
Qwen-Agent funziona sia con API cloud (come Alibaba DashScope) sia con runtime locali compatibili con OpenAI come vLLM e Ollama. Rende facile costruire app LLM potenti con:
- Integrazione rapida tramite FastAPI
- Streaming integrato per interfacce frontend
- Catene di funzioni personalizzabili
- Piena privacy e prestazioni offline
Panoramica del progetto: estensione per riassunti web in tempo reale
In questa sezione creeremo un'estensione di sintesi in tempo reale basata su Qwen-Agent. Include:
- Un backend FastAPI che accetta il testo della pagina e restituisce un riassunto
- Un pop-up dell'estensione Chrome per catturare il contenuto visibile della pagina
- Una UI in streaming mentre viene generato il riassunto
- Backend basato su Qwen3:1.7B in esecuzione locale tramite Ollama
Vediamo ogni parte del progetto.
Step 1: configurare il backend Qwen3
In questo step copriremo tutti i file di codice da predisporre perché il backend funzioni in modo sincrono con la nostra estensione.
Step 1.1: dipendenze e librerie
Inizia installando ollama sul tuo sistema. Poi elenca tutte le dipendenze Python necessarie per il backend in requirements.txt. Questo aiuta a garantire un setup coerente dell'ambiente su macchine e container.
fastapi
uvicorn
python-dateutil
python-dotenv
qwen-agent[code_interpreter]
matplotlib
Ecco a cosa serve ciascuna libreria:
qwen-agent[code_interpreter]: framework core per agenti con supporto all'uso di tool, che abilita esecuzione di codice e ragionamento strutturato.fastapieuvicorn: insieme alimentano il backend API async e servono la tua logica di sintesi.python-dotenvepython-dateutil: librerie di utilità per gestire variabili d'ambiente ed elaborazione di date/ore (utili per scalare in futuro).matplotlib: usata per grafici o output visivi (opzionale)
Se vuoi eseguire questo progetto in locale, lancia il seguente comando per installare tutte le dipendenze:
pip install -r requirements.txt
Step 1.2: setup DockerFile (opzionale)
Ora configuriamo un Dockerfile che crea un ambiente Python minimale con la nostra app ed espone la porta 7864, permettendo all'estensione Chrome di inviare richieste al 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"]
Questo setup è opzionale se preferisci eseguire il progetto in locale usando Uvicorn invece di Docker.
Step 1.3: servire Qwen con Ollama
Poi devi scaricare ed esporre localmente il modello Qwen3 usando Ollama. Questo passaggio abilita l'inferenza offline ospitando il servizio LLM sulla tua macchina.
ollama pull qwen3:1.7b
ollama serve
Step 1.4: creare il server FastAPI per la sintesi
Il file app.py definisce un server FastAPI che trasmette in streaming riassunti in tempo reale del contenuto della pagina usando un modello Qwen3:1.7B in esecuzione locale tramite Qwen-Agent.
Step 1.4.1: import
Inizia importando FastAPI, le utility per lo streaming e i componenti di Qwen-Agent come Assistant, che incapsula il modello e le sue capacità di tool.
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
Step 1.4.2: inizializzazione dell'app FastAPI
Poi definiamo uno schema semplice RequestData per ricevere il contenuto e creiamo un'istanza bot di 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."
)
Nel codice sopra, bot è un'istanza della classe Assistant di Qwen-Agent, configurata per interagire con il modello Qwen3:1.7B servito localmente via Ollama. Usa il tool integrato code_interpreter per ragionare sul contenuto ed eseguire codice Python se necessario. Un system_message personalizzato guida il comportamento per generare riassunti concisi delle pagine.
Nota: puoi usare modelli più piccoli come qwen3:0.6b o qwen3:1.7b per ridurre significativamente tempi di risposta e uso di memoria. È particolarmente utile per compiti di sintesi rapidi senza grande carico di ragionamento.
Step 1.4.3: rotta del summarizer
Infine, un endpoint legge il contenuto in arrivo e restituisce il testo del riassunto in diretta mentre viene generato dal modello. Usa output in streaming così l'utente non deve aspettare l'intera risposta finale.
@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")
Ecco come funziona il nostro summarizer:
- La funzione riceve dati
POSTusando lo schemaRequestData, aspettandosi un campo chiamatocontentche contiene il testo grezzo della pagina. - È definita una funzione generatrice annidata
stream()che produce messaggi progressivi. Questo permette di inviare feedback in tempo reale al frontend prima che il riassunto finale sia completo. - La lista di messaggi include esplicitamente un wrapper con tag
</nothink>nel messaggio utente per sopprimere il ragionamento interno, e un system prompt dettagliato rafforza questo comportamento. - Il bot di Qwen-Agent viene invocato con
bot.run(messages)dovemessagesè una lista contenente un singolo messaggio utente. - Le risposte del modello sono trasmesse in streaming e raccolte in
result_list. Il codice poi scorre la lista al contrario per estrarre la stringa di contenuto più recente restituita dall'LLM. - I marcatori di ragionamento interno di Qwen come
<think>e</think>vengono rimossi dall'output con una regex per pulire il riassunto finale. - Se non viene trovato alcun contenuto valido, viene inviato un messaggio di fallback. Altrimenti, il riassunto pulito viene trasmesso al frontend.
Step 2: creare l'estensione Chrome
Questa sezione copre tutti i file necessari per configurare l'estensione, inclusi la UI frontend, la logica per estrarre il contenuto della pagina, gli script di comunicazione in background e i metadati di configurazione.
Prima di vedere i singoli script, ecco una semplice panoramica di cosa fa ciascun file:
manifest.json: file di configurazione che definisce metadati, permessi e script dell'estensione.popup.html: definisce la UI visibile del popup dell'estensione Chrome con un pulsante e un pannello di output.popup.js: gestisce la logica frontend, cattura il testo della scheda attiva e trasmette la risposta del riassunto alla UI.content.js: estrae il contenuto visibile della pagina web su richiesta, agendo come content script.background.js: coordina la comunicazione col backend e inoltra i riassunti in streaming al popup.icon.png: l'icona dell'estensione mostrata nella toolbar di Chrome e nel gestore estensioni.
Insieme, questi file rendono l'estensione interattiva, reattiva e in grado di comunicare con un server modello locale.
Step 1: metadati dell'estensione
Lo script manifest.json contiene i metadati per Chrome. Definisce il comportamento dell'estensione, i permessi, il service worker di background e la UI del popup.
{
"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"
}
]
}
Questo file definisce la configurazione e le capacità della tua estensione Chrome. Concede permessi essenziali come scripting (per iniettare JavaScript), activeTab (per accedere alla scheda corrente) e storage (per salvare le preferenze utente).
Il campo host_permissions con "<all_urls>" permette all'estensione di funzionare su qualsiasi pagina web. Imposta anche la UI dell'estensione tramite popup.html e icon.png, e registra il comportamento in background usando un service worker (background.js). Infine, definisce un content script (content.js) da eseguire su tutte le pagine caricate, abilitando l'interazione con il contenuto quando necessario.
Step 2: creare un'interfaccia visibile
Il popup.html definisce l'interfaccia visibile dell'estensione. Si collega alla scheda attiva per catturare il testo visibile e inviarlo al tuo server backend.
<!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>
Capire il codice sopra, passo passo:
- Per prima cosa impostiamo una struttura HTML di base con un tag
<head>che include i meta tag per la codifica dei caratteri, le impostazioni del viewport e un tag<title>. - Poi, dentro il tag
<body>, un contenitore racchiude tutti gli elementi della UI, tra cui:
- Un'intestazione
<h2>che invita a riassumere la pagina corrente. - Un pulsante
Summarize(“#summarizeBtn”) che l'utente clicca per avviare l'estrazione e la sintesi del contenuto. - Un elemento
<pre>(“#output”) che funge da pannello scrollabile per visualizzare il riassunto mentre arriva in streaming.
- Lo stile è definito con CSS incorporato per rendere il popup pulito e leggibile. Il blocco
prerende l'output scrollabile e visivamente distinto. - Lo script
popup.jsascolta i clic sul pulsante, estrae il contenuto della pagina, lo invia al backend FastAPI e aggiorna il pannello di output con il testo del riassunto in streaming.
Step 3: creare una UI frontend
Il popup.js funge da controller della logica frontend che risponde ai clic, estrae il testo dalla scheda attiva, lo invia al backend e trasmette in tempo reale il riassunto nell'area di output.
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;
}
}
);
});
});
Questo file JavaScript alimenta la logica della UI frontend dell'estensione Chrome, permettendo di estrarre e riassumere il contenuto di una pagina con un clic.
Ecco come funziona:
- Quando il pop-up viene caricato (
DOMContentLoaded), viene collegato un listener al pulsante "Summarize". - Al clic, l'estensione interroga la scheda attiva con
chrome.tabs.query. - Poi esegue uno script nella scheda con
chrome.scripting.executeScript, che prova a estrarre l'innerTextdel body della pagina. - Se lo script fallisce o restituisce contenuto vuoto, la UI mostra messaggi di fallback.
- Se il testo è estratto con successo, viene inviato come richiesta POST al backend FastAPI locale su
http://127.0.0.1:7864/summarize_stream_status. - La risposta è in streaming tramite
res.body.getReader(), permettendo la comparsa progressiva del riassunto nel pannello di output. - Un
TextDecoderdecodifica ogni chunk di testo, che viene aggiunto in diretta alla visualizzazione. - Eventuali errori di rete o backend vengono intercettati e mostrati nel popup per debug o feedback.
Step 4: estrarre il contenuto della pagina
Prima di inviare il contenuto della pagina al backend, serve un modo per estrarlo dalla scheda corrente. Lo script content.js ascolta i messaggi dal background o dal popup e restituisce il testo visibile della pagina.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === "GET_PAGE_TEXT") {
const bodyText = document.body.innerText.trim();
sendResponse({ text: bodyText || null });
}
});
Questo script ascolta messaggi di tipo "GET_PAGE_TEXT" e risponde estraendo e restituendo il testo visibile del body (document.body.innerText). Consente al worker in background di richiedere facilmente il contenuto senza iniettare codice ogni volta, mantenendo la comunicazione pulita e asincrona.
Step 5: coordinamento del backend
Lo script di background (background.js) fa da ponte tra la UI del popup e i content script. Garantisce che il passaggio di messaggi e il coordinamento della sintesi avvengano correttamente dietro le quinte.
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;
}
});
Il codice sopra garantisce un flusso fluido di estrazione contenuti, sintesi e aggiornamenti in tempo reale. Ecco come funziona, passo passo:
- Ascolta un messaggio di tipo "SUMMARIZE_PAGE" innescato da un clic nel popup.
- Una volta attivato, identifica la scheda attiva tramite
sender.tab.ided esegue uno script iniettato conchrome.scripting.executeScriptper estrarre il testo visibile dalla pagina. - Se il contenuto non è accessibile o è vuoto, invia un messaggio di fallback.
- Altrimenti, recupera il
database_host(di solito127.0.0.1) dallo storage locale di Chrome e lo usa per inviare una richiestaPOSTal backend FastAPI con il contenuto estratto. - Poi apre una connessione in streaming e legge la risposta a chunk usando un
ReadableStreamDefaultReader. - Man mano che ogni chunk viene decodificato, invia aggiornamenti intermedi al popup con
chrome.runtime.sendMessagedi tipo 'SUMMARY_PROGRESS'. - Alla fine dello stream invia un messaggio finale col riassunto completo. In caso di errore, invia invece un messaggio di fallback.
Questo script di background è fondamentale per abilitare riassunti in streaming in tempo reale nell'estensione Chrome senza bloccare la UI o ricaricare la pagina.
Step 6: aggiungere un'icona all'estensione
Scegli o disegna un'icona per l'estensione e salvala come icon.png nella cartella dell'estensione. Ecco quella che useremo:

Immagine generata con FireFly
La struttura complessiva delle cartelle dovrebbe essere così:
QWEN-WEB-SUMMARIZER/
├── backend/
│ ├── app.py
│ ├── Dockerfile
│ └── requirements.txt
├── extension/
│ ├── background.js
│ ├── content.js
│ ├── icon.png
│ ├── manifest.json
│ ├── popup.html
│ └── popup.js
Step 3: eseguire l'app
Per eseguire tutto in locale, lancia il seguente comando nel terminale:
uvicorn backend.app:app --host 0.0.0.0 --port 7864
È ideale per lo sviluppo locale o il debug senza Docker. Per usare Docker, esegui i seguenti comandi uno alla volta nel terminale:
docker build -t qwen-agent-backend .
docker run -p 7864:7864 qwen-agent-backend
L'estensione Chrome comunica con la porta 7864. Ora segui questi passaggi per avviare l'estensione su Chrome:
- Apri Chrome e clicca su “Estensioni” (sul lato destro della barra di ricerca).
- Attiva la “Modalità sviluppatore” e clicca su “Carica estensione non pacchettizzata” (in alto a sinistra).

- Poi carica la cartella della nostra estensione contenente tutti i file e attendi la conferma.
- Dopo la conferma del setup, clicca l'icona dell'estensione su qualsiasi pagina per ottenere un riassunto in streaming usando il tuo modello Qwen3 locale.
Vediamo i risultati:

Conclusione
In questo progetto abbiamo costruito un assistente per il riassunto di pagine web in tempo reale usando il framework Qwen-Agent e il modello Qwen3:1.7B in esecuzione locale tramite Ollama. Abbiamo sviluppato un backend FastAPI per gestire l'inferenza LLM e lo abbiamo integrato con un'estensione Chrome che cattura il contenuto visibile della pagina e mostra un riassunto in streaming.
Questa demo mostra come Qwen-Agent possa abilitare workflow di ragionamento potenziati da tool completamente offline nel browser. Pone le basi per costruire agenti locali più avanzati come strumenti di automazione del browser, copiloti per la ricerca o chatbot basati su documenti.
Per saperne di più sugli agenti AI, leggi questi blog:
Sono una Google Developers Expert in ML (Gen AI), una Kaggle 3x Expert e una Women Techmakers Ambassador con oltre 3 anni di esperienza nel tech. Ho co-fondato una startup health-tech nel 2020 e sto conseguendo un master in informatica al Georgia Tech, con specializzazione in machine learning.


