diff --git a/docs/edge/ar/guides/flows/conversational-flows.mdx b/docs/edge/ar/guides/flows/conversational-flows.mdx index c66cf26f3..7e690f5a7 100644 --- a/docs/edge/ar/guides/flows/conversational-flows.mdx +++ b/docs/edge/ar/guides/flows/conversational-flows.mdx @@ -1,35 +1,45 @@ --- title: تدفقات المحادثة -description: أنشئ تطبيقات دردشة متعددة الجولات مع kickoff لكل جولة وسجل الرسائل وتوجيه النية والتتبع وجسور WebSocket. +description: أنشئ تطبيقات دردشة متعددة الجولات باستخدام handle_turn لكل جولة، وسجل الرسائل، وتوجيه النية، والتتبع، والبث المنظّم. icon: comments mode: "wide" --- + + **هذه ميزة تجريبية.** يقع سطح `Flow` المحادثاتي + (`conversational = True` و`handle_turn` و`stream_turn` و`ConversationConfig` + و`RouterConfig` و`ConversationState` والرسم المدمج) ضمن + `crewai.experimental`، وقد يتغير قبل أن يصبح مستقراً. ثبّت إصدار CrewAI + إذا كنت تعتمد على سلوك محدد. + + ## نظرة عامة -تعامل التطبيقات المحادثية مع كل سطر من المستخدم كـ **تشغيل flow جديد** بنفس **معرّف الجلسة**. توفر CrewAI مساعدات لسجل الرسائل وتصنيف النية الاختياري وتأجيل التتبع وجسور الواجهة، إضافة إلى REPL محلي `flow.chat()` للتدفقات المحادثية. +تعامل التطبيقات المحادثية مع كل سطر من المستخدم كـ **تشغيل flow جديد** بنفس **معرّف الجلسة**. توفر CrewAI مساعدات لسجل الرسائل، وتوجيه النية الاختياري، وتأجيل التتبع، والبث المنظّم للجولات، إضافة إلى REPL محلي عبر `flow.chat()`. | المفهوم | التنفيذ | |---------|---------| | معرّف الجلسة | `handle_turn(..., session_id=...)` → `kickoff(inputs={"id": ...})` → `state.id` | | سطر المستخدم | `handle_turn(message)` يضيف الرسالة إلى `state.messages` قبل تشغيل الرسم | -| اكتمال الجولة | `FlowFinished` لهذا **التشغيل** فقط؛ تستمر المحادثة في `handle_turn` التالي | -| تتبع الجلسة | `ConversationConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | +| اكتمال الجولة | `conversation_turn_completed`؛ ومع تأجيل التتبع الافتراضي ينتظر `FlowFinished` استدعاء `finalize_session_traces()` | +| تتبع الجلسة الكامل | `ConversationConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | ## واجهات الجولات استخدم **`flow.handle_turn(message, session_id=...)`** لكل رسالة مستخدم من REST أو WebSocket أو الاختبارات أو الواجهات المخصصة. استخدم **`flow.chat()`** عندما تريد حلقة دردشة محلية في الطرفية لـ `Flow` محادثي. -لا يقبل `Flow.kickoff()` الوسيطين `user_message=` أو `session_id=`. في التدفقات المحادثية، يخزن `handle_turn()` الرسالة المعلقة ويستدعي داخلياً `kickoff(inputs={"id": session_id})`. +لا يقبل `Flow.kickoff()` الوسيطين `user_message=` أو `session_id=`. في التدفقات المحادثية، يخزن `handle_turn()` الرسالة المعلقة ويستدعي داخلياً `kickoff(inputs={"id": session_id})` بعد إعادة ضبط حالة التنفيذ الخاصة بالجولة. | API | الاستخدام | |-----|-----------| | `handle_turn(message, session_id=...)` | غلاف مريح لجولة واحدة في `Flow` محادثي | +| `stream_turn(message, session_id=...)` | بث جولة محادثية واحدة كإطارات runtime مرتبة | | `chat()` | REPL محلي في الطرفية لـ `Flow` محادثي | | `kickoff(inputs={...})` | تشغيل متقدم للـ flow بدون معالجة جولة محادثية | -| `ask()` | مطالبة حاجزة **داخل** خطوة واحدة | +| `ask()` | مطالبة حاجزة **داخل** خطوة واحدة (معالج إرشادي أو طلب توضيح) | | `@human_feedback` | الموافقة/الرفض على **مخرجات خطوة** — وليس السطر التالي | -| `ChatSession.handle_turn(...)` | طبقة نقل فوق `handle_turn` | + +ترفع `handle_turn()` و`stream_turn()` و`chat()` الخطأ `ValueError` ما لم يكن الوضع المحادثاتي مفعّلاً. يؤدي تطبيق `@ConversationConfig(...)` إلى تفعيله تلقائياً؛ وإلا فعيّن `conversational = True`. ## بداية سريعة @@ -46,31 +56,29 @@ from crewai.experimental.conversational import ( @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 "طلب" in message or "order" in message: + if "order" in message: return "order" - if "وداع" in message or "goodbye" in message: + if "bye" in message or "goodbye" in message: return "goodbye" return "help" @listen("order") def handle_order(self): - reply = "طلبك في الطريق." + reply = "Your order is on the way." self.append_assistant_message(reply) return reply @listen("help") def handle_help(self): - reply = "كيف يمكنني المساعدة؟" + reply = "How can I help?" self.append_assistant_message(reply) return reply @listen("goodbye") def handle_goodbye(self): - reply = "وداعاً!" + reply = "Goodbye!" self.append_assistant_message(reply) return reply @@ -79,130 +87,141 @@ session_id = str(uuid4()) flow = SupportFlow() try: - flow.handle_turn("أين طلبي؟", session_id=session_id) - flow.handle_turn("وماذا عن الإرجاع؟", session_id=session_id) + flow.handle_turn("Where is my order?", session_id=session_id) + flow.handle_turn("What about returns?", session_id=session_id) finally: - flow.finalize_session_traces() + flow.finalize_session_traces() # one trace link for the whole chat ``` +## بث جولة + +استخدم `stream_turn()` عندما تحتاج واجهة مستخدم أو بيئة تشغيل إلى أحداث منظّمة لجولة دردشة واحدة. يعيد جلسة بث تحتوي على إطارات مرتبة لتوجيه Flow، وأجزاء LLM، ونشاط الأدوات، ورسائل المحادثة. + +```python +stream = flow.stream_turn("Where is my order?", session_id=session_id) + +with stream: + for frame in stream.events: + if frame.channel == "llm" and frame.type == "llm_stream_chunk": + print(frame.content, end="", flush=True) + +result = stream.result +``` + +راجع [عقد بيئة البث](/edge/ar/learn/streaming-runtime-contract) للاطلاع على عقد الإطارات الكامل وقائمة القنوات. + ## دورة حياة الجولة -كل `handle_turn` يشغّل: +يشغّل كل `handle_turn` المسار التالي: -1. **`_configure_conversational_kickoff`** — دمج `session_id` / `user_message` في `inputs` وتطبيق `ConversationalConfig`. -2. **استعادة الحالة** — عند وجود `inputs["id"]` و`@persist`. +1. **إعداد الجولة** — يخزن رسالة المستخدم المعلقة، ويحل معرّف الجلسة، ويعيد ضبط تعقّب التنفيذ الخاص بالجولة، ثم يستدعي `kickoff(inputs={"id": session_id})`. +2. **استعادة الحالة** — إذا وُجد `inputs["id"]` وكان `@persist` مهيّأً، تُحمّل أحدث لقطة. 3. **`FlowStarted`** — في أول جولة للجلسة المؤجلة فقط. -4. **`prepare_conversational_turn`** — إضافة رسالة المستخدم و`last_user_message` وتصنيف اختياري. -5. **تنفيذ الرسم** — `@start` → `@router` → معالجات `@listen`. -6. **نهاية التشغيل** — يُتخطى `flow_finished` والتتبع لكل جولة عند التأجيل؛ `Agent.kickoff()` / crews لا تغلق دفعة الأب. +4. **ترطيب الجولة المعلقة** — تُضاف رسالة المستخدم إلى `state.messages`، وتُضبط `current_user_message` / `last_user_message`، ويُجرى التصنيف اختيارياً عند ضبط `intents` / `default_intents` مع `intent_llm`. +5. **تنفيذ الرسم** — طرق `@start` التي يعرّفها المستخدم (إن وجدت) → `route_conversation` (نقطة البدء/الموجّه المدمجة) → معالج `@listen` المختار. تستدعي `route_conversation` أيضاً المساعد القابل للتجاوز `conversation_start()`. +6. **نهاية التشغيل** — يُتخطى `flow_finished` لكل جولة وإنهاء التتبع عند تفعيل التأجيل؛ كما لا تغلق استدعاءات `Agent.kickoff()` المتداخلة أو crews دفعة الأب. -استدعِ **`append_assistant_message(reply)`** في المعالجات. سطر المستخدم محفوظ عبر `handle_turn` — لا تُضفه مرة أخرى. +ينبغي للمعالجات استدعاء **`append_assistant_message(reply)`** كي تتضمن `conversation_messages` في الجولة التالية نص المساعد. سطر المستخدم محفوظ بالفعل عبر `handle_turn` — لا تُضفه مرة أخرى في المعالجات. -## `ConversationalConfig` (افتراضيات على مستوى الصنف) +## نظرة عامة على الإعداد -عيّن على صنف `Flow` كـ `conversational_config: ClassVar[ConversationalConfig | None]`. +يؤدي تزيين صنف فرعي من `Flow` بـ `ConversationConfig` إلى إرفاق افتراضيات الدردشة وتفعيل الوضع المحادثاتي معاً. راجع [مرجع الحقول الكامل](#conversationconfig) أدناه. ويمكنك تجاوز التصنيف المسبق لكل جولة عبر `handle_turn(..., intents=..., intent_llm=...)`. -| الحقل | الافتراضي | الغرض | -|-------|-----------|--------| -| `default_intents` | `None` | تسميات outcome للتصنيف التلقائي قبل kickoff | -| `intent_llm` | `None` | نموذج التصنيف (مطلوب عند وجود intents) | -| `interactive_prompt` | `"You: "` | مطالبة `kickoff(interactive=True)` | -| `interactive_timeout` | `None` | مهلة لكل سطر في الوضع التفاعلي | -| `exit_commands` | `exit`, `quit` | كلمات إنهاء الوضع التفاعلي | -| `defer_trace_finalization` | `True` | إبقاء دفعة trace واحدة مفتوحة بين الجولات | +## مساعدات `ChatState` منخفضة المستوى -يمكن التجاوز لكل kickoff عبر `intents=` و`intent_llm=`. - -## `ChatState` (شكل الحالة الموصى به للحفظ) +تظل `ChatState` و`ConversationalConfig` القديمة ومساعدات `crewai.flow.conversation` قابلة للاستيراد للتنسيق المتقدم أو الاختبارات أو الأغلفة المخصصة. وهي منفصلة عن واجهتي `ConversationState` / `ConversationConfig` التجريبيتين، ولا تضيف وسيطي `user_message=` أو `session_id=` إلى `Flow.kickoff()`. ```python from crewai.flow import ChatState class MyChatState(ChatState): - # موروث: id, messages, last_user_message, last_intent, session_ready + # Inherited: id, messages, last_user_message, last_intent, session_ready research_turn_count: int = 0 custom_flag: bool = False ``` | الحقل | الدور | |-------|------| -| `id` | UUID الجلسة (مثل `session_id` / `inputs["id"]`) | -| `messages` | قائمة `{role, content}` لسجل LLM | +| `id` | UUID الجلسة (نفس `inputs["id"]`) | +| `messages` | `list` من `{role, content}` لسجل LLM | | `last_user_message` | آخر سطر مستخدم في هذه الجولة | | `last_intent` | تسمية المسار بعد التصنيف (إن وُجد) | -| `session_ready` | علم bootstrap لمرة واحدة | +| `session_ready` | علم bootstrap لمرة واحدة (الصلاحيات، وذاكرات التخزين المؤقت، وغيرها) | -`ConversationalInputs` هو `TypedDict` لـ `kickoff(inputs={...})`: `id`, `user_message`, `last_intent`. +`ConversationalInputs` هو `TypedDict` لمفاتيح `kickoff(inputs={...})` الاصطلاحية: `id` و`user_message` و`last_intent`. + +تخزن `ConversationState` التجريبية `messages` ككائنات `ConversationMessage`، وتوفر أيضاً `current_user_message` و`ended` و`events` و`agent_threads`. استخدم `conversation_messages` عند تمرير سجلها القانوني إلى LLM. ## API المحادثة على `Flow` -### معاملات `kickoff` / `kickoff_async` +### معاملات `handle_turn` | المعامل | الغرض | |---------|--------| -| `user_message` | نص هذه الجولة (أو `{"role": "user", "content": "..."}`) | +| `message` | نص هذه الجولة | | `session_id` | UUID المحادثة → `inputs["id"]` / `state.id` | -| `intents` | تسميات outcome لـ `classify_intent` قبل kickoff | +| `intents` | تسميات النتائج لـ `classify_intent` قبل kickoff | | `intent_llm` | LLM للتصنيف (مطلوب مع `intents`) | -| `interactive` | حلقة CLI عبر `ask()` (للعروض المحلية فقط) | -| `interactive_prompt` | مطالبة الوضع التفاعلي | -| `interactive_timeout` | مهلة `ask()` لكل سطر | -| `exit_commands` | كلمات إنهاء الوضع التفاعلي | -| `inputs` | حقول حالة إضافية | -| `restore_from_state_id` | استنساخ من flow محفوظ آخر | +| `**kickoff_kwargs` | تُمرر إلى `kickoff()` لخيارات مثل `input_files` و`from_checkpoint` و`restore_from_state_id` | + +### معاملات `kickoff` + +يقبل `Flow.kickoff()` كلاً من `inputs` و`input_files` و`from_checkpoint` و`restore_from_state_id`. مرر `inputs={"id": session_id}` عندما تحتاج إلى تنفيذ flow خام، لكن استخدم `handle_turn()` عندما يمثل الاستدعاء رسالة دردشة. ### سمات المثيل | السمة | الغرض | |-------|--------| -| `conversational_config` | افتراضيات `ConversationalConfig` على مستوى الصنف | -| `defer_trace_finalization` | علم المثيل؛ يُضبط تلقائياً من config عند kickoff | -| `suppress_flow_events` | يخفي لوحات console؛ **التتبع يُسجّل** | -| `stream` | بث؛ مع `ChatSession.handle_turn(..., stream=True)` | +| `conversational` | عيّنه على `True` لتفعيل الرسم المحادثاتي و`handle_turn()` | +| `defer_trace_finalization` | تجاوز اختياري على مستوى المثيل. وإلا تقرأ `_should_defer_trace_finalization()` القيمة `ConversationConfig.defer_trace_finalization`. | +| `suppress_flow_events` | يخفي لوحات flow في الطرفية ويمنع أحداث تنفيذ الطرق؛ وتظل أحداث بدء/انتهاء flow تصدر | +| `stream` | علم البث العام لـ Flow. استخدم `stream_turn()` للجولات المحادثية بدلاً من جمع هذا العلم مع `handle_turn()`. | ### طرق وخصائص | الاسم | الوصف | |------|--------| +| `append_assistant_message(content)` | إضافة رد مساعد مرئي للمستخدم إلى `state.messages` | | `append_message(role, content, **extra)` | إضافة إلى `state.messages` | | `conversation_messages` | سجل للقراءة فقط لاستدعاءات LLM | -| `classify_intent(text, outcomes, *, llm, context=None)` | تعيين outcome | -| `receive_user_message(text, *, outcomes=None, llm=None)` | إضافة رسالة مستخدم؛ `last_intent` اختياري | +| `classify_intent(text, outcomes, *, llm, context=None)` | تعيين النص إلى نتيجة واحدة (بنفس منطق الاختزال المستخدم في `@human_feedback`) | +| `receive_user_message(text, *, outcomes=None, llm=None)` | إضافة رسالة مستخدم، وضبط `last_intent` اختيارياً | | `finalize_session_traces()` | إصدار `flow_finished` المؤجل وإنهاء دفعة trace | -| `_should_defer_trace_finalization()` | هل يُؤجل إنهاء trace لكل جولة | +| `_should_defer_trace_finalization()` | hook متقدم/داخلي يحسم ما إذا كان إنهاء trace لكل جولة مؤجلاً | | `input_history` | سجل تدقيق مطالبات وردود `ask()` | ### مساعدات الوحدة (`crewai.flow.conversation`) +يمكن استيرادها من `crewai.flow.conversation` للاختبارات أو التنسيق المخصص. تستخدم هذه المساعدات بنية `ConversationalConfig` القديمة؛ كما تمسح `prepare_conversational_turn()` قيمة `last_intent`، بخلاف `handle_turn()` التجريبية التي تحتفظ بها كسياق للموجّه. + | الدالة | الوصف | |--------|--------| -| `normalize_kickoff_inputs(...)` | دمج kwargs المحادثة في `inputs` | +| `normalize_kickoff_inputs(inputs, user_message=..., session_id=...)` | دمج وسائط المحادثة في `inputs` | | `get_conversation_messages(flow)` | قراءة الرسائل من الحالة أو المخزن | -| `append_message(flow, ...)` | مثل طريقة المثيل | -| `prepare_conversational_turn(flow, ...)` | تهيئة الجولة (عادةً kickoff يستدعيها) | -| `receive_user_message(flow, ...)` | مثل طريقة المثيل | +| `append_message(flow, role, content, **extra)` | مثل طريقة المثيل | +| `prepare_conversational_turn(flow, user_message=..., intents=..., intent_llm=..., config=...)` | ترطيب الجولة منخفض المستوى للأغلفة المخصصة | +| `receive_user_message(flow, text, ...)` | مثل طريقة المثيل | | `set_state_field(flow, name, value)` | تعيين حقل dict أو Pydantic | | `get_conversational_config(flow)` | قراءة `conversational_config` | | `input_history_to_messages(entries)` | تحويل `input_history` لصيغة رسائل LLM | ## أنماط توجيه النية -### أ. تصنيف مسبق عبر `ConversationalConfig` (الأبسط) +### أ. تصنيف مسبق عبر `ConversationConfig` (الأبسط) -عيّن `default_intents` و`intent_llm`. كل kickoff يصنّف قبل `@router`؛ اقرأ `self.state.last_intent` في `route()`. +عيّن `default_intents` و`intent_llm`. يصنّف كل `handle_turn()` الرسالة الحالية مسبقاً. تكون الأولوية لنتيجة غير فارغة يعيدها `route_turn()` مخصص؛ وإلا تستخدم `route_conversation` النية المصنّفة للجولة الحالية. -### ب. تصنيف داخل `@router` (مطالبات أغنى) +### ب. تصنيف داخل `route_turn` (مطالبات أغنى) -عيّن `default_intents=None` ليضيف kickoff الرسالة فقط. في `route()` استدعِ `classify_intent`: +عيّن `default_intents=None` كي يضيف `handle_turn()` رسالة المستخدم فقط. داخل `route_turn()`، استدعِ `classify_intent` بمطالبة أو أوصاف مخصصة: ```python -@router(bootstrap) -def route(self): +def route_turn(self, context): intent = self.classify_intent( - self._routing_prompt(self.state.last_user_message), + self._routing_prompt(self.state.current_user_message), ("GREETING", "ORDER", "RESEARCH", "GOODBYE"), - llm=self.conversational_config.intent_llm or "gpt-4o-mini", + llm="gpt-4o-mini", ) self.state.last_intent = intent return intent @@ -212,70 +231,59 @@ def route(self): ## عندما ينتهي الـ flow ويستمر المستخدم -`FlowFinished` يعني أن **تنفيذ الرسم هذا** اكتمل. تستمر المحادثة بـ `kickoff` آخر ونفس `session_id`. `@persist` يستعيد `messages` والأعلام والسياق. +يُكمل كل `handle_turn()` تشغيل رسم واحد، وتستمر المحادثة عبر `handle_turn()` آخر يستخدم `session_id` نفسه. مع دورة حياة التتبع المؤجلة افتراضياً، يصدر ذلك التشغيل `conversation_turn_completed`، بينما يصدر `FlowFinished` مرة واحدة عندما تغلق `finalize_session_traces()` الجلسة. ويستعيد `@persist` الرسائل والأعلام والسياق. -**نمط الحفظ:** يُفضّل `@persist` على **خطوة نهائية واحدة** (مثل `finalize`) وليس على صنف `Flow` بالكامل. الحفظ على مستوى الصنف بعد كل method قد يفقد تحديثات المعالجات في نفس الجولة. +**نمط الحفظ:** يُفضّل `@persist` على **خطوة نهائية واحدة** (مثل `finalize`) وليس على صنف `Flow` بالكامل. يحفظ الاستمرار على مستوى الصنف بعد كل طريقة؛ وتستخدم `load_state` أحدث صف، وقد يكون لقطة في منتصف التشغيل (مثلاً بعد `bootstrap` مباشرة) لا تتضمن تحديثات المعالج من الجولة نفسها. لا تستخدم `@human_feedback` لأسطر المتابعة في الدردشة إلا عند الحاجة لموافقة بشرية على مخرجات خطوة محددة. ## `Flow` المحادثاتي (تجريبي) - - **ميزة تجريبية.** سطح `Flow` المحادثاتي (`conversational = True`، - `handle_turn`، `ConversationConfig`، `RouterConfig`، - `ConversationState`، الرسم البياني المدمج والمساعدات) يقع تحت - `crewai.experimental` وقد يتغير شكله قبل التخرج. ثبّت إصدار CrewAI إذا - كنت تعتمد على سلوك محدد، وراقب changelog للتحديثات الكاسرة. الملاحظات - والمشاكل مرحب بها. - - -فعّل الرسم المحادثاتي بتعيين `conversational = True` على صنف فرعي من `Flow`. عندئذٍ يُظهر `Flow` الأساسي رسم `@start` / `@router` / `converse_turn` / `end_conversation` مدمجاً، ويدير `state.messages`، ويُشغّل LLM التوجيه، ويبقي دفعة trace مفتوحة عبر الجولات. أنت تكتب **المسارات المخصصة** فقط؛ والإطار يتولى الباقي. +اشترك في رسم الدردشة المحادثاتي بتعيين `conversational = True` على صنف فرعي من `Flow` أو بتطبيق `@ConversationConfig(...)`. يوفر `Flow` الأساسي عندئذٍ `route_conversation` كنقطة البدء/الموجّه المدمجة، إضافة إلى مستمعي `converse_turn` و`end_conversation` و`answer_from_history_turn`. وهو يدير `state.messages`، ويمكنه تشغيل LLM للموجّه، ويبقي دفعة trace مفتوحة عبر الجولات. أنت تكتب **المسارات المخصصة**؛ والإطار يتولى الباقي. استخدمه عندما تريد دردشة متعددة الجولات مع موجّه قائم على LLM ومعالجات لكل مسار دون توصيل دورة الحياة يدوياً. استخدم `Flow[ChatState]` (النمط الأدنى مستوى في الأعلى) عندما تحتاج تحكماً كاملاً. ### مثال سريع ```python -from crewai import LLM, Flow +from crewai import 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(), # المسارات + الأوصاف تُكتشف تلقائياً من معالجات @listen -) +@ConversationConfig(defer_trace_finalization=True) class SupportFlow(Flow[ConversationState]): - conversational = True + def route_turn(self, context: dict) -> str | None: + message = (self.state.current_user_message or "").lower() + if "search" in message or "news" in message: + return "INTERNET_SEARCH" + if "docs" in message or "crewai" in message: + return "CREWAI_DOCS" + return "converse" @listen("INTERNET_SEARCH") def handle_internet_search(self) -> str: """Fresh web research, current news, real-time lookups.""" - ... + reply = "I would run the web research route here." 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.""" - ... + reply = "I would look up the CrewAI docs here." self.append_assistant_message(reply) return reply flow = SupportFlow() try: - flow.handle_turn("ماذا يمكنك أن تفعل؟") # يوجَّه إلى converse (مدمج) - flow.handle_turn("ابحث في الويب عن أخبار الذكاء الاصطناعي.") # يوجَّه إلى INTERNET_SEARCH - flow.handle_turn("لخص النتيجة الأولى.") # يعود إلى converse + flow.handle_turn("What can you do?") # routes to converse + flow.handle_turn("Search the web for AI news.") # routes to INTERNET_SEARCH + flow.handle_turn("Check the CrewAI docs.") # routes to CREWAI_DOCS finally: flow.finalize_session_traces() ``` @@ -297,7 +305,7 @@ def kickoff() -> None: |-------|-----------|-------| | `system_prompt` | `slices.conversational_system_prompt` من i18n | رسالة system يستخدمها `converse_turn` المدمج. مرر `""` للتعطيل التام. | | `llm` | `None` | LLM المحادثة (يستخدمه `converse_turn` وكاحتياطي للموجّه). | -| `router` | `None` | `RouterConfig` للتوجيه عبر LLM. بدونه، يسقط الـ flow دائماً إلى `converse`. | +| `router` | `None` | تجاوزات `RouterConfig` اختيارية. مع وجود مستمعين مخصصين وLLM قابل للحل، يُفعّل التوجيه تلقائياً حتى عند إغفال هذا الحقل. | | `answer_from_history_prompt` | افتراضي الإطار | رسالة system للمسار الاختياري `answer_from_history`. | | `answer_from_history_llm` | `None` | يُفعّل الاختصار `answer_from_history` عند تعيينه. | | `intent_llm` | `None` | LLM لمسار التصنيف المسبق القديم `intents=`/`default_intents`. | @@ -305,19 +313,38 @@ def kickoff() -> None: | `visible_agent_outputs` | `None` | `"all"` أو قائمة بأسماء الـ agents الذين تُرفع مخرجاتهم من `append_agent_result()` إلى رسائل عامة. | | `defer_trace_finalization` | `True` | يبقي دفعة trace واحدة مفتوحة عبر استدعاءات `handle_turn()`. | +عند عدم وجود مسارات مخصصة، تسقط الجولات إلى `converse`. ومع وجود مسارات مخصصة وLLM للمحادثة/الموجّه، ينشئ الإطار `RouterConfig` افتراضية؛ لا توفر واحدة صراحةً إلا لتخصيص المطالبة أو قائمة المسارات أو الأوصاف أو سلوك fallback. أما ضبط `default_intents` فيستخدم مسار التصنيف المسبق القديم. + +إذا لم يُهيأ LLM للمحادثة، يعيد `converse_turn` المدمج عنصراً نائباً للإعداد بدلاً من توليد إجابة. + ### `RouterConfig` وفهرس المسارات المُولَّد تلقائياً ```python -RouterConfig( - prompt="تأطير اختياري للنطاق (سياسة، صوت، شخصية).", - response_format=MyRoute, # اختياري؛ يُولَّد تلقائياً عند الإغفال - llm=ROUTER_LLM, # يسقط إلى ConversationConfig.llm - routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # اختياري؛ يُستنتج من المستمعين +from typing import Literal + +from pydantic import BaseModel + +from crewai import LLM +from crewai.experimental.conversational import RouterConfig + + +class MyRoute(BaseModel): + intent: Literal["INTERNET_SEARCH", "CREWAI_DOCS", "converse"] + + +ROUTER_LLM = LLM(model="gpt-4o-mini") + + +router_config = RouterConfig( + prompt="Optional domain framing (policy, voice, persona).", + response_format=MyRoute, # optional; auto-generated otherwise + llm=ROUTER_LLM, # falls back to ConversationConfig.llm + routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # optional; inferred from listeners route_descriptions={ - "INTERNET_SEARCH": "تجاوز الـ docstring لهذا المسار فقط.", + "INTERNET_SEARCH": "Override the docstring for this one route.", }, - default_intent="converse", # يُستخدم عند فشل LLM أو غيابه - fallback_intent="converse", # يُستخدم عندما يعيد LLM مساراً غير صالح + default_intent="converse", # used when LLM call fails or no LLM available + fallback_intent="converse", # used when LLM returns an invalid route intent_field="intent", ) ``` @@ -326,12 +353,16 @@ RouterConfig( 1. `RouterConfig.route_descriptions[label]` — تجاوز صريح. 2. `Flow.builtin_route_descriptions[label]` — نص جاهز من الإطار لـ `converse` و`end` و`answer_from_history` (مصاغ لـ LLM التوجيه). -3. أول سطر غير فارغ من docstring معالج `@listen(label)`. -4. فارغ (المسار يظهر في الفهرس بلا وصف). +3. قيمة `description` المعلنة للطريقة (تستخدمها التدفقات التعريفية وإسقاطات DSL). +4. أول سطر غير فارغ من docstring معالج `@listen(label)`. +5. فارغ (المسار يظهر في الفهرس بلا وصف). عملياً، **إضافة مسار جديد = `@listen("X")` + docstring من سطر واحد**: ```python +from crewai.flow import listen + + @listen("INTERNET_SEARCH") def handle_internet_search(self) -> str: """Fresh web research, current news, real-time lookups.""" @@ -350,13 +381,34 @@ Routes: `RouterConfig.prompt` مخصص لـ **تأطير النطاق** (شخصية المساعد، قواعد العمل، النبرة). فهرس المسارات يُبنى تلقائياً — لا تُدرج المسارات في `prompt`؛ سيختل التزامن لحظة إضافة معالج جديد. +### تسمية المعالجات + +السلسلة النصية في `@listen("…")` هي **تسمية مسار للموجّه** (اسم حدث)، وليست اسم طريقة Python. تتشارك تسميات المسارات وأحداث اكتمال الطرق مساحة مشغلات واحدة، ولذلك تؤدي تسمية المعالج باسم مساره نفسه إلى إعادة تشغيل المعالج في حلقة. + +استخدم اسماً مختلفاً للطريقة — تستخدم أمثلة التوثيق بادئة `handle_*`: + +```python +@listen("create_video") +def handle_create_video(self) -> str: + """User wants a new video.""" + ... +``` + +لا تكرر تسمية المسار في اسم الطريقة: + +```python +@listen("create_video") +def create_video(self) -> str: # rejected at flow instantiation + ... +``` + ### المسارات المدمجة | المسار | المعالج | الغرض | |--------|---------|-------| | `converse` | `converse_turn` | معالج الدردشة الافتراضي. يستدعي `ConversationConfig.llm` بـ system prompt + التاريخ القانوني للرسائل. | | `end` | `end_conversation` | يضبط `state.ended = True` ويُصدر رد إنهاء. | -| `answer_from_history` | `answer_from_history_turn` | اختياري. يُوجَّه إليه عندما يكون `ConversationConfig.answer_from_history_llm` مُعيَّناً ويمكن الإجابة على الرسالة من التاريخ فقط. | +| `answer_from_history` | `answer_from_history_turn` | اختياري. بعد وجود رسالتين على الأقل، يُوجَّه إليه عندما يقرر `ConversationConfig.answer_from_history_llm` أن الرسالة الحالية يمكن الإجابة عنها من السجل. | يمكنك تجاوز أي من هذه بتعريف معالج بنفس الاسم في الصنف الفرعي. @@ -366,7 +418,7 @@ Routes: 1. يعيد ضبط تعقّب التنفيذ لكل جولة (`_completed_methods`, `_method_outputs`) ليُعاد تشغيل الرسم — بدون ذلك، استدعاءات `kickoff` المتكررة على نفس النسخة ستُحدث دائرة قصر من الجولة الثانية لأن `Flow.kickoff_async` يعتبر `inputs={"id": ...}` استعادة من نقطة تفتيش. 2. يُلحق رسالة المستخدم بـ `state.messages` ويضبط `current_user_message` / `last_user_message`. يُحافَظ على `last_intent` **من الجولة السابقة** كي يستخدمها LLM التوجيه كإشارة. -3. يُشغّل `conversation_start` → `route_conversation` → معالج `@listen` المختار. +3. يُشغّل طرق `@start` التي يعرّفها المستخدم (إن وجدت)، ثم `route_conversation` كنقطة البدء/الموجّه المدمجة، ثم معالج `@listen` المختار. وتستدعي `route_conversation` المساعد القابل للتجاوز `conversation_start()`. 4. يخزّن الموجّه قراره في `state.last_intent` (يكون مرئياً لسياق التوجيه في الجولة التالية). 5. إذا أعاد معالجك سلسلة نصية ولم يستدعِ `append_assistant_message`، فإن `handle_turn` يُلحقها نيابةً عنك. @@ -389,6 +441,8 @@ flow.chat() 4. يطبع نتيجة المساعد. 5. ينهي traces الجلسة المؤجلة داخل كتلة `finally`. +يُفعّل `chat(defer_trace_finalization=True)` مؤقتاً علم التأجيل على مستوى المثيل للـ REPL، ثم يعيد قيمته السابقة عند الخروج. + خصص سلوك الطرفية عبر I/O قابل للحقن: ```python @@ -407,6 +461,12 @@ flow.chat( لتشغيل آثار جانبية (إعداد ناقل أحداث، قياس عن بُعد) في كل قرار توجيه، تجاوز `route_turn`: ```python +from typing import Any + +from crewai import Flow +from crewai.experimental.conversational import ConversationState + + class SupportFlow(Flow[ConversationState]): conversational = True @@ -415,7 +475,7 @@ class SupportFlow(Flow[ConversationState]): return super().route_turn(context) ``` -لتجاوز موجّه LLM واختيار مسار برمجياً، أعد سلسلة نصية من `route_turn`؛ إعادة `None` تسقط إلى `_route_with_config(...)`. +لتجاوز موجّه LLM بالكامل واختيار مسار برمجياً، أعد سلسلة نصية غير فارغة من `route_turn`. لا يؤدي إرجاع قيمة falsy من التجاوز إلى استدعاء `_route_with_config()`؛ بل يسقط التوجيه إلى النية المصنّفة مسبقاً لهذه الجولة، ثم إلى `answer_from_history` عندما تكون مؤهلة، وأخيراً إلى `converse`. تكون `last_intent` من الجولة السابقة متاحة في سياق الموجّه، لكنها لا تُعاد أبداً كـ fallback. ### `append_assistant_message` و`append_agent_result` @@ -428,7 +488,7 @@ class SupportFlow(Flow[ConversationState]): ## تعريف تدفق محادثاتي بصيغة JSON/YAML -يمكن لـ [التدفق التعريفي](/edge/en/concepts/cli) أن يكون محادثاتيًا أيضًا. أضف كتلة `conversational` في المستوى الأعلى وعرّف مساراتك الخاصة كطرق تستمع (`listen`) إلى تسمية مسار: +يمكن لـ [التدفق التعريفي](/edge/ar/concepts/cli) أن يكون محادثاتيًا أيضًا. أضف كتلة `conversational` في المستوى الأعلى وعرّف مساراتك الخاصة كطرق تستمع (`listen`) إلى تسمية مسار: ```yaml schema: crewai.flow/v1 @@ -453,15 +513,17 @@ methods: input: "${state.current_user_message}" ``` -تعريف الكتلة هو الاشتراك نفسه — القيمة الافتراضية لـ `enabled` هي `true`. اضبطها على `enabled: false` للاحتفاظ بالإعدادات مع إيقاف المحادثة. +تعريف الكتلة هو الاشتراك نفسه — القيمة الافتراضية لـ `enabled` هي `true`. اضبطها على `enabled: false` للاحتفاظ بالإعدادات مع إيقاف المحادثة. يؤدي ذلك أيضاً إلى تعطيل إنشاء الطرق المدمجة، ولذلك يجب أن توفر التعريفة رسماً عادياً غير محادثاتي. تُوفَّر لك ثلاثة أشياء: | المُوفَّر | التفاصيل | |----------|--------| | الرسم البياني المدمج | تُضاف `route_conversation` و`converse_turn` و`end_conversation` و`answer_from_history_turn` تلقائيًا. عرّف طريقة بأحد هذه الأسماء لتجاوزها. | -| حالة المحادثة | تُستخدم `ConversationState` عندما لا تحتوي التعريفة على كتلة `state`. لإضافة حقول، وجّه `state` إلى نموذج Pydantic يرث من `ConversationState`. | -| كتالوج المسارات | يُبنى من الطرق التي تعلن تسمية `listen`. وصف كل طريقة (`description`) هو ما يقرأه نموذج التوجيه عند الاختيار بين المسارات. | +| حالة المحادثة | تُستخدم `ConversationState` عند عدم وجود كتلة `state`. وتُركّب حالة Pydantic ذات `ref` أو `json_schema` تلقائياً مع الحقول المحادثية؛ ولا يلزم أن ترث من `ConversationState`. | +| كتالوج المسارات | يُستنتج من الطرق غير الموجّهة التي تحمل تسميات `listen`، مع استبعاد المسارات الداخلية. تتبع الأوصاف ترتيب الأولوية أعلاه، ويمكن لـ `router.routes` الصريحة تقييد الخيارات. | + +تقبل حقول `llm` و`router.llm` و`intent_llm` و`answer_from_history_llm` التعريفية إما معرّف نموذج أو خريطة إعدادات مثل `{model: openai/gpt-4o-mini, max_tokens: 512}`. وتدعم كتلة `conversational` أيضاً `default_intents` و`answer_from_history_prompt` و`visible_agent_outputs` و`defer_trace_finalization` وحقول `RouterConfig` الموضحة أعلاه. شغّله من Python بنفس واجهات الجولة المستخدمة مع تدفق محادثاتي معرّف بصنف: @@ -484,15 +546,16 @@ finally: | غير قابل للتعبير | استخدم بدلًا منه | |-----------------|-------------| -| مثيل `LLM` حي أو `BaseLLM` مخصص | سلسلة معرّف النموذج، مثل `gpt-4o-mini` | +| مثيل `LLM` حي أو `BaseLLM` مخصص | سلسلة معرّف نموذج أو خريطة إعدادات ثابتة | | `router.response_format` كصنف نموذج | احذفه؛ يولّد الإطار واحدًا. يُتجاهل المرجع أو المخطط مع تحذير | -| تجاوزات `route_turn()` / `can_answer_from_history()` | اكتب التدفق بلغة Python، أو وجّه `do` لطريقة إلى مرجع `call: code` | +| تجاوز `route_turn()` | اكتب Flow بلغة Python، أو استبدل طريقة `route_conversation` التعريفية بإجراء `call: code` / expression | +| تجاوز `can_answer_from_history()` | اكتب Flow بلغة Python؛ واضبط توجيه السجل القياسي عبر `conversational.answer_from_history_llm` | -يفتح `crewai run` واجهة المحادثة النصية للتدفق المحادثاتي التعريفي — نفس الواجهة التي يحصل عليها تدفق محادثاتي مكتوب بلغة Python. تحتاج حلقة المحادثة إلى طرفية، لذا يُبلّغ التشغيل بدون طرفية بذلك بدلًا من تنفيذ جولة واحدة؛ شغّله من Python هناك عبر `handle_turn()` أو `stream_turn()`. والتدفق الذي يستخدم `@human_feedback` أيضًا يعمل على حلقة طرفية، لأن زمن التشغيل يجمع الملاحظات عبر مُطالبة حاجبة لا تستطيع الواجهة النصية خدمتها. لا يُقبل `--inputs` مع التدفق المحادثاتي — فمدخل كل دورة هو الرسالة التي تكتبها — واستئناف جلسة عبر المعرّف غير موصول بواجهة سطر الأوامر بعد؛ استخدم `flow.handle_turn(message, session_id=...)` من Python لذلك. +يفتح `crewai run` واجهة المحادثة النصية للتدفق المحادثاتي التعريفي — نفس الواجهة التي يحصل عليها Flow محادثاتي مكتوب بلغة Python. تحتاج حلقة المحادثة إلى طرفية، ولذلك يخرج التشغيل بدون طرفية برمز غير صفري مع إرشادات بدلاً من تنفيذ جولة واحدة؛ شغّله من Python هناك عبر `handle_turn()` أو `stream_turn()`. وتعمل الطريقة التعريفية ذات كتلة `human_feedback:` (وفي Python: ‏`@human_feedback`) على REPL طرفي، لأن runtime يجمع الملاحظات بمطالبة حاجزة لا تستطيع TUI خدمتها. لا يُقبل `--inputs` مع Flow محادثاتي — فمدخل كل جولة هو الرسالة التي تكتبها — واستئناف جلسة حسب المعرّف غير موصول بواجهة CLI بعد؛ استخدم `flow.handle_turn(message, session_id=...)` من Python لذلك. ## التتبع عبر الجولات -مع `defer_trace_finalization=True` (افتراضي في `ConversationalConfig`): +مع `defer_trace_finalization=True` (افتراضي في `ConversationConfig`): - **دفعة trace واحدة** لجلسة الدردشة. - **`flow_started`** في الجولة الأولى فقط؛ **`flow_finished`** مرة في `finalize_session_traces()`. @@ -503,17 +566,30 @@ finally: flow.chat(session_id=session_id) ``` -`flow.chat()` يستدعي `finalize_session_traces()` نيابةً عنك. عندما تملك الحلقة عبر `handle_turn()` أو `kickoff(...)`، استدعِ `finalize_session_traces()` عند انتهاء الجلسة. +`flow.chat()` يستدعي `finalize_session_traces()` نيابةً عنك. عندما تملك الحلقة عبر `handle_turn()`، استدعِ `finalize_session_traces()` عند انتهاء الجلسة. -`suppress_flow_events=True` يخفي لوحات Rich فقط؛ أحداث trace والـ methods تُصدر. +يخفي `suppress_flow_events=True` لوحات Rich ويمنع أحداث تنفيذ الطرق. وتظل أحداث بدء/انتهاء Flow تصدر، فيبقى بالإمكان تتبع دورة حياة Flow الخارجية، بينما تُحذف spans الطرق الفردية. ### دورة حياة trace لـ `Flow` المحادثاتي -يستخدم [`Flow` المحادثاتي](#flow-المحادثاتي-تجريبي) التجريبي نفس دورة حياة tracing: `defer_trace_finalization` افتراضياً `True`، فيبقي كل `handle_turn()` أثر الجلسة مفتوحاً. أنهِ دوماً عند نهاية الجلسة — لُف حلقتك بـ `try/finally` واستدعِ `flow.finalize_session_traces()` عند الخروج. بدون ذلك، تبقى الدفعة مفتوحة وقد لا تُصدَّر آخر محادثة أبداً. +يستخدم [`Flow` المحادثاتي](#flow-المحادثاتي-تجريبي) التجريبي دورة حياة التتبع نفسها: القيمة الافتراضية لـ `defer_trace_finalization` هي `True`، ولذلك يبقي كل `handle_turn()` trace الجلسة مفتوحاً. تمنع الجولات المؤجلة أيضاً إصدار `flow_failed` لكل جولة؛ وعند حدوث خطأ في جولة أو إلغاء الجلسة، أنهِ الجلسة صراحةً. يغلق ذلك الدفعة بحدث `FlowFinished` على مستوى الجلسة بدلاً من حدث `FlowFailed` لكل جولة. لُف REPL/الحلقة دائماً بـ `try/finally` واستدعِ `flow.finalize_session_traces()` عند الخروج. بدون ذلك، تبقى دفعة trace مفتوحة وقد لا تُصدَّر المحادثة النهائية أبداً. ## البث -اضبط `stream = True` على صنف `Flow`. عندئذٍ يُصدر `kickoff(...)` أحداث `assistant_delta` (وما يرتبط بها) عبر ناقل الأحداث القياسي. +استخدم `stream_turn()` للواجهات المحادثية، وكرّر عبر كائنات `StreamFrame` المرتبة التي يعيدها: + +```python +stream = flow.stream_turn("Where is my order?", session_id=session_id) + +with stream: + for frame in stream.events: + if frame.channel == "llm" and frame.type == "llm_stream_chunk": + print(frame.content, end="", flush=True) + +reply = stream.result +``` + +بالنسبة إلى Flow غير محادثاتي، يؤدي ضبط `stream = True` إلى جعل `kickoff()` يعيد `StreamSession`. لا تضبط `flow.stream = True` عند استخدام `handle_turn()`؛ إذ تملك `stream_turn()` دورة حياة البث المحادثاتي. ## الاستيراد @@ -528,10 +604,15 @@ from crewai.flow import ( router, start, ) +from crewai.flow.conversation import prepare_conversational_turn +from crewai.experimental.conversational import ( + ConversationConfig, + ConversationState, + RouterConfig, +) ``` ## مراجع - [إتقان إدارة حالة Flow](/ar/guides/flows/mastering-flow-state) - [أنشئ أول Flow](/ar/guides/flows/first-flow) -- Demo: `lib/crewai/runner_conversational_flow_simple.py` — REPL بسيط مع `RESEARCH` ووكيل Exa diff --git a/docs/edge/en/guides/flows/conversational-flows.mdx b/docs/edge/en/guides/flows/conversational-flows.mdx index 3735fede4..c76b1e75c 100644 --- a/docs/edge/en/guides/flows/conversational-flows.mdx +++ b/docs/edge/en/guides/flows/conversational-flows.mdx @@ -1,19 +1,27 @@ --- title: Conversational Flows -description: Build multi-turn chat apps with handle_turn per turn, message history, intent routing, tracing, and WebSocket bridges. +description: Build multi-turn chat apps with handle_turn per turn, message history, intent routing, tracing, and structured streaming. icon: comments mode: "wide" --- + + **This is an experimental feature.** The conversational `Flow` surface + (`conversational = True`, `handle_turn`, `stream_turn`, `ConversationConfig`, + `RouterConfig`, `ConversationState`, and the built-in graph) lives under + `crewai.experimental` and may change before it graduates. Pin your CrewAI + version if you depend on specific behavior. + + ## 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 routing, deferred tracing, UI bridges, and a local `flow.chat()` REPL for conversational flows. +Conversational apps treat each user line as a **new flow run** with the **same session id**. CrewAI adds helpers for message history, optional intent routing, deferred tracing, structured turn streaming, and a local `flow.chat()` REPL. | Concept | Implementation | |---------|----------------| | Session id | `handle_turn(..., session_id=...)` → `kickoff(inputs={"id": ...})` → `state.id` | | User line | `handle_turn(message)` appends to `state.messages` before the graph runs | -| Turn complete | `FlowFinished` for **this run** only; chat continues on the next `handle_turn` | +| Turn complete | `conversation_turn_completed`; with default trace deferral, `FlowFinished` waits for `finalize_session_traces()` | | Full-session trace | `ConversationConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | ## Turn APIs @@ -30,7 +38,8 @@ Use **`flow.handle_turn(message, session_id=...)`** for every user message from | `kickoff(inputs={...})` | Advanced flow execution without conversational turn handling | | `ask()` | Blocking prompt **inside** one step (wizard, clarification) | | `@human_feedback` | Approve/reject **a step output** — not the next chat line | -| `ChatSession.handle_turn(...)` | Transport layer over `handle_turn` (SSE / WebSocket) | + +`handle_turn()`, `stream_turn()`, and `chat()` raise `ValueError` unless conversational mode is enabled. Applying `@ConversationConfig(...)` enables it automatically; otherwise set `conversational = True`. ## Quick start @@ -47,8 +56,6 @@ from crewai.experimental.conversational import ( @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 "order" in message: @@ -96,12 +103,12 @@ stream = flow.stream_turn("Where is my order?", session_id=session_id) with stream: for frame in stream.events: if frame.channel == "llm" and frame.type == "llm_stream_chunk": - print(frame.data.get("chunk", ""), end="", flush=True) + print(frame.content, end="", flush=True) result = stream.result ``` -For the full frame contract, channel list, and async API, see [Streaming Runtime Contract](/edge/en/learn/streaming-runtime-contract). +For the full frame contract and channel list, see [Streaming Runtime Contract](/edge/en/learn/streaming-runtime-contract). ## Turn lifecycle @@ -111,29 +118,18 @@ Each `handle_turn` runs this pipeline: 2. **State restore** — if `inputs["id"]` exists and `@persist` is configured, loads the latest snapshot. 3. **`FlowStarted`** — emitted on the first deferred session turn only. 4. **Pending turn hydration** — appends the user message to `state.messages`, sets `current_user_message` / `last_user_message`, and optionally classifies when `intents` / `default_intents` + `intent_llm` are set. -5. **Graph execution** — `conversation_start` → `route_conversation` → the selected `@listen` handler. +5. **Graph execution** — user-defined `@start` methods (if any) → `route_conversation` (the built-in start/router) → the selected `@listen` handler. `route_conversation` also calls the overridable `conversation_start()` helper. 6. **End of run** — per-turn `flow_finished` and trace finalization are **skipped** when deferral is enabled; nested `Agent.kickoff()` / crews do not close the parent batch either. Handlers should call **`append_assistant_message(reply)`** so the next turn’s `conversation_messages` includes assistant text. The user line is already stored by `handle_turn` — do not append it again in handlers. -## `ConversationConfig` (class-level defaults) +## Configuration overview -Decorate your conversational `Flow` subclass with `ConversationConfig`. - -| Field | Default | Purpose | -|-------|---------|---------| -| `system_prompt` | Framework default | System message used by the built-in `converse_turn`. | -| `llm` | `None` | Conversation LLM used by `converse_turn` and as router fallback. | -| `router` | `None` | `RouterConfig` for LLM-driven routing. | -| `intent_llm` | `None` | LLM for `intents=` / `default_intents` pre-classification. | -| `default_intents` | `None` | Outcome labels for pre-classification. | -| `defer_trace_finalization` | `True` | Keep one trace batch open across `handle_turn()` calls. | - -Override pre-classification per turn with `handle_turn(..., intents=..., intent_llm=...)`. +Decorating a `Flow` subclass with `ConversationConfig` both attaches the chat defaults and enables conversational mode. See the [full field reference](#conversationconfig) below. Override pre-classification per turn with `handle_turn(..., intents=..., intent_llm=...)`. ## Lower-level `ChatState` helpers -`ChatState`, `ConversationalConfig`, and `crewai.flow.conversation` helpers are still importable for advanced orchestration, tests, or custom wrappers. They do not add `user_message=` or `session_id=` keyword arguments to `Flow.kickoff()`. +`ChatState`, the legacy `ConversationalConfig`, and `crewai.flow.conversation` helpers are still importable for advanced orchestration, tests, or custom wrappers. They are separate from the experimental `ConversationState` / `ConversationConfig` API and do not add `user_message=` or `session_id=` keyword arguments to `Flow.kickoff()`. ```python from crewai.flow import ChatState @@ -155,6 +151,8 @@ class MyChatState(ChatState): `ConversationalInputs` is a `TypedDict` for conventional `kickoff(inputs={...})` keys: `id`, `user_message`, `last_intent`. +The experimental `ConversationState` stores `messages` as `ConversationMessage` objects and additionally provides `current_user_message`, `ended`, `events`, and `agent_threads`. Use `conversation_messages` when passing its canonical history to an LLM. + ## `Flow` conversational API ### `handle_turn` parameters @@ -176,9 +174,9 @@ class MyChatState(ChatState): | Attribute | Purpose | |-----------|---------| | `conversational` | Set to `True` to enable the conversational graph and `handle_turn()` | -| `defer_trace_finalization` | Instance flag; set automatically from config on `handle_turn()` | -| `suppress_flow_events` | Hides console flow panels; **tracing still records** method/flow events | -| `stream` | Enable streaming; use with `ChatSession.handle_turn(..., stream=True)` | +| `defer_trace_finalization` | Optional instance override. Otherwise `_should_defer_trace_finalization()` reads `ConversationConfig.defer_trace_finalization`. | +| `suppress_flow_events` | Hides console flow panels and suppresses method execution events; flow start/finish events still emit | +| `stream` | Generic Flow streaming flag. For conversational turns, use `stream_turn()` instead of combining this flag with `handle_turn()`. | ### Methods and properties @@ -190,12 +188,12 @@ class MyChatState(ChatState): | `classify_intent(text, outcomes, *, llm, context=None)` | Map text to one outcome (same collapse logic as `@human_feedback`) | | `receive_user_message(text, *, outcomes=None, llm=None)` | Append user message; optionally set `last_intent` | | `finalize_session_traces()` | Emit deferred `flow_finished` and finalize the session trace batch | -| `_should_defer_trace_finalization()` | Whether this flow defers per-turn trace finalization | +| `_should_defer_trace_finalization()` | Advanced/internal hook that resolves whether per-turn trace finalization is deferred | | `input_history` | Audit trail of `ask()` prompts and responses | ### Module helpers (`crewai.flow.conversation`) -Importable for tests or custom orchestration: +Importable from `crewai.flow.conversation` for tests or custom orchestration. These helpers use the legacy `ConversationalConfig` shape; `prepare_conversational_turn()` also clears `last_intent`, unlike experimental `handle_turn()`, which preserves it as router context. | Function | Description | |----------|-------------| @@ -212,7 +210,7 @@ Importable for tests or custom orchestration: ### A. Pre-classify via `ConversationConfig` (simplest) -Set `default_intents` and `intent_llm`. Each `handle_turn()` runs classification before routing; read `self.state.last_intent` in `route_turn()`. +Set `default_intents` and `intent_llm`. Each `handle_turn()` pre-classifies the current message. A non-empty result returned by a custom `route_turn()` takes precedence; otherwise `route_conversation` uses the current turn's classified intent. ### B. Classify inside `route_turn` (richer prompts) @@ -233,7 +231,7 @@ Use **`@listen("RESEARCH")`** (or similar) for steps that run `Agent.kickoff()` ## When the flow finishes but the user keeps chatting -`FlowFinished` means **this graph run** completed. The conversation continues with another `handle_turn()` and the same `session_id`. `@persist` restores `messages`, flags, and context. +Each `handle_turn()` completes one graph run, and the conversation continues with another `handle_turn()` using the same `session_id`. With the default deferred trace lifecycle, that run emits `conversation_turn_completed`, while `FlowFinished` is emitted once when `finalize_session_traces()` closes the session. `@persist` restores `messages`, flags, and context. **Persist pattern:** prefer `@persist` on a **single terminal step** (for example `finalize`) rather than on the whole `Flow` class. Class-level persist saves after every method; `load_state` uses the latest row, which may be a mid-run snapshot (for example right after `bootstrap`) and miss handler updates from the same turn. @@ -241,16 +239,7 @@ Do **not** use `@human_feedback` for follow-up chat lines unless a human must ap ## Conversational `Flow` (experimental) - - **This is an experimental feature.** The conversational `Flow` surface - (`conversational = True`, `handle_turn`, `ConversationConfig`, - `RouterConfig`, `ConversationState`, the built-in graph + helpers) lives - under `crewai.experimental` and may change shape before it graduates. - Pin your CrewAI version if you depend on specific behavior, and watch the - changelog for breaking updates. Open issues / feedback welcome. - - -Opt into the conversational chat graph by setting `conversational = True` on a `Flow` subclass. The base `Flow` then ships a built-in `@start` / `@router` / `converse_turn` / `end_conversation` graph, manages `state.messages`, can drive a router LLM, and keeps the trace batch open across turns. You write the **custom routes**; the framework owns the rest. +Opt into the conversational chat graph by setting `conversational = True` on a `Flow` subclass or applying `@ConversationConfig(...)`. The base `Flow` then supplies `route_conversation` as the built-in start/router plus the `converse_turn`, `end_conversation`, and `answer_from_history_turn` listeners. It manages `state.messages`, can drive a router LLM, and keeps the trace batch open across turns. You write the **custom routes**; the framework owns the rest. Use this when you want a multi-turn chat with a router and per-route handlers without wiring the lifecycle yourself. Use `Flow[ChatState]` (the lower-level pattern above) when you need full control. @@ -267,8 +256,6 @@ from crewai.experimental.conversational import ( @ConversationConfig(defer_trace_finalization=True) class SupportFlow(Flow[ConversationState]): - conversational = True - def route_turn(self, context: dict) -> str | None: message = (self.state.current_user_message or "").lower() if "search" in message or "news" in message: @@ -318,7 +305,7 @@ Class decorator that attaches per-class chat defaults. |-------|---------|---------| | `system_prompt` | `slices.conversational_system_prompt` from i18n | System message used by the built-in `converse_turn`. Pass `""` to opt out entirely. | | `llm` | `None` | Conversation LLM (used by `converse_turn` and as router fallback). | -| `router` | `None` | `RouterConfig` for LLM-driven routing. Without it, the flow always falls through to `converse`. | +| `router` | `None` | Optional `RouterConfig` overrides. With custom listeners and a resolvable LLM, routing auto-enables even when this is omitted. | | `answer_from_history_prompt` | Framework default | System message for the optional `answer_from_history` route. | | `answer_from_history_llm` | `None` | Enables the `answer_from_history` short-circuit when set. | | `intent_llm` | `None` | LLM for legacy `intents=`/`default_intents` pre-classification. | @@ -326,6 +313,10 @@ Class decorator that attaches per-class chat defaults. | `visible_agent_outputs` | `None` | `"all"`, or a list of agent names whose `append_agent_result()` calls should be promoted to public assistant messages. | | `defer_trace_finalization` | `True` | Keep one trace batch open across `handle_turn()` calls. | +With no custom routes, turns fall through to `converse`. With custom routes and a conversation/router LLM, the framework synthesizes a default `RouterConfig`; provide one explicitly only to customize its prompt, route list, descriptions, or fallback behavior. Setting `default_intents` uses the legacy pre-classification path instead. + +If no conversation LLM is configured, the built-in `converse_turn` returns a configuration placeholder rather than generating an answer. + ### `RouterConfig` and the auto-built route catalog ```python @@ -361,8 +352,9 @@ The router prompt that gets sent to the LLM is built automatically. For each rou 1. `RouterConfig.route_descriptions[label]` — explicit override. 2. `Flow.builtin_route_descriptions[label]` — framework-canned text for `converse`, `end`, `answer_from_history` (phrased for the router LLM). -3. First non-empty line of the `@listen(label)` handler's docstring. -4. Empty (the route is listed without a description). +3. The method's declared `description` (used by declarative flows and DSL projections). +4. First non-empty line of the `@listen(label)` handler's docstring. +5. Empty (the route is listed without a description). So in practice, **adding a new route is `@listen("X")` + a one-line docstring**: @@ -415,7 +407,7 @@ Routes: |-------|---------|---------| | `converse` | `converse_turn` | Default chat handler. Calls `ConversationConfig.llm` with the system prompt + canonical message history. | | `end` | `end_conversation` | Sets `state.ended = True` and emits a terminator reply. | -| `answer_from_history` | `answer_from_history_turn` | Optional. Routes here when `ConversationConfig.answer_from_history_llm` is set and the message can be answered from existing history. | +| `answer_from_history` | `answer_from_history_turn` | Optional. After at least two messages exist, routes here when `ConversationConfig.answer_from_history_llm` determines the current message can be answered from history. | You can override any of these by defining a same-named handler in your subclass. @@ -425,7 +417,7 @@ You can override any of these by defining a same-named handler in your subclass. 1. Resets per-execution tracking (`_completed_methods`, `_method_outputs`) so the graph re-runs — without this, repeated `kickoff` calls on the same flow instance would short-circuit on turn 2+ because `Flow.kickoff_async` treats `inputs={"id": ...}` as a checkpoint restore. 2. Appends the user message to `state.messages`, sets `current_user_message` / `last_user_message`. `last_intent` is **preserved from the prior turn** so the router LLM can use it as a signal. -3. Runs `conversation_start` → `route_conversation` → the chosen `@listen` handler. +3. Runs user-defined `@start` methods (if any), then `route_conversation` as the built-in start/router, then the chosen `@listen` handler. `route_conversation` invokes the overridable `conversation_start()` helper. 4. The router stores its decision in `state.last_intent` (visible to the next turn's router context). 5. If your handler returned a string and didn't already call `append_assistant_message`, `handle_turn` appends it for you. @@ -448,6 +440,8 @@ It handles the common local loop: 4. Prints the assistant result. 5. Finalizes deferred session traces in a `finally` block. +`chat(defer_trace_finalization=True)` temporarily enables the instance deferral flag for the REPL and restores its prior value on exit. + Customize the terminal behavior with injectable I/O: ```python @@ -480,7 +474,7 @@ class SupportFlow(Flow[ConversationState]): return super().route_turn(context) ``` -To bypass the LLM router entirely and pick a route programmatically, return a string from `route_turn`; returning `None` falls back to `_route_with_config(...)`. +To bypass the LLM router entirely and pick a route programmatically, return a non-empty string from `route_turn`. A falsy return does **not** invoke `_route_with_config()` from your override; routing falls through to this turn's pre-classified intent, then `answer_from_history` when eligible, and finally `converse`. A previous turn's `last_intent` is available in router context but is never replayed as a fallback. ### `append_assistant_message` and `append_agent_result` @@ -518,15 +512,17 @@ methods: input: "${state.current_user_message}" ``` -Declaring the block is the opt-in — `enabled` defaults to `true`. Set `enabled: false` to keep the configuration while turning chat off. +Declaring the block is the opt-in — `enabled` defaults to `true`. Set `enabled: false` to keep the configuration while turning chat off. This also disables built-in method synthesis, so the declaration must provide a normal non-conversational graph. Three things are supplied for you: | Supplied | Detail | |----------|--------| | The built-in graph | `route_conversation`, `converse_turn`, `end_conversation` and `answer_from_history_turn` are added automatically. Declare a method under one of those names to override it. | -| Conversation state | `ConversationState` is used when the declaration has no `state` block. To add fields, point `state` at a Pydantic model that extends `ConversationState`. | -| The route catalog | Built from the methods that declare a `listen` label. Each method's `description` is what the routing model reads when choosing between routes. | +| Conversation state | `ConversationState` is used when there is no `state` block. A Pydantic `ref` or `json_schema` state is automatically composed with the conversational fields; it does not need to extend `ConversationState`. | +| The route catalog | Inferred from non-router methods with `listen` labels, excluding internal routes. Descriptions follow the precedence above, and explicit `router.routes` can limit the choices. | + +Declarative `llm`, `router.llm`, `intent_llm`, and `answer_from_history_llm` fields accept either a model id or a configuration mapping such as `{model: openai/gpt-4o-mini, max_tokens: 512}`. The `conversational` block also supports `default_intents`, `answer_from_history_prompt`, `visible_agent_outputs`, `defer_trace_finalization`, and the `RouterConfig` fields shown above. Run it from Python with the same turn APIs as a class-based conversational Flow: @@ -549,11 +545,12 @@ Route labels and method names share one trigger namespace, so a handler must not | Not expressible | Use instead | |-----------------|-------------| -| A live `LLM` instance or a custom `BaseLLM` | A model id string, such as `gpt-4o-mini` | +| A live `LLM` instance or a custom `BaseLLM` | A model id string or static configuration mapping | | `router.response_format` as a model class | Omit it; the framework synthesizes one. A ref or schema is ignored with a warning | -| `route_turn()` / `can_answer_from_history()` overrides | Author the Flow in Python, or point a method's `do` at a `call: code` ref | +| A `route_turn()` override | Author the Flow in Python, or replace the declarative `route_conversation` method with a `call: code` / expression action | +| A `can_answer_from_history()` override | Author the Flow in Python; configure standard history routing with `conversational.answer_from_history_llm` | -`crewai run` opens the chat TUI for a declarative conversational flow — the same one a Python conversational Flow gets. A chat loop needs a terminal, so a headless run says so instead of running a single turn; drive it from Python there with `handle_turn()` or `stream_turn()`. A flow that also uses `@human_feedback` runs on a terminal REPL, because the runtime collects feedback with a blocking prompt the TUI cannot service. `--inputs` is not accepted for a conversational flow — each turn's input is the message you type — and resuming a session by id is not wired into the CLI yet; use `flow.handle_turn(message, session_id=...)` from Python for that. +`crewai run` opens the chat TUI for a declarative conversational flow — the same one a Python conversational Flow gets. A chat loop needs a terminal, so a headless run exits non-zero with guidance instead of running a single turn; drive it from Python there with `handle_turn()` or `stream_turn()`. A declarative method with a `human_feedback:` block (Python: `@human_feedback`) runs on a terminal REPL, because the runtime collects feedback with a blocking prompt the TUI cannot service. `--inputs` is not accepted for a conversational flow — each turn's input is the message you type — and resuming a session by id is not wired into the CLI yet; use `flow.handle_turn(message, session_id=...)` from Python for that. ## Tracing across turns @@ -572,15 +569,28 @@ flow.chat(session_id=session_id) with `handle_turn()`, 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. +`suppress_flow_events=True` hides Rich console panels and suppresses method execution events. Flow start/finish events still emit, so the outer Flow lifecycle remains traceable, but individual method spans are omitted. ### Conversational `Flow` trace lifecycle -The experimental [conversational `Flow`](#conversational-flow-experimental) uses the same tracing lifecycle: `defer_trace_finalization` defaults to `True`, so each `handle_turn()` keeps the session trace open. Always finalize at the end of the session — wrap your REPL/loop in `try/finally` and call `flow.finalize_session_traces()` on exit. Without it, the trace batch stays open and the final conversation may never export. +The experimental [conversational `Flow`](#conversational-flow-experimental) uses the same tracing lifecycle: `defer_trace_finalization` defaults to `True`, so each `handle_turn()` keeps the session trace open. Deferred turns also suppress per-turn `flow_failed`; on a turn error or session abort, finalize the session explicitly. This closes the batch with the session-level `FlowFinished` event rather than a per-turn `FlowFailed` event. Always wrap your REPL/loop in `try/finally` and call `flow.finalize_session_traces()` on exit. Without it, the trace batch stays open and the final conversation may never export. ## Streaming -Set `stream = True` on the `Flow` class. `kickoff(...)` will then emit `assistant_delta` (and related) events through the standard event bus. +For conversational UIs, use `stream_turn()` and iterate its ordered `StreamFrame` objects: + +```python +stream = flow.stream_turn("Where is my order?", session_id=session_id) + +with stream: + for frame in stream.events: + if frame.channel == "llm" and frame.type == "llm_stream_chunk": + print(frame.content, end="", flush=True) + +reply = stream.result +``` + +For a non-conversational Flow, setting `stream = True` makes `kickoff()` return a `StreamSession`. Do not set `flow.stream = True` when using `handle_turn()`; `stream_turn()` owns the conversational streaming lifecycle. ## Imports @@ -595,10 +605,15 @@ from crewai.flow import ( router, start, ) +from crewai.flow.conversation import prepare_conversational_turn +from crewai.experimental.conversational import ( + ConversationConfig, + ConversationState, + RouterConfig, +) ``` ## See also - [Mastering Flow State Management](/en/guides/flows/mastering-flow-state) — persistence, Pydantic state, `@persist` - [Build Your First Flow](/en/guides/flows/first-flow) — flow basics -- Demo: `lib/crewai/runner_conversational_flow_simple.py` — minimal REPL with `RESEARCH` + Exa agent diff --git a/docs/edge/ko/guides/flows/conversational-flows.mdx b/docs/edge/ko/guides/flows/conversational-flows.mdx index 02259dc0d..08f24076c 100644 --- a/docs/edge/ko/guides/flows/conversational-flows.mdx +++ b/docs/edge/ko/guides/flows/conversational-flows.mdx @@ -1,35 +1,45 @@ --- title: 대화형 Flow -description: 턴마다 kickoff, 메시지 기록, 의도 라우팅, 트레이싱, WebSocket 브리지로 멀티턴 채팅 앱을 만듭니다. +description: 턴별 handle_turn, 메시지 기록, 의도 라우팅, 트레이싱, 구조화된 스트리밍으로 멀티턴 채팅 앱을 만듭니다. icon: comments mode: "wide" --- + + **실험적 기능입니다.** 대화형 `Flow` API 표면 + (`conversational = True`, `handle_turn`, `stream_turn`, `ConversationConfig`, + `RouterConfig`, `ConversationState`, 내장 그래프)은 `crewai.experimental` + 하위에 있으며 정식 기능이 되기 전에 변경될 수 있습니다. 특정 동작에 + 의존한다면 CrewAI 버전을 고정하세요. + + ## 개요 -대화형 앱은 각 사용자 입력을 **동일한 세션 id**로 **새 flow 실행**으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 분류, 지연 트레이싱, UI 브리지, 그리고 대화형 flow용 로컬 `flow.chat()` REPL을 제공합니다. +대화형 앱은 각 사용자 입력을 **동일한 세션 id**로 **새 flow 실행**으로 처리합니다. CrewAI는 메시지 기록, 선택적 의도 라우팅, 지연 트레이싱, 구조화된 턴 스트리밍, 로컬 `flow.chat()` REPL을 위한 헬퍼를 제공합니다. | 개념 | 구현 | |------|------| | 세션 id | `handle_turn(..., session_id=...)` → `kickoff(inputs={"id": ...})` → `state.id` | | 사용자 입력 | `handle_turn(message)`가 그래프 실행 전 `state.messages`에 추가 | -| 턴 완료 | `FlowFinished`는 **이번 실행**만 의미; 다음 `handle_turn`로 대화 계속 | +| 턴 완료 | `conversation_turn_completed`; 기본 trace 지연을 사용하면 `FlowFinished`는 `finalize_session_traces()`까지 대기 | | 세션 전체 트레이스 | `ConversationConfig(defer_trace_finalization=True)` + `finalize_session_traces()` | ## 턴 API REST, WebSocket, 테스트, 커스텀 UI에서 오는 모든 사용자 메시지에는 **`flow.handle_turn(message, session_id=...)`**를 사용하세요. 대화형 `Flow`를 로컬 터미널 채팅 루프로 실행하고 싶을 때는 **`flow.chat()`**을 사용하세요. -`Flow.kickoff()`는 `user_message=` 또는 `session_id=` 키워드 인자를 받지 않습니다. 대화형 flow에서는 `handle_turn()`이 보류 중인 메시지를 저장하고 내부적으로 `kickoff(inputs={"id": session_id})`를 호출합니다. +`Flow.kickoff()`는 `user_message=` 또는 `session_id=` 키워드 인자를 받지 않습니다. 대화형 flow에서는 `handle_turn()`이 보류 중인 메시지를 저장하고 턴별 실행 상태를 초기화한 뒤 내부적으로 `kickoff(inputs={"id": session_id})`를 호출합니다. | API | 용도 | |-----|------| | `handle_turn(message, session_id=...)` | 대화형 `Flow`용 한 턴 편의 래퍼 | +| `stream_turn(message, session_id=...)` | 대화형 한 턴을 순서가 보장된 런타임 frame으로 스트리밍 | | `chat()` | 대화형 `Flow`용 로컬 터미널 REPL | -| `kickoff(inputs={...})` | 대화형 턴 처리 없이 flow를 직접 실행 | +| `kickoff(inputs={...})` | 대화형 턴 처리 없이 flow를 직접 실행하는 고급 용도 | | `ask()` | 한 스텝 **내부** 블로킹 프롬프트 (마법사, 확인) | | `@human_feedback` | **스텝 출력** 승인/거부 — 다음 채팅 줄이 아님 | -| `ChatSession.handle_turn(...)` | `handle_turn` 위의 전송 계층 (SSE / WebSocket) | + +대화형 모드가 활성화되지 않으면 `handle_turn()`, `stream_turn()`, `chat()`은 `ValueError`를 발생시킵니다. `@ConversationConfig(...)`를 적용하면 자동으로 활성화되며, 그렇지 않으면 `conversational = True`로 설정하세요. ## 빠른 시작 @@ -46,8 +56,6 @@ from crewai.experimental.conversational import ( @ConversationConfig(defer_trace_finalization=True) class SupportFlow(Flow[ConversationState]): - conversational = True - def route_turn(self, context): message = self.state.current_user_message or "" if "주문" in message or "order" in message.lower(): @@ -85,35 +93,43 @@ finally: flow.finalize_session_traces() # 전체 대화에 대한 단일 trace 링크 ``` +## 턴 스트리밍 + +UI나 런타임에서 한 채팅 턴의 구조화된 이벤트가 필요하면 `stream_turn()`을 사용하세요. Flow 라우팅, LLM chunk, tool 활동, 대화 메시지를 순서가 보장된 frame으로 제공하는 stream session을 반환합니다. + +```python +stream = flow.stream_turn("Where is my order?", session_id=session_id) + +with stream: + for frame in stream.events: + if frame.channel == "llm" and frame.type == "llm_stream_chunk": + print(frame.content, end="", flush=True) + +result = stream.result +``` + +전체 frame 계약과 channel 목록은 [스트리밍 런타임 계약](/edge/ko/learn/streaming-runtime-contract)을 참고하세요. + ## 턴 생명주기 각 `handle_turn`은 다음 파이프라인을 실행합니다: -1. **`_configure_conversational_kickoff`** — `session_id` / `user_message`를 `inputs`에 병합, `ConversationalConfig` 적용, 설정 시 지연 트레이싱 활성화. +1. **턴 설정** — 보류 중인 사용자 메시지를 저장하고 세션 id를 결정하며 턴별 실행 추적을 초기화한 뒤 `kickoff(inputs={"id": session_id})`를 호출. 2. **상태 복원** — `inputs["id"]`가 있고 `@persist`가 설정되면 최신 스냅샷 로드. 3. **`FlowStarted`** — 지연 세션의 첫 턴에서만 발생. -4. **`prepare_conversational_turn`** — 사용자 메시지를 `state.messages`에 추가, `last_user_message` 설정, `last_intent` 초기화, `intents` / `default_intents` + `intent_llm` 설정 시 분류. -5. **그래프 실행** — `@start` → `@router` → `@listen` 핸들러. +4. **보류 중인 턴 수화** — 사용자 메시지를 `state.messages`에 추가하고 `current_user_message` / `last_user_message`를 설정하며, `intents` / `default_intents` + `intent_llm` 설정 시 선택적으로 분류. +5. **그래프 실행** — 사용자 정의 `@start` 메서드(있는 경우) → `route_conversation`(내장 start/router) → 선택된 `@listen` 핸들러. `route_conversation`은 재정의 가능한 `conversation_start()` 헬퍼도 호출합니다. 6. **실행 종료** — 지연 활성화 시 턴별 `flow_finished` 및 trace 종료 **건너뜀**; 중첩 `Agent.kickoff()` / crew도 부모 batch를 닫지 않음. 핸들러는 **`append_assistant_message(reply)`**를 호출해 다음 턴의 `conversation_messages`에 어시스턴트 응답이 포함되게 하세요. 사용자 입력은 `handle_turn`이 이미 저장합니다 — 핸들러에서 다시 추가하지 마세요. -## `ConversationalConfig` (클래스 수준 기본값) +## 설정 개요 -`Flow` 서브클래스에 `conversational_config: ClassVar[ConversationalConfig | None]`로 설정합니다. +`Flow` 서브클래스에 `ConversationConfig`를 데코레이터로 적용하면 채팅 기본값이 부착되고 대화형 모드도 활성화됩니다. 아래의 [전체 필드 레퍼런스](#conversationconfig)를 참고하세요. 턴마다 `handle_turn(..., intents=..., intent_llm=...)`로 사전 분류 설정을 재정의할 수 있습니다. -| 필드 | 기본값 | 목적 | -|------|--------|------| -| `default_intents` | `None` | kickoff 전 자동 분류용 outcome 라벨 | -| `intent_llm` | `None` | 분류용 모델 (intent 사용 시 필수) | -| `interactive_prompt` | `"You: "` | `kickoff(interactive=True)` 프롬프트 | -| `interactive_timeout` | `None` | 대화형 모드 줄 단위 타임아웃 | -| `exit_commands` | `exit`, `quit` | 대화형 모드 종료 단어 | -| `defer_trace_finalization` | `True` | 턴 간 하나의 trace batch 유지 | +## 하위 수준 `ChatState` 헬퍼 -`intents=` 및 `intent_llm=` 키워드로 kickoff마다 재정의할 수 있습니다. - -## `ChatState` (권장 persist 형태) +`ChatState`, 레거시 `ConversationalConfig`, `crewai.flow.conversation` 헬퍼는 고급 오케스트레이션, 테스트, 커스텀 래퍼에서 계속 import할 수 있습니다. 이들은 실험적 `ConversationState` / `ConversationConfig` API와 별개이며 `Flow.kickoff()`에 `user_message=` 또는 `session_id=` 키워드 인자를 추가하지 않습니다. ```python from crewai.flow import ChatState @@ -127,7 +143,7 @@ class MyChatState(ChatState): | 필드 | 역할 | |------|------| -| `id` | 세션 UUID (`session_id` / `inputs["id"]`와 동일) | +| `id` | 세션 UUID (`inputs["id"]`와 동일) | | `messages` | LLM 기록용 `{role, content}` 리스트 | | `last_user_message` | 이번 턴의 최신 사용자 입력 | | `last_intent` | 분류 후 라우트 라벨 (사용 시) | @@ -135,76 +151,77 @@ class MyChatState(ChatState): `ConversationalInputs`는 `kickoff(inputs={...})`용 `TypedDict`: `id`, `user_message`, `last_intent`. +실험적 `ConversationState`는 `messages`를 `ConversationMessage` 객체로 저장하며 `current_user_message`, `ended`, `events`, `agent_threads`도 제공합니다. 정식 기록을 LLM에 전달할 때는 `conversation_messages`를 사용하세요. + ## `Flow` 대화 API -### `kickoff` / `kickoff_async` 파라미터 +### `handle_turn` 파라미터 | 파라미터 | 목적 | |----------|------| -| `user_message` | 이번 턴 텍스트 (또는 `{"role": "user", "content": "..."}`) | +| `message` | 이번 턴의 텍스트 | | `session_id` | 대화 UUID → `inputs["id"]` / `state.id` | -| `intents` | kickoff 전 `classify_intent`용 outcome 라벨 | +| `intents` | kickoff 전 `classify_intent`용 결과 라벨 | | `intent_llm` | 분류 LLM (`intents`와 함께 필수) | -| `interactive` | `ask()` CLI 루프 (로컬 데모 전용) | -| `interactive_prompt` | 대화형 모드 프롬프트 | -| `interactive_timeout` | 줄 단위 `ask()` 타임아웃 | -| `exit_commands` | 대화형 모드 종료 단어 | -| `inputs` | 추가 상태 필드 | -| `restore_from_state_id` | 다른 persist flow에서 fork 복원 | +| `**kickoff_kwargs` | `input_files`, `from_checkpoint`, `restore_from_state_id` 같은 옵션을 `kickoff()`로 전달 | + +### `kickoff` 파라미터 + +`Flow.kickoff()`는 `inputs`, `input_files`, `from_checkpoint`, `restore_from_state_id`를 받습니다. 원시 flow 실행이 필요하면 `inputs={"id": session_id}`를 전달할 수 있지만, 채팅 메시지를 나타내는 호출에는 `handle_turn()`을 사용하세요. ### 인스턴스 속성 | 속성 | 목적 | |------|------| -| `conversational_config` | 클래스 수준 `ConversationalConfig` | -| `defer_trace_finalization` | 인스턴스 플래그; kickoff 시 config에서 자동 설정 | -| `suppress_flow_events` | 콘솔 flow 패널 숨김; **트레이싱은 계속 기록** | -| `stream` | 스트리밍; `ChatSession.handle_turn(..., stream=True)`와 함께 | +| `conversational` | 대화형 그래프와 `handle_turn()`을 활성화하려면 `True`로 설정 | +| `defer_trace_finalization` | 선택적 인스턴스 재정의. 없으면 `_should_defer_trace_finalization()`이 `ConversationConfig.defer_trace_finalization`을 읽음 | +| `suppress_flow_events` | 콘솔 flow 패널과 메서드 실행 이벤트를 숨김. flow start/finish 이벤트는 계속 발생 | +| `stream` | 일반 Flow 스트리밍 플래그. 대화형 턴에서는 이 플래그와 `handle_turn()`을 함께 쓰지 말고 `stream_turn()` 사용 | ### 메서드 및 프로퍼티 | 이름 | 설명 | |------|------| +| `append_assistant_message(content)` | 사용자에게 보이는 어시스턴트 응답을 `state.messages`에 추가 | | `append_message(role, content, **extra)` | `state.messages`에 추가 | | `conversation_messages` | LLM 호출용 읽기 전용 기록 | | `classify_intent(text, outcomes, *, llm, context=None)` | outcome 매핑 (`@human_feedback`와 동일 collapse) | | `receive_user_message(text, *, outcomes=None, llm=None)` | 사용자 메시지 추가; 선택적 `last_intent` | | `finalize_session_traces()` | 지연 `flow_finished` 발생 및 세션 trace batch 종료 | -| `_should_defer_trace_finalization()` | 턴별 trace 종료 지연 여부 | +| `_should_defer_trace_finalization()` | 턴별 trace 종료 지연 여부를 결정하는 고급/내부 hook | | `input_history` | `ask()` 프롬프트/응답 감사 기록 | ### 모듈 헬퍼 (`crewai.flow.conversation`) -테스트 또는 커스텀 오케스트레이션용: +테스트 또는 커스텀 오케스트레이션을 위해 `crewai.flow.conversation`에서 import할 수 있습니다. 이 헬퍼들은 레거시 `ConversationalConfig` 형태를 사용합니다. 또한 `prepare_conversational_turn()`은 `last_intent`를 지우지만, 실험적 `handle_turn()`은 router 컨텍스트로 보존합니다. | 함수 | 설명 | |------|------| -| `normalize_kickoff_inputs(...)` | 대화 kwargs를 `inputs`에 병합 | +| `normalize_kickoff_inputs(inputs, user_message=..., session_id=...)` | 대화 kwargs를 `inputs`에 병합 | | `get_conversation_messages(flow)` | 상태 또는 내부 버퍼에서 메시지 읽기 | -| `append_message(flow, ...)` | 인스턴스 메서드와 동일 | -| `prepare_conversational_turn(flow, ...)` | 턴 수화 (보통 kickoff가 호출) | -| `receive_user_message(flow, ...)` | 인스턴스 메서드와 동일 | +| `append_message(flow, role, content, **extra)` | 인스턴스 메서드와 동일 | +| `prepare_conversational_turn(flow, user_message=..., intents=..., intent_llm=..., config=...)` | 커스텀 래퍼용 하위 수준 턴 수화 | +| `receive_user_message(flow, text, ...)` | 인스턴스 메서드와 동일 | | `set_state_field(flow, name, value)` | dict 또는 Pydantic 상태 필드 설정 | | `get_conversational_config(flow)` | 클래스 `conversational_config` 읽기 | | `input_history_to_messages(entries)` | `input_history`를 LLM 메시지 형식으로 | ## 의도 라우팅 패턴 -### A. `ConversationalConfig`로 사전 분류 (가장 단순) +### A. `ConversationConfig`로 사전 분류 (가장 단순) -`default_intents`와 `intent_llm` 설정. 각 kickoff가 `@router` 전에 분류; `route()`에서 `self.state.last_intent` 읽기. +`default_intents`와 `intent_llm`을 설정하세요. 각 `handle_turn()`이 현재 메시지를 사전 분류합니다. 커스텀 `route_turn()`이 반환한 비어 있지 않은 결과가 우선하며, 그렇지 않으면 `route_conversation`이 현재 턴의 분류된 intent를 사용합니다. -### B. `@router` 내부에서 분류 (풍부한 프롬프트) +### B. `route_turn` 내부에서 분류 (풍부한 프롬프트) -`default_intents=None`으로 kickoff는 메시지만 추가. `route()`에서 커스텀 프롬프트로 `classify_intent` 호출: +`default_intents=None`으로 설정하면 `handle_turn()`은 사용자 메시지만 추가합니다. `route_turn()`에서 커스텀 프롬프트나 설명과 함께 `classify_intent`를 호출하세요: ```python -@router(bootstrap) -def route(self): +def route_turn(self, context): intent = self.classify_intent( - self._routing_prompt(self.state.last_user_message), + self._routing_prompt(self.state.current_user_message), ("GREETING", "ORDER", "RESEARCH", "GOODBYE"), - llm=self.conversational_config.intent_llm or "gpt-4o-mini", + llm="gpt-4o-mini", ) self.state.last_intent = intent return intent @@ -214,7 +231,7 @@ def route(self): ## flow가 끝났지만 사용자는 계속 대화할 때 -`FlowFinished`는 **이번 그래프 실행**이 완료됨을 의미합니다. 같은 `session_id`로 또 다른 `kickoff`로 대화가 이어집니다. `@persist`가 `messages`, 플래그, 컨텍스트를 복원합니다. +각 `handle_turn()`은 하나의 그래프 실행을 완료하며, 같은 `session_id`로 다음 `handle_turn()`을 호출해 대화를 이어갑니다. 기본 지연 trace 수명 주기에서는 해당 실행이 `conversation_turn_completed`를 발생시키고, `finalize_session_traces()`가 세션을 닫을 때 `FlowFinished`가 한 번 발생합니다. `@persist`는 `messages`, 플래그, 컨텍스트를 복원합니다. **Persist 패턴:** 전체 `Flow` 클래스보다 **단일 종료 스텝**(예: `finalize`)에 `@persist`를 두는 것이 좋습니다. 클래스 수준 persist는 매 메서드 후 저장하며, `load_state`는 최신 행을 사용해 같은 턴의 핸들러 업데이트를 놓칠 수 있습니다. @@ -222,61 +239,51 @@ def route(self): ## 대화형 `Flow` (실험적) - - **실험적 기능입니다.** 대화형 `Flow`의 API 표면(`conversational = True`, - `handle_turn`, `ConversationConfig`, `RouterConfig`, `ConversationState`, - 내장 그래프와 헬퍼)은 `crewai.experimental` 하위에 있으며 정식 출시 - 전까지 변경될 수 있습니다. 특정 동작에 의존한다면 CrewAI 버전을 고정하고 - 변경 사항이 있는지 changelog를 확인하세요. 피드백과 이슈 환영합니다. - - -`Flow` 서브클래스에 `conversational = True`를 지정하면 대화형 챗 그래프가 활성화됩니다. 베이스 `Flow`가 `@start` / `@router` / `converse_turn` / `end_conversation` 그래프를 노출하고, `state.messages`를 관리하며, router LLM을 구동하고, 턴 간 trace 배치를 열린 상태로 유지합니다. 여러분은 **커스텀 라우트**만 작성하면 되고, 나머지는 프레임워크가 담당합니다. +`Flow` 서브클래스에 `conversational = True`를 지정하거나 `@ConversationConfig(...)`를 적용하면 대화형 채팅 그래프가 활성화됩니다. 베이스 `Flow`는 내장 start/router인 `route_conversation`과 `converse_turn`, `end_conversation`, `answer_from_history_turn` 리스너를 제공합니다. 또한 `state.messages`를 관리하고 router LLM을 구동할 수 있으며 턴 간 trace batch를 열린 상태로 유지합니다. 여러분은 **커스텀 라우트**를 작성하고 나머지는 프레임워크에 맡기면 됩니다. LLM 기반 라우터와 라우트별 핸들러로 멀티턴 챗을 만들고 싶지만 라이프사이클을 직접 배선하고 싶지 않을 때 사용하세요. 완전한 제어가 필요하면 위의 `Flow[ChatState]`로 내려가세요. ### 빠른 예제 ```python -from crewai import LLM, Flow +from crewai import 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(), # 라우트 + 설명은 @listen 핸들러에서 자동 발견 -) +@ConversationConfig(defer_trace_finalization=True) class SupportFlow(Flow[ConversationState]): - conversational = True + def route_turn(self, context: dict) -> str | None: + message = (self.state.current_user_message or "").lower() + if "search" in message or "news" in message: + return "INTERNET_SEARCH" + if "docs" in message or "crewai" in message: + return "CREWAI_DOCS" + return "converse" @listen("INTERNET_SEARCH") def handle_internet_search(self) -> str: """Fresh web research, current news, real-time lookups.""" - ... + reply = "I would run the web research route here." 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.""" - ... + reply = "I would look up the CrewAI docs here." self.append_assistant_message(reply) return reply flow = SupportFlow() try: - flow.handle_turn("뭘 할 수 있어?") # converse(빌트인)로 라우팅 - flow.handle_turn("AI 뉴스를 웹에서 찾아줘.") # INTERNET_SEARCH로 라우팅 - flow.handle_turn("첫 번째 결과를 요약해줘.") # 다시 converse로 라우팅 + flow.handle_turn("What can you do?") # routes to converse + flow.handle_turn("Search the web for AI news.") # routes to INTERNET_SEARCH + flow.handle_turn("Check the CrewAI docs.") # routes to CREWAI_DOCS finally: flow.finalize_session_traces() ``` @@ -298,7 +305,7 @@ def kickoff() -> None: |------|--------|------| | `system_prompt` | i18n `slices.conversational_system_prompt` | 빌트인 `converse_turn`이 사용하는 system 메시지. 빈 문자열(`""`)을 전달하면 system 메시지를 끕니다. | | `llm` | `None` | 대화용 LLM (빌트인 `converse_turn`이 사용하고 router 폴백도 됨). | -| `router` | `None` | LLM 기반 라우팅을 위한 `RouterConfig`. 없으면 항상 `converse`로 떨어집니다. | +| `router` | `None` | 선택적 `RouterConfig` 재정의. 커스텀 listener와 결정 가능한 LLM이 있으면 생략해도 라우팅이 자동 활성화됩니다. | | `answer_from_history_prompt` | 프레임워크 기본값 | 선택적인 `answer_from_history` 라우트용 system 메시지. | | `answer_from_history_llm` | `None` | 설정되면 `answer_from_history` 단축 경로가 활성화됩니다. | | `intent_llm` | `None` | 레거시 `intents=`/`default_intents` 사전 분류용 LLM. | @@ -306,19 +313,37 @@ def kickoff() -> None: | `visible_agent_outputs` | `None` | `"all"` 또는 `append_agent_result()` 결과를 사용자에게 공개로 승격할 에이전트 이름 목록. | | `defer_trace_finalization` | `True` | `handle_turn()` 호출들 사이에서 하나의 trace 배치를 열어 둡니다. | +커스텀 라우트가 없으면 턴은 `converse`로 이어집니다. 커스텀 라우트와 대화/router LLM이 있으면 프레임워크가 기본 `RouterConfig`를 합성합니다. prompt, 라우트 목록, 설명, fallback 동작을 바꿔야 할 때만 명시적으로 제공하세요. `default_intents`를 설정하면 레거시 사전 분류 경로를 사용합니다. + +대화 LLM을 설정하지 않으면 내장 `converse_turn`은 답변을 생성하는 대신 설정 안내 placeholder를 반환합니다. + ### `RouterConfig`와 자동 생성되는 라우트 카탈로그 ```python -RouterConfig( - prompt="선택적인 도메인 프레이밍 (정책, 톤, 페르소나).", - response_format=MyRoute, # 선택; 없으면 자동 생성 - llm=ROUTER_LLM, # ConversationConfig.llm으로 폴백 - routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # 선택; 리스너에서 추론 +from typing import Literal + +from pydantic import BaseModel + +from crewai import LLM +from crewai.experimental.conversational import RouterConfig + + +class MyRoute(BaseModel): + intent: Literal["INTERNET_SEARCH", "CREWAI_DOCS", "converse"] + + +ROUTER_LLM = LLM(model="gpt-4o-mini") + +router_config = RouterConfig( + prompt="Optional domain framing (policy, voice, persona).", + response_format=MyRoute, # optional; auto-generated otherwise + llm=ROUTER_LLM, # falls back to ConversationConfig.llm + routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # optional; inferred from listeners route_descriptions={ - "INTERNET_SEARCH": "이 라우트만 docstring 대신 사용할 설명.", + "INTERNET_SEARCH": "Override the docstring for this one route.", }, - default_intent="converse", # LLM 호출 실패 또는 LLM 없음일 때 사용 - fallback_intent="converse", # LLM이 잘못된 라우트를 반환할 때 사용 + default_intent="converse", # used when LLM call fails or no LLM available + fallback_intent="converse", # used when LLM returns an invalid route intent_field="intent", ) ``` @@ -327,8 +352,9 @@ router에 전달되는 프롬프트는 자동으로 만들어집니다. 각 라 1. `RouterConfig.route_descriptions[label]` — 명시적 오버라이드. 2. `Flow.builtin_route_descriptions[label]` — `converse`, `end`, `answer_from_history`용 프레임워크 캐닝 텍스트 (router LLM용으로 다듬어진 문구). -3. `@listen(label)` 핸들러 docstring의 첫 줄(비어있지 않은 줄). -4. 빈 문자열 (라우트만 카탈로그에 등장하고 설명은 없음). +3. 메서드에 선언된 `description` — 선언적 flow와 DSL projection에서 사용. +4. `@listen(label)` 핸들러 docstring의 첫 번째 비어 있지 않은 줄. +5. 빈 문자열 — 설명 없이 라우트만 표시. 실제 사용에서 **새 라우트를 추가하는 방법은 `@listen("X")` + 한 줄짜리 docstring**입니다: @@ -339,6 +365,27 @@ def handle_internet_search(self) -> str: ... ``` +### 핸들러 이름 짓기 + +`@listen("…")`의 문자열은 Python 메서드 이름이 아니라 **router 라우트 레이블**(이벤트 이름)입니다. 라우트 레이블과 메서드 완료 이벤트는 하나의 트리거 namespace를 공유하므로, 핸들러 이름을 라우트와 같게 지정하면 핸들러가 자기 자신을 반복해서 다시 실행합니다. + +서로 다른 메서드 이름을 사용하세요. 문서 예제에서는 `handle_*` 접두사를 사용합니다: + +```python +@listen("create_video") +def handle_create_video(self) -> str: + """User wants a new video.""" + ... +``` + +메서드 이름을 라우트 레이블과 같게 만들지 마세요: + +```python +@listen("create_video") +def create_video(self) -> str: # rejected at flow instantiation + ... +``` + …그러면 router LLM은 다음을 봅니다: ``` @@ -357,7 +404,7 @@ Routes: |--------|--------|------| | `converse` | `converse_turn` | 기본 챗 핸들러. system prompt + 정식 메시지 히스토리와 함께 `ConversationConfig.llm`을 호출합니다. | | `end` | `end_conversation` | `state.ended = True`로 설정하고 종료 응답을 보냅니다. | -| `answer_from_history` | `answer_from_history_turn` | 선택적. `ConversationConfig.answer_from_history_llm`이 설정되어 있고 메시지를 히스토리만으로 답할 수 있을 때 라우팅됩니다. | +| `answer_from_history` | `answer_from_history_turn` | 선택적. 메시지가 두 개 이상 존재한 뒤 `ConversationConfig.answer_from_history_llm`이 현재 메시지를 기록만으로 답할 수 있다고 판단하면 이곳으로 라우팅됩니다. | 서브클래스에 같은 이름의 핸들러를 정의하면 어떤 것이든 오버라이드할 수 있습니다. @@ -367,7 +414,7 @@ Routes: 1. 그래프가 다시 실행되도록 턴 단위 실행 추적(`_completed_methods`, `_method_outputs`)을 초기화합니다 — 이게 없으면 동일 인스턴스에서 반복 `kickoff` 호출 시 `Flow.kickoff_async`가 `inputs={"id": ...}`를 체크포인트 복원으로 간주해 2번째 턴부터 단락 회로가 발생합니다. 2. 사용자 메시지를 `state.messages`에 추가하고 `current_user_message` / `last_user_message`를 설정합니다. `last_intent`는 **이전 턴 값이 유지**되어 router LLM이 신호로 활용할 수 있습니다. -3. `conversation_start` → `route_conversation` → 선택된 `@listen` 핸들러 순으로 실행됩니다. +3. 사용자 정의 `@start` 메서드(있는 경우)를 실행한 다음 내장 start/router인 `route_conversation`을 거쳐 선택된 `@listen` 핸들러를 실행합니다. `route_conversation`은 재정의 가능한 `conversation_start()` 헬퍼를 호출합니다. 4. router는 결정을 `state.last_intent`에 저장합니다 (다음 턴의 router 컨텍스트에서 보입니다). 5. 핸들러가 문자열을 반환했지만 `append_assistant_message`를 직접 호출하지 않았다면, `handle_turn`이 대신 추가해 줍니다. @@ -390,6 +437,8 @@ flow.chat() 4. 어시스턴트 결과를 출력합니다. 5. `finally` 블록에서 지연된 세션 trace를 finalize합니다. +`chat(defer_trace_finalization=True)`는 REPL 동안 인스턴스의 지연 플래그를 임시로 활성화하고 종료할 때 이전 값으로 복원합니다. + 주입 가능한 I/O로 터미널 동작을 커스터마이즈할 수 있습니다: ```python @@ -408,6 +457,12 @@ flow.chat( 매 라우팅 결정마다 사이드 이펙트(이벤트 버스 셋업, 텔레메트리)를 실행하려면 `route_turn`을 오버라이드하세요: ```python +from typing import Any + +from crewai import Flow +from crewai.experimental.conversational import ConversationState + + class SupportFlow(Flow[ConversationState]): conversational = True @@ -416,7 +471,7 @@ class SupportFlow(Flow[ConversationState]): return super().route_turn(context) ``` -LLM router를 우회해 프로그램적으로 라우트를 선택하려면 `route_turn`에서 문자열을 반환하세요. `None`을 반환하면 `_route_with_config(...)`로 떨어집니다. +LLM router를 완전히 우회하고 프로그램 방식으로 라우트를 선택하려면 `route_turn`에서 비어 있지 않은 문자열을 반환하세요. falsy 값을 반환해도 오버라이드에서 `_route_with_config()`가 호출되지는 않습니다. 대신 현재 턴의 사전 분류된 intent, 사용 가능한 경우 `answer_from_history`, 마지막으로 `converse` 순으로 fallback합니다. 이전 턴의 `last_intent`는 router 컨텍스트에서 사용할 수 있지만 fallback으로 다시 실행되지는 않습니다. ### `append_assistant_message`와 `append_agent_result` @@ -429,7 +484,7 @@ LLM router를 우회해 프로그램적으로 라우트를 선택하려면 `rout ## JSON/YAML로 대화형 플로우 선언하기 -[선언적 플로우](/edge/en/concepts/cli)도 대화형이 될 수 있습니다. 최상위 `conversational` 블록을 추가하고, 라우트 레이블을 `listen`하는 메서드로 직접 라우트를 선언하세요: +[선언적 Flow](/edge/ko/concepts/cli)도 대화형으로 만들 수 있습니다. 최상위 `conversational` 블록을 추가하고 라우트 레이블을 `listen`하는 메서드로 자체 라우트를 선언하세요: ```yaml schema: crewai.flow/v1 @@ -454,15 +509,17 @@ methods: input: "${state.current_user_message}" ``` -블록을 선언하는 것 자체가 옵트인입니다 — `enabled`의 기본값은 `true`입니다. 설정은 유지하면서 채팅만 끄려면 `enabled: false`로 지정하세요. +블록 선언 자체가 opt-in이며 `enabled`의 기본값은 `true`입니다. 설정은 유지하면서 채팅을 끄려면 `enabled: false`로 지정하세요. 이 경우 내장 메서드 합성도 비활성화되므로 선언에 일반 비대화형 그래프를 제공해야 합니다. 세 가지가 자동으로 제공됩니다: | 제공 항목 | 설명 | |----------|--------| | 내장 그래프 | `route_conversation`, `converse_turn`, `end_conversation`, `answer_from_history_turn`이 자동으로 추가됩니다. 같은 이름의 메서드를 선언하면 재정의됩니다. | -| 대화 상태 | 선언에 `state` 블록이 없으면 `ConversationState`가 사용됩니다. 필드를 추가하려면 `ConversationState`를 상속한 Pydantic 모델을 `state`에 지정하세요. | -| 라우트 카탈로그 | `listen` 레이블을 선언한 메서드들로부터 구성됩니다. 각 메서드의 `description`이 라우팅 모델이 라우트를 선택할 때 읽는 내용입니다. | +| 대화 상태 | `state` 블록이 없으면 `ConversationState`가 사용됩니다. Pydantic `ref` 또는 `json_schema` state는 대화형 필드와 자동으로 합성되며 `ConversationState`를 상속할 필요가 없습니다. | +| 라우트 카탈로그 | 내부 라우트를 제외하고 `listen` 레이블이 있는 비-router 메서드에서 추론됩니다. 설명에는 위 우선순위가 적용되며 명시적인 `router.routes`로 선택지를 제한할 수 있습니다. | + +선언적 `llm`, `router.llm`, `intent_llm`, `answer_from_history_llm` 필드는 모델 id 또는 `{model: openai/gpt-4o-mini, max_tokens: 512}` 같은 설정 mapping을 받습니다. `conversational` 블록은 `default_intents`, `answer_from_history_prompt`, `visible_agent_outputs`, `defer_trace_finalization`과 위에 나온 `RouterConfig` 필드도 지원합니다. 클래스 기반 대화형 플로우와 동일한 턴 API로 Python에서 실행합니다: @@ -485,15 +542,16 @@ finally: | 표현 불가 | 대신 사용 | |-----------------|-------------| -| 살아 있는 `LLM` 인스턴스나 커스텀 `BaseLLM` | `gpt-4o-mini` 같은 모델 ID 문자열 | +| 살아 있는 `LLM` 인스턴스나 커스텀 `BaseLLM` | 모델 id 문자열 또는 정적 설정 mapping | | 모델 클래스로서의 `router.response_format` | 생략하세요; 프레임워크가 생성합니다. ref나 스키마는 경고와 함께 무시됩니다 | -| `route_turn()` / `can_answer_from_history()` 재정의 | 플로우를 Python으로 작성하거나, 메서드의 `do`를 `call: code` ref로 지정하세요 | +| `route_turn()` 재정의 | Flow를 Python으로 작성하거나 선언적 `route_conversation` 메서드를 `call: code` / expression action으로 교체 | +| `can_answer_from_history()` 재정의 | Flow를 Python으로 작성하고 표준 기록 라우팅은 `conversational.answer_from_history_llm`으로 설정 | -`crewai run`은 선언적 대화형 플로우에 대해 채팅 TUI를 엽니다 — Python 대화형 Flow가 받는 것과 같은 TUI입니다. 채팅 루프에는 터미널이 필요하므로, 헤드리스 실행에서는 단일 턴을 실행하는 대신 그 사실을 알립니다; 그 환경에서는 Python에서 `handle_turn()` 또는 `stream_turn()`으로 실행하세요. `@human_feedback`도 사용하는 플로우는 터미널 REPL에서 실행됩니다. 런타임이 TUI가 처리할 수 없는 블로킹 프롬프트로 피드백을 수집하기 때문입니다. 대화형 플로우에서는 `--inputs`를 받지 않습니다 — 각 턴의 입력은 여러분이 입력하는 메시지입니다 — 그리고 id로 세션을 재개하는 기능은 아직 CLI에 연결되지 않았습니다; 그것이 필요하면 Python에서 `flow.handle_turn(message, session_id=...)`을 사용하세요. +`crewai run`은 선언적 대화형 Flow에 대해 Python 대화형 Flow와 같은 채팅 TUI를 엽니다. 채팅 루프에는 터미널이 필요하므로 headless 실행은 단일 턴을 실행하는 대신 안내와 함께 0이 아닌 코드로 종료됩니다. 이런 환경에서는 Python의 `handle_turn()` 또는 `stream_turn()`으로 실행하세요. `human_feedback:` 블록이 있는 선언적 메서드(Python: `@human_feedback`)는 터미널 REPL에서 실행됩니다. 런타임이 TUI가 처리할 수 없는 블로킹 prompt로 feedback을 수집하기 때문입니다. 대화형 Flow에서는 `--inputs`를 받지 않습니다. 각 턴의 입력은 사용자가 입력하는 메시지이며 id로 세션을 재개하는 기능은 아직 CLI에 연결되지 않았습니다. 필요하면 Python에서 `flow.handle_turn(message, session_id=...)`을 사용하세요. ## 턴 간 트레이싱 -`defer_trace_finalization=True` (`ConversationalConfig` 기본값): +`defer_trace_finalization=True` (`ConversationConfig` 기본값): - 채팅 세션 전체에 **하나의 trace batch**. - 첫 턴에만 **`flow_started`**; `finalize_session_traces()`에서 **`flow_finished`** 한 번. @@ -504,17 +562,30 @@ finally: flow.chat(session_id=session_id) ``` -`flow.chat()`이 `finalize_session_traces()`를 대신 호출합니다. `handle_turn()`이나 `kickoff(...)`로 직접 루프를 소유하는 경우, 세션이 끝날 때 `finalize_session_traces()`를 호출하세요. +`flow.chat()`이 `finalize_session_traces()`를 대신 호출합니다. `handle_turn()`으로 직접 루프를 소유하는 경우 세션이 끝날 때 `finalize_session_traces()`를 호출하세요. -`suppress_flow_events=True`는 Rich 콘솔 패널만 숨깁니다. trace 및 method 이벤트는 계속 발생합니다. +`suppress_flow_events=True`는 Rich 콘솔 패널을 숨기고 메서드 실행 이벤트를 억제합니다. Flow start/finish 이벤트는 계속 발생하므로 바깥쪽 Flow 수명 주기는 추적할 수 있지만 개별 메서드 span은 생략됩니다. ### 대화형 `Flow` trace 수명 주기 -실험적 [대화형 `Flow`](#대화형-flow-실험적)는 동일한 tracing 수명 주기를 따릅니다. `defer_trace_finalization` 기본값이 `True`이므로 각 `handle_turn()`이 세션 trace를 열어 둡니다. 세션 끝에서 항상 finalize하세요 — REPL/루프를 `try/finally`로 감싸고 종료 시 `flow.finalize_session_traces()`를 호출하세요. 호출하지 않으면 batch가 열린 채 남아 마지막 대화가 export되지 않을 수 있습니다. +실험적 [대화형 `Flow`](#대화형-flow-실험적)는 동일한 tracing 수명 주기를 따릅니다. `defer_trace_finalization` 기본값이 `True`이므로 각 `handle_turn()`은 세션 trace를 열린 상태로 유지합니다. 지연된 턴은 턴별 `flow_failed`도 억제합니다. 턴 오류나 세션 중단이 발생하면 세션을 명시적으로 finalize하세요. 그러면 턴별 `FlowFailed` 이벤트 대신 세션 수준 `FlowFinished` 이벤트로 batch가 닫힙니다. REPL/루프는 항상 `try/finally`로 감싸고 종료 시 `flow.finalize_session_traces()`를 호출하세요. 호출하지 않으면 trace batch가 열린 채 남아 최종 대화가 export되지 않을 수 있습니다. ## 스트리밍 -`Flow` 클래스에 `stream = True`. `kickoff(...)`가 표준 이벤트 버스를 통해 `assistant_delta` 등 이벤트를 발생시킵니다. +대화형 UI에서는 `stream_turn()`을 사용하고 순서가 보장된 `StreamFrame` 객체를 순회하세요: + +```python +stream = flow.stream_turn("Where is my order?", session_id=session_id) + +with stream: + for frame in stream.events: + if frame.channel == "llm" and frame.type == "llm_stream_chunk": + print(frame.content, end="", flush=True) + +reply = stream.result +``` + +비대화형 Flow에서는 `stream = True`로 설정하면 `kickoff()`가 `StreamSession`을 반환합니다. `handle_turn()`을 사용할 때 `flow.stream = True`로 설정하지 마세요. 대화형 스트리밍 수명 주기는 `stream_turn()`이 관리합니다. ## import @@ -529,10 +600,15 @@ from crewai.flow import ( router, start, ) +from crewai.flow.conversation import prepare_conversational_turn +from crewai.experimental.conversational import ( + ConversationConfig, + ConversationState, + RouterConfig, +) ``` ## 참고 - [Flow 상태 관리 마스터하기](/ko/guides/flows/mastering-flow-state) - [첫 Flow 만들기](/ko/guides/flows/first-flow) -- 데모: `lib/crewai/runner_conversational_flow_simple.py` diff --git a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx index afe46ee55..ba15f111b 100644 --- a/docs/edge/pt-BR/guides/flows/conversational-flows.mdx +++ b/docs/edge/pt-BR/guides/flows/conversational-flows.mdx @@ -1,35 +1,46 @@ --- 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. +description: Crie apps de chat multi-turno com handle_turn por turno, histórico de mensagens, roteamento de intenção, tracing e streaming estruturado. icon: comments mode: "wide" --- + + **Esta é uma funcionalidade experimental.** A superfície conversacional de + `Flow` (`conversational = True`, `handle_turn`, `stream_turn`, + `ConversationConfig`, `RouterConfig`, `ConversationState` e o grafo + embutido) vive em `crewai.experimental` e pode mudar antes de se tornar + estável. Fixe sua versão do CrewAI se depender de um comportamento + específico. + + ## 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. +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, roteamento opcional de intenção, tracing adiado, streaming estruturado de turnos e um REPL local `flow.chat()`. | 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()` | +| Turno concluído | `conversation_turn_completed`; com o adiamento padrão de traces, `FlowFinished` aguarda `finalize_session_traces()` | +| Trace da sessão inteira | `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. +`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 depois de redefinir o estado de execução do turno. | API | Uso | |-----|-----| | `handle_turn(message, session_id=...)` | Wrapper ergonômico de um turno para `Flow` conversacional | +| `stream_turn(message, session_id=...)` | Transmite um turno conversacional como frames ordenados do runtime | | `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) | + +`handle_turn()`, `stream_turn()` e `chat()` geram `ValueError` se o modo conversacional não estiver habilitado. Aplicar `@ConversationConfig(...)` o habilita automaticamente; caso contrário, defina `conversational = True`. ## Início rápido @@ -46,31 +57,29 @@ from crewai.experimental.conversational import ( @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: + if "order" in message: return "order" - if "tchau" in message or "goodbye" in message: + if "bye" in message or "goodbye" in message: return "goodbye" return "help" @listen("order") def handle_order(self): - reply = "Seu pedido está a caminho." + reply = "Your order is on the way." self.append_assistant_message(reply) return reply @listen("help") def handle_help(self): - reply = "Como posso ajudar?" + reply = "How can I help?" self.append_assistant_message(reply) return reply @listen("goodbye") def handle_goodbye(self): - reply = "Até logo!" + reply = "Goodbye!" self.append_assistant_message(reply) return reply @@ -79,41 +88,49 @@ 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) + flow.handle_turn("Where is my order?", session_id=session_id) + flow.handle_turn("What about returns?", session_id=session_id) finally: - flow.finalize_session_traces() # um link de trace para o chat inteiro + flow.finalize_session_traces() # one trace link for the whole chat ``` +## Streaming de um turno + +Use `stream_turn()` quando uma UI ou um runtime precisar de eventos estruturados para um turno de chat. Ele retorna uma sessão de stream com frames ordenados para roteamento do Flow, chunks do LLM, atividade de tools e mensagens da conversa. + +```python +stream = flow.stream_turn("Where is my order?", session_id=session_id) + +with stream: + for frame in stream.events: + if frame.channel == "llm" and frame.type == "llm_stream_chunk": + print(frame.content, end="", flush=True) + +result = stream.result +``` + +Para o contrato completo dos frames e a lista de canais, consulte [Contrato do Runtime de Streaming](/edge/pt-BR/learn/streaming-runtime-contract). + ## 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. +1. **Preparação do turno** — armazena a mensagem pendente do usuário, resolve o id da sessão, redefine o acompanhamento de execução por turno e chama `kickoff(inputs={"id": session_id})`. 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`. +4. **Hidratação do turno pendente** — acrescenta a mensagem do usuário em `state.messages`, define `current_user_message` / `last_user_message` e classifica opcionalmente quando `intents` / `default_intents` + `intent_llm` estão definidos. +5. **Execução do grafo** — métodos `@start` definidos pelo usuário (se houver) → `route_conversation` (o start/router embutido) → o handler `@listen` selecionado. `route_conversation` também chama o helper sobrescrevível `conversation_start()`. 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) +## Visão geral da configuração -Defina na subclasse de `Flow` como `conversational_config: ClassVar[ConversationalConfig | None]`. +Decorar uma subclasse de `Flow` com `ConversationConfig` anexa os padrões de chat e habilita o modo conversacional. Consulte a [referência completa de campos](#conversationconfig) abaixo. Sobrescreva a pré-classificação por turno com `handle_turn(..., intents=..., intent_llm=...)`. -| 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 | +## Helpers `ChatState` de mais baixo nível -Sobrescreva por kickoff com `intents=` e `intent_llm=`. - -## `ChatState` (formato persistido recomendado) +`ChatState`, o `ConversationalConfig` legado e os helpers de `crewai.flow.conversation` continuam disponíveis para importação em orquestração avançada, testes ou wrappers customizados. Eles são separados da API experimental `ConversationState` / `ConversationConfig` e não adicionam os argumentos nomeados `user_message=` ou `session_id=` a `Flow.kickoff()`. ```python from crewai.flow import ChatState @@ -127,62 +144,64 @@ class MyChatState(ChatState): | Campo | Função | |-------|--------| -| `id` | UUID da sessão (igual a `session_id` / `inputs["id"]`) | +| `id` | UUID da sessão (igual a `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`. +`ConversationalInputs` é um `TypedDict` para as chaves convencionais de `kickoff(inputs={...})`: `id`, `user_message`, `last_intent`. + +O `ConversationState` experimental armazena `messages` como objetos `ConversationMessage` e também fornece `current_user_message`, `ended`, `events` e `agent_threads`. Use `conversation_messages` ao passar seu histórico canônico para um LLM. ## API conversacional em `Flow` -### Parâmetros de `kickoff` / `kickoff_async` +### Parâmetros de `handle_turn` | Parâmetro | Propósito | |-----------|-----------| -| `user_message` | Texto deste turno (ou `{"role": "user", "content": "..."}`) | +| `message` | Texto deste turno | | `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 | +| `**kickoff_kwargs` | Encaminhados para `kickoff()` para opções como `input_files`, `from_checkpoint` e `restore_from_state_id` | + +### Parâmetros de `kickoff` + +`Flow.kickoff()` aceita `inputs`, `input_files`, `from_checkpoint` e `restore_from_state_id`. Passe `inputs={"id": session_id}` quando precisar executar o flow diretamente, mas use `handle_turn()` quando a chamada representar uma mensagem de chat. ### 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)` | +| `conversational` | Defina como `True` para habilitar o grafo conversacional e `handle_turn()` | +| `defer_trace_finalization` | Sobrescrita opcional na instância. Caso contrário, `_should_defer_trace_finalization()` lê `ConversationConfig.defer_trace_finalization`. | +| `suppress_flow_events` | Oculta painéis do flow no console e suprime eventos de execução de métodos; os eventos de início/fim do flow continuam sendo emitidos | +| `stream` | Flag genérica de streaming do Flow. Para turnos conversacionais, use `stream_turn()` em vez de combinar esta flag com `handle_turn()`. | ### Métodos e propriedades | Nome | Descrição | |------|-------------| -| `append_message(role, content, **extra)` | Acrescenta em `state.messages` (roles: `user`, `assistant`, `system`, `tool`) | +| `append_assistant_message(content)` | Acrescenta uma resposta visível ao usuário em `state.messages` | +| `append_message(role, content, **extra)` | Acréscimo de mais baixo nível em `state.messages` | | `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 | +| `_should_defer_trace_finalization()` | Hook avançado/interno que resolve se a finalização de trace por turno é adiada | | `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: +Importáveis de `crewai.flow.conversation` para testes ou orquestração customizada. Esses helpers usam o formato legado de `ConversationalConfig`; `prepare_conversational_turn()` também limpa `last_intent`, ao contrário do `handle_turn()` experimental, que o preserva como contexto do router. | 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) | +| `prepare_conversational_turn(flow, user_message=..., intents=..., intent_llm=..., config=...)` | Hidratação de turno de mais baixo nível para wrappers customizados | | `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 | @@ -192,19 +211,18 @@ Importáveis para testes ou orquestração customizada: ### 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()`. +Defina `default_intents` e `intent_llm`. Cada `handle_turn()` pré-classifica a mensagem atual. Um resultado não vazio retornado por um `route_turn()` customizado tem precedência; caso contrário, `route_conversation` usa a intenção classificada do turno atual. -### B. Classificar dentro do `@router` (prompts mais ricos) +### B. Classificar dentro de `route_turn` (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: +Defina `default_intents=None` para `handle_turn()` apenas acrescentar a mensagem do usuário. Em `route_turn()`, chame `classify_intent` com um prompt ou descrições customizadas: ```python -@router(bootstrap) -def route(self): +def route_turn(self, context): intent = self.classify_intent( - self._routing_prompt(self.state.last_user_message), + self._routing_prompt(self.state.current_user_message), ("GREETING", "ORDER", "RESEARCH", "GOODBYE"), - llm=self.conversational_config.intent_llm or "gpt-4o-mini", + llm="gpt-4o-mini", ) self.state.last_intent = intent return intent @@ -214,7 +232,7 @@ Use **`@listen("RESEARCH")`** (ou similar) para passos com `Agent.kickoff()` e f ## 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. +Cada `handle_turn()` conclui uma execução do grafo, e a conversa continua com outro `handle_turn()` usando o mesmo `session_id`. Com o ciclo de vida de trace adiado padrão, essa execução emite `conversation_turn_completed`, enquanto `FlowFinished` é emitido uma vez quando `finalize_session_traces()` encerra a sessão. `@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. @@ -222,62 +240,51 @@ Não use `@human_feedback` para linhas de chat de follow-up, a menos que um huma ## `Flow` conversacional (experimental) - - **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. - +Habilite o grafo de chat conversacional definindo `conversational = True` em uma subclasse de `Flow` ou aplicando `@ConversationConfig(...)`. O `Flow` base passa a fornecer `route_conversation` como start/router embutido, além dos listeners `converse_turn`, `end_conversation` e `answer_from_history_turn`. Ele gerencia `state.messages`, pode acionar um LLM de roteamento e mantém o batch de trace aberto entre turnos. Você escreve as **rotas customizadas**; o framework cuida do resto. -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. +Use isto quando quiser um chat multi-turno com router 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 import 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 -) +@ConversationConfig(defer_trace_finalization=True) class SupportFlow(Flow[ConversationState]): - conversational = True + def route_turn(self, context: dict) -> str | None: + message = (self.state.current_user_message or "").lower() + if "search" in message or "news" in message: + return "INTERNET_SEARCH" + if "docs" in message or "crewai" in message: + return "CREWAI_DOCS" + return "converse" @listen("INTERNET_SEARCH") def handle_internet_search(self) -> str: """Fresh web research, current news, real-time lookups.""" - ... + reply = "I would run the web research route here." 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.""" - ... + reply = "I would look up the CrewAI docs here." 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 + flow.handle_turn("What can you do?") # routes to converse + flow.handle_turn("Search the web for AI news.") # routes to INTERNET_SEARCH + flow.handle_turn("Check the CrewAI docs.") # routes to CREWAI_DOCS finally: flow.finalize_session_traces() ``` @@ -299,7 +306,7 @@ Decorador de classe que anexa os defaults de chat por classe. |-------|--------|-----------| | `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`. | +| `router` | `None` | Sobrescritas opcionais de `RouterConfig`. Com listeners customizados e um LLM que possa ser resolvido, o roteamento é habilitado automaticamente mesmo quando este campo é omitido. | | `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`. | @@ -307,19 +314,38 @@ Decorador de classe que anexa os defaults de chat por classe. | `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()`. | +Sem rotas customizadas, os turnos caem em `converse`. Com rotas customizadas e um LLM de conversa/router, o framework sintetiza um `RouterConfig` padrão; forneça um explicitamente apenas para customizar seu prompt, lista de rotas, descrições ou comportamento de fallback. Definir `default_intents` usa o caminho legado de pré-classificação. + +Se nenhum LLM de conversa estiver configurado, o `converse_turn` embutido retorna um placeholder de configuração em vez de gerar uma resposta. + ### `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 +from typing import Literal + +from pydantic import BaseModel + +from crewai import LLM +from crewai.experimental.conversational import RouterConfig + + +class MyRoute(BaseModel): + intent: Literal["INTERNET_SEARCH", "CREWAI_DOCS", "converse"] + + +ROUTER_LLM = LLM(model="gpt-4o-mini") + + +router_config = RouterConfig( + prompt="Optional domain framing (policy, voice, persona).", + response_format=MyRoute, # optional; auto-generated otherwise + llm=ROUTER_LLM, # falls back to ConversationConfig.llm + routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # optional; inferred from listeners route_descriptions={ - "INTERNET_SEARCH": "Sobrescreve a docstring só desta rota.", + "INTERNET_SEARCH": "Override the docstring for this one route.", }, - 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 + default_intent="converse", # used when LLM call fails or no LLM available + fallback_intent="converse", # used when LLM returns an invalid route intent_field="intent", ) ``` @@ -328,12 +354,16 @@ O prompt do router é montado automaticamente. Para cada rota o framework escolh 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). +3. O `description` declarado do método (usado por flows declarativos e projeções da DSL). +4. Primeira linha não vazia da docstring do handler `@listen(label)`. +5. Vazio (a rota aparece no catálogo sem descrição). Na prática, **adicionar uma rota é `@listen("X")` + uma docstring de uma linha**: ```python +from crewai.flow import listen + + @listen("INTERNET_SEARCH") def handle_internet_search(self) -> str: """Fresh web research, current news, real-time lookups.""" @@ -352,13 +382,34 @@ Routes: `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. +### Nomeando handlers + +A string em `@listen("…")` é um **rótulo de rota do router** (um nome de evento), e não o nome do método Python. Rótulos de rota e eventos de conclusão de métodos compartilham o mesmo namespace de gatilhos; portanto, dar ao handler o mesmo nome de sua rota faria o handler acionar a si próprio em loop. + +Use um nome de método diferente — os exemplos da documentação usam o prefixo `handle_*`: + +```python +@listen("create_video") +def handle_create_video(self) -> str: + """User wants a new video.""" + ... +``` + +**Não** replique o rótulo da rota no método: + +```python +@listen("create_video") +def create_video(self) -> str: # rejected at flow instantiation + ... +``` + ### 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. | +| `answer_from_history` | `answer_from_history_turn` | Opcional. Depois que houver pelo menos duas mensagens, roteia para cá quando `ConversationConfig.answer_from_history_llm` determinar que a mensagem atual pode ser respondida pelo histórico. | Você pode sobrescrever qualquer uma definindo um handler com o mesmo nome na subclasse. @@ -368,7 +419,7 @@ Você pode sobrescrever qualquer uma definindo um handler com o mesmo nome na su 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. +3. Executa métodos `@start` definidos pelo usuário (se houver), depois `route_conversation` como start/router embutido e, por fim, o handler `@listen` escolhido. `route_conversation` invoca o helper sobrescrevível `conversation_start()`. 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ê. @@ -391,6 +442,8 @@ Ele cobre o loop local comum: 4. Imprime o resultado do assistente. 5. Finaliza traces de sessão adiados em um bloco `finally`. +`chat(defer_trace_finalization=True)` habilita temporariamente a flag de adiamento na instância durante o REPL e restaura o valor anterior ao sair. + Customize o comportamento do terminal com I/O injetável: ```python @@ -409,6 +462,12 @@ Para apps web, workers em background, testes e transportes customizados, continu Para rodar efeitos colaterais (setup de event bus, telemetria) em toda decisão de routing, sobrescreva `route_turn`: ```python +from typing import Any + +from crewai import Flow +from crewai.experimental.conversational import ConversationState + + class SupportFlow(Flow[ConversationState]): conversational = True @@ -417,7 +476,7 @@ class SupportFlow(Flow[ConversationState]): 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(...)`. +Para ignorar completamente o router LLM e escolher uma rota programaticamente, retorne uma string não vazia de `route_turn`. Um retorno falsy **não** invoca `_route_with_config()` a partir da sua sobrescrita; o roteamento segue para a intenção pré-classificada deste turno, depois para `answer_from_history` quando elegível e, por fim, para `converse`. O `last_intent` do turno anterior fica disponível no contexto do router, mas nunca é repetido como fallback. ### `append_assistant_message` e `append_agent_result` @@ -430,7 +489,7 @@ Dentro de um handler `@listen(label)`, escolha: ## Declarando um flow conversacional em JSON/YAML -Um [flow declarativo](/edge/en/concepts/cli) também pode ser conversacional. Adicione um bloco `conversational` no nível raiz e declare suas próprias rotas como métodos que escutam (`listen`) um rótulo de rota: +Um [Flow declarativo](/edge/pt-BR/concepts/cli) também pode ser conversacional. Adicione um bloco `conversational` no nível raiz e declare suas próprias rotas como métodos que fazem `listen` em um rótulo de rota: ```yaml schema: crewai.flow/v1 @@ -455,15 +514,17 @@ methods: input: "${state.current_user_message}" ``` -Declarar o bloco já é o opt-in — `enabled` tem valor padrão `true`. Use `enabled: false` para manter a configuração e desligar o chat. +Declarar o bloco já é o opt-in — `enabled` tem valor padrão `true`. Use `enabled: false` para manter a configuração e desligar o chat. Isso também desabilita a síntese de métodos embutidos, portanto a declaração deve fornecer um grafo não conversacional normal. Três coisas são fornecidas para você: | Fornecido | Detalhe | |----------|--------| | O grafo interno | `route_conversation`, `converse_turn`, `end_conversation` e `answer_from_history_turn` são adicionados automaticamente. Declare um método com um desses nomes para sobrescrevê-lo. | -| Estado da conversa | `ConversationState` é usado quando a declaração não tem bloco `state`. Para adicionar campos, aponte `state` para um modelo Pydantic que estenda `ConversationState`. | -| O catálogo de rotas | Construído a partir dos métodos que declaram um rótulo `listen`. O `description` de cada método é o que o modelo de roteamento lê ao escolher entre rotas. | +| Estado da conversa | `ConversationState` é usado quando não há bloco `state`. Um estado Pydantic definido por `ref` ou `json_schema` é composto automaticamente com os campos conversacionais; ele não precisa estender `ConversationState`. | +| O catálogo de rotas | Inferido de métodos que não são routers e têm rótulos `listen`, excluindo rotas internas. As descrições seguem a precedência acima, e `router.routes` explícito pode limitar as opções. | + +Os campos declarativos `llm`, `router.llm`, `intent_llm` e `answer_from_history_llm` aceitam um id de modelo ou um mapping de configuração, como `{model: openai/gpt-4o-mini, max_tokens: 512}`. O bloco `conversational` também aceita `default_intents`, `answer_from_history_prompt`, `visible_agent_outputs`, `defer_trace_finalization` e os campos de `RouterConfig` mostrados acima. Execute a partir do Python com as mesmas APIs de turno de um Flow conversacional baseado em classe: @@ -486,15 +547,16 @@ Rótulos de rota e nomes de métodos compartilham um único namespace de gatilho | Não expressável | Use no lugar | |-----------------|-------------| -| Uma instância `LLM` viva ou um `BaseLLM` customizado | Uma string de id de modelo, como `gpt-4o-mini` | +| Uma instância `LLM` viva ou um `BaseLLM` customizado | Um id de modelo em string ou mapping estático de configuração | | `router.response_format` como classe de modelo | Omita; o framework sintetiza uma. Um ref ou schema é ignorado com um aviso | -| Overrides de `route_turn()` / `can_answer_from_history()` | Escreva o Flow em Python, ou aponte o `do` de um método para um ref `call: code` | +| Uma sobrescrita de `route_turn()` | Escreva o Flow em Python ou substitua o método declarativo `route_conversation` por uma ação `call: code` / expressão | +| Uma sobrescrita de `can_answer_from_history()` | Escreva o Flow em Python; configure o roteamento padrão por histórico com `conversational.answer_from_history_llm` | -O `crewai run` abre a TUI de chat para um flow conversacional declarativo — a mesma que um Flow conversacional em Python recebe. Um loop de chat precisa de um terminal, então uma execução headless informa isso em vez de rodar um único turno; ali, conduza pelo Python com `handle_turn()` ou `stream_turn()`. Um flow que também usa `@human_feedback` roda em um REPL de terminal, porque o runtime coleta feedback com um prompt bloqueante que a TUI não consegue atender. O `--inputs` não é aceito em um flow conversacional — a entrada de cada turno é a mensagem que você digita — e retomar uma sessão por id ainda não está ligado à CLI; use `flow.handle_turn(message, session_id=...)` no Python para isso. +O `crewai run` abre a TUI de chat para um flow conversacional declarativo — a mesma que um Flow conversacional em Python recebe. Um loop de chat precisa de um terminal, então uma execução headless encerra com código diferente de zero e orientações, em vez de rodar um único turno; ali, conduza pelo Python com `handle_turn()` ou `stream_turn()`. Um método declarativo com um bloco `human_feedback:` (Python: `@human_feedback`) roda em um REPL de terminal, porque o runtime coleta feedback com um prompt bloqueante que a TUI não consegue atender. O `--inputs` não é aceito em um flow conversacional — a entrada de cada turno é a mensagem que você digita — e retomar uma sessão por id ainda não está ligado à CLI; use `flow.handle_turn(message, session_id=...)` no Python para isso. ## Tracing entre turnos -Com `defer_trace_finalization=True` (padrão em `ConversationalConfig`): +Com `defer_trace_finalization=True` (padrão em `ConversationConfig`): - **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()`. @@ -505,17 +567,30 @@ Com `defer_trace_finalization=True` (padrão em `ConversationalConfig`): 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. +`flow.chat()` chama `finalize_session_traces()` para você. Quando você controla o loop com `handle_turn()`, 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. +`suppress_flow_events=True` oculta painéis Rich no console e suprime eventos de execução de métodos. Os eventos de início/fim do Flow continuam sendo emitidos, portanto o ciclo de vida externo do Flow permanece rastreável, mas os spans de métodos individuais são omitidos. ### 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. +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. Turnos adiados também suprimem `flow_failed` por turno; em caso de erro em um turno ou encerramento antecipado da sessão, finalize a sessão explicitamente. Isso fecha o batch com o evento `FlowFinished` no nível da sessão, em vez de um evento `FlowFailed` por turno. Sempre envolva seu REPL/loop em `try/finally` e chame `flow.finalize_session_traces()` na saída. Sem isso, o batch fica aberto e a conversa final 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. +Para UIs conversacionais, use `stream_turn()` e itere sobre seus objetos `StreamFrame` ordenados: + +```python +stream = flow.stream_turn("Where is my order?", session_id=session_id) + +with stream: + for frame in stream.events: + if frame.channel == "llm" and frame.type == "llm_stream_chunk": + print(frame.content, end="", flush=True) + +reply = stream.result +``` + +Para um Flow não conversacional, definir `stream = True` faz `kickoff()` retornar uma `StreamSession`. Não defina `flow.stream = True` ao usar `handle_turn()`; `stream_turn()` controla o ciclo de vida do streaming conversacional. ## Imports @@ -530,10 +605,15 @@ from crewai.flow import ( router, start, ) +from crewai.flow.conversation import prepare_conversational_turn +from crewai.experimental.conversational import ( + ConversationConfig, + ConversationState, + RouterConfig, +) ``` ## 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