visualagents 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.
- visualagents/__init__.py +20 -0
- visualagents/__main__.py +3 -0
- visualagents/_autoinstrument/__init__.py +0 -0
- visualagents/_autoinstrument/sitecustomize.py +40 -0
- visualagents/_autoinstrument/visualagents_runtime.py +864 -0
- visualagents/_hook/visualagents_boot.py +42 -0
- visualagents/blueprint.py +276 -0
- visualagents/cli.py +726 -0
- visualagents/collector.py +146 -0
- visualagents/demo/__init__.py +0 -0
- visualagents/demo/blueprint.json +68 -0
- visualagents/demo/research_crew.py +146 -0
- visualagents/detect.py +337 -0
- visualagents/home.py +139 -0
- visualagents/hooks.py +63 -0
- visualagents/hub.py +143 -0
- visualagents/launcher.py +87 -0
- visualagents/llm.py +314 -0
- visualagents/narrator.py +58 -0
- visualagents/patterns.py +81 -0
- visualagents/projects.py +140 -0
- visualagents/runner.py +355 -0
- visualagents/scanner.py +91 -0
- visualagents/sdk.py +18 -0
- visualagents/server.py +209 -0
- visualagents/static_scan.py +677 -0
- visualagents/termui.py +94 -0
- visualagents/tracker.py +501 -0
- visualagents/tui.py +240 -0
- visualagents/web/app.js +1753 -0
- visualagents/web/index.html +117 -0
- visualagents/web/styles.css +551 -0
- visualagents/wizard.py +331 -0
- visualagents-0.1.0.dist-info/METADATA +438 -0
- visualagents-0.1.0.dist-info/RECORD +38 -0
- visualagents-0.1.0.dist-info/WHEEL +4 -0
- visualagents-0.1.0.dist-info/entry_points.txt +3 -0
- visualagents-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Run at interpreter startup by the `visualagents_hook.pth` file that `visualagents hook` installs.
|
|
2
|
+
|
|
3
|
+
Stdlib only, Python 3.8+, and it must never break the host program: every failure is swallowed.
|
|
4
|
+
The .pth line executes this file with VISUALAGENTS_PROJECT set to the hooked folder.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _start(project):
|
|
13
|
+
if os.environ.get("VISUALAGENTS_DISABLE") == "1" or os.environ.get("VISUALAGENTS_URL"):
|
|
14
|
+
return # turned off, or `visualagents run` is already instrumenting this process
|
|
15
|
+
home = os.environ.get("VISUALAGENTS_HOME") or os.path.join(os.path.expanduser("~"), ".visualagents")
|
|
16
|
+
daemon_file = os.path.join(home, "daemon.json")
|
|
17
|
+
info = {}
|
|
18
|
+
try:
|
|
19
|
+
with open(daemon_file, encoding="utf-8") as fh:
|
|
20
|
+
info = json.load(fh)
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
rt = sys.modules.get("visualagents_runtime")
|
|
24
|
+
if rt is None:
|
|
25
|
+
import importlib.util
|
|
26
|
+
|
|
27
|
+
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
28
|
+
path = os.path.join(here, "_autoinstrument", "visualagents_runtime.py")
|
|
29
|
+
spec = importlib.util.spec_from_file_location("visualagents_runtime", path)
|
|
30
|
+
rt = importlib.util.module_from_spec(spec)
|
|
31
|
+
sys.modules["visualagents_runtime"] = rt
|
|
32
|
+
spec.loader.exec_module(rt)
|
|
33
|
+
rt.configure(url=info.get("url"), token=info.get("token"), project=project, daemon_file=daemon_file, lazy=True)
|
|
34
|
+
rt.instrument()
|
|
35
|
+
if os.environ.get("VISUALAGENTS_CAPTURE_OUTPUT", "1") != "0":
|
|
36
|
+
rt.capture_output()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
_start(globals().get("VISUALAGENTS_PROJECT") or os.getcwd())
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""The blueprint: a static map of the agents in a project and how work flows between them.
|
|
2
|
+
|
|
3
|
+
A blueprint is plain JSON so it can be hand-edited, committed, or produced by any tool:
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
"version": 1,
|
|
7
|
+
"project": "research-crew",
|
|
8
|
+
"framework": "crewai",
|
|
9
|
+
"summary": "...",
|
|
10
|
+
"agents": [{"id": "researcher", "name": "Researcher", "role": "...", ...}],
|
|
11
|
+
"edges": [{"from": "planner", "to": "researcher", "label": "plan", "conditional": false}],
|
|
12
|
+
"entry": ["planner"],
|
|
13
|
+
"exits": ["writer"],
|
|
14
|
+
"steps": [{"order": 1, "title": "Plan", "agents": ["planner"], "description": "..."}]
|
|
15
|
+
}
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import datetime as _dt
|
|
21
|
+
import json
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
SCHEMA_VERSION = 1
|
|
27
|
+
STATE_DIR = ".visualagents"
|
|
28
|
+
|
|
29
|
+
# JSON schema handed to the LLM as a structured-output format. Every property is
|
|
30
|
+
# required (structured outputs reject optional keys); "unknown" values are empty strings.
|
|
31
|
+
_AGENT_SCHEMA = {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {
|
|
34
|
+
"id": {"type": "string", "description": "short snake_case identifier, e.g. 'researcher'"},
|
|
35
|
+
"name": {"type": "string", "description": "display name, e.g. 'Researcher'"},
|
|
36
|
+
"role": {"type": "string", "description": "one-line role, max ~8 words"},
|
|
37
|
+
"description": {"type": "string", "description": "1-2 sentences: what this agent does and when"},
|
|
38
|
+
"tools": {"type": "array", "items": {"type": "string"}, "description": "tool / function names it can call"},
|
|
39
|
+
"file": {"type": "string", "description": "relative path where the agent is defined"},
|
|
40
|
+
"line": {"type": "integer", "description": "1-based line of the definition, 0 if unknown"},
|
|
41
|
+
"aliases": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"items": {"type": "string"},
|
|
44
|
+
"description": "other strings that identify this agent in logs: variable names, node names, role titles",
|
|
45
|
+
},
|
|
46
|
+
"keywords": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": {"type": "string"},
|
|
49
|
+
"description": "distinctive words/phrases this agent is likely to print while working",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"required": ["id", "name", "role", "description", "tools", "file", "line", "aliases", "keywords"],
|
|
53
|
+
"additionalProperties": False,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
BLUEPRINT_JSON_SCHEMA: dict[str, Any] = {
|
|
57
|
+
"type": "object",
|
|
58
|
+
"properties": {
|
|
59
|
+
"project": {"type": "string"},
|
|
60
|
+
"framework": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "langgraph, langchain, crewai, autogen, openai-agents, swarm, llamaindex, smolagents, custom, ...",
|
|
63
|
+
},
|
|
64
|
+
"summary": {"type": "string", "description": "2-4 sentences describing the overall workflow"},
|
|
65
|
+
"agents": {"type": "array", "items": _AGENT_SCHEMA},
|
|
66
|
+
"edges": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"items": {
|
|
69
|
+
"type": "object",
|
|
70
|
+
"properties": {
|
|
71
|
+
"from": {"type": "string", "description": "agent id"},
|
|
72
|
+
"to": {"type": "string", "description": "agent id"},
|
|
73
|
+
"label": {"type": "string", "description": "what is handed over, max ~5 words"},
|
|
74
|
+
"conditional": {"type": "boolean"},
|
|
75
|
+
},
|
|
76
|
+
"required": ["from", "to", "label", "conditional"],
|
|
77
|
+
"additionalProperties": False,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
"entry": {"type": "array", "items": {"type": "string"}, "description": "agent ids that receive the user query first"},
|
|
81
|
+
"exits": {"type": "array", "items": {"type": "string"}, "description": "agent ids that produce the final answer"},
|
|
82
|
+
"steps": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"description": "the general blueprint as ordered phases",
|
|
85
|
+
"items": {
|
|
86
|
+
"type": "object",
|
|
87
|
+
"properties": {
|
|
88
|
+
"order": {"type": "integer"},
|
|
89
|
+
"title": {"type": "string", "description": "2-4 words"},
|
|
90
|
+
"agents": {"type": "array", "items": {"type": "string"}},
|
|
91
|
+
"description": {"type": "string"},
|
|
92
|
+
},
|
|
93
|
+
"required": ["order", "title", "agents", "description"],
|
|
94
|
+
"additionalProperties": False,
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
"required": ["project", "framework", "summary", "agents", "edges", "entry", "exits", "steps"],
|
|
99
|
+
"additionalProperties": False,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def slugify(text: str) -> str:
|
|
104
|
+
s = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", str(text).strip())
|
|
105
|
+
s = re.sub(r"[^a-zA-Z0-9]+", "_", s).strip("_").lower()
|
|
106
|
+
return s or "agent"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def norm_key(text: str) -> str:
|
|
110
|
+
"""Loose key for matching names in logs: lowercase alphanumerics only."""
|
|
111
|
+
return re.sub(r"[^a-z0-9]", "", str(text).lower())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def state_dir(project: Path) -> Path:
|
|
115
|
+
return Path(project) / STATE_DIR
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def blueprint_path(project: Path) -> Path:
|
|
119
|
+
return state_dir(project) / "blueprint.json"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def load(path: Path) -> dict[str, Any]:
|
|
123
|
+
with open(path, encoding="utf-8") as fh:
|
|
124
|
+
return normalize(json.load(fh))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def save(bp: dict[str, Any], path: Path) -> Path:
|
|
128
|
+
path = Path(path)
|
|
129
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
130
|
+
ignore = path.parent / ".gitignore"
|
|
131
|
+
if path.parent.name == STATE_DIR and not ignore.exists():
|
|
132
|
+
ignore.write_text("runs/\n", encoding="utf-8")
|
|
133
|
+
path.write_text(json.dumps(bp, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
134
|
+
return path
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def empty(project: str = "project") -> dict[str, Any]:
|
|
138
|
+
return normalize({"project": project, "framework": "unknown", "agents": []})
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def normalize(raw: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
"""Make any blueprint-ish dict safe to consume: unique ids, resolved references, ordered steps."""
|
|
143
|
+
bp: dict[str, Any] = {
|
|
144
|
+
"version": SCHEMA_VERSION,
|
|
145
|
+
"project": str(raw.get("project") or "project"),
|
|
146
|
+
"framework": str(raw.get("framework") or "unknown"),
|
|
147
|
+
"summary": str(raw.get("summary") or ""),
|
|
148
|
+
"generated_by": str(raw.get("generated_by") or ""),
|
|
149
|
+
"generated_at": str(raw.get("generated_at") or ""),
|
|
150
|
+
"agents": [],
|
|
151
|
+
"edges": [],
|
|
152
|
+
"entry": [],
|
|
153
|
+
"exits": [],
|
|
154
|
+
"steps": [],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
lookup: dict[str, str] = {} # norm_key(any name) -> id
|
|
158
|
+
used: set[str] = set()
|
|
159
|
+
for a in raw.get("agents") or []:
|
|
160
|
+
if not isinstance(a, dict):
|
|
161
|
+
continue
|
|
162
|
+
name = str(a.get("name") or a.get("id") or "Agent").strip()
|
|
163
|
+
aid = slugify(a.get("id") or name)
|
|
164
|
+
base, n = aid, 2
|
|
165
|
+
while aid in used:
|
|
166
|
+
aid, n = f"{base}_{n}", n + 1
|
|
167
|
+
used.add(aid)
|
|
168
|
+
source = a.get("source") if isinstance(a.get("source"), dict) else {}
|
|
169
|
+
agent = {
|
|
170
|
+
"id": aid,
|
|
171
|
+
"name": name,
|
|
172
|
+
"role": str(a.get("role") or ""),
|
|
173
|
+
"description": str(a.get("description") or ""),
|
|
174
|
+
"tools": _str_list(a.get("tools")),
|
|
175
|
+
"file": str(a.get("file") or source.get("file") or ""),
|
|
176
|
+
"line": _int(a.get("line") or source.get("line")),
|
|
177
|
+
"aliases": _str_list(a.get("aliases")),
|
|
178
|
+
"keywords": _str_list(a.get("keywords")),
|
|
179
|
+
}
|
|
180
|
+
bp["agents"].append(agent)
|
|
181
|
+
for key in [aid, name, *agent["aliases"]]:
|
|
182
|
+
k = norm_key(key)
|
|
183
|
+
if k and k not in lookup:
|
|
184
|
+
lookup[k] = aid
|
|
185
|
+
|
|
186
|
+
def resolve(ref: Any) -> str | None:
|
|
187
|
+
return lookup.get(norm_key(ref)) if ref is not None else None
|
|
188
|
+
|
|
189
|
+
seen_edges: set[tuple[str, str]] = set()
|
|
190
|
+
for e in raw.get("edges") or []:
|
|
191
|
+
if not isinstance(e, dict):
|
|
192
|
+
continue
|
|
193
|
+
src, dst = resolve(e.get("from") or e.get("source")), resolve(e.get("to") or e.get("target"))
|
|
194
|
+
if not src or not dst or src == dst or (src, dst) in seen_edges:
|
|
195
|
+
continue
|
|
196
|
+
seen_edges.add((src, dst))
|
|
197
|
+
bp["edges"].append(
|
|
198
|
+
{"from": src, "to": dst, "label": str(e.get("label") or ""), "conditional": bool(e.get("conditional"))}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
ids = [a["id"] for a in bp["agents"]]
|
|
202
|
+
bp["entry"] = _dedupe(r for r in map(resolve, _as_list(raw.get("entry"))) if r)
|
|
203
|
+
bp["exits"] = _dedupe(r for r in map(resolve, _as_list(raw.get("exits"))) if r)
|
|
204
|
+
if not bp["entry"] and ids:
|
|
205
|
+
targets = {e["to"] for e in bp["edges"]}
|
|
206
|
+
bp["entry"] = [i for i in ids if i not in targets][:1] or ids[:1]
|
|
207
|
+
if not bp["exits"] and ids:
|
|
208
|
+
sources = {e["from"] for e in bp["edges"]}
|
|
209
|
+
bp["exits"] = [i for i in ids if i not in sources][-1:] or ids[-1:]
|
|
210
|
+
|
|
211
|
+
steps = []
|
|
212
|
+
for i, s in enumerate(raw.get("steps") or []):
|
|
213
|
+
if not isinstance(s, dict):
|
|
214
|
+
continue
|
|
215
|
+
agents = _dedupe(r for r in map(resolve, _as_list(s.get("agents"))) if r)
|
|
216
|
+
steps.append(
|
|
217
|
+
{
|
|
218
|
+
"order": _int(s.get("order")) or i + 1,
|
|
219
|
+
"title": str(s.get("title") or f"Step {i + 1}"),
|
|
220
|
+
"agents": agents,
|
|
221
|
+
"description": str(s.get("description") or ""),
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
steps.sort(key=lambda s: s["order"])
|
|
225
|
+
if not steps and ids:
|
|
226
|
+
steps = [
|
|
227
|
+
{"order": i + 1, "title": a["name"], "agents": [a["id"]], "description": a["role"]}
|
|
228
|
+
for i, a in enumerate(_topo_order(bp))
|
|
229
|
+
]
|
|
230
|
+
for i, s in enumerate(steps):
|
|
231
|
+
s["order"] = i + 1
|
|
232
|
+
bp["steps"] = steps
|
|
233
|
+
return bp
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _topo_order(bp: dict[str, Any]) -> list[dict[str, Any]]:
|
|
237
|
+
"""Agents in BFS order from the entry points; unreachable agents appended."""
|
|
238
|
+
by_id = {a["id"]: a for a in bp["agents"]}
|
|
239
|
+
out: dict[str, None] = {}
|
|
240
|
+
queue = list(bp["entry"])
|
|
241
|
+
while queue:
|
|
242
|
+
cur = queue.pop(0)
|
|
243
|
+
if cur in out or cur not in by_id:
|
|
244
|
+
continue
|
|
245
|
+
out[cur] = None
|
|
246
|
+
queue.extend(e["to"] for e in bp["edges"] if e["from"] == cur)
|
|
247
|
+
for aid in by_id:
|
|
248
|
+
out.setdefault(aid, None)
|
|
249
|
+
return [by_id[i] for i in out]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def stamp(bp: dict[str, Any], generated_by: str) -> dict[str, Any]:
|
|
253
|
+
bp["generated_by"] = generated_by
|
|
254
|
+
bp["generated_at"] = _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds")
|
|
255
|
+
return bp
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _as_list(v: Any) -> list[Any]:
|
|
259
|
+
if v is None:
|
|
260
|
+
return []
|
|
261
|
+
return list(v) if isinstance(v, (list, tuple)) else [v]
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _str_list(v: Any) -> list[str]:
|
|
265
|
+
return _dedupe(str(x).strip() for x in _as_list(v) if str(x).strip())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _dedupe(items: Any) -> list[Any]:
|
|
269
|
+
return list(dict.fromkeys(items))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _int(v: Any) -> int:
|
|
273
|
+
try:
|
|
274
|
+
return int(v)
|
|
275
|
+
except (TypeError, ValueError):
|
|
276
|
+
return 0
|