Usando IA para etiquetar y categorizar publicaciones en foros

Creé una Persona para intentar categorizar y etiquetar automáticamente nuevos temas. Este es el prompt que estoy usando para esa Categoría y Etiquetador.

Eres un asistente de IA para mi foro de Discourse. Tu trabajo es clasificar nuevos temas en una de las categorías predefinidas y aplicar hasta 5 etiquetas relevantes.

Categorías:

  • Categoría 1
  • Categoría 2
  • Categoría 3
  • Categoría 4
  • Categoría 5

Etiquetas:

Tu selección debe provenir de esta lista predefinida (elige hasta 5):
tag1, tag2, tag3, tag4, tag5

Tarea:

  • Selecciona exactamente una categoría de la lista anterior que mejor se ajuste al tema.
  • Selecciona hasta 5 etiquetas de la lista anterior según la relevancia.
  • Formatea tu respuesta como JSON exactamente así:
{
  "category": "Nombre de la Categoría Elegida",
  "tags": ["tag1", "tag2", "tag3"]
}

Cuando selecciono esta persona para usarla en Automatizaciones y al crear la publicación, la IA identifica correctamente las categorías y etiquetas con este resultado en una publicación.

Listar categorías

{
  "category": "Categoría Identificada Correctamente",
  "tags": ["Todas ", "etiquetas", "válidas"]
}

La pregunta es si ya es posible utilizar la IA y las Automatizaciones para mover nuevas publicaciones y categorizarlas, y para que se asignen las etiquetas al tema. Si es así, estoy seguro de que el prompting se puede ajustar para refinar su funcionamiento.

Gracias,

1 me gusta

Llegando… es un poco complicado, nos faltan 2 pequeñas piezas:

  1. Las herramientas personalizadas ahora admiten muchas cosas, pero no admiten la categorización y el etiquetado, podemos agregar eso fácilmente.
  2. Necesito un respondedor que funcione en modo “silencioso”, para que en realidad no responda al tema.

Una vez que ambas estén implementadas, le darías acceso a 2 herramientas personalizadas:

  1. Etiquetar tema
  2. Categorizar tema

(o una sola herramienta que haga ambas cosas)

5 Me gusta

En realidad, mientras estés de acuerdo con el susurro, puedes hacer algo ahora.

La idea es que definirías una herramienta personalizada para categorizar un tema (o etiquetar un tema) y luego harías que la persona la llamara.

Tengo curiosidad por ver cómo te funciona este enfoque, tendrás que probar la herramienta, debería ser bastante sencillo desde la interfaz de usuario de la herramienta.

Es bastante engorroso manejar todo esto, pero también es bastante :exploding_head: que se pueda hacer. El susurro es en realidad razonablemente útil porque te da un poco un “proceso de pensamiento” de cómo el respondedor llegó a las etiquetas/categorías.

3 Me gusta

Gracias Sam,

Más fácil para unos que para otros. Yo pertenezco a esta última categoría :joy:

Estoy trasteando con esto y no he conseguido que funcione. No tengo experiencia con herramientas, así que tengo muchas ganas de que esto funcione para pensar en otras formas de usar las herramientas.

Intenté usar el código de tu blog con una clave de API, lo cual no funcionó. La IA sugirió que usara una URL codificada de forma rígida, así que lo intenté sin éxito.

No se llamó a la clave de API y no se crearon registros con errores.

Esto se utiliza para categorizar automáticamente los temas al crearlos

Clasificaré el tema “Perspectivas de los primeros años y milagros de Jesús” en la categoría Nuevo Testamento, ya que se relaciona con la vida y las enseñanzas de Jesús.
Moviendo el tema ahora…

Esto es lo que intenté.

