pyagent-studio 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.
- pyagent_studio/__init__.py +14 -0
- pyagent_studio/app.py +84 -0
- pyagent_studio/cli.py +30 -0
- pyagent_studio/py.typed +0 -0
- pyagent_studio/screens/__init__.py +22 -0
- pyagent_studio/screens/cost.py +33 -0
- pyagent_studio/screens/dashboard.py +55 -0
- pyagent_studio/screens/editor.py +64 -0
- pyagent_studio/screens/governance.py +77 -0
- pyagent_studio/screens/graph.py +42 -0
- pyagent_studio/screens/simulation.py +64 -0
- pyagent_studio/screens/traces.py +43 -0
- pyagent_studio/services/__init__.py +13 -0
- pyagent_studio/services/blueprint_service.py +115 -0
- pyagent_studio/services/governance_service.py +105 -0
- pyagent_studio/services/simulation_service.py +109 -0
- pyagent_studio/services/trace_service.py +115 -0
- pyagent_studio/widgets/__init__.py +23 -0
- pyagent_studio/widgets/agent_card.py +40 -0
- pyagent_studio/widgets/blueprint_tree.py +73 -0
- pyagent_studio/widgets/cost_chart.py +40 -0
- pyagent_studio/widgets/diff_view.py +50 -0
- pyagent_studio/widgets/trace_table.py +29 -0
- pyagent_studio/widgets/validation_log.py +28 -0
- pyagent_studio-0.1.0.dist-info/METADATA +140 -0
- pyagent_studio-0.1.0.dist-info/RECORD +28 -0
- pyagent_studio-0.1.0.dist-info/WHEEL +4 -0
- pyagent_studio-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""PyAgent Studio — terminal-based interactive workbench for agent systems."""
|
|
2
|
+
|
|
3
|
+
from pyagent_studio.services.blueprint_service import BlueprintService
|
|
4
|
+
from pyagent_studio.services.governance_service import GovernanceService
|
|
5
|
+
from pyagent_studio.services.simulation_service import SimulationService
|
|
6
|
+
from pyagent_studio.services.trace_service import TraceService
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BlueprintService",
|
|
10
|
+
"GovernanceService",
|
|
11
|
+
"SimulationService",
|
|
12
|
+
"TraceService",
|
|
13
|
+
]
|
|
14
|
+
__version__ = "0.1.0"
|
pyagent_studio/app.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""StudioApp: main Textual application with screen routing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import App, ComposeResult
|
|
6
|
+
from textual.widgets import Footer, Header, TabbedContent, TabPane
|
|
7
|
+
|
|
8
|
+
from pyagent_blueprint.schema import BlueprintSpec
|
|
9
|
+
from pyagent_studio.services.blueprint_service import BlueprintService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StudioApp(App):
|
|
13
|
+
"""PyAgent Studio — terminal-based interactive workbench.
|
|
14
|
+
|
|
15
|
+
Launch via: ``pyagent-studio [blueprint.yaml]``
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
TITLE = "PyAgent Studio"
|
|
19
|
+
CSS = """
|
|
20
|
+
TabbedContent {
|
|
21
|
+
height: 1fr;
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
BINDINGS = [
|
|
25
|
+
("q", "quit", "Quit"),
|
|
26
|
+
("ctrl+s", "save", "Save"),
|
|
27
|
+
("ctrl+v", "validate", "Validate"),
|
|
28
|
+
("ctrl+r", "render", "Render Graph"),
|
|
29
|
+
("ctrl+t", "simulate", "Simulate"),
|
|
30
|
+
("ctrl+d", "diff", "Diff"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def __init__(self, blueprint_path: str | None = None, **kwargs) -> None:
|
|
34
|
+
super().__init__(**kwargs)
|
|
35
|
+
self._blueprint_path = blueprint_path
|
|
36
|
+
self._service = BlueprintService()
|
|
37
|
+
self._spec: BlueprintSpec | None = None
|
|
38
|
+
|
|
39
|
+
def compose(self) -> ComposeResult:
|
|
40
|
+
yield Header()
|
|
41
|
+
with TabbedContent():
|
|
42
|
+
with TabPane("Dashboard", id="tab-dashboard"):
|
|
43
|
+
from textual.widgets import Static
|
|
44
|
+
yield Static("[bold]Dashboard[/bold]\n\nLoad a blueprint to get started.")
|
|
45
|
+
with TabPane("Editor", id="tab-editor"):
|
|
46
|
+
from textual.widgets import Static
|
|
47
|
+
yield Static("Editor — use Ctrl+V to validate")
|
|
48
|
+
with TabPane("Graph", id="tab-graph"):
|
|
49
|
+
from textual.widgets import Static
|
|
50
|
+
yield Static("Graph — load a blueprint first")
|
|
51
|
+
with TabPane("Simulation", id="tab-sim"):
|
|
52
|
+
from textual.widgets import Static
|
|
53
|
+
yield Static("Simulation — load a blueprint first")
|
|
54
|
+
with TabPane("Traces", id="tab-traces"):
|
|
55
|
+
from textual.widgets import Static
|
|
56
|
+
yield Static("Traces — no trace file loaded")
|
|
57
|
+
with TabPane("Cost", id="tab-cost"):
|
|
58
|
+
from textual.widgets import Static
|
|
59
|
+
yield Static("Cost — no data available")
|
|
60
|
+
with TabPane("Governance", id="tab-gov"):
|
|
61
|
+
from textual.widgets import Static
|
|
62
|
+
yield Static("Governance — load a blueprint first")
|
|
63
|
+
yield Footer()
|
|
64
|
+
|
|
65
|
+
def on_mount(self) -> None:
|
|
66
|
+
if self._blueprint_path:
|
|
67
|
+
try:
|
|
68
|
+
self._spec = self._service.load(self._blueprint_path)
|
|
69
|
+
self.notify(f"Loaded: {self._spec.metadata.name}")
|
|
70
|
+
except Exception as exc:
|
|
71
|
+
self.notify(f"Load error: {exc}", severity="error")
|
|
72
|
+
|
|
73
|
+
def action_quit(self) -> None:
|
|
74
|
+
self.exit()
|
|
75
|
+
|
|
76
|
+
def action_validate(self) -> None:
|
|
77
|
+
if self._spec is None:
|
|
78
|
+
self.notify("No blueprint loaded", severity="warning")
|
|
79
|
+
return
|
|
80
|
+
issues = self._service.validate()
|
|
81
|
+
if issues:
|
|
82
|
+
self.notify(f"{len(issues)} validation issue(s) found", severity="warning")
|
|
83
|
+
else:
|
|
84
|
+
self.notify("Blueprint is valid ✓")
|
pyagent_studio/cli.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""CLI entry point: pyagent-studio [blueprint.yaml]."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@click.command()
|
|
9
|
+
@click.argument("blueprint", required=False, default=None, type=click.Path())
|
|
10
|
+
def main(blueprint: str | None) -> None:
|
|
11
|
+
"""Launch PyAgent Studio — interactive agent system workbench.
|
|
12
|
+
|
|
13
|
+
Optionally pass a blueprint YAML file to load on startup.
|
|
14
|
+
"""
|
|
15
|
+
try:
|
|
16
|
+
from pyagent_studio.app import StudioApp
|
|
17
|
+
except ImportError:
|
|
18
|
+
click.echo(
|
|
19
|
+
"Error: textual is not installed. "
|
|
20
|
+
"Install with: pip install pyagent-studio[tui]",
|
|
21
|
+
err=True,
|
|
22
|
+
)
|
|
23
|
+
raise SystemExit(1)
|
|
24
|
+
|
|
25
|
+
app = StudioApp(blueprint_path=blueprint)
|
|
26
|
+
app.run()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
pyagent_studio/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Studio screens — Textual Screen subclasses for each TUI view."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from pyagent_studio.screens.dashboard import DashboardScreen
|
|
5
|
+
from pyagent_studio.screens.editor import EditorScreen
|
|
6
|
+
from pyagent_studio.screens.graph import GraphScreen
|
|
7
|
+
from pyagent_studio.screens.simulation import SimulationScreen
|
|
8
|
+
from pyagent_studio.screens.traces import TracesScreen
|
|
9
|
+
from pyagent_studio.screens.cost import CostScreen
|
|
10
|
+
from pyagent_studio.screens.governance import GovernanceScreen
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CostScreen",
|
|
14
|
+
"DashboardScreen",
|
|
15
|
+
"EditorScreen",
|
|
16
|
+
"GovernanceScreen",
|
|
17
|
+
"GraphScreen",
|
|
18
|
+
"SimulationScreen",
|
|
19
|
+
"TracesScreen",
|
|
20
|
+
]
|
|
21
|
+
except ImportError:
|
|
22
|
+
__all__ = []
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""CostScreen: cost breakdown tables by pattern/agent/model."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
from textual.widgets import Footer, Header, Static
|
|
8
|
+
|
|
9
|
+
from pyagent_studio.widgets.cost_chart import CostChart
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CostScreen(Screen):
|
|
13
|
+
"""Display cost breakdown for agent system runs."""
|
|
14
|
+
|
|
15
|
+
BINDINGS = [("q", "quit", "Quit")]
|
|
16
|
+
|
|
17
|
+
def __init__(self, costs: dict[str, float] | None = None, **kwargs) -> None:
|
|
18
|
+
super().__init__(**kwargs)
|
|
19
|
+
self._costs = costs
|
|
20
|
+
|
|
21
|
+
def compose(self) -> ComposeResult:
|
|
22
|
+
yield Header()
|
|
23
|
+
yield Static("[bold]Cost Analysis[/bold]", id="title")
|
|
24
|
+
yield CostChart(id="cost-chart")
|
|
25
|
+
yield Footer()
|
|
26
|
+
|
|
27
|
+
def on_mount(self) -> None:
|
|
28
|
+
if self._costs:
|
|
29
|
+
chart = self.query_one("#cost-chart", CostChart)
|
|
30
|
+
chart.load_data(self._costs)
|
|
31
|
+
|
|
32
|
+
def action_quit(self) -> None:
|
|
33
|
+
self.app.exit()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""DashboardScreen: list blueprints, health summary, quick actions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
from textual.widgets import DataTable, Footer, Header, Static
|
|
8
|
+
|
|
9
|
+
from pyagent_studio.services.blueprint_service import BlueprintService
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DashboardScreen(Screen):
|
|
13
|
+
"""Main dashboard showing discovered blueprints and their status."""
|
|
14
|
+
|
|
15
|
+
BINDINGS = [
|
|
16
|
+
("n", "new_blueprint", "New"),
|
|
17
|
+
("enter", "open_blueprint", "Open"),
|
|
18
|
+
("q", "quit", "Quit"),
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
def __init__(self, service: BlueprintService | None = None, **kwargs) -> None:
|
|
22
|
+
super().__init__(**kwargs)
|
|
23
|
+
self._service = service or BlueprintService()
|
|
24
|
+
|
|
25
|
+
def compose(self) -> ComposeResult:
|
|
26
|
+
yield Header()
|
|
27
|
+
yield Static("[bold]PyAgent Studio — Dashboard[/bold]", id="title")
|
|
28
|
+
yield DataTable(id="blueprint-list")
|
|
29
|
+
yield Footer()
|
|
30
|
+
|
|
31
|
+
def on_mount(self) -> None:
|
|
32
|
+
table = self.query_one("#blueprint-list", DataTable)
|
|
33
|
+
table.add_columns("File", "Name", "Agents", "Workflows", "Status")
|
|
34
|
+
self._refresh_list(table)
|
|
35
|
+
|
|
36
|
+
def _refresh_list(self, table: DataTable) -> None:
|
|
37
|
+
"""Discover and list blueprints."""
|
|
38
|
+
table.clear()
|
|
39
|
+
for path in self._service.discover_blueprints():
|
|
40
|
+
try:
|
|
41
|
+
spec = self._service.load(path)
|
|
42
|
+
issues = self._service.validate()
|
|
43
|
+
status = "✓ Valid" if not issues else f"✗ {len(issues)} issue(s)"
|
|
44
|
+
table.add_row(
|
|
45
|
+
str(path.name),
|
|
46
|
+
spec.metadata.name,
|
|
47
|
+
str(len(spec.agents)),
|
|
48
|
+
str(len(spec.workflows)),
|
|
49
|
+
status,
|
|
50
|
+
)
|
|
51
|
+
except Exception:
|
|
52
|
+
table.add_row(str(path.name), "?", "?", "?", "✗ Load error")
|
|
53
|
+
|
|
54
|
+
def action_quit(self) -> None:
|
|
55
|
+
self.app.exit()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""EditorScreen: YAML editing with live validation panel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Horizontal
|
|
7
|
+
from textual.screen import Screen
|
|
8
|
+
from textual.widgets import Footer, Header, TextArea
|
|
9
|
+
|
|
10
|
+
from pyagent_studio.widgets.validation_log import ValidationLog
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EditorScreen(Screen):
|
|
14
|
+
"""YAML editor with live validation sidebar."""
|
|
15
|
+
|
|
16
|
+
BINDINGS = [
|
|
17
|
+
("ctrl+s", "save", "Save"),
|
|
18
|
+
("ctrl+v", "validate", "Validate"),
|
|
19
|
+
("q", "quit", "Quit"),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
DEFAULT_CSS = """
|
|
23
|
+
EditorScreen Horizontal {
|
|
24
|
+
height: 1fr;
|
|
25
|
+
}
|
|
26
|
+
EditorScreen TextArea {
|
|
27
|
+
width: 2fr;
|
|
28
|
+
}
|
|
29
|
+
EditorScreen ValidationLog {
|
|
30
|
+
width: 1fr;
|
|
31
|
+
border-left: solid green;
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, content: str = "", **kwargs) -> None:
|
|
36
|
+
super().__init__(**kwargs)
|
|
37
|
+
self._content = content
|
|
38
|
+
|
|
39
|
+
def compose(self) -> ComposeResult:
|
|
40
|
+
yield Header()
|
|
41
|
+
with Horizontal():
|
|
42
|
+
yield TextArea(self._content, language="yaml", id="editor")
|
|
43
|
+
yield ValidationLog(id="validation-log")
|
|
44
|
+
yield Footer()
|
|
45
|
+
|
|
46
|
+
def action_validate(self) -> None:
|
|
47
|
+
"""Validate current editor content."""
|
|
48
|
+
from pyagent_blueprint import BlueprintValidator, load_blueprint_from_str
|
|
49
|
+
from pyagent_blueprint.loader import BlueprintLoadError
|
|
50
|
+
|
|
51
|
+
editor = self.query_one("#editor", TextArea)
|
|
52
|
+
log = self.query_one("#validation-log", ValidationLog)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
spec = load_blueprint_from_str(editor.text)
|
|
56
|
+
validator = BlueprintValidator()
|
|
57
|
+
issues = validator.validate(spec)
|
|
58
|
+
log.load_issues(issues)
|
|
59
|
+
except (BlueprintLoadError, Exception) as exc:
|
|
60
|
+
log.clear()
|
|
61
|
+
log.write(f"[red]Parse error: {exc}[/red]")
|
|
62
|
+
|
|
63
|
+
def action_quit(self) -> None:
|
|
64
|
+
self.app.exit()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""GovernanceScreen: validation issues, policy compliance, diff view."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Vertical
|
|
7
|
+
from textual.screen import Screen
|
|
8
|
+
from textual.widgets import Footer, Header, Static
|
|
9
|
+
|
|
10
|
+
from pyagent_blueprint.schema import BlueprintSpec
|
|
11
|
+
from pyagent_studio.services.governance_service import GovernanceService
|
|
12
|
+
from pyagent_studio.widgets.diff_view import DiffView
|
|
13
|
+
from pyagent_studio.widgets.validation_log import ValidationLog
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GovernanceScreen(Screen):
|
|
17
|
+
"""Display validation issues, compliance score, and spec diff."""
|
|
18
|
+
|
|
19
|
+
BINDINGS = [("q", "quit", "Quit")]
|
|
20
|
+
|
|
21
|
+
DEFAULT_CSS = """
|
|
22
|
+
GovernanceScreen Vertical {
|
|
23
|
+
height: 1fr;
|
|
24
|
+
}
|
|
25
|
+
GovernanceScreen ValidationLog {
|
|
26
|
+
height: 1fr;
|
|
27
|
+
border: solid green;
|
|
28
|
+
}
|
|
29
|
+
GovernanceScreen DiffView {
|
|
30
|
+
height: 1fr;
|
|
31
|
+
border: solid blue;
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
spec: BlueprintSpec | None = None,
|
|
38
|
+
old_spec: BlueprintSpec | None = None,
|
|
39
|
+
**kwargs,
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__(**kwargs)
|
|
42
|
+
self._spec = spec
|
|
43
|
+
self._old_spec = old_spec
|
|
44
|
+
self._service = GovernanceService()
|
|
45
|
+
|
|
46
|
+
def compose(self) -> ComposeResult:
|
|
47
|
+
yield Header()
|
|
48
|
+
yield Static("[bold]Governance & Compliance[/bold]", id="title")
|
|
49
|
+
with Vertical():
|
|
50
|
+
yield Static("", id="score")
|
|
51
|
+
yield ValidationLog(id="gov-validation")
|
|
52
|
+
yield DiffView(id="gov-diff")
|
|
53
|
+
yield Footer()
|
|
54
|
+
|
|
55
|
+
def on_mount(self) -> None:
|
|
56
|
+
if self._spec:
|
|
57
|
+
self._run_compliance()
|
|
58
|
+
if self._spec and self._old_spec:
|
|
59
|
+
self._run_diff()
|
|
60
|
+
|
|
61
|
+
def _run_compliance(self) -> None:
|
|
62
|
+
report = self._service.check_compliance(self._spec)
|
|
63
|
+
score_widget = self.query_one("#score", Static)
|
|
64
|
+
score_widget.update(
|
|
65
|
+
f"[bold]Compliance Score: {report.score:.0%}[/bold] "
|
|
66
|
+
f"({report.passed}/{report.total_checks} checks passing)"
|
|
67
|
+
)
|
|
68
|
+
log = self.query_one("#gov-validation", ValidationLog)
|
|
69
|
+
log.load_issues(report.issues)
|
|
70
|
+
|
|
71
|
+
def _run_diff(self) -> None:
|
|
72
|
+
changes = self._service.diff(self._old_spec, self._spec)
|
|
73
|
+
diff_view = self.query_one("#gov-diff", DiffView)
|
|
74
|
+
diff_view.load_changes(changes)
|
|
75
|
+
|
|
76
|
+
def action_quit(self) -> None:
|
|
77
|
+
self.app.exit()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""GraphScreen: ASCII/Rich renderable DAG of workflow topology."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
from textual.widgets import Footer, Header, Static
|
|
8
|
+
|
|
9
|
+
from pyagent_blueprint.renderer import BlueprintRenderer
|
|
10
|
+
from pyagent_blueprint.schema import BlueprintSpec
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GraphScreen(Screen):
|
|
14
|
+
"""Display the workflow DAG as a Mermaid-like ASCII diagram."""
|
|
15
|
+
|
|
16
|
+
BINDINGS = [("q", "quit", "Quit")]
|
|
17
|
+
|
|
18
|
+
def __init__(self, spec: BlueprintSpec | None = None, **kwargs) -> None:
|
|
19
|
+
super().__init__(**kwargs)
|
|
20
|
+
self._spec = spec
|
|
21
|
+
|
|
22
|
+
def compose(self) -> ComposeResult:
|
|
23
|
+
yield Header()
|
|
24
|
+
yield Static("[bold]Workflow Graph[/bold]", id="title")
|
|
25
|
+
yield Static("No blueprint loaded", id="graph-content")
|
|
26
|
+
yield Footer()
|
|
27
|
+
|
|
28
|
+
def on_mount(self) -> None:
|
|
29
|
+
if self._spec is not None:
|
|
30
|
+
self.load_spec(self._spec)
|
|
31
|
+
|
|
32
|
+
def load_spec(self, spec: BlueprintSpec) -> None:
|
|
33
|
+
"""Render the blueprint graph."""
|
|
34
|
+
self._spec = spec
|
|
35
|
+
renderer = BlueprintRenderer()
|
|
36
|
+
mermaid = renderer.to_mermaid(spec)
|
|
37
|
+
|
|
38
|
+
content = self.query_one("#graph-content", Static)
|
|
39
|
+
content.update(f"```\n{mermaid}\n```")
|
|
40
|
+
|
|
41
|
+
def action_quit(self) -> None:
|
|
42
|
+
self.app.exit()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""SimulationScreen: run blueprint with MockLLM, stream results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.screen import Screen
|
|
9
|
+
from textual.widgets import Footer, Header, Input, RichLog, Static
|
|
10
|
+
|
|
11
|
+
from pyagent_blueprint.schema import BlueprintSpec
|
|
12
|
+
from pyagent_studio.services.simulation_service import SimulationService
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimulationScreen(Screen):
|
|
16
|
+
"""Run simulations against a compiled blueprint and display results."""
|
|
17
|
+
|
|
18
|
+
BINDINGS = [
|
|
19
|
+
("ctrl+t", "run_simulation", "Run"),
|
|
20
|
+
("q", "quit", "Quit"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
DEFAULT_CSS = """
|
|
24
|
+
SimulationScreen RichLog {
|
|
25
|
+
height: 1fr;
|
|
26
|
+
border: solid green;
|
|
27
|
+
}
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, spec: BlueprintSpec | None = None, **kwargs) -> None:
|
|
31
|
+
super().__init__(**kwargs)
|
|
32
|
+
self._spec = spec
|
|
33
|
+
self._service = SimulationService()
|
|
34
|
+
|
|
35
|
+
def compose(self) -> ComposeResult:
|
|
36
|
+
yield Header()
|
|
37
|
+
yield Static("[bold]Simulation[/bold]", id="title")
|
|
38
|
+
yield Input(placeholder="Enter task to simulate...", id="task-input")
|
|
39
|
+
yield RichLog(id="sim-output")
|
|
40
|
+
yield Footer()
|
|
41
|
+
|
|
42
|
+
async def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
43
|
+
"""Run simulation when input is submitted."""
|
|
44
|
+
if self._spec is None:
|
|
45
|
+
log = self.query_one("#sim-output", RichLog)
|
|
46
|
+
log.write("[red]No blueprint loaded[/red]")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
await self._run(event.value)
|
|
50
|
+
|
|
51
|
+
async def _run(self, task: str) -> None:
|
|
52
|
+
log = self.query_one("#sim-output", RichLog)
|
|
53
|
+
log.write(f"\n[bold]Running:[/bold] {task}")
|
|
54
|
+
|
|
55
|
+
for wf_name in self._spec.workflows:
|
|
56
|
+
result = await self._service.run(self._spec, wf_name, task)
|
|
57
|
+
if result.success:
|
|
58
|
+
log.write(f"[green]✓ {wf_name}[/green] ({result.elapsed_ms:.0f}ms)")
|
|
59
|
+
log.write(f" Output: {result.output[:200]}")
|
|
60
|
+
else:
|
|
61
|
+
log.write(f"[red]✗ {wf_name}[/red]: {result.error}")
|
|
62
|
+
|
|
63
|
+
def action_quit(self) -> None:
|
|
64
|
+
self.app.exit()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""TracesScreen: browse recorded traces, inspect spans."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
from textual.widgets import Footer, Header, Static
|
|
8
|
+
|
|
9
|
+
from pyagent_studio.services.trace_service import TraceService
|
|
10
|
+
from pyagent_studio.widgets.trace_table import TraceTable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TracesScreen(Screen):
|
|
14
|
+
"""Browse recorded trace spans from pyagent-trace Recorder."""
|
|
15
|
+
|
|
16
|
+
BINDINGS = [("q", "quit", "Quit")]
|
|
17
|
+
|
|
18
|
+
def __init__(self, trace_path: str | None = None, **kwargs) -> None:
|
|
19
|
+
super().__init__(**kwargs)
|
|
20
|
+
self._trace_path = trace_path
|
|
21
|
+
self._service = TraceService()
|
|
22
|
+
|
|
23
|
+
def compose(self) -> ComposeResult:
|
|
24
|
+
yield Header()
|
|
25
|
+
yield Static("[bold]Traces[/bold]", id="title")
|
|
26
|
+
yield TraceTable(id="trace-table")
|
|
27
|
+
yield Footer()
|
|
28
|
+
|
|
29
|
+
def on_mount(self) -> None:
|
|
30
|
+
if self._trace_path:
|
|
31
|
+
self.load_traces(self._trace_path)
|
|
32
|
+
|
|
33
|
+
def load_traces(self, path: str) -> None:
|
|
34
|
+
"""Load and display traces from a JSONL file."""
|
|
35
|
+
try:
|
|
36
|
+
spans = self._service.load(path)
|
|
37
|
+
table = self.query_one("#trace-table", TraceTable)
|
|
38
|
+
table.load_spans(spans)
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def action_quit(self) -> None:
|
|
43
|
+
self.app.exit()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Studio services — headless logic for blueprint, simulation, traces, governance."""
|
|
2
|
+
|
|
3
|
+
from pyagent_studio.services.blueprint_service import BlueprintService
|
|
4
|
+
from pyagent_studio.services.governance_service import GovernanceService
|
|
5
|
+
from pyagent_studio.services.simulation_service import SimulationService
|
|
6
|
+
from pyagent_studio.services.trace_service import TraceService
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BlueprintService",
|
|
10
|
+
"GovernanceService",
|
|
11
|
+
"SimulationService",
|
|
12
|
+
"TraceService",
|
|
13
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""BlueprintService: load, validate, compile blueprints for the studio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pyagent_blueprint import (
|
|
9
|
+
BlueprintCompiler,
|
|
10
|
+
BlueprintValidator,
|
|
11
|
+
RuntimeGraph,
|
|
12
|
+
load_blueprint,
|
|
13
|
+
)
|
|
14
|
+
from pyagent_blueprint.loader import BlueprintLoadError
|
|
15
|
+
from pyagent_blueprint.schema import BlueprintSpec
|
|
16
|
+
from pyagent_blueprint.validator import ValidationIssue
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BlueprintService:
|
|
20
|
+
"""Headless service for loading, validating, and compiling blueprints.
|
|
21
|
+
|
|
22
|
+
Wraps ``pyagent-blueprint`` for use by TUI screens and tests.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._compiler = BlueprintCompiler()
|
|
27
|
+
self._validator = BlueprintValidator()
|
|
28
|
+
self._spec: BlueprintSpec | None = None
|
|
29
|
+
self._graph: RuntimeGraph | None = None
|
|
30
|
+
self._path: Path | None = None
|
|
31
|
+
|
|
32
|
+
def load(self, path: str | Path) -> BlueprintSpec:
|
|
33
|
+
"""Load a blueprint from file.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
path: Path to YAML/JSON blueprint.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Validated ``BlueprintSpec``.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
BlueprintLoadError: On load/validation failure.
|
|
43
|
+
"""
|
|
44
|
+
self._path = Path(path)
|
|
45
|
+
self._spec = load_blueprint(self._path)
|
|
46
|
+
self._graph = None
|
|
47
|
+
return self._spec
|
|
48
|
+
|
|
49
|
+
def validate(self) -> list[ValidationIssue]:
|
|
50
|
+
"""Run static validation on the loaded spec.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List of validation issues.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
RuntimeError: If no spec loaded.
|
|
57
|
+
"""
|
|
58
|
+
if self._spec is None:
|
|
59
|
+
raise RuntimeError("No blueprint loaded. Call load() first.")
|
|
60
|
+
return self._validator.validate(self._spec)
|
|
61
|
+
|
|
62
|
+
def compile(self) -> RuntimeGraph:
|
|
63
|
+
"""Compile the loaded spec into a RuntimeGraph.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Executable ``RuntimeGraph``.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
RuntimeError: If no spec loaded.
|
|
70
|
+
"""
|
|
71
|
+
if self._spec is None:
|
|
72
|
+
raise RuntimeError("No blueprint loaded. Call load() first.")
|
|
73
|
+
self._graph = self._compiler.compile(self._spec)
|
|
74
|
+
return self._graph
|
|
75
|
+
|
|
76
|
+
def discover_blueprints(self, directory: str | Path = ".") -> list[Path]:
|
|
77
|
+
"""Find all YAML/JSON blueprint files in a directory.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
directory: Root directory to search.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
List of file paths.
|
|
84
|
+
"""
|
|
85
|
+
root = Path(directory)
|
|
86
|
+
files: list[Path] = []
|
|
87
|
+
for ext in ("*.yaml", "*.yml", "*.json"):
|
|
88
|
+
files.extend(root.glob(f"**/{ext}"))
|
|
89
|
+
return sorted(files)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def spec(self) -> BlueprintSpec | None:
|
|
93
|
+
return self._spec
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def graph(self) -> RuntimeGraph | None:
|
|
97
|
+
return self._graph
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def path(self) -> Path | None:
|
|
101
|
+
return self._path
|
|
102
|
+
|
|
103
|
+
def summary(self) -> dict[str, Any]:
|
|
104
|
+
"""Quick summary of loaded blueprint."""
|
|
105
|
+
if self._spec is None:
|
|
106
|
+
return {"loaded": False}
|
|
107
|
+
return {
|
|
108
|
+
"loaded": True,
|
|
109
|
+
"name": self._spec.metadata.name,
|
|
110
|
+
"version": self._spec.metadata.version,
|
|
111
|
+
"agents": len(self._spec.agents),
|
|
112
|
+
"workflows": len(self._spec.workflows),
|
|
113
|
+
"providers": len(self._spec.providers),
|
|
114
|
+
"contracts": len(self._spec.contracts),
|
|
115
|
+
}
|