AI Connect/How to create/Copilotes par département (logistique)
Copilotes par département (logistique)
Modélisez chaque département d'une entreprise de logistique comme son propre utilisateur de l'AI Connect SDK : Finance, Expédition, Entrepôt et Opérations de flotte, chacun avec ses propres connecteurs, identifiants et capacités, sur une seule clé d'application.
Voici ce qu'une plateforme IA interne n'avait jamais le droit d'être jusqu'ici : une entreprise de logistique où la Finance rapproche les factures, l'Expédition répond "où est la charge 4412 ?", l'Entrepôt compte le quai, les Opérations de flotte suivent la maintenance, et chaque équipe reçoit son propre copilote avec ses propres connecteurs et identifiants, sur une seule Application key, adossé à des milliers de connexions IA dès le premier jour. NetSuite, l'ERP, le WMS, une API de télématique, des canaux Slack, Google Sheets : aucune intégration construite par vous, et les identifiants de chaque équipe appartiennent à l'équipe, pas à la personne actuellement connectée.
Chaque plateforme d'intégration que vous avez évaluée s'arrête exactement là. Elles connectent une application à un service. Aucune n'a jamais proposé une équipe, un département, un rôle partagé comme utilisateur de première classe avec ses propres identifiants isolés, parce qu'aucune n'a un modèle d'utilisateurs capable de nommer autre chose qu'un login humain. L'AI Connect SDK le peut, et ce recadrage est ce projet : le département est l'utilisateur. Un external_id par équipe. Les humains ne sont que ceux qui opèrent le copilote ; la frontière d'isolation est le département. La même architecture qui sert une personne avec un Gmail sert tout l'organigramme, sans nouvelle infrastructure, sans nouvelle plateforme, sans nouvelle conversation avec un fournisseur. Vous faites grandir une organisation en ajoutant des ids, pas en achetant du logiciel.
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
...La même boucle que vous avez vue pour un humain s'exécute désormais contre dept-dispatch au lieu de alice_123. Le copilote agit sur le TMS et le Slack propres à l'Expédition, jamais sur le NetSuite de la Finance, car une connexion appartient à celui qui possède l'external_id.
Pourquoi un département est un "utilisateur" parfait
- État partagé et durable. Personne ne "possède" la connexion ;
dept-warehousela possède. Le renouvellement du personnel ne compromet jamais l'intégration. - Isolation du rayon d'effet. Chaque connexion porte son propre jeton de plan de données, si bien que les dépenses et la révocation immédiate de l'Expédition sont indépendantes de celles de l'Entrepôt.
- Privilège minimal par construction. Un copilote ne peut littéralement pas voir un connecteur connecté par un autre département, la requête de capacités est limitée à un seul
external_id. - Une seule clé d'application. Tous les départements vivent sous une seule Application Vinkius. Vous ajoutez une équipe en définissant un nouvel id, pas en provisionnant de l'infrastructure.
1. Nommez les départements
Utilisez un id stable, lisible et sûr pour les URL. Préfixez-les pour qu'ils n'entrent jamais en collision avec un id humain provenant d'une autre partie de votre système.
type Department = 'finance' | 'dispatch' | 'warehouse' | 'fleet';
const departmentUserId = (dept: Department) => `dept-${dept}`;
// "dept-finance", "dept-dispatch", "dept-warehouse", "dept-fleet"2. Déclarez les connecteurs de chaque département
Des équipes différentes ont besoin d'outils différents. Gardez cela comme configuration, le reste du code ne change jamais par équipe.
// server/departments.ts
interface DeptSpec {
label: string;
connectors: string[]; // slugs du catalogue
}
export const DEPARTMENTS: Record<Department, DeptSpec> = {
finance: { label: 'Finance', connectors: ['netsuite', 'stripe', 'gmail'] },
dispatch: { label: 'Expédition', connectors: ['sap', 'slack', 'google-sheets'] },
warehouse: { label: 'Entrepôt', connectors: ['wms', 'google-sheets', 'jira'] },
fleet: { label: 'Opérations de flotte', connectors: ['telematics', 'servicemax', 'slack'] },
};Les slugs de connecteurs proviennent du catalogue en direct. Laissez votre interface d'administration les découvrir au lieu de les coder en dur : await vinkius.catalog.search('telematics') ou for await (const c of vinkius.catalog.iterate()). Voir Connecteurs et identifiants.
3. Provisionnez un département une seule fois (une action d'administration)
Quand une équipe est intégrée, connectez ses comptes et stockez les identifiants. L'id du département est l'externalId partout, aucun humain n'intervient dans ce parcours.
import { vinkius } from './vinkius';
import { DEPARTMENTS, type Department } from './departments';
async function bootstrapDepartment(dept: Department) {
const user = vinkius.user(`dept-${dept}`);
// attachez des métadonnées non secrètes pour pouvoir filtrer/auditer plus tard
await user.ensure({ kind: 'department', label: DEPARTMENTS[dept].label });
for (const slug of DEPARTMENTS[dept].connectors) {
const connector = user.connector(slug);
await connector.connect();
// connecteurs api_key : connector.credentials.set({ API_KEY: ... })
// connecteurs oauth : connect() revient après le consentement du fournisseur
}
// signalez la préparation par connecteur pour que les administrateurs voient ce qui manque encore d'identifiants
return Promise.all(
DEPARTMENTS[dept].connectors.map(async (slug) => ({
slug,
status: await user.connector(slug).status(),
})),
);
}4. Chargez les capacités du département au moment de la requête
Une requête arrive avec le département que le copilote sert. Résolvez ses capacités, délimitées et prêtes à être remises au modèle.
async function departmentCapabilities(dept: Department) {
const spec = DEPARTMENTS[dept];
return vinkius.user(`dept-${dept}`).capabilities({
include: spec.connectors,
onConnectorError: (slug, error) => {
// remontez-le aux administrateurs ; n'interrompez pas la réponse
console.warn(`${dept}/${slug}`, (error as Error).message);
},
});
}5. Répondez en tant que département
Routez le message vers le bon copilote, donnez au modèle uniquement les outils de ce département et exécutez sur la connexion de ce département.
// 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: "Vous êtes le copilote de la Finance. Rapprochez les factures et répondez aux questions de facturation à l'aide des outils comptables connectés.",
dispatch: "Vous êtes le copilote de l'Expédition. Indiquez où sont les chargements et mettez à jour les ETA à l'aide du TMS et de Slack.",
warehouse: "Vous êtes le copilote de l'Entrepôt. Signalez les comptages du quai et les tâches ouvertes à l'aide du WMS et des Sheets.",
fleet: "Vous êtes le copilote des Opérations de flotte. Signalez l'état des véhicules et la maintenance à l'aide de la télématique et de 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); // s'exécute en tant que ce département
return { answer: result.content, tool: call.function.name };
}// L'Expédition interroge son propre TMS ; elle ne peut pas atteindre le NetSuite de la Finance.
await ask('dispatch', "Quel est l'ETA de la charge 4412 et quel chauffeur la conduit ?");
await ask('finance', "Quelles factures clients de plus de 30 jours sont encore ouvertes ce mois-ci ?");6. Laissez des humains agir en tant que département, avec une piste d'audit
Les personnes qui opèrent un copilote ne sont pas l'"utilisateur" au sens du SDK, mais vous devez quand même savoir qui a demandé. Journalisez l'opérateur aux côtés de l'id du département, la frontière de capacités reste celle du département.
async function askAs(dept: Department, operator: string, question: string) {
const answer = await ask(dept, question);
// votre propre magasin d'audit — le SDK ne voit jamais l'opérateur
await audit.record({
actor: operator,
on_behalf_of: `dept-${dept}`,
question,
});
return answer;
}Conservez l'identité de l'opérateur entièrement dans votre système. Pour Vinkius, l'acteur est toujours dept-finance. C'est ce qui vous donne une isolation au niveau du département et fait que le même copilote se comporte de façon identique, peu importe quel employé est en train d'écrire.
7. Accordez et révoquez une équipe en une seule opération
Comme un département est un seul external_id, sa mise hors service est triviale. Déconnecter un connecteur ne supprime que l'accès de cette équipe.
async function retireConnector(dept: Department, slug: string) {
await vinkius.user(`dept-${dept}`).connector(slug).disconnect();
}Fonctionnalités avancées à connaître ici
Un département, un connecteur, sans tout re-lister
Un copilote ciblé qui n'accède qu'à un seul système ne devrait pas encourir le coût d'un fan-out sur toutes les connexions de l'équipe. forConnector découpe un ensemble déjà chargé ; et pour ne même pas récupérer les autres, listez à partir d'un seul handle de connecteur :
// depuis un ensemble agrégé :
const sheetsOnly = capabilities.forConnector('google-sheets');
// ou en évitant de lister toutes les connexions :
const tmsOnly = await vinkius.user(`dept-${dept}`).connector('sap').capabilities();Le fan-out est concurrent et auto-réparant
user.capabilities() liste les résumés des connexions une seule fois, puis fait un fan-out vers les connecteurs ready avec un plafond de concurrence de 8 et réutilise chaque id de connexion résolu (pas de re-listing par connecteur). Le dépassement de délai du runtime d'un connecteur n'interrompt pas la réponse, les autres continuent de se résoudre, et onConnectorError vous indique quel outil de l'équipe était indisponible afin que vous puissiez avertir l'administrateur de ce département.
Relisez les métadonnées propres au département
Le kind: 'department' non secret que vous avez attaché avec ensure() est disponible pour l'affichage. user.get() renvoie les métadonnées et le statut stockés, de quoi construire un tableau de bord "quelles équipes sont intégrées ?" sans base de données supplémentaire.
const profile = await vinkius.user('dept-finance').get();
console.log(profile.metadata); // { kind: 'department', label: 'Finance' }
console.log(profile.status); // active | ...Liste de contrôle pour la production
- [ ] Préfixez les ids de département pour qu'ils ne puissent jamais entrer en collision avec des ids humains (
dept-…). - [ ] Stockez la liste de connecteurs par département comme configuration, et non comme embranchements de code.
- [ ] Intégrez les équipes avec
user.ensure({ kind: 'department' })pour que les tableaux de bord pilotés par les métadonnées fonctionnent. - [ ] Affichez
connector.status()dans la console d'administration afin que les manques (needs_credentials) soient visibles. - [ ] Routez les opérateurs humains vers le copilote de leur département ; gardez la journalisation de qui-a-demandé de votre côté.
- [ ] Donnez à toute action mutante du copilote un
idempotencyKeystable (p. ex. ticket + département).
Vous exploitez désormais une plateforme IA interne où la Finance, l'Expédition, l'Entrepôt et les Opérations de flotte ont chacun leurs propres outils connectés et leur propre copilote, le tout à partir d'une seule clé d'application et d'un seul external_id par équipe. Vous n'avez pas acheté une plateforme pour les départements ; la plateforme n'a tout simplement pas de plafond pour ce qu'un utilisateur peut être. Ajouter un cinquième équipe demain est une ligne de configuration. C'est ce que signifie posséder la couche de connectivité : l'organigramme devient votre table d'utilisateurs.
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.
