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.
@@ -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