diff --git a/docs/ar/guides/flows/conversational-flows.mdx b/docs/ar/guides/flows/conversational-flows.mdx index bfb45c90ba..9d397d73ec 100644 --- a/docs/ar/guides/flows/conversational-flows.mdx +++ b/docs/ar/guides/flows/conversational-flows.mdx @@ -7,7 +7,7 @@ mode: "wide" ## نظرة عامة -تعامل التطبيقات المحادثية مع كل سطر من المستخدم كـ **تشغيل flow جديد** بنفس **معرّف الجلسة**. توفر CrewAI مساعدات لسجل الرسائل وتصنيف النية الاختياري وتأجيل التتبع وجسور الواجهة — دون API منفصل `chat()` على `Flow`. +تعامل التطبيقات المحادثية مع كل سطر من المستخدم كـ **تشغيل flow جديد** بنفس **معرّف الجلسة**. توفر CrewAI مساعدات لسجل الرسائل وتصنيف النية الاختياري وتأجيل التتبع وجسور الواجهة، إضافة إلى REPL محلي `flow.chat()` للتدفقات المحادثية. | المفهوم | التنفيذ | |---------|---------| @@ -16,13 +16,15 @@ mode: "wide" | اكتمال الجولة | `FlowFinished` لهذا **التشغيل** فقط؛ تستمر المحادثة في `kickoff` التالي | | تتبع الجلسة | `ConversationalConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | -## نقطة دخول واحدة: `kickoff` +## واجهات الجولات -استخدم **`flow.kickoff(user_message=..., session_id=...)`** لكل رسالة مستخدم (REST أو WebSocket أو CLI). لا تنشئ غلاف `chat()` مخصصاً على `Flow`. +استخدم **`flow.kickoff(user_message=..., session_id=...)`** أو **`flow.handle_turn(...)`** لكل رسالة مستخدم من REST أو WebSocket أو الاختبارات أو الواجهات المخصصة. استخدم **`flow.chat()`** عندما تريد حلقة دردشة محلية في الطرفية لـ `Flow` محادثي. | API | الاستخدام | |-----|-----------| | `kickoff(user_message=..., session_id=...)` | كل رسالة مستخدم | +| `handle_turn(message, session_id=...)` | غلاف مريح لجولة واحدة في `Flow` محادثي | +| `chat()` | REPL محلي في الطرفية لـ `Flow` محادثي | | `kickoff_async(...)` | نفس المعاملات؛ دخول async أصلي | | `ask()` | مطالبة حاجزة **داخل** خطوة واحدة | | `@human_feedback` | الموافقة/الرفض على **مخرجات خطوة** — وليس السطر التالي | @@ -290,6 +292,15 @@ finally: flow.finalize_session_traces() ``` +للدردشة المحلية في الطرفية، استخدم `chat()`: + +```python +def kickoff() -> None: + SupportFlow().chat() +``` + +يلف `chat()` استدعاءات `handle_turn()` داخل REPL، ويخرج عند `exit` / `quit`، ويتجاهل الأسطر الفارغة افتراضياً، ويستدعي `finalize_session_traces()` عند انتهاء الجلسة. + ### `ConversationConfig` مزخرف صنف يُلحق افتراضيات الدردشة على مستوى الصنف. @@ -373,6 +384,36 @@ Routes: يمكنك أيضاً استدعاء `flow.kickoff(user_message=..., session_id=...)` مباشرةً — نفس منطق الإعادة والتشغيل يعمل. `handle_turn` هو الغلاف المريح. +### `chat()` للـ REPL المحلي + +`flow.chat()` هو غلاف الطرفية الجاهز فوق `handle_turn()`: + +```python +flow = SupportFlow() +flow.chat() +``` + +يتولى الحلقة المحلية الشائعة: + +1. يطلب رسالة من المستخدم. +2. يتوقف عند `exit` / `quit` أو `EOFError` أو `KeyboardInterrupt`. +3. يستدعي `handle_turn(message, session_id=...)`. +4. يطبع نتيجة المساعد. +5. ينهي traces الجلسة المؤجلة داخل كتلة `finally`. + +خصص سلوك الطرفية عبر I/O قابل للحقن: + +```python +flow.chat( + session_id="demo-session", + prompt="You: ", + assistant_prefix="Assistant: ", + exit_commands=("exit", "quit", "bye"), +) +``` + +لتطبيقات الويب والـ workers الخلفية والاختبارات ووسائط النقل المخصصة، استمر في استخدام `handle_turn()` مباشرةً. + ### سلوك موجّه مخصص لتشغيل آثار جانبية (إعداد ناقل أحداث، قياس عن بُعد) في كل قرار توجيه، تجاوز `route_turn`: @@ -407,17 +448,10 @@ class SupportFlow(Flow[ConversationState]): - **العمل المتداخل** (`Agent.kickoff()`, crews, Exa) يُلحق بدفعة **الأب**؛ flow داخلي من `AgentExecutor` لا يغلق دفعة الجلسة مبكراً. ```python -try: - while True: - line = input("You: ").strip() - if not line: - break - flow.kickoff(user_message=line, session_id=session_id) -finally: - flow.finalize_session_traces() +flow.chat(session_id=session_id) ``` -`ChatSession.close()` يستدعي `finalize_session_traces()` عند التأجيل. +`flow.chat()` يستدعي `finalize_session_traces()` نيابةً عنك. عندما تملك الحلقة عبر `handle_turn()` أو `kickoff(...)`، استدعِ `finalize_session_traces()` عند انتهاء الجلسة. `suppress_flow_events=True` يخفي لوحات Rich فقط؛ أحداث trace والـ methods تُصدر. diff --git a/docs/en/guides/flows/conversational-flows.mdx b/docs/en/guides/flows/conversational-flows.mdx index 8325740954..00084cae70 100644 --- a/docs/en/guides/flows/conversational-flows.mdx +++ b/docs/en/guides/flows/conversational-flows.mdx @@ -7,7 +7,7 @@ mode: "wide" ## Overview -Conversational apps treat each user line as a **new flow run** with the **same session id**. CrewAI adds helpers for message history, optional intent classification, deferred tracing, and UI bridges — without a separate `chat()` API on `Flow`. +Conversational apps treat each user line as a **new flow run** with the **same session id**. CrewAI adds helpers for message history, optional intent classification, deferred tracing, UI bridges, and a local `flow.chat()` REPL for conversational flows. | Concept | Implementation | |---------|----------------| @@ -16,13 +16,15 @@ Conversational apps treat each user line as a **new flow run** with the **same s | Turn complete | `FlowFinished` for **this run** only; chat continues on the next `kickoff` | | Full-session trace | `ConversationalConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | -## One entry point: `kickoff` +## Turn APIs -Use **`flow.kickoff(user_message=..., session_id=...)`** for every user message (REST, WebSocket, CLI). Do not add a custom `chat()` wrapper on `Flow`. +Use **`flow.kickoff(user_message=..., session_id=...)`** or **`flow.handle_turn(...)`** for every user message from REST, WebSocket, tests, and custom UIs. Use **`flow.chat()`** when you want a local terminal chat loop for a conversational `Flow`. | API | Use for | |-----|---------| | `kickoff(user_message=..., session_id=...)` | Each user message | +| `handle_turn(message, session_id=...)` | Ergonomic one-turn wrapper for conversational `Flow` | +| `chat()` | Local terminal REPL for conversational `Flow` | | `kickoff_async(...)` | Same parameters; native async entry | | `ask()` | Blocking prompt **inside** one step (wizard, clarification) | | `@human_feedback` | Approve/reject **a step output** — not the next chat line | @@ -293,6 +295,15 @@ finally: flow.finalize_session_traces() ``` +For a local terminal chat, use `chat()`: + +```python +def kickoff() -> None: + SupportFlow().chat() +``` + +`chat()` wraps `handle_turn()` in a REPL, exits on `exit` / `quit`, skips blank lines by default, and calls `finalize_session_traces()` when the session ends. + ### `ConversationConfig` Class decorator that attaches per-class chat defaults. @@ -376,6 +387,36 @@ You can override any of these by defining a same-named handler in your subclass. You can also call `flow.kickoff(user_message=..., session_id=...)` directly — the same reset/run logic fires. `handle_turn` is the ergonomic wrapper. +### `chat()` for local REPLs + +`flow.chat()` is the batteries-included terminal wrapper around `handle_turn()`: + +```python +flow = SupportFlow() +flow.chat() +``` + +It handles the common local loop: + +1. Prompts for a user message. +2. Stops on `exit` / `quit`, `EOFError`, or `KeyboardInterrupt`. +3. Calls `handle_turn(message, session_id=...)`. +4. Prints the assistant result. +5. Finalizes deferred session traces in a `finally` block. + +Customize the terminal behavior with injectable I/O: + +```python +flow.chat( + session_id="demo-session", + prompt="You: ", + assistant_prefix="Assistant: ", + exit_commands=("exit", "quit", "bye"), +) +``` + +For web apps, background workers, tests, and custom transports, keep using `handle_turn()` directly. + ### Custom router behavior To run side effects (event bus setup, telemetry) on every routing decision, override `route_turn`: @@ -410,17 +451,12 @@ With `defer_trace_finalization=True` (default in `ConversationalConfig`): - **Nested work** (`Agent.kickoff()`, crews, Exa tools) appends to the **parent** batch; inner `AgentExecutor` flows do not close the session batch early. ```python -try: - while True: - line = input("You: ").strip() - if not line: - break - flow.kickoff(user_message=line, session_id=session_id) -finally: - flow.finalize_session_traces() +flow.chat(session_id=session_id) ``` -`ChatSession.close()` calls `finalize_session_traces()` when deferral is enabled. +`flow.chat()` calls `finalize_session_traces()` for you. When you own the loop +with `handle_turn()` or `kickoff(...)`, call `finalize_session_traces()` when +the session ends. `suppress_flow_events=True` only hides Rich console panels; trace and method events still emit for observability. diff --git a/docs/ko/guides/flows/conversational-flows.mdx b/docs/ko/guides/flows/conversational-flows.mdx index 677a014a58..35c63caccc 100644 --- a/docs/ko/guides/flows/conversational-flows.mdx +++ b/docs/ko/guides/flows/conversational-flows.mdx @@ -7,7 +7,7 @@ mode: "wide" ## 개요 -대화형 앱은 각 사용자 입력을 **동일한 세션 id**로 **새 flow 실행**으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 분류, 지연 트레이싱, UI 브리지를 제공하며, `Flow`에 별도 `chat()` API는 없습니다. +대화형 앱은 각 사용자 입력을 **동일한 세션 id**로 **새 flow 실행**으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 분류, 지연 트레이싱, UI 브리지, 그리고 대화형 flow용 로컬 `flow.chat()` REPL을 제공합니다. | 개념 | 구현 | |------|------| @@ -16,13 +16,15 @@ mode: "wide" | 턴 완료 | `FlowFinished`는 **이번 실행**만 의미; 다음 `kickoff`로 대화 계속 | | 세션 전체 트레이스 | `ConversationalConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | -## 단일 진입점: `kickoff` +## 턴 API -모든 사용자 메시지에 **`flow.kickoff(user_message=..., session_id=...)`**를 사용하세요 (REST, WebSocket, CLI). `Flow`에 커스텀 `chat()` 래퍼를 만들지 마세요. +REST, WebSocket, 테스트, 커스텀 UI에서 오는 모든 사용자 메시지에는 **`flow.kickoff(user_message=..., session_id=...)`** 또는 **`flow.handle_turn(...)`**를 사용하세요. 대화형 `Flow`를 로컬 터미널 채팅 루프로 실행하고 싶을 때는 **`flow.chat()`**을 사용하세요. | API | 용도 | |-----|------| | `kickoff(user_message=..., session_id=...)` | 각 사용자 메시지 | +| `handle_turn(message, session_id=...)` | 대화형 `Flow`용 한 턴 편의 래퍼 | +| `chat()` | 대화형 `Flow`용 로컬 터미널 REPL | | `kickoff_async(...)` | 동일 파라미터; 네이티브 async 진입 | | `ask()` | 한 스텝 **내부** 블로킹 프롬프트 (마법사, 확인) | | `@human_feedback` | **스텝 출력** 승인/거부 — 다음 채팅 줄이 아님 | @@ -292,6 +294,15 @@ finally: flow.finalize_session_traces() ``` +로컬 터미널 채팅에는 `chat()`을 사용하세요: + +```python +def kickoff() -> None: + SupportFlow().chat() +``` + +`chat()`은 `handle_turn()`을 REPL로 감싸고, `exit` / `quit`에서 종료하며, 기본적으로 빈 줄을 건너뛰고, 세션이 끝날 때 `finalize_session_traces()`를 호출합니다. + ### `ConversationConfig` 클래스 단위의 챗 기본값을 부착하는 클래스 데코레이터입니다. @@ -375,6 +386,36 @@ Routes: `flow.kickoff(user_message=..., session_id=...)`를 직접 호출해도 동일한 reset/run 로직이 동작합니다. `handle_turn`은 그 위에 얹은 편의 래퍼입니다. +### 로컬 REPL용 `chat()` + +`flow.chat()`은 `handle_turn()` 위에 얹은 바로 쓸 수 있는 터미널 래퍼입니다: + +```python +flow = SupportFlow() +flow.chat() +``` + +일반적인 로컬 루프를 처리합니다: + +1. 사용자 메시지를 입력받습니다. +2. `exit` / `quit`, `EOFError`, `KeyboardInterrupt`에서 멈춥니다. +3. `handle_turn(message, session_id=...)`를 호출합니다. +4. 어시스턴트 결과를 출력합니다. +5. `finally` 블록에서 지연된 세션 trace를 finalize합니다. + +주입 가능한 I/O로 터미널 동작을 커스터마이즈할 수 있습니다: + +```python +flow.chat( + session_id="demo-session", + prompt="You: ", + assistant_prefix="Assistant: ", + exit_commands=("exit", "quit", "bye"), +) +``` + +웹 앱, 백그라운드 worker, 테스트, 커스텀 transport에서는 계속 `handle_turn()`을 직접 사용하세요. + ### 커스텀 router 동작 매 라우팅 결정마다 사이드 이펙트(이벤트 버스 셋업, 텔레메트리)를 실행하려면 `route_turn`을 오버라이드하세요: @@ -409,17 +450,10 @@ LLM router를 우회해 프로그램적으로 라우트를 선택하려면 `rout - **중첩 작업** (`Agent.kickoff()`, crew, Exa tool)은 **부모** batch에 추가; 내부 `AgentExecutor` flow가 세션 batch를 조기 종료하지 않음. ```python -try: - while True: - line = input("You: ").strip() - if not line: - break - flow.kickoff(user_message=line, session_id=session_id) -finally: - flow.finalize_session_traces() +flow.chat(session_id=session_id) ``` -지연 활성화 시 `ChatSession.close()`가 `finalize_session_traces()`를 호출합니다. +`flow.chat()`이 `finalize_session_traces()`를 대신 호출합니다. `handle_turn()`이나 `kickoff(...)`로 직접 루프를 소유하는 경우, 세션이 끝날 때 `finalize_session_traces()`를 호출하세요. `suppress_flow_events=True`는 Rich 콘솔 패널만 숨깁니다. trace 및 method 이벤트는 계속 발생합니다. diff --git a/docs/pt-BR/guides/flows/conversational-flows.mdx b/docs/pt-BR/guides/flows/conversational-flows.mdx index 755f282e56..905cdce3ad 100644 --- a/docs/pt-BR/guides/flows/conversational-flows.mdx +++ b/docs/pt-BR/guides/flows/conversational-flows.mdx @@ -7,7 +7,7 @@ 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 e pontes para UI — sem uma API `chat()` separada em `Flow`. +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 | |---------|----------------| @@ -16,13 +16,15 @@ Apps conversacionais tratam cada linha do usuário como uma **nova execução do | Fim do turno | `FlowFinished` só para **esta execução**; o chat segue no próximo `kickoff` | | Trace da sessão | `ConversationalConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | -## Um ponto de entrada: `kickoff` +## APIs de turno -Use **`flow.kickoff(user_message=..., session_id=...)`** para cada mensagem (REST, WebSocket, CLI). Não crie um wrapper `chat()` customizado em `Flow`. +Use **`flow.kickoff(user_message=..., session_id=...)`** ou **`flow.handle_turn(...)`** 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. | API | Uso | |-----|-----| | `kickoff(user_message=..., session_id=...)` | Cada mensagem do usuário | +| `handle_turn(message, session_id=...)` | Wrapper ergonômico de um turno para `Flow` conversacional | +| `chat()` | REPL local no terminal para `Flow` conversacional | | `kickoff_async(...)` | Mesmos parâmetros; entrada async nativa | | `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 | @@ -293,6 +295,15 @@ 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. @@ -376,6 +387,36 @@ Você pode sobrescrever qualquer uma definindo um handler com o mesmo nome na su Você também pode chamar `flow.kickoff(user_message=..., session_id=...)` diretamente — a mesma lógica de reset/run é acionada. `handle_turn` é o wrapper ergonômico. +### `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`: @@ -410,17 +451,10 @@ Com `defer_trace_finalization=True` (padrão em `ConversationalConfig`): - **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 -try: - while True: - line = input("You: ").strip() - if not line: - break - flow.kickoff(user_message=line, session_id=session_id) -finally: - flow.finalize_session_traces() +flow.chat(session_id=session_id) ``` -`ChatSession.close()` chama `finalize_session_traces()` quando o adiamento está habilitado. +`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. diff --git a/lib/crewai/src/crewai/experimental/conversational_mixin.py b/lib/crewai/src/crewai/experimental/conversational_mixin.py index a66c5bc683..3d3a8d05d5 100644 --- a/lib/crewai/src/crewai/experimental/conversational_mixin.py +++ b/lib/crewai/src/crewai/experimental/conversational_mixin.py @@ -16,7 +16,7 @@ from __future__ import annotations -from collections.abc import Mapping, Sequence +from collections.abc import Callable, Mapping, Sequence from enum import Enum import json import logging @@ -243,6 +243,59 @@ def handle_turn( self.append_assistant_message(self._stringify_result(result)) return result + def chat( + self, + *, + session_id: str | None = None, + prompt: str = "\nYou: ", + assistant_prefix: str = "\nAssistant: ", + exit_commands: Sequence[str] = ("exit", "quit"), + input_fn: Callable[[str], str] = input, + output_fn: Callable[[str], None] = print, + skip_empty: bool = True, + defer_trace_finalization: bool = True, + **handle_turn_kwargs: Any, + ) -> None: + """Run an interactive terminal chat loop for a conversational Flow. + + ``chat()`` is a convenience wrapper around ``handle_turn()`` for local + REPLs. For web apps, tests, and custom transports, call + ``handle_turn()`` directly. The input/output callables are injectable so + callers can customize prompts or exercise the loop without patching + builtins. + """ + if not getattr(type(self), "conversational", False): + raise ValueError("Flow.chat() is only available on conversational flows") + + exit_set = {command.lower() for command in exit_commands} + previous_defer = getattr(self, "defer_trace_finalization", False) + if defer_trace_finalization: + self.defer_trace_finalization = True + + try: + while True: + try: + message = input_fn(prompt).strip() + except (EOFError, KeyboardInterrupt): + output_fn("") + break + + if message.lower() in exit_set: + break + if skip_empty and not message: + continue + + result = self.handle_turn( + message, + session_id=session_id, + **handle_turn_kwargs, + ) + output_fn(f"{assistant_prefix}{self._stringify_result(result)}") + finally: + self.finalize_session_traces() + if defer_trace_finalization: + self.defer_trace_finalization = previous_defer + def build_router_context(self) -> dict[str, Any]: """Build context used by the routing policy for the current turn.""" state = cast(ConversationState, self.state) diff --git a/lib/crewai/src/crewai/flow/runtime.py b/lib/crewai/src/crewai/flow/runtime.py index 0290b7ff16..07e48e0e94 100644 --- a/lib/crewai/src/crewai/flow/runtime.py +++ b/lib/crewai/src/crewai/flow/runtime.py @@ -706,16 +706,16 @@ class Flow(_ConversationalMixin, BaseModel, Generic[T], metaclass=FlowMeta): # When ``conversational = True`` on a subclass, the built-in conversational # graph (``conversation_start`` -> ``route_conversation`` -> ``converse_turn`` # / ``end_conversation`` / ``answer_from_history_turn``) registers and - # ``handle_turn`` becomes the chat entry point. When ``False`` (default), - # the methods exist as inert attributes and never register or fire — - # non-chat flows pay no runtime cost. + # ``handle_turn`` / ``chat`` become the chat entry points. When ``False`` + # (default), the methods exist as inert attributes and never register or + # fire — non-chat flows pay no runtime cost. # # ⚠ EXPERIMENTAL FEATURE. The whole conversational surface - # (``conversational`` ClassVar, ``handle_turn``, ``ConversationConfig``, - # ``RouterConfig``, ``ConversationState``, the built-in graph + helpers) - # lives under ``crewai.experimental`` and may change shape before - # graduating. Pin your CrewAI version if you depend on specific - # behavior, and watch the changelog for breaking updates. + # (``conversational`` ClassVar, ``handle_turn``, ``chat``, + # ``ConversationConfig``, ``RouterConfig``, ``ConversationState``, the + # built-in graph + helpers) lives under ``crewai.experimental`` and may + # change shape before graduating. Pin your CrewAI version if you depend on + # specific behavior, and watch the changelog for breaking updates. conversational: ClassVar[bool] = False conversational_config: ClassVar[ConversationConfig | None] = None builtin_routes: ClassVar[tuple[str, ...]] = ("converse", "end") diff --git a/lib/crewai/tests/test_flow_conversation.py b/lib/crewai/tests/test_flow_conversation.py index 77567fe5d1..4ed61394ec 100644 --- a/lib/crewai/tests/test_flow_conversation.py +++ b/lib/crewai/tests/test_flow_conversation.py @@ -858,6 +858,86 @@ def route_turn(self, context: dict[str, Any]) -> str | None: flow.handle_turn("anything") assert flow.state.messages[-1].content == "worked" + def test_chat_runs_repl_over_handle_turn_and_finalizes(self) -> None: + @ConversationConfig(defer_trace_finalization=False) + class MyChat(ConversationalFlow): + turns: int = 0 + + def route_turn(self, context: dict[str, Any]) -> str | None: + return "work" + + @listen("work") + def do_work(self) -> str: + self.turns += 1 + reply = f"worked: {self.state.current_user_message}" + self.append_assistant_message(reply) + return reply + + flow = MyChat() + inputs = iter(["first", "", "second", "quit"]) + prompts: list[str] = [] + outputs: list[str] = [] + + def input_fn(prompt: str) -> str: + prompts.append(prompt) + return next(inputs) + + with patch.object(flow, "finalize_session_traces") as mock_finalize: + flow.chat( + session_id="session-1", + input_fn=input_fn, + output_fn=outputs.append, + ) + + assert flow.turns == 2 + assert prompts == ["\nYou: ", "\nYou: ", "\nYou: ", "\nYou: "] + assert outputs == [ + "\nAssistant: worked: first", + "\nAssistant: worked: second", + ] + mock_finalize.assert_called_once_with() + assert flow.defer_trace_finalization is False + + def test_chat_stringifies_repl_output_like_conversation_helpers(self) -> None: + class RawResult: + raw = "raw assistant output" + + @ConversationConfig(defer_trace_finalization=False) + class MyChat(ConversationalFlow): + def route_turn(self, context: dict[str, Any]) -> str | None: + return "work" + + @listen("work") + def do_work(self) -> RawResult: + return RawResult() + + flow = MyChat() + inputs = iter(["first", "quit"]) + outputs: list[str] = [] + + with patch.object(flow, "finalize_session_traces"): + flow.chat( + input_fn=lambda _: next(inputs), + output_fn=outputs.append, + ) + + assert outputs == ["\nAssistant: raw assistant output"] + + def test_chat_rejects_non_conversational_flows(self) -> None: + class PlainFlow(Flow): + @start() + def begin(self) -> str: + return "done" + + flow = PlainFlow() + + try: + flow.chat(input_fn=lambda _: "quit") + except ValueError as exc: + assert "conversational flows" in str(exc) + else: + raise AssertionError("Flow.chat() should reject regular flows") + def test_defer_trace_finalization_skips_per_turn_finalize(self) -> None: """``defer_trace_finalization = True`` suppresses per-turn ``finalize_batch``.