Saltar al contenido principal

Qwen-Agente: Una guía con proyecto de demostración

Aprende a utilizar Qwen-Agent y Qwen3 para construir una extensión de resumidor de páginas web en tiempo real.
Actualizado 7 may 2025  · 12 min de lectura

En esta entrada del blog, te explicaré cómo crear una extensión del navegador basada en Qwen-Agent que resuma el contenido de cualquier página web en tiempo real. Veremos cómo hacerlo:

  • Utiliza el modelo Qwen3 de Alibaba localmente a través de Ollama
  • Utiliza Qwen-Agent para la orientación estructurada y el uso de herramientas
  • Crea una extensión de Chrome con actualizaciones de interfaz de usuario en tiempo real
  • Añade el backend FastAPI para soportar el resumen en tiempo real

Al final, dispondrás de un agente resumidor con capacidad offline, impulsado por Qwen3, que lee y resume páginas web en tu navegador con un solo clic.

Mantenemos a nuestros lectores al día de lo último en IA enviándoles The Median, nuestro boletín gratuito de los viernes que desglosa las noticias clave de la semana. Suscríbete y mantente alerta en sólo unos minutos a la semana:

¿Qué es Qwen-Agent?

Qwen-Agente es un marco de trabajo ligero en Python creado por Alibaba para desarrollar potentes aplicaciones LLM utilizando la plataforma Qwen3 de modelos. Se centra en apoyar:

  • Agentes seguidores de instrucciones
  • Uso de herramientas y llamada a funciones
  • Memoria y planificación multivuelta
  • Aviso flexible mediante mensajes estructurados

En esencia, Qwen-Agent simplifica el desarrollo de agentes modulares de IA con sólidas capacidades de razonamiento y ejecución de código. Incluye componentes atómicos como envoltorios LLM y herramientas, así como clases de alto nivel Agent para orquestar flujos de trabajo.

Qwen-Agent funciona tanto con API basadas en la nube (como Alibaba DashScope) como con tiempos de ejecución locales compatibles con OpenAI, como vLLM y Ollama. Facilita la creación de potentes aplicaciones LLM:

  • Integración rápida mediante FastAPI
  • Streaming integrado para interfaces de usuario frontales
  • Cadenas de herramientas de funciones personalizables
  • Total privacidad y rendimiento offline

Resumen del proyecto: Extensión del Resumidor Web en Tiempo Real

En esta sección, construiremos una extensión de resumidor en tiempo real con Qwen-Agent. Esto incluye

  • Un backend FastAPI que acepta el texto de una página web y devuelve un resumen
  • Una extensión de Chrome emergente para capturar el contenido visible de la página
  • Se genera una interfaz de usuario de streaming como resumen
  • Backend alimentado por Qwen3:1.7B ejecutándose localmente a través de Ollama

Recorramos cada parte de este proyecto.

Paso 1: Configurar el backend Qwen3

En este paso, cubriremos todos los archivos de código que necesitamos configurar para que nuestro backend funcione de forma sincronizada con nuestra extensión.

Paso 1.1: Dependencias y bibliotecas 

Empieza instalando ollama en tu sistema. A continuación, enumera todas las dependencias de Python necesarias para el backend en requirements.txt.. Esto ayuda a garantizar una configuración coherente del entorno en todas las máquinas y contenedores.

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

Aquí tienes un desglose de para qué se utiliza cada biblioteca:

  • qwen-agent[code_interpreter]: Se trata de un marco central de agentes con soporte para el uso de herramientas, que permite la ejecución de código y el razonamiento estructurado.
  • fastapi y uvicorn: Estas bibliotecas juntas alimentan el backend asíncrono de la API y sirven a tu lógica de resumen.
  • python-dotenv y python-dateutil: Estas bibliotecas de utilidades gestionan las variables de entorno y el procesamiento de fecha/hora (útil para futuros escalados).
  • matplotlib: Se utiliza para gráficar o mostrar resultados visuales (opcional) 

