crewui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crewui/__init__.py +42 -0
- crewui/__main__.py +15 -0
- crewui/_helpers.py +78 -0
- crewui/app.py +308 -0
- crewui/cli.py +65 -0
- crewui/default.tcss +105 -0
- crewui/demo.py +158 -0
- crewui/py.typed +0 -0
- crewui-0.1.0.dist-info/METADATA +164 -0
- crewui-0.1.0.dist-info/RECORD +14 -0
- crewui-0.1.0.dist-info/WHEEL +5 -0
- crewui-0.1.0.dist-info/entry_points.txt +2 -0
- crewui-0.1.0.dist-info/licenses/LICENSE +21 -0
- crewui-0.1.0.dist-info/top_level.txt +1 -0
crewui/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""crewui - a Textual TUI for any sequential CrewAI crew.
|
|
2
|
+
|
|
3
|
+
``CrewAIPipelineTUI`` is a Textual App that renders a sidebar task tracker, an
|
|
4
|
+
agent output log, and a pipeline log for a CrewAI ``Crew``. The host builds the
|
|
5
|
+
crew and hands it in; the TUI stays out of crew construction entirely.
|
|
6
|
+
|
|
7
|
+
The package depends only on ``crewai`` and ``textual``. Live metrics come
|
|
8
|
+
straight from CrewAI (token usage off ``kickoff()``'s result); everything
|
|
9
|
+
host-specific is delegated to injected callbacks, so nothing here reaches up
|
|
10
|
+
into the host application:
|
|
11
|
+
|
|
12
|
+
- ``on_start()`` - fired in the worker thread right before kickoff (e.g. to
|
|
13
|
+
bind a run id and stamp the start time)
|
|
14
|
+
- ``on_complete(result)`` - fired right after kickoff (e.g. to persist run
|
|
15
|
+
metrics)
|
|
16
|
+
- ``get_token_cost(input_tokens, output_tokens)`` - returns the USD estimate to
|
|
17
|
+
display; cost is not a CrewAI metric, so the host supplies it
|
|
18
|
+
|
|
19
|
+
Human review (``Task(human_input=True)``) is routed to the TUI's input box
|
|
20
|
+
instead of a blocking terminal ``input()`` - see ``crewui.app``.
|
|
21
|
+
|
|
22
|
+
Typical usage::
|
|
23
|
+
|
|
24
|
+
from crewui import CrewAIPipelineTUI
|
|
25
|
+
|
|
26
|
+
CrewAIPipelineTUI(
|
|
27
|
+
crew=build_my_crew(),
|
|
28
|
+
record_prefix="myapp",
|
|
29
|
+
pipeline_name="My Pipeline",
|
|
30
|
+
).run()
|
|
31
|
+
|
|
32
|
+
The class owns a default theme; a derived class ships its own look by setting
|
|
33
|
+
its own ``CSS_PATH``.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from crewui.app import CrewAIPipelineTUI
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
41
|
+
|
|
42
|
+
__all__ = ["CrewAIPipelineTUI", "__version__"]
|
crewui/__main__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""``python -m crewui`` entry point.
|
|
2
|
+
|
|
3
|
+
Kept to two lines and a guard so it mirrors the console script exactly: both
|
|
4
|
+
routes go through ``crewui.cli:main``, so a wheel cannot ship one working and
|
|
5
|
+
the other broken.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from crewui.cli import main
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
sys.exit(main())
|
crewui/_helpers.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""crewui._helpers - Pure functions extracted from CrewAIPipelineTUI.
|
|
2
|
+
|
|
3
|
+
The TUI class itself (CrewAIPipelineTUI) is App + widgets + threading and needs
|
|
4
|
+
a textual.pilot harness to test properly. The pure-logic helpers it relies on -
|
|
5
|
+
truncation, log routing, step-message formatting, sidebar layout, metrics
|
|
6
|
+
block formatting - have no Textual or threading dependency and live here so
|
|
7
|
+
they can be branch-covered by ordinary unit tests.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from crewai.agents.parser import AgentAction, AgentFinish
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def truncate(text: str, limit: int) -> str:
|
|
18
|
+
"""Return ``text`` truncated to ``limit`` characters.
|
|
19
|
+
|
|
20
|
+
Returns the input unchanged when it is already at or below the limit.
|
|
21
|
+
"""
|
|
22
|
+
return text[:limit] if len(text) > limit else text
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def route_log_record(record_name: str, prefix: str) -> str:
|
|
26
|
+
"""Decide which TUI log pane a logging record belongs in.
|
|
27
|
+
|
|
28
|
+
Returns ``"agent"`` when ``record_name`` starts with ``prefix`` (the host
|
|
29
|
+
app's record prefix), else ``"crew"``.
|
|
30
|
+
"""
|
|
31
|
+
return "agent" if record_name.startswith(prefix) else "crew"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def task_layout(tasks: list[Any]) -> list[tuple[str, str]]:
|
|
35
|
+
"""Build the sidebar entries for a sequential pipeline.
|
|
36
|
+
|
|
37
|
+
For each task that has an assigned agent, return a ``(heading, role)``
|
|
38
|
+
pair in pipeline order. ``heading`` is the task's display name
|
|
39
|
+
(``Task.name``), falling back to the agent role when a task carries no
|
|
40
|
+
name; ``role`` is the agent role shown on the task's status row. Because
|
|
41
|
+
the heading is per-task rather than per-agent, an agent that runs more than
|
|
42
|
+
one task in the pipeline gets a distinct heading for each.
|
|
43
|
+
|
|
44
|
+
Tasks with no agent are skipped, so a partially-wired crew never raises.
|
|
45
|
+
"""
|
|
46
|
+
layout: list[tuple[str, str]] = []
|
|
47
|
+
for task in tasks:
|
|
48
|
+
agent = getattr(task, "agent", None)
|
|
49
|
+
if agent is None:
|
|
50
|
+
continue
|
|
51
|
+
role = agent.role
|
|
52
|
+
layout.append((task.name or role, role))
|
|
53
|
+
return layout
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_metrics_block(total_tokens: int, estimated_cost_usd: float, status: str = "done") -> str:
|
|
57
|
+
"""Render the fixed-width metrics summary shown in the sidebar."""
|
|
58
|
+
return f" Tokens: {total_tokens:,}\n Cost: ${estimated_cost_usd:.4f}\n Status: {status}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def format_step_message(step: object) -> str:
|
|
62
|
+
"""Format a CrewAI step (AgentAction / AgentFinish / other) as rich-text.
|
|
63
|
+
|
|
64
|
+
AgentAction yields a Thought + tool-call line and an optional result block.
|
|
65
|
+
AgentFinish yields an Answer line. Anything else is rendered as its
|
|
66
|
+
truncated ``str()``. Trusts the crewai parser types - the caller's
|
|
67
|
+
callback is responsible for swallowing any unexpected exceptions, since
|
|
68
|
+
the step-callback contract is fire-and-forget telemetry.
|
|
69
|
+
"""
|
|
70
|
+
if isinstance(step, AgentAction):
|
|
71
|
+
tool_call = f"[cyan]> {step.tool}[/cyan]({truncate(step.tool_input, 120)})"
|
|
72
|
+
msg = f"[yellow]Thought:[/yellow] {step.thought}\n{tool_call}"
|
|
73
|
+
if step.result:
|
|
74
|
+
msg += f"\n[dim]{truncate(step.result, 300)}[/dim]"
|
|
75
|
+
return msg
|
|
76
|
+
if isinstance(step, AgentFinish):
|
|
77
|
+
return f"[bold green]Answer:[/bold green] {truncate(str(step.output), 500)}"
|
|
78
|
+
return truncate(str(step), 300)
|
crewui/app.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""crewui.app - the Textual App for a sequential CrewAI pipeline.
|
|
2
|
+
|
|
3
|
+
``CrewAIPipelineTUI`` renders a sidebar task tracker, an agent output log, and
|
|
4
|
+
a pipeline log for a sequential crew. The host builds the crew and hands it in,
|
|
5
|
+
so the TUI stays out of crew construction.
|
|
6
|
+
|
|
7
|
+
Human review (``Task(human_input=True)``) is handled by routing CrewAI's
|
|
8
|
+
feedback prompt to the input box instead of a blocking terminal ``input()`` -
|
|
9
|
+
see ``_make_tui_human_input_provider`` and ``_await_feedback``.
|
|
10
|
+
|
|
11
|
+
The sidebar title is the host-supplied ``pipeline_name``; ``record_prefix`` is
|
|
12
|
+
used only to route log records to the agent vs pipeline pane. The sidebar reads
|
|
13
|
+
each task's display name (``Task.name``) and agent role straight off
|
|
14
|
+
``crew.tasks``, so the caller only wires the crew - no separate task map.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
import threading
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
from crewai import Crew
|
|
26
|
+
from textual import work
|
|
27
|
+
from textual.app import App, ComposeResult
|
|
28
|
+
from textual.containers import Horizontal, Vertical
|
|
29
|
+
from textual.css.query import NoMatches
|
|
30
|
+
from textual.widgets import Input, Label, RichLog, Static
|
|
31
|
+
|
|
32
|
+
from crewui._helpers import (
|
|
33
|
+
format_metrics_block,
|
|
34
|
+
format_step_message,
|
|
35
|
+
route_log_record,
|
|
36
|
+
task_layout,
|
|
37
|
+
truncate,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from crewai.core.providers.human_input import SyncHumanInputProvider
|
|
42
|
+
|
|
43
|
+
logger = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CrewAIPipelineTUI(App[None]):
|
|
47
|
+
# The class owns the default theme. Absolute (not the bare "default.tcss")
|
|
48
|
+
# because Textual resolves a relative CSS_PATH against the *concrete*
|
|
49
|
+
# class's module file - so a derived class would otherwise look for the
|
|
50
|
+
# stylesheet next to its own module. A derived class overrides the theme
|
|
51
|
+
# by setting its own CSS_PATH.
|
|
52
|
+
CSS_PATH = str(Path(__file__).parent / "default.tcss")
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
crew: Crew,
|
|
57
|
+
record_prefix: str = "pipeline",
|
|
58
|
+
pipeline_name: str = "",
|
|
59
|
+
dry_run: bool = False,
|
|
60
|
+
on_start: Callable[[], None] | None = None,
|
|
61
|
+
on_complete: Callable[[object], None] | None = None,
|
|
62
|
+
get_token_cost: Callable[[int, int], float] | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
super().__init__()
|
|
65
|
+
self._crew = crew
|
|
66
|
+
self._record_prefix = record_prefix
|
|
67
|
+
self._pipeline_name = pipeline_name
|
|
68
|
+
self._dry_run = dry_run
|
|
69
|
+
self._on_start = on_start
|
|
70
|
+
self._on_complete = on_complete
|
|
71
|
+
self._get_token_cost = get_token_cost
|
|
72
|
+
self._task_widgets: list[tuple[Label, Label]] = []
|
|
73
|
+
# Human-review bridge: the worker thread parks on this event while the
|
|
74
|
+
# operator types feedback into the input box on the UI thread.
|
|
75
|
+
self._feedback_event: threading.Event | None = None
|
|
76
|
+
self._feedback_value: str = ""
|
|
77
|
+
self._log_handler: _TUILogHandler | None = None
|
|
78
|
+
self._crew.step_callback = self._make_step_callback()
|
|
79
|
+
|
|
80
|
+
def compose(self) -> ComposeResult:
|
|
81
|
+
with Horizontal():
|
|
82
|
+
with Vertical(id="sidebar"):
|
|
83
|
+
yield Label(self._pipeline_name, id="sidebar-title")
|
|
84
|
+
|
|
85
|
+
for heading, role in task_layout(self._crew.tasks):
|
|
86
|
+
yield Label(heading, classes="phase-heading")
|
|
87
|
+
name_lbl = Label(role, classes="task-name")
|
|
88
|
+
status_lbl = Label("Waiting", classes="task-status")
|
|
89
|
+
self._task_widgets.append((name_lbl, status_lbl))
|
|
90
|
+
yield name_lbl
|
|
91
|
+
yield status_lbl
|
|
92
|
+
|
|
93
|
+
yield Static("", id="metrics")
|
|
94
|
+
|
|
95
|
+
with Vertical(id="main"):
|
|
96
|
+
with Vertical(id="messages-pane"):
|
|
97
|
+
yield Label("Agent Output", classes="pane-title")
|
|
98
|
+
yield RichLog(id="agent-log", highlight=True, markup=True, wrap=True)
|
|
99
|
+
yield Input(
|
|
100
|
+
placeholder="Human review (idle)",
|
|
101
|
+
disabled=True,
|
|
102
|
+
id="human-input",
|
|
103
|
+
)
|
|
104
|
+
with Vertical(id="logs-pane"):
|
|
105
|
+
yield Label("Pipeline Logs", id="logs-title", classes="pane-title")
|
|
106
|
+
yield RichLog(id="crew-log", highlight=True, markup=True)
|
|
107
|
+
|
|
108
|
+
def on_mount(self) -> None:
|
|
109
|
+
# Held so on_unmount can detach it. A handler left on the root logger
|
|
110
|
+
# after the app stops would route later records through call_from_thread
|
|
111
|
+
# into a dead app - harmless in a one-shot CLI, a leak anywhere the host
|
|
112
|
+
# keeps running after the TUI closes.
|
|
113
|
+
self._log_handler = _TUILogHandler(self)
|
|
114
|
+
logging.getLogger().addHandler(self._log_handler)
|
|
115
|
+
if self._dry_run:
|
|
116
|
+
self._write_crew("[yellow]Dry run mode: pipeline not started.[/yellow]")
|
|
117
|
+
# Render the metrics block zeroed so the sidebar reads as a complete
|
|
118
|
+
# preview rather than a blank panel - no run happened, so the
|
|
119
|
+
# figures are zero and the status says so.
|
|
120
|
+
self.query_one("#metrics", Static).update(
|
|
121
|
+
format_metrics_block(total_tokens=0, estimated_cost_usd=0.0, status="dry run")
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
self._start_run()
|
|
125
|
+
|
|
126
|
+
def on_unmount(self) -> None:
|
|
127
|
+
if self._log_handler is not None:
|
|
128
|
+
logging.getLogger().removeHandler(self._log_handler)
|
|
129
|
+
self._log_handler = None
|
|
130
|
+
|
|
131
|
+
@work(thread=True)
|
|
132
|
+
def _start_run(self) -> None:
|
|
133
|
+
from crewai.core.providers.human_input import reset_provider, set_provider
|
|
134
|
+
|
|
135
|
+
if self._on_start is not None:
|
|
136
|
+
self._on_start()
|
|
137
|
+
|
|
138
|
+
for i, task in enumerate(self._crew.tasks):
|
|
139
|
+
orig: Callable[..., None] | None = task.callback
|
|
140
|
+
task.callback = self._make_task_callback(i, orig)
|
|
141
|
+
|
|
142
|
+
self.call_from_thread(self._set_task_running, 0)
|
|
143
|
+
|
|
144
|
+
# Route CrewAI's human_input feedback prompt to the input box instead of
|
|
145
|
+
# a blocking terminal input(). Set in this (worker) thread so kickoff's
|
|
146
|
+
# get_provider() - same thread - picks it up; reset when the run ends.
|
|
147
|
+
token = set_provider(_make_tui_human_input_provider(self))
|
|
148
|
+
try:
|
|
149
|
+
result = self._crew.kickoff()
|
|
150
|
+
self.call_from_thread(self._on_done, result)
|
|
151
|
+
except Exception as exc:
|
|
152
|
+
self.call_from_thread(self._write_agent, f"[bold red]Pipeline error: {exc}[/bold red]")
|
|
153
|
+
self.call_from_thread(self._write_crew, f"[bold red]Pipeline error: {exc}[/bold red]")
|
|
154
|
+
finally:
|
|
155
|
+
reset_provider(token)
|
|
156
|
+
|
|
157
|
+
def _make_task_callback(
|
|
158
|
+
self, idx: int, orig: Callable[..., None] | None
|
|
159
|
+
) -> Callable[..., None]:
|
|
160
|
+
def _cb(output: object) -> None:
|
|
161
|
+
self.call_from_thread(self._set_task_done, idx)
|
|
162
|
+
if orig is not None:
|
|
163
|
+
orig(output)
|
|
164
|
+
|
|
165
|
+
return _cb
|
|
166
|
+
|
|
167
|
+
def _make_step_callback(self) -> Callable[[object], None]:
|
|
168
|
+
def _cb(step: object) -> None:
|
|
169
|
+
try:
|
|
170
|
+
msg = format_step_message(step)
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
logger.debug("step callback error: %s", exc)
|
|
173
|
+
return
|
|
174
|
+
self.call_from_thread(self._write_agent, msg)
|
|
175
|
+
|
|
176
|
+
return _cb
|
|
177
|
+
|
|
178
|
+
def _set_task_running(self, idx: int) -> None:
|
|
179
|
+
if idx < len(self._task_widgets):
|
|
180
|
+
name_lbl, status_lbl = self._task_widgets[idx]
|
|
181
|
+
name_lbl.add_class("running")
|
|
182
|
+
status_lbl.add_class("running")
|
|
183
|
+
status_lbl.update("Running...")
|
|
184
|
+
|
|
185
|
+
def _set_task_done(self, idx: int) -> None:
|
|
186
|
+
if idx < len(self._task_widgets):
|
|
187
|
+
name_lbl, status_lbl = self._task_widgets[idx]
|
|
188
|
+
name_lbl.remove_class("running")
|
|
189
|
+
name_lbl.add_class("done")
|
|
190
|
+
status_lbl.remove_class("running")
|
|
191
|
+
status_lbl.add_class("done")
|
|
192
|
+
status_lbl.update("Done")
|
|
193
|
+
next_idx = idx + 1
|
|
194
|
+
if next_idx < len(self._task_widgets):
|
|
195
|
+
self._set_task_running(next_idx)
|
|
196
|
+
|
|
197
|
+
def _on_done(self, result: object) -> None:
|
|
198
|
+
raw = getattr(result, "raw", str(result))
|
|
199
|
+
self._write_agent("[bold green]Pipeline complete.[/bold green]")
|
|
200
|
+
self._write_agent(truncate(raw, 2000))
|
|
201
|
+
|
|
202
|
+
# Hand the result to the host for persistence; a save failure must not
|
|
203
|
+
# take the UI down, so swallow and surface it in the pipeline log.
|
|
204
|
+
if self._on_complete is not None:
|
|
205
|
+
try:
|
|
206
|
+
self._on_complete(result)
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
logger.debug("on_complete callback error: %s", exc)
|
|
209
|
+
self._write_crew(f"[yellow]Metrics error: {exc}[/yellow]")
|
|
210
|
+
|
|
211
|
+
usage = getattr(result, "token_usage", None)
|
|
212
|
+
if usage is None:
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
input_tokens = getattr(usage, "prompt_tokens", 0)
|
|
216
|
+
output_tokens = getattr(usage, "completion_tokens", 0)
|
|
217
|
+
cost = self._get_token_cost(input_tokens, output_tokens) if self._get_token_cost else 0.0
|
|
218
|
+
try:
|
|
219
|
+
self.query_one("#metrics", Static).update(
|
|
220
|
+
format_metrics_block(
|
|
221
|
+
total_tokens=getattr(usage, "total_tokens", input_tokens + output_tokens),
|
|
222
|
+
estimated_cost_usd=cost,
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
except NoMatches:
|
|
226
|
+
logger.debug("metrics widget not mounted")
|
|
227
|
+
|
|
228
|
+
def _write_agent(self, msg: str) -> None:
|
|
229
|
+
try:
|
|
230
|
+
self.query_one("#agent-log", RichLog).write(msg)
|
|
231
|
+
except NoMatches:
|
|
232
|
+
logger.debug("agent-log widget not mounted, dropping message")
|
|
233
|
+
|
|
234
|
+
def _write_crew(self, msg: str) -> None:
|
|
235
|
+
try:
|
|
236
|
+
self.query_one("#crew-log", RichLog).write(msg)
|
|
237
|
+
except NoMatches:
|
|
238
|
+
logger.debug("crew-log widget not mounted, dropping message")
|
|
239
|
+
|
|
240
|
+
# -- human review --
|
|
241
|
+
|
|
242
|
+
def _await_feedback(self) -> str:
|
|
243
|
+
"""Worker-thread side of the human-review gate.
|
|
244
|
+
|
|
245
|
+
Opens the input box on the UI thread, parks until the operator submits,
|
|
246
|
+
then returns what they typed. Empty (just Enter) means "accept", per
|
|
247
|
+
CrewAI's feedback loop.
|
|
248
|
+
"""
|
|
249
|
+
self._feedback_event = threading.Event()
|
|
250
|
+
self._feedback_value = ""
|
|
251
|
+
self.call_from_thread(self._open_feedback_gate)
|
|
252
|
+
self._feedback_event.wait()
|
|
253
|
+
return self._feedback_value
|
|
254
|
+
|
|
255
|
+
def _open_feedback_gate(self) -> None:
|
|
256
|
+
self._write_agent(
|
|
257
|
+
"[bold yellow]Human review requested - reply below (Enter to accept).[/bold yellow]"
|
|
258
|
+
)
|
|
259
|
+
inp = self.query_one("#human-input", Input)
|
|
260
|
+
inp.placeholder = "Your feedback - Enter to accept, or type changes"
|
|
261
|
+
inp.disabled = False
|
|
262
|
+
inp.focus()
|
|
263
|
+
|
|
264
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
265
|
+
if event.input.id != "human-input" or self._feedback_event is None:
|
|
266
|
+
return
|
|
267
|
+
self._feedback_value = event.value
|
|
268
|
+
event.input.value = ""
|
|
269
|
+
event.input.disabled = True
|
|
270
|
+
event.input.placeholder = "Human review (idle)"
|
|
271
|
+
self._feedback_event.set()
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _make_tui_human_input_provider(app: CrewAIPipelineTUI) -> SyncHumanInputProvider:
|
|
275
|
+
"""Build a CrewAI human-input provider that routes the feedback prompt to
|
|
276
|
+
the TUI input box instead of a blocking terminal ``input()``.
|
|
277
|
+
|
|
278
|
+
Isolated here, with a deferred import, because it leans on crewai's
|
|
279
|
+
semi-internal provider API (``crewai.core.providers.human_input``). That is
|
|
280
|
+
the sanctioned injection point but may move between versions - this is the
|
|
281
|
+
one place to fix if it does. ``output_to_review`` is accepted (crewai passes the
|
|
282
|
+
answer under review) but unused: the step-callback already streams that
|
|
283
|
+
answer into the agent-log pane before the gate opens.
|
|
284
|
+
"""
|
|
285
|
+
from crewai.core.providers.human_input import SyncHumanInputProvider
|
|
286
|
+
|
|
287
|
+
# crewai ships no stubs, so SyncHumanInputProvider is Any and mypy --strict
|
|
288
|
+
# rejects subclassing it; the subclass is the sanctioned injection point.
|
|
289
|
+
class _TUIHumanInputProvider(SyncHumanInputProvider): # type: ignore[misc]
|
|
290
|
+
@staticmethod
|
|
291
|
+
def _prompt_input(crew: Crew | None = None, output_to_review: str | None = None) -> str:
|
|
292
|
+
return app._await_feedback()
|
|
293
|
+
|
|
294
|
+
return _TUIHumanInputProvider()
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class _TUILogHandler(logging.Handler):
|
|
298
|
+
def __init__(self, app: CrewAIPipelineTUI) -> None:
|
|
299
|
+
super().__init__()
|
|
300
|
+
self._app = app
|
|
301
|
+
|
|
302
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
303
|
+
msg = self.format(record)
|
|
304
|
+
target = route_log_record(record.name, self._app._record_prefix)
|
|
305
|
+
if target == "agent":
|
|
306
|
+
self._app.call_from_thread(self._app._write_agent, msg)
|
|
307
|
+
else:
|
|
308
|
+
self._app.call_from_thread(self._app._write_crew, msg)
|
crewui/cli.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""crewui.cli - the command-line surface.
|
|
2
|
+
|
|
3
|
+
``crewui`` is a library first: the point of the package is ``from crewui import
|
|
4
|
+
CrewAIPipelineTUI`` inside a host app. The CLI carries just enough to make an
|
|
5
|
+
install self-evidently working - a ``--version`` that both entry points share,
|
|
6
|
+
and a ``demo`` subcommand that runs the bundled offline pipeline so a fresh
|
|
7
|
+
``pipx install crewui`` shows something on screen without an API key.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
|
|
16
|
+
from crewui import __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(
|
|
21
|
+
prog="crewui",
|
|
22
|
+
description="A Textual TUI for sequential CrewAI pipelines.",
|
|
23
|
+
)
|
|
24
|
+
parser.add_argument(
|
|
25
|
+
"--version",
|
|
26
|
+
action="version",
|
|
27
|
+
version=f"crewui {__version__}",
|
|
28
|
+
)
|
|
29
|
+
sub = parser.add_subparsers(dest="command")
|
|
30
|
+
demo = sub.add_parser(
|
|
31
|
+
"demo",
|
|
32
|
+
help="Run the bundled offline demo pipeline in the TUI (no API key needed).",
|
|
33
|
+
)
|
|
34
|
+
demo.add_argument(
|
|
35
|
+
"--dry-run",
|
|
36
|
+
action="store_true",
|
|
37
|
+
help="Render the layout without walking the pipeline.",
|
|
38
|
+
)
|
|
39
|
+
return parser
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
43
|
+
"""Entry point for the ``crewui`` console script and ``python -m crewui``.
|
|
44
|
+
|
|
45
|
+
Returns a process exit code. With no subcommand, prints help and exits 0 -
|
|
46
|
+
the install is proven working by ``--version`` and ``demo``, so a bare
|
|
47
|
+
invocation is informational, not an error.
|
|
48
|
+
"""
|
|
49
|
+
parser = build_parser()
|
|
50
|
+
args = parser.parse_args(argv)
|
|
51
|
+
|
|
52
|
+
if args.command == "demo":
|
|
53
|
+
# Imported lazily so ``crewui --version`` does not pull in textual and
|
|
54
|
+
# construct crewai agents just to print a string.
|
|
55
|
+
from crewui.demo import run_demo
|
|
56
|
+
|
|
57
|
+
run_demo(dry_run=args.dry_run)
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
parser.print_help()
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
sys.exit(main())
|
crewui/default.tcss
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Screen {
|
|
2
|
+
layout: horizontal;
|
|
3
|
+
background: $background;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
#sidebar {
|
|
7
|
+
width: 34;
|
|
8
|
+
border-right: solid $surface;
|
|
9
|
+
padding: 1 1;
|
|
10
|
+
layout: vertical;
|
|
11
|
+
overflow-y: auto;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#sidebar-title {
|
|
15
|
+
color: $accent;
|
|
16
|
+
text-style: bold;
|
|
17
|
+
margin-bottom: 1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.phase-heading {
|
|
21
|
+
color: $accent;
|
|
22
|
+
text-style: bold;
|
|
23
|
+
margin-top: 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.task-name {
|
|
27
|
+
color: $text-muted;
|
|
28
|
+
padding-left: 2;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.task-name.running {
|
|
32
|
+
color: $warning;
|
|
33
|
+
text-style: bold;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.task-name.done {
|
|
37
|
+
color: $success;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.task-status {
|
|
41
|
+
color: $text-muted;
|
|
42
|
+
padding-left: 4;
|
|
43
|
+
height: 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.task-status.running {
|
|
47
|
+
color: $warning;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.task-status.done {
|
|
51
|
+
color: $success;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#metrics {
|
|
55
|
+
dock: bottom;
|
|
56
|
+
border-top: solid $surface;
|
|
57
|
+
padding: 1;
|
|
58
|
+
color: $text-muted;
|
|
59
|
+
height: 6;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#main {
|
|
63
|
+
layout: vertical;
|
|
64
|
+
width: 1fr;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#messages-pane {
|
|
68
|
+
height: 60%;
|
|
69
|
+
border-bottom: solid $surface;
|
|
70
|
+
layout: vertical;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.pane-title {
|
|
74
|
+
padding: 0 1;
|
|
75
|
+
color: $accent;
|
|
76
|
+
text-style: bold;
|
|
77
|
+
height: 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
#agent-log {
|
|
81
|
+
height: 1fr;
|
|
82
|
+
padding: 0 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#human-input {
|
|
86
|
+
margin: 0 1 1 1;
|
|
87
|
+
opacity: 0.4;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#logs-pane {
|
|
91
|
+
height: 40%;
|
|
92
|
+
layout: vertical;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#logs-title {
|
|
96
|
+
padding: 0 1;
|
|
97
|
+
color: $text-muted;
|
|
98
|
+
text-style: bold;
|
|
99
|
+
height: 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#crew-log {
|
|
103
|
+
height: 1fr;
|
|
104
|
+
padding: 0 1;
|
|
105
|
+
}
|
crewui/demo.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""crewui.demo - a self-contained, offline demonstration of the TUI.
|
|
2
|
+
|
|
3
|
+
``crewui demo`` builds a small three-phase sequential crew and drives it through
|
|
4
|
+
``CrewAIPipelineTUI`` without any LLM call or network access. It exists so that
|
|
5
|
+
``pipx install crewui`` yields something immediately runnable - a live look at
|
|
6
|
+
the sidebar tracker, the agent-output stream, and the pipeline log - and so the
|
|
7
|
+
release smoke test has an entry point that exercises the real App end to end.
|
|
8
|
+
|
|
9
|
+
The crew is a genuine ``crewai.Crew`` of genuine ``Agent`` / ``Task`` objects
|
|
10
|
+
(so the sidebar reads real ``Task.name`` / ``agent.role`` values), but its
|
|
11
|
+
``kickoff`` is replaced with a scripted walk that emits canned step messages
|
|
12
|
+
and fires each task callback in turn. That keeps the demo deterministic and
|
|
13
|
+
free of API keys while still driving every code path the App uses to render a
|
|
14
|
+
run: step callbacks, per-task status transitions, and the final result +
|
|
15
|
+
token-usage block.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import time
|
|
22
|
+
import types
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from crewai import Agent, Crew, Process, Task
|
|
26
|
+
from crewai.agents.parser import AgentAction, AgentFinish
|
|
27
|
+
|
|
28
|
+
from crewui.app import CrewAIPipelineTUI
|
|
29
|
+
|
|
30
|
+
# The demo's scripted phases: (task name, agent role, thought, tool, tool input,
|
|
31
|
+
# tool result, final answer). Nothing here is sent anywhere - it is canned prose
|
|
32
|
+
# rendered into the panes so the layout and status flow are visible.
|
|
33
|
+
_PHASES = [
|
|
34
|
+
(
|
|
35
|
+
"Reconnaissance",
|
|
36
|
+
"Scout",
|
|
37
|
+
"Map the surface before touching anything.",
|
|
38
|
+
"enumerate",
|
|
39
|
+
"example.com",
|
|
40
|
+
"found 3 subdomains, 2 open ports",
|
|
41
|
+
"Surface mapped: api.example.com, www.example.com, mail.example.com.",
|
|
42
|
+
),
|
|
43
|
+
(
|
|
44
|
+
"Analysis",
|
|
45
|
+
"Analyst",
|
|
46
|
+
"Weigh what the scout turned up.",
|
|
47
|
+
"assess",
|
|
48
|
+
"3 subdomains",
|
|
49
|
+
"api.example.com exposes an unauthenticated debug route",
|
|
50
|
+
"One notable exposure on api.example.com; the rest look routine.",
|
|
51
|
+
),
|
|
52
|
+
(
|
|
53
|
+
"Report",
|
|
54
|
+
"Scribe",
|
|
55
|
+
"Write it up so a human can act on it.",
|
|
56
|
+
"compose",
|
|
57
|
+
"1 finding",
|
|
58
|
+
"drafted a 1-paragraph summary",
|
|
59
|
+
"Report ready: 1 finding worth a closer look on api.example.com.",
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class _Usage:
|
|
66
|
+
"""Stand-in for CrewAI's token-usage object; only these three fields are
|
|
67
|
+
read by the App's metrics block."""
|
|
68
|
+
|
|
69
|
+
prompt_tokens: int
|
|
70
|
+
completion_tokens: int
|
|
71
|
+
total_tokens: int
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class _Result:
|
|
76
|
+
"""Stand-in for a CrewOutput: the App reads ``.raw`` and ``.token_usage``."""
|
|
77
|
+
|
|
78
|
+
raw: str
|
|
79
|
+
token_usage: _Usage
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _scripted_kickoff(crew: Crew) -> _Result:
|
|
83
|
+
"""Walk the crew's tasks without calling an LLM.
|
|
84
|
+
|
|
85
|
+
For each task: stream a Thought + tool-call and an Answer through the crew's
|
|
86
|
+
``step_callback`` (both set by the App before this runs), pause briefly so
|
|
87
|
+
the transition is visible, then fire the task's ``callback`` to advance the
|
|
88
|
+
sidebar. Returns a canned result carrying ``raw`` + ``token_usage`` so the
|
|
89
|
+
App renders the completion panel and metrics block.
|
|
90
|
+
"""
|
|
91
|
+
for task, phase in zip(crew.tasks, _PHASES, strict=True):
|
|
92
|
+
_, _, thought, tool, tool_input, tool_result, answer = phase
|
|
93
|
+
if crew.step_callback is not None:
|
|
94
|
+
crew.step_callback(
|
|
95
|
+
AgentAction(
|
|
96
|
+
thought=thought, tool=tool, tool_input=tool_input, text="", result=tool_result
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
time.sleep(0.6)
|
|
100
|
+
if crew.step_callback is not None:
|
|
101
|
+
crew.step_callback(AgentFinish(thought=thought, output=answer, text=answer))
|
|
102
|
+
if task.callback is not None:
|
|
103
|
+
task.callback(answer)
|
|
104
|
+
time.sleep(0.4)
|
|
105
|
+
|
|
106
|
+
return _Result(
|
|
107
|
+
raw="Demo pipeline complete - 3 phases, 1 finding.",
|
|
108
|
+
token_usage=_Usage(prompt_tokens=1200, completion_tokens=340, total_tokens=1540),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_demo_crew() -> Crew:
|
|
113
|
+
"""Build the offline demo crew with a scripted ``kickoff``.
|
|
114
|
+
|
|
115
|
+
A dummy ``OPENAI_API_KEY`` is set only if none is present so that
|
|
116
|
+
constructing the agents never prompts; no request is ever made because
|
|
117
|
+
``kickoff`` is replaced before the App can call it.
|
|
118
|
+
"""
|
|
119
|
+
os.environ.setdefault("OPENAI_API_KEY", "crewui-demo-no-network")
|
|
120
|
+
|
|
121
|
+
agents = []
|
|
122
|
+
tasks = []
|
|
123
|
+
for name, role, *_ in _PHASES:
|
|
124
|
+
agent = Agent(
|
|
125
|
+
role=role,
|
|
126
|
+
goal=f"{role} phase of the demo pipeline",
|
|
127
|
+
backstory=f"The {role.lower()} in a small demonstration crew.",
|
|
128
|
+
llm="gpt-4o-mini",
|
|
129
|
+
verbose=False,
|
|
130
|
+
)
|
|
131
|
+
agents.append(agent)
|
|
132
|
+
tasks.append(
|
|
133
|
+
Task(
|
|
134
|
+
description=f"{name} phase",
|
|
135
|
+
expected_output="a short summary",
|
|
136
|
+
agent=agent,
|
|
137
|
+
name=name,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
crew = Crew(agents=agents, tasks=tasks, process=Process.sequential)
|
|
142
|
+
# Replace kickoff with the scripted walk. Crew is a pydantic model, so a
|
|
143
|
+
# plain attribute assignment is rejected; object.__setattr__ installs the
|
|
144
|
+
# bound method past that guard. This is the whole reason the demo can run
|
|
145
|
+
# with no API key and no network.
|
|
146
|
+
object.__setattr__(crew, "kickoff", types.MethodType(_scripted_kickoff, crew))
|
|
147
|
+
return crew
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def run_demo(dry_run: bool = False) -> None:
|
|
151
|
+
"""Launch the TUI against the offline demo crew."""
|
|
152
|
+
CrewAIPipelineTUI(
|
|
153
|
+
crew=build_demo_crew(),
|
|
154
|
+
record_prefix="crewui.demo",
|
|
155
|
+
pipeline_name="crewui demo pipeline",
|
|
156
|
+
dry_run=dry_run,
|
|
157
|
+
get_token_cost=lambda inp, out: (inp * 3 + out * 15) / 1_000_000,
|
|
158
|
+
).run()
|
crewui/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: crewui
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Textual TUI for sequential CrewAI pipelines
|
|
5
|
+
Author: Nathan Bottomley-Cook
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/coolhandle01/crewui
|
|
8
|
+
Project-URL: Issues, https://github.com/coolhandle01/crewui/issues
|
|
9
|
+
Keywords: crewai,crewai-tools,textual,tui,terminal,console,ai-agents,agentic,pipeline,observability,monitoring
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: User Interfaces
|
|
22
|
+
Classifier: Topic :: System :: Monitoring
|
|
23
|
+
Classifier: Topic :: Terminals
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: <3.14,>=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: crewai<1.16,>=1.15.6
|
|
29
|
+
Requires-Dist: textual>=0.60
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-cov>=5; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy>=1.11; extra == "dev"
|
|
35
|
+
Requires-Dist: pylint>=3.2; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff==0.16.0; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# crewui
|
|
40
|
+
|
|
41
|
+
A [Textual](https://textual.textualize.io/) terminal UI for a sequential
|
|
42
|
+
[CrewAI](https://github.com/crewAIInc/crewAI) pipeline. You build a `Crew`;
|
|
43
|
+
crewui renders it — a sidebar task tracker, a live agent-output stream, a
|
|
44
|
+
pipeline log, and a token/cost summary — and, when a task asks for human
|
|
45
|
+
review, routes the feedback prompt to an input box instead of a blocking
|
|
46
|
+
terminal `input()`.
|
|
47
|
+
|
|
48
|
+
crewui is a presentation layer and nothing more. It reads a `Crew` and touches
|
|
49
|
+
only two things beyond it: the step/task callback contract and, for human
|
|
50
|
+
review, one semi-internal crewai provider hook. Everything application-specific
|
|
51
|
+
— where a run id is recorded, how run metrics are computed and priced — is
|
|
52
|
+
injected through callbacks, so the package never reaches up into your app.
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pipx install crewui # the CLI + the demo, isolated
|
|
58
|
+
# or, as a library in your project:
|
|
59
|
+
pip install crewui
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
See it run without writing any code or setting an API key:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
crewui demo # drives a scripted three-phase pipeline in the TUI
|
|
66
|
+
crewui demo --dry-run # render the layout without walking the pipeline
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The demo is fully offline — no LLM call, no network — so it doubles as a smoke
|
|
70
|
+
test that the install works.
|
|
71
|
+
|
|
72
|
+
## Use it in your app
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from crewui import CrewAIPipelineTUI
|
|
76
|
+
|
|
77
|
+
CrewAIPipelineTUI(
|
|
78
|
+
crew=build_my_crew(), # any sequential crewai.Crew
|
|
79
|
+
record_prefix="myapp", # log records under this name render in the agent pane
|
|
80
|
+
pipeline_name="My Pipeline", # sidebar title
|
|
81
|
+
).run()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The sidebar reads each task's display name (`Task.name`) and agent role straight
|
|
85
|
+
off `crew.tasks`, so wiring the crew is the whole job — there is no separate task
|
|
86
|
+
map to maintain.
|
|
87
|
+
|
|
88
|
+
### Injecting host behaviour
|
|
89
|
+
|
|
90
|
+
Three optional callbacks carry everything crewui deliberately does not know:
|
|
91
|
+
|
|
92
|
+
| Callback | When it fires | Typical use |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `on_start()` | worker thread, right before `kickoff()` | bind a run id, stamp the start time |
|
|
95
|
+
| `on_complete(result)` | right after `kickoff()` | persist run metrics |
|
|
96
|
+
| `get_token_cost(input_tokens, output_tokens)` | when rendering the summary | return the USD estimate to display |
|
|
97
|
+
|
|
98
|
+
A save failure in `on_complete` is swallowed and surfaced in the pipeline log —
|
|
99
|
+
persistence never takes the UI down.
|
|
100
|
+
|
|
101
|
+
### Theming
|
|
102
|
+
|
|
103
|
+
`CrewAIPipelineTUI` ships a usable dark theme and owns it as an absolute
|
|
104
|
+
`CSS_PATH`. A subclass ships its own look by setting its own `CSS_PATH`:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
class MyTUI(CrewAIPipelineTUI):
|
|
108
|
+
CSS_PATH = "my_app.tcss"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Human review
|
|
112
|
+
|
|
113
|
+
A `Task(human_input=True)` pauses the run for feedback. crewui opens the input
|
|
114
|
+
box, parks the worker thread until you submit, and feeds what you type back into
|
|
115
|
+
crewai's feedback loop (empty input — just Enter — means "accept"). The routing
|
|
116
|
+
leans on `crewai.core.providers.human_input`, a semi-internal API isolated to a
|
|
117
|
+
single factory in `crewui/app.py` so there is one place to fix if it moves
|
|
118
|
+
between crewai versions.
|
|
119
|
+
|
|
120
|
+
## The crewai version pin, and the fork
|
|
121
|
+
|
|
122
|
+
crewui declares `crewai>=1.15.6,<1.16`.
|
|
123
|
+
|
|
124
|
+
- **The floor (1.15.6)** is the first release where the agent executor's
|
|
125
|
+
`ask_for_human_input` compatibility property is back. A 1.14.5 executor
|
|
126
|
+
regression crashed `human_input=True` with
|
|
127
|
+
`'AgentExecutor' object has no attribute 'ask_for_human_input'`; below the
|
|
128
|
+
floor, the human-review feature does not work.
|
|
129
|
+
- **The ceiling (<1.16)** is deliberate, not lazy. The human-input routing is
|
|
130
|
+
the one place crewui reaches past crewai's public surface, so a minor bump is
|
|
131
|
+
a review event: re-check that seam, then move the ceiling.
|
|
132
|
+
|
|
133
|
+
There is a **second, separate** human-input bug that a stock PyPI crewai still
|
|
134
|
+
carries: the feedback prompt tells the operator to review "the Final Result
|
|
135
|
+
above", but crewai only renders that result when `verbose=True` — so under a TUI
|
|
136
|
+
with verbose off, the operator is asked to review output that was never printed.
|
|
137
|
+
The downstream project this library was extracted from pins a
|
|
138
|
+
[crewai fork](https://github.com/coolhandle01/crewai) that adds a
|
|
139
|
+
Result-for-Review panel so the referenced output is always shown. **crewui does
|
|
140
|
+
not pin that fork** — it works against stock crewai, and simply streams each
|
|
141
|
+
answer into the agent-output pane before the review gate opens, which is the
|
|
142
|
+
generic equivalent of what the fork's panel does. If you need the fork's exact
|
|
143
|
+
behaviour, pin it in *your* application, not here.
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
python -m venv .venv && . .venv/bin/activate
|
|
149
|
+
pip install -e ".[dev]"
|
|
150
|
+
|
|
151
|
+
ruff check .
|
|
152
|
+
mypy
|
|
153
|
+
pylint crewui
|
|
154
|
+
pytest --cov=crewui --cov-branch --cov-report=term-missing --cov-fail-under=95
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The pure helpers in `crewui/_helpers.py` are unit-tested; the App itself is
|
|
158
|
+
driven through Textual's `pilot` harness in `tests/test_app.py` — status
|
|
159
|
+
transitions, the dry-run preview, the metrics block, the error path, and the
|
|
160
|
+
human-review gate all run against a fake offline crew.
|
|
161
|
+
|
|
162
|
+
## Licence
|
|
163
|
+
|
|
164
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
crewui/__init__.py,sha256=ibQcjcIs_X2EpBTxVje2cjv0TCS5HI9L92eQPANkU_E,1479
|
|
2
|
+
crewui/__main__.py,sha256=oxFD0ILWMa6IzhdyVv9Ny88R4ZfaF9g7p2Z3qvncHNI,341
|
|
3
|
+
crewui/_helpers.py,sha256=YaSRuyzUhIOmqIUYfw9-SdMk1_rUgsjU3tI2gcgZ8ho,3235
|
|
4
|
+
crewui/app.py,sha256=-wYmmB-8fDYFGzVmXGeb7pajvHGr29eb9MJoIrplkfs,12701
|
|
5
|
+
crewui/cli.py,sha256=xp4qtMkV_3is69uXhHffyD7tgnp0w3w7gzW3fVY_mmw,1973
|
|
6
|
+
crewui/default.tcss,sha256=-OOwuZEr3sLGXRPhAWvDB9_QIcSTog13p6WJ3nWzQzA,1356
|
|
7
|
+
crewui/demo.py,sha256=zMciXTv3fKYOU4c2MHl2qSWPkUIuBdtBlH8DgVL-Ugc,5631
|
|
8
|
+
crewui/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
crewui-0.1.0.dist-info/licenses/LICENSE,sha256=8of4gImqhbFqBBXO4BxTR1Z0l6NXyQnGg1TTKm--WmY,1078
|
|
10
|
+
crewui-0.1.0.dist-info/METADATA,sha256=oMp7i3LIvGPz7WmyMr8YPqunL691cNT8jFjVF0Pv0eo,6521
|
|
11
|
+
crewui-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
12
|
+
crewui-0.1.0.dist-info/entry_points.txt,sha256=6KvX8lnhMe08KVnT2OCXTVqpTc9N5c4JNt3jo5JBLho,43
|
|
13
|
+
crewui-0.1.0.dist-info/top_level.txt,sha256=GtxE0pBpJDt9WbY08D_1CgSRE3Ply5X3Tuwl3XkTwSY,7
|
|
14
|
+
crewui-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nathan Bottomley-Cook
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
crewui
|