AI Connect/How to create/Chatbot de consumo multiusuario

Chatbot de consumo multiusuario

Pregunta a la IA sobre Vinkius

Cree una única ruta de chatbot que atienda a miles de personas, cada una con su propio GitHub, Slack y Gmail conectados y aislados, desde una sola Application key, y conéctela a OpenAI con el AI Connect SDK. Ninguna plataforma ha entregado nunca esto: cada usuario trae sus propias cuentas y usted jamás almacena un token.

Esta es la construcción que casi todo equipo intenta primero, y la que la industria nunca logró abaratar: una única ruta de chatbot donde miles de personas conectan su propio GitHub, Slack y Gmail, todas aisladas, todas desde una sola Application key. Cada usuario de tu producto entra con el catálogo completo de Vinkius detrás: miles de conexiones de IA desde el primer día, cero integraciones construidas por ti, cero tokens almacenados por ti, cero identidad expuesta a nadie. Ese es el futuro que esta página te entrega, y cabe en unas ochenta líneas de backend.

La alternativa, la que tus competidores siguen viviendo, es la respuesta estándar de la industria: un ejército de flujos de OAuth, un almacén de tokens cifrado con aislamiento de claves por usuario, un planificador de refrescos con locks distribuidos y un cuestionario de seguridad que usted falla frente a los contratos enterprise. Los análisis de ese camino casero lo tasan en entre 200.000 y 250.000 dólares a lo largo de tres años, y más de 640 horas de ingeniería antes de que la primera llamada a una tool funcione. Su asistente crea la issue en el repositorio del usuario, resume su Slack sin leer, agenda en su calendario. El modelo nunca fue la parte difícil. La conectividad lo era, y en el AI Connect SDK ya está resuelta.

Aquí el «usuario» se toma en el sentido literal: una persona con una cuenta en su producto. Su external_id es simplemente lo que su inicio de sesión ya le entrega.

Agent loop · one turn
One user turn of the quickstart, exactly as the console serves it. Click a step or press Run.
vinkius.user('alice_123').capabilities({ include: ['github'] })
HTTPGET /apps/vk_app_xxx/users/alice_123/tools?connector=github
const capabilities = await vinkius
  .user('alice_123')
  .capabilities({ include: ['github'] });
CapabilitySet (6)
  github__list_issues        read-only
  github__create_issue       POST /repos/{owner}/{repo}/issues
  github__list_pull_requests read-only
  github__search_code        read-only
  ...
Step 1 of 6
The agent loop, step by step. Press Run and follow one user turn: capabilities load, convert to tools, the model calls github__create_issue, the SDK executes on that user connection and the result feeds back. Every step shows the real SDK call and its HTTP request.

Pulse Run arriba para ver un turno de usuario de extremo a extremo: las capacidades se cargan para el usuario autenticado, se convierten en tools, el modelo elige github__create_issue, el SDK se ejecuta sobre la conexión propia de ese usuario. Cambie el id de usuario y cada panel cambia, ese aislamiento es todo el producto.

The catalogthousands from day onePOST /chatone route, one keyAny modelOpenAI · Anthropicalice_123own connectionsbob_42own connectionscarol_7own connections+ thousandsof userstools
One route, every user isolated: each person connects their own accounts from the day-one catalog, and the model sees only that user's tools.

Qué obtiene al final

Un único endpoint POST /chat. Dados userId y un mensaje, este:

  • devuelve solo las capacidades que ese usuario ha conectado,
  • las entrega a OpenAI como tools,
  • ejecuta la tool que el modelo elija,
  • y hace todo eso en su servidor, para que ninguna credencial salga de él.

1. Un cliente, para toda la aplicación

Crea exactamente una instancia de Vinkius. Ella guarda la configuración de la aplicación, no un usuario actual. Impórtela desde un único módulo.

typescript
// server/vinkius.ts
import { Vinkius } from '@vinkius/connect';

export const vinkius = new Vinkius({
  appId: process.env.VINKIUS_APP_ID!,     // vk_app_…
  apiKey: process.env.VINKIUS_APP_KEY!,   // vk_app_sk_…  (solo en el servidor)
  timeoutMs: 20_000,
});

La construcción no realiza ninguna petición; solo valida los prefijos de las credenciales. Reutilizar una única instancia en todas las peticiones es el patrón previsto.

2. Autentique y luego identifique al actor

Nunca confíe en un userId que venga del cuerpo de la petición. Resuélvalo a partir de su sesión y páselo al SDK. El SDK nunca necesita un correo ni un nombre, el id es opaco y Vinkius no sabe nada sobre quiénes son sus clientes.

typescript
import { vinkius } from './vinkius';

// su autenticación devuelve el id estable con el que creó los usuarios
function requireUser(req: Request): string {
  const userId = req.headers.get('x-user-id');
  if (!userId) throw new Error('not authenticated');
  return userId; // p. ej. "alice_123"
}
typescript
const user = vinkius.user(requireUser(req)); // perezoso: cero llamadas de red

3. Conecte una cuenta cuando el usuario haga clic en «Conectar GitHub»

