Conceitos, Padrões e Convenções do Projeto
O IA BOI é um sistema de gestão pecuária que utiliza inteligência artificial para análise e sugestões de manejo de gado. O projeto é dividido em três partes principais:
ia-boi-api (Backend Java)
C:/dotum/projetos/ia-boi/ia-boi-apiia-coreia-boi-web (Frontend React/Next.js)
C:/dotum/projetos/ia-boi-web/dot/apps/iaboiweb@dot/ui e @dot/coreia-core (Framework Backend)
C:/dotum/projetos/ia-boi/ia-coreia-boi-api@dot/ui (Biblioteca de Componentes UI)
C:/dotum/projetos/ia-boi-web/dot/packages/uiia-boi-web@dot/core (Biblioteca Core Frontend)
C:/dotum/projetos/ia-boi-web/dot/packages/coresrc/main/java/
├── bo/ # Business Objects (lógica de negócio)
├── controller/ # Controllers REST (endpoints da API)
│ └── dto/ # Data Transfer Objects
├── helper/ # Classes auxiliares (cálculos, etc.)
├── model/
│ ├── bean/ # Entidades do Framework (tipo Bean próprio)
│ ├── dao/ # Data Access Objects (acesso a dados)
│ └── dd/ # Default Data (tabelas com dados padrões)
└── service/ # Classes para selects utilizados com frequência
AbstractController@ServiceClass@Endpoint expõem endpoints REST[Modulo]Controller
SuplementoController, AnaliseDetalhadaControllerselectAll(), selectById(Integer id)cadastrar(...)inativar(String token)Controllers são responsáveis por receber requisições HTTP e coordenar a execução da lógica de negócio através de BOs e DAOs.
@ServiceClass
public class SuplementoController extends AbstractController {
@Endpoint(path = "/suplemento", method = "POST")
public SuplementoBean cadastrar(String data, String lote, String pasto,
String suplementoTipo, BigDecimal quantidadeAlimento,
String token) throws Exception {
TransactionDB trans = getTransaction();
UserContext userContext = getUserContext();
SuplementoBO supBO = new SuplementoBO(trans, userContext);
SuplementoBean supBean = supBO.cadastrar(data, lote, pasto, suplementoTipo,
quantidadeAlimento, token);
return supBean;
}
@Endpoint(path = "/suplemento/{id}", method = "GET")
public SuplementoBean selectById(Integer id) throws Exception {
TransactionDB trans = getTransaction();
SuplementoBean supBean = SuplementoDAO.getInstance().selectById(trans, id);
return supBean;
}
@Endpoint(path = "/suplemento", method = "GET")
public List<SuplementoListagemDTO> selectAll() throws Exception {
TransactionDB trans = getTransaction();
// ✅ CORRETO: DAO já trata exceções internamente e retorna lista vazia se necessário
List<SuplementoBean> supList = SuplementoDAO.getInstance()
.selectAllComDescricoes(trans);
return SuplementoListagemDTO.convert(supList);
}
@Endpoint(path = "/suplemento", method = "DELETE")
public void inativar(String token) throws Exception {
TransactionDB trans = getTransaction();
UserContext userContext = getUserContext();
SuplementoBO supBO = new SuplementoBO(trans, userContext);
supBO.inativar(token);
}
}
BOs contêm a lógica de negócio do sistema. Eles estendem AbstractBO e recebem TransactionDB e UserContext no construtor.
public class ManejoBO extends AbstractBO {
public ManejoBO(TransactionDB trans, UserContext userContext) {
super(trans, userContext);
}
public void cadastrar(String lote, String tipo, BigDecimal peso,
LocalDateTime data) throws Exception {
// Validações de negócio
if (lote == null || lote.isEmpty()) {
throw new WarningException("Lote é obrigatório.");
}
// Busca o lote
LoteBean lotBean = LoteDAO.getInstance()
.selectByDescricao(trans, lote);
if (lotBean == null) {
throw new NotFoundException("Lote não encontrado.");
}
// Cria o manejo
ManejoBean manBean = new ManejoBean();
manBean.setLote(lotBean);
manBean.setData(data);
manBean.setPeso(peso);
// ... outros campos
// Insere no banco
trans.insert(manBean);
}
}
DAOs são responsáveis pelo acesso a dados. Eles estendem GenericDAO e seguem o padrão Singleton.
public class SuplementoDAO extends GenericDAO<SuplementoBean> {
private static final SuplementoDAO INSTANCE = new SuplementoDAO();
private SuplementoDAO() { super(SuplementoBean.class); }
public static SuplementoDAO getInstance() { return INSTANCE; }
public SuplementoBean selectById(TransactionDB trans, Integer id) throws Exception {
Integer uniId = trans.getUserContext().getUnidade().getId();
WhereDB<SuplementoBean> where = new WhereDB<>(SuplementoBean.class)
.and("unidade.id", OperatorDB.EQ, uniId)
.and("ativo", OperatorDB.EQ, Constants.SIM)
.and("id", OperatorDB.EQ, id);
List<SuplementoBean> list = new ArrayList<>();
try {
list = select(trans, where);
return list.get(0);
} catch (NotFoundException e) {
// Se não encontrar registro, retorna null ao invés de lançar exceção
return null;
}
}
}
NotFoundException, WarningException)public List<SuplementoBean> selectAll(TransactionDB trans) throws Exception {
Integer uniId = trans.getUserContext().getUnidade().getId();
WhereDB<SuplementoBean> where = new WhereDB<>(SuplementoBean.class)
.and("unidade.id", OperatorDB.EQ, uniId)
.and("ativo", OperatorDB.EQ, Constants.SIM);
List<SuplementoBean> list = new ArrayList<>();
try {
list = select(trans, where);
return list;
} catch (NotFoundException e) {
// ✅ CORRETO: DAO retorna lista vazia ao invés de lançar exceção
return list;
}
}
public List<SuplementoBean> selectAllComDescricoes(TransactionDB trans) throws Exception {
Integer uniId = trans.getUserContext().getUnidade().getId();
StringBuilder sb = new StringBuilder();
sb.append(" select sup.sup_id, ");
sb.append(" sup.emp_id, ");
// ... resto da query
List<Object> params = new ArrayList<Object>();
params.add(uniId);
ORMMapper<SuplementoBean> orm = new ORMMapper<SuplementoBean>(SuplementoBean.class);
orm.add(Integer.class, "sup_id", "id");
// ... resto do mapeamento
List<SuplementoBean> list = new ArrayList<>();
try {
list = selectCustom(trans, sb.toString(), params, orm);
return list;
} catch (NotFoundException e) {
// ✅ CORRETO: DAO retorna lista vazia ao invés de lançar exceção
return list;
}
}
// ❌ ERRADO: Não deve propagar exceção
public List<SuplementoBean> selectAllComDescricoes(TransactionDB trans) throws Exception {
// ❌ ERRADO: DAO não deve propagar NotFoundException
// Deve tratar internamente e retornar lista vazia
return selectCustom(trans, sb.toString(), params, orm);
}
Services são classes utilitárias para selects complexos ou cálculos que são utilizados com frequência. Eles geralmente contêm métodos estáticos.
StringBuilder, selectCustom, ORMMapperpublic class AnaliseDetalhadaService {
/**
* Calcula o estoque atual descontando as suplementações realizadas.
*/
public static BigDecimal calcularEstoqueAtual(TransactionDB trans, Integer uniId,
Integer suptId) throws Exception {
// ✅ CORRETO: Service chama DAO (não faz consulta SQL direta)
BigDecimal estoqueTotal = FornecedorEstoqueDAO.getInstance()
.selectEstoqueAtualByUnidadeAndTipo(trans, uniId, suptId);
// ✅ CORRETO: Service chama DAO (não faz consulta SQL direta)
// DAO já trata exceções e retorna lista vazia se necessário
List<SuplementoBean> suplementos = SuplementoDAO.getInstance().selectAll(trans);
// ✅ CORRETO: Service faz cálculos com dados retornados pelos DAOs
BigDecimal totalSuplementado = BigDecimal.ZERO;
for (SuplementoBean sup : suplementos) {
if (sup.getSuplementoTipo() != null &&
sup.getSuplementoTipo().getId().equals(suptId)) {
if (sup.getQuantidadeAlimento() != null) {
totalSuplementado = Calc.add(totalSuplementado,
sup.getQuantidadeAlimento());
}
}
}
// Estoque atual = Estoque Total - Quantidades Suplementadas
return Calc.subtract(estoqueTotal, totalSuplementado);
}
}
// ❌ ERRADO: Service não pode fazer consulta SQL direta
public static BigDecimal calcularEstoqueAtual(TransactionDB trans, Integer uniId,
Integer suptId) throws Exception {
// ❌ ERRADO: Service não deve usar StringBuilder, selectCustom, ORMMapper
StringBuilder sb = new StringBuilder();
sb.append("select ...");
ORMMapper<SuplementoBean> orm = new ORMMapper<SuplementoBean>(SuplementoBean.class);
// ... consulta SQL direta no Service
return selectCustom(trans, sb.toString(), params, orm);
}
Helpers são classes auxiliares que contêm lógica de cálculo ou transformação de dados complexa.
public class ManejoCalcHelper {
public record CalcGmdCmcVO(BigDecimal gmd, BigDecimal gmc,
BigDecimal gmdQuantidadePonderada,
BigDecimal gmdProducaoVivo,
BigDecimal gmdProducaoCarcaca) {
};
public static CalcGmdCmcVO calcFazendaGmdGmc(
ManejoFazendaTotalizadoresVO mfcDTO,
BigDecimal producaoVivoTotal,
BigDecimal producaoCarcacaTotal) {
BigDecimal fazendaGmdDias = new BigDecimal(mfcDTO.getFazendaDiasTotal());
// ... cálculos complexos
return new CalcGmdCmcVO(fazendaGmd, fazendaGmc,
fazendaGmdQuantidadePonderada,
producaoVivoTotal, producaoCarcacaTotal);
}
}
Bean é próprio do Framework IA Coremodel/bean/@Table(sigla="...") do Beanmodel/dd/[Acrônimo]DD (exemplo: MantDD = Manejo Tipo DD)@Table(sigla="...") do Bean correspondenteMantDD.PESO contém o ID do tipo de manejo "Peso"Record do Javacontroller/dto/[Modulo][Acao]DTO
SuplementoListagemDTO, AnaliseDetalhadaDTOBean diretamente)convert para converter de Bean para DTOExemplo de DTO usando Record:
package controller.dto;
import java.util.List;
import model.bean.LoteBean;
public record PastoDTO(String pasto, String lote, Integer quantidade) {
public static PastoDTO convert(LoteBean lotBean) {
String lote = lotBean.getDescricao();
String pasto = lotBean.getPasto().getDescricao();
Integer quantidade = lotBean.getQuantidade();
return new PastoDTO(pasto, lote, quantidade);
}
public static List<PastoDTO> convert(List<LoteBean> lotList) {
return lotList.stream().map(PastoDTO::convert).toList();
}
}
O projeto utiliza duas exceções principais:
NotFoundException: Quando um registro não é encontrado no banco de dadosWarningException: Quando há um aviso de negócio (não é um erro crítico)@Endpoint(path = "/suplemento", method = "GET")
public List<SuplementoListagemDTO> selectAll() throws Exception {
TransactionDB trans = getTransaction();
// ✅ CORRETO: DAO já trata exceções internamente, retorna lista vazia se necessário
List<SuplementoBean> supList = SuplementoDAO.getInstance()
.selectAllComDescricoes(trans);
return SuplementoListagemDTO.convert(supList);
}
public static SuplementoParametroBean buscarParametro(TransactionDB trans,
Integer uniId, Integer empId,
Integer usuFornecedorId,
Integer tipoId) throws Exception {
// ✅ CORRETO: DAO já trata exceções internamente, retorna lista vazia se não encontrar
List<SuplementoParametroBean> parametros = SuplementoParametroDAO.getInstance()
.selectByPrioridade(trans, uniId, empId, usuFornecedorId, tipoId);
// Retorna o primeiro elemento da lista ou null se a lista estiver vazia
return parametros.isEmpty() ? null : parametros.get(0);
}
O UserContext identifica quem está fazendo a requisição e o TransactionDB representa uma transação no banco de dados.
// Em classes que estendem AbstractController
UserContext userContext = getUserContext();
TransactionDB trans = getTransaction();
// Em classes que estendem AbstractBO
// UserContext e TransactionDB já estão disponíveis através do super
// Podem ser acessados via getTransaction() e getUserContext()
public SuplementoBean selectById(TransactionDB trans, Integer id) throws Exception {
// ✅ CORRETO: Obter unidade do UserContext da transação
Integer uniId = trans.getUserContext().getUnidade().getId();
WhereDB<SuplementoBean> where = new WhereDB<>(SuplementoBean.class)
.and("unidade.id", OperatorDB.EQ, uniId)
.and("ativo", OperatorDB.EQ, Constants.SIM)
.and("id", OperatorDB.EQ, id);
List<SuplementoBean> list = new ArrayList<>();
try {
list = select(trans, where);
return list;
} catch (NotFoundException e) {
// Se não encontrar registro, retorna null ao invés de lançar exceção
return null;
}
}
Para queries complexas, use StringBuilder e ORMMapper.
StringBuilder, selectCustom, ORMMapperStringBuilder ao invés de String - é mais rápido para concatenação");"public List<SuplementoBean> selectAllComDescricoes(TransactionDB trans) throws Exception {
Integer uniId = trans.getUserContext().getUnidade().getId();
StringBuilder sb = new StringBuilder();
sb.append(" select sup.sup_id, ");
sb.append(" sup.emp_id, ");
sb.append(" sup.uni_id, ");
sb.append(" sup.sup_utccadastro, ");
sb.append(" sup.sup_utcatualizado, ");
sb.append(" sup.usu_cadastrou_id, ");
sb.append(" sup.sup_ativo, ");
sb.append(" sup.sup_data, ");
sb.append(" sup.supt_id, ");
sb.append(" supt.supt_descricao, ");
sb.append(" sup.lot_id, ");
sb.append(" lot.lot_descricao, ");
sb.append(" sup.pas_id, ");
sb.append(" pas.pas_descricao, ");
sb.append(" sup.sup_quantidadecabeca, ");
sb.append(" sup.sup_quantidadealimento, ");
sb.append(" sup.sup_token ");
sb.append(" from suplemento sup ");
sb.append(" join lote lot on lot.lot_id = sup.lot_id ");
sb.append(" join suplementotipo supt on supt.supt_id = sup.supt_id ");
sb.append(" left join pasto pas on pas.pas_id = sup.pas_id ");
sb.append(" where sup.uni_id = ? ");
sb.append(" and sup.sup_ativo = " + Constants.SIM + " ");
sb.append(" order by sup.sup_data desc, sup.sup_id desc ");
List<Object> params = new ArrayList<Object>();
params.add(uniId);
ORMMapper<SuplementoBean> orm = new ORMMapper<SuplementoBean>(SuplementoBean.class);
orm.add(Integer.class, "sup_id", "id");
orm.add(Integer.class, "emp_id", "empresa.id");
orm.add(Integer.class, "uni_id", "unidade.id");
orm.add(Integer.class, "usu_cadastrou_id", "usuarioCadastrou.id");
orm.add(Integer.class, "supt_id", "suplementoTipo.id");
orm.add(String.class, "supt_descricao", "suplementoTipo.descricao");
orm.add(Integer.class, "lot_id", "lote.id");
orm.add(String.class, "lot_descricao", "lote.descricao");
orm.add(Integer.class, "pas_id", "pasto.id");
orm.add(String.class, "pas_descricao", "pasto.descricao");
orm.add(LocalDateTime.class, "sup_utccadastro", "utcCadastro");
orm.add(LocalDateTime.class, "sup_utcatualizado", "utcAtualizado");
orm.add(Integer.class, "sup_ativo", "ativo");
orm.add(LocalDateTime.class, "sup_data", "data");
orm.add(Integer.class, "sup_quantidadecabeca", "quantidadeCabeca");
orm.add(BigDecimal.class, "sup_quantidadealimento", "quantidadeAlimento");
orm.add(String.class, "sup_token", "token");
List<SuplementoBean> list = new ArrayList<>();
try {
list = selectCustom(trans, sb.toString(), params, orm);
return list;
} catch (NotFoundException e) {
// Se não encontrar registros, retorna lista vazia ao invés de lançar exceção
return list;
}
}
src/
├── app/
│ └── (private)/ # Rotas privadas (requerem autenticação)
│ └── [modulo]/ # Módulos da aplicação
│ ├── page.tsx # Página principal do módulo
│ └── components/ # Componentes específicos do módulo
├── components/ # Componentes reutilizáveis
├── hooks/ # Hooks customizados
└── services/ # Serviços de API
└── [modulo]/
├── index.ts # Funções de API
└── schemas/ # Schemas TypeScript (interfaces/types)
services/[modulo]/index.ts[acao][Modulo]
listarAnalises, buscarSuplementoPromise<DotumResponse<T>>services/[modulo]/schemas/[Modulo][Acao]Response ou [Modulo][Acao]Request
AnaliseDetalhadaResponseZod para validação e tipageminterface ou type - Sempre usar Zod[Modulo][Componente]
AnaliseDetalhadaTable, StatusBadgeexport function para componentes ReactServiços de API são funções que fazem chamadas HTTP para o backend.
// services/analise-detalhada/index.ts
import { api, DotumResponse } from "@dot/core";
import { AnaliseDetalhadaResponse } from "./schemas/AnaliseDetalhadaResponse";
const URL_PATH = "/analise-detalhada";
/**
* Lista todas as análises detalhadas com filtro opcional
*/
export async function listarAnalises(
filtro?: string
): Promise<DotumResponse<AnaliseDetalhadaResponse[]>> {
// Sempre envia o parâmetro filtro (mesmo que vazio) para o ApiServlet fazer o matching correto
const res = await api.get<DotumResponse<AnaliseDetalhadaResponse[]>>(URL_PATH, {
params: { filtro: filtro || "" },
});
return res.data;
}
// services/analise-detalhada/schemas/AnaliseDetalhadaResponse.ts
import { z } from "zod";
export const analiseDetalhadaResponseSchema = z.object({
unidadeId: z.number(),
unidadeDescricao: z.string(),
suplementoTipoId: z.number(),
suplementoTipoDescricao: z.string(),
ultimoTratamento: z.string().nullable(),
animaisTratados: z.number(),
animaisTotal: z.number(),
consumoIdeal: z.number(),
percentualConsumoIdeal: z.number().nullable(),
estoqueAtual: z.number(),
statusEstoque: z.enum(["ZERADO", "BAIXO", "NORMAL"]),
ultimaCompra: z.string().nullable(),
sugestao: z.string(),
vendedores: z.array(z.string()),
});
export type AnaliseDetalhadaResponse = z.infer<typeof analiseDetalhadaResponseSchema>;
// ❌ ERRADO: Não usar apenas interface ou type
export interface AnaliseDetalhadaResponse {
unidadeId: number;
unidadeDescricao: string;
// ...
}
Componentes React são a interface visual da aplicação.
// app/(private)/analise-detalhada/components/analise-detalhada-table.tsx
"use client";
import { DotumTable, useDotumTable } from "@dot/ui/dotum-table";
import { listarAnalises } from "~/services/analise-detalhada";
import { AnaliseDetalhadaResponse } from "~/services/analise-detalhada/schemas/AnaliseDetalhadaResponse";
interface AnaliseDetalhadaTableProps {
filtro: string;
}
export function AnaliseDetalhadaTable({ filtro }: AnaliseDetalhadaTableProps) {
const table = useDotumTable<AnaliseDetalhadaResponse>({
queryKey: ["analise-detalhada", filtro],
queryFn: async () => {
const filtroFinal = filtro === "TODOS" ? undefined : filtro;
const res = await listarAnalises(filtroFinal);
return res.data ?? ([] as AnaliseDetalhadaResponse[]);
},
});
return (
<DotumTable.Root {...table}>
<DotumTable.Columns>
<DotumTable.Column
label="Produtor/Fazenda"
name="unidadeDescricao"
/>
<DotumTable.Column
label="Tipo de Nutriente"
name="suplementoTipoDescricao"
render={({ row }) => {
const data = row.original as AnaliseDetalhadaResponse;
return (
<div>
<div className="font-medium">{data.suplementoTipoDescricao}</div>
{data.ultimoTratamento && (
<div className="text-sm text-muted-foreground">
Último trato: {formatarData(data.ultimoTratamento)}
</div>
)}
</div>
);
}}
/>
</DotumTable.Columns>
</DotumTable.Root>
);
}
DotumTable é um componente de tabela genérico do @dot/ui.
row.original no render, o TypeScript pode inferir um tipo muito restrito baseado apenas no name da colunaconst data = row.original as TipoCompletoname para funcionalidades do DotumTable (ordenação, filtro, etc.)<DotumTable.Column
label="Tipo de Nutriente"
name="suplementoTipoDescricao"
render={({ row }) => {
// ✅ CORRETO: Type casting explícito quando precisa acessar múltiplas propriedades
const data = row.original as AnaliseDetalhadaResponse;
return (
<div>
<div className="font-medium">{data.suplementoTipoDescricao}</div>
{data.ultimoTratamento && (
<div className="text-sm text-muted-foreground">
Último trato: {formatarData(data.ultimoTratamento)}
</div>
)}
</div>
);
}}
/>
any - Sempre tipar corretamenteDotumTable)Hooks customizados encapsulam lógica reutilizável.
// hooks/use-sidebar-data.ts
import { useQuery } from "@tanstack/react-query";
import { SidebarApi, staticSidebarDataResponse } from "~/services/sidebar";
export function useSidebarData() {
const query = useQuery({
queryKey: ["sidebar"],
queryFn: SidebarApi.doDataSidebarByUser,
placeholderData: staticSidebarDataResponse,
initialData: staticSidebarDataResponse,
});
return query;
}
O ia-core é o framework Java interno usado pelo ia-boi-api. Ele fornece:
NotFoundException: Quando um registro não é encontradoWarningException: Quando há um aviso de negócioCalc: Operações matemáticas com BigDecimalUtils: Utilitários gerais (coalesce, formatação, etc.)Constants: Constantes do sistemaTransactionDB: Representa uma transação no banco de dadosGenericDAO: Classe base para DAOs com métodos CRUDORMMapper: Mapeia resultados SQL para objetos BeanWhereDB: Constrói cláusulas WHERE de forma type-safeOperatorDB: Operadores para WhereDB (EQ, NE, GT, LT, etc.)ia-core é um framework interno e não deve ser editado diretamente. Se precisar de funcionalidades novas, discuta com a equipe antes de modificar.
O N8N é usado para automações e integrações do IA BOI, especialmente para o chatbot via WhatsApp que utiliza inteligência artificial para processar mensagens dos usuários.
O workflow principal "IABoi V2" é responsável por:
1. WhatsApp Trigger → Recebe mensagem
2. Normaliza → Padroniza dados da mensagem
3. Auth → Autentica usuário na API
4. Get Context → Busca contexto do usuário (unidade, onboarding, etc.)
5. Tipo Mensagem → Identifica tipo (texto, áudio, interativa)
6. Processamento:
- Texto: Direto para buffer
- Áudio: Transcreve → Buffer
- Interativa: Processa interação (botões, flows)
7. Agente IA → Processa mensagem e decide qual tool chamar
8. Tools → Executam workflows específicos (manejo, relatório, CRUD, etc.)
9. Resposta → Formata e envia via WhatsApp
10. Histórico → Registra mensagem no banco
button_reply: Respostas de botõesnfm_reply: Respostas de flows (formulários)// Nó: normaliza
- Extrai dados da mensagem do WhatsApp
- Cria systemKey único: "ia_boi_{{ remoteJid }}"
- Normaliza número de telefone, tipo de mensagem, etc.
- Prepara dados para processamento
O sistema usa um buffer no Redis para agrupar múltiplas mensagens enviadas rapidamente:
{{ systemKey }}_bufferO agente de IA é o coração do sistema. Ele utiliza:
{{ systemKey }}_memory (único por usuário)O agente recebe um system message extenso que define:
O prompt de resposta define regras para formatação:
O agente de IA possui acesso a várias ferramentas que são, na verdade, workflows do n8n:
query: Texto do usuárioauthToken: Token de autenticaçãomemoryKey: Chave de memória RedisbaseUrl: URL base da APIusuarioToken: Token do usuáriopromptPadrao: Prompt padrão com regras{{ systemKey }}_memory){{ systemKey }}_buffer){{ systemKey }}_limpa_base)POST /auth - Autentica usuárioGET /dados-base - Busca contexto do usuárioPOST /mensagemhistorico - Registra mensagensPOST /usuario/aceitar-termo-uso - Aceita termosPOST /usuario/tutorial-* - Gerencia tutorialDELETE /usuario/remover-dados - Remove dados do usuárioA memória do agente é gerenciada pelo Redis:
{{ systemKey }}_memoryO buffer agrupa mensagens enviadas rapidamente:
1. Mensagem chega → Adiciona ao buffer Redis (lista)
2. Verifica buffer → Se tem mais de 1 mensagem, aguarda 2 segundos
3. Timer → Após 2 segundos, busca todas as mensagens do buffer
4. Normaliza → Junta todas as mensagens em uma única string
5. Processa → Envia para o agente de IA
6. Limpa → Remove buffer após processamento
auth → qual_erro → parse_erro/parse_erro_externo → envia_mensagem_erro
Mensagem padrão: "🤖 Iaaa boi, estamos com uns probleminhas aqui
para te atender agora, tenta de novo mais tarde"
agente_ia → formata_erro → envia_mensagem
- Se erro contém "Received tool input did not match expected schema":
"⚠️ Ocorreu um erro ao processar sua solicitação.
Por favor, revise os dados e tente novamente."
- Caso contrário: Retorna mensagem de erro padrão
normaliza_interacao → do_action → mensagem_historico_inativar
- Quando usuário cancela uma operação via flow
- Registra no histórico como "cancelar {tipo}"
- Envia mensagem de confirmação
verificacoes → Primeira vez:
- Envia mensagem de boas-vindas
- Envia vídeo tutorial
- Envia flow de termos de uso
verificacoes → Aceitou termo uso:
- Registra aceite
- Continua fluxo normal
aceita_pula_tutorial → aceita_tutorial:
- Envia mensagem: "Já preparei uma base..."
- Registra início do tutorial
- Envia exemplos de perguntas
aceita_pula_tutorial → pula_tutorial:
- Registra que pulou tutorial
- Envia flow de termos de uso
is_flow → Switch → send_flow_whatsapp:
- Detecta se agente chamou agente_manejo ou agente_suplemento
- Envia resposta com flow interativo para cancelar
- Flow: "iaboi_cancelar_manejo" ou "iaboi_cancelar_suplemento"
normaliza_interacao → do_action:
- Processa cancelamento via DELETE na API
- Endpoint: /manejo-inativar ou /suplemento
limpa_base_webhook → has_codigo:
- Se não tem código: Gera código de 6 dígitos
- Armazena no Redis com TTL 300s
- Envia código para usuário
- Se tem código: Busca código no Redis
- Valida código
- Se válido: Limpa dados e memória
- Se inválido: Retorna erro
systemKey único por usuário - Formato: ia_boi_{{ remoteJid }}api.get/post/delete do @dot/coreDotumResponse<DTO> e atualiza UI@ServiceClass
public class AnaliseDetalhadaController extends AbstractController {
@Endpoint(path = "/analise-detalhada", method = "GET")
public List<AnaliseDetalhadaDTO> selectAll(@QueryParam String filtro) throws Exception {
TransactionDB trans = getTransaction();
UserContext userContext = getUserContext();
List<AnaliseDetalhadaDTO> resultado = AnaliseDetalhadaService
.gerarAnaliseDetalhada(trans, userContext, filtro);
return resultado;
}
}
public class AnaliseDetalhadaService {
public static List<AnaliseDetalhadaDTO> gerarAnaliseDetalhada(
TransactionDB trans, UserContext userContext, String filtro) throws Exception {
// Lógica de negócio...
return resultado;
}
}
// services/analise-detalhada/index.ts
export async function listarAnalises(
filtro?: string
): Promise<DotumResponse<AnaliseDetalhadaResponse[]>> {
const res = await api.get<DotumResponse<AnaliseDetalhadaResponse[]>>(
"/analise-detalhada",
{ params: { filtro: filtro || "" } }
);
return res.data;
}
export function AnaliseDetalhadaTable({ filtro }: AnaliseDetalhadaTableProps) {
const table = useDotumTable<AnaliseDetalhadaResponse>({
queryKey: ["analise-detalhada", filtro],
queryFn: async () => {
const res = await listarAnalises(filtro);
return res.data ?? [];
},
});
return <DotumTable.Root {...table}>...</DotumTable.Root>;
}
UserContext para segurança e auditoriaStringBuilder para queries complexas (apenas em DAOs)Record do JavaBean diretamente)any - Sempre tipar corretamenteZod - Não usar apenas interface ou typeDotumTableasync na queryFn do useQueryres.data ?? [] quando apropriado📝 Criação completa da documentação do projeto IA BOI
Seções criadas:
🔧 Padrões estabelecidos:
Nota: Este documento deve ser atualizado conforme o projeto evolui e novos padrões são estabelecidos.