system://luna-ai repository truth

Luna.AI Arquitetura conversacional,
por dentro.

Do webhook à resposta: uma leitura técnica do runtime, da NLU e das guardas que mantêm cada afirmação ancorada em dados reais.

snapshot 28.07.2026 escopo código ativo status verificado
server.js · flow-client.js ● source
L1737processEvolutionMessageentrada
L1745sendEvolutionTextresposta
L2566flowClient.executeFlowruntime
L2593POST /webhook/:tenantHTTP
L2635extractTwimlMessageparser
L1756sendEvolutionTextsaída
$ rg "processEvolutionMessage" apps/chatbot-whatsapp/server.js
scroll to inspect
01 / 07
Arquitetura

Uma mensagem.
Seis fronteiras claras.

O chatbot recebe o evento da Evolution, resolve o tenant, delega a execução ao backend de flows e devolve o texto pelo mesmo gateway.

  1. 01
    WhatsApp mensagem do cliente
  2. 02
    Evolution webhook autenticado
  3. 03
    chatbot tenant + roteamento
  4. 04
    flows-backend sessão + grafo
  5. 05
    NLU / IA intenção + resposta
  6. 06
    resposta sendEvolutionText
evidence_01

O caminho de volta também está no código.

O mesmo handler que extrai a mensagem chama processEvolutionMessage() e finaliza em sendEvolutionText(). Para tenants em flows, o corpo é encaminhado a /webhook/:tenant.

source apps/chatbot-whatsapp/server.js · L1745–1760 / L2593–2606
apps/chatbot-whatsapp/server.js · L1745–1760
const replyText = await processEvolutionMessage({
  tenantSlug,
  from: remoteJid,
  message: messageText,
  event,
});

if (!replyText) {
  return res.json({ ok: true, ignored: 'no_reply' });
}

await sendEvolutionText({
  instanceName,
  numberCandidates: buildEvolutionNumberCandidates(remoteJid),
  text: replyText,
});
Recebe, processa e envia pela Evolution
apps/chatbot-whatsapp/server.js · L2593–2606
const url = `${String(FLOWS_BACKEND_URL).replace(/\/$/, '')}/webhook/${encodeURIComponent(effectiveTenantSlug)}`;
let response;
let raw = '';

try {
  response = await fetchWithTimeout(
    url,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ From: String(from || ''), Body: String(message || '') }),
    },
    FLOWS_REQUEST_TIMEOUT_MS
  );
Ingress real para o flows-backend
02 / 07
Motor conversacional

O fluxo é um grafo.
next é a aresta.

Cada turno executa o handler do nó atual, combina variáveis, resolve a transição e persiste onde a conversa deve continuar.

messageinicionext: saudacao_menu
messagesaudacao_menunext: menu_principal
conditionmenu_principalis_subscriber?
truemenu_principal_assinante
falsemenu_principal_publico
apps/verticals/barbearia/flow.json · excerto real
{
  "nodes": [
    {
      "id": "inicio",
      "next": "saudacao_menu",
      "type": "message",
      "config": {
        "text": ""
      },
      "position": {
        "x": 250,
        "y": 0
      }
    },
    {
      "id": "saudacao_menu",
      "next": "menu_principal",
      "type": "message",
      "config": {
        "text": "Oi! Seja bem-vindo à Barbearia do Léo 💈 Posso te ajudar a agendar um horário, ver valores ou tirar dúvidas. É só me dizer o que você precisa 👇"
      },
      "position": {
        "x": 250,
        "y": 50
      }
    },
    {
      "id": "menu_principal",
      "next": {
        "true": "menu_principal_assinante",
        "false": "menu_principal_publico"
      },
      "type": "condition",
      "config": {
        "value": "true",
        "operator": "equals",
        "variable": "is_subscriber"
      },
      "position": {
        "x": 250,
        "y": 100
      }
    }
  ]
}
Nós declarativos e transições via next
apps/flows-backend/src/services/flow-executor.ts · L769–807
private resolveNext(node: FlowNode, result: NodeResult): string | null {
  // Jump / NLU router / confirm / fetch_slots (noSlotsNext) — use branch as target node ID
  if (
    (node.type === 'jump' || node.type === 'nlu_router' || node.type === 'confirm') &&
    result.branch
  ) {
    return result.branch;
  }

  // fetch_slots noSlotsNext — branch is a target node ID
  if (node.type === 'fetch_slots' && result.branch) {
    return result.branch;
  }

  // ai_agent exitConditions contain target node IDs directly.
  if (node.type === 'ai_agent' && result.branch) {
    const exitConditions = (node.config as { exitConditions?: Record<string, string> })
      .exitConditions;
    if (exitConditions && Object.values(exitConditions).includes(result.branch)) {
      return result.branch;
    }
  }

  // No next configured
  if (node.next === null || node.next === undefined) {
    return null;
  }

  // Simple string next
  if (typeof node.next === 'string') {
    return node.next;
  }

  // Conditional next (object with branches)
  if (typeof node.next === 'object' && result.branch) {
    return node.next[result.branch] || node.next['default'] || null;
  }

  return null;
}
O resolvedor real de transições
03 / 07
NLU em três camadas