Si quieres ejecutar este proyecto localmente, ejecuta el siguiente comando para instalar todas las dependencias:

pip install -r requirements.txt

Paso 1.2: Configuración de DockerFile (opcional)

Ahora, vamos a configurar un archivo Dockerfile que cree un entorno Python mínimo con nuestra aplicación y exponga el puerto 7864, permitiendo que la extensión de Chrome envíe peticiones 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"]

Esta configuración es opcional si decides ejecutar el proyecto localmente utilizando Uvicorn en lugar de Docker.

Paso 1.3: Servir a Qwen con Ollama

A continuación, tendrás que extraer y servir el modelo Qwen3 localmente utilizando Ollama. Este paso permite la inferencia fuera de línea alojando el servicio LLM en tu máquina.

ollama pull qwen3:1.7b
ollama serve 

Paso 1.4: Construir el servidor de integración FastAPI

El archivo app.py define un servidor FastAPI que transmite resúmenes en tiempo real del contenido de una página web utilizando un modelo Qwen3:1.7B de ejecución local a través de Qwen-Agent.

Paso 1.4.1: Importaciones

Empieza importando FastAPI, utilidades de streaming y componentes de Qwen-Agent como Assistant, que envuelve el modelo y sus capacidades de herramienta.

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

Paso 1.4.2: Inicialización de la aplicación FastAPI

A continuación, definimos un esquema sencillo de RequestData para recibir contenidos, y creamos una instancia 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."
)

En el código anterior, el bot es una instancia de la clase Assistant de Qwen-Agent, configurada para interactuar con el modelo Qwen3:1.7B servido localmente a través de Ollama. Utiliza la herramienta integrada code_interpreter para razonar sobre el contenido y ejecutar código Python si es necesario. Un system_message personalizado guía su comportamiento para generar resúmenes concisos de páginas web.

Nota: Puedes utilizar modelos más pequeños como qwen3:0.6b o qwen3:1.7b para reducir significativamente el tiempo de respuesta y el uso de memoria. Esto es especialmente útil para tareas de resumen más rápidas sin mucha sobrecarga de razonamiento.

Paso 1.4.3: Ruta del resumidor

Por último, un punto final lee el contenido entrante y proporciona texto resumido en directo a medida que lo genera el modelo. Utiliza salida en streaming para que el usuario no tenga que esperar a que se genere la respuesta completa.

@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")

Así funciona nuestro resumidor:

  • La función recibe la instrucción de datos POST utilizando el esquema RequestData, esperando un campo llamado content que contiene el texto en bruto de la página web.
  • Se define una función generadora anidada stream() para producir mensajes progresivamente. Esto permite enviar información en tiempo real al frontend antes de completar el resumen final.
  • La lista de mensajes incluye explícitamente una envoltura de la etiqueta en el mensaje de usuario para suprimir el razonamiento interno, y una indicación detallada del sistema refuerza este comportamiento.
  • El bot Qwen-Agent se invoca utilizando bot.run(messages), donde messages es una lista que contiene un único mensaje de entrada del usuario. 
  • Las respuestas del modelo se transmiten y recogen en result_list. A continuación, el código recorre la lista en sentido inverso para extraer la cadena de contenido más reciente devuelta por el LLM.
  • Los marcadores de razonamiento interno de Qwen, como y , se eliminan de la salida mediante una regex para limpiar el resumen final.
  • Si no se encuentra ningún contenido válido, se envía un mensaje alternativo. De lo contrario, el resumen limpio se transmite al frontend.

Paso 2: Crear la extensión de Chrome

Esta sección recorre todos los archivos necesarios para configurar la extensión, incluida la interfaz de usuario del frontend, la lógica para extraer el contenido de la página, los scripts de comunicación en segundo plano y los metadatos de configuración.

