1

Integração com APIs e Modelos

OpenAI SDK — autenticação, mensagens, parâmetros, limites, retry e logging

pip install openai python-dotenv tenacity structlog

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.

Python
llm_client.py — Cliente OpenAI completo com retry e error handling
"""
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}")
Output esperado
# 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.
2

LangChain

O que é, quando usar, Chains, Parsers, RouterChain e Memória

pip install langchain langchain-openai langchain-community chromadb pypdf sentence-transformers

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.

ℹ️
Quando usar LangChain (e quando não usar)

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

Python
langchain_basics.py — PromptTemplate, LLMChain e SequentialChain
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

Python
memory_chat.py — Chatbot com 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}")
3

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.

1
Documento — PDF, página web, banco de dados, CSV
2
Chunking — Divide em fragmentos menores com sobreposição estratégica
3
Embedding — Converte chunks em vetores numéricos (text-embedding-3-small)
4
Vector Store — Armazena vetores (ChromaDB, Pinecone, Weaviate)
↓ Retrieval (por consulta do usuário)
5
Busca Semântica — Encontra chunks mais similares à pergunta
6
Reranking — Reordena por relevância (cross-encoder)
7
Augmented Generation — LLM responde com contexto dos chunks

Implementação RAG com LangChain

Python
rag_pipeline.py — Pipeline RAG completo com PDF e ChromaDB
"""
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'])}")
Output esperado
🔨 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.

📏
Recursive Character Splitter

Divide por parágrafos, depois sentenças, depois palavras. Melhor para texto geral. Use chunk_overlap de 15-20% do chunk_size.

📑
Semantic Chunking

Divide nos pontos de maior mudança semântica. Chunks mais coesos. Melhor para documentos com estrutura irregular. Mais lento.

🏷️
Header-Based Chunking

Para Markdown e documentação estruturada. Usa headers como separadores naturais. Mantém contexto de seção no metadata.

🔢
Token-Based Chunking

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.

💡
Estratégia Two-Stage Retrieval

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.

4

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.

Python
multi_function_app.py — App com roteamento de intenção
"""
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]}...")
5

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.

Python
extraction_classifier.py — Extração estruturada e análise de sentimento
"""
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']]}")
Boas Práticas de Extração

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 "aluci​nar" campos — valide dados críticos com regras de negócio.