lm-deluge 0.0.67__py3-none-any.whl → 0.0.90__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.
Potentially problematic release.
This version of lm-deluge might be problematic. Click here for more details.
- lm_deluge/__init__.py +1 -2
- lm_deluge/api_requests/anthropic.py +117 -22
- lm_deluge/api_requests/base.py +84 -11
- lm_deluge/api_requests/bedrock.py +30 -6
- lm_deluge/api_requests/chat_reasoning.py +4 -0
- lm_deluge/api_requests/gemini.py +166 -20
- lm_deluge/api_requests/openai.py +145 -25
- lm_deluge/batches.py +15 -45
- lm_deluge/client.py +309 -50
- lm_deluge/config.py +15 -3
- lm_deluge/models/__init__.py +14 -1
- lm_deluge/models/anthropic.py +29 -14
- lm_deluge/models/arcee.py +16 -0
- lm_deluge/models/deepseek.py +36 -4
- lm_deluge/models/google.py +42 -0
- lm_deluge/models/grok.py +24 -0
- lm_deluge/models/kimi.py +36 -0
- lm_deluge/models/minimax.py +18 -0
- lm_deluge/models/openai.py +100 -0
- lm_deluge/models/openrouter.py +133 -7
- lm_deluge/models/together.py +11 -0
- lm_deluge/models/zai.py +50 -0
- lm_deluge/pipelines/gepa/__init__.py +95 -0
- lm_deluge/pipelines/gepa/core.py +354 -0
- lm_deluge/pipelines/gepa/docs/samples.py +705 -0
- lm_deluge/pipelines/gepa/examples/01_synthetic_keywords.py +140 -0
- lm_deluge/pipelines/gepa/examples/02_gsm8k_math.py +261 -0
- lm_deluge/pipelines/gepa/examples/03_hotpotqa_multihop.py +300 -0
- lm_deluge/pipelines/gepa/examples/04_batch_classification.py +271 -0
- lm_deluge/pipelines/gepa/examples/simple_qa.py +129 -0
- lm_deluge/pipelines/gepa/optimizer.py +435 -0
- lm_deluge/pipelines/gepa/proposer.py +235 -0
- lm_deluge/pipelines/gepa/util.py +165 -0
- lm_deluge/{llm_tools → pipelines}/score.py +2 -2
- lm_deluge/{llm_tools → pipelines}/translate.py +5 -3
- lm_deluge/prompt.py +537 -88
- lm_deluge/request_context.py +7 -2
- lm_deluge/server/__init__.py +24 -0
- lm_deluge/server/__main__.py +144 -0
- lm_deluge/server/adapters.py +369 -0
- lm_deluge/server/app.py +388 -0
- lm_deluge/server/auth.py +71 -0
- lm_deluge/server/model_policy.py +215 -0
- lm_deluge/server/models_anthropic.py +172 -0
- lm_deluge/server/models_openai.py +175 -0
- lm_deluge/tool/__init__.py +1130 -0
- lm_deluge/tool/builtin/anthropic/__init__.py +300 -0
- lm_deluge/tool/builtin/anthropic/bash.py +0 -0
- lm_deluge/tool/builtin/anthropic/computer_use.py +0 -0
- lm_deluge/tool/builtin/gemini.py +59 -0
- lm_deluge/tool/builtin/openai.py +74 -0
- lm_deluge/tool/cua/__init__.py +173 -0
- lm_deluge/tool/cua/actions.py +148 -0
- lm_deluge/tool/cua/base.py +27 -0
- lm_deluge/tool/cua/batch.py +215 -0
- lm_deluge/tool/cua/converters.py +466 -0
- lm_deluge/tool/cua/kernel.py +702 -0
- lm_deluge/tool/cua/trycua.py +989 -0
- lm_deluge/tool/prefab/__init__.py +45 -0
- lm_deluge/tool/prefab/batch_tool.py +156 -0
- lm_deluge/tool/prefab/docs.py +1119 -0
- lm_deluge/tool/prefab/email.py +294 -0
- lm_deluge/tool/prefab/filesystem.py +1711 -0
- lm_deluge/tool/prefab/full_text_search/__init__.py +285 -0
- lm_deluge/tool/prefab/full_text_search/tantivy_index.py +396 -0
- lm_deluge/tool/prefab/memory.py +458 -0
- lm_deluge/tool/prefab/otc/__init__.py +165 -0
- lm_deluge/tool/prefab/otc/executor.py +281 -0
- lm_deluge/tool/prefab/otc/parse.py +188 -0
- lm_deluge/tool/prefab/random.py +212 -0
- lm_deluge/tool/prefab/rlm/__init__.py +296 -0
- lm_deluge/tool/prefab/rlm/executor.py +349 -0
- lm_deluge/tool/prefab/rlm/parse.py +144 -0
- lm_deluge/tool/prefab/sandbox/__init__.py +19 -0
- lm_deluge/tool/prefab/sandbox/daytona_sandbox.py +483 -0
- lm_deluge/tool/prefab/sandbox/docker_sandbox.py +609 -0
- lm_deluge/tool/prefab/sandbox/fargate_sandbox.py +546 -0
- lm_deluge/tool/prefab/sandbox/modal_sandbox.py +469 -0
- lm_deluge/tool/prefab/sandbox/seatbelt_sandbox.py +827 -0
- lm_deluge/tool/prefab/sheets.py +385 -0
- lm_deluge/tool/prefab/skills.py +0 -0
- lm_deluge/tool/prefab/subagents.py +233 -0
- lm_deluge/tool/prefab/todos.py +342 -0
- lm_deluge/tool/prefab/tool_search.py +169 -0
- lm_deluge/tool/prefab/web_search.py +199 -0
- lm_deluge/tracker.py +16 -13
- lm_deluge/util/schema.py +412 -0
- lm_deluge/warnings.py +8 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/METADATA +23 -9
- lm_deluge-0.0.90.dist-info/RECORD +132 -0
- lm_deluge/built_in_tools/anthropic/__init__.py +0 -128
- lm_deluge/built_in_tools/openai.py +0 -28
- lm_deluge/presets/cerebras.py +0 -17
- lm_deluge/presets/meta.py +0 -13
- lm_deluge/tool.py +0 -849
- lm_deluge-0.0.67.dist-info/RECORD +0 -72
- lm_deluge/{llm_tools → pipelines}/__init__.py +1 -1
- /lm_deluge/{llm_tools → pipelines}/classify.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/extract.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/locate.py +0 -0
- /lm_deluge/{llm_tools → pipelines}/ocr.py +0 -0
- /lm_deluge/{built_in_tools/anthropic/bash.py → skills/anthropic.py} +0 -0
- /lm_deluge/{built_in_tools/anthropic/computer_use.py → skills/compat.py} +0 -0
- /lm_deluge/{built_in_tools → tool/builtin}/anthropic/editor.py +0 -0
- /lm_deluge/{built_in_tools → tool/builtin}/base.py +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/WHEEL +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/licenses/LICENSE +0 -0
- {lm_deluge-0.0.67.dist-info → lm_deluge-0.0.90.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
"""Google Sheets manipulation prefab tool."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from lm_deluge.tool import Tool
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SheetsManager:
|
|
11
|
+
"""
|
|
12
|
+
A prefab tool for manipulating Google Sheets.
|
|
13
|
+
|
|
14
|
+
Provides tools to read ranges and update individual cells in a Google Sheet.
|
|
15
|
+
Outputs are formatted as LLM-friendly HTML tables with row/col attributes.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
sheet_id: The ID of the Google Sheet to manipulate
|
|
19
|
+
credentials_json: Optional. JSON string or dict containing Google service account credentials.
|
|
20
|
+
If not provided, will look for GOOGLE_SHEETS_CREDENTIALS env variable.
|
|
21
|
+
credentials_file: Optional. Path to a JSON file containing credentials.
|
|
22
|
+
Only used if credentials_json is not provided.
|
|
23
|
+
read_tool_name: Name for the read range tool (default: "sheets_read_range")
|
|
24
|
+
update_tool_name: Name for the update cell tool (default: "sheets_update_cell")
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
```python
|
|
28
|
+
# Using credentials from environment
|
|
29
|
+
manager = SheetsManager(sheet_id="your-sheet-id-here")
|
|
30
|
+
|
|
31
|
+
# Using credentials directly
|
|
32
|
+
manager = SheetsManager(
|
|
33
|
+
sheet_id="your-sheet-id-here",
|
|
34
|
+
credentials_json={"type": "service_account", ...}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Get tools
|
|
38
|
+
tools = manager.get_tools()
|
|
39
|
+
```
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
sheet_id: str,
|
|
45
|
+
*,
|
|
46
|
+
credentials_json: str | dict[str, Any] | None = None,
|
|
47
|
+
credentials_file: str | None = None,
|
|
48
|
+
list_sheets_tool_name: str = "sheets_list_sheets",
|
|
49
|
+
get_used_range_tool_name: str = "sheets_get_used_range",
|
|
50
|
+
read_tool_name: str = "sheets_read_range",
|
|
51
|
+
update_tool_name: str = "sheets_update_cell",
|
|
52
|
+
):
|
|
53
|
+
self.sheet_id = sheet_id
|
|
54
|
+
self.list_sheets_tool_name = list_sheets_tool_name
|
|
55
|
+
self.get_used_range_tool_name = get_used_range_tool_name
|
|
56
|
+
self.read_tool_name = read_tool_name
|
|
57
|
+
self.update_tool_name = update_tool_name
|
|
58
|
+
|
|
59
|
+
# Handle credentials
|
|
60
|
+
if credentials_json is not None:
|
|
61
|
+
if isinstance(credentials_json, str):
|
|
62
|
+
self.credentials = json.loads(credentials_json)
|
|
63
|
+
else:
|
|
64
|
+
self.credentials = credentials_json
|
|
65
|
+
elif credentials_file is not None:
|
|
66
|
+
with open(credentials_file, "r") as f:
|
|
67
|
+
self.credentials = json.load(f)
|
|
68
|
+
else:
|
|
69
|
+
# Try to load from environment
|
|
70
|
+
env_creds = os.environ.get("GOOGLE_SHEETS_CREDENTIALS")
|
|
71
|
+
if env_creds:
|
|
72
|
+
self.credentials = json.loads(env_creds)
|
|
73
|
+
else:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
"No credentials provided. Please provide credentials_json, "
|
|
76
|
+
"credentials_file, or set GOOGLE_SHEETS_CREDENTIALS environment variable."
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
self._service = None
|
|
80
|
+
self._tools: list[Tool] | None = None
|
|
81
|
+
|
|
82
|
+
def _get_service(self):
|
|
83
|
+
"""Lazily initialize the Google Sheets API service."""
|
|
84
|
+
if self._service is not None:
|
|
85
|
+
return self._service
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
from google.oauth2 import service_account
|
|
89
|
+
from googleapiclient.discovery import build
|
|
90
|
+
except ImportError:
|
|
91
|
+
raise ImportError(
|
|
92
|
+
"Google Sheets API dependencies not installed. "
|
|
93
|
+
"Please install with: pip install google-api-python-client google-auth"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Create credentials from service account info
|
|
97
|
+
creds = service_account.Credentials.from_service_account_info(
|
|
98
|
+
self.credentials, scopes=["https://www.googleapis.com/auth/spreadsheets"]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Build the service
|
|
102
|
+
self._service = build("sheets", "v4", credentials=creds)
|
|
103
|
+
return self._service
|
|
104
|
+
|
|
105
|
+
def _list_sheets(self) -> str:
|
|
106
|
+
"""
|
|
107
|
+
List all sheets (tabs) in the spreadsheet.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
JSON string with status and list of sheet names
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
service = self._get_service()
|
|
114
|
+
spreadsheet = (
|
|
115
|
+
service.spreadsheets()
|
|
116
|
+
.get(spreadsheetId=self.sheet_id, fields="sheets.properties")
|
|
117
|
+
.execute()
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
sheets = []
|
|
121
|
+
for sheet in spreadsheet.get("sheets", []):
|
|
122
|
+
props = sheet.get("properties", {})
|
|
123
|
+
sheets.append(
|
|
124
|
+
{
|
|
125
|
+
"name": props.get("title", ""),
|
|
126
|
+
"index": props.get("index", 0),
|
|
127
|
+
"sheetId": props.get("sheetId", 0),
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return json.dumps({"status": "success", "sheets": sheets})
|
|
132
|
+
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return json.dumps({"status": "error", "error": str(e)})
|
|
135
|
+
|
|
136
|
+
def _get_used_range(self, sheet_name: str | None = None) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Get the used range of a sheet (the bounding box of all non-empty cells).
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
sheet_name: Name of the sheet to check. If None, uses the first sheet.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
JSON string with status and the used range in A1 notation
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
service = self._get_service()
|
|
148
|
+
|
|
149
|
+
# If no sheet name provided, get the first sheet's name
|
|
150
|
+
if not sheet_name:
|
|
151
|
+
spreadsheet = (
|
|
152
|
+
service.spreadsheets()
|
|
153
|
+
.get(spreadsheetId=self.sheet_id, fields="sheets.properties.title")
|
|
154
|
+
.execute()
|
|
155
|
+
)
|
|
156
|
+
sheets = spreadsheet.get("sheets", [])
|
|
157
|
+
if not sheets:
|
|
158
|
+
return json.dumps(
|
|
159
|
+
{"status": "error", "error": "No sheets found in spreadsheet"}
|
|
160
|
+
)
|
|
161
|
+
sheet_name = sheets[0]["properties"]["title"]
|
|
162
|
+
|
|
163
|
+
# Get all values from the sheet to determine the used range
|
|
164
|
+
result = (
|
|
165
|
+
service.spreadsheets()
|
|
166
|
+
.values()
|
|
167
|
+
.get(spreadsheetId=self.sheet_id, range=f"'{sheet_name}'")
|
|
168
|
+
.execute()
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
values = result.get("values", [])
|
|
172
|
+
|
|
173
|
+
if not values:
|
|
174
|
+
return json.dumps(
|
|
175
|
+
{
|
|
176
|
+
"status": "success",
|
|
177
|
+
"sheet_name": sheet_name,
|
|
178
|
+
"used_range": None,
|
|
179
|
+
"message": "Sheet is empty",
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Find the max column across all rows
|
|
184
|
+
max_col = 0
|
|
185
|
+
for row in values:
|
|
186
|
+
if row: # Skip empty rows
|
|
187
|
+
max_col = max(max_col, len(row))
|
|
188
|
+
|
|
189
|
+
num_rows = len(values)
|
|
190
|
+
end_col = self._col_num_to_letter(max_col)
|
|
191
|
+
used_range = f"A1:{end_col}{num_rows}"
|
|
192
|
+
|
|
193
|
+
return json.dumps(
|
|
194
|
+
{
|
|
195
|
+
"status": "success",
|
|
196
|
+
"sheet_name": sheet_name,
|
|
197
|
+
"used_range": f"'{sheet_name}'!{used_range}",
|
|
198
|
+
"rows": num_rows,
|
|
199
|
+
"cols": max_col,
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
return json.dumps({"status": "error", "error": str(e)})
|
|
205
|
+
|
|
206
|
+
def _read_range(self, range_spec: str) -> str:
|
|
207
|
+
"""
|
|
208
|
+
Read a range from the Google Sheet and return as HTML table.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
range_spec: A1 notation range (e.g., "Sheet1!A1:C10" or just "A1:C10")
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
JSON string with status and HTML table data
|
|
215
|
+
"""
|
|
216
|
+
try:
|
|
217
|
+
service = self._get_service()
|
|
218
|
+
sheet = service.spreadsheets()
|
|
219
|
+
|
|
220
|
+
result = (
|
|
221
|
+
sheet.values()
|
|
222
|
+
.get(spreadsheetId=self.sheet_id, range=range_spec)
|
|
223
|
+
.execute()
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
values = result.get("values", [])
|
|
227
|
+
|
|
228
|
+
if not values:
|
|
229
|
+
return json.dumps(
|
|
230
|
+
{
|
|
231
|
+
"status": "success",
|
|
232
|
+
"message": "No data found in range",
|
|
233
|
+
"html": "<p>No data found</p>",
|
|
234
|
+
}
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Convert to HTML table with cell attribute for reference
|
|
238
|
+
html_parts = ["<table>"]
|
|
239
|
+
|
|
240
|
+
for row_idx, row in enumerate(values, start=1):
|
|
241
|
+
html_parts.append("<tr>")
|
|
242
|
+
for col_idx, cell_value in enumerate(row, start=1):
|
|
243
|
+
# Convert column index to letter (1=A, 2=B, etc.)
|
|
244
|
+
col_letter = self._col_num_to_letter(col_idx)
|
|
245
|
+
cell_ref = f"{col_letter}{row_idx}"
|
|
246
|
+
|
|
247
|
+
html_parts.append(f'<td cell="{cell_ref}">{cell_value}</td>')
|
|
248
|
+
html_parts.append("</tr>")
|
|
249
|
+
|
|
250
|
+
html_parts.append("</table>")
|
|
251
|
+
html_table = "\n".join(html_parts)
|
|
252
|
+
|
|
253
|
+
return json.dumps(
|
|
254
|
+
{"status": "success", "rows": len(values), "html": html_table}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
except Exception as e:
|
|
258
|
+
return json.dumps({"status": "error", "error": str(e)})
|
|
259
|
+
|
|
260
|
+
def _update_cell(self, cell: str, value: str) -> str:
|
|
261
|
+
"""
|
|
262
|
+
Update a single cell in the Google Sheet.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
cell: Cell reference in A1 notation (e.g., "A1", "B5", "Sheet1!C3")
|
|
266
|
+
value: The value to set in the cell
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
JSON string with status and result
|
|
270
|
+
"""
|
|
271
|
+
try:
|
|
272
|
+
service = self._get_service()
|
|
273
|
+
sheet = service.spreadsheets()
|
|
274
|
+
|
|
275
|
+
body = {"values": [[value]]}
|
|
276
|
+
|
|
277
|
+
result = (
|
|
278
|
+
sheet.values()
|
|
279
|
+
.update(
|
|
280
|
+
spreadsheetId=self.sheet_id,
|
|
281
|
+
range=cell,
|
|
282
|
+
valueInputOption="USER_ENTERED", # Parse values like formulas, numbers, dates
|
|
283
|
+
body=body,
|
|
284
|
+
)
|
|
285
|
+
.execute()
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
return json.dumps(
|
|
289
|
+
{
|
|
290
|
+
"status": "success",
|
|
291
|
+
"updated_cells": result.get("updatedCells", 0),
|
|
292
|
+
"updated_range": result.get("updatedRange", ""),
|
|
293
|
+
"message": f"Successfully updated {cell} to '{value}'",
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
except Exception as e:
|
|
298
|
+
return json.dumps({"status": "error", "error": str(e)})
|
|
299
|
+
|
|
300
|
+
@staticmethod
|
|
301
|
+
def _col_num_to_letter(n: int) -> str:
|
|
302
|
+
"""Convert column number to letter (1=A, 2=B, ..., 26=Z, 27=AA, etc.)."""
|
|
303
|
+
result = ""
|
|
304
|
+
while n > 0:
|
|
305
|
+
n -= 1
|
|
306
|
+
result = chr(65 + (n % 26)) + result
|
|
307
|
+
n //= 26
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
def get_tools(self) -> list[Tool]:
|
|
311
|
+
"""Return the list of Google Sheets tools."""
|
|
312
|
+
if self._tools is not None:
|
|
313
|
+
return self._tools
|
|
314
|
+
|
|
315
|
+
self._tools = [
|
|
316
|
+
Tool(
|
|
317
|
+
name=self.list_sheets_tool_name,
|
|
318
|
+
description=(
|
|
319
|
+
"List all sheets (tabs) in the spreadsheet. Returns the name, index, "
|
|
320
|
+
"and ID of each sheet."
|
|
321
|
+
),
|
|
322
|
+
run=self._list_sheets,
|
|
323
|
+
parameters={},
|
|
324
|
+
required=[],
|
|
325
|
+
),
|
|
326
|
+
Tool(
|
|
327
|
+
name=self.get_used_range_tool_name,
|
|
328
|
+
description=(
|
|
329
|
+
"Get the used range of a sheet (the bounding box containing all non-empty cells). "
|
|
330
|
+
"Returns the range in A1 notation (e.g., 'Sheet1'!A1:C4)."
|
|
331
|
+
),
|
|
332
|
+
run=self._get_used_range,
|
|
333
|
+
parameters={
|
|
334
|
+
"sheet_name": {
|
|
335
|
+
"type": "string",
|
|
336
|
+
"description": (
|
|
337
|
+
"Name of the sheet to check. If not provided, uses the first sheet."
|
|
338
|
+
),
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
required=[],
|
|
342
|
+
),
|
|
343
|
+
Tool(
|
|
344
|
+
name=self.read_tool_name,
|
|
345
|
+
description=(
|
|
346
|
+
"Read a range of cells from the Google Sheet and return as an HTML table. "
|
|
347
|
+
"Each cell has a 'cell' attribute with its A1 reference (e.g., cell='A1'). "
|
|
348
|
+
"Use A1 notation for the range (e.g., 'A1:C10' or 'Sheet1!A1:C10')."
|
|
349
|
+
),
|
|
350
|
+
run=self._read_range,
|
|
351
|
+
parameters={
|
|
352
|
+
"range_spec": {
|
|
353
|
+
"type": "string",
|
|
354
|
+
"description": (
|
|
355
|
+
"The range to read in A1 notation. Examples: 'A1:C10', 'Sheet1!A1:C10', "
|
|
356
|
+
"'A:A' (entire column A), '1:1' (entire row 1)"
|
|
357
|
+
),
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
required=["range_spec"],
|
|
361
|
+
),
|
|
362
|
+
Tool(
|
|
363
|
+
name=self.update_tool_name,
|
|
364
|
+
description=(
|
|
365
|
+
"Update a single cell in the Google Sheet. The value will be parsed "
|
|
366
|
+
"automatically (formulas, numbers, dates, etc.)."
|
|
367
|
+
),
|
|
368
|
+
run=self._update_cell,
|
|
369
|
+
parameters={
|
|
370
|
+
"cell": {
|
|
371
|
+
"type": "string",
|
|
372
|
+
"description": (
|
|
373
|
+
"The cell to update in A1 notation. Examples: 'A1', 'B5', 'Sheet1!C3'"
|
|
374
|
+
),
|
|
375
|
+
},
|
|
376
|
+
"value": {
|
|
377
|
+
"type": "string",
|
|
378
|
+
"description": "The value to set in the cell",
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
required=["cell", "value"],
|
|
382
|
+
),
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
return self._tools
|
|
File without changes
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from lm_deluge.api_requests.base import APIResponse
|
|
2
|
+
from lm_deluge.client import AgentLoopResponse, _LLMClient
|
|
3
|
+
from lm_deluge.prompt import Conversation, prompts_to_conversations
|
|
4
|
+
from lm_deluge.tool import Tool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SubAgentManager:
|
|
8
|
+
"""Manages subagent tasks that can be spawned by a main LLM via tool calls.
|
|
9
|
+
|
|
10
|
+
The SubAgentManager exposes tools that allow a main LLM to delegate subtasks
|
|
11
|
+
to specialized or cheaper subagent models, saving context and improving efficiency.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> manager = SubAgentManager(
|
|
15
|
+
... client=LLMClient("gpt-4o-mini"), # Subagent model
|
|
16
|
+
... tools=[search_tool, calculator_tool] # Tools available to subagents
|
|
17
|
+
... )
|
|
18
|
+
>>> main_client = LLMClient("gpt-4o") # More expensive main model
|
|
19
|
+
>>> conv = Conversation.user("Research AI and calculate market size")
|
|
20
|
+
>>> # Main model can now call manager tools to spawn subagents
|
|
21
|
+
>>> conv, resp = await main_client.run_agent_loop(
|
|
22
|
+
... conv,
|
|
23
|
+
... tools=manager.get_tools()
|
|
24
|
+
... )
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
client: _LLMClient,
|
|
30
|
+
tools: list[Tool] | None = None,
|
|
31
|
+
max_rounds: int = 5,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize the SubAgentManager.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
client: LLMClient to use for subagent tasks
|
|
37
|
+
tools: Tools available to subagents (optional)
|
|
38
|
+
max_rounds: Maximum rounds for each subagent's agent loop
|
|
39
|
+
"""
|
|
40
|
+
self.client = client
|
|
41
|
+
self.tools = tools or []
|
|
42
|
+
self.max_rounds = max_rounds
|
|
43
|
+
self.subagents: dict[int, dict] = {}
|
|
44
|
+
|
|
45
|
+
async def _start_subagent(self, task: str) -> int:
|
|
46
|
+
"""Start a subagent with the given task.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
task: The task description for the subagent
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Subagent task ID
|
|
53
|
+
"""
|
|
54
|
+
conversation = prompts_to_conversations([task])[0]
|
|
55
|
+
assert isinstance(conversation, Conversation)
|
|
56
|
+
|
|
57
|
+
# Use agent loop nowait API to start the subagent
|
|
58
|
+
task_id = self.client.start_agent_loop_nowait(
|
|
59
|
+
conversation,
|
|
60
|
+
tools=self.tools, # type: ignore
|
|
61
|
+
max_rounds=self.max_rounds,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Track the subagent
|
|
65
|
+
self.subagents[task_id] = {
|
|
66
|
+
"status": "running",
|
|
67
|
+
"conversation": None,
|
|
68
|
+
"response": None,
|
|
69
|
+
"error": None,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return task_id
|
|
73
|
+
|
|
74
|
+
def _finalize_subagent_result(
|
|
75
|
+
self, agent_id: int, result: AgentLoopResponse
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Update subagent tracking state from a finished agent loop."""
|
|
78
|
+
agent = self.subagents[agent_id]
|
|
79
|
+
agent["conversation"] = result.conversation
|
|
80
|
+
agent["response"] = result.final_response
|
|
81
|
+
|
|
82
|
+
if result.final_response.is_error:
|
|
83
|
+
agent["status"] = "error"
|
|
84
|
+
agent["error"] = result.final_response.error_message
|
|
85
|
+
return f"Error: {agent['error']}"
|
|
86
|
+
|
|
87
|
+
agent["status"] = "finished"
|
|
88
|
+
return result.final_response.completion or "Subagent finished with no output"
|
|
89
|
+
|
|
90
|
+
async def _check_subagent(self, agent_id: int) -> str:
|
|
91
|
+
"""Check the status of a subagent.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
agent_id: The subagent task ID
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Status string describing the subagent's state
|
|
98
|
+
"""
|
|
99
|
+
if agent_id not in self.subagents:
|
|
100
|
+
return f"Error: Subagent {agent_id} not found"
|
|
101
|
+
|
|
102
|
+
agent = self.subagents[agent_id]
|
|
103
|
+
status = agent["status"]
|
|
104
|
+
|
|
105
|
+
if status == "finished":
|
|
106
|
+
response: APIResponse = agent["response"]
|
|
107
|
+
return response.completion or "Subagent finished with no output"
|
|
108
|
+
elif status == "error":
|
|
109
|
+
return f"Error: {agent['error']}"
|
|
110
|
+
else:
|
|
111
|
+
# Try to check if it's done
|
|
112
|
+
try:
|
|
113
|
+
# Check if the task exists in client's results
|
|
114
|
+
stored_result = self.client._results.get(agent_id)
|
|
115
|
+
if isinstance(stored_result, AgentLoopResponse):
|
|
116
|
+
return self._finalize_subagent_result(agent_id, stored_result)
|
|
117
|
+
|
|
118
|
+
task = self.client._tasks.get(agent_id)
|
|
119
|
+
if task and task.done():
|
|
120
|
+
try:
|
|
121
|
+
task_result = task.result()
|
|
122
|
+
except Exception as e:
|
|
123
|
+
agent["status"] = "error"
|
|
124
|
+
agent["error"] = str(e)
|
|
125
|
+
return f"Error: {agent['error']}"
|
|
126
|
+
|
|
127
|
+
if isinstance(task_result, AgentLoopResponse):
|
|
128
|
+
return self._finalize_subagent_result(agent_id, task_result)
|
|
129
|
+
|
|
130
|
+
agent["status"] = "error"
|
|
131
|
+
agent["error"] = (
|
|
132
|
+
f"Unexpected task result type: {type(task_result).__name__}"
|
|
133
|
+
)
|
|
134
|
+
return f"Error: {agent['error']}"
|
|
135
|
+
|
|
136
|
+
# Still running
|
|
137
|
+
return f"Subagent {agent_id} is still running. Call this tool again to check status."
|
|
138
|
+
except Exception as e:
|
|
139
|
+
agent["status"] = "error"
|
|
140
|
+
agent["error"] = str(e)
|
|
141
|
+
return f"Error checking subagent: {e}"
|
|
142
|
+
|
|
143
|
+
async def _wait_for_subagent(self, agent_id: int) -> str:
|
|
144
|
+
"""Wait for a subagent to complete and return its output.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
agent_id: The subagent task ID
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The subagent's final output
|
|
151
|
+
"""
|
|
152
|
+
if agent_id not in self.subagents:
|
|
153
|
+
return f"Error: Subagent {agent_id} not found"
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
# Use the wait_for_agent_loop API
|
|
157
|
+
conversation, response = await self.client.wait_for_agent_loop(agent_id)
|
|
158
|
+
|
|
159
|
+
agent = self.subagents[agent_id]
|
|
160
|
+
agent["conversation"] = conversation
|
|
161
|
+
agent["response"] = response
|
|
162
|
+
|
|
163
|
+
if response.is_error:
|
|
164
|
+
agent["status"] = "error"
|
|
165
|
+
agent["error"] = response.error_message
|
|
166
|
+
return f"Error: {response.error_message}"
|
|
167
|
+
else:
|
|
168
|
+
agent["status"] = "finished"
|
|
169
|
+
return response.completion or "Subagent finished with no output"
|
|
170
|
+
except Exception as e:
|
|
171
|
+
agent = self.subagents[agent_id]
|
|
172
|
+
agent["status"] = "error"
|
|
173
|
+
agent["error"] = str(e)
|
|
174
|
+
return f"Error waiting for subagent: {e}"
|
|
175
|
+
|
|
176
|
+
def get_tools(self) -> list[Tool]:
|
|
177
|
+
"""Get the tools that allow a main LLM to control subagents.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of Tool objects for starting, checking, and waiting for subagents
|
|
181
|
+
"""
|
|
182
|
+
start_tool = Tool(
|
|
183
|
+
name="start_subagent",
|
|
184
|
+
description=(
|
|
185
|
+
"Start a subagent to work on a subtask independently. "
|
|
186
|
+
"Use this to delegate complex subtasks or when you need to save context. "
|
|
187
|
+
"Returns the subagent's task ID which can be used to check its status."
|
|
188
|
+
),
|
|
189
|
+
run=self._start_subagent,
|
|
190
|
+
parameters={
|
|
191
|
+
"task": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "The task description for the subagent to work on",
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
required=["task"],
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
check_tool = Tool(
|
|
200
|
+
name="check_subagent",
|
|
201
|
+
description=(
|
|
202
|
+
"Check the status and output of a running subagent. "
|
|
203
|
+
"If the subagent is still running, you'll be told to check again later. "
|
|
204
|
+
"If finished, returns the subagent's final output."
|
|
205
|
+
),
|
|
206
|
+
run=self._check_subagent,
|
|
207
|
+
parameters={
|
|
208
|
+
"agent_id": {
|
|
209
|
+
"type": "integer",
|
|
210
|
+
"description": "The task ID of the subagent to check",
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
required=["agent_id"],
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
wait_tool = Tool(
|
|
217
|
+
name="wait_for_subagent",
|
|
218
|
+
description=(
|
|
219
|
+
"Wait for a subagent to complete and return its output. "
|
|
220
|
+
"This will block until the subagent finishes. "
|
|
221
|
+
"Use check_subagent if you want to do other work while waiting."
|
|
222
|
+
),
|
|
223
|
+
run=self._wait_for_subagent,
|
|
224
|
+
parameters={
|
|
225
|
+
"agent_id": {
|
|
226
|
+
"type": "integer",
|
|
227
|
+
"description": "The task ID of the subagent to wait for",
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
required=["agent_id"],
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return [start_tool, check_tool, wait_tool]
|