Antes de ver los guiones individuales, aquí tienes un desglose sencillo de lo que hace cada archivo:

  • manifest.json: Es un archivo de configuración que define los metadatos, permisos y scripts de la extensión.
  • popup.html: Este script define la interfaz de usuario visible para la ventana emergente de la extensión de Chrome con un botón y un panel de salida.
  • popup.js: Se encarga de la lógica del frontend y captura el texto de la pestaña actual y transmite la respuesta del resumen a la interfaz de usuario.
  • content.js: Extrae el contenido visible de la página web cuando se le solicita, actuando como script de contenido.
  • background.js: Este script coordina la comunicación del backend y transmite los resúmenes en streaming a la ventana emergente.
  • icon.png: Se trata de un icono de extensión que aparece en la barra de herramientas y en el gestor de extensiones de Chrome.

Juntos, estos archivos hacen que la extensión sea interactiva, reactiva y capaz de hablar con un servidor de modelos local.

Paso 1: Metadatos de la extensión

El script manifest.json contiene metadatos para Chrome. Define el comportamiento de la extensión, los permisos, el trabajador de servicios en segundo plano y la interfaz de usuario emergente.

{
    "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"
      }
    ]
}  

Este archivo define la configuración y las capacidades de tu extensión de Chrome. Concede permisos esenciales como scripting (para inyectar JavaScript), activeTab (para acceder a la pestaña actual) y storage (para guardar las preferencias del usuario).

El campo host_permissions con "<all_urls>" permite que la extensión se ejecute en cualquier página web. También configura la interfaz de usuario de la extensión a través de popup.html y icon.png, y registra el comportamiento en segundo plano utilizando un trabajador de servicios (background.js). Por último, define un script de contenido (content.js) para que se ejecute en todas las páginas cargadas, permitiendo la interacción con el contenido de la página web cuando sea necesario.

Paso 2: Construir una interfaz visible 

La página popup.html define la interfaz visible de la extensión. Se conecta a la pestaña activa que captura el texto visible y lo envía a tu servidor 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>

