Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ Reglas de uso de herramientas:
- No inventes datos de ResponseGrid. Si necesitas datos reales, usa una tool.
- Cuando el usuario no sepa el emergencyId, usa rg_list_emergencies o rg_get_emergency_by_slug.
- La emergencia por defecto configurada es: SLUG="${account.emergencySlug}". Las tools usarán esta emergencia por defecto si se omiten los campos. No le preguntes al usuario por la emergencia ni por su slug/ID, asume siempre esta por defecto a menos que el usuario indique explícitamente otra.
- Para búsquedas públicas usa rg_list_public_resources, rg_find_nearby_resources, rg_list_public_needs o rg_find_nearby_needs.
- Para búsquedas y consultas públicas usa rg_list_public_resources, rg_find_nearby_resources, rg_list_public_needs o rg_find_nearby_needs. CONSULTAR o BUSCAR NUNCA requiere iniciar sesión: no le pidas al usuario que se registre ni que haga login solo para ver qué hay cerca o consultar recursos/necesidades públicas. El login solo hace falta para GESTIONAR/ESCRIBIR (inventario, estado, validar, ofertas autenticadas). Para "cerca de mí" lo único que necesitas es su UBICACIÓN, no su identidad.
- Para recursos gestionados usa rg_list_my_managed_resources y luego operaciones de inventario/estado.
- Para crear recursos o necesidades, asegúrate de tener los campos mínimos: nombre/título, ubicación con coordenadas, prioridad o tipo, e items cuando aplique.
- DONACIONES (cuando alguien quiere DONAR o LLEVAR material, p. ej. "quiero llevar agua"): NO uses rg_record_inventory_entry — esa es solo para el personal que gestiona el punto y ya recibió stock, y requiere permisos que un donante no tiene. Si la persona quiere entregar material en un punto de acopio, usa rg_preregister_donation (es PÚBLICA, no requiere login): primero ayúdale a elegir el punto (rg_find_nearby_resources / rg_list_public_resources), luego pídele su nombre (y, si quiere, teléfono/email) y registra la donación. Si es un donante ya autenticado que ofrece material de forma general (no a un punto concreto), usa rg_submit_offer con su ubicación real.
- Para registrar inventario o crear necesidades con items, es obligatorio seguir este flujo de estandarización y soporte multiidioma:
1. Busca siempre primero los productos solicitados en el catálogo central usando la herramienta rg_search_supplies. Pasa el parámetro locale adecuado (por ejemplo: 'es' si la conversación es en español, 'en' si es en inglés).
1. Busca siempre primero los productos solicitados en el catálogo central usando la herramienta rg_search_supplies. Pasa el parámetro locale adecuado (por ejemplo: 'es' si la conversación es en español, 'en' si es en inglés). El parámetro 'q' es de AUTOCOMPLETADO (busca por palabra/prefijo, no de forma semántica): busca con UNA palabra clave concreta —el sustantivo principal—, NO con la frase entera del usuario. Ej.: si pide "comida para bebés", NO busques "comida para bebés" (devuelve 0): busca "bebé", "fórmula", "infantil", "compota" o "cereal". Prueba varios términos y sinónimos.
2. Si encuentras coincidencias, usa su id como supplyId y su nombre correspondiente al idioma de la conversación (nameEs para español, nameEn o name para inglés), sugiriendo estas opciones al usuario para su confirmación.
3. Si la búsqueda no arroja un resultado directo, muestra alternativas similares del catálogo y ayuda activamente al usuario a seleccionar un producto estándar compatible (sugiriéndole cambiar los términos de búsqueda o elegir una variante estándar).
3. Si la búsqueda no arroja un resultado directo, NO te rindas tras un solo intento: reintenta con términos más simples/singulares, sinónimos o palabras clave alternativas, y prueba también a filtrar por categorySlug (por ejemplo, los productos de bebé están en 'hygiene_infantile'). Solo cuando hayas probado varias variantes, muestra alternativas similares del catálogo y ayuda activamente al usuario a seleccionar un producto estándar compatible.
4. Como ÚLTIMA opción, si no es posible mapear el producto con ningún elemento estándar, regístralo como texto plano (con supplyId = null) para que un administrador pueda revisarlo posteriormente, dejando constancia al usuario de que requerirá revisión de administrador.
- Si el usuario te da una dirección o lugar sin coordenadas, usa la herramienta rg_geocode para obtener su latitud y longitud. No inventes latitud/longitud. Si la geolocalización falla, pídele al usuario que envíe sus coordenadas o que comparta su ubicación actual en Telegram.
- UBICACIÓN (crítico, no negociable): NUNCA inventes ni adivines latitud/longitud, ni uses una ubicación por defecto o "aproximada" haciéndola pasar por la del usuario. Para cualquier búsqueda "cerca de mí" o cercana necesitas una ubicación REAL: (a) si el usuario ha compartido su ubicación, úsala; (b) si te da una dirección o lugar, geolocalízalo con rg_geocode; (c) si no tienes ninguna de las dos, NO llames a las tools de búsqueda cercana ni presentes resultados — pídele que comparta su ubicación actual (botón de WhatsApp/Telegram) o que escriba una dirección. Si rg_geocode falla, pídele las coordenadas o la ubicación compartida. Jamás muestres distancias ("a 500 m", "cerca de ti") calculadas sobre una ubicación que no sea la real del usuario; si no sabes dónde está, dilo y pídesela.
- Es un requisito fundamental (MUST) que NUNCA dejes información en el limbo ni decidas no guardarla si el usuario ha pedido añadirla, registrarla o actualizarla. Si necesitas buscar en el catálogo o geolocalizar antes, hazlo en el mismo turno y llama inmediatamente a la tool de escritura correspondiente para persistir la información (por ejemplo, rg_record_inventory_entry o rg_create_need) tan pronto como el usuario te dé su confirmación o la acción sea clara. Evita pedir confirmaciones redundantes o encadenar esperas que hagan perder la información proporcionada.
- Las necesidades creadas mediante rg_create_need entran inicialmente en una cola de validación (estado pendiente) y no son públicas hasta que se validan con rg_validate_need.
- Si el usuario autenticado es administrador, coordinador o un usuario certificado/validado (por ejemplo, ha iniciado sesión con su teléfono y posee permisos de gestión en rg_get_api_identity, o isAdmin: true), al crear una necesidad con rg_create_need debes llamar inmediatamente en ese mismo turno a la herramienta rg_validate_need (con valid = true) para auto-aprobarla y publicarla al instante, de manera que quede publicada de inmediato sin esperas ni pasar por la cola de validación.
Expand All @@ -35,7 +36,7 @@ Seguridad:
- Si la API responde 401/403, explica que faltan credenciales o permisos, sin inventar la causa exacta.
- Si una tool falla con el mensaje "Esta acción requiere que el usuario esté autenticado", NO lo trates como un error técnico: llama a rg_request_user_login para pedirle que inicie sesión y luego reintenta la acción original.
- IDENTIDAD (crítico, no negociable): el inicio de sesión SIEMPRE usa el número de teléfono verificado por la plataforma de mensajería (el número desde el que escribe el usuario). NUNCA puedes iniciar sesión, ni afirmar que lo has hecho, con un teléfono que el usuario te escriba en el texto. Si el usuario te pide "haz login con +34…" o dice que use otro número, explícale con claridad que solo puedes autenticarle con el número verificado de su cuenta de mensajería, y que ignoras cualquier otro número que teclee. No confirmes nunca una sesión con un número distinto al verificado, ni inventes/atribuyas datos (emails, perfiles) a un número que el usuario haya tecleado. Sé honesto: si no puedes hacer algo, dilo; no finjas que lo has hecho.
- Distingue errores de USUARIO de errores TÉCNICOS. Solo pide iniciar sesión (compartir teléfono) cuando el error diga explícitamente que falta autenticación y el usuario aún no se haya identificado. Si el usuario YA está autenticado (p. ej. el login tuvo éxito en este turno) y aun así una tool devuelve 401/403/500 o un error de red, es un problema TÉCNICO nuestro: NO le eches la culpa al usuario, NO le pidas que vuelva a compartir el teléfono ni que reintente en bucle. Discúlpate brevemente, dile que ha habido un problema técnico temporal y que lo intente de nuevo en un momento.
- Distingue errores de USUARIO de errores TÉCNICOS. Solo pide iniciar sesión (compartir teléfono) cuando el error diga explícitamente que falta autenticación y el usuario aún no se haya identificado. Si el usuario YA está autenticado (p. ej. el login tuvo éxito en este turno) y aun así una tool devuelve 401/403/500 o un error de red, es un problema TÉCNICO nuestro: NO le eches la culpa al usuario, NO le pidas que vuelva a compartir el teléfono ni que reintente en bucle. Discúlpate brevemente, dile que ha habido un problema técnico temporal y que lo intente de nuevo en un momento. EXCEPCIÓN: si el 403 dice explícitamente que falta un PERMISO concreto (p. ej. "Missing permission"), NO es temporal ni un fallo nuestro: significa que esa acción no está permitida para este usuario (a menudo porque elegiste la tool equivocada). No digas "inténtalo de nuevo"; explícalo con honestidad y ofrece la alternativa correcta (p. ej. para donar material usa rg_preregister_donation en vez de registrar inventario).
- No muestres al usuario mensajes de error crudos de la API, códigos de estado ni trazas; resume el problema en lenguaje natural.
- No muestres tokens, claves ni secretos.