Determinismo primeiro.
IA quando agrega.

A interpretação começa sem custo e com alta precisão local. Só escala para modelo contextual quando regex e dicionário não resolvem.

L1
zero cost

Regex + dicionário

Normaliza acentos, menus numéricos, confirmações, horários e padrões por estado.

0.85—0.99
L2
context aware

LLM contextual

Sonnet em contextos críticos; Haiku nos simples. Timeout de 3 segundos.

≥ 0.60
L3
honest fallback

Clarificação

Baixa confiança vira unknown; o runtime pode pedir outra formulação.

no guess
apps/flows-backend/src/services/nlu.service.ts · L1159–1191
// Layer 1: Regex
const regexResult = regexMatch(text, effectiveConfig);
if (regexResult) {
  logger.debug('NLU regex match', {
    state: effectiveConfig.state,
    intent: regexResult.intent,
  });
  setCache(cacheKey, regexResult);
  return regexResult;
}

// Layer 2: LLM
const llmResult = await llmInterpret(text, effectiveConfig);
if (llmResult && llmResult.confidence >= 0.6) {
  logger.debug('NLU LLM match', {
    state: effectiveConfig.state,
    intent: llmResult.intent,
    confidence: llmResult.confidence,
  });
  setCache(cacheKey, llmResult);
  return llmResult;
}

// Layer 3: Low confidence / failure
const fallbackResult: NLUResult = {
  intent: 'unknown',
  entities: {},
  confidence: llmResult?.confidence ?? 0,
  originalText: text,
  source: llmResult?.source ?? 'regex',
};

return fallbackResult;
Fallback em três camadas, exatamente como implementado
evidence_03

Confiança baixa não recebe uma resposta inventada.

A terceira camada retorna intenção unknown e preserva a confiança do modelo. As mensagens de clarificação também vivem no serviço, separadas da classificação.

criticalclaude-sonnet-4-20250514
simpleclaude-haiku-4-5-20251001
source nlu.service.ts · L61–80 / L1083–1086
04 / 07
Sistema de manifesto

Um vertical começa
com dois contratos.

O manifesto descreve identidade, intents, integrações e onboarding. O flow descreve o comportamento transacional.

01
manifest.tscapacidade + configuração
typed
02
flow.jsongrafo + transições
declarative
apps/verticals/outsourcing/manifest.ts · arquivo completo
import type { VerticalManifest } from '@/types/vertical-manifest';

export const manifest: VerticalManifest = {
  slug: 'outsourcing',
  name: 'Outsourcing de Impressão e Informática',
  industry: 'outsourcing',
  language: 'pt-BR',
  timezone: 'America/Sao_Paulo',
  schemaPrefix: 'os_',
  useGenericTables: false,
  defaultFlowPath: 'apps/verticals/outsourcing/flow.json',
  customIntents: [
    'novo_chamado',
    'status_chamado',
    'solicitar_suprimento',
    'agendar_manutencao',
    'consultar_contrato',
  ],
  intentsPath: 'apps/verticals/outsourcing/nlu-intents.ts',
  messagesPath: 'apps/verticals/outsourcing/messages.ts',
  integrations: [
    {
      name: 'printwayy',
      type: 'erp',
      required: false,
      configFields: [
        { key: 'api_key', label: 'API Key Printwayy', type: 'secret', required: true },
        { key: 'company_id', label: 'Company ID', type: 'string', required: true },
      ],
      adapterPath: 'apps/verticals/outsourcing/integrations/printwayy.ts',
    },
  ],
  customPages: [],
  onboardingFields: [
    { key: 'company_name', label: 'Nome da Empresa', type: 'string', required: true },
    { key: 'whatsapp', label: 'WhatsApp', type: 'phone', required: true },
    { key: 'main_contact', label: 'Contato Principal', type: 'string', required: true },
  ],
  ragAutoIndex: true,
  ragDocsPath: 'apps/verticals/outsourcing/skills',
};

