AI Connect/How to create/Copilotos por departamento (logística)

Copilotos por departamento (logística)

Pregunta a la IA sobre Vinkius

Modela cada departamento de una empresa de logística como su propio usuario del AI Connect SDK: Finanzas, Expedición, Almacén y Operaciones de Flota, cada uno con sus propios conectores, credenciales y capacidades, sobre una sola clave de aplicación.

Esto es lo que una plataforma interna de IA nunca pudo ser hasta ahora: una empresa de logística donde Finanzas concilia facturas, Expedición responde "¿dónde está la carga 4412?", Almacén cuenta el muelle, Operaciones de Flota sigue el mantenimiento, y cada equipo recibe su propio copilot con sus propios conectores y credenciales, sobre una sola Application key, respaldado por miles de conexiones de IA desde el primer día. NetSuite, el ERP, el WMS, una API de telemática, canales de Slack, Google Sheets: ninguna integración construida por usted, y las credenciales de cada equipo pertenecen al equipo, no a quien esté conectado en ese momento.

Toda plataforma de integración que haya evaluado se detiene exactamente ahí. Conectan una aplicación con un servicio. Ninguna ha ofrecido jamás un equipo, un departamento, un rol compartido como usuario de primera clase con sus propias credenciales aisladas, porque ninguna tiene un modelo de usuarios capaz de nombrar algo más que un inicio de sesión humano. El AI Connect SDK sí puede, y ese replanteamiento es esta construcción: el departamento es el usuario. Un external_id por equipo. Los humanos son solo quienes operan el copilot; la frontera de aislamiento es el departamento. La misma arquitectura que atiende a una persona con un Gmail atiende el organigrama completo, sin nueva infraestructura, sin nueva plataforma, sin nuevas conversaciones con proveedores. Usted escala una organización agregando ids, no comprando software.

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.

El mismo bucle que vio para un humano ahora se ejecuta contra dept-dispatch en lugar de alice_123. El copilot actúa sobre el TMS y el Slack propios de Expedición, nunca sobre el NetSuite de Finanzas, porque una conexión pertenece a quien es dueño del external_id.

One Application keyvk_app_*dept-financeown credentialsdept-dispatchown credentialsdept-warehouseown credentialsdept-fleetown credentialsisolatedper external_id
The org chart as user table: each department owns its connections, credentials and spend, isolated by external_id under one Application key.

Por qué un departamento es un "usuario" perfecto

  • Estado compartido y duradero. Nadie "posee" la conexión; dept-warehouse la posee. La rotación de personal nunca rompe la integración.
  • Aislamiento del radio de impacto. Cada conexión lleva su propio token de plano de datos, así que el gasto y la revocación inmediata de Expedición son independientes de los de Almacén.
  • Privilegio mínimo por construcción. Un copilot literalmente no puede ver un conector que otro departamento conectó, la consulta de capacidades está limitada a un único external_id.
  • Una sola clave de aplicación. Todos los departamentos viven bajo una única Application de Vinkius. Usted añade un equipo definiendo un nuevo id, no aprovisionando infraestructura.

1. Nombre los departamentos

Use un id estable, legible y seguro para URL. Añada un prefijo para que nunca colisionen con un id humano de otra parte de su sistema.

typescript
type Department = 'finance' | 'dispatch' | 'warehouse' | 'fleet';

const departmentUserId = (dept: Department) => `dept-${dept}`;
// "dept-finance", "dept-dispatch", "dept-warehouse", "dept-fleet"

2. Declare los conectores de cada departamento

Equipos diferentes necesitan herramientas diferentes. Mantenga eso como configuración, el resto del código nunca cambia por equipo.

typescript
// server/departments.ts
interface DeptSpec {
  label: string;
  connectors: string[]; // slugs del catálogo
}