Vamos a entender el código anterior paso a paso:

  1. En primer lugar, creamos una estructura HTML básica con una etiqueta que incluye metaetiquetas para la codificación de caracteres, la configuración de la ventana gráfica y una etiqueta </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 continuación, dentro de la etiqueta <code><body></code>, un contenedor contiene todos los elementos de la IU, incluidos:</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 encabezamiento <code><h2></code> que pide al usuario que resuma la página actual.</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 botón <code>Summarize</code> ("#summarizeBtn") que el usuario pulsa para activar la extracción y el resumen del contenido.</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 elemento <code><pre></code> ("#output") que sirve como panel desplazable para mostrar el resumen a medida que entra.</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;">El estilo se define mediante CSS incrustado para que la ventana emergente sea visualmente limpia y legible. El bloque <code>pre</code> se utiliza para que la salida sea desplazable y 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;">El script <code>popup.js</code> escucha los clics de los botones, extrae el contenido de la página, lo envía al backend FastAPI y actualiza el panel de salida con el texto de resumen en streaming.</span></li> </ol> <h3 dir="ltr">Paso 3: Construir una interfaz de usuario frontend </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;">El <code>popup.js</code> actúa como un controlador lógico del frontend que responde a los clics de los botones, extrae texto de la pestaña actual, lo envía al backend y transmite el resumen de vuelta al área de salida en tiempo 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;">Este archivo JavaScript impulsa la lógica de la interfaz de usuario de la extensión de Chrome, permitiendo a los usuarios extraer y resumir el contenido de una página web con 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;">Funciona así:</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;">Cuando se carga la ventana emergente (<code>DOMContentLoaded</code>), se adjunta un receptor de eventos al botón </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;">"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;"> resumir".</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;">Al pulsar el botón, la extensión consulta la pestaña activa en ese momento utilizando <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;">A continuación, ejecuta un script de contenido dentro de esa pestaña utilizando <code>chrome.scripting.executeScript</code>, que intenta extraer el <code>innerText</code> del cuerpo de la 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;">Si la secuencia de comandos falla o devuelve un contenido vacío, se mostrarán los mensajes de error correspondientes en la interfaz de usuario.</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 el texto se extrae correctamente, se envía como una solicitud POST al backend local FastAPI en <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 respuesta se transmite mediante <code>res.body.getReader()</code>, lo que permite que aparezcan trozos parciales del resumen en tiempo real en el panel de salida.</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 <code>TextDecoder</code> descodifica cada fragmento de texto transmitido, añadiéndolo en directo a la pantalla.</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;">Cualquier error de red o de backend se detecta y se muestra en la interfaz emergente para que el usuario pueda depurarlo o dar su opinión.</span></li> </ul> <h3 dir="ltr">Paso 4: Extraer el contenido de la página 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 poder enviar el contenido de la página web a nuestro backend, necesitamos una forma de extraerlo de la pestaña actual. El script <code>content.js</code> escucha los mensajes de los scripts de fondo o emergentes y devuelve el contenido del texto visible de la página 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;">Este script escucha los mensajes de tipo "GET_PAGE_TEXT" y responde extrayendo y devolviendo el texto del cuerpo visible (<code>document.body.innerText</code>). Esto permite al trabajador en segundo plano solicitar fácilmente el contenido de la página web sin inyectar código cada vez, manteniendo la comunicación limpia y asíncrona.</span></p> <h3 dir="ltr">Paso 5: Coordinación de 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;">El script de fondo (<code>background.js</code>) sirve de puente entre los scripts de interfaz de usuario y de contenido de la ventana emergente. Garantiza que el paso de mensajes y la coordinación de la integración se produzcan correctamente entre 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;">El código anterior garantiza un flujo fluido de extracción de contenidos, resumen y actualizaciones en tiempo real. Aquí tienes un desglose paso a paso de cómo 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;">Está a la escucha de un mensaje de tipo "SUMMARIZE_PAGE" desencadenado por la pulsación de un botón en la ventana emergente.</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;">Una vez activado, identifica la pestaña activa mediante <code>sender.tab.id</code> y ejecuta un script inyectado a través de <code>chrome.scripting.executeScript</code> para extraer el texto visible de la página 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 no se puede acceder al contenido o está vacío, devuelve un mensaje de respuesta alternativa.</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;">De lo contrario, obtiene el <code>database_host</code> (normalmente <code>127.0.0.1</code>) del almacenamiento local de Chrome y lo utiliza para enviar una petición <code>POST</code> al backend FastAPI con el contenido 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;">A continuación, abre una conexión de streaming y lee la respuesta trozo a trozo utilizando un <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;">A medida que se descodifica cada trozo, envía actualizaciones provisionales a la ventana emergente utilizando <code>chrome.runtime.sendMessage</code> con el tipo 'RESUMEN_PROGRESO'.</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;">Una vez finalizado el flujo, envía un mensaje final con el resumen completo. Si se produce un error durante este proceso, se envía en su lugar un mensaje de emergencia.</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;">Este script en segundo plano es clave para permitir resúmenes en tiempo real y en streaming en la extensión de Chrome, sin bloquear la interfaz de usuario ni recargar la página.</span></p> <h3 dir="ltr">Paso 6: Añadir un icono de extensión</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;">Elige o dibuja una imagen de icono para la extensión y guárdala como <code>icon.png</code> dentro de la carpeta de la extensión. Esto es lo que utilizaremos:</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="Icono de extensión" 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;">Imagen generada con 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;">Tu estructura general de carpetas debería tener este aspecto:</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">Paso 3: Ejecutar la aplicación</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 ejecutarlo todo localmente, ejecuta el siguiente comando en el 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;">Esto es ideal para el desarrollo local o la depuración sin Docker. Para ejecutarlo en Docker, ejecuta los siguientes comandos de uno en uno en tu 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;">La extensión de Chrome se comunica con el puerto 7864.  Ahora, sigue los siguientes pasos para que tu extensión funcione en 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;">Abre Chrome y haz clic en "Extensiones"</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;">(a la derecha de la barra de búsqueda).</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;">Activa el "Modo desarrollador" y haz clic en "Cargar desempaquetado" (arriba a la izquierda). </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="Pestaña Extensión de Chrome para activar una extensión con 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;">A continuación, carga nuestra carpeta de extensiones que contiene todos los archivos y espera la confirmación.</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;">Una vez que la ventana emergente confirme la configuración de la extensión, haz clic en el icono de la extensión en cualquier página web y obtén un resumen del streaming utilizando tu modelo local de 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;">Veamos los 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="Ejemplo de extensión Qwen-Agent" width="800" height="512" /></p> <h2 dir="ltr">Conclusión</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;">En este proyecto, construimos un asistente de resumen de páginas web en tiempo real utilizando el marco Qwen-Agent y el modelo Qwen3:1.7B ejecutándose localmente a través de Ollama. Desarrollamos un backend FastAPI para manejar la inferencia LLM y lo integramos con una extensión de Chrome que captura el contenido visible de la página y muestra un resumen transmitido en directo.</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 demostración muestra cómo Qwen-Agent puede permitir flujos de trabajo de razonamiento totalmente fuera de línea e incrementados por herramientas en el navegador. Sienta las bases para construir agentes locales más avanzados, como herramientas de automatización del navegador, copilotos de investigación o chatbots basados en 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 más sobre los agentes de IA, consulta estos blogs:</span></p> <ul> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/es/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: Una guía con ejemplos</span></a></li> <li dir="ltr" style="text-align: justify;"><a href="https://www.datacamp.com/es/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/es/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é es la IA Agenética?</span></a></li> </ul>

