Skip to content

Commit d14e08a

Browse files
committed
wip
1 parent 6a4b0fd commit d14e08a

File tree

7 files changed

+188
-120
lines changed

7 files changed

+188
-120
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
*.whl
12
task-*.txt
23
test-output/
34

.mdl/runtime.sh

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#!/usr/bin/env bash
2+
# Mudyla Runtime - Sourced by all generated scripts
3+
# This provides the ret() and dep() pseudo-commands
4+
5+
# dep pseudo-command (no-op, used for dependency declaration)
6+
dep() {
7+
# Dependencies are extracted at parse time, this is a no-op at runtime
8+
:
9+
}
10+
11+
# ret pseudo-command (captures return values)
12+
ret() {
13+
local declaration="$1"
14+
local name="${declaration%%:*}"
15+
local rest="${declaration#*:}"
16+
local type="${rest%%=*}"
17+
local value="${rest#*=}"
18+
19+
# Store as JSON line
20+
MDL_OUTPUT_LINES+=("$(printf '%s' "$name:$type:$value")")
21+
}
22+
23+
# Trap to write JSON on exit
24+
trap 'mudyla_write_outputs' EXIT
25+
26+
mudyla_write_outputs() {
27+
echo "{" > "$MDL_OUTPUT_JSON"
28+
local first=true
29+
for line in "${MDL_OUTPUT_LINES[@]}"; do
30+
local name="${line%%:*}"
31+
local rest="${line#*:}"
32+
local type="${rest%%:*}"
33+
local value="${rest#*:}"
34+
35+
if [ "$first" = true ]; then
36+
first=false
37+
else
38+
echo "," >> "$MDL_OUTPUT_JSON"
39+
fi
40+
41+
# Escape value for JSON
42+
local json_value=$(printf '%s' "$value" | python3 -c 'import sys, json; print(json.dumps(sys.stdin.read().strip()))')
43+
printf ' "%s": {"type": "%s", "value": %s}' "$name" "$type" "$json_value" >> "$MDL_OUTPUT_JSON"
44+
done
45+
echo "" >> "$MDL_OUTPUT_JSON"
46+
echo "}" >> "$MDL_OUTPUT_JSON"
47+
}
48+
49+
# Initialize output tracking
50+
MDL_OUTPUT_LINES=()
51+
52+
# Fail on errors
53+
set -euo pipefail

flake.nix

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,31 @@
1111
let
1212
pkgs = nixpkgs.legacyPackages.${system};
1313

14-
python = pkgs.python311;
14+
python = pkgs.python312;
1515

