---
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