Aashi Dutt's photo
Author
Aashi Dutt
LinkedIn
Twitter

Soy una Google Developers Expert en ML(Gen AI), una Kaggle 3x Expert y una Women Techmakers Ambassador con más de 3 años de experiencia en tecnología. Cofundé una startup de tecnología sanitaria en 2020 y estoy cursando un máster en informática en Georgia Tech, especializándome en aprendizaje automático.

Temas
Relacionado
An AI juggles tasks

blog

Cinco proyectos que puedes crear con modelos de IA generativa (con ejemplos)

Aprende a utilizar modelos de IA generativa para crear un editor de imágenes, un chatbot similar a ChatGPT con pocos recursos y una aplicación clasificadora de aprobación de préstamos y a automatizar interacciones PDF y un asistente de voz con GPT.
Abid Ali Awan's photo

Abid Ali Awan

10 min

Tutorial

Construir agentes LangChain para automatizar tareas en Python

Un tutorial completo sobre la construcción de agentes LangChain multiherramienta para automatizar tareas en Python utilizando LLMs y modelos de chat utilizando OpenAI.
Bex Tuychiev's photo

Bex Tuychiev

14 min

Tutorial

Guía para principiantes sobre la ingeniería de avisos ChatGPT

Descubra cómo conseguir que ChatGPT le proporcione los resultados que desea dándole las entradas que necesita.
Matt Crabtree's photo

Matt Crabtree

6 min

Tutorial

Tutorial de la API de OpenAI Assistants

Una visión completa de la API Assistants con nuestro artículo, que ofrece una mirada en profundidad a sus características, usos en la industria, guía de configuración y las mejores prácticas para maximizar su potencial en diversas aplicaciones empresariales.
Zoumana Keita 's photo

Zoumana Keita

14 min

Tutorial

Tutorial sobre cómo crear aplicaciones LLM con LangChain

Explore el potencial sin explotar de los grandes modelos lingüísticos con LangChain, un marco Python de código abierto para crear aplicaciones avanzadas de IA.
Moez Ali's photo

Moez Ali

12 min

Tutorial

Tutorial FLAN-T5: Guía y puesta a punto

Una guía completa para afinar un modelo FLAN-T5 para una tarea de respuesta a preguntas utilizando la biblioteca de transformadores, y ejecutando la inferencia optmizada en un escenario del mundo real.
Zoumana Keita 's photo

Zoumana Keita

15 min

Ver másVer más