Uma biblioteca simples e fortemente tipada para criar agentes de IA com LangChain e LangGraph.
Inspirada no CrewAI e criada para ser mais descritiva e direta, com padrões bem-definidos para a criação de agentes reutilizáveis.
- Parsero
O Parsero foi desenvolvido para simplificar a criação de agentes de IA utilizando LangChain e LangGraph. Ele oferece uma interface mais intuitiva, fortemente tipada e com padrões claros, permitindo:
- Facilitar a orquestração de procedimentos (Procedures) em agentes de IA.
- Reduzir a complexidade de criação de fluxos e decisões (grafos).
- Reaproveitar a lógica de agentes em diferentes projetos, graças às definições claras de entrada/saída.
- Forte Tipagem: Usa Zod para schemas de entrada/saída, garantindo segurança de tipos.
-
Simplicidade: Os superpoderes do LangGraph mas com interface mais direta, seguindo padrões definidos (Procedures do tipo
action
echeck
). - Extensibilidade: Compatível nativamente com LangChain e LangGraph, para que você possa aproveitar o ecossistema existente.
-
Orquestração por Procedimentos: Define procedures que podem modificar o estado (
action
) ou indicar o próximo passo (check
). - Múltiplos LLMs: Suporte para utilizar diferentes modelos de linguagem em diferentes procedures, otimizando custo e desempenho.
-
Organização Modular: Permite definir procedures fora dos agentes com a utilidade
InferState
, facilitando a organização e reuso do código.
-
LangChain: Pode ser usado junto com qualquer ChatModel disponível (por exemplo,
ChatOpenAI
,ChatGoogleGenerativeAI
, etc.). -
LangGraph: Totalmente integrável ao LangGraph, para que você possa desenhar fluxos de conversação e lógicas mais complexas de maneira visual e tipada. A classe
Agent
fornece acesso ao seu equivalente em grafo.
-
Criação de Agentes de IA Customizados
- Defina procedures específicas para o seu caso.
- Aplique validação de entrada e saída para garantir conformidade dos dados.
-
Fluxos de Decisão com IA
- Utilize CheckProcedure para direcionar o fluxo conforme o conteúdo do estado.
-
Aplicações com Entrada e Saída Bem Definidas
- Perfeito para pipelines de dados, chatbots especializados, ou qualquer agente que precise controlar o estado de forma clara.
-
Orquestração de Múltiplos LLMs
- Cada procedure pode acessar diferentes instâncias de LLM, permitindo combinar modelos especializados.
- Use modelos mais econômicos para tarefas simples e modelos avançados apenas para tarefas complexas.
- Combine diferentes famílias de modelos (OpenAI, Anthropic, Google, etc.) no mesmo agente.
O Parsero permite que você use diferentes modelos de linguagem para diferentes partes do seu agente, otimizando tanto o desempenho quanto os custos.
Ao criar seu agente, você pode fornecer um mapa de modelos em vez de um único modelo:
import { Agent, State } from "@cosmixclub/parsero";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ChatOpenAI } from "@langchain/openai";
const agent = new Agent({
// Outros parâmetros...
llm: {
default: new ChatOpenAI({ model: "gpt-4o" }), // Modelo principal
summarize: new ChatGoogleGenerativeAI({ model: "gemini-pro" }), // Para resumos
classify: new ChatOpenAI({ model: "gpt-4o-mini", temperature: 0 }), // Para classificações
creative: new ChatAnthropic({ model: "claude-3-opus-20240229" }), // Para geração criativa
},
// ...
});
Dentro de suas procedures, você pode acessar o modelo específico que deseja utilizar:
// Em uma ActionProcedure
async run(state, llms) {
// Use o modelo específico para tarefas de classificação
const result = await llms.classify.invoke("Classifique este texto...");
// Use o modelo específico para resumos
const summary = await llms.summarize.invoke("Resuma este conteúdo...");
// Use o modelo padrão
const response = await llms.default.invoke("Responda esta pergunta...");
// Continue seu código...
return state;
}
Ao utilizar agent.graph
para acessar o grafo LangGraph equivalente, a biblioteca automaticamente:
- Usará o modelo "default" se estiver disponível no objeto de LLMs
- Usará o primeiro modelo do objeto se não houver um modelo chamado "default"
Esta abordagem garante compatibilidade com o LangGraph que atualmente espera um único modelo, mas ainda permite que você utilize múltiplos modelos dentro do seu agente Parsero.
const agent = new Agent({
llm: {
default: new ChatOpenAI(), // Este será usado no LangGraph
specialTask: new ChatAnthropic(),
},
// ...
});
// O grafo usará o modelo "default" internamente
const graph = agent.graph;
// Execute o grafo
await graph.invoke({ input: "exemplo" });
Se você estiver usando TypeScript, pode tirar vantagem do sistema de tipos para garantir que suas procedures acessem apenas LLMs que realmente existem:
// Defina o tipo do seu mapa de LLMs
type MyLLMs = {
default: ChatOpenAI;
summarize: ChatGoogleGenerativeAI;
classify: ChatOpenAI;
};
// Use o tipo genérico na sua procedure
const classifyProcedure: ActionProcedure<any, MyLLMs> = {
name: "classify",
type: "action",
async run(state, llms) {
// TypeScript sabe que llms.classify existe e é do tipo ChatOpenAI
const result = await llms.classify.invoke("...");
// ...
},
};
O Parsero fornece a utilidade InferState<>
que permite definir procedures de forma independente e reutilizável, separadas da definição do agente. Isso traz várias vantagens para a organização do código:
InferState<>
é um tipo utilitário que extrai os tipos de entrada e saída de um State
, facilitando a definição de procedures fora do contexto do agente:
import { z } from "zod";
import { Agent, InferState, Procedure, State } from "@cosmixclub/parsero";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
// 1. Defina seu State
const state = new State({
inputSchema: z.object({
query: z.string(),
}),
outputSchema: z.object({
result: z.string(),
}),
});
// 2. Crie procedures reutilizáveis com tipagem correta
const analyzeQuery: Procedure<InferState<typeof state>, BaseChatModel> = {
name: "analyzeQuery",
nextProcedure: "generateResponse",
type: "action",
async run(state, llm) {
// Seu código aqui...
return state;
},
};
const generateResponse: Procedure<InferState<typeof state>, BaseChatModel> = {
name: "generateResponse",
type: "action",
nextProcedure: END,
async run(state, llm) {
// Seu código aqui...
return state;
},
};
// 3. Use as procedures no agente
const agent = new Agent({
state,
llm: new ChatOpenAI(),
procedures: [
analyzeQuery,
generateResponse,
// Outras procedures...
],
});
- Organização de Código: Separe a lógica em arquivos distintos para melhor manutenção.
- Reusabilidade: Reutilize procedures em diferentes agentes.
- Testabilidade: Teste procedures individualmente, facilitando os testes unitários.
- Colaboração: Permite que diferentes membros da equipe trabalhem em diferentes procedures.
Veja como organizar seu código com procedures em arquivos separados:
// state.ts
import { z } from "zod";
import { State } from "@cosmixclub/parsero";
export const numberClassifierState = new State({
inputSchema: z.object({
number: z.number(),
}),
outputSchema: z.object({
class: z.enum(["odd", "even"]),
explanation: z.string(),
}),
});
// procedures/classify.ts
import { Procedure } from "@cosmixclub/parsero";
import { InferState } from "@cosmixclub/parsero";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { z } from "zod";
import { numberClassifierState } from "../state";
export const whatNumberIs: Procedure<InferState<typeof numberClassifierState>, BaseChatModel> = {
name: "whatNumberIs",
nextProcedure: "router",
async run(state, llm) {
const chain = llm.withStructuredOutput(
z.object({
class: z.enum(["odd", "even"]).describe("Se o número é par ou ímpar"),
}),
);
const output = await chain.invoke(`Determine se o número a seguir é par ou ímpar: ${state.input.number}`);
state.output.class = output.class;
return state;
},
type: "action",
};
// procedures/router.ts
import { Procedure } from "@cosmixclub/parsero";
import { InferState } from "@cosmixclub/parsero";
import { numberClassifierState } from "../state";
export const router: Procedure<InferState<typeof numberClassifierState>, any> = {
name: "router",
async run(state) {
const numberClass = state.output.class;
if (numberClass === "odd") return "isOdd";
return "isEven";
},
type: "check",
};
// agent.ts
import { Agent, END } from "@cosmixclub/parsero";
import { ChatOpenAI } from "@langchain/openai";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { numberClassifierState } from "./state";
import { whatNumberIs } from "./procedures/classify";
import { router } from "./procedures/router";
const agent = new Agent({
state: numberClassifierState,
llm: new ChatOpenAI(),
procedures: [
whatNumberIs,
router,
{
name: "isOdd",
nextProcedure: END,
async run(state, llm) {
const chain = llm.pipe(new StringOutputParser());
const output = await chain.invoke(
`Gere uma explicação do motivo de '${state.input.number}' ser ímpar.`,
);
state.output.explanation = output;
return state;
},
type: "action",
},
{
name: "isEven",
nextProcedure: END,
async run(state, llm) {
const chain = llm.pipe(new StringOutputParser());
const output = await chain.invoke(`Gere uma explicação do motivo de '${state.input.number}' ser par.`);
state.output.explanation = output;
return state;
},
type: "action",
},
],
});
Usando essa abordagem, seu código fica mais organizado, modular e fácil de manter, especialmente em projetos maiores com múltiplos agentes e procedures complexas.
No exemplo abaixo, o agente:
- Descobre se o número é par ou ímpar (action).
- Verifica o resultado e decide qual próximo passo a seguir (check).
- Executa a procedure correspondente a par ou ímpar (action).
import { z } from "zod";
import { Agent, END, State } from "@cosmixclub/parsero";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";
// exemplificando, pois depende do seu setup
const agent = new Agent({
state: new State({
inputSchema: z.object({
number: z.number(),
}),
outputSchema: z.object({
class: z.enum(["odd", "even"]),
explanation: z.string(),
}),
}),
llm: new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0.1,
maxTokens: 500,
streaming: true,
cache: true,
apiKey: process.env.OPENAI_API_KEY,
}),
options: {
verbose: true,
},
procedures: [
{
// 1. Procedure que classifica o número como par ou ímpar.
name: "whatNumberIs",
type: "action",
nextProcedure: "router",
async run(state, llm) {
const chain = llm.withStructuredOutput(
z.object({
class: z.enum(["odd", "even"]).describe("Se o número é par ou ímpar"),
}),
);
const output = await chain.invoke(
`Determine se o número a seguir é par ou ímpar: ${state.input.number}`,
);
state.output.class = output.class;
return state;
},
},
{
// 2. Procedure do tipo 'check' que decide qual caminho seguir
name: "router",
type: "check",
async run(state) {
const numberClass = state.output.class;
if (numberClass === "odd") return "isOdd";
return "isEven";
},
},
{
// 3a. Se for ímpar, chama esta procedure.
name: "isOdd",
type: "action",
nextProcedure: END,
async run(state, llm) {
const chain = llm.pipe(new StringOutputParser());
const output = await chain.invoke(
`Gere uma explicação do motivo de '${state.input.number}' ser ímpar.`,
);
state.output.explanation = output;
return state;
},
},
{
// 3b. Se for par, chama esta procedure.
name: "isEven",
type: "action",
nextProcedure: END,
async run(state, llm) {
const chain = llm.pipe(new StringOutputParser());
const output = await chain.invoke(`Gere uma explicação do motivo de '${state.input.number}' ser par.`);
state.output.explanation = output;
return state;
},
},
],
});
// Execução:
const output = await agent.run({ number: 11 });
console.log(output);
// => { class: "odd", explanation: "Explicação sobre por que 11 é ímpar..." }
Observe que uma
CheckProcedure
não altera o estado. Ela apenas retorna o nome da próxima procedure a ser executada.
O exemplo abaixo mostra um fluxo estritamente sequencial, onde cada procedure de tipo action
é executada na ordem em que foi definida. Assim que uma procedure termina, o agente avança para a próxima.
import { z } from "zod";
import { Agent, State } from "@cosmixclub/parsero";
import { ChatOpenAI } from "@langchain/openai";
const agent = new Agent({
state: new State({
inputSchema: z.object({
text: z.string(),
}),
outputSchema: z.object({
uppercase: z.string(),
reversed: z.string(),
}),
}),
llm: new ChatOpenAI({ model: "gpt-4", apiKey: "..." }),
procedures: [
{
// 1. Procedure que converte o texto para maiúsculas.
name: "toUpperCase",
type: "action",
async run(state) {
state.output.uppercase = state.input.text.toUpperCase();
return state;
},
},
{
// 2. Procedure que reverte o texto já convertido.
name: "reverseText",
type: "action",
async run(state) {
state.output.reversed = state.output.uppercase.split("").reverse().join("");
return state;
},
},
],
});
// Ao chamar `agent.run`, ele executa `toUpperCase` e depois `reverseText`.
const output = await agent.run({ text: "parsero" });
console.log(output);
// => { uppercase: "PARSERO", reversed: "ORESRAP" }
Como não há
CheckProcedure
ounextProcedure
, o fluxo é linear, executando cada procedure na ordem em que foi definida na lista.
Caso você queira controlar a ordem entre procedures de modo mais explícito (sem check
), basta utilizar a propriedade nextProcedure
em uma ActionProcedure
.
import { z } from "zod";
import { Agent, END, State } from "@cosmixclub/parsero";
import { ChatOpenAI } from "@langchain/openai";
const agent = new Agent({
state: new State({
inputSchema: z.object({
text: z.string(),
}),
outputSchema: z.object({
processed: z.string(),
summary: z.string(),
}),
}),
llm: new ChatOpenAI({ model: "gpt-4", apiKey: "..." }),
procedures: [
{
// 1. Lê e processa a entrada, definindo `processed`.
name: "processInput",
type: "action",
nextProcedure: "generateSummary",
async run(state, llm) {
// Suponha que faça algum processamento local:
state.output.processed = `Processed: ${state.input.text}`;
return state;
},
},
{
// 2. Gera um resumo do texto processado, definindo `summary`.
name: "generateSummary",
type: "action",
nextProcedure: END,
async run(state, llm) {
const response = await llm.invoke(`Resuma o seguinte texto: "${state.output.processed}"`);
state.output.summary = response;
return state;
},
},
],
});
const output = await agent.run({ text: "Esta é uma frase de teste" });
console.log(output);
// => { processed: "Processed: Esta é uma frase de teste", summary: "..." }
A execução passa explicitamente de
"processInput"
para"generateSummary"
. Em seguida,"generateSummary"
definenextProcedure: END
para indicar o fim.
Este exemplo mostra como utilizar diferentes modelos para diferentes partes do fluxo, otimizando o custo e especialização:
import { z } from "zod";
import { Agent, END, State } from "@cosmixclub/parsero";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ChatOpenAI } from "@langchain/openai";
const agent = new Agent({
state: new State({
inputSchema: z.object({
text: z.string(),
}),
outputSchema: z.object({
classification: z.string(),
summary: z.string(),
response: z.string(),
}),
}),
// Configuração de múltiplos modelos para diferentes funções
llm: {
// Modelo padrão para casos gerais
default: new ChatOpenAI({ model: "gpt-4o" }),
// Modelo especializado e econômico para classificação
classify: new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0, // Temperatura baixa para classificação precisa
}),
// Modelo especializado em resumos
summarize: new ChatGoogleGenerativeAI({
model: "gemini-pro",
temperature: 0.2,
}),
},
procedures: [
{
name: "classifyContent",
type: "action",
nextProcedure: "summarizeContent",
async run(state, llms) {
// Usa o modelo econômico e especializado para classificação
const response = await llms.classify.invoke(
`Classifique o seguinte texto em uma categoria: "${state.input.text}"`,
);
state.output.classification = response.toString().trim();
return state;
},
},
{
name: "summarizeContent",
type: "action",
nextProcedure: "generateFullResponse",
async run(state, llms) {
// Usa o modelo especializado em resumos
const response = await llms.summarize.invoke(
`Resuma o seguinte texto de maneira concisa: "${state.input.text}"`,
);
state.output.summary = response.toString().trim();
return state;
},
},
{
name: "generateFullResponse",
type: "action",
nextProcedure: END,
async run(state, llms) {
// Usa o modelo principal (mais poderoso) para a resposta final
const response = await llms.default.invoke(`
Crie uma resposta detalhada para o texto a seguir, considerando que:
- Ele foi classificado como: ${state.output.classification}
- Um resumo conciso seria: ${state.output.summary}
Texto original: "${state.input.text}"
Sua resposta deve ser completa e considerar tanto a classificação quanto o resumo.
`);
state.output.response = response.toString().trim();
return state;
},
},
],
});
const output = await agent.run({ text: "Um texto longo para análise..." });
console.log(output);
// => {
// classification: "Artigo científico",
// summary: "Este texto aborda...",
// response: "Análise detalhada considerando a classificação e o resumo..."
// }
Neste exemplo, cada procedure usa um modelo diferente otimizado para sua tarefa específica: classificação, resumo e geração de resposta completa.
Este exemplo demonstra como combinar diferentes famílias de modelos de linguagem para aproveitar as vantagens de cada uma:
import { z } from "zod";
import { Agent, END, State } from "@cosmixclub/parsero";
import { ChatAnthropic } from "@langchain/anthropic";
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
import { ChatOpenAI } from "@langchain/openai";
const agent = new Agent({
state: new State({
inputSchema: z.object({
query: z.string(),
context: z.string().optional(),
}),
outputSchema: z.object({
queryType: z.enum(["factual", "creative", "technical"]),
response: z.string(),
sources: z.array(z.string()).optional(),
}),
}),
llm: {
// GPT-4o como modelo padrão
default: new ChatOpenAI({
model: "gpt-4o",
temperature: 0.7,
}),
// Claude para consultas factuais e precisas
factual: new ChatAnthropic({
model: "claude-3-opus-20240229",
temperature: 0.1,
}),
// Gemini para geração de conteúdo criativo
creative: new ChatGoogleGenerativeAI({
model: "gemini-pro",
temperature: 1.0,
}),
// Modelo econômico para roteamento
router: new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
}),
},
procedures: [
// Primeiro determina o tipo de consulta
{
name: "analyzeQuery",
type: "action",
nextProcedure: "routeQuery",
async run(state, llms) {
// Usa o modelo mais econômico para classificação
const result = await llms.router.withStructuredOutput(
z.object({
queryType: z
.enum(["factual", "creative", "technical"])
.describe("O tipo de consulta baseado na natureza da pergunta"),
explanation: z.string().describe("Explicação rápida sobre porque essa categoria foi escolhida"),
}),
).invoke(`
Classifique a seguinte consulta em uma das categorias:
- factual: Busca por informações factuais, precisas e verificáveis
- creative: Busca por conteúdo criativo, ideias, ou explorações conceituais
- technical: Busca por explicações técnicas ou soluções para problemas
Consulta: "${state.input.query}"
`);
state.output.queryType = result.queryType;
return state;
},
},
// Router decide qual modelo usar com base no tipo de consulta
{
name: "routeQuery",
type: "check",
async run(state, llms) {
// Lógica de roteamento baseada no tipo de consulta
switch (state.output.queryType) {
case "factual":
return "processFact";
case "creative":
return "generateCreative";
case "technical":
default:
return "handleTechnical";
}
},
},
// Processa consultas factuais com Claude (alta precisão)
{
name: "processFact",
type: "action",
nextProcedure: END,
async run(state, llms) {
const response = await llms.factual.invoke(`
Responda a seguinte consulta factual com alta precisão.
Forneça fontes ou referências quando possível.
Consulta: ${state.input.query}
${state.input.context ? `Contexto adicional: ${state.input.context}` : ""}
`);
state.output.response = response.toString();
// Em um caso real, você poderia extrair fontes usando structured output
state.output.sources = ["Conhecimento integrado do Claude"];
return state;
},
},
// Gera conteúdo criativo com Gemini
{
name: "generateCreative",
type: "action",
nextProcedure: END,
async run(state, llms) {
const response = await llms.creative.invoke(`
Crie uma resposta criativa e inspiradora para:
${state.input.query}
${state.input.context ? `Considerando este contexto: ${state.input.context}` : ""}
Seja original, imaginativo e expressivo em sua resposta.
`);
state.output.response = response.toString();
return state;
},
},
// Processa consultas técnicas com o modelo padrão (GPT-4o)
{
name: "handleTechnical",
type: "action",
nextProcedure: END,
async run(state, llms) {
const response = await llms.default.invoke(`
Forneça uma resposta técnica detalhada e precisa para:
${state.input.query}
${state.input.context ? `Contexto adicional: ${state.input.context}` : ""}
Inclua exemplos práticos quando relevante.
`);
state.output.response = response.toString();
return state;
},
},
],
});
// Teste com diferentes tipos de consultas
const factualResult = await agent.run({
query: "Qual é a distância média da Terra ao Sol?",
});
// Usará o Claude para esta consulta factual
const creativeResult = await agent.run({
query: "Escreva um poema sobre inteligência artificial e a natureza humana.",
});
// Usará o Gemini para esta consulta criativa
const technicalResult = await agent.run({
query: "Como implementar uma árvore binária de busca em JavaScript?",
});
// Usará o GPT-4o para esta consulta técnica
Este exemplo mostra um agente sofisticado que roteia consultas para diferentes modelos com base no tipo de pergunta, utilizando os pontos fortes de cada modelo.
O Parsero possui integração nativa com o LangSmith, a plataforma de observabilidade da LangChain. Isso permite rastrear, inspecionar e depurar a execução dos agentes, procedures e fluxos de decisão de forma detalhada.
- Toda execução do agente (
agent.run(...)
) é automaticamente rastreada pelo LangSmith, incluindo cada procedure executada, entradas, saídas, erros e metadados. - Você pode customizar o rastreamento de cada procedure usando a propriedade
tracing
:-
label
: nome amigável para exibição na interface do LangSmith. -
runType
: tipo da execução (ex: "llm", "parser", "tool", etc). -
metadata
: metadados extras para facilitar a análise.
-
- O agente também aceita metadados globais via
options.metadata
. - Para visualizar os rastreamentos, basta configurar as variáveis de ambiente do LangSmith (ex:
LANGCHAIN_API_KEY
,LANGCHAIN_PROJECT
, etc).
const agent = new Agent({
// ...outros parâmetros...
procedures: [
{
name: "classify",
type: "action",
tracing: {
label: "Classificação do texto",
runType: "llm",
metadata: { etapa: "classificação" },
},
async run(state, llm) {
// ...
return state;
},
},
// ...outras procedures...
],
options: {
metadata: { projeto: "meu-agente" },
name: "Agente de exemplo",
},
});
await agent.run({ text: "Exemplo" });
// A execução será rastreada e poderá ser inspecionada no painel do LangSmith
Para detalhes sobre configuração, variáveis de ambiente e análise dos rastreamentos, consulte a documentação oficial do LangSmith.
Happy Coding!
Nota: Exemplos e instruções podem variar de acordo com a versão utilizada do Parsero, LangChain e LangGraph. Consulte sempre a documentação oficial para detalhes de configuração e versões compatíveis.