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,146 @@
|
|
|
1
|
+
"""Collects the source files of a project that are most likely to define agents.
|
|
2
|
+
|
|
3
|
+
Secrets never leave the machine: env/key files are skipped and anything that looks
|
|
4
|
+
like an API key is redacted before file contents are handed to an LLM.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
SKIP_DIRS = {
|
|
16
|
+
".git", ".hg", ".svn", "node_modules", "__pycache__", ".venv", "venv", "env", ".env", ".tox",
|
|
17
|
+
".nox", ".mypy_cache", ".pytest_cache", ".ruff_cache", "dist", "build", "site-packages",
|
|
18
|
+
".idea", ".vscode", ".next", ".turbo", "coverage", ".visualagents", ".ipynb_checkpoints",
|
|
19
|
+
"target", "vendor", ".cache", "htmlcov", "migrations",
|
|
20
|
+
}
|
|
21
|
+
SOURCE_EXTS = {".py", ".ts", ".tsx", ".js", ".mjs", ".cjs", ".jsx", ".yaml", ".yml", ".toml", ".json", ".ipynb", ".md"}
|
|
22
|
+
SECRET_NAMES = re.compile(r"(^\.env)|(\.(pem|key|p12|pfx|crt)$)|(secret|credential|password|token)", re.I)
|
|
23
|
+
MAX_FILE_BYTES = 400_000
|
|
24
|
+
|
|
25
|
+
AGENT_HINTS = re.compile(
|
|
26
|
+
r"\b(agent|agents|crew|crewai|autogen|langgraph|langchain|StateGraph|add_node|add_edge|handoff|"
|
|
27
|
+
r"supervisor|swarm|tool|tools|llm|chat_model|ChatOpenAI|ChatAnthropic|prompt|system_message|"
|
|
28
|
+
r"instructions|role|backstory|goal|task|workflow|orchestrat\w*|planner|executor|router|"
|
|
29
|
+
r"GroupChat|AssistantAgent|Runner\.run|create_react_agent|function_call)\b",
|
|
30
|
+
re.I,
|
|
31
|
+
)
|
|
32
|
+
NAME_HINTS = re.compile(r"(agent|crew|graph|workflow|flow|team|main|app|orchestr|pipeline|node|task|config)", re.I)
|
|
33
|
+
|
|
34
|
+
SECRET_PATTERNS = [
|
|
35
|
+
re.compile(r"sk-ant-[A-Za-z0-9_\-]{10,}"),
|
|
36
|
+
re.compile(r"sk-[A-Za-z0-9_\-]{20,}"),
|
|
37
|
+
re.compile(r"(?:AKIA|ASIA)[A-Z0-9]{16}"),
|
|
38
|
+
re.compile(r"gh[pousr]_[A-Za-z0-9]{20,}"),
|
|
39
|
+
re.compile(r"xox[baprs]-[A-Za-z0-9\-]{10,}"),
|
|
40
|
+
re.compile(r"AIza[0-9A-Za-z_\-]{30,}"),
|
|
41
|
+
re.compile(r"""(?i)((?:api[_-]?key|secret|token|password)\s*[:=]\s*["'])([^"']{8,})(["'])"""),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SourceFile:
|
|
47
|
+
path: Path
|
|
48
|
+
rel: str
|
|
49
|
+
text: str
|
|
50
|
+
score: float
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def redact(text: str) -> str:
|
|
54
|
+
for pat in SECRET_PATTERNS:
|
|
55
|
+
if pat.groups >= 3:
|
|
56
|
+
text = pat.sub(lambda m: f"{m.group(1)}<redacted>{m.group(3)}", text)
|
|
57
|
+
else:
|
|
58
|
+
text = pat.sub("<redacted>", text)
|
|
59
|
+
return text
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read(path: Path) -> str | None:
|
|
63
|
+
try:
|
|
64
|
+
if path.stat().st_size > MAX_FILE_BYTES:
|
|
65
|
+
return None
|
|
66
|
+
raw = path.read_text(encoding="utf-8", errors="replace")
|
|
67
|
+
except OSError:
|
|
68
|
+
return None
|
|
69
|
+
if path.suffix == ".ipynb":
|
|
70
|
+
try:
|
|
71
|
+
nb = json.loads(raw)
|
|
72
|
+
cells = [
|
|
73
|
+
"".join(c.get("source", [])) for c in nb.get("cells", []) if c.get("cell_type") == "code"
|
|
74
|
+
]
|
|
75
|
+
raw = "\n\n# %% cell\n".join(cells)
|
|
76
|
+
except (ValueError, AttributeError):
|
|
77
|
+
return None
|
|
78
|
+
return raw
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def iter_files(root: Path):
|
|
82
|
+
root = Path(root)
|
|
83
|
+
if root.is_file():
|
|
84
|
+
yield root
|
|
85
|
+
return
|
|
86
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
87
|
+
dirnames[:] = sorted(
|
|
88
|
+
d for d in dirnames if d not in SKIP_DIRS and not d.startswith(".") and not d.endswith(".egg-info")
|
|
89
|
+
)
|
|
90
|
+
for name in sorted(filenames):
|
|
91
|
+
p = Path(dirpath) / name
|
|
92
|
+
if p.suffix.lower() not in SOURCE_EXTS or SECRET_NAMES.search(name):
|
|
93
|
+
continue
|
|
94
|
+
if name in {"package-lock.json", "poetry.lock", "uv.lock", "yarn.lock", "pnpm-lock.yaml"}:
|
|
95
|
+
continue
|
|
96
|
+
yield p
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def collect(root: Path, max_files: int = 400) -> list[SourceFile]:
|
|
100
|
+
"""All candidate files, ranked by how agent-related they look (highest first)."""
|
|
101
|
+
root = Path(root).resolve()
|
|
102
|
+
base = root if root.is_dir() else root.parent
|
|
103
|
+
out: list[SourceFile] = []
|
|
104
|
+
for p in iter_files(root):
|
|
105
|
+
text = _read(p)
|
|
106
|
+
if not text or not text.strip():
|
|
107
|
+
continue
|
|
108
|
+
rel = str(p.relative_to(base))
|
|
109
|
+
hits = len(AGENT_HINTS.findall(text))
|
|
110
|
+
score = hits / (1 + len(text) / 4000) + hits * 0.05
|
|
111
|
+
if NAME_HINTS.search(p.stem):
|
|
112
|
+
score += 3
|
|
113
|
+
if p.suffix == ".md":
|
|
114
|
+
score = 5 if p.stem.lower() == "readme" and rel.count(os.sep) == 0 else score * 0.2
|
|
115
|
+
if p.suffix in {".json", ".toml"}:
|
|
116
|
+
score *= 0.3
|
|
117
|
+
if "test" in rel.lower():
|
|
118
|
+
score *= 0.3
|
|
119
|
+
out.append(SourceFile(p, rel, text, score))
|
|
120
|
+
if len(out) >= max_files * 4:
|
|
121
|
+
break
|
|
122
|
+
out.sort(key=lambda f: (-f.score, f.rel))
|
|
123
|
+
return out[:max_files]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def pack(files: list[SourceFile], budget_chars: int = 180_000) -> tuple[str, list[str]]:
|
|
127
|
+
"""Concatenate the best files into one prompt-sized block. Returns (text, included rel paths)."""
|
|
128
|
+
parts: list[str] = []
|
|
129
|
+
included: list[str] = []
|
|
130
|
+
used = 0
|
|
131
|
+
for f in files:
|
|
132
|
+
if f.score <= 0 and included:
|
|
133
|
+
continue
|
|
134
|
+
body = redact(f.text)
|
|
135
|
+
numbered = "\n".join(f"{i:>4}| {line}" for i, line in enumerate(body.splitlines(), 1))
|
|
136
|
+
chunk = f'<file path="{f.rel}">\n{numbered}\n</file>\n'
|
|
137
|
+
if used + len(chunk) > budget_chars:
|
|
138
|
+
remaining = budget_chars - used
|
|
139
|
+
if remaining > 2000:
|
|
140
|
+
parts.append(chunk[: remaining - 40] + "\n... (truncated)\n</file>\n")
|
|
141
|
+
included.append(f.rel + " (truncated)")
|
|
142
|
+
break
|
|
143
|
+
parts.append(chunk)
|
|
144
|
+
included.append(f.rel)
|
|
145
|
+
used += len(chunk)
|
|
146
|
+
return "".join(parts), included
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"project": "research_crew (demo)",
|
|
4
|
+
"framework": "crewai-style",
|
|
5
|
+
"summary": "A four-agent research crew. The Planner splits the user's question into sub-questions, the Researcher gathers and verifies sources with web search and paper reading, the Writer drafts a cited report, and the Critic fact-checks it - sending it back to the Writer until it passes review.",
|
|
6
|
+
"generated_by": "the bundled demo blueprint",
|
|
7
|
+
"generated_at": "",
|
|
8
|
+
"agents": [
|
|
9
|
+
{
|
|
10
|
+
"id": "planner",
|
|
11
|
+
"name": "Planner",
|
|
12
|
+
"role": "Research strategist",
|
|
13
|
+
"description": "Breaks the user's question into independent sub-questions and a research plan.",
|
|
14
|
+
"tools": ["outline_builder"],
|
|
15
|
+
"file": "research_crew.py",
|
|
16
|
+
"line": 37,
|
|
17
|
+
"aliases": ["planner", "Research strategist"],
|
|
18
|
+
"keywords": ["plan", "sub-questions", "outline"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "researcher",
|
|
22
|
+
"name": "Researcher",
|
|
23
|
+
"role": "Senior research analyst",
|
|
24
|
+
"description": "Searches the web and reads papers to find and verify evidence for each sub-question.",
|
|
25
|
+
"tools": ["web_search", "read_paper"],
|
|
26
|
+
"file": "research_crew.py",
|
|
27
|
+
"line": 39,
|
|
28
|
+
"aliases": ["researcher", "Senior research analyst"],
|
|
29
|
+
"keywords": ["sources", "search", "results", "paper"]
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "writer",
|
|
33
|
+
"name": "Writer",
|
|
34
|
+
"role": "Technical writer",
|
|
35
|
+
"description": "Turns the findings into a clear, structured report with citations; revises after review.",
|
|
36
|
+
"tools": [],
|
|
37
|
+
"file": "research_crew.py",
|
|
38
|
+
"line": 41,
|
|
39
|
+
"aliases": ["writer", "Technical writer"],
|
|
40
|
+
"keywords": ["draft", "section", "revising"]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"id": "critic",
|
|
44
|
+
"name": "Critic",
|
|
45
|
+
"role": "Reviewer",
|
|
46
|
+
"description": "Fact-checks every claim in the draft and either approves it or sends it back with fixes.",
|
|
47
|
+
"tools": ["fact_check"],
|
|
48
|
+
"file": "research_crew.py",
|
|
49
|
+
"line": 42,
|
|
50
|
+
"aliases": ["critic", "Reviewer"],
|
|
51
|
+
"keywords": ["claim", "unsupported", "approved", "revise"]
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"edges": [
|
|
55
|
+
{"from": "planner", "to": "researcher", "label": "research plan", "conditional": false},
|
|
56
|
+
{"from": "researcher", "to": "writer", "label": "findings + sources", "conditional": false},
|
|
57
|
+
{"from": "writer", "to": "critic", "label": "draft", "conditional": false},
|
|
58
|
+
{"from": "critic", "to": "writer", "label": "revise", "conditional": true}
|
|
59
|
+
],
|
|
60
|
+
"entry": ["planner"],
|
|
61
|
+
"exits": ["critic"],
|
|
62
|
+
"steps": [
|
|
63
|
+
{"order": 1, "title": "Plan", "agents": ["planner"], "description": "Split the question into sub-questions."},
|
|
64
|
+
{"order": 2, "title": "Research", "agents": ["researcher"], "description": "Gather and verify sources for each sub-question."},
|
|
65
|
+
{"order": 3, "title": "Draft", "agents": ["writer"], "description": "Write a cited report from the findings."},
|
|
66
|
+
{"order": 4, "title": "Review", "agents": ["critic"], "description": "Fact-check; loop back to the Writer until approved."}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""A simulated multi-agent research crew for `visualagents demo`.
|
|
2
|
+
|
|
3
|
+
It needs no API key: the agents "think" from a script and print CrewAI-style chain-of-thought,
|
|
4
|
+
exactly the kind of console output VisualAgents maps onto the blueprint. The tiny framework
|
|
5
|
+
below is real enough that `visualagents scan` finds the same four agents statically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import random
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
SPEED = 1.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Agent:
|
|
19
|
+
def __init__(self, name: str, role: str, goal: str, tools: list | None = None):
|
|
20
|
+
self.name, self.role, self.goal, self.tools = name, role, goal, tools or []
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Workflow:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.edges: list[tuple[str, str]] = []
|
|
26
|
+
|
|
27
|
+
def add_edge(self, src: str, dst: str) -> None:
|
|
28
|
+
self.edges.append((src, dst))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def web_search(query: str) -> str: ...
|
|
32
|
+
def read_paper(url: str) -> str: ...
|
|
33
|
+
def outline_builder(question: str) -> str: ...
|
|
34
|
+
def fact_check(claim: str) -> str: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
planner = Agent(name="Planner", role="Research strategist", goal="Break the question into a research plan",
|
|
38
|
+
tools=[outline_builder])
|
|
39
|
+
researcher = Agent(name="Researcher", role="Senior research analyst", goal="Find and verify sources for each sub-question",
|
|
40
|
+
tools=[web_search, read_paper])
|
|
41
|
+
writer = Agent(name="Writer", role="Technical writer", goal="Turn the findings into a clear, cited report")
|
|
42
|
+
critic = Agent(name="Critic", role="Reviewer", goal="Check the draft for unsupported claims and gaps",
|
|
43
|
+
tools=[fact_check])
|
|
44
|
+
|
|
45
|
+
flow = Workflow()
|
|
46
|
+
flow.add_edge("planner", "researcher")
|
|
47
|
+
flow.add_edge("researcher", "writer")
|
|
48
|
+
flow.add_edge("writer", "critic")
|
|
49
|
+
flow.add_edge("critic", "writer")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def say(text: str = "", pause: float = 0.35) -> None:
|
|
53
|
+
print(text, flush=True)
|
|
54
|
+
time.sleep(pause / SPEED * random.uniform(0.7, 1.3))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def stream(prefix: str, text: str, pause: float = 0.9) -> None:
|
|
58
|
+
"""Print a thought word-by-word-ish, the way a streaming LLM would."""
|
|
59
|
+
sys.stdout.write(prefix)
|
|
60
|
+
for word in text.split(" "):
|
|
61
|
+
sys.stdout.write(word + " ")
|
|
62
|
+
sys.stdout.flush()
|
|
63
|
+
time.sleep(0.035 / SPEED)
|
|
64
|
+
sys.stdout.write("\n")
|
|
65
|
+
sys.stdout.flush()
|
|
66
|
+
time.sleep(pause / SPEED)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def agent_header(agent: Agent) -> None:
|
|
70
|
+
say("")
|
|
71
|
+
say(f"# Agent: {agent.name}", 0.6)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> None:
|
|
75
|
+
global SPEED
|
|
76
|
+
ap = argparse.ArgumentParser()
|
|
77
|
+
ap.add_argument("--speed", type=float, default=1.0)
|
|
78
|
+
ap.add_argument("question", nargs="?", default="How are solid-state batteries changing electric vehicles?")
|
|
79
|
+
args = ap.parse_args()
|
|
80
|
+
SPEED = max(0.1, args.speed)
|
|
81
|
+
|
|
82
|
+
say(f"🚀 Crew: research_crew · question: {args.question!r}", 1.0)
|
|
83
|
+
|
|
84
|
+
agent_header(planner)
|
|
85
|
+
say("## Task: Break the question into a research plan", 0.5)
|
|
86
|
+
stream("## Thought: ", "The question spans chemistry, manufacturing and market adoption. I should split it so each part can be researched independently.")
|
|
87
|
+
say("## Using tool: outline_builder")
|
|
88
|
+
say('## Tool Input: {"question": "solid-state batteries in EVs"}', 0.8)
|
|
89
|
+
say("## Tool Output: 3 sub-questions: energy density vs Li-ion, manufacturing hurdles, which automakers ship first", 0.8)
|
|
90
|
+
stream("## Final Answer: ", "Plan → (1) energy density and charging, (2) manufacturing & cost, (3) adoption timeline by automaker.")
|
|
91
|
+
say("Handing off to Researcher", 0.8)
|
|
92
|
+
|
|
93
|
+
agent_header(researcher)
|
|
94
|
+
say("## Task: Find and verify sources for each sub-question", 0.5)
|
|
95
|
+
stream("## Thought: ", "Start with sub-question 1. I need recent, quantitative numbers on energy density, ideally from peer-reviewed work or manufacturer data sheets.")
|
|
96
|
+
say("## Using tool: web_search")
|
|
97
|
+
say('## Tool Input: {"query": "solid-state battery energy density Wh/kg 2025 prototype"}', 1.0)
|
|
98
|
+
say("## Tool Output: 8 results — QuantumScape QSE-5 (~844 Wh/L), Toyota sulfide roadmap, Nature Energy review…", 0.9)
|
|
99
|
+
stream("## Thought: ", "The Nature Energy review summarises lab vs pilot-line numbers. Reading it gives me a credible baseline.")
|
|
100
|
+
say("## Using tool: read_paper")
|
|
101
|
+
say('## Tool Input: {"url": "https://doi.org/10.1038/s41560-0xx"}', 1.2)
|
|
102
|
+
say("## Tool Output: pilot cells reach 350–400 Wh/kg; 10–80% charge in ~15 min; dendrite suppression still limits cycle life", 0.9)
|
|
103
|
+
stream("## Thought: ", "Sub-question 1 covered. Now manufacturing and cost — dry-room processing and solid electrolyte yield are the usual bottlenecks.")
|
|
104
|
+
say("## Using tool: web_search")
|
|
105
|
+
say('## Tool Input: {"query": "solid-state battery manufacturing cost per kWh yield 2025"}', 1.0)
|
|
106
|
+
say("## Tool Output: analysts estimate 2–3× Li-ion cost today; sulfide electrolytes need moisture-free lines", 0.9)
|
|
107
|
+
say("## Using tool: web_search")
|
|
108
|
+
say('## Tool Input: {"query": "automaker solid-state battery production timeline"}', 1.0)
|
|
109
|
+
say("## Tool Output: Toyota targets 2027–28; Nissan pilot line 2028; Mercedes & Factorial road tests underway", 0.9)
|
|
110
|
+
stream("## Final Answer: ", "Findings: 350–400 Wh/kg in pilot cells, ~15 min fast-charge, 2–3× cost today, first production cars 2027–28. 6 sources.")
|
|
111
|
+
say("Passing results to Writer", 0.8)
|
|
112
|
+
|
|
113
|
+
agent_header(writer)
|
|
114
|
+
say("## Task: Turn the findings into a clear, cited report", 0.5)
|
|
115
|
+
stream("## Thought: ", "Readers care about range, charging time and when they can buy one. I'll lead with those and keep chemistry details brief.")
|
|
116
|
+
stream("## Thought: ", "Structure: summary, three sections matching the plan, and a short outlook.", 0.6)
|
|
117
|
+
say("Drafting section 1/3: energy density & charging…", 1.2)
|
|
118
|
+
say("Drafting section 2/3: manufacturing & cost…", 1.2)
|
|
119
|
+
say("Drafting section 3/3: who ships first…", 1.2)
|
|
120
|
+
stream("## Final Answer: ", "Draft v1 ready — 640 words, 6 citations.")
|
|
121
|
+
say("Handing off to Critic", 0.8)
|
|
122
|
+
|
|
123
|
+
agent_header(critic)
|
|
124
|
+
say("## Task: Check the draft for unsupported claims and gaps", 0.5)
|
|
125
|
+
stream("## Thought: ", "Check every number against a source. The '50% more range' line in the summary has no citation.")
|
|
126
|
+
say("## Using tool: fact_check")
|
|
127
|
+
say('## Tool Input: {"claim": "solid-state packs give 50% more range"}', 1.0)
|
|
128
|
+
say("## Tool Output: unsupported — sources show +20–40% depending on pack design", 0.9)
|
|
129
|
+
stream("## Final Answer: ", "REVISE: correct the range claim to 20–40% and cite it; add one sentence on cycle-life risk.")
|
|
130
|
+
say("Sending the draft back to Writer", 0.8)
|
|
131
|
+
|
|
132
|
+
agent_header(writer)
|
|
133
|
+
stream("## Thought: ", "Fix the range figure, add the citation, and mention dendrite-related cycle-life limits in section 1.")
|
|
134
|
+
say("Revising summary and section 1…", 1.4)
|
|
135
|
+
stream("## Final Answer: ", "Draft v2 ready — range claim corrected (20–40%), cycle-life caveat added.")
|
|
136
|
+
say("Handing off to Critic", 0.8)
|
|
137
|
+
|
|
138
|
+
agent_header(critic)
|
|
139
|
+
stream("## Thought: ", "All quantitative claims now trace to a source and the risks are stated. Good to ship.")
|
|
140
|
+
stream("## Final Answer: ", "APPROVED ✅")
|
|
141
|
+
say("")
|
|
142
|
+
say("✅ Crew finished: report delivered (640 words, 7 citations)", 0.2)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
main()
|