/**

  • Referencia rápida de la API de herramientas
  • Funciones de entrada
  • invoke(parameters): Función principal. Recibe parámetros (Objeto). Debe devolver un valor serializable en JSON.
  • Ejemplo:
  • function invoke(parameters) { return “resultado”; }
  • details(): Opcional. Devuelve una cadena que describe la herramienta.
  • Ejemplo:
  • function details() { return “Descripción de la herramienta.”; }
  • Objetos proporcionados
    1. http
  • http.get(url, options?): Realiza una solicitud HTTP GET.
  • Parámetros:
  •  url (string): La URL de la solicitud.
    
  •  options (Object, opcional):
    
  •    headers (Object): Encabezados de la solicitud.
    
  • Devuelve:
  •  { status: number, body: string }
    
  • http.post(url, options?): Realiza una solicitud HTTP POST.
  • Parámetros:
  •  url (string): La URL de la solicitud.
    
  •  options (Object, opcional):
    
  •    headers (Object): Encabezados de la solicitud.
    
  •    body (string): Cuerpo de la solicitud.
    
  • Devuelve:
  •  { status: number, body: string }
    
  • (también disponible: http.put, http.patch, http.delete)
  • Nota: Máximo 20 solicitudes HTTP por ejecución.
    1. llm
  • llm.truncate(text, length): Trunca el texto a una longitud de tokens especificada.
  • Parámetros:
  •  text (string): Texto a truncar.
    
  •  length (number): Tokens máximos.
    
  • Devuelve:
  •  Cadena truncada.
    
    1. index
  • index.search(query, options?): Busca documentos indexados.
  • Parámetros:
  •  query (string): Consulta de búsqueda.
    
  •  options (Object, opcional):
    
  •    filenames (Array): Limita la búsqueda a archivos específicos.
    
  •    limit (number): Máximos fragmentos (hasta 200).
    
  • Devuelve:
  •  Matriz de { fragment: string, metadata: string }
    
    1. upload
  • upload.create(filename, base_64_content): Sube un archivo.
  • Parámetros:
  •  filename (string): Nombre del archivo.
    
  •  base_64_content (string): Contenido del archivo codificado en Base64.
    
  • Devuelve:
  •  { id: number, short_url: string }
    
    1. chain
  • chain.setCustomRaw(raw): Establece el cuerpo de la publicación y sale de la cadena.
  • Parámetros:
  •  raw (string): contenido sin procesar para añadir a la publicación.
    
  • Restricciones
  • Tiempo de ejecución: ≤ 2000 ms
  • Memoria: ≤ 10 MB
  • Solicitudes HTTP: ≤ 20 por ejecución
  • Superar los límites provocará errores o la terminación.
  • Seguridad
  • Entorno aislado: Sin acceso a objetos del sistema o globales.
  • Sin acceso al sistema de archivos: No se pueden leer ni escribir archivos.
    */

/**

  • Categorizador de temas de Discourse
  • Esta herramienta te permite cambiar la categoría de un tema de Discourse
  • utilizando la API de Discourse.
    */

/**

  • Categorizador de temas de Discourse
  • Esta herramienta te permite cambiar la categoría de un tema de Discourse
  • utilizando la API de Discourse.
    */

/**

  • Categorizador de temas de Discourse
  • Esta herramienta te permite cambiar la categoría de un tema de Discourse
  • utilizando la API de Discourse.
    */

function invoke(params) {
// Validación de parámetros requeridos
if (!params.topic_id) {
return { error: “Falta el parámetro requerido: topic_id” };
}

if (!params.category_id) {
return { error: “Falta el parámetro requerido: category_id” };
}

// URL base para tu instancia de Discourse
const baseUrl = “https://community.mysite.com”;

// URL completa del punto final de la API para actualizar un tema
const apiUrl = ${baseUrl}/t/${params.topic_id}.json;

// Prepara el cuerpo de la solicitud
const requestBody = {
category_id: params.category_id
};

// Parámetro opcional: actualiza el título si se proporciona
if (params.title) {
requestBody.title = params.title;
}

// Usa tu clave de API proporcionada
const apiKey = “Discourse-API-Key”;

try {
// Realiza la solicitud PUT para actualizar el tema
const response = http.put(apiUrl, {
headers: {
“Content-Type”: “application/json”,
“Api-Key”: apiKey,
“Api-Username”: params.api_username || “system”
},
body: JSON.stringify(requestBody)
});

if (response.status >= 200 && response.status < 300) {
  return {
    success: true,
    topic_id: params.topic_id,
    category_id: params.category_id,
    response: JSON.parse(response.body)
  };
} else {
  return {
    error: `No se pudo actualizar la categoría del tema. Estado: ${response.status}`,
    details: response.body
  };
}

} catch (error) {
return {
error: “Ocurrió un error al actualizar la categoría del tema”,
details: error.toString()
};
}
}

function details() {
return “Clasifica un tema moviéndolo a una categoría especificada”;
}

2 Me gusta

¿@BrianC, finalmente lo resolviste?

1 me gusta

Discourse AI Tagger Categoría Guía Rápida.pdf|adjunto (147.1 KB)

Discourse AI Tagger Categoría.pdf|adjunto (506.0 KB)

@Sam,

Gracias por consultar. Hice un intento pero no pude hacerlo funcionar, así que lo pause. Ejecuté la prueba del escenario con Gemini 2.5 y guardé la salida en PDFs.

Voy a intentarlo nuevamente. Las herramientas aún no son lo mío, pero necesito familiarizarme más con ellas. PDFs adjuntos—quizás hay algo útil en ellos.

3 Me gusta

