Files
crewAI/docs/pt-BR/guides/flows/conversational-flows.mdx

476 lines
22 KiB
Plaintext

---
title: Flows Conversacionais
description: Crie apps de chat multi-turno com kickoff por turno, histórico de mensagens, roteamento de intenção, tracing e pontes WebSocket.
icon: comments
mode: "wide"
---
## Visão geral
Apps conversacionais tratam cada linha do usuário como uma **nova execução do flow** com o **mesmo id de sessão**. A CrewAI oferece helpers para histórico de mensagens, classificação opcional de intenção, tracing adiado, pontes para UI e um REPL local `flow.chat()` para flows conversacionais.
| Conceito | Implementação |
|---------|----------------|
| Id de sessão | `handle_turn(..., session_id=...)` → `kickoff(inputs={"id": ...})` → `state.id` |
| Linha do usuário | `handle_turn(message)` acrescenta em `state.messages` antes do grafo rodar |
| Fim do turno | `FlowFinished` só para **esta execução**; o chat segue no próximo `handle_turn` |
| Trace da sessão | `ConversationConfig(defer_trace_finalization=True)` + `finalize_session_traces()` |
## APIs de turno
Use **`flow.handle_turn(message, session_id=...)`** para cada mensagem de usuário em REST, WebSocket, testes e UIs customizadas. Use **`flow.chat()`** quando quiser um loop de chat local no terminal para um `Flow` conversacional.
`Flow.kickoff()` não aceita os argumentos nomeados `user_message=` ou `session_id=`. Para flows conversacionais, `handle_turn()` guarda a mensagem pendente e chama `kickoff(inputs={"id": session_id})` internamente.
| API | Uso |
|-----|-----|
| `handle_turn(message, session_id=...)` | Wrapper ergonômico de um turno para `Flow` conversacional |
| `chat()` | REPL local no terminal para `Flow` conversacional |
| `kickoff(inputs={...})` | Execução avançada do flow sem tratamento de turno conversacional |
| `ask()` | Prompt bloqueante **dentro** de um passo (wizard, esclarecimento) |
| `@human_feedback` | Aprovar/rejeitar **saída de um passo** — não a próxima linha do chat |
| `ChatSession.handle_turn(...)` | Camada de transporte sobre `handle_turn` (SSE / WebSocket) |
## Início rápido
```python
from uuid import uuid4
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
ConversationConfig,
ConversationState,
)
@ConversationConfig(defer_trace_finalization=True)
class SupportFlow(Flow[ConversationState]):
conversational = True
def route_turn(self, context):
message = (self.state.current_user_message or "").lower()
if "pedido" in message or "order" in message:
return "order"
if "tchau" in message or "goodbye" in message:
return "goodbye"
return "help"
@listen("order")
def handle_order(self):
reply = "Seu pedido está a caminho."
self.append_assistant_message(reply)
return reply
@listen("help")
def handle_help(self):
reply = "Como posso ajudar?"
self.append_assistant_message(reply)
return reply
@listen("goodbye")
def handle_goodbye(self):
reply = "Até logo!"
self.append_assistant_message(reply)
return reply
session_id = str(uuid4())
flow = SupportFlow()
try:
flow.handle_turn("Onde está meu pedido?", session_id=session_id)
flow.handle_turn("E as devoluções?", session_id=session_id)
finally:
flow.finalize_session_traces() # um link de trace para o chat inteiro
```
## Ciclo de vida do turno
Cada `handle_turn` executa este pipeline:
1. **`_configure_conversational_kickoff`** — mescla `session_id` / `user_message` em `inputs`, aplica `ConversationalConfig`, habilita tracing adiado quando configurado.
2. **Restauração de estado** — se `inputs["id"]` existe e `@persist` está configurado, carrega o snapshot mais recente.
3. **`FlowStarted`** — emitido apenas no primeiro turno da sessão adiada.
4. **`prepare_conversational_turn`** — acrescenta a mensagem do usuário em `state.messages`, define `last_user_message`, limpa `last_intent`, classifica opcionalmente quando `intents` / `default_intents` + `intent_llm` estão definidos.
5. **Execução do grafo** — `@start` → `@router` → handlers `@listen`.
6. **Fim da execução** — `flow_finished` por turno e finalização de trace são **ignorados** com adiamento; `Agent.kickoff()` / crews aninhados também não fecham o batch pai.
Os handlers devem chamar **`append_assistant_message(reply)`** para que o próximo turno inclua a resposta do assistente. A linha do usuário já é salva por `handle_turn` — não acrescente de novo nos handlers.
## `ConversationalConfig` (padrões em nível de classe)
Defina na subclasse de `Flow` como `conversational_config: ClassVar[ConversationalConfig | None]`.
| Campo | Padrão | Propósito |
|-------|---------|-----------|
| `default_intents` | `None` | Rótulos de outcome para classificação automática antes do kickoff |
| `intent_llm` | `None` | Modelo para classificação (obrigatório quando há intents) |
| `interactive_prompt` | `"You: "` | Prompt para `kickoff(interactive=True)` |
| `interactive_timeout` | `None` | Timeout por linha no modo interativo |
| `exit_commands` | `exit`, `quit` | Palavras que encerram o modo interativo |
| `defer_trace_finalization` | `True` | Manter um batch de trace aberto entre turnos |
Sobrescreva por kickoff com `intents=` e `intent_llm=`.
## `ChatState` (formato persistido recomendado)
```python
from crewai.flow import ChatState
class MyChatState(ChatState):
# Herdados: id, messages, last_user_message, last_intent, session_ready
research_turn_count: int = 0
custom_flag: bool = False
```
| Campo | Função |
|-------|--------|
| `id` | UUID da sessão (igual a `session_id` / `inputs["id"]`) |
| `messages` | `list` de `{role, content}` para histórico de LLM |
| `last_user_message` | Última linha do usuário neste turno |
| `last_intent` | Rótulo de rota após classificação (se usado) |
| `session_ready` | Flag de bootstrap único (permissões, caches, etc.) |
`ConversationalInputs` é um `TypedDict` para `kickoff(inputs={...})`: `id`, `user_message`, `last_intent`.
## API conversacional em `Flow`
### Parâmetros de `kickoff` / `kickoff_async`
| Parâmetro | Propósito |
|-----------|-----------|
| `user_message` | Texto deste turno (ou `{"role": "user", "content": "..."}`) |
| `session_id` | UUID da conversa → `inputs["id"]` / `state.id` |
| `intents` | Rótulos de outcome para `classify_intent` antes do kickoff |
| `intent_llm` | LLM para classificação (obrigatório com `intents`) |
| `interactive` | Loop CLI via `ask()` (só demos locais) |
| `interactive_prompt` | Prompt no modo interativo |
| `interactive_timeout` | Timeout de `ask()` por linha |
| `exit_commands` | Palavras que encerram o modo interativo |
| `inputs` | Campos extras de estado (mesclados com chaves conversacionais) |
| `restore_from_state_id` | Hidratação fork de outro flow persistido |
### Atributos de instância
| Atributo | Propósito |
|-----------|-----------|
| `conversational_config` | Padrões `ConversationalConfig` em nível de classe |
| `defer_trace_finalization` | Flag de instância; definida automaticamente a partir do config no kickoff |
| `suppress_flow_events` | Oculta painéis Rich no console; **tracing ainda registra** eventos |
| `stream` | Habilita streaming; use com `ChatSession.handle_turn(..., stream=True)` |
### Métodos e propriedades
| Nome | Descrição |
|------|-------------|
| `append_message(role, content, **extra)` | Acrescenta em `state.messages` (roles: `user`, `assistant`, `system`, `tool`) |
| `conversation_messages` | Histórico somente leitura para chamadas LLM |
| `classify_intent(text, outcomes, *, llm, context=None)` | Mapeia texto a um outcome (mesma lógica de `@human_feedback`) |
| `receive_user_message(text, *, outcomes=None, llm=None)` | Acrescenta mensagem do usuário; opcionalmente define `last_intent` |
| `finalize_session_traces()` | Emite `flow_finished` adiado e finaliza o batch de trace da sessão |
| `_should_defer_trace_finalization()` | Se este flow adia finalização de trace por turno |
| `input_history` | Trilha de auditoria de prompts e respostas de `ask()` |
### Helpers do módulo (`crewai.flow.conversation`)
Importáveis para testes ou orquestração customizada:
| Função | Descrição |
|----------|-------------|
| `normalize_kickoff_inputs(inputs, user_message=..., session_id=...)` | Mescla kwargs conversacionais em `inputs` |
| `get_conversation_messages(flow)` | Lê mensagens do estado ou buffer interno |
| `append_message(flow, role, content, **extra)` | Igual ao método de instância |
| `prepare_conversational_turn(flow, ...)` | Hidratação do turno (geralmente chamado pelo kickoff) |
| `receive_user_message(flow, text, ...)` | Igual ao método de instância |
| `set_state_field(flow, name, value)` | Define campo em estado dict ou Pydantic |
| `get_conversational_config(flow)` | Lê `conversational_config` da classe |
| `input_history_to_messages(entries)` | Converte `input_history` para formato de mensagens LLM |
## Padrões de roteamento de intenção
### A. Pré-classificar via `ConversationalConfig` (mais simples)
Defina `default_intents` e `intent_llm`. Cada kickoff classifica antes do `@router`; leia `self.state.last_intent` em `route()`.
### B. Classificar dentro do `@router` (prompts mais ricos)
Defina `default_intents=None` para o kickoff só acrescentar a mensagem. Em `route()`, chame `classify_intent` com prompt ou descrições customizadas:
```python
@router(bootstrap)
def route(self):
intent = self.classify_intent(
self._routing_prompt(self.state.last_user_message),
("GREETING", "ORDER", "RESEARCH", "GOODBYE"),
llm=self.conversational_config.intent_llm or "gpt-4o-mini",
)
self.state.last_intent = intent
return intent
```
Use **`@listen("RESEARCH")`** (ou similar) para passos com `Agent.kickoff()` e ferramentas — não `LLM.call()` puro — quando precisar de pesquisa web ou uso multi-etapa de tools.
## Quando o flow termina mas o usuário continua conversando
`FlowFinished` significa que **esta execução do grafo** terminou. A conversa segue com outro `kickoff` e o mesmo `session_id`. `@persist` restaura `messages`, flags e contexto.
**Padrão de persistência:** prefira `@persist` em um **único passo terminal** (por exemplo `finalize`) em vez de na classe `Flow` inteira. Persist em nível de classe salva após cada método; `load_state` usa a linha mais recente, que pode ser snapshot no meio da execução e perder atualizações dos handlers no mesmo turno.
Não use `@human_feedback` para linhas de chat de follow-up, a menos que um humano precise aprovar uma saída específica antes de exibi-la.
## `Flow` conversacional (experimental)
<Warning>
**Funcionalidade experimental.** A superfície do `Flow` conversacional
(`conversational = True`, `handle_turn`, `ConversationConfig`,
`RouterConfig`, `ConversationState`, o grafo embutido + helpers) vive em
`crewai.experimental` e pode mudar de formato antes de graduar. Fixe a
versão do CrewAI se depende de comportamento específico e acompanhe o
changelog para mudanças quebradoras. Feedback / issues bem-vindos.
</Warning>
Habilite o grafo conversacional definindo `conversational = True` em uma subclasse de `Flow`. O `Flow` base passa a expor um grafo embutido `@start` / `@router` / `converse_turn` / `end_conversation`, gerencia `state.messages`, dirige o LLM de roteamento e mantém o batch de trace aberto entre os turnos. Você escreve as **rotas customizadas**; o framework cuida do resto.
Use isto quando quiser um chat multi-turno com router LLM e handlers por rota sem cablar o ciclo de vida na mão. Use `Flow[ChatState]` (o padrão de mais baixo nível acima) quando precisar de controle total.
### Exemplo rápido
```python
from crewai import LLM, Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
ConversationConfig,
ConversationState,
RouterConfig,
)
ROUTER_LLM = LLM(model="gpt-4o-mini")
@ConversationConfig(
system_prompt="A multi-agent assistant for ordinary chat and tool-backed tasks.",
llm=ROUTER_LLM,
router=RouterConfig(), # rotas + descrições auto-descobertas pelos handlers @listen
)
class SupportFlow(Flow[ConversationState]):
conversational = True
@listen("INTERNET_SEARCH")
def handle_internet_search(self) -> str:
"""Fresh web research, current news, real-time lookups."""
...
self.append_assistant_message(reply)
return reply
@listen("CREWAI_DOCS")
def handle_crewai_docs(self) -> str:
"""Look up the CrewAI documentation for framework/API questions."""
...
self.append_assistant_message(reply)
return reply
flow = SupportFlow()
try:
flow.handle_turn("O que você pode fazer?") # roteia para converse (built-in)
flow.handle_turn("Pesquise na web por notícias de IA.") # roteia para INTERNET_SEARCH
flow.handle_turn("Resuma o primeiro resultado.") # volta para converse
finally:
flow.finalize_session_traces()
```
Para um chat local no terminal, use `chat()`:
```python
def kickoff() -> None:
SupportFlow().chat()
```
`chat()` envolve `handle_turn()` em um REPL, sai com `exit` / `quit`, ignora linhas em branco por padrão e chama `finalize_session_traces()` quando a sessão termina.
### `ConversationConfig`
Decorador de classe que anexa os defaults de chat por classe.
| Campo | Padrão | Propósito |
|-------|--------|-----------|
| `system_prompt` | `slices.conversational_system_prompt` (i18n) | System message usado pelo `converse_turn` embutido. Passe `""` para desativar totalmente. |
| `llm` | `None` | LLM de conversa (usado pelo `converse_turn` e como fallback do router). |
| `router` | `None` | `RouterConfig` para roteamento por LLM. Sem ele, o flow sempre cai em `converse`. |
| `answer_from_history_prompt` | padrão do framework | System message para a rota opcional `answer_from_history`. |
| `answer_from_history_llm` | `None` | Habilita o atalho `answer_from_history` quando definido. |
| `intent_llm` | `None` | LLM para o caminho legado `intents=`/`default_intents`. |
| `default_intents` | `None` | Labels de outcome para pré-classificação legada. |
| `visible_agent_outputs` | `None` | `"all"` ou lista de nomes de agentes cujos `append_agent_result()` devem virar mensagens públicas. |
| `defer_trace_finalization` | `True` | Mantém um único batch de trace aberto entre chamadas de `handle_turn()`. |
### `RouterConfig` e o catálogo de rotas auto-gerado
```python
RouterConfig(
prompt="Enquadramento de domínio opcional (política, voz, persona).",
response_format=MyRoute, # opcional; auto-gerado caso contrário
llm=ROUTER_LLM, # usa ConversationConfig.llm como fallback
routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # opcional; inferido dos listeners
route_descriptions={
"INTERNET_SEARCH": "Sobrescreve a docstring só desta rota.",
},
default_intent="converse", # usado quando a chamada ao LLM falha ou não há LLM
fallback_intent="converse", # usado quando o LLM retorna rota inválida
intent_field="intent",
)
```
O prompt do router é montado automaticamente. Para cada rota o framework escolhe a descrição nesta precedência:
1. `RouterConfig.route_descriptions[label]` — override explícito.
2. `Flow.builtin_route_descriptions[label]` — texto canônico do framework para `converse`, `end`, `answer_from_history` (otimizado para o LLM de routing).
3. Primeira linha não vazia da docstring do handler `@listen(label)`.
4. Vazio (a rota aparece no catálogo sem descrição).
Na prática, **adicionar uma rota é `@listen("X")` + uma docstring de uma linha**:
```python
@listen("INTERNET_SEARCH")
def handle_internet_search(self) -> str:
"""Fresh web research, current news, real-time lookups."""
...
```
…e o LLM de routing vê:
```
Routes:
- CREWAI_DOCS: Look up the CrewAI documentation for framework/API questions.
- INTERNET_SEARCH: Fresh web research, current news, real-time lookups.
- converse: Ordinary chat, follow-ups, summaries, clarifications…
- end: User signals the conversation is finished (goodbye, exit, done).
```
`RouterConfig.prompt` é para **enquadramento de domínio** (persona do assistente, regras de negócio, voz). O catálogo de rotas é auto-gerado — não liste rotas em `prompt`; elas vão sair de sincronia assim que você adicionar um handler.
### Rotas embutidas
| Rota | Handler | Propósito |
|------|---------|-----------|
| `converse` | `converse_turn` | Handler de chat padrão. Chama `ConversationConfig.llm` com o system prompt + histórico canônico. |
| `end` | `end_conversation` | Define `state.ended = True` e emite uma resposta de encerramento. |
| `answer_from_history` | `answer_from_history_turn` | Opcional. Cai aqui quando `ConversationConfig.answer_from_history_llm` está definido e a mensagem pode ser respondida só pelo histórico. |
Você pode sobrescrever qualquer uma definindo um handler com o mesmo nome na subclasse.
### Semântica de `handle_turn()`
`flow.handle_turn(message)` roda um turno:
1. Reseta o tracking por execução (`_completed_methods`, `_method_outputs`) para o grafo re-rodar — sem isso, chamadas repetidas de `kickoff` na mesma instância dariam curto-circuito no turno 2+ porque `Flow.kickoff_async` trata `inputs={"id": ...}` como restauração de checkpoint.
2. Anexa a mensagem do usuário em `state.messages`, define `current_user_message` / `last_user_message`. `last_intent` é **preservado do turno anterior** para que o LLM de routing possa usá-lo como sinal.
3. Roda `conversation_start` → `route_conversation` → o handler `@listen` escolhido.
4. O router grava sua decisão em `state.last_intent` (visível para o contexto de routing do próximo turno).
5. Se seu handler retornou uma string e ainda não chamou `append_assistant_message`, `handle_turn` anexa para você.
Chame `handle_turn()` para mensagens de chat. Chamar `kickoff(inputs={"id": ...})` diretamente executa o grafo sem aplicar o wrapper de turno conversacional.
### `chat()` para REPLs locais
`flow.chat()` é o wrapper de terminal pronto para uso em cima de `handle_turn()`:
```python
flow = SupportFlow()
flow.chat()
```
Ele cobre o loop local comum:
1. Solicita uma mensagem do usuário.
2. Para com `exit` / `quit`, `EOFError` ou `KeyboardInterrupt`.
3. Chama `handle_turn(message, session_id=...)`.
4. Imprime o resultado do assistente.
5. Finaliza traces de sessão adiados em um bloco `finally`.
Customize o comportamento do terminal com I/O injetável:
```python
flow.chat(
session_id="demo-session",
prompt="You: ",
assistant_prefix="Assistant: ",
exit_commands=("exit", "quit", "bye"),
)
```
Para apps web, workers em background, testes e transportes customizados, continue usando `handle_turn()` diretamente.
### Comportamento customizado do router
Para rodar efeitos colaterais (setup de event bus, telemetria) em toda decisão de routing, sobrescreva `route_turn`:
```python
class SupportFlow(Flow[ConversationState]):
conversational = True
def route_turn(self, context: dict[str, Any]) -> str | None:
self.event_bus = MyBus(self)
return super().route_turn(context)
```
Para ignorar o router LLM e escolher uma rota programaticamente, retorne uma string de `route_turn`; retornar `None` cai no `_route_with_config(...)`.
### `append_assistant_message` e `append_agent_result`
Dentro de um handler `@listen(label)`, escolha:
- `self.append_assistant_message(text)` — adiciona um turno de assistente visível ao usuário em `state.messages`. O `converse_turn` do próximo turno vai vê-lo.
- `self.append_agent_result(agent_name, result, visibility="private")` — registra um evento estruturado em `state.events` e uma thread em `state.agent_threads[agent_name]`. Visibilidade pública também chama `append_assistant_message` automaticamente. Use resultados privados para trabalho de bastidor que não deve poluir o histórico canônico.
`ConversationConfig.visible_agent_outputs` pode promover globalmente os resultados privados de agentes específicos para públicos (`"all"` ou lista de nomes).
## Tracing entre turnos
Com `defer_trace_finalization=True` (padrão em `ConversationalConfig`):
- **Um batch de trace** para toda a sessão de chat.
- **`flow_started`** só no primeiro turno; **`flow_finished`** uma vez em `finalize_session_traces()`.
- **`kickoff` por turno** não exibe “Trace batch finalized”.
- **Trabalho aninhado** (`Agent.kickoff()`, crews, tools Exa) acrescenta ao batch **pai**; flows internos de `AgentExecutor` não fecham o batch da sessão cedo.
```python
flow.chat(session_id=session_id)
```
`flow.chat()` chama `finalize_session_traces()` para você. Quando você controla o loop com `handle_turn()` ou `kickoff(...)`, chame `finalize_session_traces()` quando a sessão terminar.
`suppress_flow_events=True` só oculta painéis do console; eventos de trace e método ainda são emitidos.
### Ciclo de vida de trace do `Flow` conversacional
O [`Flow` conversacional](#flow-conversacional-experimental) experimental usa o mesmo ciclo de vida de tracing: `defer_trace_finalization` é `True` por padrão, então cada `handle_turn()` mantém o trace da sessão aberto. Sempre finalize ao fim da sessão — envolva seu loop em `try/finally` e chame `flow.finalize_session_traces()` na saída. Sem isso, o batch fica aberto e a última conversa pode nunca ser exportada.
## Streaming
Defina `stream = True` na classe `Flow`. `kickoff(...)` então emitirá `assistant_delta` (e eventos relacionados) pelo event bus padrão.
## Imports
```python
from crewai.flow import (
ChatState,
ConversationalConfig,
ConversationalInputs,
Flow,
listen,
persist,
router,
start,
)
```
## Veja também
- [Dominando o Gerenciamento de Estado em Flows](/pt-BR/guides/flows/mastering-flow-state) — persistência, estado Pydantic, `@persist`
- [Construa Seu Primeiro Flow](/pt-BR/guides/flows/first-flow) — fundamentos de flow
- Demo: `lib/crewai/runner_conversational_flow_simple.py` — REPL mínimo com `RESEARCH` + agente Exa