export default manifest;
Manifesto real do vertical de outsourcing
05 / 07
Motor de correlação · Tier 2

O insight nasce
da interseção.

Tier 1 produz sinais isolados. Tier 2 cruza concentração, máquinas stale, piso contratual, suprimentos e tendência de volume.

ordemregracondição de disparosaída
P10 concentracao_stale

Cliente é rank 1 ou > 50% do volume + ao menos uma máquina stale.

Maior cliente com máquina abandonada
P20 stale_suprimento

Máquina stale + consumível com nível conhecido entre 0% e < 10%.

Máquina parada com suprimento crítico
P30 piso_volume

Cliente no piso + série suficiente + volume em alta.

Cliente no piso com volume subindo
P40 concentracao_queda_volume

Cliente concentrado + série suficiente em queda + gatilho churn ligado.

Cliente concentrado com queda de volume
apps/admin-unified-backend/src/svcs/cockpit-correlacao-tier2.service.ts · L368–383
export function avaliarCorrelacao(
  stale: StaleMachineInsight[],
  concentracao: ClientConcentrationInsight[],
  piso: SubutilizacaoPisoInsight[],
  options: CorrelacaoOptions = {}
): CorrelacaoEvaluation {
  const audit: CorrelacaoAuditEvent[] = [];
  const volumeTrends = options.volumeTrends ?? [];
  const correlacoes = [
    ...concentrationStaleRule(concentracao, stale),
    ...staleSupplyRule(stale, options.criticalSupplies ?? []),
    ...pisoVolumeRule(piso, volumeTrends, audit),
    ...churnRule(concentracao, volumeTrends, options.churnEnabled === true, audit),
  ].sort((left, right) => left.priority - right.priority);

  return { correlacoes, audit };
}
Composição e prioridade das quatro regras
generated_insight.test_fixture grounded
🌙

Antes de tudo: olhe RIS primeiro.

Porque RIS concentra 59,8% do volume e há máquina sem comunicação há 327 dias, com 1 críticos no radar, esse é o risco que mais conecta operação e carteira.

Próximo passo: vale uma ligação para confirmar se a máquina foi substituída ou se é problema de comunicação.

RIS59,8%327 dias1 críticos
Texto literal do mock de narração validado em src/__tests__/correlacao-tier2.test.ts.
06 / 07
Disciplina de dado

A tela
nunca mente.

Identidade pode ser mascarada. Ausência permanece ausência. Narração que inventa número, omite âncora ou promete ação fantasma cai para fallback determinístico.

01
privacy boundary

Mascaramento preserva a operação.

Identidades viram pseudônimos; os números operacionais continuam reais.

src/scripts/export-outsourcing-dashboard-masked-snapshot.ts · L159–186
const scrub = (value: unknown): unknown => {
  if (typeof value === 'string') {
    const exactPseudonym = replacements.get(value);
    if (exactPseudonym) return exactPseudonym;
    return orderedReplacements.reduce(
      (masked, [real, pseudonym]) => masked.replaceAll(real, pseudonym),
      value
    );
  }
  if (Array.isArray(value)) return value.map(scrub);
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.entries(value).map(([key, nested]) => [key, scrub(nested)])
    );
  }
  return value;
};

const masked = scrub(source) as FleetSnapshot;

masked.tenant = { id: 'parque-real-mascarado', name: 'Parque real mascarado' };
masked.presentation = {
  source: 'masked',
  time_zone: OUTSOURCING_DASHBOARD_TIME_ZONE,
  captured_at: counterCapturedAt.toISOString(),
  identities_masked: true,
  operational_numbers_real: true,
};
Scrub recursivo + metadado explícito de origem
02
null is a feature

Sentinela desconhecida vira NULL.

-1, faixa inválida e 0 de peça não viram nível operacional.

src/integrations/printwayy/printwayy-sentinels.ts · L110–123
export function normalizeSupplyLevel(
  rawLevel: unknown,
  type?: unknown,
  levelDescription?: unknown
): number | null {
  if (normalizeText(levelDescription) === 'desconhecido') return null;

  const value = numberValue(rawLevel);
  if (value === null) return null;
  if (value === SENTINEL_ESTIMATED_LEVEL_UNKNOWN) return null;
  if (value < 0 || value > 100) return null;
  if (value === 0 && !isConsumivel(type)) return null;

  return value;
}
Normalização na fronteira de ingestão
03
output firewall

O narrador não ganha fatos novos.