Hemos añadido una automatización de etiquetado con IA que podría interesarte probar: FEATURE: create AI tagging automation (#34587) · discourse/discourse@e470f3d · GitHub

Prompt que he estado usando para probarlo
 Eres un clasificador de contenido experto y asistente de etiquetado para este foro.

 Tu tarea es analizar las publicaciones y sugerir etiquetas apropiadas basadas en el contenido, las imágenes y la
 lista de etiquetas disponibles proporcionada.

 Directrices:
 - Solo sugiere etiquetas de la lista de etiquetas disponibles proporcionada
 - Sé conservador: etiqueta solo aquello de lo que estés seguro
 - Considera tanto el tema del contenido como la intención de la publicación

 Siempre debes responder con JSON válido en este formato exacto:
 {"tags": ["etiqueta1", "etiqueta2"], "confidence": 85}

 - tags: matriz de nombres de etiquetas de la lista disponible
 - confidence: entero de 0 a 100 que representa tu nivel de confianza

 Si no hay etiquetas apropiadas, usa: {"tags": [], "confidence": 0}

Formato de respuesta JSON de Persona:

{
  "tags": "[string]",
  "confidence": "integer"
}

Herramientas habilitadas para Persona: etiquetas (opcional, esto es si quieres que utilice las etiquetas existentes en el sitio)
La automatización tiene dos modos:

  • Usar etiquetas del sitio existentes
  • Usar la lista de etiquetas proporcionada

La automatización también tiene un nivel de confianza configurable, puedes habilitar/deshabilitar etiquetas restringidas y ajustar el número de publicaciones que utiliza en un tema para el contexto.

6 Me gusta

Gracias por esta gran característica, solo que no pude hacerlo funcionar, el error que recibí fue:

llm_tagger: failed to process post 30550 /t/gecici-baslik-1756753838814/17361/1 : NoMethodError : private method select’ called for an instance of String`

Message

llm_tagger: failed to process post 30550 /t/gecici-baslik-1756753838814/17361/1 : NoMethodError : private method `select' called for an instance of String

Backtrace

/var/www/discourse/plugins/discourse-ai/lib/automation/llm_tagger.rb:140:in `handle'
/var/www/discourse/plugins/discourse-ai/discourse_automation/llm_tagger.rb:110:in `block (2 levels) in <main>'
/var/www/discourse/plugins/automation/app/models/discourse_automation/automation.rb:158:in `block in trigger!'
/var/www/discourse/plugins/automation/app/models/discourse_automation/stat.rb:11:in `log'
/var/www/discourse/plugins/automation/app/models/discourse_automation/automation.rb:156:in `trigger!'
/var/www/discourse/plugins/automation/app/jobs/regular/discourse_automation/trigger.rb:29:in `execute'
/var/www/discourse/app/jobs/base.rb:318:in `block (2 levels) in perform'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rails_multisite-7.0.0/lib/rails_multisite/connection_management/null_instance.rb:49:in `with_connection'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/rails_multisite-7.0.0/lib/rails_multisite/connection_management.rb:17:in `with_connection'
/var/www/discourse/app/jobs/base.rb:305:in `block in perform'
/var/www/discourse/app/jobs/base.rb:301:in `each'
/var/www/discourse/app/jobs/base.rb:301:in `perform'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:220:in `execute_job'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:185:in `block (4 levels) in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:180:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/lib/sidekiq/discourse_event.rb:6:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/lib/sidekiq/pausable.rb:131:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job/interrupt_handler.rb:9:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:183:in `block in traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/metrics/tracking.rb:26:in `track'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/metrics/tracking.rb:134:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:182:in `traverse'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/middleware/chain.rb:173:in `invoke'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:183:in `block (3 levels) in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:145:in `block (6 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_retry.rb:118:in `local'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:144:in `block (5 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/config.rb:39:in `block in <class:Config>'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:139:in `block (4 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:281:in `stats'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:134:in `block (3 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_logger.rb:15:in `call'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:133:in `block (2 levels) in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_retry.rb:85:in `global'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:132:in `block in dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/job_logger.rb:40:in `prepare'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:131:in `dispatch'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:183:in `block (2 levels) in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:182:in `handle_interrupt'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:182:in `block in process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:181:in `handle_interrupt'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:181:in `process'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:86:in `process_one'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/processor.rb:76:in `run'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/component.rb:10:in `watchdog'
/var/www/discourse/vendor/bundle/ruby/3.3.0/gems/sidekiq-7.3.9/lib/sidekiq/component.rb:19:in `block in safe_thread'

¿Meta lo usa? Pensé que había visto a @system etiquetar un tema hace unos días.

1 me gusta

Podría ser complicado de configurar, pero si Discourse ya está generando incrustaciones (para búsqueda semántica) para temas nuevos, probablemente podrías generar incrustaciones para las descripciones de las etiquetas, luego usar la similitud semántica para sugerir etiquetas para temas nuevos, o etiquetar automáticamente temas existentes.

3 Me gusta

Sí, lo ejecuté durante unos días en Meta para hacer más pruebas… funciona más o menos, pero sin descripciones de etiquetas, estaba usando incorrectamente how-to para algunas preguntas de tipo “cómo hago” y aplicaba moderator a publicaciones donde se usaba la palabra, pero no era del todo relevante para el tema. Algunos ajustes en el prompt probablemente ayudarán, y las ideas de @simon parecen que podrían ser grandes mejoras futuras.

2 Me gusta

Mi suposición es que los embeddings podrían funcionar mejor y ser más rápidos/baratos. Mi interés real en esto se alinea más con un sistema de etiquetado dinámico que podría reemplazar el codificado de etiquetas en la base de datos. Por ejemplo, “cómo” + “sso” sería una lista de temas con embeddings que estuvieran semánticamente cerca de los embeddings de la descripción de las etiquetas “cómo” y “sso”.

2 Me gusta