export const DEPARTMENTS: Record<Department, DeptSpec> = {
  finance:   { label: 'Finanzas',             connectors: ['netsuite', 'stripe', 'gmail'] },
  dispatch:  { label: 'Expedición',           connectors: ['sap', 'slack', 'google-sheets'] },
  warehouse: { label: 'Almacén',              connectors: ['wms', 'google-sheets', 'jira'] },
  fleet:     { label: 'Operaciones de Flota', connectors: ['telematics', 'servicemax', 'slack'] },
};

Los slugs de conectores provienen del catálogo en vivo. Deje que su interfaz de administración los descubra en lugar de codificarlos rígidamente: await vinkius.catalog.search('telematics') o for await (const c of vinkius.catalog.iterate()). Consulte Conectores y credenciales.

3. Provisione un departamento una vez (una acción de administrador)

Cuando un equipo se incorpora, conecte sus cuentas y almacene las credenciales. El id del departamento es el externalId en todas partes, no hay ningún humano en este flujo.

typescript
import { vinkius } from './vinkius';
import { DEPARTMENTS, type Department } from './departments';

async function bootstrapDepartment(dept: Department) {
  const user = vinkius.user(`dept-${dept}`);

  // adjunte metadatos no secretos para poder filtrar/auditar después
  await user.ensure({ kind: 'department', label: DEPARTMENTS[dept].label });

  for (const slug of DEPARTMENTS[dept].connectors) {
    const connector = user.connector(slug);
    await connector.connect();
    // conectores api_key: connector.credentials.set({ API_KEY: ... })
    // conectores oauth: connect() retorna tras el consentimiento del proveedor
  }

  // reporte la preparación por conector para que los administradores vean qué aún necesita credenciales
  return Promise.all(
    DEPARTMENTS[dept].connectors.map(async (slug) => ({
      slug,
      status: await user.connector(slug).status(),
    })),
  );
}

4. Cargue las capacidades del departamento en el momento de la petición

Una petición llega con el departamento al que sirve el copilot. Resuelva sus capacidades, con alcance definido y listas para entregar al modelo.

typescript
async function departmentCapabilities(dept: Department) {
  const spec = DEPARTMENTS[dept];
  return vinkius.user(`dept-${dept}`).capabilities({
    include: spec.connectors,
    onConnectorError: (slug, error) => {
      // muéstrelo a los administradores; no interrumpa la respuesta
      console.warn(`${dept}/${slug}`, (error as Error).message);
    },
  });
}

5. Responda como el departamento

Enrute el mensaje al copilot correcto, dé al modelo solo las herramientas de ese departamento y ejecute sobre la conexión de ese departamento.

typescript
// server/copilot.ts
import OpenAI from 'openai';
import { toOpenAITools, runOpenAIToolCall } from '@vinkius/connect/openai';
import { type Department } from './departments';
import { departmentCapabilities } from './capabilities';

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

const SYSTEM_PROMPT: Record<Department, string> = {
  finance: 'Eres el copilot de Finanzas. Concilia facturas y responde preguntas de facturación usando las herramientas contables conectadas.',
  dispatch: 'Eres el copilot de Expedición. Responde dónde están las cargas y actualiza los ETA usando el TMS y Slack.',
  warehouse: 'Eres el copilot de Almacén. Informa los conteos del muelle y las tareas abiertas usando el WMS y Sheets.',
  fleet: 'Eres el copilot de Operaciones de Flota. Informa la salud de los vehículos y el mantenimiento usando telemática y ServiceMax.',
};

export async function ask(dept: Department, question: string) {
  const capabilities = await departmentCapabilities(dept);

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

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

  const result = await runOpenAIToolCall(capabilities, call); // se ejecuta como este departamento
  return { answer: result.content, tool: call.function.name };
}
typescript
// Expedición pregunta por su propio TMS; no puede alcanzar el NetSuite de Finanzas.
await ask('dispatch', '¿Cuál es el ETA de la carga 4412 y qué conductor la lleva?');
await ask('finance', '¿Qué facturas de clientes con más de 30 días siguen abiertas este mes?');

6. Permita que los humanos actúen como un departamento, con rastro de auditoría

