smlflg's picture
Initial public upload from Projekte/agent-process-dashboard
06357e7 verified
Raw History Blame Contribute Delete
3.82 kB
from __future__ import annotations
from collections.abc import Iterable
from .models import Classification, ProcessGroup, ProcessNode, ProcessSample
def build_nodes(
samples: list[ProcessSample],
classifications: dict[str, Classification],
) -> dict[str, ProcessNode]:
nodes = {
sample.process_key: ProcessNode(
sample=sample,
classification=classifications.get(sample.process_key, Classification("unknown", 0.0)),
)
for sample in samples
}
pid_to_key = {sample.pid: sample.process_key for sample in samples}
for node in nodes.values():
parent_key = pid_to_key.get(node.sample.ppid)
if parent_key and parent_key in nodes and parent_key != node.process_key:
node.parent_key = parent_key
nodes[parent_key].children.append(node)
for node in nodes.values():
node.children.sort(key=lambda item: item.sample.pid)
for root in _roots(nodes.values()):
_propagate_agent_context(root, inherited=False)
return nodes
def _roots(nodes: Iterable[ProcessNode]) -> list[ProcessNode]:
node_list = list(nodes)
keys = {node.process_key for node in node_list}
return [node for node in node_list if not node.parent_key or node.parent_key not in keys]
def _propagate_agent_context(node: ProcessNode, *, inherited: bool) -> None:
current = inherited or node.classification.is_agent or node.classification.is_agent_related
node.inherited_agent = current
for child in node.children:
_propagate_agent_context(child, inherited=current)
def iter_descendants(node: ProcessNode, *, include_self: bool = True) -> Iterable[ProcessNode]:
if include_self:
yield node
for child in node.children:
yield from iter_descendants(child, include_self=True)
def _has_agent_ancestor(node: ProcessNode, nodes: dict[str, ProcessNode]) -> bool:
parent_key = node.parent_key
while parent_key and parent_key in nodes:
parent = nodes[parent_key]
if parent.classification.is_agent:
return True
parent_key = parent.parent_key
return False
def build_groups(nodes: dict[str, ProcessNode]) -> list[ProcessGroup]:
group_roots: list[ProcessNode] = []
for node in nodes.values():
classification = node.classification
if classification.is_agent and not _has_agent_ancestor(node, nodes):
group_roots.append(node)
elif (
classification.is_mcp or classification.is_bridge
) and not _has_agent_ancestor(node, nodes):
group_roots.append(node)
seen: set[str] = set()
groups: list[ProcessGroup] = []
for root in sorted(group_roots, key=lambda item: item.sample.pid):
if root.process_key in seen:
continue
seen.add(root.process_key)
members = list(iter_descendants(root))
bridge_count = sum(1 for node in members if node.classification.is_bridge)
mcp_count = sum(1 for node in members if node.classification.is_mcp)
groups.append(
ProcessGroup(
group_id=root.process_key,
root=root,
nodes=members,
label=_group_label(root),
child_count=max(0, len(members) - 1),
bridge_count=bridge_count,
mcp_count=mcp_count,
total_rss_bytes=sum(node.sample.rss_bytes for node in members),
max_age_seconds=max((node.sample.age_seconds for node in members), default=0.0),
)
)
return groups
def _group_label(root: ProcessNode) -> str:
argv0 = root.sample.argv[0] if root.sample.argv else root.sample.comm
name = argv0.rsplit("/", 1)[-1] or root.sample.comm
return f"{name} - PID {root.sample.pid}"