Repository navigation
feat(replay): watch() / replay_app(): a Run button that draws each run live, and embedding #6017
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
Changes from all commits
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 |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| # PraisonAI Replay: watch runs live | ||
|
|
||
| | Example | What it shows | | ||
| |---|---| | ||
| | `watch_flow.py` | Click **Run** and watch each step light up on the map; **Play** goes through it again. No API key. | | ||
| | `watch_team.py` | The same for two real agents (needs `OPENAI_API_KEY`). | | ||
| | `embed_fastapi.py` | Your own app starts a run and shows its progress in an `<iframe>` (`app.mount("/replay", replay_app())`). | | ||
|
|
||
| The same from the command line, for a YAML file: | ||
|
|
||
| ```bash | ||
| praisonai replay watch agents.yaml | ||
| ``` | ||
|
|
||
| Install: `pip install "praisonai[ui]"`. Keep the page on localhost or behind your app's | ||
| login: anyone who can reach it can read the runs, and with a Run button, start one. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| """Show agent progress inside your own web app. | ||
|
|
||
| Your app starts the run and records it; the Replay view, mounted at /replay, | ||
| draws it live in an <iframe>. No dashboard, no second server. | ||
|
|
||
| pip install "praisonai[ui]" fastapi uvicorn | ||
| uvicorn embed_fastapi:app --port 8000 # open http://127.0.0.1:8000/ | ||
| """ | ||
|
|
||
| import threading | ||
| import time | ||
| import uuid | ||
|
|
||
| from fastapi import FastAPI | ||
| from fastapi.responses import HTMLResponse | ||
| from praisonaiagents import AgentFlow | ||
| from praisonaiagents.workflows import StepResult | ||
| from praisonai.replay import record, replay_app | ||
|
|
||
|
|
||
| def research(ctx): | ||
| time.sleep(1.2) | ||
| return StepResult(output="notes") | ||
|
|
||
|
|
||
| def write(ctx): | ||
| time.sleep(1.2) | ||
| return StepResult(output="article") | ||
|
|
||
|
|
||
| flow = AgentFlow(name="Article", steps=[research, write]) | ||
|
|
||
| app = FastAPI() | ||
| app.mount("/replay", replay_app()) # read-only view: /replay/?run=<id> shows one run | ||
|
|
||
|
|
||
| @app.post("/generate") | ||
| def generate(): | ||
| run_id = f"run-{uuid.uuid4().hex[:12]}" | ||
|
|
||
| def job(): | ||
| with record(run_id, live=True): # live: the view follows it as it is written | ||
| flow.start("solar sails") | ||
|
|
||
| threading.Thread(target=job, daemon=True).start() | ||
| return {"run_id": run_id} | ||
|
|
||
|
|
||
| @app.get("/", response_class=HTMLResponse) | ||
| def home(): | ||
| return """<!doctype html><title>My app</title> | ||
| <h1>My app</h1> | ||
| <button id="go">Generate article</button> | ||
| <iframe id="progress" title="Progress" style="width:100%;height:640px;border:1px solid #ccc"></iframe> | ||
| <script> | ||
| go.onclick = async () => { | ||
| const { run_id } = await (await fetch('/generate', { method: 'POST' })).json(); | ||
| progress.src = '/replay/?run=' + run_id; | ||
| }; | ||
| </script>""" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| """Watch a flow run, step by step: click Run and each step lights up on the map. | ||
|
|
||
| No API key needed (the steps are plain Python that take a moment each). | ||
|
|
||
| pip install "praisonai[ui]" | ||
| python watch_flow.py # opens http://127.0.0.1:8090/ | ||
|
|
||
| Map: dashed = not reached, blue = running now, green = done. | ||
| After a run, press Play to go through it again one step at a time. | ||
| """ | ||
|
|
||
| import time | ||
|
|
||
| from praisonaiagents import AgentFlow | ||
| from praisonaiagents.workflows import StepResult | ||
| from praisonai.replay import watch | ||
|
|
||
|
|
||
| def research(ctx): | ||
| time.sleep(1.5) | ||
| return StepResult(output=f"3 sources about {ctx.input}") | ||
|
|
||
|
|
||
| def outline(ctx): | ||
| time.sleep(1.5) | ||
| return StepResult(output="intro, body, close") | ||
|
|
||
|
|
||
| def draft(ctx): | ||
| time.sleep(1.5) | ||
| return StepResult(output="first draft") | ||
|
|
||
|
|
||
| def review(ctx): | ||
| time.sleep(1.5) | ||
| return StepResult(output="approved") | ||
|
|
||
|
|
||
| flow = AgentFlow(name="Write an article", steps=[research, outline, draft, review]) | ||
| watch(flow, input="solar sails") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| """Watch two agents work together, live (calls the LLM: needs OPENAI_API_KEY). | ||
|
|
||
| python watch_team.py # opens http://127.0.0.1:8090/, press Run | ||
| """ | ||
|
|
||
| from praisonaiagents import Agent, AgentTeam, Task | ||
| from praisonai.replay import watch | ||
|
|
||
| researcher = Agent(name="Researcher", instructions="Give 3 short facts.", llm="gpt-4o-mini") | ||
| writer = Agent(name="Writer", instructions="Write one short paragraph from the facts.", llm="gpt-4o-mini") | ||
| team = AgentTeam( | ||
| agents=[researcher, writer], | ||
| tasks=[ | ||
| Task(name="research", description="3 facts about solar sails", agent=researcher, expected_output="3 facts"), | ||
| Task(name="write", description="One paragraph using the facts", agent=writer, expected_output="a paragraph"), | ||
| ], | ||
| ) | ||
| watch(team) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| """Watch an agent, team or flow run live: a page with a Run button (PraisonAI Replay). | ||
|
|
||
| from praisonai.replay import watch | ||
| watch(flow) # opens the browser; each Run is recorded and drawn live | ||
|
|
||
| from praisonai.replay import replay_app | ||
| app.mount("/replay", replay_app(flow)) # inside your own FastAPI/Starlette app | ||
|
|
||
| The page is aiui's Replay view without the dashboard around it | ||
| (``praisonaiui.features.replay.create_replay_app``). Requires ``pip install "praisonai[ui]"``. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import inspect | ||
| import threading | ||
| import webbrowser | ||
| from pathlib import Path | ||
| from typing import Any, Callable, Optional | ||
|
|
||
| _LOOPBACK_HOSTS = {"127.0.0.1", "localhost", "::1"} | ||
| _INSTALL_HINT = 'PraisonAI Replay needs PraisonAI UI: pip install -U "praisonai[ui]"' | ||
|
|
||
|
|
||
| def _runner(target: Any) -> Optional[Callable[[str, str], None]]: | ||
| """``run(input, run_id)`` that records *target* under ``run_id``, live.""" | ||
| if target is None: | ||
| return None | ||
| from .recorder import record | ||
| from .yaml_run import is_yaml_path, run_yaml_recorded | ||
|
|
||
| if isinstance(target, (str, Path)): | ||
| if not is_yaml_path(str(target)): | ||
| raise ValueError(f"not a YAML file: {target}") | ||
| path = str(target) | ||
|
|
||
| def run_yaml(_text: str, run_id: str) -> None: | ||
| run_yaml_recorded(path, run_id=run_id, live=True) | ||
|
|
||
| return run_yaml | ||
|
|
||
| start = getattr(target, "start", None) | ||
| call = start if callable(start) else target | ||
| if not callable(call): | ||
| raise TypeError("target must be an Agent, AgentTeam, AgentFlow, a YAML path or a callable(input)") | ||
|
|
||
| # An AgentTeam skips tasks it already completed and appends each input to | ||
| # every task's context; each Run starts them afresh with the original context. | ||
| reset = getattr(target, "_reset_task_state_for_item", None) | ||
| tasks = getattr(target, "tasks", None) | ||
| tasks = list(tasks.values()) if isinstance(tasks, dict) and callable(reset) else [] | ||
| contexts = [(t, list(t.context) if isinstance(getattr(t, "context", None), list) else None) for t in tasks] | ||
|
|
||
| def run(text: str, run_id: str) -> None: | ||
| if callable(reset): | ||
| reset() | ||
| for task, context in contexts: | ||
| task.context = list(context) if context is not None else None | ||
| with record(run_id, live=True): | ||
| out = call(text) | ||
| if inspect.isawaitable(out): | ||
| asyncio.run(out) | ||
|
Comment on lines
+62
to
+63
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. Wrap non-coroutine awaitables before calling On supported Python versions before 3.14,
|
||
|
|
||
| return run | ||
|
|
||
|
|
||
| def _title(target: Any) -> str: | ||
| if isinstance(target, (str, Path)): | ||
| return f"PraisonAI Replay: {Path(target).name}" | ||
| name = getattr(target, "name", None) or getattr(target, "__name__", None) | ||
| return f"PraisonAI Replay: {name}" if isinstance(name, str) and name else "PraisonAI Replay" | ||
|
|
||
|
|
||
| def replay_app(target: Any = None, *, input: str = "", placeholder: str = "", title: Optional[str] = None): | ||
| """The Replay page as an ASGI app; with *target*, it has a Run button. | ||
|
|
||
| Args: | ||
| target: An Agent, AgentTeam or AgentFlow (``.start(input)`` is called), a | ||
| YAML file (run as written; no input box), or any ``callable(input)``. | ||
| ``None`` gives a read-only view of recorded runs. | ||
| input: Text the input box starts with. | ||
| placeholder: Placeholder for the input box. | ||
| title: Page title (default: from the target's name). | ||
|
|
||
| Each Run is recorded with :func:`praisonai.replay.record` under a new run id | ||
| and the page follows it live. One run at a time. The same *target* object is | ||
| reused: a team's tasks start afresh each run, an Agent keeps its chat history. | ||
|
|
||
| Open ``<mount>/`` for the run list, or ``<mount>/?run=<run_id>`` for one run | ||
| (handy in an ``<iframe>``). | ||
| """ | ||
| try: | ||
| from praisonaiui.features.replay import create_replay_app | ||
| except ImportError as e: # aiui missing or older than the embeddable app | ||
| raise ImportError(_INSTALL_HINT) from e | ||
| is_yaml = isinstance(target, (str, Path)) | ||
| return create_replay_app( | ||
| _runner(target), | ||
| title=title or _title(target), | ||
| input=None if is_yaml else input, | ||
| placeholder=placeholder, | ||
| ) | ||
|
|
||
|
|
||
| def watch( | ||
| target: Any = None, | ||
| *, | ||
| input: str = "", | ||
| placeholder: str = "", | ||
| title: Optional[str] = None, | ||
| host: str = "127.0.0.1", | ||
| port: int = 8090, | ||
| open_browser: bool = True, | ||
| ) -> None: | ||
| """Serve :func:`replay_app` and open it in the browser (blocks until Ctrl+C). | ||
|
|
||
| Example:: | ||
|
|
||
| from praisonai.replay import watch | ||
| watch(flow, input="Write about solar sails") | ||
| """ | ||
| import uvicorn | ||
|
|
||
| app = replay_app(target, input=input, placeholder=placeholder, title=title) | ||
| if host not in _LOOPBACK_HOSTS: | ||
| print( | ||
| f"WARNING: serving on {host} lets anyone who can reach it read your runs" | ||
| + (" and start new ones." if target is not None else ".") | ||
| ) | ||
| browser_host = "127.0.0.1" if host in ("0.0.0.0", "::") else host | ||
| if ":" in browser_host and not browser_host.startswith("["): | ||
| browser_host = f"[{browser_host}]" | ||
| url = f"http://{browser_host}:{port}/" | ||
| print(f"PraisonAI Replay: {url} (Ctrl+C to stop)") | ||
| if open_browser: | ||
| threading.Timer(1.0, webbrowser.open, [url]).start() | ||
| uvicorn.run(app, host=host, port=port, log_level="warning") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reject a second request while the flow runs.
If a user clicks Generate article twice before the first run finishes,
/generatestarts two threads and returns two run IDs. Both threads use the sameflow.AgentFlow.run()rejects the concurrent call, so the second accepted run fails without executing its steps. Reserve the flow before starting the thread, and return a busy response for a second request. Release the reservation when the job finishes. (raw.githubusercontent.com)