flowly-code 1.0.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.
- flowly_code/__init__.py +30 -0
- flowly_code/__main__.py +8 -0
- flowly_code/activity/__init__.py +1 -0
- flowly_code/activity/bus.py +91 -0
- flowly_code/activity/events.py +40 -0
- flowly_code/agent/__init__.py +8 -0
- flowly_code/agent/context.py +485 -0
- flowly_code/agent/loop.py +1349 -0
- flowly_code/agent/memory.py +109 -0
- flowly_code/agent/skills.py +259 -0
- flowly_code/agent/subagent.py +249 -0
- flowly_code/agent/tools/__init__.py +6 -0
- flowly_code/agent/tools/base.py +55 -0
- flowly_code/agent/tools/delegate.py +194 -0
- flowly_code/agent/tools/dispatch.py +840 -0
- flowly_code/agent/tools/docker.py +609 -0
- flowly_code/agent/tools/filesystem.py +280 -0
- flowly_code/agent/tools/mcp.py +85 -0
- flowly_code/agent/tools/message.py +235 -0
- flowly_code/agent/tools/registry.py +257 -0
- flowly_code/agent/tools/screenshot.py +444 -0
- flowly_code/agent/tools/shell.py +166 -0
- flowly_code/agent/tools/spawn.py +65 -0
- flowly_code/agent/tools/system.py +917 -0
- flowly_code/agent/tools/trello.py +420 -0
- flowly_code/agent/tools/web.py +139 -0
- flowly_code/agent/tools/x.py +399 -0
- flowly_code/bus/__init__.py +6 -0
- flowly_code/bus/events.py +37 -0
- flowly_code/bus/queue.py +81 -0
- flowly_code/channels/__init__.py +6 -0
- flowly_code/channels/base.py +121 -0
- flowly_code/channels/manager.py +135 -0
- flowly_code/channels/telegram.py +1132 -0
- flowly_code/cli/__init__.py +1 -0
- flowly_code/cli/commands.py +1831 -0
- flowly_code/cli/setup.py +1356 -0
- flowly_code/compaction/__init__.py +39 -0
- flowly_code/compaction/estimator.py +88 -0
- flowly_code/compaction/pruning.py +223 -0
- flowly_code/compaction/service.py +297 -0
- flowly_code/compaction/summarizer.py +384 -0
- flowly_code/compaction/types.py +71 -0
- flowly_code/config/__init__.py +6 -0
- flowly_code/config/loader.py +102 -0
- flowly_code/config/schema.py +324 -0
- flowly_code/exec/__init__.py +39 -0
- flowly_code/exec/approvals.py +288 -0
- flowly_code/exec/executor.py +184 -0
- flowly_code/exec/safety.py +247 -0
- flowly_code/exec/types.py +88 -0
- flowly_code/gateway/__init__.py +5 -0
- flowly_code/gateway/server.py +103 -0
- flowly_code/heartbeat/__init__.py +5 -0
- flowly_code/heartbeat/service.py +130 -0
- flowly_code/multiagent/README.md +248 -0
- flowly_code/multiagent/__init__.py +1 -0
- flowly_code/multiagent/invoke.py +210 -0
- flowly_code/multiagent/orchestrator.py +156 -0
- flowly_code/multiagent/router.py +156 -0
- flowly_code/multiagent/setup.py +171 -0
- flowly_code/pairing/__init__.py +21 -0
- flowly_code/pairing/store.py +343 -0
- flowly_code/providers/__init__.py +6 -0
- flowly_code/providers/base.py +69 -0
- flowly_code/providers/litellm_provider.py +178 -0
- flowly_code/providers/transcription.py +64 -0
- flowly_code/session/__init__.py +5 -0
- flowly_code/session/manager.py +249 -0
- flowly_code/skills/README.md +24 -0
- flowly_code/skills/compact/SKILL.md +27 -0
- flowly_code/skills/github/SKILL.md +48 -0
- flowly_code/skills/skill-creator/SKILL.md +371 -0
- flowly_code/skills/summarize/SKILL.md +67 -0
- flowly_code/skills/tmux/SKILL.md +121 -0
- flowly_code/skills/tmux/scripts/find-sessions.sh +112 -0
- flowly_code/skills/tmux/scripts/wait-for-text.sh +83 -0
- flowly_code/skills/weather/SKILL.md +49 -0
- flowly_code/utils/__init__.py +5 -0
- flowly_code/utils/helpers.py +91 -0
- flowly_code-1.0.0.dist-info/METADATA +724 -0
- flowly_code-1.0.0.dist-info/RECORD +86 -0
- flowly_code-1.0.0.dist-info/WHEEL +4 -0
- flowly_code-1.0.0.dist-info/entry_points.txt +2 -0
- flowly_code-1.0.0.dist-info/licenses/LICENSE +191 -0
- flowly_code-1.0.0.dist-info/licenses/NOTICE +74 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
"""Dispatch App integration tools.
|
|
2
|
+
|
|
3
|
+
These tools allow the Flowly agent to interact with Dispatch's
|
|
4
|
+
Rust backend (Axum) via HTTP API, giving access to projects,
|
|
5
|
+
tasks, and Ralph AI workers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from flowly_code.agent.tools.base import Tool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DispatchListProjectsTool(Tool):
|
|
17
|
+
"""List all Dispatch projects."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, port: int = 8080):
|
|
20
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def name(self) -> str:
|
|
24
|
+
return "dispatch_list_projects"
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def description(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
"List all projects in Dispatch. "
|
|
30
|
+
"Returns project IDs, names, and creation dates."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def parameters(self) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {},
|
|
38
|
+
"required": [],
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async def execute(self, **kwargs: Any) -> str:
|
|
42
|
+
try:
|
|
43
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
44
|
+
resp = await client.get(f"{self.base_url}/projects")
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
body = resp.json()
|
|
47
|
+
projects = body.get("data", body)
|
|
48
|
+
|
|
49
|
+
if not projects:
|
|
50
|
+
return "No Dispatch projects found."
|
|
51
|
+
|
|
52
|
+
lines = ["Dispatch Projects:"]
|
|
53
|
+
for p in projects:
|
|
54
|
+
name = p.get("name", "Unnamed")
|
|
55
|
+
pid = p.get("id", "?")
|
|
56
|
+
lines.append(f" - {name} (id: {pid})")
|
|
57
|
+
return "\n".join(lines)
|
|
58
|
+
except httpx.ConnectError:
|
|
59
|
+
return "Error: Cannot connect to Dispatch backend. Is Dispatch running?"
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return f"Error listing projects: {e}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class DispatchGetProjectTool(Tool):
|
|
65
|
+
"""Get detailed info about a single project."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, port: int = 8080):
|
|
68
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def name(self) -> str:
|
|
72
|
+
return "dispatch_get_project"
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def description(self) -> str:
|
|
76
|
+
return "Get detailed information about a specific Dispatch project by ID."
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def parameters(self) -> dict[str, Any]:
|
|
80
|
+
return {
|
|
81
|
+
"type": "object",
|
|
82
|
+
"properties": {
|
|
83
|
+
"project_id": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "The Dispatch project UUID",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
"required": ["project_id"],
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async def execute(self, project_id: str, **kwargs: Any) -> str:
|
|
92
|
+
try:
|
|
93
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
94
|
+
resp = await client.get(f"{self.base_url}/projects/{project_id}")
|
|
95
|
+
resp.raise_for_status()
|
|
96
|
+
body = resp.json()
|
|
97
|
+
project = body.get("data", body)
|
|
98
|
+
return json.dumps(project, indent=2, default=str)
|
|
99
|
+
except httpx.HTTPStatusError as e:
|
|
100
|
+
if e.response.status_code == 404:
|
|
101
|
+
return f"Project not found: {project_id}"
|
|
102
|
+
return f"Error: {e}"
|
|
103
|
+
except httpx.ConnectError:
|
|
104
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return f"Error getting project: {e}"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DispatchListTasksTool(Tool):
|
|
110
|
+
"""List tasks (kanban items) for a project."""
|
|
111
|
+
|
|
112
|
+
def __init__(self, port: int = 8080):
|
|
113
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def name(self) -> str:
|
|
117
|
+
return "dispatch_list_tasks"
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def description(self) -> str:
|
|
121
|
+
return (
|
|
122
|
+
"List tasks in a Dispatch project. "
|
|
123
|
+
"Shows task title, status, and description. "
|
|
124
|
+
"IMPORTANT: Use a valid project_id from dispatch_list_projects."
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def parameters(self) -> dict[str, Any]:
|
|
129
|
+
return {
|
|
130
|
+
"type": "object",
|
|
131
|
+
"properties": {
|
|
132
|
+
"project_id": {
|
|
133
|
+
"type": "string",
|
|
134
|
+
"description": "The Dispatch project UUID",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
"required": ["project_id"],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async def execute(self, project_id: str, **kwargs: Any) -> str:
|
|
141
|
+
try:
|
|
142
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
143
|
+
resp = await client.get(
|
|
144
|
+
f"{self.base_url}/tasks",
|
|
145
|
+
params={"project_id": project_id},
|
|
146
|
+
)
|
|
147
|
+
resp.raise_for_status()
|
|
148
|
+
body = resp.json()
|
|
149
|
+
tasks = body.get("data", body)
|
|
150
|
+
|
|
151
|
+
if not tasks:
|
|
152
|
+
return f"No tasks found for project {project_id}."
|
|
153
|
+
|
|
154
|
+
lines = [f"Tasks for project {project_id}:"]
|
|
155
|
+
for t in tasks:
|
|
156
|
+
status = t.get("status", "unknown")
|
|
157
|
+
title = t.get("title", "Untitled")
|
|
158
|
+
tid = t.get("id", "?")
|
|
159
|
+
desc = t.get("description") or ""
|
|
160
|
+
desc_preview = (desc[:80] + "...") if len(desc) > 80 else desc
|
|
161
|
+
lines.append(f" [{status}] {title} (id: {tid})")
|
|
162
|
+
if desc_preview:
|
|
163
|
+
lines.append(f" {desc_preview}")
|
|
164
|
+
return "\n".join(lines)
|
|
165
|
+
except httpx.ConnectError:
|
|
166
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
167
|
+
except Exception as e:
|
|
168
|
+
return f"Error listing tasks: {e}"
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class DispatchCreateTaskTool(Tool):
|
|
172
|
+
"""Create a new task in a Dispatch project."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, port: int = 8080):
|
|
175
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
176
|
+
|
|
177
|
+
@property
|
|
178
|
+
def name(self) -> str:
|
|
179
|
+
return "dispatch_create_task"
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def description(self) -> str:
|
|
183
|
+
return (
|
|
184
|
+
"Create a new task in a Dispatch project's task board. "
|
|
185
|
+
"IMPORTANT: You must use a valid project_id from dispatch_list_projects. "
|
|
186
|
+
"Do NOT guess or make up project IDs."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def parameters(self) -> dict[str, Any]:
|
|
191
|
+
return {
|
|
192
|
+
"type": "object",
|
|
193
|
+
"properties": {
|
|
194
|
+
"project_id": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"description": "The Dispatch project UUID",
|
|
197
|
+
},
|
|
198
|
+
"title": {
|
|
199
|
+
"type": "string",
|
|
200
|
+
"description": "Task title",
|
|
201
|
+
},
|
|
202
|
+
"description": {
|
|
203
|
+
"type": "string",
|
|
204
|
+
"description": "Task description (markdown supported)",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
"required": ["project_id", "title"],
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async def execute(
|
|
211
|
+
self,
|
|
212
|
+
project_id: str,
|
|
213
|
+
title: str,
|
|
214
|
+
description: str = "",
|
|
215
|
+
**kwargs: Any,
|
|
216
|
+
) -> str:
|
|
217
|
+
try:
|
|
218
|
+
payload: dict[str, Any] = {
|
|
219
|
+
"project_id": project_id,
|
|
220
|
+
"title": title,
|
|
221
|
+
}
|
|
222
|
+
if description:
|
|
223
|
+
payload["description"] = description
|
|
224
|
+
|
|
225
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
226
|
+
resp = await client.post(
|
|
227
|
+
f"{self.base_url}/tasks",
|
|
228
|
+
json=payload,
|
|
229
|
+
)
|
|
230
|
+
resp.raise_for_status()
|
|
231
|
+
body = resp.json()
|
|
232
|
+
task = body.get("data", body)
|
|
233
|
+
tid = task.get("id", "?")
|
|
234
|
+
return f"Created task '{title}' (id: {tid}) in project {project_id}."
|
|
235
|
+
except httpx.ConnectError:
|
|
236
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
237
|
+
except Exception as e:
|
|
238
|
+
return f"Error creating task: {e}"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class DispatchUpdateTaskTool(Tool):
|
|
242
|
+
"""Update an existing task."""
|
|
243
|
+
|
|
244
|
+
def __init__(self, port: int = 8080):
|
|
245
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def name(self) -> str:
|
|
249
|
+
return "dispatch_update_task"
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def description(self) -> str:
|
|
253
|
+
return (
|
|
254
|
+
"Update an existing task in Dispatch. "
|
|
255
|
+
"Can change title, description, or status."
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
@property
|
|
259
|
+
def parameters(self) -> dict[str, Any]:
|
|
260
|
+
return {
|
|
261
|
+
"type": "object",
|
|
262
|
+
"properties": {
|
|
263
|
+
"task_id": {
|
|
264
|
+
"type": "string",
|
|
265
|
+
"description": "The task UUID to update",
|
|
266
|
+
},
|
|
267
|
+
"title": {
|
|
268
|
+
"type": "string",
|
|
269
|
+
"description": "New title (optional)",
|
|
270
|
+
},
|
|
271
|
+
"description": {
|
|
272
|
+
"type": "string",
|
|
273
|
+
"description": "New description (optional)",
|
|
274
|
+
},
|
|
275
|
+
"status": {
|
|
276
|
+
"type": "string",
|
|
277
|
+
"description": "New status",
|
|
278
|
+
"enum": ["open", "in_progress", "in_review", "done", "cancelled"],
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
"required": ["task_id"],
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async def execute(
|
|
285
|
+
self,
|
|
286
|
+
task_id: str,
|
|
287
|
+
title: str | None = None,
|
|
288
|
+
description: str | None = None,
|
|
289
|
+
status: str | None = None,
|
|
290
|
+
**kwargs: Any,
|
|
291
|
+
) -> str:
|
|
292
|
+
try:
|
|
293
|
+
payload: dict[str, Any] = {}
|
|
294
|
+
if title is not None:
|
|
295
|
+
payload["title"] = title
|
|
296
|
+
if description is not None:
|
|
297
|
+
payload["description"] = description
|
|
298
|
+
if status is not None:
|
|
299
|
+
payload["status"] = status
|
|
300
|
+
|
|
301
|
+
if not payload:
|
|
302
|
+
return "No fields to update. Provide at least one of: title, description, status."
|
|
303
|
+
|
|
304
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
305
|
+
resp = await client.put(
|
|
306
|
+
f"{self.base_url}/tasks/{task_id}",
|
|
307
|
+
json=payload,
|
|
308
|
+
)
|
|
309
|
+
resp.raise_for_status()
|
|
310
|
+
return f"Updated task {task_id}."
|
|
311
|
+
except httpx.HTTPStatusError as e:
|
|
312
|
+
if e.response.status_code == 404:
|
|
313
|
+
return f"Task not found: {task_id}"
|
|
314
|
+
return f"Error: {e}"
|
|
315
|
+
except httpx.ConnectError:
|
|
316
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
317
|
+
except Exception as e:
|
|
318
|
+
return f"Error updating task: {e}"
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class DispatchRalphStatusTool(Tool):
|
|
322
|
+
"""Check Ralph AI worker status for a task."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, port: int = 8080):
|
|
325
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def name(self) -> str:
|
|
329
|
+
return "dispatch_ralph_status"
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def description(self) -> str:
|
|
333
|
+
return (
|
|
334
|
+
"Check the status of Ralph AI coding workspaces for a task. "
|
|
335
|
+
"Shows whether workers are running, completed, or failed."
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def parameters(self) -> dict[str, Any]:
|
|
340
|
+
return {
|
|
341
|
+
"type": "object",
|
|
342
|
+
"properties": {
|
|
343
|
+
"task_id": {
|
|
344
|
+
"type": "string",
|
|
345
|
+
"description": "The task UUID to check Ralph status for",
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
"required": ["task_id"],
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async def execute(self, task_id: str, **kwargs: Any) -> str:
|
|
352
|
+
try:
|
|
353
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
354
|
+
resp = await client.get(
|
|
355
|
+
f"{self.base_url}/tasks/{task_id}/workspaces",
|
|
356
|
+
)
|
|
357
|
+
resp.raise_for_status()
|
|
358
|
+
body = resp.json()
|
|
359
|
+
workspaces = body.get("data", body)
|
|
360
|
+
|
|
361
|
+
if not workspaces:
|
|
362
|
+
return f"No Ralph workspaces found for task {task_id}."
|
|
363
|
+
|
|
364
|
+
lines = [f"Ralph workspaces for task {task_id}:"]
|
|
365
|
+
for ws in workspaces:
|
|
366
|
+
ws_id = ws.get("id", "?")
|
|
367
|
+
status = ws.get("status", "unknown")
|
|
368
|
+
lines.append(f" - Workspace {ws_id}: {status}")
|
|
369
|
+
return "\n".join(lines)
|
|
370
|
+
except httpx.HTTPStatusError as e:
|
|
371
|
+
if e.response.status_code == 404:
|
|
372
|
+
return f"Task not found: {task_id}"
|
|
373
|
+
return f"Error: {e}"
|
|
374
|
+
except httpx.ConnectError:
|
|
375
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
376
|
+
except Exception as e:
|
|
377
|
+
return f"Error checking Ralph status: {e}"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class DispatchStartRalphTool(Tool):
|
|
381
|
+
"""Start a Ralph AI coding session for a task."""
|
|
382
|
+
|
|
383
|
+
def __init__(self, port: int = 8080):
|
|
384
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def name(self) -> str:
|
|
388
|
+
return "dispatch_start_ralph"
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def description(self) -> str:
|
|
392
|
+
return (
|
|
393
|
+
"Start a Ralph AI coding session for a project. "
|
|
394
|
+
"Ralph is an autonomous AI coding agent that works on tasks."
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
@property
|
|
398
|
+
def parameters(self) -> dict[str, Any]:
|
|
399
|
+
return {
|
|
400
|
+
"type": "object",
|
|
401
|
+
"properties": {
|
|
402
|
+
"project_id": {
|
|
403
|
+
"type": "string",
|
|
404
|
+
"description": "The Dispatch project UUID",
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
"required": ["project_id"],
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async def execute(self, project_id: str, **kwargs: Any) -> str:
|
|
411
|
+
try:
|
|
412
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
413
|
+
resp = await client.post(
|
|
414
|
+
f"{self.base_url}/projects/{project_id}/ralph/start",
|
|
415
|
+
)
|
|
416
|
+
resp.raise_for_status()
|
|
417
|
+
return f"Ralph session started for project {project_id}."
|
|
418
|
+
except httpx.HTTPStatusError as e:
|
|
419
|
+
if e.response.status_code == 404:
|
|
420
|
+
return f"Project not found: {project_id}"
|
|
421
|
+
return f"Error: {e}"
|
|
422
|
+
except httpx.ConnectError:
|
|
423
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
424
|
+
except Exception as e:
|
|
425
|
+
return f"Error starting Ralph: {e}"
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class DispatchDeleteTaskTool(Tool):
|
|
429
|
+
"""Delete a task from a Dispatch project."""
|
|
430
|
+
|
|
431
|
+
def __init__(self, port: int = 8080):
|
|
432
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def name(self) -> str:
|
|
436
|
+
return "dispatch_delete_task"
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def description(self) -> str:
|
|
440
|
+
return (
|
|
441
|
+
"Delete a task from Dispatch. "
|
|
442
|
+
"IMPORTANT: Use a valid task_id from dispatch_list_tasks."
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def parameters(self) -> dict[str, Any]:
|
|
447
|
+
return {
|
|
448
|
+
"type": "object",
|
|
449
|
+
"properties": {
|
|
450
|
+
"task_id": {
|
|
451
|
+
"type": "string",
|
|
452
|
+
"description": "The task UUID to delete",
|
|
453
|
+
},
|
|
454
|
+
},
|
|
455
|
+
"required": ["task_id"],
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async def execute(self, task_id: str, **kwargs: Any) -> str:
|
|
459
|
+
try:
|
|
460
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
461
|
+
resp = await client.delete(f"{self.base_url}/tasks/{task_id}")
|
|
462
|
+
resp.raise_for_status()
|
|
463
|
+
return f"Deleted task {task_id}."
|
|
464
|
+
except httpx.HTTPStatusError as e:
|
|
465
|
+
if e.response.status_code == 404:
|
|
466
|
+
return f"Task not found: {task_id}"
|
|
467
|
+
return f"Error: {e}"
|
|
468
|
+
except httpx.ConnectError:
|
|
469
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
470
|
+
except Exception as e:
|
|
471
|
+
return f"Error deleting task: {e}"
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
class DispatchGetTaskTool(Tool):
|
|
475
|
+
"""Get detailed info about a single task."""
|
|
476
|
+
|
|
477
|
+
def __init__(self, port: int = 8080):
|
|
478
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
479
|
+
|
|
480
|
+
@property
|
|
481
|
+
def name(self) -> str:
|
|
482
|
+
return "dispatch_get_task"
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def description(self) -> str:
|
|
486
|
+
return (
|
|
487
|
+
"Get detailed information about a specific task by ID. "
|
|
488
|
+
"Returns title, description, status, priority, labels, and more."
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
@property
|
|
492
|
+
def parameters(self) -> dict[str, Any]:
|
|
493
|
+
return {
|
|
494
|
+
"type": "object",
|
|
495
|
+
"properties": {
|
|
496
|
+
"task_id": {
|
|
497
|
+
"type": "string",
|
|
498
|
+
"description": "The task UUID",
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
"required": ["task_id"],
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async def execute(self, task_id: str, **kwargs: Any) -> str:
|
|
505
|
+
try:
|
|
506
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
507
|
+
resp = await client.get(f"{self.base_url}/tasks/{task_id}")
|
|
508
|
+
resp.raise_for_status()
|
|
509
|
+
body = resp.json()
|
|
510
|
+
task = body.get("data", body)
|
|
511
|
+
return json.dumps(task, indent=2, default=str)
|
|
512
|
+
except httpx.HTTPStatusError as e:
|
|
513
|
+
if e.response.status_code == 404:
|
|
514
|
+
return f"Task not found: {task_id}"
|
|
515
|
+
return f"Error: {e}"
|
|
516
|
+
except httpx.ConnectError:
|
|
517
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
518
|
+
except Exception as e:
|
|
519
|
+
return f"Error getting task: {e}"
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class DispatchListTaskAttemptsTool(Tool):
|
|
523
|
+
"""List task attempts (workspaces) for a task."""
|
|
524
|
+
|
|
525
|
+
def __init__(self, port: int = 8080):
|
|
526
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
527
|
+
|
|
528
|
+
@property
|
|
529
|
+
def name(self) -> str:
|
|
530
|
+
return "dispatch_list_task_attempts"
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def description(self) -> str:
|
|
534
|
+
return (
|
|
535
|
+
"List all task attempts (workspaces) for a task. "
|
|
536
|
+
"Shows attempt IDs, status, branch names, and executor info. "
|
|
537
|
+
"Use the attempt ID with dispatch_stop_ralph to stop a running session."
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
@property
|
|
541
|
+
def parameters(self) -> dict[str, Any]:
|
|
542
|
+
return {
|
|
543
|
+
"type": "object",
|
|
544
|
+
"properties": {
|
|
545
|
+
"task_id": {
|
|
546
|
+
"type": "string",
|
|
547
|
+
"description": "The task UUID",
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
"required": ["task_id"],
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async def execute(self, task_id: str, **kwargs: Any) -> str:
|
|
554
|
+
try:
|
|
555
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
556
|
+
resp = await client.get(
|
|
557
|
+
f"{self.base_url}/task-attempts",
|
|
558
|
+
params={"task_id": task_id},
|
|
559
|
+
)
|
|
560
|
+
resp.raise_for_status()
|
|
561
|
+
body = resp.json()
|
|
562
|
+
attempts = body.get("data", body)
|
|
563
|
+
|
|
564
|
+
if not attempts:
|
|
565
|
+
return f"No task attempts found for task {task_id}."
|
|
566
|
+
|
|
567
|
+
lines = [f"Task attempts for {task_id}:"]
|
|
568
|
+
for a in attempts:
|
|
569
|
+
aid = a.get("id", "?")
|
|
570
|
+
status = a.get("status", "unknown")
|
|
571
|
+
branch = a.get("branch", "")
|
|
572
|
+
lines.append(f" - {aid}: [{status}] branch: {branch}")
|
|
573
|
+
return "\n".join(lines)
|
|
574
|
+
except httpx.ConnectError:
|
|
575
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
576
|
+
except Exception as e:
|
|
577
|
+
return f"Error listing task attempts: {e}"
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
class DispatchStartRalphSessionTool(Tool):
|
|
581
|
+
"""Start a Ralph AI coding session on a workspace."""
|
|
582
|
+
|
|
583
|
+
def __init__(self, port: int = 8080):
|
|
584
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def name(self) -> str:
|
|
588
|
+
return "dispatch_start_ralph_session"
|
|
589
|
+
|
|
590
|
+
@property
|
|
591
|
+
def description(self) -> str:
|
|
592
|
+
return (
|
|
593
|
+
"Start a Ralph AI coding session on a specific task attempt (workspace). "
|
|
594
|
+
"First use dispatch_list_task_attempts to get the attempt ID. "
|
|
595
|
+
"Ralph is an autonomous AI coding agent that works on the task."
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
@property
|
|
599
|
+
def parameters(self) -> dict[str, Any]:
|
|
600
|
+
return {
|
|
601
|
+
"type": "object",
|
|
602
|
+
"properties": {
|
|
603
|
+
"attempt_id": {
|
|
604
|
+
"type": "string",
|
|
605
|
+
"description": "The task attempt (workspace) UUID",
|
|
606
|
+
},
|
|
607
|
+
},
|
|
608
|
+
"required": ["attempt_id"],
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async def execute(self, attempt_id: str, **kwargs: Any) -> str:
|
|
612
|
+
try:
|
|
613
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
614
|
+
resp = await client.post(
|
|
615
|
+
f"{self.base_url}/task-attempts/{attempt_id}/ralph/start",
|
|
616
|
+
)
|
|
617
|
+
resp.raise_for_status()
|
|
618
|
+
return f"Ralph session started for workspace {attempt_id}."
|
|
619
|
+
except httpx.HTTPStatusError as e:
|
|
620
|
+
if e.response.status_code == 404:
|
|
621
|
+
return f"Workspace not found: {attempt_id}"
|
|
622
|
+
return f"Error: {e}"
|
|
623
|
+
except httpx.ConnectError:
|
|
624
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
625
|
+
except Exception as e:
|
|
626
|
+
return f"Error starting Ralph session: {e}"
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class DispatchStopRalphSessionTool(Tool):
|
|
630
|
+
"""Stop a Ralph AI coding session."""
|
|
631
|
+
|
|
632
|
+
def __init__(self, port: int = 8080):
|
|
633
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
634
|
+
|
|
635
|
+
@property
|
|
636
|
+
def name(self) -> str:
|
|
637
|
+
return "dispatch_stop_ralph_session"
|
|
638
|
+
|
|
639
|
+
@property
|
|
640
|
+
def description(self) -> str:
|
|
641
|
+
return (
|
|
642
|
+
"Stop a running Ralph AI coding session on a task attempt. "
|
|
643
|
+
"Use dispatch_list_task_attempts to find the attempt ID."
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
@property
|
|
647
|
+
def parameters(self) -> dict[str, Any]:
|
|
648
|
+
return {
|
|
649
|
+
"type": "object",
|
|
650
|
+
"properties": {
|
|
651
|
+
"attempt_id": {
|
|
652
|
+
"type": "string",
|
|
653
|
+
"description": "The task attempt (workspace) UUID to stop",
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
"required": ["attempt_id"],
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async def execute(self, attempt_id: str, **kwargs: Any) -> str:
|
|
660
|
+
try:
|
|
661
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
662
|
+
resp = await client.post(
|
|
663
|
+
f"{self.base_url}/task-attempts/{attempt_id}/stop",
|
|
664
|
+
)
|
|
665
|
+
resp.raise_for_status()
|
|
666
|
+
return f"Stopped Ralph session for workspace {attempt_id}."
|
|
667
|
+
except httpx.HTTPStatusError as e:
|
|
668
|
+
if e.response.status_code == 404:
|
|
669
|
+
return f"Workspace not found: {attempt_id}"
|
|
670
|
+
return f"Error: {e}"
|
|
671
|
+
except httpx.ConnectError:
|
|
672
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
673
|
+
except Exception as e:
|
|
674
|
+
return f"Error stopping Ralph session: {e}"
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class DispatchKanbanSummaryTool(Tool):
|
|
678
|
+
"""Get a kanban board summary for a project."""
|
|
679
|
+
|
|
680
|
+
def __init__(self, port: int = 8080):
|
|
681
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
682
|
+
|
|
683
|
+
@property
|
|
684
|
+
def name(self) -> str:
|
|
685
|
+
return "dispatch_kanban_summary"
|
|
686
|
+
|
|
687
|
+
@property
|
|
688
|
+
def description(self) -> str:
|
|
689
|
+
return (
|
|
690
|
+
"Get a summary of the kanban board for a project. "
|
|
691
|
+
"Shows task counts grouped by status. "
|
|
692
|
+
"IMPORTANT: Use a valid project_id from dispatch_list_projects."
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
@property
|
|
696
|
+
def parameters(self) -> dict[str, Any]:
|
|
697
|
+
return {
|
|
698
|
+
"type": "object",
|
|
699
|
+
"properties": {
|
|
700
|
+
"project_id": {
|
|
701
|
+
"type": "string",
|
|
702
|
+
"description": "The Dispatch project UUID",
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
"required": ["project_id"],
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async def execute(self, project_id: str, **kwargs: Any) -> str:
|
|
709
|
+
try:
|
|
710
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
711
|
+
resp = await client.get(
|
|
712
|
+
f"{self.base_url}/tasks",
|
|
713
|
+
params={"project_id": project_id},
|
|
714
|
+
)
|
|
715
|
+
resp.raise_for_status()
|
|
716
|
+
body = resp.json()
|
|
717
|
+
tasks = body.get("data", body)
|
|
718
|
+
|
|
719
|
+
if not tasks:
|
|
720
|
+
return f"No tasks in project {project_id}."
|
|
721
|
+
|
|
722
|
+
# Group by status
|
|
723
|
+
counts: dict[str, int] = {}
|
|
724
|
+
for t in tasks:
|
|
725
|
+
status = t.get("status", "unknown")
|
|
726
|
+
counts[status] = counts.get(status, 0) + 1
|
|
727
|
+
|
|
728
|
+
total = len(tasks)
|
|
729
|
+
lines = [f"Kanban Summary for project {project_id} ({total} tasks):"]
|
|
730
|
+
for status, count in sorted(counts.items()):
|
|
731
|
+
lines.append(f" {status}: {count}")
|
|
732
|
+
return "\n".join(lines)
|
|
733
|
+
except httpx.ConnectError:
|
|
734
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
735
|
+
except Exception as e:
|
|
736
|
+
return f"Error getting kanban summary: {e}"
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class DispatchGetRalphSessionTool(Tool):
|
|
740
|
+
"""Get full Ralph session info including iterations."""
|
|
741
|
+
|
|
742
|
+
def __init__(self, port: int = 8080):
|
|
743
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
744
|
+
|
|
745
|
+
@property
|
|
746
|
+
def name(self) -> str:
|
|
747
|
+
return "dispatch_get_ralph_session"
|
|
748
|
+
|
|
749
|
+
@property
|
|
750
|
+
def description(self) -> str:
|
|
751
|
+
return (
|
|
752
|
+
"Get detailed Ralph session info for a workspace, including all iterations. "
|
|
753
|
+
"Use dispatch_list_task_attempts to get the attempt ID first."
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
@property
|
|
757
|
+
def parameters(self) -> dict[str, Any]:
|
|
758
|
+
return {
|
|
759
|
+
"type": "object",
|
|
760
|
+
"properties": {
|
|
761
|
+
"attempt_id": {
|
|
762
|
+
"type": "string",
|
|
763
|
+
"description": "The task attempt (workspace) UUID",
|
|
764
|
+
},
|
|
765
|
+
},
|
|
766
|
+
"required": ["attempt_id"],
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
async def execute(self, attempt_id: str, **kwargs: Any) -> str:
|
|
770
|
+
try:
|
|
771
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
772
|
+
resp = await client.get(
|
|
773
|
+
f"{self.base_url}/task-attempts/{attempt_id}/ralph/full",
|
|
774
|
+
)
|
|
775
|
+
resp.raise_for_status()
|
|
776
|
+
body = resp.json()
|
|
777
|
+
session = body.get("data", body)
|
|
778
|
+
return json.dumps(session, indent=2, default=str)
|
|
779
|
+
except httpx.HTTPStatusError as e:
|
|
780
|
+
if e.response.status_code == 404:
|
|
781
|
+
return f"No Ralph session found for workspace {attempt_id}"
|
|
782
|
+
return f"Error: {e}"
|
|
783
|
+
except httpx.ConnectError:
|
|
784
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
785
|
+
except Exception as e:
|
|
786
|
+
return f"Error getting Ralph session: {e}"
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class DispatchGetRalphPrdTool(Tool):
|
|
790
|
+
"""Get Ralph PRD tasks for a project."""
|
|
791
|
+
|
|
792
|
+
def __init__(self, port: int = 8080):
|
|
793
|
+
self.base_url = f"http://127.0.0.1:{port}/api"
|
|
794
|
+
|
|
795
|
+
@property
|
|
796
|
+
def name(self) -> str:
|
|
797
|
+
return "dispatch_get_ralph_prd"
|
|
798
|
+
|
|
799
|
+
@property
|
|
800
|
+
def description(self) -> str:
|
|
801
|
+
return (
|
|
802
|
+
"Get Ralph PRD (Product Requirements Document) tasks for a project. "
|
|
803
|
+
"Shows PRD task details including title, description, and status."
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
@property
|
|
807
|
+
def parameters(self) -> dict[str, Any]:
|
|
808
|
+
return {
|
|
809
|
+
"type": "object",
|
|
810
|
+
"properties": {
|
|
811
|
+
"project_id": {
|
|
812
|
+
"type": "string",
|
|
813
|
+
"description": "The Dispatch project UUID",
|
|
814
|
+
},
|
|
815
|
+
},
|
|
816
|
+
"required": ["project_id"],
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
async def execute(self, project_id: str, **kwargs: Any) -> str:
|
|
820
|
+
try:
|
|
821
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
822
|
+
resp = await client.get(
|
|
823
|
+
f"{self.base_url}/projects/{project_id}/ralph/prd",
|
|
824
|
+
)
|
|
825
|
+
resp.raise_for_status()
|
|
826
|
+
body = resp.json()
|
|
827
|
+
prd_tasks = body.get("data", body)
|
|
828
|
+
|
|
829
|
+
if not prd_tasks:
|
|
830
|
+
return f"No PRD tasks found for project {project_id}."
|
|
831
|
+
|
|
832
|
+
return json.dumps(prd_tasks, indent=2, default=str)
|
|
833
|
+
except httpx.HTTPStatusError as e:
|
|
834
|
+
if e.response.status_code == 404:
|
|
835
|
+
return f"Project not found: {project_id}"
|
|
836
|
+
return f"Error: {e}"
|
|
837
|
+
except httpx.ConnectError:
|
|
838
|
+
return "Error: Cannot connect to Dispatch backend."
|
|
839
|
+
except Exception as e:
|
|
840
|
+
return f"Error getting PRD: {e}"
|