Qwen-Agent: Um guia com projeto de demonstração
Nesta publicação do blog, mostrarei a você como criar uma extensão de navegador com o Qwen-Agent que resume o conteúdo de qualquer página da Web em tempo real. Abordaremos como você pode:
- Use o modelo Qwen3 da Alibaba localmente via Ollama
- Usar o Qwen-Agent para estímulos estruturados e uso de ferramentas
- Crie uma extensão do Chrome com atualizações de interface do usuário em fluxo contínuo
- Adicionar backend FastAPI para oferecer suporte à compactação em tempo real
Ao final, você terá um agente resumidor habilitado para off-line, com a tecnologia Qwen3, que lê e resume páginas da Web em seu navegador com um único clique.
Mantemos nossos leitores atualizados sobre as últimas novidades em IA enviando o The Median, nosso boletim informativo gratuito de sexta-feira que detalha as principais histórias da semana. Inscreva-se e fique atento em apenas alguns minutos por semana:
O que é o Qwen-Agent?
Qwen-Agent é uma estrutura Python leve criada pela Alibaba para desenvolver aplicativos LLM poderosos usando o Qwen3 usando a família de modelos Qwen3. Ele se concentra no suporte:
- Agentes de acompanhamento de instruções
- Uso de ferramentas e chamadas de funções
- Memória e planejamento de várias voltas
- Solicitação flexível por meio de mensagens estruturadas
Em sua essência, o Qwen-Agent simplifica o desenvolvimento de agentes de IA modulares com raciocínio robusto e recursos de execução de código. Ele inclui componentes atômicos, como ferramentas e wrappers LLM, bem como classes Agent
de alto nível para orquestrar fluxos de trabalho.
O Qwen-Agent funciona com APIs baseadas em nuvem (como o Alibaba DashScope) e tempos de execução locais compatíveis com OpenAI, como vLLM e Ollama. Com ele, você pode criar facilmente aplicativos LLM avançados:
- Integração rápida via FastAPI
- Fluxo integrado para UIs de front-end
- Cadeias de ferramentas de funções personalizáveis
- Privacidade e desempenho off-line completos
Visão geral do projeto: Extensão do Web Summarizer em tempo real
Nesta seção, criaremos uma extensão de resumidor em tempo real alimentada pelo Qwen-Agent. Isso inclui:
- Um backend FastAPI que aceita texto de página da Web e retorna um resumo
- Um pop-up de extensão do Chrome para capturar o conteúdo visível da página
- Uma interface de usuário de fluxo contínuo como um resumo é gerada
- Backend com tecnologia Qwen3:1.7B em execução local via Ollama
Vamos examinar cada parte desse projeto.
Etapa 1: Configurando o back-end do Qwen3
Nesta etapa, abordaremos todos os arquivos de código que precisamos configurar para que nosso backend funcione de forma síncrona com nossa extensão.
Etapa 1.1: Dependências e bibliotecas
Comece instalando o ollama em seu sistema. Em seguida, liste todas as dependências do Python necessárias para o backend em requirements.txt.
. Isso ajuda a garantir uma configuração consistente do ambiente entre máquinas e contêineres.
fastapi
uvicorn
python-dateutil
python-dotenv
qwen-agent[code_interpreter]
matplotlib
Aqui está um detalhamento da finalidade de cada biblioteca:
qwen-agent[code_interpreter]
: Trata-se de uma estrutura de agente central com suporte ao uso de ferramentas, permitindo a execução de código e o raciocínio estruturado.fastapi
euvicorn
: Juntas, essas bibliotecas alimentam o backend da API assíncrona e atendem à lógica do resumidor.python-dotenv
epython-dateutil
: Essas bibliotecas de utilitários lidam com variáveis de ambiente e processamento de data/hora (útil para escalonamento futuro).matplotlib
: É usado para plotar ou mostrar resultados visuais (opcional)
Se você quiser executar esse projeto localmente, execute o seguinte comando para instalar todas as dependências:
pip install -r requirements.txt
Etapa 1.2: Configuração do DockerFile (opcional)
Agora, vamos configurar um Dockerfile que cria um ambiente Python mínimo com nosso aplicativo e expõe a porta 7864, permitindo que a extensão do Chrome envie solicitações ao 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"]
Essa configuração é opcional se você optar por executar o projeto localmente usando o Uvicorn em vez do Docker.
Etapa 1.3: Servindo Qwen com Ollama
Em seguida, você precisará extrair e servir o modelo Qwen3 localmente usando o Ollama. Esta etapa permite a inferência off-line ao hospedar o serviço LLM em seu computador.
ollama pull qwen3:1.7b
ollama serve
Etapa 1.4: Criando o servidor de compactação FastAPI
O arquivo app.py
define um servidor FastAPI que transmite resumos em tempo real do conteúdo de páginas da Web usando um modelo Qwen3:1.7B em execução local por meio do Qwen-Agent.
Etapa 1.4.1: Importações
Comece importando FastAPI, utilitários de streaming e componentes do Qwen-Agent, como Assistant
, que envolve o modelo e suas habilidades de ferramenta.
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
Etapa 1.4.2: Inicialização do aplicativo FastAPI
Em seguida, definimos um esquema simples de RequestData
para receber conteúdo e criamos uma instância de bot de 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."
)
No código acima, o bot
é uma instância da classe Assistant
do Qwen-Agent, configurada para interagir com o modelo Qwen3:1.7B servido localmente por meio do Ollama. Ele usa a ferramenta integrada code_interpreter
para analisar o conteúdo e executar o código Python, se necessário. Um system_message
personalizado orienta seu comportamento para gerar resumos concisos de páginas da Web.
Observação: Você pode usar modelos menores, como qwen3:0.6b
ou qwen3:1.7b
, para reduzir significativamente o tempo de resposta e o uso da memória. Isso é especialmente útil para tarefas de resumo mais rápidas sem muita sobrecarga de raciocínio.
Etapa 1.4.3: Rota do resumidor
Por fim, um endpoint lê o conteúdo de entrada e produz um texto de resumo em tempo real à medida que é gerado pelo modelo. Ele usa saída de streaming para que o usuário não precise esperar que a resposta completa seja gerada.
@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")
Veja como nosso resumidor funciona:
- A função recebe a instrução de dados
POST
usando o esquemaRequestData
, esperando um campo chamadocontent
que contém o texto bruto da página da Web. - Uma função geradora
stream()
aninhada é definida para produzir mensagens progressivamente. Isso permite que o feedback em tempo real seja enviado de volta ao front-end antes que o resumo final seja concluído. - A lista de mensagens inclui explicitamente um wrapper de tag
na mensagem do usuário para suprimir o raciocínio interno, e um prompt detalhado do sistema reforça esse comportamento.
- O bot Qwen-Agent é chamado usando
bot.run(messages)
, ondemessages
é uma lista que contém uma única mensagem de entrada do usuário. - As respostas do modelo são transmitidas e coletadas em
result_list
. Em seguida, o código percorre a lista de forma inversa para extrair a string de conteúdo mais recente retornada pelo LLM. - Os marcadores de raciocínio interno do Qwen, como
e
, são removidos da saída usando um regex para limpar o resumo final.
- Se nenhum conteúdo válido for encontrado, uma mensagem de fallback será enviada. Caso contrário, o resumo limpo é transmitido para o frontend.
Etapa 2: Criando a extensão do Chrome
Esta seção apresenta todos os arquivos necessários para configurar a extensão, inclusive a interface do usuário de front-end, a lógica para extrair o conteúdo da página, os scripts de comunicação em segundo plano e os metadados de configuração.
Antes de examinarmos os scripts individuais, aqui está um detalhamento simples do que cada arquivo faz:
manifest.json
: Esse é um arquivo de configuração que define metadados, permissões e scripts de extensão.popup.html
: Esse script define a interface do usuário visível para o pop-up da extensão do Chrome com um botão e um painel de saída.popup.js
: Isso lida com a lógica de front-end e captura o texto da guia atual e transmite a resposta resumida para a interface do usuário.content.js
: Ele extrai o conteúdo visível da página da Web quando solicitado, atuando como o script de conteúdo.background.js
: Esse script coordena a comunicação de back-end e retransmite os resumos transmitidos para o pop-up.icon.png
: Esse é um ícone de extensão exibido na barra de ferramentas e no gerenciador de extensões do Chrome.
Juntos, esses arquivos tornam a extensão interativa, reativa e capaz de se comunicar com um servidor de modelo local.
Etapa 1: Metadados de extensão
O script manifest.json
contém metadados para o Chrome. Ele define o comportamento da extensão, as permissões, o trabalhador do serviço em segundo plano e a interface do usuário pop-up.
{
"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"
}
]
}
Esse arquivo define a configuração e os recursos de sua extensão do Chrome. Ele concede permissões essenciais, como scripting
(para injetar JavaScript), activeTab
(para acessar a guia atual) e storage
(para salvar as preferências do usuário).
O campo host_permissions
com "<all_urls>" permite que a extensão seja executada em qualquer página da Web. Ele também configura a interface do usuário da extensão por meio de popup.html
e icon.png
e registra o comportamento em segundo plano usando um service worker (background.js
). Por fim, ele define um script de conteúdo (content.js
) a ser executado em todas as páginas carregadas, permitindo a interação com o conteúdo da página da Web quando necessário.
Etapa 2: Criando uma interface visível
O site popup.html
define a interface visível da extensão. Ele se conecta à guia ativa que captura o texto visível e o envia ao seu servidor de back-end.
<!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>
Vamos entender o código acima passo a passo:
- Primeiro, configuramos uma estrutura HTML básica com uma tag
que inclui metatags para codificação de caracteres, configurações de viewport e uma tag
</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;">Em seguida, dentro da tag <code><body></code>, um contêiner contém todos os elementos da interface do usuário, inclusive:</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;">Um título <code><h2></code> que solicita que o usuário resuma a página atual.</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;">Um botão <code>Summarize</code> ("#summarizeBtn") no qual o usuário clica para acionar a extração e o resumo do conteúdo.</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;">Um elemento <code><pre></code> ("#output") que funciona como um painel rolável para exibir o resumo à medida que ele é transmitido.</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;">O estilo é definido usando CSS incorporado para tornar o pop-up visualmente limpo e legível. O bloco <code>pre</code> é usado para tornar a saída rolável e visualmente distinta.</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;">O script <code>popup.js</code> escuta os cliques nos botões, extrai o conteúdo da página, envia-o para o back-end da FastAPI e atualiza o painel de saída com o texto de resumo do fluxo.</span></li> </ol> <h3 dir="ltr">Etapa 3: Criação de uma interface de usuário de front-end </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;">O site <code>popup.js</code> atua como um controlador lógico de front-end que responde a cliques em botões, extrai texto da guia atual, envia-o para o back-end e transmite o resumo de volta para a área de saída em tempo real.</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;">Esse arquivo JavaScript alimenta a lógica da interface do usuário de front-end da extensão do Chrome, permitindo que os usuários extraiam e resumam o conteúdo da página da Web com um clique.</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;">Veja como isso funciona:</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;">Quando o pop-up é carregado (<code>DOMContentLoaded</code>), um ouvinte de evento é anexado ao botão </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;">"Summarize" (Resumir)</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;"> botão.</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;">Ao clicar no botão, a extensão consulta a guia ativa no momento usando <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;">Em seguida, ele executa um script de conteúdo dentro dessa guia usando <code>chrome.scripting.executeScript</code>, que tenta extrair o <code>innerText</code> do corpo da página.</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;">Se o script falhar ou retornar conteúdo vazio, as mensagens de fallback apropriadas serão mostradas na interface do usuário.</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;">Se o texto for extraído com êxito, ele será enviado como uma solicitação POST para o backend local da FastAPI em <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;">A resposta é transmitida usando <code>res.body.getReader()</code>, o que permite que partes parciais do resumo sejam exibidas em tempo real no painel de saída.</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;">O site <code>TextDecoder</code> decodifica cada trecho de texto transmitido, anexando-o ao vivo à tela.</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;">Todos os erros de rede ou de back-end são detectados e exibidos na interface pop-up para depuração ou feedback do usuário.</span></li> </ul> <h3 dir="ltr">Etapa 4: Extração do conteúdo da página da 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;">Antes de enviarmos o conteúdo da página da Web para o backend, precisamos de uma maneira de extraí-lo da guia atual. O script <code>content.js</code> escuta as mensagens dos scripts de fundo ou pop-up e retorna o conteúdo de texto visível da página da 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;">Esse script escuta mensagens do tipo "GET_PAGE_TEXT" e responde extraindo e retornando o texto do corpo visível (<code>document.body.innerText</code>). Isso permite que o trabalhador em segundo plano solicite facilmente o conteúdo da página da Web sem injetar código a cada vez, mantendo a comunicação limpa e assíncrona.</span></p> <h3 dir="ltr">Etapa 5: Coordenação de back-end</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;">O script de plano de fundo (<code>background.js</code>) faz a ponte entre a IU do popup e os scripts de conteúdo. Ele garante que a transmissão de mensagens e a coordenação de compactação ocorram corretamente nos bastidores.</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;">O código acima garante um fluxo suave de extração de conteúdo, resumo e atualizações em tempo real. Aqui está um detalhamento passo a passo de como isso funciona:</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;">Ele fica atento a uma mensagem do tipo "SUMMARIZE_PAGE" acionada por um clique de botão no pop-up.</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;">Uma vez acionado, ele identifica a guia ativa usando <code>sender.tab.id</code> e executa um script injetado via <code>chrome.scripting.executeScript</code> para extrair o texto visível da página da 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;">Se o conteúdo não puder ser acessado ou estiver vazio, ele enviará de volta uma mensagem de fallback.</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;">Caso contrário, ele obtém o <code>database_host</code> (geralmente <code>127.0.0.1</code>) do armazenamento local do Chrome e o utiliza para enviar uma solicitação <code>POST</code> para o backend da FastAPI com o conteúdo extraído.</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;">Em seguida, ele abre uma conexão de streaming e lê a resposta trecho por trecho usando um <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;">À medida que cada bloco é decodificado, ele envia atualizações provisórias para o pop-up usando <code>chrome.runtime.sendMessage</code> com o tipo '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;">Quando o fluxo termina, ele envia uma mensagem final com o resumo completo. Se ocorrer um erro durante esse processo, uma mensagem de fallback será enviada em seu lugar.</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;">Esse script em segundo plano é fundamental para permitir resumos de streaming em tempo real na extensão do Chrome sem bloquear a interface do usuário ou recarregar a página.</span></p> <h3 dir="ltr">Etapa 6: Adicionar um ícone de extensão</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;">Escolha ou desenhe uma imagem de ícone para a extensão e salve-a como <code>icon.png</code> na pasta da extensão. É isso que usaremos:</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="Ícone de extensão" 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;">Imagem gerada com o 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;">Sua estrutura geral de pastas deve ser semelhante a esta:</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">Etapa 3: Executando o aplicativo</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;">Para executar tudo localmente, execute o seguinte comando no 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;">Isso é ideal para desenvolvimento local ou depuração sem o Docker. Para executar no Docker, execute os seguintes comandos, um de cada vez, em seu 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;">A extensão do Chrome se comunica com a porta 7864. Agora, siga as etapas a seguir para que sua extensão seja executada no 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;">Abra o Chrome e clique em "Extensões"</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;">(no lado direito da barra de pesquisa).</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;">Ative o "Modo de desenvolvedor" e clique em "Carregar descompactado" (canto superior esquerdo). </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="Guia Extensão do Chrome para ativar uma extensão com o 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;">Em seguida, carregue nossa pasta de extensões que contém todos os arquivos e aguarde a confirmação.</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;">Depois que a janela pop-up confirmar a configuração da extensão, clique no ícone da extensão em qualquer página da Web e obtenha um resumo da transmissão usando o modelo local do Qwen3.</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;">Vamos ver os resultados:</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="Exemplo de extensão do Qwen-Agent" width="800" height="512" /></p> <h2 dir="ltr">Conclusão</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;">Neste projeto, criamos um assistente de resumo de páginas da Web em tempo real usando a estrutura Qwen-Agent e o modelo Qwen3:1.7B executado localmente via Ollama. Desenvolvemos um backend FastAPI para lidar com a inferência LLM e o integramos a uma extensão do Chrome que captura o conteúdo visível da página e exibe um resumo transmitido ao vivo.</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;">Esta demonstração mostra como o Qwen-Agent pode habilitar fluxos de trabalho de raciocínio totalmente off-line e com ferramentas no navegador. Ele estabelece a base para a criação de agentes locais mais avançados, como ferramentas de automação de navegador, copilotos de pesquisa ou chatbots baseados em documentos.</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;">Para saber mais sobre agentes de IA, confira estes blogs:</span></p> <ul> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/pt/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: Um guia com exemplos</span></a></li> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/pt/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;">Tipos de agentes de IA</span></a></li> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/pt/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;">O que é IA agêntica?</span></a></li> </ul>

Sou Google Developers Expert em ML (Gen AI), Kaggle 3x Expert e Women Techmakers Ambassador com mais de 3 anos de experiência em tecnologia. Fui cofundador de uma startup de tecnologia de saúde em 2020 e estou fazendo mestrado em ciência da computação na Georgia Tech, com especialização em machine learning.
Tutorial
Um guia para iniciantes na engenharia de prompts do ChatGPT

Tutorial
Guia para iniciantes no uso da API do ChatGPT
Tutorial
Guia de Introdução ao Ajuste Fino de LLMs

Josep Ferrer
11 min

Tutorial
Tutorial da API de assistentes da OpenAI

Tutorial
Visão GPT-4: Um guia abrangente para iniciantes

Tutorial