mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 08:11:07 -10:00
fix(cli): surface continued-failure reason and wire serve ui-gateway --agents
workflows.py: a None-output step with on_error=continue now carries its failure_reason in the step result, so the final error reports the real reason instead of the generic 'step(s) failed' fallback. serve.py: serve ui-gateway --agents now loads the agents YAML into a parsed list and forwards agents=[...] (the gateway host takes a list via configure_host, not an agents_file path); the previous signature check always rejected the option. Fails loudly (exit 2) on a missing/malformed file. Co-authored-by: Mervin Praison <[email protected]>
This commit is contained in:
1 parent
3b3b82d9af
commit
69769cbdd8
3 files changed
+91
-16
No files matched your search
@@ -1818,13 +1818,19 @@ class AgentFlow:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save output to file: {e}")
|
||||
|
||||
# Store result
|
||||
results.append({
|
||||
# Store result. A step that failed under on_error="continue" reaches
|
||||
# here (the on_error="stop" branch above already returned); carry its
|
||||
# failure_reason through so the final result surfaces *why* it failed
|
||||
# rather than the generic "step(s) failed: <name>" fallback.
|
||||
step_record = {
|
||||
"step": step.name,
|
||||
"output": output,
|
||||
"status": self.step_statuses.get(step.name, "completed"),
|
||||
"retries": retry_count
|
||||
})
|
||||
}
|
||||
if step_failed and failure_reason:
|
||||
step_record["error"] = failure_reason
|
||||
results.append(step_record)
|
||||
previous_output = output
|
||||
|
||||
if verbose:
|
||||
|
||||
@@ -101,6 +101,39 @@ class TestAgentStepProducingNothingFailsTheRun:
|
||||
assert "error" not in result
|
||||
|
||||
|
||||
class TestContinuedFailureKeepsItsReason:
|
||||
"""A None-output step with on_error='continue' must still carry its reason.
|
||||
|
||||
The run continues past the failed step, so it takes the general result path
|
||||
rather than the on_error='stop' branch. That path used to omit the step's
|
||||
error, so the final result fell back to the generic 'step(s) failed: <name>'
|
||||
text instead of the computed 'agent produced no output' diagnostic.
|
||||
"""
|
||||
|
||||
def test_continued_none_step_reports_its_reason_not_the_generic_fallback(self):
|
||||
first = Task(
|
||||
name="first", agent=_NullAgent(), action="do it", max_retries=0
|
||||
)
|
||||
first.on_error = "continue"
|
||||
wf = Workflow(
|
||||
name="continue-flow",
|
||||
steps=[
|
||||
first,
|
||||
Task(name="second", agent=_RealAgent(), action="Write a haiku", max_retries=0),
|
||||
],
|
||||
)
|
||||
result = wf.start("go")
|
||||
assert result["status"] == "failed"
|
||||
failed = [s for s in result["steps"] if s.get("status") == "failed"]
|
||||
assert failed and failed[0]["step"] == "first"
|
||||
assert "produced no output" in (failed[0].get("error") or ""), (
|
||||
"a continued failure lost its diagnostic reason"
|
||||
)
|
||||
assert "produced no output" in result.get("error", ""), (
|
||||
"the final result must surface the real reason, not the fallback"
|
||||
)
|
||||
|
||||
|
||||
class TestHandlerStepsAreNotJudgedThisWay:
|
||||
"""A custom handler returning None is legitimate -- leave it alone."""
|
||||
|
||||
|
||||
@@ -429,6 +429,51 @@ def serve_ui(
|
||||
raise typer.Exit(4)
|
||||
|
||||
|
||||
def _load_gateway_agents(agents_file: str, output):
|
||||
"""Load an agents YAML file into a list of ``Agent`` objects.
|
||||
|
||||
The integrated gateway host forwards ``agents`` (a parsed list) to
|
||||
``configure_host``; it does not accept a file path. Parse the YAML here and
|
||||
fail loudly (exit 2) on a missing/malformed/empty file rather than silently
|
||||
starting an empty gateway.
|
||||
"""
|
||||
import os
|
||||
import yaml
|
||||
|
||||
if not os.path.exists(agents_file):
|
||||
output.print_error(f"Agent file not found: {agents_file}")
|
||||
raise typer.Exit(2)
|
||||
try:
|
||||
with open(agents_file, "r") as f:
|
||||
config = yaml.safe_load(f)
|
||||
except (OSError, yaml.YAMLError) as e:
|
||||
output.print_error(f"Could not read agent file {agents_file}: {e}")
|
||||
raise typer.Exit(2)
|
||||
|
||||
agents_cfg = config.get("agents") if isinstance(config, dict) else None
|
||||
if not isinstance(agents_cfg, list) or not agents_cfg:
|
||||
output.print_error(f"Agent file {agents_file} has no 'agents' list")
|
||||
raise typer.Exit(2)
|
||||
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agents = []
|
||||
for entry in agents_cfg:
|
||||
if not isinstance(entry, dict):
|
||||
output.print_error(
|
||||
f"Agent file {agents_file} contains a non-mapping agent entry"
|
||||
)
|
||||
raise typer.Exit(2)
|
||||
agents.append(
|
||||
Agent(
|
||||
name=entry.get("name", "agent"),
|
||||
instructions=entry.get("instructions", ""),
|
||||
llm=entry.get("llm"),
|
||||
)
|
||||
)
|
||||
return agents
|
||||
|
||||
|
||||
@app.command("ui-gateway")
|
||||
def serve_ui_gateway(
|
||||
host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"),
|
||||
@@ -455,20 +500,11 @@ def serve_ui_gateway(
|
||||
run_integrated_gateway = getattr(_mod, "run_integrated_gateway")
|
||||
kwargs = dict(host=host, port=port, title=title, style=style)
|
||||
# `--agents` was declared (and used in this command's own docstring
|
||||
# example) but never forwarded. Pass it only when supplied, and only
|
||||
# when the gateway host accepts it, so an older praisonai-bot still
|
||||
# starts instead of raising TypeError.
|
||||
# example) but never forwarded. The gateway host forwards **kwargs to
|
||||
# ``configure_host``, which accepts a parsed ``agents`` list (not a
|
||||
# file path), so load the YAML here and pass ``agents=[...]``.
|
||||
if agents_file:
|
||||
import inspect
|
||||
|
||||
if "agents_file" in inspect.signature(run_integrated_gateway).parameters:
|
||||
kwargs["agents_file"] = agents_file
|
||||
else:
|
||||
output.print_error(
|
||||
"--agents is not supported by the installed praisonai-bot "
|
||||
"gateway host; upgrade praisonai-bot to use it."
|
||||
)
|
||||
raise typer.Exit(2)
|
||||
kwargs["agents"] = _load_gateway_agents(agents_file, output)
|
||||
run_integrated_gateway(**kwargs)
|
||||
except ImportError as e:
|
||||
output.print_error(f"UI-Gateway module not available: {e}")
|
||||
|
||||
Reference in new issue
Block a user