Repository navigation
fix: return usable text when input guardrails reject a turn #5933
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
783262a
41f16a1
cf7ac66
3d1d00f
883504f
6f89765
bf1518e
6b94786
bfcf57d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -197,8 +197,10 @@ async def astart(self, prompt: str, **kwargs): | |
| start_time = time_module.time() | ||
| started_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") | ||
|
|
||
| from ..guardrails.guardrail_result import _GuardrailBlockedResponse | ||
| response = await self.achat(prompt, **kwargs) | ||
| response_str = str(response) if response else "" | ||
| blocked = isinstance(response, _GuardrailBlockedResponse) | ||
| response_str = response if blocked else (str(response) if response else "") | ||
|
|
||
| # Auto-save session after chat. Offloaded to a worker thread so | ||
| # the synchronous FileLock/JSON read-modify-write never stalls | ||
|
|
@@ -210,7 +212,8 @@ async def astart(self, prompt: str, **kwargs): | |
| # Wrap in AutonomyResult for consistent API | ||
| from .autonomy import AutonomyResult | ||
| return AutonomyResult( | ||
| success=True, | ||
| success=not blocked, | ||
| error=response_str if blocked else None, | ||
|
dajiaohuang marked this conversation as resolved.
|
||
| output=response_str, | ||
| completion_reason="caller_mode", | ||
| iterations=1, | ||
|
|
@@ -294,6 +297,13 @@ def _outcome_for_result(self, result): | |
| existing ``completed`` semantics, so behaviour is unchanged on success. | ||
| """ | ||
| from .run_outcome import RunOutcome, PROVIDER_BLOCK_REASONS | ||
| from .autonomy import AutonomyResult | ||
| from ..guardrails.guardrail_result import _GuardrailBlockedResponse | ||
|
|
||
| if isinstance(result, _GuardrailBlockedResponse): | ||
| return RunOutcome(reason="failed", output=str(result), error=str(result)) | ||
| if isinstance(result, AutonomyResult) and isinstance(result.output, _GuardrailBlockedResponse): | ||
| return RunOutcome(reason="failed", output=str(result.output), error=result.error) | ||
| output = str(result) if result is not None else None | ||
| # Read the recorded classification without letting a lookup problem mask a | ||
| # provider block as a successful ``completed``. Only an expected | ||
|
|
@@ -626,7 +636,12 @@ def _start_with_planning(self, prompt: str, **kwargs): | |
| from rich.console import Console | ||
| from rich.panel import Panel | ||
| from rich.markdown import Markdown | ||
|
|
||
| from ..guardrails.guardrail_result import _GuardrailBlockedResponse | ||
|
|
||
| _in_ok, prompt, _in_err = self._validate_input_with_guardrail(prompt) | ||
| if not _in_ok: | ||
| return self._guardrail_blocked_message(_in_err) | ||
|
dajiaohuang marked this conversation as resolved.
Comment on lines
+641
to
+643
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When planning returns no steps, the fallback sends the already-changed prompt through the input guardrail again. Keep the original prompt for the fallback, or avoid applying the same guard twice to that request. |
||
|
|
||
| console = _get_console()() | ||
|
|
||
| # Step 1: Create the plan | ||
|
|
@@ -669,6 +684,8 @@ def _start_with_planning(self, prompt: str, **kwargs): | |
|
|
||
| # Execute the step | ||
| result = self.chat(step_prompt, **kwargs) | ||
| if isinstance(result, _GuardrailBlockedResponse): | ||
| return result | ||
| results.append({"step": i + 1, "description": step.description, "result": result}) | ||
|
|
||
| # Update context for next step (use full result, not truncated) | ||
|
|
@@ -807,6 +824,10 @@ def start(self, prompt: Optional[str] = None, **kwargs: Any) -> Union[str, Gener | |
| kwargs['stream'] = self.stream | ||
| else: | ||
| kwargs['stream'] = sys.stdout.isatty() | ||
| _in_ok, prompt, _in_err = self._validate_input_with_guardrail(prompt) | ||
| if not _in_ok: | ||
| blocked = self._guardrail_blocked_message(_in_err) | ||
| return iter((blocked,)) if kwargs.get('stream') else blocked | ||
| return self._delegate_to_backend(prompt, **kwargs) | ||
|
|
||
| # ───────────────────────────────────────────────────────────────────── | ||
|
|
@@ -844,16 +865,19 @@ def start(self, prompt: Optional[str] = None, **kwargs: Any) -> Union[str, Gener | |
| self._load_history_context() | ||
|
|
||
| # Use chat() — all tools, approval, doom-loop per-tool work | ||
| from ..guardrails.guardrail_result import _GuardrailBlockedResponse | ||
| response = self.chat(prompt, **kwargs) | ||
| response_str = str(response) if response else "" | ||
| blocked = isinstance(response, _GuardrailBlockedResponse) | ||
| response_str = response if blocked else (str(response) if response else "") | ||
|
|
||
| # Auto-save session after chat | ||
| self._auto_save_session() | ||
|
|
||
| # Wrap in AutonomyResult for consistent API | ||
| from .autonomy import AutonomyResult | ||
| return AutonomyResult( | ||
| success=True, | ||
| success=not blocked, | ||
| error=response_str if blocked else None, | ||
| output=response_str, | ||
| completion_reason="caller_mode", | ||
| iterations=1, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -571,11 +571,17 @@ def _process_task_result(agents_instance, context, agent_output): | |
| Eliminates duplication in result processing between sync/async paths. | ||
| """ | ||
| from .protocols import TaskResult | ||
| from ..guardrails.guardrail_result import _GuardrailBlockedResponse | ||
|
|
||
| task = context.task | ||
| task_id = context.task_id | ||
| executor_agent = context.executor_agent | ||
| llm = context.llm | ||
|
|
||
| if isinstance(agent_output, _GuardrailBlockedResponse): | ||
| object.__setattr__(task, "_guardrail_rejection_terminal", True) | ||
| task.status = "failed" | ||
| return TaskResult(task_output=None, success=False, error="Agent input or output was blocked by a guardrail.") | ||
|
Comment on lines
+581
to
+584
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1630,1700p' src/praisonai-agents/praisonaiagents/process/process.py
sed -n '1710,1755p' src/praisonai-agents/praisonaiagents/process/process.py
rg -n 'next_tasks|skip_on_failure|on_error|workflow' src/praisonai-agents/tests/unit/test_input_guardrail_blocked_response.py src/praisonai-agents/tests/unit/test_task_guardrail.py src/praisonai-agents/praisonaiagents/process/process.py | head -110Repository: MervinPraison/PraisonAI Length of output: 20932 🏁 Script executed: set -o pipefail
printf '%s\n' '--- synchronous workflow loop ---'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '1320,1535p'
printf '%s\n' '--- relevant process/agent diff against merge base ---'
git diff --unified=35 1aeee2f39eae8ef79b0d5283b0e40448967b835d 41f16a10f0f36cb59a02a15a7e0dcb95f142ed88 -- src/praisonai-agents/praisonaiagents/process/process.py src/praisonai-agents/praisonaiagents/agents/agents.py
printf '%s\n' '--- workflow failure-related tests and declarations ---'
rg -n -F --glob '*.py' -e 'skip_on_failure' -e 'on_error' -e 'process="workflow"' -e 'Process.WORKFLOW' -e 'next_tasks' src/praisonai-agents/tests src/praisonai-agents/praisonaiagents | head -240Repository: MervinPraison/PraisonAI Length of output: 44409 🏁 Script executed: set -o pipefail
printf '%s\n' '--- remainder of synchronous workflow loop ---'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '1535,1710p'
printf '%s\n' '--- task execution configuration and failure semantics ---'
nl -ba src/praisonai-agents/praisonaiagents/workflows/workflow_configs.py | sed -n '190,235p'
nl -ba src/praisonai-agents/tests/unit/test_task_execution_config.py | sed -n '50,95p;160,230p'
printf '%s\n' '--- workflow stop/continue implementation ---'
nl -ba src/praisonai-agents/praisonaiagents/workflows/workflows.py | sed -n '2170,2250p'Repository: MervinPraison/PraisonAI Length of output: 23488 Stop failed workflow tasks before routing
Suggested fix if not next_task and current_task and current_task.next_tasks:
next_task_name = current_task.next_tasks[0]
next_task = self._task_by_name(next_task_name)
if next_task:
+ failure_continues = (
+ current_task.status != "failed"
+ or getattr(current_task, "on_error", "stop") == "continue"
+ or getattr(next_task, "skip_on_failure", False)
+ or getattr(next_task, "on_error", "stop") == "continue"
+ )
+ if not failure_continues:
+ current_task = None
+ break
# Reset the next task to allow re-execution
next_task.status = "not started"🤖 Prompt for AI Agents🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '1885,1928p' src/praisonai-agents/praisonaiagents/agents/agents.py
sed -n '960,1010p' src/praisonai-agents/praisonaiagents/process/process.py
rg -n 'def _deps_failed|def _should_continue_on_dep_failure|def _depends_on_pending' src/praisonai-agents/praisonaiagents/agents/agents.pyRepository: MervinPraison/PraisonAI Length of output: 4905 🏁 Script executed: set -o pipefail
printf '%s\n' '--- agents helpers and async scheduler ---'
nl -ba src/praisonai-agents/praisonaiagents/agents/agents.py | sed -n '1815,1985p'
printf '%s\n' '--- task dependency fields and defaults ---'
rg -n -F --glob '*.py' -e 'previous_tasks' -e 'next_tasks' -e 'depends_on' -e 'skip_on_failure' -e 'on_error' src/praisonai-agents/praisonaiagents/task src/praisonai-agents/praisonaiagents | head -160
printf '%s\n' '--- process workflow routing ---'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '820,1015p'
printf '%s\n' '--- relevant diff ---'
git diff 1aeee2f39eae8ef79b0d5283b0e40448967b835d..41f16a10f0f36cb59a02a15a7e0dcb95f142ed88 -- src/praisonai-agents/praisonaiagents/agents/agents.py src/praisonai-agents/praisonaiagents/process/process.pyRepository: MervinPraison/PraisonAI Length of output: 44128 🏁 Script executed: set -o pipefail
printf '%s\n' '--- next_tasks registration ---'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '630,675p'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '1305,1340p'
printf '%s\n' '--- process method boundaries and failure guards ---'
rg -n '^ (async )?def (workflow|aworkflow)|_deps_failed|skip_on_failure|on_error' src/praisonai-agents/praisonaiagents/process/process.py
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '1025,1065p'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '1710,1750p'
printf '%s\n' '--- task defaults and initialization ---'
nl -ba src/praisonai-agents/praisonaiagents/task/task.py | sed -n '45,105p'
nl -ba src/praisonai-agents/praisonaiagents/task/task.py | sed -n '375,395p'Repository: MervinPraison/PraisonAI Length of output: 13648 🏁 Script executed: set -o pipefail
printf '%s\n' '--- synchronous workflow route ---'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '1290,1515p'
printf '%s\n' '--- synchronous next_tasks routing and completion ---'
nl -ba src/praisonai-agents/praisonaiagents/process/process.py | sed -n '1515,1708p'
printf '%s\n' '--- async route failure-policy search ---'
rg -n -C 4 -F -- 'status == "failed"' src/praisonai-agents/praisonaiagents/process/process.pyRepository: MervinPraison/PraisonAI Length of output: 30932 Check failed workflow predecessors before async dispatch.
This async scheduler change is separate from the synchronous Suggested fix if task.async_execution:
# If this async task depends on another async task still
# pending in the current batch, flush first so its context
# is available before this one reads it.
if self._depends_on_pending(task, tasks_to_run):
await self._gather_with_isolation([c for _, c in tasks_to_run])
tasks_to_run = []
- # A just-flushed prerequisite may now be failed; don't
- # queue a dependent that would run with missing context
- # unless it opted into graceful degradation.
- if self._deps_failed(task_id) and not self._should_continue_on_dep_failure(task_id):
- self.tasks[task_id].status = "failed"
- continue
+ # Block failed predecessors, including synchronous
+ # next_tasks predecessors, unless continuation is enabled.
+ if self._deps_failed(task_id) and not self._should_continue_on_dep_failure(task_id):
+ self.tasks[task_id].status = "failed"
+ continue
tasks_to_run.append((task_id, self.arun_task(task_id)))
else:
# If we encounter a sync task, we must wait for the previous async tasks to finish.
if tasks_to_run:
await self._gather_with_isolation([c for _, c in tasks_to_run])
tasks_to_run = []
+ if self._deps_failed(task_id) and not self._should_continue_on_dep_failure(task_id):
+ self.tasks[task_id].status = "failed"
+ continue
# Run sync task in an executor to avoid blocking the event loop🤖 Prompt for AI Agents |
||
|
|
||
| if agent_output: | ||
| # Store the response in memory | ||
|
|
@@ -1822,7 +1828,11 @@ async def _arun_task_body(self, task_id, task): | |
| while task.status != "completed" and retries < task_max: | ||
| logger.debug(f"Attempt {retries+1} for task {task_id}") | ||
| if task.status in ["not started", "in progress"]: | ||
| object.__setattr__(task, "_guardrail_rejection_terminal", False) | ||
| task_output = await self.aexecute_task(task_id) | ||
| # Empty output can also mark failure, but remains retryable. | ||
| if getattr(task, "_guardrail_rejection_terminal", False): | ||
| return | ||
| if task_output and self.completion_checker(task, task_output.raw): | ||
| # Apply guardrail validation using shared helper (offloaded to | ||
| # a thread so a blocking LLM guardrail does not stall the loop) | ||
|
|
@@ -2422,7 +2432,11 @@ def _run_task_body(self, task_id, task): | |
| while task.status != "completed" and retries < task_max: | ||
| logger.debug(f"Attempt {retries+1} for task {task_id}") | ||
| if task.status in ["not started", "in progress"]: | ||
| object.__setattr__(task, "_guardrail_rejection_terminal", False) | ||
| task_output = self.execute_task(task_id) | ||
| # Empty output can also mark failure, but remains retryable. | ||
| if getattr(task, "_guardrail_rejection_terminal", False): | ||
| return | ||
| if task_output and self.completion_checker(task, task_output.raw): | ||
| # Apply guardrail validation using shared helper | ||
| task_output, should_retry = self._apply_task_guardrail(task, task_id, task_output) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.