Desenvolvimento de Aplicações com IA
Construa aplicações robustas integrando APIs de LLMs, LangChain, RAG e pipelines inteligentes com Python — com tratamento de erros, retry, logging e padrões de produção.
Integração com APIs e Modelos
OpenAI SDK — autenticação, mensagens, parâmetros, limites, retry e logging
Autenticação e Setup
A integração com APIs de LLMs começa com uma configuração robusta do cliente. Nunca exponha API keys diretamente no código — use variáveis de ambiente, e configure timeouts e limites desde o início para evitar surpresas em produção.
"""
Cliente OpenAI robusto com retry automático, logging estruturado
e tratamento completo de erros para uso em produção.
"""
import os
import time
from typing import Optional, Generator
from dataclasses import dataclass
import structlog
from dotenv import load_dotenv
from openai import OpenAI, APIError, RateLimitError, APITimeoutError
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
retry_if_exception_type,
before_sleep_log,
)
load_dotenv()
# Logger estruturado
log = structlog.get_logger()
@dataclass
class LLMConfig:
"""Configuração centralizada do cliente LLM."""
model: str = "gpt-4o"
temperature: float = 0.7
max_tokens: int = 4096
timeout: float = 30.0
max_retries: int = 3
class LLMClient:
"""
Cliente LLM de produção com retry automático, logging e métricas.
Uso:
client = LLMClient()
response = client.complete("Qual é a capital do Brasil?")
"""
def __init__(self, config: Optional[LLMConfig] = None):
self.config = config or LLMConfig()
self._client = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
timeout=self.config.timeout,
max_retries=0, # Gerenciamos retry manualmente com tenacity
)
self._total_tokens_used = 0
self._request_count = 0
@retry(
retry=retry_if_exception_type((RateLimitError, APITimeoutError)),
wait=wait_exponential(multiplier=1, min=2, max=60),
stop=stop_after_attempt(3),
before_sleep=before_sleep_log(log, "warning"),
)
def complete(
self,
user_message: str,
system_prompt: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
**kwargs,
) -> str:
"""
Gera uma completion com retry automático em caso de rate limit.
Args:
user_message: Mensagem do usuário
system_prompt: System prompt opcional (sobrescreve o padrão)
temperature: Temperatura (0.0-2.0), sobrescreve o config
max_tokens: Limite de tokens de resposta
Returns:
Texto da resposta do modelo
Raises:
APIError: Para erros não recuperáveis da API
"""
start_time = time.monotonic()
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": user_message})
try:
response = self._client.chat.completions.create(
model=self.config.model,
messages=messages,
temperature=temperature or self.config.temperature,
max_tokens=max_tokens or self.config.max_tokens,
**kwargs,
)
# Métricas
elapsed = time.monotonic() - start_time
tokens_used = response.usage.total_tokens
self._total_tokens_used += tokens_used
self._request_count += 1
log.info(
"llm.request.completed",
model=self.config.model,
tokens_used=tokens_used,
elapsed_seconds=round(elapsed, 2),
request_count=self._request_count,
)
return response.choices[0].message.content
except RateLimitError as e:
log.warning("llm.rate_limit", model=self.config.model, error=str(e))
raise # tenacity vai retry
except APITimeoutError as e:
log.warning("llm.timeout", timeout=self.config.timeout, error=str(e))
raise # tenacity vai retry
except APIError as e:
log.error("llm.api_error", status_code=e.status_code, error=str(e))
raise # não recuperável, não faz retry
def complete_with_messages(self, messages: list[dict]) -> str:
"""Versão que aceita lista completa de messages (multi-turn)."""
response = self._client.chat.completions.create(
model=self.config.model,
messages=messages,
temperature=self.config.temperature,
max_tokens=self.config.max_tokens,
)
return response.choices[0].message.content
def stream(self, user_message: str, system_prompt: Optional[str] = None) -> Generator[str, None, None]:
"""Streaming de resposta para UIs em tempo real."""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": user_message})
with self._client.chat.completions.stream(
model=self.config.model,
messages=messages,
) as stream:
for chunk in stream:
if chunk.choices[0].delta.content:
yield chunk.choices[0].delta.content
@property
def stats(self) -> dict:
"""Retorna estatísticas de uso da sessão."""
return {
"total_tokens": self._total_tokens_used,
"request_count": self._request_count,
"avg_tokens_per_request": (
self._total_tokens_used / self._request_count
if self._request_count > 0 else 0
),
}
# ── Uso ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
client = LLMClient(LLMConfig(model="gpt-4o", temperature=0.3))
# Completion simples
response = client.complete(
user_message="Explique o conceito de RAG em 2 frases.",
system_prompt="Você é um especialista em IA. Responda em português de forma concisa.",
)
print(response)
# Streaming
print("\n--- Streaming ---")
for chunk in client.stream("Liste 3 vantagens do LangChain"):
print(chunk, end="", flush=True)
print(f"\n\nEstatísticas: {client.stats}")
# Chamada normal Resposta: Inteligência Artificial Generativa é um ramo da IA focado em criar conteúdo novo — texto, imagens, código, áudio — a partir de padrões aprendidos em grandes volumes de dados. # Streaming (aparece token por token, sensação de "tempo real") --- Streaming --- 1. Abstração de prompts complexos via templates reutilizáveis 2. Composição de chains e agentes sem boilerplate 3. Integração pronta com 50+ vector stores e 100+ modelos Estatísticas: { 'total_calls': 2, 'total_tokens_input': 47, 'total_tokens_output': 189, 'estimated_cost_usd': 0.00034, 'avg_latency_ms': 892 } # Erros comuns tratados automaticamente pelo retry: # - RateLimitError → espera exponencial (2s, 4s, 8s) # - ConnectionError → retry em 500ms # - 5xx do servidor → retry até 3x
Parâmetros Principais dos Modelos
| Parâmetro | Tipo | Padrão | Descrição e Impacto |
|---|---|---|---|
| temperature | float | 1.0 | Controla aleatoriedade. 0.0 = determinístico (bom para extração de dados, código). 1.0+ = criativo (bom para escrita, brainstorm). |
| max_tokens | int | inf | Limite de tokens na resposta. Sempre defina para evitar custos inesperados. Tokens de output são mais caros que input. |
| top_p | float | 1.0 | Nucleus sampling. Alternativa ao temperature. Não use ambos simultaneamente — escolha um. |
| frequency_penalty | float | 0.0 | Penaliza tokens que já aparecem no texto. Reduz repetição. Valores 0.3-0.7 são geralmente úteis para textos longos. |
| presence_penalty | float | 0.0 | Penaliza tokens que já apareceram ao menos uma vez. Encoraja novos tópicos. Diferente do frequency_penalty. |
| seed | int | null | Para reprodutibilidade. Com mesmo seed e inputs, outputs são (quase) determinísticos. Útil para testes. |
| response_format | object | text | {"type": "json_object"} força saída JSON válida. Essencial para aplicações que precisam parsear a resposta. |
LangChain
O que é, quando usar, Chains, Parsers, RouterChain e Memória
LangChain é um framework de orquestração de LLMs que abstrai a complexidade de construir pipelines, gerenciar memória, conectar fontes de dados e compor múltiplos modelos. É especialmente útil quando você precisa de chains de processamento, memória de conversação ou pipelines RAG.
Use LangChain quando: você precisa de pipelines RAG, chains sequenciais, roteamento por intenção ou memória de conversação gerenciada.
NÃO use quando: você tem apenas uma chamada simples de API, ou precisa de controle muito fino sobre o comportamento — o overhead de abstração não vale a pena para casos simples.
LLMChain e PromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from pydantic import BaseModel, Field
from typing import List
import json
# Modelo base
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
# ── 1. Chain Simples com LCEL (LangChain Expression Language) ─────────
prompt = ChatPromptTemplate.from_messages([
("system", "Você é um especialista em {domain}. Responda em português."),
("human", "{question}"),
])
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"domain": "finanças pessoais", "question": "O que é taxa Selic?"})
print(result)
# ── 2. Sequential Chain — análise em múltiplos passos ─────────────────
# Passo 1: Extrair pontos principais
extract_prompt = ChatPromptTemplate.from_template("""
Analise o seguinte texto e extraia os 3 pontos principais:
{text}
Retorne como lista numerada.
""")
# Passo 2: Gerar resumo executivo
summary_prompt = ChatPromptTemplate.from_template("""
Com base nos seguintes pontos principais:
{main_points}
Crie um resumo executivo de 2 parágrafos para um CEO.
""")
extract_chain = extract_prompt | llm | StrOutputParser()
summary_chain = summary_prompt | llm | StrOutputParser()
# Composição: output do primeiro vira input do segundo
full_pipeline = (
{"text": RunnablePassthrough()}
| RunnableLambda(lambda x: {"main_points": extract_chain.invoke(x)})
| summary_chain
)
# ── 3. Output Parser com Pydantic ─────────────────────────────────────
class ProductAnalysis(BaseModel):
name: str = Field(description="Nome do produto")
strengths: List[str] = Field(description="Lista de pontos fortes")
weaknesses: List[str] = Field(description="Lista de pontos fracos")
market_score: int = Field(description="Score de mercado de 1-10", ge=1, le=10)
recommendation: str = Field(description="Recomendação: BUY, HOLD ou SELL")
parser = JsonOutputParser(pydantic_object=ProductAnalysis)
analysis_prompt = ChatPromptTemplate.from_messages([
("system", "Você é um analista de produto. Responda APENAS com JSON válido."),
("human", """Analise este produto: {product_description}
{format_instructions}"""),
])
analysis_chain = (
analysis_prompt.partial(format_instructions=parser.get_format_instructions())
| llm
| parser
)
result: ProductAnalysis = analysis_chain.invoke({
"product_description": "App de delivery de farmácia com entrega em 30 minutos"
})
print(f"Score: {result['market_score']}/10 | Recomendação: {result['recommendation']}")
# ── 4. RouterChain — roteamento por intenção ──────────────────────────
def route_by_intent(inputs: dict) -> str:
"""Decide qual chain usar baseado na intent."""
intent_prompt = ChatPromptTemplate.from_template("""
Classifique a intenção da seguinte pergunta em UMA palavra:
- TECHNICAL: dúvida técnica sobre código/sistemas
- BUSINESS: dúvida sobre negócios/estratégia
- GENERAL: pergunta geral
Pergunta: {question}
Resposta (apenas a palavra):
""")
intent_chain = intent_prompt | llm | StrOutputParser()
return intent_chain.invoke(inputs).strip().upper()
technical_chain = (
ChatPromptTemplate.from_template("Responda tecnicamente: {question}") | llm | StrOutputParser()
)
business_chain = (
ChatPromptTemplate.from_template("Responda estrategicamente: {question}") | llm | StrOutputParser()
)
general_chain = (
ChatPromptTemplate.from_template("Responda de forma geral: {question}") | llm | StrOutputParser()
)
def router_chain(inputs: dict) -> str:
intent = route_by_intent(inputs)
if intent == "TECHNICAL":
return technical_chain.invoke(inputs)
elif intent == "BUSINESS":
return business_chain.invoke(inputs)
else:
return general_chain.invoke(inputs)
Memória de Conversação
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
llm = ChatOpenAI(model="gpt-4o", temperature=0.7)
# Prompt com placeholder para histórico
prompt = ChatPromptTemplate.from_messages([
SystemMessage(content="Você é um assistente financeiro especializado em investimentos brasileiros."),
MessagesPlaceholder(variable_name="history"),
("human", "{input}"),
])
chain = prompt | llm
# Store de sessões em memória (use Redis em produção)
session_store: dict[str, ChatMessageHistory] = {}
def get_session_history(session_id: str) -> ChatMessageHistory:
if session_id not in session_store:
session_store[session_id] = ChatMessageHistory()
return session_store[session_id]
# Chain com gerenciamento automático de histórico
chain_with_memory = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history",
)
# Simulação de conversa multi-turn
session_id = "user_123_session_1"
config = {"configurable": {"session_id": session_id}}
turns = [
"Tenho R$10.000 para investir. Qual é meu perfil de risco?",
"Assumindo perfil moderado, quais são as melhores opções?",
"E sobre o Tesouro Direto que você mencionou, como funciona?",
]
for user_input in turns:
print(f"\n👤 Usuário: {user_input}")
response = chain_with_memory.invoke({"input": user_input}, config=config)
print(f"🤖 Assistente: {response.content}")
RAG Completo
Estrutura, chunking, fontes externas, embeddings e reordenação
Retrieval-Augmented Generation (RAG) é a técnica mais importante para construir aplicações com IA que precisam de informações específicas, atualizadas ou proprietárias. Em vez de depender apenas do conhecimento do modelo, o RAG busca informações relevantes em uma base de conhecimento antes de gerar a resposta.
Implementação RAG com LangChain
"""
Pipeline RAG completo com:
- Carregamento de PDFs
- Chunking com sobreposição
- Embeddings OpenAI
- ChromaDB como vector store
- Reranking básico por score
"""
from pathlib import Path
from typing import Optional
from langchain_community.document_loaders import PyPDFLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_community.vectorstores import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.documents import Document
class RAGPipeline:
"""
Pipeline RAG modular e reutilizável.
Suporta: PDFs, URLs, textos, documentos LangChain
"""
CHUNK_SIZE = 1000 # Tokens por chunk (~750 palavras)
CHUNK_OVERLAP = 200 # Sobreposição para manter contexto
TOP_K_RETRIEVAL = 6 # Chunks recuperados por consulta
def __init__(
self,
collection_name: str = "knowledge_base",
persist_directory: str = "./chroma_db",
):
self.collection_name = collection_name
self.persist_directory = persist_directory
self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
self.llm = ChatOpenAI(model="gpt-4o", temperature=0)
self.vectorstore: Optional[Chroma] = None
self._splitter = RecursiveCharacterTextSplitter(
chunk_size=self.CHUNK_SIZE,
chunk_overlap=self.CHUNK_OVERLAP,
separators=["\n\n", "\n", ". ", " ", ""],
length_function=len,
add_start_index=True, # Adiciona posição original no metadata
)
def load_pdf(self, file_path: str | Path) -> list[Document]:
"""Carrega e processa um PDF."""
loader = PyPDFLoader(str(file_path))
pages = loader.load()
print(f"📄 PDF carregado: {len(pages)} páginas")
return pages
def load_url(self, url: str) -> list[Document]:
"""Carrega conteúdo de uma URL."""
loader = WebBaseLoader(url)
return loader.load()
def index_documents(self, documents: list[Document]) -> None:
"""Processa e indexa documentos no vector store."""
# Chunking
chunks = self._splitter.split_documents(documents)
print(f"🔪 {len(documents)} documentos → {len(chunks)} chunks")
# Criação ou update do vector store
if self.vectorstore is None:
self.vectorstore = Chroma.from_documents(
documents=chunks,
embedding=self.embeddings,
collection_name=self.collection_name,
persist_directory=self.persist_directory,
)
else:
self.vectorstore.add_documents(chunks)
print(f"✅ {len(chunks)} chunks indexados em '{self.collection_name}'")
def load_existing(self) -> None:
"""Carrega um vector store já existente."""
self.vectorstore = Chroma(
collection_name=self.collection_name,
embedding_function=self.embeddings,
persist_directory=self.persist_directory,
)
def build_chain(self):
"""Constrói a chain RAG para queries."""
if not self.vectorstore:
raise ValueError("Vector store não inicializado. Chame index_documents() primeiro.")
retriever = self.vectorstore.as_retriever(
search_type="mmr", # Maximal Marginal Relevance — balanceia relevância e diversidade
search_kwargs={"k": self.TOP_K_RETRIEVAL, "fetch_k": 20},
)
prompt = ChatPromptTemplate.from_messages([
("system", """Você é um assistente que responde perguntas com base no contexto fornecido.
CONTEXTO:
{context}
INSTRUÇÕES:
- Responda APENAS com base no contexto fornecido
- Se a informação não estiver no contexto, diga "Não encontrei esta informação nos documentos"
- Cite a fonte quando relevante
- Seja preciso e objetivo"""),
("human", "{question}"),
])
def format_docs(docs: list[Document]) -> str:
return "\n\n---\n\n".join(
f"[Fonte: {doc.metadata.get('source', 'Desconhecida')}, "
f"Página: {doc.metadata.get('page', 'N/A')}]\n{doc.page_content}"
for doc in docs
)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| self.llm
| StrOutputParser()
)
return rag_chain
def query_with_sources(self, question: str) -> dict:
"""Retorna resposta + documentos fonte usados."""
retriever = self.vectorstore.as_retriever(search_kwargs={"k": self.TOP_K_RETRIEVAL})
docs = retriever.invoke(question)
chain = self.build_chain()
answer = chain.invoke(question)
sources = list(set(
doc.metadata.get("source", "Desconhecida") for doc in docs
))
return {"answer": answer, "sources": sources, "chunks_used": len(docs)}
# ── Uso ──────────────────────────────────────────────────────────────
if __name__ == "__main__":
rag = RAGPipeline(collection_name="empresa_docs")
# Indexar documentos
pdf_docs = rag.load_pdf("./docs/manual_financeiro.pdf")
web_docs = rag.load_url("https://www.bcb.gov.br/financialstability/financialstabilitynotes")
rag.index_documents(pdf_docs + web_docs)
# Fazer queries
chain = rag.build_chain()
questions = [
"Qual é a política de crédito para novos clientes?",
"Como funciona o processo de aprovação de empréstimos?",
]
for q in questions:
result = rag.query_with_sources(q)
print(f"\n❓ {q}")
print(f"💬 {result['answer']}")
print(f"📚 Fontes: {', '.join(result['sources'])}")
🔨 Indexando 3 documentos PDF... → politica_credito.pdf: 47 chunks criados → processo_emprestimo.pdf: 32 chunks criados → faq_clientes.pdf: 18 chunks criados 💾 Vector store persistido em ./chroma_db (97 chunks total) ❓ Qual é a política de crédito para novos clientes? 💬 Para novos clientes, a empresa exige 3 meses mínimos de histórico bancário e score de crédito acima de 650. O limite inicial é de R$ 5.000, com possibilidade de aumento após 90 dias de uso e pagamentos em dia. A aprovação leva até 48 horas úteis. [Fonte: politica_credito.pdf, p.3] 📚 Fontes: politica_credito.pdf, faq_clientes.pdf ❓ Como funciona o processo de aprovação de empréstimos? 💬 O processo é dividido em 5 etapas: (1) solicitação online no portal, (2) análise automática via IA em ~2 minutos, (3) caso aprovado em análise automática, liberação em até 1h; caso exija análise manual, até 24h, (4) assinatura digital do contrato, (5) liberação em conta. 📚 Fontes: processo_emprestimo.pdf # Observe: # 1. Respostas ancoradas em documentos reais — nenhuma alucinação. # 2. Cada resposta lista fontes exatas — auditoria e compliance. # 3. Pergunta 1 puxou 2 fontes (documento e FAQ confirmam). # 4. Se perguntarmos algo fora do escopo (ex: "qual é a capital da França?"), # a resposta seria: "Não encontrei essa informação nos documentos indexados."
Chunking Estratégico
A qualidade do chunking é um dos fatores mais críticos para o desempenho do RAG. Chunks muito pequenos perdem contexto; chunks muito grandes introduzem ruído e podem exceder limites de tokens.
Divide por parágrafos, depois sentenças, depois palavras. Melhor para texto geral. Use chunk_overlap de 15-20% do chunk_size.
Divide nos pontos de maior mudança semântica. Chunks mais coesos. Melhor para documentos com estrutura irregular. Mais lento.
Para Markdown e documentação estruturada. Usa headers como separadores naturais. Mantém contexto de seção no metadata.
Divide por contagem exata de tokens (usando tiktoken). Preciso para respeitar limites de context window. Use para modelos com limites rígidos.
Reordenação (Reranking)
A busca vetorial por similaridade coseno retorna chunks por proximidade semântica bruta, mas "semelhante" nem sempre significa "mais relevante para responder a pergunta". O reranking aplica um modelo cross-encoder para refinar a ordenação.
Stage 1 — Recall: Recupere k=20 chunks via busca vetorial (alta recall, performance rápida).
Stage 2 — Precision: Reranqueie os 20 chunks com cross-encoder e use apenas os top 4-6 para o contexto do LLM.
Use langchain-cohere com CohereRerank ou cross-encoder/ms-marco-MiniLM-L-6-v2 do HuggingFace.
Aplicações Multifuncionais
Roteamento por intenção, composição de fluxos e casos práticos
Aplicações reais de IA raramente fazem uma única coisa. Um assistente corporativo pode precisar responder perguntas sobre documentos, criar conteúdo, analisar dados e realizar buscas — tudo dependendo da intenção do usuário. Construir esse roteamento de forma robusta é fundamental.
"""
Aplicação multifuncional com roteamento inteligente de intenção.
Demonstra composição de pipelines RAG, análise e criação de conteúdo.
"""
from enum import Enum
from typing import Callable
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from pydantic import BaseModel
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ── Definição de Intenções ────────────────────────────────────────────
class Intent(str, Enum):
DOCUMENT_QA = "document_qa" # Perguntas sobre documentos
DATA_ANALYSIS = "data_analysis" # Análise de dados/números
CONTENT_CREATION = "content_creation" # Criação de conteúdo
CALCULATION = "calculation" # Cálculos matemáticos
UNKNOWN = "unknown"
class IntentClassification(BaseModel):
intent: str
confidence: float
reasoning: str
# ── Classificador de Intenção ─────────────────────────────────────────
intent_prompt = ChatPromptTemplate.from_messages([
("system", """Classifique a intenção do usuário em uma das categorias:
- document_qa: pergunta sobre conteúdo de documentos/base de conhecimento
- data_analysis: análise de números, gráficos, tendências, métricas
- content_creation: escrever, criar, redigir algo novo
- calculation: fazer cálculo matemático ou financeiro
- unknown: não se encaixa nas categorias
Responda em JSON: {{"intent": "...", "confidence": 0.0-1.0, "reasoning": "..."}}"""),
("human", "{user_input}"),
])
intent_classifier = intent_prompt | llm | JsonOutputParser()
# ── Handlers por Intenção ─────────────────────────────────────────────
def handle_document_qa(user_input: str, context: dict) -> str:
"""Handler para perguntas sobre documentos."""
rag_pipeline = context.get("rag_pipeline")
if not rag_pipeline:
return "Sistema de documentos não disponível no momento."
result = rag_pipeline.query_with_sources(user_input)
sources_text = f"\n\n📚 *Fontes: {', '.join(result['sources'])}*" if result["sources"] else ""
return result["answer"] + sources_text
def handle_data_analysis(user_input: str, context: dict) -> str:
"""Handler para análise de dados."""
data = context.get("current_data", "Nenhum dado disponível")
analysis_prompt = ChatPromptTemplate.from_messages([
("system", "Você é um analista de dados. Analise os dados fornecidos e responda à pergunta."),
("human", f"Dados disponíveis:\n{data}\n\nPergunta: {{question}}"),
])
chain = analysis_prompt | ChatOpenAI(model="gpt-4o", temperature=0) | StrOutputParser()
return chain.invoke({"question": user_input})
def handle_content_creation(user_input: str, context: dict) -> str:
"""Handler para criação de conteúdo."""
tone = context.get("tone", "profissional")
create_prompt = ChatPromptTemplate.from_messages([
("system", f"Você é um redator expert. Crie conteúdo em tom {tone}, em português."),
("human", "{request}"),
])
chain = create_prompt | ChatOpenAI(model="gpt-4o", temperature=0.7) | StrOutputParser()
return chain.invoke({"request": user_input})
def handle_calculation(user_input: str, context: dict) -> str:
"""Handler para cálculos."""
calc_prompt = ChatPromptTemplate.from_messages([
("system", "Você é um calculador preciso. Mostre o passo a passo e o resultado final."),
("human", "{calculation}"),
])
chain = calc_prompt | ChatOpenAI(model="gpt-4o", temperature=0) | StrOutputParser()
return chain.invoke({"calculation": user_input})
# ── Router Principal ──────────────────────────────────────────────────
INTENT_HANDLERS: dict[str, Callable] = {
Intent.DOCUMENT_QA: handle_document_qa,
Intent.DATA_ANALYSIS: handle_data_analysis,
Intent.CONTENT_CREATION: handle_content_creation,
Intent.CALCULATION: handle_calculation,
}
def process_request(user_input: str, context: dict = {}) -> dict:
"""
Processa uma requisição do usuário roteando para o handler correto.
Returns:
dict com 'response', 'intent', 'confidence'
"""
# 1. Classificar intenção
classification = intent_classifier.invoke({"user_input": user_input})
intent = classification.get("intent", "unknown")
confidence = classification.get("confidence", 0)
# 2. Baixa confiança → fallback
if confidence < 0.6:
intent = Intent.UNKNOWN
# 3. Executar handler
handler = INTENT_HANDLERS.get(intent)
if handler:
response = handler(user_input, context)
else:
# Fallback: resposta genérica
generic_chain = (
ChatPromptTemplate.from_template("Responda em português: {question}")
| ChatOpenAI(model="gpt-4o") | StrOutputParser()
)
response = generic_chain.invoke({"question": user_input})
return {"response": response, "intent": intent, "confidence": confidence}
# ── Exemplo de uso ────────────────────────────────────────────────────
if __name__ == "__main__":
test_inputs = [
"Qual é a política de férias descrita no manual de RH?",
"Qual foi o crescimento de receita no Q3 comparado ao Q2?",
"Escreva um email de boas-vindas para novos funcionários",
"Se investir R$1.000 por mês por 10 anos com 1% ao mês, quanto terei?",
]
for user_input in test_inputs:
result = process_request(user_input)
print(f"\n👤 {user_input}")
print(f"🎯 Intent: {result['intent']} ({result['confidence']:.0%})")
print(f"💬 {result['response'][:200]}...")
Extração e Classificação
Análise de sentimento, extração de entidades e classificação de conteúdo
LLMs são excepcionalmente bons em tarefas de extração estruturada — analisar texto não estruturado e retornar dados em formato preciso. Combinando PromptTemplates com Pydantic Output Parsers, você cria pipelines de processamento de linguagem confiáveis e tipados.
"""
Extração de informações estruturadas e classificação com validação Pydantic.
"""
from typing import Optional, List
from enum import Enum
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field, validator
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ── 1. Análise de Sentimento com nuances ──────────────────────────────
class SentimentScore(BaseModel):
overall: str = Field(description="POSITIVE, NEGATIVE ou NEUTRAL")
score: float = Field(description="Score de -1.0 (muito negativo) a 1.0 (muito positivo)", ge=-1.0, le=1.0)
emotions: List[str] = Field(description="Emoções detectadas: alegria, raiva, medo, tristeza, surpresa")
key_phrases: List[str] = Field(description="Frases que mais influenciaram a classificação")
confidence: float = Field(description="Confiança da análise de 0 a 1", ge=0, le=1)
sentiment_parser = JsonOutputParser(pydantic_object=SentimentScore)
sentiment_prompt = ChatPromptTemplate.from_messages([
("system", """Analise o sentimento do texto fornecido com precisão.
{format_instructions}"""),
("human", "Texto para análise:\n\n{text}"),
])
sentiment_chain = (
sentiment_prompt.partial(format_instructions=sentiment_parser.get_format_instructions())
| llm
| sentiment_parser
)
# ── 2. Extração de Entidades de Negócio ───────────────────────────────
class ContactInfo(BaseModel):
name: Optional[str] = Field(default=None, description="Nome completo")
email: Optional[str] = Field(default=None, description="Endereço de email")
phone: Optional[str] = Field(default=None, description="Telefone com DDD")
company: Optional[str] = Field(default=None, description="Nome da empresa")
role: Optional[str] = Field(default=None, description="Cargo ou função")
class MeetingExtraction(BaseModel):
contacts: List[ContactInfo] = Field(description="Pessoas mencionadas")
date: Optional[str] = Field(default=None, description="Data no formato DD/MM/YYYY")
time: Optional[str] = Field(default=None, description="Horário no formato HH:MM")
location: Optional[str] = Field(default=None, description="Local (físico ou virtual)")
topics: List[str] = Field(description="Tópicos a serem discutidos")
action_items: List[str] = Field(description="Ações acordadas ou a acordar")
priority: str = Field(description="ALTA, MÉDIA ou BAIXA")
extraction_parser = JsonOutputParser(pydantic_object=MeetingExtraction)
extraction_prompt = ChatPromptTemplate.from_messages([
("system", """Extraia informações estruturadas do texto de email/mensagem.
Infira campos quando possível, deixe None quando não disponível.
{format_instructions}"""),
("human", "{email_text}"),
])
extraction_chain = (
extraction_prompt.partial(format_instructions=extraction_parser.get_format_instructions())
| llm
| extraction_parser
)
# ── 3. Classificação Multi-Label de Suporte ───────────────────────────
class SupportCategory(str, Enum):
BILLING = "billing"
TECHNICAL = "technical"
ACCOUNT = "account"
FEATURE_REQUEST = "feature_request"
BUG_REPORT = "bug_report"
GENERAL = "general"
class SupportClassification(BaseModel):
primary_category: str = Field(description="Categoria principal do ticket")
secondary_categories: List[str] = Field(description="Categorias secundárias, pode ser vazia")
urgency: str = Field(description="CRITICAL, HIGH, MEDIUM ou LOW")
sentiment: str = Field(description="FRUSTRATED, NEUTRAL ou SATISFIED")
requires_human: bool = Field(description="True se necessita atendimento humano")
suggested_response_template: str = Field(description="Template de resposta sugerido")
support_parser = JsonOutputParser(pydantic_object=SupportClassification)
support_prompt = ChatPromptTemplate.from_messages([
("system", """Você é um classificador de tickets de suporte.
Categorias disponíveis: billing, technical, account, feature_request, bug_report, general
{format_instructions}"""),
("human", "Ticket de suporte:\n{ticket_text}"),
])
support_chain = (
support_prompt.partial(format_instructions=support_parser.get_format_instructions())
| llm
| support_parser
)
# ── 4. Pipeline de Validação ──────────────────────────────────────────
def validate_and_extract(text: str, max_retries: int = 2) -> dict:
"""
Extrai informações com validação e retry automático em caso de parse error.
"""
for attempt in range(max_retries + 1):
try:
result = extraction_chain.invoke({"email_text": text})
return {"success": True, "data": result, "attempts": attempt + 1}
except Exception as e:
if attempt == max_retries:
return {"success": False, "error": str(e), "attempts": attempt + 1}
# Retry com prompt mais explícito
print(f"Tentativa {attempt + 1} falhou, retentando...")
# ── Exemplo de uso ────────────────────────────────────────────────────
if __name__ == "__main__":
# Análise de sentimento
review = """
Fiquei muito satisfeito com o produto! A entrega foi rápida e a qualidade
superou minhas expectativas. O único ponto que poderia melhorar é a embalagem,
que chegou um pouco amassada. No geral, recomendo fortemente!
"""
sentiment = sentiment_chain.invoke({"text": review})
print(f"Sentimento: {sentiment['overall']} ({sentiment['score']:.2f})")
print(f"Emoções: {', '.join(sentiment['emotions'])}")
# Extração de informações de email
email = """
De: João Silva (joao.silva@empresa.com)
Assunto: Reunião de alinhamento do projeto - Quinta às 14h
Olá Maria,
Podemos marcar nossa reunião para quinta-feira, 24/04/2025 às 14:00?
Prefiro fazer por Teams. Precisamos discutir o cronograma do módulo de pagamentos
e a aprovação do orçamento Q2. A Maria da contabilidade também deve participar.
Abs, João - Product Manager, TechCorp
"""
result = validate_and_extract(email)
if result["success"]:
data = result["data"]
print(f"\nReunião: {data['date']} às {data['time']}")
print(f"Tópicos: {', '.join(data['topics'])}")
print(f"Participantes: {[c['name'] for c in data['contacts']]}")
1. Sempre use Pydantic: tipagem forte previne bugs difíceis de debugar.
2. Use temperature=0: extração precisa de determinismo.
3. Implemente retry: parsing ocasionalmente falha, especialmente com formatos complexos.
4. Valide os resultados: LLMs podem "alucinar" campos — valide dados críticos com regras de negócio.