AI Connect/Como criar/Copilots por departamento (logística)
Copilots por departamento (logística)
Modele cada departamento de uma transportadora como um usuário próprio do AI Connect SDK: Financeiro, Expedição, Armazém e Operação de Frota, cada um com seus próprios conectores, credenciais e capacidades, em uma única chave de aplicação.
Eis o que uma plataforma interna de IA nunca pôde ser até agora: uma transportadora em que o Financeiro reconcilia faturas, a Expedição responde "onde está a carga 4412?", o Armazém confere o cais, a Operação de Frota acompanha a manutenção, e cada time ganha o próprio copilot com conectores e credenciais próprios, em uma única Application Key, com milhares de conexões de IA desde o primeiro dia. NetSuite, o ERP, o WMS, uma API de telemetria, canais de Slack, Google Sheets: nenhuma integração construída por você, e as credenciais de cada time pertencem ao time, não a quem estiver logado.
Toda plataforma de integração que você já avaliou para exatamente aí. Elas conectam uma aplicação a um serviço. Nenhuma delas jamais ofereceu um time, um departamento, um papel compartilhado como usuário de primeira classe com credenciais isoladas próprias, porque nenhuma delas tem um modelo de usuários capaz de nomear algo além de um login humano. O AI Connect SDK tem, e esse reposicionamento é este projeto: o departamento é o usuário. Um external_id por time. Os humanos são apenas quem opera o copilot; a fronteira de isolamento é o departamento. A mesma arquitetura que atende uma pessoa com um Gmail atende o organograma inteiro, sem infraestrutura nova, sem plataforma nova, sem conversa nova com fornecedor. Você escala uma organização adicionando ids, não 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
...O mesmo loop que você viu para um humano agora roda contra dept-dispatch em vez de alice_123. O copilot age no TMS e no Slack da Expedição, nunca no NetSuite do Financeiro, porque uma conexão pertence a quem é dono do external_id.
Por que um departamento é um "usuário" perfeito
- Estado compartilhado e durável. Ninguém "possui" a conexão; quem possui é
dept-warehouse. Rotação de equipe nunca quebra a integração. - Isolamento de raio de impacto. Cada conexão carrega seu próprio token de dados, então o gasto e a revogação imediata da Expedição são independentes dos do Armazém.
- Menor privilégio por construção. Um copilot literalmente não consegue ver um conector que outro departamento conectou, a consulta de capacidades tem escopo em um único
external_id. - Uma única chave de aplicação. Todos os departamentos vivem sob uma mesma Application Vinkius. Você adiciona um time definindo um novo id, não provisionando infraestrutura.
1. Dê nome aos departamentos
Use um id estável, legível e seguro para URL. Coloque prefixo para que nunca colida com um id humano de outra parte do seu sistema.
type Department = 'finance' | 'dispatch' | 'warehouse' | 'fleet';
const departmentUserId = (dept: Department) => `dept-${dept}`;
// "dept-finance", "dept-dispatch", "dept-warehouse", "dept-fleet"2. Declare os conectores de cada departamento
Times diferentes precisam de ferramentas diferentes. Mantenha isso como configuração, o resto do código nunca muda por time.
// server/departments.ts
interface DeptSpec {
label: string;
connectors: string[]; // slugs do catálogo
}
export const DEPARTMENTS: Record<Department, DeptSpec> = {
finance: { label: 'Financeiro', connectors: ['netsuite', 'stripe', 'gmail'] },
dispatch: { label: 'Expedição', connectors: ['sap', 'slack', 'google-sheets'] },
warehouse: { label: 'Armazém', connectors: ['wms', 'google-sheets', 'jira'] },
fleet: { label: 'Operação de Frota', connectors: ['telematics', 'servicemax', 'slack'] },
};Os slugs vêm do catálogo ao vivo. Deixe a interface do administrador descobri-los em vez de fixar: await vinkius.catalog.search('telematics') ou for await (const c of vinkius.catalog.iterate()). Veja Conectores e credenciais.
3. Provisone um departamento uma vez (uma ação de admin)
Quando um time entra em produção, conecte as contas e armazene as credenciais. O id do departamento é o externalId em toda parte, não há humano neste caminho.
import { vinkius } from './vinkius';
import { DEPARTMENTS, type Department } from './departments';
async function bootstrapDepartment(dept: Department) {
const user = vinkius.user(`dept-${dept}`);
// anexe metadados não secretos para filtrar/auditar depois
await user.ensure({ kind: 'department', label: DEPARTMENTS[dept].label });
for (const slug of DEPARTMENTS[dept].connectors) {
const connector = user.connector(slug);
await connector.connect();
// api_key: connector.credentials.set({ API_KEY: ... })
// oauth: connect() retorna após o consentimento no provedor
}
// reporte a prontidão por conector para os admins verem o que falta
return Promise.all(
DEPARTMENTS[dept].connectors.map(async (slug) => ({
slug,
status: await user.connector(slug).status(),
})),
);
}4. Carregue as capacidades do departamento na hora da requisição
Chega uma requisição com o departamento que o copilot atende. Resolva as capacidades dele, com escopo e prontas para entregar ao modelo.
async function departmentCapabilities(dept: Department) {
const spec = DEPARTMENTS[dept];
return vinkius.user(`dept-${dept}`).capabilities({
include: spec.connectors,
onConnectorError: (slug, error) => {
// mostre ao administrador; não interrompa a resposta
console.warn(`${dept}/${slug}`, (error as Error).message);
},
});
}5. Responda como o departamento
Encaminhe a mensagem ao copilot certo, dê ao modelo só as ferramentas daquele departamento e execute na conexão daquele 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: 'Você é o copilot do Financeiro. Reconcilie faturas e responda dúvidas de cobrança usando as ferramentas contábeis conectadas.',
dispatch: 'Você é o copilot da Expedição. Diga onde estão as cargas e atualize ETAs usando o TMS e o Slack.',
warehouse: 'Você é o copilot do Armazém. Reporte contagens no cais e tarefas abertas usando o WMS e as Planilhas.',
fleet: 'Você é o copilot da Operação de Frota. Reporte a saúde dos veículos e a manutenção usando telemetria e 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); // roda como este depto
return { answer: result.content, tool: call.function.name };
}// A Expedição pergunta pelo próprio TMS; não alcança o NetSuite do Financeiro.
await ask('dispatch', 'Qual o ETA da carga 4412 e qual motorista está nela?');
await ask('finance', 'Quais faturas de clientes acima de 30 dias seguem em aberto este mês?');6. Deixe humanos agirem como um departamento, com rastro
As pessoas que operam um copilot não são o "usuário" no sentido do SDK, mas você ainda deve saber quem perguntou. Registre o operador junto ao id do departamento, a fronteira de capacidades permanece com o departamento.
async function askAs(dept: Department, operator: string, question: string) {
const answer = await ask(dept, question);
// sua própria trilha de auditoria — o SDK nunca vê o operador
await audit.record({
actor: operator,
on_behalf_of: `dept-${dept}`,
question,
});
return answer;
}Mantenha a identidade do operador inteiramente no seu sistema. Para o Vinkius, o ator é sempre dept-finance. É isso que dá isolamento em nível de departamento e faz o mesmo copilot se comportar de forma idêntica não importa qual funcionário esteja digitando.
7. Conceda e revogue um time em uma única ação
Como um departamento é um único external_id, a desativação é trivial. Desconectar um conector remove o acesso apenas daquele time.
async function retireConnector(dept: Department, slug: string) {
await vinkius.user(`dept-${dept}`).connector(slug).disconnect();
}Recursos que valem a pena conhecer aqui
Um departamento, um conector, sem re-listar tudo
Um copilot focado que só acessa um sistema não deve incorrer no custo de um fan-out sobre todas as conexões do time. O forConnector fatia um conjunto já carregado; e para nem buscar os outros, liste a partir de um único handle de conector:
// a partir de um conjunto agregado:
const sheetsOnly = capabilities.forConnector('google-sheets');
// ou evitando listar toda conexão:
const tmsOnly = await vinkius.user(`dept-${dept}`).connector('sap').capabilities();O fan-out é concorrente e auto-curável
O user.capabilities() lista os resumos das conexões uma vez, faz fan-out aos conectores ready com teto de concorrência de 8 e reutiliza o id de conexão já resolvido (sem re-listar por conector). O timeout de um conector não interrompe a resposta, os demais continuam resolvendo, e o onConnectorError diz qual tool do time ficou indisponível, para você avisar o administrador daquele departamento.
Leia os metadados do próprio departamento de volta
O kind: 'department' não secreto que você anexou com ensure() está disponível para renderização. O user.get() devolve metadados e status armazenados, suficiente para montar um painel "quais times estão onboarded?" sem nenhum banco extra.
const profile = await vinkius.user('dept-finance').get();
console.log(profile.metadata); // { kind: 'department', label: 'Financeiro' }
console.log(profile.status); // active | ...Checklist de produção
- [ ] Prefixe ids de departamento para nunca colidir com ids humanos (
dept-…). - [ ] Guarde a lista de conectores por departamento como configuração, não como ramificações de código.
- [ ] Faça o onboarding com
user.ensure({ kind: 'department' })para que dashboards por metadado funcionem. - [ ] Exiba
connector.status()no console administrativo para ver lacunas (needs_credentials). - [ ] Encaminhe operadores humanos ao copilot do departamento; mantenha o log de quem-perguntou do seu lado.
- [ ] Dê a toda ação mutável do copilot um
idempotencyKeyestável (ex.: ticket + departamento).
Você agora opera uma plataforma interna de IA em que Financeiro, Expedição, Armazém e Frota têm cada um suas próprias ferramentas conectadas e seu próprio copilot, tudo a partir de uma única chave de aplicação e um external_id por time. Você não comprou uma plataforma para departamentos; a plataforma simplesmente não tem teto para o que um usuário pode ser. Adicionar um quinto time amanhã é uma linha de configuração. É isso que possuir a camada de conectividade significa: o organograma vira a sua tabela de usuários.
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.
