mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 01:08:39 -10:00
fix: route bare prompts through the modern Typer run engine (fixes #3310)
A bare free-text prompt (`praisonai "do something"`) now forwards to the same modern `run` path as `praisonai run`, inheriting session continuity, --output modes, the credential gate and permissions. `.yaml` workflows and flag-bearing legacy invocations remain on the legacy dispatcher. Co-authored-by: MervinPraison <[email protected]>
This commit is contained in:
1 parent
7494bbadc9
commit
82f55c97b0
2 files changed
+98
-6
No files matched your search
@@ -74,6 +74,31 @@ def _find_first_command(argv):
|
||||
return None
|
||||
|
||||
|
||||
def _looks_like_bare_prompt(argv, first_cmd):
|
||||
"""Return True when argv is a bare free-text prompt for the modern `run` path.
|
||||
|
||||
A *bare prompt* is a first positional token that is neither a Typer command
|
||||
(already handled upstream) nor a ``.yaml``/``.yml`` file token, invoked with
|
||||
no ``-``/``--`` flags at all. Such an invocation expresses the same intent as
|
||||
``praisonai run "<prompt>"`` and should reach the modern Typer ``run`` engine
|
||||
(session continuity, ``--output`` modes, credential gate, permissions).
|
||||
|
||||
The rule is deliberately conservative: any flag present keeps the invocation
|
||||
on the legacy dispatcher, because the legacy argparse surface owns a large set
|
||||
of flags (``--framework``, ``--auto``, ``--call``, ``--serve``, ...) that
|
||||
``run`` does not implement. ``.yaml``/``.yml`` workflows also stay on legacy.
|
||||
"""
|
||||
if not first_cmd:
|
||||
return False
|
||||
# Any flag anywhere → legacy owns the large deprecated/legacy flag surface.
|
||||
if any(arg.startswith("-") for arg in argv):
|
||||
return False
|
||||
# YAML workflow files stay on the legacy multi-framework path.
|
||||
if first_cmd.lower().endswith((".yaml", ".yml")):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _run_typer(argv):
|
||||
"""Dispatch to the Typer CLI app."""
|
||||
import os
|
||||
@@ -149,7 +174,8 @@ def main():
|
||||
2. --help / -h → Typer help (global or command-level)
|
||||
3. No arguments → Typer interactive TUI
|
||||
4. First arg is a Typer cmd→ Typer (auto-discovered from app.py)
|
||||
5. Everything else → Legacy (prompt, .yaml, deprecated flags)
|
||||
5. Bare free-text prompt → Typer `run` (modern engine)
|
||||
6. Everything else → Legacy (.yaml, deprecated flags)
|
||||
"""
|
||||
argv = sys.argv[1:]
|
||||
|
||||
@@ -180,8 +206,13 @@ def main():
|
||||
if first_cmd in _get_typer_commands():
|
||||
# Known Typer command → Typer
|
||||
_run_typer(argv)
|
||||
elif _looks_like_bare_prompt(argv, first_cmd):
|
||||
# Bare free-text prompt → modern Typer `run` engine (same as
|
||||
# `praisonai run "<prompt>"`), inheriting session continuity,
|
||||
# --output modes, the credential gate and permissions.
|
||||
_run_typer(["run", *argv])
|
||||
else:
|
||||
# Prompt, YAML file, or legacy invocation → legacy
|
||||
# YAML workflow or legacy/deprecated-flag invocation → legacy
|
||||
_run_legacy(argv)
|
||||
|
||||
|
||||
|
||||
@@ -72,6 +72,49 @@ class TestFindFirstCommand(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestLooksLikeBarePrompt(unittest.TestCase):
|
||||
"""``_looks_like_bare_prompt`` gates the modern `run` forwarder.
|
||||
|
||||
True only for a flagless, non-YAML first positional token — so a bare
|
||||
``praisonai "hello"`` reaches Typer `run` while ``.yaml`` workflows and any
|
||||
flag-bearing (legacy) invocation stay on the legacy dispatcher.
|
||||
"""
|
||||
|
||||
def test_plain_prompt_is_bare(self):
|
||||
argv = ["Build a weather agent"]
|
||||
first = dispatcher._find_first_command(argv)
|
||||
self.assertTrue(dispatcher._looks_like_bare_prompt(argv, first))
|
||||
|
||||
def test_single_word_prompt_is_bare(self):
|
||||
argv = ["hello"]
|
||||
first = dispatcher._find_first_command(argv)
|
||||
self.assertTrue(dispatcher._looks_like_bare_prompt(argv, first))
|
||||
|
||||
def test_yaml_token_is_not_bare(self):
|
||||
for token in ("agents.yaml", "agents.yml", "AGENTS.YAML"):
|
||||
argv = [token]
|
||||
first = dispatcher._find_first_command(argv)
|
||||
self.assertFalse(
|
||||
dispatcher._looks_like_bare_prompt(argv, first),
|
||||
f"{token!r} should not be treated as a bare prompt",
|
||||
)
|
||||
|
||||
def test_any_flag_is_not_bare(self):
|
||||
argv = ["Do something", "--framework", "crewai"]
|
||||
first = dispatcher._find_first_command(argv)
|
||||
self.assertFalse(dispatcher._looks_like_bare_prompt(argv, first))
|
||||
|
||||
def test_leading_flag_is_not_bare(self):
|
||||
# A leading flag means the first positional was discovered past a flag;
|
||||
# such invocations belong to legacy, not the bare `run` forwarder.
|
||||
argv = ["--verbose", "hello"]
|
||||
first = dispatcher._find_first_command(argv)
|
||||
self.assertFalse(dispatcher._looks_like_bare_prompt(argv, first))
|
||||
|
||||
def test_no_positional_is_not_bare(self):
|
||||
self.assertFalse(dispatcher._looks_like_bare_prompt([], None))
|
||||
|
||||
|
||||
class TestGetTyperCommandsCache(unittest.TestCase):
|
||||
"""``_get_typer_commands`` caches its result under a lock and does
|
||||
not poison the cache on failure."""
|
||||
@@ -236,8 +279,23 @@ class TestMainRouting(unittest.TestCase):
|
||||
run_typer.assert_called_once()
|
||||
run_legacy.assert_not_called()
|
||||
|
||||
def test_freetext_prompt_routes_to_legacy(self):
|
||||
def test_freetext_prompt_routes_to_typer_run(self):
|
||||
# A bare free-text prompt now reaches the modern Typer `run` engine
|
||||
# (session continuity, --output modes, credential gate) instead of the
|
||||
# legacy path — rewritten to ``run <prompt>``.
|
||||
sys.argv = ["praisonai", "Create a weather app"]
|
||||
with mock.patch.object(
|
||||
dispatcher, "_get_typer_commands", return_value={"chat", "ui"}
|
||||
), mock.patch.object(dispatcher, "_run_typer") as run_typer, \
|
||||
mock.patch.object(dispatcher, "_run_legacy") as run_legacy:
|
||||
dispatcher.main()
|
||||
run_typer.assert_called_once_with(["run", "Create a weather app"])
|
||||
run_legacy.assert_not_called()
|
||||
|
||||
def test_bare_prompt_with_legacy_flag_routes_to_legacy(self):
|
||||
# A prompt combined with any flag stays on legacy: the legacy argparse
|
||||
# surface owns the large deprecated/legacy flag set that `run` lacks.
|
||||
sys.argv = ["praisonai", "Create a weather app", "--framework", "crewai"]
|
||||
with mock.patch.object(
|
||||
dispatcher, "_get_typer_commands", return_value={"chat", "ui"}
|
||||
), mock.patch.object(dispatcher, "_run_typer") as run_typer, \
|
||||
@@ -259,15 +317,18 @@ class TestMainRouting(unittest.TestCase):
|
||||
run_legacy.assert_called_once()
|
||||
run_typer.assert_not_called()
|
||||
|
||||
def test_unknown_command_routes_to_legacy(self):
|
||||
def test_unknown_single_token_routes_to_typer_run(self):
|
||||
# A lone unknown token (no flags, not a YAML path) is treated as a bare
|
||||
# prompt and forwarded to the modern Typer `run` engine — matching the
|
||||
# prior legacy behaviour of running an unknown word as a one-shot prompt.
|
||||
sys.argv = ["praisonai", "totally-unknown"]
|
||||
with mock.patch.object(
|
||||
dispatcher, "_get_typer_commands", return_value={"chat"}
|
||||
), mock.patch.object(dispatcher, "_run_typer") as run_typer, \
|
||||
mock.patch.object(dispatcher, "_run_legacy") as run_legacy:
|
||||
dispatcher.main()
|
||||
run_legacy.assert_called_once()
|
||||
run_typer.assert_not_called()
|
||||
run_typer.assert_called_once_with(["run", "totally-unknown"])
|
||||
run_legacy.assert_not_called()
|
||||
|
||||
|
||||
class TestRunLegacyArgvRestoration(unittest.TestCase):
|
||||
|
||||
Reference in new issue
Block a user