1616
# Build package using Python build tools
17-
mudyla = pkgs.python311Packages.buildPythonApplication {
17+
mudyla = pkgs.python312Packages.buildPythonApplication {
1818
pname = "mudyla";
1919
version = "0.1.0";
2020
src = ./.;
2121
format = "pyproject";
2222

23-
nativeBuildInputs = with pkgs.python311Packages; [
23+
nativeBuildInputs = with pkgs.python312Packages; [
2424
hatchling
2525
];
2626

27-
propagatedBuildInputs = with pkgs.python311Packages; [
27+
propagatedBuildInputs = with pkgs.python312Packages; [
2828
mistune
2929
pyparsing
3030
rich
31+
networkx
32+
phart
3133
];
3234

35+
postInstall = ''
36+
cp $src/mudyla/runtime.sh $out/lib/python3.12/site-packages/mudyla/
37+
'';
38+
3339
meta = {
3440
description = "Multimodal Dynamic Launcher - Shell script orchestrator";
3541
homepage = "https://github.com/yourusername/mudyla";

mudyla/cli.py

Lines changed: 29 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@
1717
from .utils.colors import ColorFormatter
1818

1919
try:
20-
from asciidag.graph import Graph
21-
from asciidag.node import Node
22-
ASCIIDAG_AVAILABLE = True
20+
import networkx as nx
21+
from phart import ASCIIRenderer
22+
PHART_AVAILABLE = True
2323
except ImportError:
24-
ASCIIDAG_AVAILABLE = False
24+
PHART_AVAILABLE = False
2525

2626

2727
class CLI:
@@ -208,14 +208,11 @@ def run(self, argv: Optional[list[str]] = None) -> int:
208208
print(f"{'❌' if not args.no_color else '✗'} {color.error('Error:')} No markdown files found matching pattern: {args.defs}")
209209
return 1
210210

211-
print(f"{'📁' if not args.no_color else '▸'} {color.dim('Found')} {color.bold(str(len(md_files)))} {color.dim('definition file(s)')}")
212-
213211
# Parse markdown files
214-
print(f"{'📝' if not args.no_color else '▸'} {color.dim('Parsing definitions...')}")
215212
parser = MarkdownParser()
216213
document = parser.parse_files(md_files)
217214

218-
print(f"{'' if not args.no_color else ''} {color.dim('Loaded')} {color.bold(str(len(document.actions)))} {color.dim('action(s)')}")
215+
print(f"{'📚' if not args.no_color else ''} {color.dim('Found')} {color.bold(str(len(md_files)))} {color.dim('definition file(s) with')} {color.bold(str(len(document.actions)))} {color.dim('actions')}")
219216

220217
# Handle --list-actions
221218
if args.list_actions:
@@ -252,33 +249,30 @@ def run(self, argv: Optional[list[str]] = None) -> int:
252249
custom_args[arg_name] = arg_def.default_value
253250

254251
# Build DAG
255-
print(f"\n{'🔨' if not args.no_color else '▸'} {color.dim('Building dependency graph...')}")
256252
builder = DAGBuilder(document)
257253
builder.validate_goals(goals)
258254
graph = builder.build_graph(goals, axis_values)
259255

260256
# Prune to goals
261257
pruned_graph = graph.prune_to_goals()
262-
print(f"{'📊' if not args.no_color else '▸'} {color.dim('Graph contains')} {color.bold(str(len(pruned_graph.nodes)))} {color.dim('required action(s)')}")
263258

264259
# Validate
265-
print(f"{'🔍' if not args.no_color else '▸'} {color.dim('Validating...')}")
266260
validator = DAGValidator(document, pruned_graph)
267261

268262
# Initialize flags with all defined flags
269263
all_flags = {name: False for name in document.flags}
270264
all_flags.update(custom_flags)
271265

272266
validator.validate_all(custom_args, all_flags, axis_values)
273-
print(f"{'✅' if not args.no_color else '✓'} {color.success('Validation passed')}")
267+
print(f"{'✅' if not args.no_color else '✓'} {color.dim('Built plan graph with')} {color.bold(str(len(pruned_graph.nodes)))} {color.dim('required action(s)')}")
274268

275269
# Show execution plan
276270
execution_order = pruned_graph.get_execution_order()
277271
print(f"\n{'📋' if not args.no_color else '▸'} {color.bold('Execution plan:')}")
278272

279-
if ASCIIDAG_AVAILABLE and not args.no_color:
280-
# Use asciidag to visualize the DAG
281-
self._visualize_execution_plan_dag(pruned_graph, execution_order, goals, color)
273+
if PHART_AVAILABLE and not args.no_color:
274+
# Use phart to visualize the DAG
275+
self._visualize_execution_plan_phart(pruned_graph, execution_order, goals, color)
282276
else:
283277
# Fallback to simple text output
284278
for i, action_name in enumerate(execution_order, 1):
@@ -365,59 +359,42 @@ def run(self, argv: Optional[list[str]] = None) -> int:
365359
traceback.print_exc()
366360
return 1
367361

368-
def _visualize_execution_plan_dag(self, graph, execution_order: list[str], goals: list[str], color) -> None:
369-
"""Visualize execution plan using asciidag.
362+
def _visualize_execution_plan_phart(self, graph, execution_order: list[str], goals: list[str], color) -> None:
363+
"""Visualize execution plan using phart.
370364
371365
Args:
372366
graph: The execution graph
373367
execution_order: List of actions in execution order
374368
goals: List of goal actions
375369
color: Color formatter
376370
"""
377-
from asciidag.graph import Graph as AsciiGraph
378-
from asciidag.node import Node as AsciiNode
371+
import networkx as nx
372+
from phart import ASCIIRenderer
379373

380-
# Create a mapping of action names to asciidag nodes
381-
node_map = {}
374+
# Create a NetworkX DiGraph
375+
# Use labels as node IDs so they show up in the visualization
376+
G = nx.DiGraph()
382377

383-
# Build asciidag nodes in execution order
378+
# Create a mapping from action names to labels
379+
label_map = {}
384380
for action_name in execution_order:
385-
exec_node = graph.get_node(action_name)
386-
387-
# Get parent nodes (dependencies)
388-
parent_nodes = []
389-
for dep in sorted(exec_node.dependencies):
390-
if dep in node_map:
391-
parent_nodes.append(node_map[dep])
392-
393-
# Create label with execution order number and goal marker
394381
order_num = execution_order.index(action_name) + 1
395382
is_goal = action_name in goals
396383
goal_marker = " 🎯" if is_goal else ""
397384
label = f"{order_num}. {action_name}{goal_marker}"
385+
label_map[action_name] = label
386+
G.add_node(label)
398387

399-
# Create asciidag node
400-
ascii_node = AsciiNode(label, parents=parent_nodes)
401-
node_map[action_name] = ascii_node
402-
403-
# Get goal nodes (tips of the DAG to display)
404-
goal_nodes = [node_map[goal] for goal in goals if goal in node_map]
405-
406-
# If no specific goals or all actions shown, use actions with no dependents
407-
if not goal_nodes:
408-
# Find actions with no dependents (leaf nodes)
409-
has_dependents = set()
410-
for action_name in execution_order:
411-
exec_node = graph.get_node(action_name)
412-
for dep in exec_node.dependencies:
413-
has_dependents.add(dep)
414-
415-
goal_nodes = [node_map[action] for action in execution_order
416-
if action not in has_dependents and action in node_map]
388+
# Add edges using labels
389+
for action_name in execution_order:
390+
exec_node = graph.get_node(action_name)
391+
for dep in exec_node.dependencies:
392+
# In a dependency graph, edges go from dependency to dependent
393+
G.add_edge(label_map[dep], label_map[action_name])
417394

418-
# Render the DAG
419-
ascii_graph = AsciiGraph()
420-
ascii_graph.show_nodes(goal_nodes)
395+
# Render the graph with better spacing
396+
renderer = ASCIIRenderer(G, node_spacing=1, layer_spacing=1)
397+
print(renderer.render())
421398

422399
def _list_actions(self, document: ParsedDocument) -> None:
423400
"""List all available actions."""

mudyla/executor/engine.py

Lines changed: 33 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from datetime import datetime
1111
from pathlib import Path
1212
from typing import Any, Optional
13+
import importlib.resources
1314

1415
from ..ast.types import ReturnType
1516
from ..dag.graph import ActionGraph
@@ -60,59 +61,6 @@ def get_goal_outputs(self, goals: list[str]) -> dict[str, dict[str, Any]]:
6061
class ExecutionEngine:
6162
"""Engine for executing actions in a DAG."""
6263

63-
RET_FUNCTION_TEMPLATE = """
64-
# Mudyla pseudo-commands
65-
MDL_OUTPUT_JSON="{output_json}"
66-
MDL_OUTPUT_LINES=()
67-
68-
# dep pseudo-command (no-op, used for dependency declaration)
69-
dep() {{
70-
# Dependencies are extracted at parse time, this is a no-op at runtime
71-
:
72-
}}
73-
74-
# ret pseudo-command (captures return values)
75-
ret() {{
76-
local declaration="$1"
77-
local name="${{declaration%%:*}}"
78-
local rest="${{declaration#*:}}"
79-
local type="${{rest%%=*}}"
80-
local value="${{rest#*=}}"
81-
82-
# Store as JSON line
83-
MDL_OUTPUT_LINES+=("$(printf '%s' "$name:$type:$value")")
84-
}}
85-
86-
# Trap to write JSON on exit
87-
trap 'mudyla_write_outputs' EXIT
88-
89-
mudyla_write_outputs() {{
90-
echo "{{" > "$MDL_OUTPUT_JSON"
91-
local first=true
92-
for line in "${{MDL_OUTPUT_LINES[@]}}"; do
93-
local name="${{line%%:*}}"
94-
local rest="${{line#*:}}"
95-
local type="${{rest%%:*}}"
96-
local value="${{rest#*:}}"
97-
98-
if [ "$first" = true ]; then
99-
first=false
100-
else
101-
echo "," >> "$MDL_OUTPUT_JSON"
102-
fi
103-
104-
# Escape value for JSON
105-
local json_value=$(printf '%s' "$value" | python3 -c 'import sys, json; print(json.dumps(sys.stdin.read().strip()))')
106-
printf ' "%s": {{"type": "%s", "value": %s}}' "$name" "$type" "$json_value" >> "$MDL_OUTPUT_JSON"
107-
done
108-
echo "" >> "$MDL_OUTPUT_JSON"
109-
echo "}}" >> "$MDL_OUTPUT_JSON"
110-
}}
111-
112-
set -euo pipefail
113-
114-
"""
115-
11664
def __init__(
11765
self,
11866
graph: ActionGraph,
@@ -160,6 +108,29 @@ def __init__(
160108

161109
self.run_directory.mkdir(parents=True, exist_ok=True)
162110

111+
# Copy runtime.sh to .mdl directory
112+
self._install_runtime()
113+
114+
def _install_runtime(self) -> None:
115+
"""Install runtime.sh to .mdl directory."""
116+
mdl_dir = self.project_root / ".mdl"
117+
runtime_dest = mdl_dir / "runtime.sh"
118+
119+
# Get runtime.sh from package resources
120+
try:
121+
# Python 3.12+ uses importlib.resources.files
122+
import importlib.resources as resources
123+
runtime_content = resources.files('mudyla').joinpath('runtime.sh').read_text()
124+
except (AttributeError, FileNotFoundError):
125+
# Fallback for older Python or development mode
126+
import mudyla
127+
runtime_path = Path(mudyla.__file__).parent / 'runtime.sh'
128+
runtime_content = runtime_path.read_text()
129+
130+
# Write runtime to .mdl/runtime.sh
131+
runtime_dest.write_text(runtime_content)
132+
runtime_dest.chmod(0o755)
133+
163134
def execute_all(self) -> ExecutionResult:
164135
"""Execute all actions in the graph.
165136
@@ -363,11 +334,15 @@ def _execute_action(
363334
version.bash_script, action_name, action_outputs
364335
)
365336

366-
# Add ret function
337+
# Source runtime and set output path
367338
output_json_path = action_dir / "output.json"
368-
ret_function = self.RET_FUNCTION_TEMPLATE.format(
369-
output_json=str(output_json_path)
370-
)
339+
runtime_path = self.project_root / ".mdl" / "runtime.sh"
340+
runtime_header = f"""#!/usr/bin/env bash
341+
# Source Mudyla runtime
342+
export MDL_OUTPUT_JSON="{output_json_path}"
343+
source "{runtime_path}"
344+
345+
"""
371346

372347
# Add environment variable exports
373348
env_exports = ""
@@ -379,7 +354,7 @@ def _execute_action(
379354
env_exports += f'export {var_name}="{escaped_value}"\n'
380355
env_exports += "\n"
381356

382-
full_script = ret_function + env_exports + rendered_script
357+
full_script = runtime_header + env_exports + rendered_script
383358

384359
# Save script
385360
script_path = action_dir / "script.sh"

0 commit comments

Comments
 (0)