Proporcione a su producto una ruta ligera de aprovisionamiento. connect() es idempotente (get-or-create), y las credenciales se guardan solo para escritura: la respuesta informa qué campos están configurados y nunca devuelve valores.

typescript
// POST /connect/github  { token }
async function connectGithub(userId: string, githubToken: string) {
  const github = vinkius.user(userId).connector('github');

  await github.connect();                           // aprovisiona la conexión
  const schema = await github.credentials.schema(); // lo que este conector necesita
  await github.credentials.set({ GITHUB_TOKEN: githubToken });

  return { status: await github.status(), requires: Object.keys(schema) };
}

credentials.schema() lee el catálogo y no requiere una conexión, así que puede renderizar los campos correctos del formulario antes de que el usuario se conecte. Para conectores OAuth no hay nada que definir: connect() regresa tras el consentimiento del proveedor y el estado pasa a ready.

4. Cargue solo las capacidades de este usuario

Una sola llamada agrega todos los conectores listos del actor. Se distribuye de forma concurrente y es tolerante a fallos: un conector inestable degrada, no hace fallar el turno.

typescript
const capabilities = await user.capabilities({
  include: ['github', 'slack', 'gmail'],   // limita a lo que este producto usa
  onConnectorError: (slug, error) => {
    console.warn('connector skipped', slug, (error as Error).message);
  },
});

if (capabilities.length === 0) {
  // nada conectado aún — pida al usuario que conecte una cuenta
}

Dos usuarios pueden conectar la misma integración de GitHub y obtener conexiones, credenciales y capacidades completamente separadas. Nada cruza la frontera, y usted no implementó ninguna parte de esa lógica de aislamiento.

5. Entregue las capacidades al modelo

Aquí es donde se materializa la promesa de «funciona con cualquier modelo». El adaptador /openai convierte un conjunto de capacidades en el array tools que OpenAI espera y despacha la llamada a tool devuelta de nuevo sobre la conexión con alcance de usuario.

typescript
// server/chat.ts
import OpenAI from 'openai';
import { vinkius } from './vinkius';
import { toOpenAITools, runOpenAIToolCall } from '@vinkius/connect/openai';

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });

export async function handleChat(userId: string, message: string) {
  const capabilities = await vinkius.user(userId).capabilities({
    include: ['github', 'slack', 'gmail'],
  });

  const completion = await openai.chat.completions.create({
    model: '[MODEL_ID]',
    messages: [{ role: 'user', content: message }],
    tools: toOpenAITools(capabilities),
    tool_choice: 'auto',
  });

  const call = completion.choices[0]?.message.tool_calls?.[0];
  if (!call) {
    return { text: completion.choices[0]?.message.content ?? '' };
  }

  // se ejecuta en la conexión DE ESTE usuario; los errores vuelven como datos
  const result = await runOpenAIToolCall(capabilities, call);
  return { tool: call.function.name, result };
}

Para Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Cloudflare Workers AI o un puente neutro de JSON-Schema para cualquier otro runtime, consulte Adaptadores de frameworks. La línea de conversión es lo único que cambia.

6. Haga iterar al agente hasta que termine

Una conversación real invoca varias tools. Devuelva los resultados y deje que el modelo concluya:

typescript
export async function runTurn(userId: string, messages: object[]) {
  const capabilities = await vinkius.user(userId).capabilities();
  const tools = toOpenAITools(capabilities);

  for (let step = 0; step < 6; step++) {
    const completion = await openai.chat.completions.create({
      model: '[MODEL_ID]',
      messages: messages as never,
      tools,
    });
    const msg = completion.choices[0].message;
    messages.push(msg as object);

    if (!msg.tool_calls?.length) return msg.content;

    for (const call of msg.tool_calls) {
      const result = await runOpenAIToolCall(capabilities, call);
      messages.push({
        role: 'tool',
        tool_call_id: call.id,
        content: JSON.stringify(result.content),
      });
    }
  }
  return 'Stopped after too many steps.';
}

isError: true en un resultado es un desenlace del conector (la acción falló), no un fallo del sistema. Pasar el contenido fallido de vuelta al modelo es exactamente lo que le permite recuperarse, reintentar, elegir otra tool o avisar al usuario. Reserve su try/catch para las subclases de VinkiusError que se lanzan: autenticación, cuota y transporte. Consulte Gestión de errores.

7. Recupérese de «aún no conectado»

Cuando un usuario no ha conectado una cuenta, findCapability no devuelve nada o la ejecución lanza ConnectorNotConnectedError. Convierta eso en un momento de producto, no en un 500:

typescript
import { ConnectorNotConnectedError } from '@vinkius/connect';

try {
  const result = await runOpenAIToolCall(capabilities, call);
} catch (error) {
  if (error instanceof ConnectorNotConnectedError) {
    return { needsConnection: error.message };
    // UI: «Conecte GitHub para hacer eso» → su ruta /connect/github
  }
  throw error;
}

Patrones avanzados que convierten el prototipo en producto

Lo básico funciona. Cuatro capacidades que el SDK ya incluye separan un prototipo inicial de un sistema en producción.

Haga que toda escritura sea idempotente, acotada y cancelable