Las personas que operan un copilot no son el "usuario" en el sentido del SDK, pero aun así debe saber quién preguntó. Registre al operador junto con el id del departamento, la frontera de capacidades permanece en el departamento.

typescript
async function askAs(dept: Department, operator: string, question: string) {
  const answer = await ask(dept, question);
  // su propio almacén de auditoría — el SDK nunca ve al operador
  await audit.record({
    actor: operator,
    on_behalf_of: `dept-${dept}`,
    question,
  });
  return answer;
}

Mantenga la identidad del operador por completo en su sistema. Para Vinkius, el actor es siempre dept-finance. Eso es lo que le da aislamiento a nivel de departamento y hace que el mismo copilot se comporte de forma idéntica sin importar qué empleado esté escribiendo.

7. Otorgue y revoque un equipo en un solo paso

Como un departamento es un único external_id, su retirada es trivial. Desconectar un conector elimina solo el acceso de ese equipo.

typescript
async function retireConnector(dept: Department, slug: string) {
  await vinkius.user(`dept-${dept}`).connector(slug).disconnect();
}

Funciones avanzadas que conviene conocer aquí

Un departamento, un conector, sin volver a listar todo

Un copilot enfocado que solo accede a un sistema no debería incurrir en el costo de un fan-out sobre las conexiones de todo el equipo. forConnector recorta un conjunto ya cargado; y para omitir por completo la obtención de los demás, liste desde un único handle de conector:

typescript
// desde un conjunto agregado:
const sheetsOnly = capabilities.forConnector('google-sheets');

// o evitando listar cada conexión en absoluto:
const tmsOnly = await vinkius.user(`dept-${dept}`).connector('sap').capabilities();

El fan-out es concurrente y auto-reparable

user.capabilities() lista los resúmenes de conexiones una sola vez, luego hace fan-out hacia los conectores ready con un límite de concurrencia de 8 y reutiliza cada id de conexión resuelto (sin volver a listar por conector). Que el runtime de un conector agote el tiempo de espera no interrumpe la respuesta, los demás siguen resolviéndose, y onConnectorError le indica qué herramienta del equipo no estaba disponible, para que pueda avisar al administrador de ese departamento.

Lea los metadatos propios del departamento

El kind: 'department' no secreto que adjuntó con ensure() está disponible para su renderizado. user.get() devuelve los metadatos y el estado almacenados, suficiente para construir un panel "¿qué equipos están incorporados?" sin ninguna base de datos adicional.

typescript
const profile = await vinkius.user('dept-finance').get();
console.log(profile.metadata); // { kind: 'department', label: 'Finanzas' }
console.log(profile.status);   // active | ...

Lista de verificación de producción

  • [ ] Prefije los ids de departamento para que nunca colisionen con ids humanos (dept-…).
  • [ ] Guarde la lista de conectores por departamento como configuración, no como ramificaciones de código.
  • [ ] Incorpore con user.ensure({ kind: 'department' }) para que los paneles basados en metadatos funcionen.
  • [ ] Renderice connector.status() en la consola de administración para que las brechas (needs_credentials) sean visibles.
  • [ ] Enrute a los operadores humanos al copilot de su departamento; mantenga el registro de quién-preguntó de su lado.
  • [ ] Dé a cada acción mutable del copilot un idempotencyKey estable (p. ej. ticket + departamento).

Ahora opera una plataforma interna de IA en la que Finanzas, Expedición, Almacén y Operaciones de Flota tienen cada uno sus propias herramientas conectadas y su propio copilot, todo a partir de una sola clave de aplicación y un external_id por equipo. Usted no compró una plataforma para departamentos; la plataforma simplemente no tiene techo para lo que un usuario puede ser. Agregar un quinto equipo mañana es una línea de configuración. Eso es lo que significa poseer la capa de conectividad: el organigrama se convierte en su tabla de usuarios.

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.

Least privilege per team

A copilot literally cannot see another department’s connectors. Onboarding a team is a new external_id, not new infrastructure.

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 department copilots inherit, plus the SKILL.md, in your language, for your coding agent.

Próximos pasos