roundtable-cli 0.4.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.
- roundtable/__init__.py +10 -0
- roundtable/agents.py +218 -0
- roundtable/cli.py +722 -0
- roundtable/config.py +265 -0
- roundtable/dashboard.py +533 -0
- roundtable/discovery.py +71 -0
- roundtable/engine.py +714 -0
- roundtable/errors.py +20 -0
- roundtable/insights.py +210 -0
- roundtable/llm.py +1048 -0
- roundtable/mcp.py +248 -0
- roundtable/modelpick.py +309 -0
- roundtable/models.py +202 -0
- roundtable/prompts.py +205 -0
- roundtable/runctl.py +212 -0
- roundtable/scan.py +187 -0
- roundtable/store.py +339 -0
- roundtable_cli-0.4.0.dist-info/METADATA +570 -0
- roundtable_cli-0.4.0.dist-info/RECORD +22 -0
- roundtable_cli-0.4.0.dist-info/WHEEL +4 -0
- roundtable_cli-0.4.0.dist-info/entry_points.txt +4 -0
- roundtable_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
roundtable/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""roundtable: a multi-LLM planning and orchestration tool.
|
|
2
|
+
|
|
3
|
+
Flow: Planner LLM -> approved Plan (phases -> tasks) -> Main
|
|
4
|
+
Orchestrator drives phases; each Phase Orchestrator (fresh context) dispatches
|
|
5
|
+
Task Agents, summarizes, and reports to Main, which maintains the docs.
|
|
6
|
+
|
|
7
|
+
Every role's model is choosable via config or the plan manifest.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.4.0"
|
roundtable/agents.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""The agent roles: Planner, Main Orchestrator, Phase Orchestrator, Task Agent.
|
|
2
|
+
|
|
3
|
+
Each agent is a thin wrapper binding a *choosable* model to a role. Control flow
|
|
4
|
+
lives in the engine; these classes only turn inputs into the LLM call for their
|
|
5
|
+
role and return the produced text (Markdown, or JSON for the planner).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from . import prompts
|
|
13
|
+
from .llm import LLMProvider, extract_json
|
|
14
|
+
from .models import AgentRef, Phase, Plan, Task
|
|
15
|
+
from .prompts import render_prompt
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _truncate(text: str, limit: int = 4000) -> str:
|
|
19
|
+
return text if len(text) <= limit else text[:limit] + "\n...[truncated]..."
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _models_meta(defaults: dict[str, AgentRef]) -> dict[str, str]:
|
|
23
|
+
"""Stringify role -> AgentRef so the (scripted) backend gets plain strings."""
|
|
24
|
+
return {k: str(v) for k, v in defaults.items()}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Planner:
|
|
28
|
+
def __init__(self, provider: LLMProvider, ref: AgentRef, temperature: float = 0.2):
|
|
29
|
+
self.provider, self.ref, self.temperature = provider, ref, temperature
|
|
30
|
+
|
|
31
|
+
async def create_plan(
|
|
32
|
+
self, goal: str, allowed_models: list[str], defaults: dict[str, AgentRef]
|
|
33
|
+
) -> Plan:
|
|
34
|
+
user = (
|
|
35
|
+
f"GOAL:\n{goal}\n\n"
|
|
36
|
+
f"ALLOWED (agent:model): {', '.join(allowed_models)}\n"
|
|
37
|
+
f"ROLE DEFAULTS: phase orchestrator -> {defaults.get('phase')}, "
|
|
38
|
+
f"task agent -> {defaults.get('task')}\n\n"
|
|
39
|
+
"Produce the plan JSON now."
|
|
40
|
+
)
|
|
41
|
+
raw = await self.provider.complete(
|
|
42
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.PLANNER_SYSTEM, user=user,
|
|
43
|
+
json_mode=True, temperature=self.temperature, role="planner",
|
|
44
|
+
meta={"models": _models_meta(defaults)},
|
|
45
|
+
)
|
|
46
|
+
return Plan.model_validate(extract_json(raw))
|
|
47
|
+
|
|
48
|
+
async def structure_plan(
|
|
49
|
+
self, existing: str, allowed_models: list[str], defaults: dict[str, AgentRef]
|
|
50
|
+
) -> Plan:
|
|
51
|
+
"""Convert an existing free-form plan / PRD into the roundtable schema."""
|
|
52
|
+
user = (
|
|
53
|
+
f"EXISTING PLAN / PRD:\n{existing}\n\n"
|
|
54
|
+
f"ALLOWED (agent:model): {', '.join(allowed_models)}\n"
|
|
55
|
+
f"ROLE DEFAULTS: phase orchestrator -> {defaults.get('phase')}, "
|
|
56
|
+
f"task agent -> {defaults.get('task')}\n\n"
|
|
57
|
+
"Convert it into the plan JSON now."
|
|
58
|
+
)
|
|
59
|
+
raw = await self.provider.complete(
|
|
60
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.PLAN_IMPORT_SYSTEM, user=user,
|
|
61
|
+
json_mode=True, temperature=self.temperature, role="planner",
|
|
62
|
+
meta={"models": _models_meta(defaults)},
|
|
63
|
+
)
|
|
64
|
+
return Plan.model_validate(extract_json(raw))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Analyst:
|
|
68
|
+
"""Maps an existing codebase: an architecture overview, then a PRD.
|
|
69
|
+
|
|
70
|
+
Both calls take the (already size-bounded) codebase digest from ``scan``.
|
|
71
|
+
The PRD call also gets the architecture overview for grounding.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(self, provider: LLMProvider, ref: AgentRef, temperature: float = 0.2):
|
|
75
|
+
self.provider, self.ref, self.temperature = provider, ref, temperature
|
|
76
|
+
|
|
77
|
+
async def architecture(self, digest: str) -> str:
|
|
78
|
+
user = f"CODEBASE DIGEST:\n{digest}\n\nWrite the architecture overview."
|
|
79
|
+
return await self.provider.complete(
|
|
80
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.MAP_ARCH_SYSTEM, user=user,
|
|
81
|
+
temperature=self.temperature, role="map_arch", meta={"digest_bytes": len(digest)},
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def prd(self, digest: str, architecture: str) -> str:
|
|
85
|
+
user = (
|
|
86
|
+
f"CODEBASE DIGEST:\n{digest}\n\n"
|
|
87
|
+
f"ARCHITECTURE OVERVIEW:\n{_truncate(architecture, 8000)}\n\n"
|
|
88
|
+
"Write the reverse-engineered PRD."
|
|
89
|
+
)
|
|
90
|
+
return await self.provider.complete(
|
|
91
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.MAP_PRD_SYSTEM, user=user,
|
|
92
|
+
temperature=self.temperature, role="map_prd", meta={"digest_bytes": len(digest)},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class MainOrchestrator:
|
|
97
|
+
def __init__(self, provider: LLMProvider, ref: AgentRef, temperature: float = 0.2):
|
|
98
|
+
self.provider, self.ref, self.temperature = provider, ref, temperature
|
|
99
|
+
|
|
100
|
+
async def kickoff(self, plan: Plan) -> str:
|
|
101
|
+
roadmap = "\n".join(f"- {p.index}. {p.title}: {p.objective}" for p in plan.phases)
|
|
102
|
+
user = f"GOAL:\n{plan.goal}\n\nPHASE ROADMAP:\n{roadmap}\n\nWrite the overview."
|
|
103
|
+
return await self.provider.complete(
|
|
104
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.MAIN_KICKOFF_SYSTEM, user=user,
|
|
105
|
+
temperature=self.temperature, role="main_kickoff", meta={"goal": plan.goal},
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async def integrate_phase(self, goal: str, phase: Phase, summary: str) -> str:
|
|
109
|
+
"""Main sees ONLY the phase summary here — never raw task transcripts."""
|
|
110
|
+
user = (
|
|
111
|
+
f"GOAL:\n{goal}\n\nCOMPLETED PHASE: {phase.index}. {phase.title}\n\n"
|
|
112
|
+
f"PHASE SUMMARY:\n{_truncate(summary)}\n\nWrite the progress entry."
|
|
113
|
+
)
|
|
114
|
+
return await self.provider.complete(
|
|
115
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.MAIN_INTEGRATE_SYSTEM, user=user,
|
|
116
|
+
temperature=self.temperature, role="main_integrate",
|
|
117
|
+
meta={"goal": goal, "phase_title": phase.title},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
async def finalize(self, plan: Plan) -> str:
|
|
121
|
+
roadmap = "\n".join(f"- {p.index}. {p.title}" for p in plan.phases)
|
|
122
|
+
user = f"GOAL:\n{plan.goal}\n\nPHASES:\n{roadmap}\n\nWrite the final report."
|
|
123
|
+
return await self.provider.complete(
|
|
124
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.MAIN_FINALIZE_SYSTEM, user=user,
|
|
125
|
+
temperature=self.temperature, role="main_finalize", meta={"goal": plan.goal},
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class PhaseOrchestrator:
|
|
130
|
+
"""Instantiated fresh per phase (its context is discarded after summarize)."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, provider: LLMProvider, ref: AgentRef, temperature: float = 0.2):
|
|
133
|
+
self.provider, self.ref, self.temperature = provider, ref, temperature
|
|
134
|
+
|
|
135
|
+
async def define_task(self, goal: str, phase: Phase, task: Task, *, project_context: str = "") -> str:
|
|
136
|
+
user = (
|
|
137
|
+
f"PROJECT GOAL:\n{goal}\n\nPHASE: {phase.title} — {phase.objective}\n\n"
|
|
138
|
+
f"TASK: {task.title}\nDESCRIPTION: {task.description}\n\n"
|
|
139
|
+
"Write the work definition."
|
|
140
|
+
)
|
|
141
|
+
system = render_prompt(prompts.PHASE_DEFINE_SYSTEM, project_context=project_context)
|
|
142
|
+
return await self.provider.complete(
|
|
143
|
+
model=self.ref.model, agent=self.ref.agent, system=system, user=user,
|
|
144
|
+
temperature=self.temperature, role="phase_define",
|
|
145
|
+
meta={"task_title": task.title, "phase_title": phase.title},
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
async def replan(
|
|
149
|
+
self,
|
|
150
|
+
phase: Phase,
|
|
151
|
+
failed_task: Task,
|
|
152
|
+
failure_output: str,
|
|
153
|
+
remaining: list[Task],
|
|
154
|
+
) -> dict[str, str]:
|
|
155
|
+
"""After a task failure, return updated descriptions for remaining tasks."""
|
|
156
|
+
remaining_lines = "\n".join(f"- [{t.id}] {t.title}: {t.description}" for t in remaining)
|
|
157
|
+
user = (
|
|
158
|
+
f"PHASE: {phase.title} — {phase.objective}\n\n"
|
|
159
|
+
f"FAILED TASK: [{failed_task.id}] {failed_task.title}\n"
|
|
160
|
+
f"FAILURE OUTPUT:\n{_truncate(failure_output, 1000)}\n\n"
|
|
161
|
+
f"REMAINING TASKS:\n{remaining_lines}\n\n"
|
|
162
|
+
"Return a JSON object mapping task_id to updated description for tasks "
|
|
163
|
+
"that need adjustment. Return {{}} if no changes are needed."
|
|
164
|
+
)
|
|
165
|
+
raw = await self.provider.complete(
|
|
166
|
+
model=self.ref.model, agent=self.ref.agent,
|
|
167
|
+
system=prompts.PHASE_REPLAN_SYSTEM, user=user,
|
|
168
|
+
json_mode=True, temperature=self.temperature, role="phase_replan",
|
|
169
|
+
meta={"phase_title": phase.title, "failed_task_id": failed_task.id},
|
|
170
|
+
)
|
|
171
|
+
try:
|
|
172
|
+
result = extract_json(raw)
|
|
173
|
+
except ValueError:
|
|
174
|
+
return {}
|
|
175
|
+
if not isinstance(result, dict): # a model may return a list; ignore it
|
|
176
|
+
return {}
|
|
177
|
+
return {k: v for k, v in result.items() if isinstance(k, str) and isinstance(v, str)}
|
|
178
|
+
|
|
179
|
+
async def summarize(self, phase: Phase, results: list[tuple[Task, str]]) -> str:
|
|
180
|
+
body = "\n\n".join(
|
|
181
|
+
f"### {t.title} [{t.id}]\n{_truncate(r, 1500)}" for t, r in results
|
|
182
|
+
)
|
|
183
|
+
user = (
|
|
184
|
+
f"PHASE: {phase.title} — {phase.objective}\n\n"
|
|
185
|
+
f"TASK RESULTS:\n{body}\n\nWrite the phase summary for the main orchestrator."
|
|
186
|
+
)
|
|
187
|
+
return await self.provider.complete(
|
|
188
|
+
model=self.ref.model, agent=self.ref.agent, system=prompts.PHASE_SUMMARY_SYSTEM, user=user,
|
|
189
|
+
temperature=self.temperature, role="phase_summary",
|
|
190
|
+
meta={"phase_title": phase.title, "task_count": len(results)},
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TaskAgent:
|
|
195
|
+
def __init__(self, provider: LLMProvider, ref: AgentRef, temperature: float = 0.2):
|
|
196
|
+
self.provider, self.ref, self.temperature = provider, ref, temperature
|
|
197
|
+
|
|
198
|
+
async def execute(
|
|
199
|
+
self, goal: str, phase: Phase, task: Task, task_def: str, deps: list[tuple[Task, str]],
|
|
200
|
+
*, project_context: str = "", on_output: Callable[[str], None] | None = None,
|
|
201
|
+
) -> str:
|
|
202
|
+
deps_ctx = (
|
|
203
|
+
"\n\n".join(f"### Dependency {t.title} [{t.id}]\n{_truncate(r, 1200)}" for t, r in deps)
|
|
204
|
+
if deps
|
|
205
|
+
else "(none)"
|
|
206
|
+
)
|
|
207
|
+
user = (
|
|
208
|
+
f"PROJECT GOAL:\n{goal}\n\nPHASE: {phase.title}\n\n"
|
|
209
|
+
f"WORK DEFINITION:\n{task_def}\n\nUPSTREAM RESULTS:\n{deps_ctx}\n\n"
|
|
210
|
+
"Execute the task and produce the deliverable."
|
|
211
|
+
)
|
|
212
|
+
system = render_prompt(prompts.TASK_EXEC_SYSTEM, project_context=project_context)
|
|
213
|
+
return await self.provider.complete(
|
|
214
|
+
model=self.ref.model, agent=self.ref.agent, system=system, user=user,
|
|
215
|
+
temperature=self.temperature, role="task_exec",
|
|
216
|
+
meta={"task_title": task.title, "phase_title": phase.title},
|
|
217
|
+
on_output=on_output,
|
|
218
|
+
)
|