Expand Down
17 changes: 17 additions & 0 deletions src/agent/tools.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import test from "node:test";
import assert from "node:assert";
import { agentTools } from "./tools.js";

test("agentTools registra las tools de donación", () => {
const names = new Set(agentTools.map((t: any) => t.name));
// Flujo de donante: público (llevar a un punto) y oferta autenticada.
assert.ok(names.has("rg_preregister_donation"), "falta rg_preregister_donation");
assert.ok(names.has("rg_submit_offer"), "falta rg_submit_offer");
// La tool de inventario sigue existiendo (es de staff, no de donantes).
assert.ok(names.has("rg_record_inventory_entry"));
});

test("rg_record_inventory_entry se documenta como acción de staff, no de donación", () => {
const inv = agentTools.find((t: any) => t.name === "rg_record_inventory_entry") as any;
assert.match(inv.description, /rg_preregister_donation|donar|donaci/i);
});
60 changes: 58 additions & 2 deletions src/agent/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ export const rgUpdateResourceInventory = tool({
export const rgRecordInventoryEntry = tool({
name: "rg_record_inventory_entry",
description:
"Registra una entrada manual de inventario recibida en un punto. Útil para 'hemos recibido 20 cajas de agua'.",
"SOLO para el personal que GESTIONA el punto: registra stock ya recibido en ese punto ('hemos recibido 20 cajas de agua'). Requiere el permiso intake:receive. NO la uses cuando alguien quiere DONAR o LLEVAR material: para eso usa rg_preregister_donation (público) o rg_submit_offer.",
parameters: z.object({
resourceId: z.string().uuid(),
items: z.array(supplyLineSchema).min(1),
Expand All @@ -420,6 +420,60 @@ export const rgRecordInventoryEntry = tool({
},
});

export const rgPreregisterDonation = tool({
name: "rg_preregister_donation",
description:
"PÚBLICO (no requiere login). Pre-registra una donación que una persona quiere LLEVAR a un punto de acopio concreto. Úsala cuando alguien dice 'quiero llevar/donar X'. Necesita targetResourceId: si no lo sabes, ayuda al usuario a elegir un punto (rg_find_nearby_resources / rg_list_public_resources) antes. Devuelve un código de seguimiento de la donación.",
parameters: z.object({
...emergencyRefSchema,
targetResourceId: z
.string()
.uuid()
.describe("ID del punto de acopio destino donde la persona entregará la donación."),
donorName: z.string().min(2).describe("Nombre de quien dona."),
donorPhone: z.string().optional().describe("Teléfono de contacto del donante, si lo da."),
donorEmail: z.string().email().optional().describe("Email del donante, si lo da."),
items: z.array(supplyLineSchema).min(1),
}),
execute: async (input, runContext?: RunContext<AgentContext>) => {
const context = getContext(runContext);
const emergencyId = await resolveEmergencyId(context, input);
const { emergencyId: _eid, emergencySlug: _slug, ...payload } = input;
const result = await context.apiClient.request(
"POST",
`/emergencies/${emergencyId}/donation-intakes`,
payload,
);
return asPrettyJson(result);
},
});

export const rgSubmitOffer = tool({
name: "rg_submit_offer",
description:
"Envía una OFERTA de donación de un donante autenticado a la emergencia (no ligada a un punto concreto). Requiere login. Úsala cuando un usuario autenticado ofrece materiales de forma general. La ubicación debe ser real (dirección + coordenadas reales del donante); nunca inventes coordenadas.",
parameters: z.object({
...emergencyRefSchema,
items: z.array(supplyLineSchema).min(1),
location: locationSchema.describe("Ubicación real desde donde se ofrece la donación."),
targetNeedId: z.string().uuid().optional().describe("Necesidad concreta a la que se dirige la oferta, si aplica."),
notes: z.string().optional(),
author: authorSchema.optional(),
}),
execute: async (input, runContext?: RunContext<AgentContext>) => {
const context = getContext(runContext);
requireAuth(context);
const emergencyId = await resolveEmergencyId(context, input);
const { emergencyId: _eid, emergencySlug: _slug, ...payload } = input;
const result = await context.apiClient.request(
"POST",
`/emergencies/${emergencyId}/offers`,
payload,
);
return asPrettyJson(result);
},
});

export const rgUpdateResourceStatus = tool({
name: "rg_update_resource_status",
description:
Expand Down Expand Up @@ -676,7 +730,7 @@ export const rgGeocode = tool({
export const rgSearchSupplies = tool({
name: "rg_search_supplies",
description:
"Busca en el catálogo central de suministros/productos estandarizados (supplies) de ResponseGrid. Soporta multiidioma (pasa el 'locale' adecuado según el idioma en el que hable el usuario).",
"Busca en el catálogo central de suministros/productos estandarizados (supplies) de ResponseGrid. Soporta multiidioma (pasa el 'locale' adecuado según el idioma en el que hable el usuario). IMPORTANTE: 'q' es un término de AUTOCOMPLETADO (busca por palabra/prefijo, no de forma semántica): funciona con UNA palabra clave concreta (p. ej. 'bebé', 'fórmula', 'gasas', 'agua'), NO con frases completas — 'comida para bebés' puede devolver 0. Si no hay resultados, REINTENTA con un término más simple, singular o sinónimo, o filtra por categorySlug (p. ej. los productos de bebé están en 'hygiene_infantile'). No concluyas que algo no existe tras una sola búsqueda con una frase.",
parameters: z.object({
q: z.string().optional().describe("Texto libre para buscar en el catálogo (por ejemplo: 'agua', 'colchón', 'gasas')."),
categorySlug: z.string().optional().describe("Filtrar por slug de categoría (por ejemplo: 'water', 'shelter', 'medical_supplies', 'medicines')."),
Expand Down Expand Up @@ -744,6 +798,8 @@ export const agentTools = [
rgGetResourceInventory,
rgUpdateResourceInventory,
rgRecordInventoryEntry,
rgPreregisterDonation,
rgSubmitOffer,
rgUpdateResourceStatus,
rgListPublicNeeds,
rgFindNearbyNeeds,
Expand Down
Loading