Número sem grounding, âncora ausente ou promessa fantasma aciona fallback.

src/svcs/b2b-cockpit-surface.service.ts · L711–735
const phantom = text ? phantomActionOffers(text) : [];
if (phantom.length > 0) {
  logger.warn('B2B cockpit Sonnet narration phantom action fallback', {
    intent: input.intent,
    phantom,
  });
  return withLunarPrefix(input.fallbackNarrative);
}
const ungrounded = text ? ungroundedNumbers(text, input.structuredInsight) : [];
if (ungrounded.length > 0) {
  logger.warn('B2B cockpit Sonnet narration ungrounded number fallback', {
    intent: input.intent,
    ungrounded,
  });
  return withLunarPrefix(input.fallbackNarrative);
}
const missing = text ? missingAnchors(text, input.structuredInsight) : [];
if (missing.length > 0) {
  logger.warn('B2B cockpit Sonnet narration missing anchor fallback', {
    intent: input.intent,
    missing,
  });
  return withLunarPrefix(input.fallbackNarrative);
}
return text ? withLunarPrefix(text) : withLunarPrefix(input.fallbackNarrative);
Guard anti-alucinação antes da resposta chegar à tela
princípio operacional

Dado desconhecido não é zero. Texto plausível não é evidência. Fallback honesto é parte do produto.

voltar ao topo
07 / 07
Dashboard outsourcing · cockpit operacional

Menos painel.
Mais decisão.

A superfície responde “está tudo bem?” com poucos sinais. A profundidade fica com a Luna — sem romper a cadeia entre origem, agregação, janela e exibição.

domain.topology PlacesGraphSummary source / typed
01 customer Cliente customer_id
02 place Site customer_place_id
03 printer Equipamento printer_id
04 stock Suprimentos estoque_itens
o insight está nos buracos

O grafo não esconde cadastro incompleto; ele o transforma em fila acionável.

  • impressoras_sem_local[]equipamento órfão
  • locais_sem_estoque[]site sem cobertura
  • locais_sem_impressora[]site sem ativo
apps/outsourcing-dashboard/src/data/types.ts · L238–268
origem é estado de interface

A degradação aparece
antes do número.

O provider escolhe o contrato. A tela declara se o snapshot é autenticado, real mascarado ou fictício de demonstração.

masked DADOS REAIS MASCARADOS identidades protegidas
authenticated QUALIDADE DAS LEITURAS tenant do token + vínculo confirmado
backend-fallback BACKEND INDISPONÍVEL · MODO DEMONSTRAÇÃO dados fictícios · não representam operação real
Strings de estado: src/App.tsx · L264–273 e src/views/TrustStrip.tsx · L69–102
apps/outsourcing-dashboard/src/data/provider.ts · L447–458
export type DashboardDataMode = 'fixture' | 'masked' | 'authenticated';

const runtimeFixture =
  typeof window !== 'undefined' &&
  new URLSearchParams(window.location.search).get('dados') === 'fixture';
const providerFlag = import.meta.env.VITE_DASHBOARD_PROVIDER;

export const dashboardDataMode: DashboardDataMode = providerFlag === 'real'
  ? 'authenticated'
  : providerFlag === 'fixture' || runtimeFixture
    ? 'fixture'
    : 'masked';
A origem é selecionada explicitamente; o padrão público é o snapshot real mascarado
presentation_boundary

A Luna é “burra” por contrato.

O componente recebe estado pronto e emite a mensagem. Endpoint, autenticação e escolha de fonte ficam fora da camada visual.

apps/outsourcing-dashboard/src/views/LunaExperience.tsx · L70–84
export function LunaChat({
  messages,
  busy,
  error,
  onSend,
  compact = false,
}: {
  messages: LunaMessage[];
  busy: boolean;
  error: string | null;
  onSend: (message: string) => Promise<void>;
  compact?: boolean;
}) {
Contrato real atual do componente de conversa
action_boundary

Escrita nasce desligada.

Ação é dado declarativo, executor é plugável e consequência externa exige confirmação. Ajuste de estoque existe no catálogo — mas permanece desativado.

suggestação sugerida
confirmclique explícito
executeexecutor
apps/admin-unified-backend/src/svcs/outsourcing-dashboard-actions.ts · L49–55
{
  tipo: 'estoque.ajustar',
  enabled: false,
  reversible: true,
  requires_confirmation: true,
}
Ação de escrita declarada, reversível e desligada
dashboard

orienta

Luna

interpreta

humano

confirma

voltar ao topo