Skip to content

Commit d238e67

Browse files
leshypaul-nechifor
andauthored
Docs Clean (#933)
* modules docs sketch * extracted introspection tooling * first sketch of module docs * including go2 basic blueprint svg * new dot2 generator * adding different layouts * removing other blueprint rendering algos * better dot2 algo * modules tutorial work * more docs work * small modules docs changes * go2 agentic svg * mutliprocessing sketch * small wording changes * config docs, moved old docs, dot file included * doclinks tool * config docs moved to generic place * doclinks updates, transform docs sketch * transforms docs * docs agent docs, reactivex docs * reactivex docs * small changes on transform images * folded code blocks * moved modules doc * transforms work * transform docs * transforms modules image * Update docs/concepts/modules.md Co-authored-by: Paul Nechifor <paul@nechifor.net> * Update docs/concepts/modules.md Co-authored-by: Paul Nechifor <paul@nechifor.net> * Update docs/concepts/modules.md Co-authored-by: Paul Nechifor <paul@nechifor.net> * Update docs/concepts/modules.md Co-authored-by: Paul Nechifor <paul@nechifor.net> * Update docs/concepts/modules.md Co-authored-by: Paul Nechifor <paul@nechifor.net> * doclinks ignore * doclinks in pre-commit hook * camera module fixes * vis cleanup * doclinks always runs * introspection tooling cleanup * kwargs typing * paul comment * lcm and transports docs * camera module fixes * small cleanup, vibed sensor.py deleted * sharpness barrier fix * moved gstreamer into separate dir, removed fake zed * removed all typing ignores * mypy fix * Out stream is observable * better video stream skill * stream changes undo, typing fixes * reverted pyproject for fast tests in CI for now * CI code cleanup * type fixes * ignore gps skill publish * transform test fix * core test fix * watchdog was missing --------- Co-authored-by: Paul Nechifor <paul@nechifor.net> Former-commit-id: d9ee59a
1 parent 5569418 commit d238e67

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+5499
-82
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
*.mov filter=lfs diff=lfs merge=lfs -text binary
1616
*.gif filter=lfs diff=lfs merge=lfs -text binary
1717
*.foxe filter=lfs diff=lfs merge=lfs -text binary
18+
docs/**/*.png filter=lfs diff=lfs merge=lfs -text

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,11 @@ repos:
7676
pass_filenames: false
7777
entry: bin/lfs_check
7878
language: script
79+
80+
- id: doclinks
81+
name: Doclinks
82+
always_run: true
83+
pass_filenames: false
84+
entry: python -m dimos.utils.docs.doclinks docs/
85+
language: system
86+
files: ^docs/.*\.md$
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Copyright 2025 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Module and blueprint introspection utilities."""
16+
17+
from dimos.core.introspection.module import INTERNAL_RPCS, render_module_io
18+
from dimos.core.introspection.svg import to_svg
19+
20+
__all__ = ["INTERNAL_RPCS", "render_module_io", "to_svg"]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Copyright 2025 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Blueprint introspection and rendering.
16+
17+
Renderers:
18+
- dot: Graphviz DOT format (hub-style with type nodes as intermediate hubs)
19+
"""
20+
21+
from dimos.core.introspection.blueprint import dot
22+
from dimos.core.introspection.blueprint.dot import LayoutAlgo, render_svg
23+
24+
__all__ = ["LayoutAlgo", "dot", "render_svg"]
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# Copyright 2025-2026 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Hub-style Graphviz DOT renderer for blueprint visualization.
16+
17+
This renderer creates intermediate "type nodes" for data flow, making it clearer
18+
when one output fans out to multiple consumers:
19+
20+
ModuleA --> [name:Type] --> ModuleB
21+
--> ModuleC
22+
"""
23+
24+
from collections import defaultdict
25+
from enum import Enum, auto
26+
27+
from dimos.core.blueprints import ModuleBlueprintSet
28+
from dimos.core.introspection.utils import (
29+
GROUP_COLORS,
30+
TYPE_COLORS,
31+
color_for_string,
32+
sanitize_id,
33+
)
34+
from dimos.core.module import Module
35+
from dimos.utils.cli import theme
36+
37+
38+
class LayoutAlgo(Enum):
39+
"""Layout algorithms for controlling graph structure."""
40+
41+
STACK_CLUSTERS = auto() # Stack clusters vertically (invisible edges between clusters)
42+
STACK_NODES = auto() # Stack nodes within clusters vertically
43+
FDP = auto() # Use fdp (force-directed) layout engine instead of dot
44+
45+
46+
# Connections to ignore (too noisy/common)
47+
DEFAULT_IGNORED_CONNECTIONS = {("odom", "PoseStamped")}
48+
49+
DEFAULT_IGNORED_MODULES = {
50+
"WebsocketVisModule",
51+
"UtilizationModule",
52+
# "FoxgloveBridge",
53+
}
54+
55+
56+
def render(
57+
blueprint_set: ModuleBlueprintSet,
58+
*,
59+
layout: set[LayoutAlgo] | None = None,
60+
ignored_connections: set[tuple[str, str]] | None = None,
61+
ignored_modules: set[str] | None = None,
62+
) -> str:
63+
"""Generate a hub-style DOT graph from a ModuleBlueprintSet.
64+
65+
This creates intermediate "type nodes" that represent data channels,
66+
connecting producers to consumers through a central hub node.
67+
68+
Args:
69+
blueprint_set: The blueprint set to visualize.
70+
layout: Set of layout algorithms to apply. Default is none (let graphviz decide).
71+
ignored_connections: Set of (name, type_name) tuples to ignore.
72+
ignored_modules: Set of module names to ignore.
73+
74+
Returns:
75+
A string in DOT format showing modules as nodes, type nodes as
76+
small colored hubs, and edges connecting them.
77+
"""
78+
if layout is None:
79+
layout = set()
80+
if ignored_connections is None:
81+
ignored_connections = DEFAULT_IGNORED_CONNECTIONS
82+
if ignored_modules is None:
83+
ignored_modules = DEFAULT_IGNORED_MODULES
84+
85+
# Collect all outputs: (name, type) -> list of producer modules
86+
producers: dict[tuple[str, type], list[type[Module]]] = defaultdict(list)
87+
# Collect all inputs: (name, type) -> list of consumer modules
88+
consumers: dict[tuple[str, type], list[type[Module]]] = defaultdict(list)
89+
# Module name -> module class (for getting package info)
90+
module_classes: dict[str, type[Module]] = {}
91+
92+
for bp in blueprint_set.blueprints:
93+
module_classes[bp.module.__name__] = bp.module
94+
for conn in bp.connections:
95+
# Apply remapping
96+
remapped_name = blueprint_set.remapping_map.get((bp.module, conn.name), conn.name)
97+
key = (remapped_name, conn.type)
98+
if conn.direction == "out":
99+
producers[key].append(bp.module)
100+
else:
101+
consumers[key].append(bp.module)
102+
103+
# Find all active channels (have both producers AND consumers)
104+
active_channels: dict[tuple[str, type], str] = {} # key -> color
105+
for key in producers:
106+
name, type_ = key
107+
type_name = type_.__name__
108+
if key not in consumers:
109+
continue
110+
if (name, type_name) in ignored_connections:
111+
continue
112+
# Check if all modules are ignored
113+
valid_producers = [m for m in producers[key] if m.__name__ not in ignored_modules]
114+
valid_consumers = [m for m in consumers[key] if m.__name__ not in ignored_modules]
115+
if not valid_producers or not valid_consumers:
116+
continue
117+
label = f"{name}:{type_name}"
118+
active_channels[key] = color_for_string(TYPE_COLORS, label)
119+
120+
# Group modules by package
121+
def get_group(mod_class: type[Module]) -> str:
122+
module_path = mod_class.__module__
123+
parts = module_path.split(".")
124+
if len(parts) >= 2 and parts[0] == "dimos":
125+
return parts[1]
126+
return "other"
127+
128+
by_group: dict[str, list[str]] = defaultdict(list)
129+
for mod_name, mod_class in module_classes.items():
130+
if mod_name in ignored_modules:
131+
continue
132+
group = get_group(mod_class)
133+
by_group[group].append(mod_name)
134+
135+
# Build DOT output
136+
lines = [
137+
"digraph modules {",
138+
" bgcolor=transparent;",
139+
" rankdir=LR;",
140+
# " nodesep=1;", # horizontal spacing between nodes
141+
# " ranksep=1.5;", # vertical spacing between ranks
142+
" splines=true;",
143+
f' node [shape=box, style=filled, fillcolor="{theme.BACKGROUND}", fontcolor="{theme.FOREGROUND}", color="{theme.BLUE}", fontname=fixed, fontsize=12, margin="0.1,0.1"];',
144+
" edge [fontname=fixed, fontsize=10];",
145+
"",
146+
]
147+
148+
# Add subgraphs for each module group
149+
sorted_groups = sorted(by_group.keys())
150+
for group in sorted_groups:
151+
mods = sorted(by_group[group])
152+
color = color_for_string(GROUP_COLORS, group)
153+
lines.append(f" subgraph cluster_{group} {{")
154+
lines.append(f' label="{group}";')
155+
lines.append(" labeljust=r;")
156+
lines.append(" fontname=fixed;")
157+
lines.append(" fontsize=14;")
158+
lines.append(f' fontcolor="{theme.FOREGROUND}";')
159+
lines.append(' style="filled,dashed";')
160+
lines.append(f' color="{color}";')
161+
lines.append(" penwidth=1;")
162+
lines.append(f' fillcolor="{color}10";')
163+
for mod in mods:
164+
lines.append(f" {mod};")
165+
# Stack nodes vertically within cluster
166+
if LayoutAlgo.STACK_NODES in layout and len(mods) > 1:
167+
for i in range(len(mods) - 1):
168+
lines.append(f" {mods[i]} -> {mods[i + 1]} [style=invis];")
169+
lines.append(" }")
170+
lines.append("")
171+
172+
# Add invisible edges between clusters to force vertical stacking
173+
if LayoutAlgo.STACK_CLUSTERS in layout and len(sorted_groups) > 1:
174+
lines.append(" // Force vertical cluster layout")
175+
for i in range(len(sorted_groups) - 1):
176+
group_a = sorted_groups[i]
177+
group_b = sorted_groups[i + 1]
178+
# Pick first node from each cluster
179+
node_a = sorted(by_group[group_a])[0]
180+
node_b = sorted(by_group[group_b])[0]
181+
lines.append(f" {node_a} -> {node_b} [style=invis, weight=10];")
182+
lines.append("")
183+
184+
# Add type nodes (outside all clusters)
185+
lines.append(" // Type nodes (data channels)")
186+
for key, color in sorted(
187+
active_channels.items(), key=lambda x: f"{x[0][0]}:{x[0][1].__name__}"
188+
):
189+
name, type_ = key
190+
type_name = type_.__name__
191+
node_id = sanitize_id(f"chan_{name}_{type_name}")
192+
label = f"{name}:{type_name}"
193+
lines.append(
194+
f' {node_id} [label="{label}", shape=note, style=filled, '
195+
f'fillcolor="{color}35", color="{color}", fontcolor="{theme.FOREGROUND}", '
196+
f'width=0, height=0, margin="0.1,0.05", fontsize=10];'
197+
)
198+
199+
lines.append("")
200+
201+
# Add edges: producer -> type_node -> consumer
202+
lines.append(" // Edges")
203+
for key, color in sorted(
204+
active_channels.items(), key=lambda x: f"{x[0][0]}:{x[0][1].__name__}"
205+
):
206+
name, type_ = key
207+
type_name = type_.__name__
208+
node_id = sanitize_id(f"chan_{name}_{type_name}")
209+
210+
# Edges from producers to type node (no arrow, kept close)
211+
for producer in producers[key]:
212+
if producer.__name__ in ignored_modules:
213+
continue
214+
lines.append(f' {producer.__name__} -> {node_id} [color="{color}", arrowhead=none];')
215+
216+
# Edges from type node to consumers (with arrow)
217+
for consumer in consumers[key]:
218+
if consumer.__name__ in ignored_modules:
219+
continue
220+
lines.append(f' {node_id} -> {consumer.__name__} [color="{color}"];')
221+
222+
lines.append("}")
223+
return "\n".join(lines)
224+
225+
226+
def render_svg(
227+
blueprint_set: ModuleBlueprintSet,
228+
output_path: str,
229+
*,
230+
layout: set[LayoutAlgo] | None = None,
231+
) -> None:
232+
"""Generate an SVG file from a ModuleBlueprintSet using graphviz.
233+
234+
Args:
235+
blueprint_set: The blueprint set to visualize.
236+
output_path: Path to write the SVG file.
237+
layout: Set of layout algorithms to apply.
238+
"""
239+
import subprocess
240+
241+
if layout is None:
242+
layout = set()
243+
244+
dot_code = render(blueprint_set, layout=layout)
245+
engine = "fdp" if LayoutAlgo.FDP in layout else "dot"
246+
result = subprocess.run(
247+
[engine, "-Tsvg", "-o", output_path],
248+
input=dot_code,
249+
text=True,
250+
capture_output=True,
251+
)
252+
if result.returncode != 0:
253+
raise RuntimeError(f"graphviz failed: {result.stderr}")
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2025 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Module introspection and rendering.
16+
17+
Renderers:
18+
- ansi: ANSI terminal output (default)
19+
- dot: Graphviz DOT format
20+
"""
21+
22+
from dimos.core.introspection.module import ansi, dot
23+
from dimos.core.introspection.module.info import (
24+
INTERNAL_RPCS,
25+
ModuleInfo,
26+
ParamInfo,
27+
RpcInfo,
28+
SkillInfo,
29+
StreamInfo,
30+
extract_module_info,
31+
)
32+
from dimos.core.introspection.module.render import render_module_io
33+
34+
__all__ = [
35+
"INTERNAL_RPCS",
36+
"ModuleInfo",
37+
"ParamInfo",
38+
"RpcInfo",
39+
"SkillInfo",
40+
"StreamInfo",
41+
"ansi",
42+
"dot",
43+
"extract_module_info",
44+
"render_module_io",
45+
]

0 commit comments

Comments
 (0)