Los helpers de despacho (runOpenAIToolCall) ejecutan la tool que el modelo eligió, pero no adjuntan una clave de idempotencia, un timeout por llamada ni una señal de cancelación. Para cualquier cosa que modifique el mundo, resuelva la capacidad usted mismo y pase esos controles directamente a execute():

typescript
const capability = capabilities.findCapability(call.function.name);

const result = await capability?.execute(
  JSON.parse(call.function.arguments || '{}'),
  {
    idempotencyKey: `chat:${messageId}`, // un turno reintentado nunca crea una issue duplicada
    timeoutMs: 15_000,                    // una tool lenta obtiene su propio plazo
    signal,                   // el usuario cerró la pestaña: cancele la ejecución y evite incurrir en costes
  },
);

Declarar idempotencyKey es precisamente lo que vuelve seguro reintentar un POST no idempotente: activa los reintentos de transporte del SDK para esa llamada y el servidor deduplica las repeticiones. Sin ella, los 429/502/503/504 transitorios nunca se reintentan en una escritura.

Trate runtime_url como un secreto

connect() devuelve un Connection cuyo runtime_url incrusta el token de data-plane vk_live_* de este usuario. Se entrega una única vez y autentiza cada llamada, nunca lo registre en logs, nunca lo persista en el navegador ni lo incruste en un prompt del modelo. Para tener visibilidad no lo necesita: registre hooks, y el SDK limpia Authorization, los campos con forma de credencial y la ruta vk_live_* antes de que su callback se ejecute.

typescript
const vinkius = new Vinkius({
  appId: process.env.VINKIUS_APP_ID!,
  apiKey: process.env.VINKIUS_APP_KEY!,
  hooks: {
    onRequest: ({ method, attempt }) => metrics.count(method, attempt),
    onResponse: ({ status, requestId }) => trace.record(status, requestId),
  },
});

Mantenga nombres de tool válidos para su modelo

toOpenAITools lanza un ConfigError por adelantado cuando un nombre con espacio de nombres como github__create_issue incumple la regla de 64 caracteres [A-Za-z0-9_-] de OpenAI, en lugar de un 400 críptico del proveedor a mitad de la conversación. Reduzca el espacio de nombres en la construcción si sus conectores son verbosos:

typescript
new Vinkius({
  appId,
  apiKey,
  namespaceCapability: (connector, name) =>
    `${connector}_${name}`.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 64),
});

Lea la salida tipada cuando exista

Algunas capacidades devuelven datos estructurados junto con su texto. result.structuredContent los presenta literalmente (el SDK nunca los analiza), de modo que una tool «resuma mi Slack sin leer» puede entregar a su UI un objeto limpio en lugar de una cadena que usted deba volver a analizar:

typescript
const result = await capability!.execute(args);
const data = result.structuredContent; // objeto tipado cuando el conector lo proporciona

Lista de verificación de producción

  • [ ] El SDK solo se ejecuta en su servidor; su navegador/móvil llama a sus rutas, nunca a Vinkius.
  • [ ] external_id proviene de su sesión autenticada, nunca de entrada del cliente.
  • [ ] Derive ids estables (una clave de BD), y manténgalos por debajo de 255 caracteres sin /, \ ni espacios.
  • [ ] Pase include a capabilities() para que el modelo solo vea las tools que este producto debe usar.
  • [ ] Dé a cada llamada mutante un idempotencyKey estable, para que los reintentos no generen ejecuciones duplicadas.
  • [ ] Adjunte metadatos no secretos con vinkius.user(id).ensure({ plan }) si segmenta por plan.

Ya tiene una única ruta de chatbot que atiende a todos los usuarios, cada uno con sus conectores y capacidades aislados, conectada al modelo que elija. La capa de integración de seis cifras que sus competidores siguen construyendo a mano, usted la cambió por una clave, un SDK y una tarde. Usted compite en su producto. La infraestructura está lista.

What you just got

Not a pitch: the properties this build inherits automatically.

Isolation by construction

Connections and capabilities resolve only inside one external_id. No cross-actor leakage is possible, and you wrote none of that enforcement.

Write-only credentials

Your server stores secrets and can read back which fields are configured, never the values. Not your code, the model, or a dashboard can exfiltrate them.

Metered, revocable spend

Every connection owns a vk_live_* token, so cost and revocation are per connection. One call to disconnect() is a complete, auditable stop.

Any model runtime

One CapabilitySet converts to OpenAI, Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Workers AI or neutral JSON Schema. Only the last line changes.

Production safety built in

idempotencyKey, timeoutMs and AbortSignal per call; automatic full-jitter retries on transient failures; typed VinkiusError branches. No bespoke harness.

One route, every customer

HandleChat(userId, ...) serves your whole base. Adding a user is one external_id, never a new integration.

Give it to your AI agent

An Agent Skill (SKILL.md) for this build. Preview the first lines below, then copy or download it into your repo under .claude/skills/: Claude Code, Cursor or any Agent-Skills-compatible agent follows it to implement this pattern correctly.

Download SKILL.md6 · Available in your language
What this chatbot build inherits, plus the SKILL.md, in your language, for your coding agent.

Próximos pasos