AI Connect/How to create/Copilotos por departamento (logística)
Copilotos por departamento (logística)
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.
vinkius.user('alice_123').capabilities({ include: ['github'] })GET /apps/vk_app_xxx/users/alice_123/tools?connector=githubconst 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
...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.
Por qué un departamento es un "usuario" perfecto
- Estado compartido y duradero. Nadie "posee" la conexión;
dept-warehousela 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.
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.
// 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.
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.
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.
// 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 };
}// 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.
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.
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:
// 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.
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
idempotencyKeyestable (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.
Connections and capabilities resolve only inside one external_id. No cross-actor leakage is possible, and you wrote none of that enforcement.
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.
Every connection owns a vk_live_* token, so cost and revocation are per connection. One call to disconnect() is a complete, auditable stop.
One CapabilitySet converts to OpenAI, Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Workers AI or neutral JSON Schema. Only the last line changes.
idempotencyKey, timeoutMs and AbortSignal per call; automatic full-jitter retries on transient failures; typed VinkiusError branches. No bespoke harness.
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.
