Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 78 additions & 9 deletions lib/crewai/src/crewai/core/providers/human_input.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import asyncio
from contextvars import ContextVar, Token
import sys
from typing import TYPE_CHECKING, Protocol, runtime_checkable
from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable


if TYPE_CHECKING:
Expand Down Expand Up @@ -143,6 +143,33 @@ def _get_output_string(answer: AgentFinish) -> str:
return answer.output
return answer.output.model_dump_json()

@staticmethod
def _render_answer_panel(formatter: Any, answer: AgentFinish) -> None:
"""Print the agent's current answer above the feedback prompt.

Rendered in training mode as well as normal mode: both prompts ask the
operator to judge the result, so the result has to be visible in both.

Args:
formatter: Event-listener formatter owning the rich console.
answer: The agent's finished answer to display.
"""
from rich.panel import Panel
from rich.text import Text

result_content = Text()
result_content.append(
HumanInputProvider._get_output_string(answer), style="bright_green"
)
formatter.console.print(
Panel(
result_content,
title="✅ Agent Final Answer",
border_style="green",
padding=(1, 2),
)
)


class SyncHumanInputProvider(HumanInputProvider):
"""Default human input provider with sync and async support."""
Expand Down Expand Up @@ -179,12 +206,20 @@ def handle_feedback(
Returns:
The final answer after feedback processing.
"""
feedback = self._prompt_input(context.crew)
is_verbose = context.agent.verbose or bool(
context.crew and getattr(context.crew, "verbose", False)
)
feedback = self._prompt_input(
context.crew,
answer=None if is_verbose else formatted_answer,
)

if context._is_training_mode():
return self._handle_training_feedback(formatted_answer, feedback, context)

return self._handle_regular_feedback(formatted_answer, feedback, context)
return self._handle_regular_feedback(
formatted_answer, feedback, context, show_answer=not is_verbose
)

async def handle_feedback_async(
self,
Expand All @@ -200,15 +235,21 @@ async def handle_feedback_async(
Returns:
The final answer after feedback processing.
"""
feedback = await self._prompt_input_async(context.crew)
is_verbose = context.agent.verbose or bool(
context.crew and getattr(context.crew, "verbose", False)
)
feedback = await self._prompt_input_async(
context.crew,
answer=None if is_verbose else formatted_answer,
)

if context._is_training_mode():
return await self._handle_training_feedback_async(
formatted_answer, feedback, context
)

return await self._handle_regular_feedback_async(
formatted_answer, feedback, context
formatted_answer, feedback, context, show_answer=not is_verbose
)

@staticmethod
Expand Down Expand Up @@ -239,13 +280,15 @@ def _handle_regular_feedback(
current_answer: AgentFinish,
initial_feedback: str,
context: ExecutorContext,
show_answer: bool = False,
) -> AgentFinish:
"""Process regular feedback with iteration loop.

Args:
current_answer: The agent's current answer.
initial_feedback: Initial human feedback string.
context: Executor context for callbacks.
show_answer: When True, display the updated answer before each prompt.

Returns:
Final answer after all feedback iterations.
Expand All @@ -259,7 +302,10 @@ def _handle_regular_feedback(
else:
context.messages.append(context._format_feedback_message(feedback))
answer = context._invoke_loop()
feedback = self._prompt_input(context.crew)
feedback = self._prompt_input(
context.crew,
answer=answer if show_answer else None,
)

return answer

Expand Down Expand Up @@ -291,13 +337,15 @@ async def _handle_regular_feedback_async(
current_answer: AgentFinish,
initial_feedback: str,
context: AsyncExecutorContext,
show_answer: bool = False,
) -> AgentFinish:
"""Process regular feedback with async iteration loop.

Args:
current_answer: The agent's current answer.
initial_feedback: Initial human feedback string.
context: Async executor context for callbacks.
show_answer: When True, display the updated answer before each prompt.

Returns:
Final answer after all feedback iterations.
Expand All @@ -311,16 +359,25 @@ async def _handle_regular_feedback_async(
else:
context.messages.append(context._format_feedback_message(feedback))
answer = await context._ainvoke_loop()
feedback = await self._prompt_input_async(context.crew)
feedback = await self._prompt_input_async(
context.crew,
answer=answer if show_answer else None,
)

return answer

@staticmethod
def _prompt_input(crew: Crew | None) -> str:
def _prompt_input(
crew: Crew | None,
answer: AgentFinish | None = None,
) -> str:
"""Show rich panel and prompt for input.

Args:
crew: The crew instance for context.
answer: When provided, the agent result is printed before the
feedback prompt so the operator can see it even when verbose
is disabled.

Returns:
User input string from terminal.
Expand All @@ -334,6 +391,9 @@ def _prompt_input(crew: Crew | None) -> str:
formatter.pause_live_updates()

try:
if answer is not None:
HumanInputProvider._render_answer_panel(formatter, answer)

if crew and getattr(crew, "_train", False):
prompt_text = (
"TRAINING MODE: Provide feedback to improve the agent's performance.\n\n"
Expand Down Expand Up @@ -369,11 +429,17 @@ def _prompt_input(crew: Crew | None) -> str:
formatter.resume_live_updates()

@staticmethod
async def _prompt_input_async(crew: Crew | None) -> str:
async def _prompt_input_async(
crew: Crew | None,
answer: AgentFinish | None = None,
) -> str:
"""Show rich panel and prompt for input without blocking the event loop.

Args:
crew: The crew instance for context.
answer: When provided, the agent result is printed before the
feedback prompt so the operator can see it even when verbose
is disabled.

Returns:
User input string from terminal.
Expand All @@ -387,6 +453,9 @@ async def _prompt_input_async(crew: Crew | None) -> str:
formatter.pause_live_updates()

try:
if answer is not None:
HumanInputProvider._render_answer_panel(formatter, answer)

if crew and getattr(crew, "_train", False):
prompt_text = (
"TRAINING MODE: Provide feedback to improve the agent's performance.\n\n"
Expand Down
37 changes: 37 additions & 0 deletions lib/crewai/tests/agents/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,43 @@ async def kickoff_side_effect(executor, *_args, **_kwargs):
assert mock_kickoff.await_count == 2


@pytest.mark.parametrize("training_mode", [False, True])
def test_prompt_input_renders_answer_in_both_modes(training_mode):
"""The agent's answer must be shown above the feedback prompt in training mode too.

Both prompts ask the operator to judge the result ("Provide feedback on the
Final Result above" / "feedback about the result quality"), so hiding the
answer in training mode leaves them judging output they cannot see.
"""
from unittest.mock import MagicMock

from crewai.core.providers.human_input import SyncHumanInputProvider

crew = MagicMock()
crew._train = training_mode
formatter = MagicMock()
answer = AgentFinish(output="Hello", thought="", text="")

with (
patch(
"crewai.events.event_listener.event_listener.formatter",
formatter,
),
patch("builtins.input", return_value=""),
):
SyncHumanInputProvider._prompt_input(crew, answer=answer)

printed = [str(call.args[0]) for call in formatter.console.print.call_args_list]
rendered = [
call.args[0]
for call in formatter.console.print.call_args_list
if str(getattr(call.args[0], "title", "")) == "✅ Agent Final Answer"
]
assert rendered, (
f"answer panel not rendered with _train={training_mode}; printed={printed}"
)


def test_interpolate_inputs():
agent = Agent(
role="{topic} specialist",
Expand Down