dslighting 1.3.9__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.
- dsat/__init__.py +3 -0
- dsat/benchmark/__init__.py +1 -0
- dsat/benchmark/benchmark.py +168 -0
- dsat/benchmark/datasci.py +291 -0
- dsat/benchmark/mle.py +777 -0
- dsat/benchmark/sciencebench.py +304 -0
- dsat/common/__init__.py +0 -0
- dsat/common/constants.py +11 -0
- dsat/common/exceptions.py +48 -0
- dsat/common/typing.py +19 -0
- dsat/config.py +79 -0
- dsat/models/__init__.py +3 -0
- dsat/models/candidates.py +16 -0
- dsat/models/formats.py +52 -0
- dsat/models/task.py +64 -0
- dsat/operators/__init__.py +0 -0
- dsat/operators/aflow_ops.py +90 -0
- dsat/operators/autokaggle_ops.py +170 -0
- dsat/operators/automind_ops.py +38 -0
- dsat/operators/base.py +22 -0
- dsat/operators/code.py +45 -0
- dsat/operators/dsagent_ops.py +123 -0
- dsat/operators/llm_basic.py +84 -0
- dsat/prompts/__init__.py +0 -0
- dsat/prompts/aflow_prompt.py +76 -0
- dsat/prompts/aide_prompt.py +52 -0
- dsat/prompts/autokaggle_prompt.py +290 -0
- dsat/prompts/automind_prompt.py +29 -0
- dsat/prompts/common.py +51 -0
- dsat/prompts/data_interpreter_prompt.py +82 -0
- dsat/prompts/dsagent_prompt.py +88 -0
- dsat/runner.py +554 -0
- dsat/services/__init__.py +0 -0
- dsat/services/data_analyzer.py +387 -0
- dsat/services/llm.py +486 -0
- dsat/services/llm_single.py +421 -0
- dsat/services/sandbox.py +386 -0
- dsat/services/states/__init__.py +0 -0
- dsat/services/states/autokaggle_state.py +43 -0
- dsat/services/states/base.py +14 -0
- dsat/services/states/dsa_log.py +13 -0
- dsat/services/states/experience.py +237 -0
- dsat/services/states/journal.py +153 -0
- dsat/services/states/operator_library.py +290 -0
- dsat/services/vdb.py +76 -0
- dsat/services/workspace.py +178 -0
- dsat/tasks/__init__.py +3 -0
- dsat/tasks/handlers.py +376 -0
- dsat/templates/open_ended/grade_template.py +107 -0
- dsat/tools/__init__.py +4 -0
- dsat/utils/__init__.py +0 -0
- dsat/utils/context.py +172 -0
- dsat/utils/dynamic_import.py +71 -0
- dsat/utils/parsing.py +33 -0
- dsat/workflows/__init__.py +12 -0
- dsat/workflows/base.py +53 -0
- dsat/workflows/factory.py +439 -0
- dsat/workflows/manual/__init__.py +0 -0
- dsat/workflows/manual/autokaggle_workflow.py +148 -0
- dsat/workflows/manual/data_interpreter_workflow.py +153 -0
- dsat/workflows/manual/deepanalyze_workflow.py +484 -0
- dsat/workflows/manual/dsagent_workflow.py +76 -0
- dsat/workflows/search/__init__.py +0 -0
- dsat/workflows/search/aflow_workflow.py +344 -0
- dsat/workflows/search/aide_workflow.py +283 -0
- dsat/workflows/search/automind_workflow.py +237 -0
- dsat/workflows/templates/__init__.py +0 -0
- dsat/workflows/templates/basic_kaggle_loop.py +71 -0
- dslighting/__init__.py +170 -0
- dslighting/core/__init__.py +13 -0
- dslighting/core/agent.py +646 -0
- dslighting/core/config_builder.py +318 -0
- dslighting/core/data_loader.py +422 -0
- dslighting/core/task_detector.py +422 -0
- dslighting/utils/__init__.py +19 -0
- dslighting/utils/defaults.py +151 -0
- dslighting-1.3.9.dist-info/METADATA +554 -0
- dslighting-1.3.9.dist-info/RECORD +80 -0
- dslighting-1.3.9.dist-info/WHEEL +5 -0
- dslighting-1.3.9.dist-info/top_level.txt +2 -0
dsat/services/sandbox.py
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# dsat/services/sandbox.py
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
import asyncio
|
|
9
|
+
import shutil
|
|
10
|
+
import time
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from contextlib import asynccontextmanager
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import AsyncGenerator, List, Dict, Any, Optional
|
|
15
|
+
from multiprocessing import Process, Queue
|
|
16
|
+
|
|
17
|
+
import nbformat
|
|
18
|
+
from nbclient import NotebookClient
|
|
19
|
+
from nbclient.exceptions import CellExecutionError, CellTimeoutError, DeadKernelError
|
|
20
|
+
|
|
21
|
+
from dsat.common.typing import ExecutionResult
|
|
22
|
+
from dsat.services.workspace import WorkspaceService
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
NOTEBOOK_INIT_CODE = """
|
|
27
|
+
import warnings
|
|
28
|
+
import pandas as pd
|
|
29
|
+
import numpy as np
|
|
30
|
+
import matplotlib.pyplot as plt
|
|
31
|
+
import os
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
import seaborn as sns
|
|
35
|
+
sns.set_theme(style="whitegrid")
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
warnings.filterwarnings('ignore')
|
|
40
|
+
print("DSAT Notebook environment initialized.")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
class NotebookExecutor:
|
|
44
|
+
"""
|
|
45
|
+
Manages a persistent Jupyter kernel for cell-by-cell code execution.
|
|
46
|
+
This class is intended to be used as an async context manager.
|
|
47
|
+
"""
|
|
48
|
+
def __init__(self, workspace: WorkspaceService, timeout: int):
|
|
49
|
+
self.workspace = workspace
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
self.nb = nbformat.v4.new_notebook()
|
|
52
|
+
self.client = NotebookClient(
|
|
53
|
+
self.nb,
|
|
54
|
+
timeout=self.timeout,
|
|
55
|
+
cwd=str(self.workspace.get_path("sandbox_workdir"))
|
|
56
|
+
)
|
|
57
|
+
self._initialized = False
|
|
58
|
+
self._kernel_cm = None
|
|
59
|
+
|
|
60
|
+
async def start(self):
|
|
61
|
+
if self._initialized:
|
|
62
|
+
return
|
|
63
|
+
logger.info("Starting new Jupyter kernel for notebook execution...")
|
|
64
|
+
self._kernel_cm = self.client.async_setup_kernel()
|
|
65
|
+
await self._kernel_cm.__aenter__()
|
|
66
|
+
logger.info("Kernel started. Initializing environment...")
|
|
67
|
+
self._initialized = True
|
|
68
|
+
sandbox_workdir_path = self.workspace.get_path("sandbox_workdir").resolve()
|
|
69
|
+
full_init_code = (
|
|
70
|
+
f"import os\n"
|
|
71
|
+
f"os.chdir(r'{sandbox_workdir_path}')\n"
|
|
72
|
+
f"print(f'CWD set to: {{os.getcwd()}}')\n"
|
|
73
|
+
f"\n"
|
|
74
|
+
f"{NOTEBOOK_INIT_CODE}"
|
|
75
|
+
)
|
|
76
|
+
init_result = await self.execute_cell(full_init_code)
|
|
77
|
+
if not init_result.success:
|
|
78
|
+
await self.stop()
|
|
79
|
+
raise RuntimeError(f"Could not initialize notebook environment. Error: {init_result.stderr}")
|
|
80
|
+
if self.nb.cells:
|
|
81
|
+
self.nb.cells.pop(0)
|
|
82
|
+
logger.info(f"Notebook environment ready. CWD is now {sandbox_workdir_path}")
|
|
83
|
+
|
|
84
|
+
async def stop(self):
|
|
85
|
+
logger.info("Shutting down Jupyter kernel.")
|
|
86
|
+
if self._kernel_cm is not None:
|
|
87
|
+
try:
|
|
88
|
+
await self._kernel_cm.__aexit__(None, None, None)
|
|
89
|
+
finally:
|
|
90
|
+
self._kernel_cm = None
|
|
91
|
+
self._initialized = False
|
|
92
|
+
|
|
93
|
+
def _parse_cell_outputs(self, outputs: list) -> ExecutionResult:
|
|
94
|
+
stdout_lines, stderr_lines, artifacts = [], [], []
|
|
95
|
+
success = True
|
|
96
|
+
exc_type = None
|
|
97
|
+
for out in outputs:
|
|
98
|
+
if out.output_type == 'stream':
|
|
99
|
+
if out.name == 'stdout': stdout_lines.append(out.text)
|
|
100
|
+
else: stderr_lines.append(out.text)
|
|
101
|
+
elif out.output_type in ['execute_result', 'display_data']:
|
|
102
|
+
if 'text/plain' in out.data: stdout_lines.append(out.data['text/plain'])
|
|
103
|
+
if 'image/png' in out.data: artifacts.append("[Image data generated in notebook]")
|
|
104
|
+
elif out.output_type == 'error':
|
|
105
|
+
success = False
|
|
106
|
+
exc_type = out.ename
|
|
107
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
108
|
+
traceback_text = ansi_escape.sub('', "\n".join(out.traceback))
|
|
109
|
+
stderr_lines.append(f"ERROR: {out.ename}\n{traceback_text}")
|
|
110
|
+
return ExecutionResult(
|
|
111
|
+
success=success, stdout="\n".join(stdout_lines).strip(),
|
|
112
|
+
stderr="\n".join(stderr_lines).strip(), artifacts=artifacts, exc_type=exc_type
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def execute_cell(self, code: str) -> ExecutionResult:
|
|
116
|
+
if not self._initialized:
|
|
117
|
+
raise RuntimeError("NotebookExecutor not started. Use 'async with' context manager.")
|
|
118
|
+
cell_index = len(self.nb.cells)
|
|
119
|
+
self.nb.cells.append(nbformat.v4.new_code_cell(source=code))
|
|
120
|
+
try:
|
|
121
|
+
await self.client.async_execute_cell(self.nb.cells[cell_index], cell_index)
|
|
122
|
+
result = self._parse_cell_outputs(self.nb.cells[cell_index].outputs)
|
|
123
|
+
except CellExecutionError:
|
|
124
|
+
result = self._parse_cell_outputs(self.nb.cells[cell_index].outputs)
|
|
125
|
+
except (CellTimeoutError, DeadKernelError) as e:
|
|
126
|
+
result = ExecutionResult(success=False, stderr=str(e), exc_type=e.__class__.__name__)
|
|
127
|
+
|
|
128
|
+
notebook_path = self.workspace.get_path("artifacts") / "session.ipynb"
|
|
129
|
+
with open(notebook_path, "w", encoding='utf-8') as f:
|
|
130
|
+
nbformat.write(self.nb, f)
|
|
131
|
+
return result
|
|
132
|
+
|
|
133
|
+
# ==============================================================================
|
|
134
|
+
# == NEW: WORKER FUNCTION FOR SUBPROCESS ==
|
|
135
|
+
# ==============================================================================
|
|
136
|
+
|
|
137
|
+
def notebook_worker(
|
|
138
|
+
task_queue: Queue,
|
|
139
|
+
result_queue: Queue,
|
|
140
|
+
workspace: WorkspaceService,
|
|
141
|
+
timeout: int
|
|
142
|
+
):
|
|
143
|
+
"""
|
|
144
|
+
This function runs in a separate process. It creates an asyncio event loop,
|
|
145
|
+
manages a NotebookExecutor, and processes tasks from the queue.
|
|
146
|
+
"""
|
|
147
|
+
async def main_loop():
|
|
148
|
+
# The executor's lifecycle is tied to this async function
|
|
149
|
+
executor = NotebookExecutor(workspace, timeout)
|
|
150
|
+
await executor.start()
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
# Get code from the main process. A `None` object is the shutdown signal.
|
|
154
|
+
code = task_queue.get()
|
|
155
|
+
if code is None:
|
|
156
|
+
break
|
|
157
|
+
|
|
158
|
+
# Execute the code and put the result back
|
|
159
|
+
result = await executor.execute_cell(code)
|
|
160
|
+
result_queue.put(result)
|
|
161
|
+
|
|
162
|
+
await executor.stop()
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
asyncio.run(main_loop())
|
|
166
|
+
except Exception as e:
|
|
167
|
+
# If something catastrophic happens, report it back
|
|
168
|
+
logger.error(f"Notebook worker process failed: {e}", exc_info=True)
|
|
169
|
+
result_queue.put(ExecutionResult(
|
|
170
|
+
success=False, stderr=f"Worker process failed: {e}", exc_type=e.__class__.__name__
|
|
171
|
+
))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ProcessIsolatedNotebookExecutor:
|
|
175
|
+
"""
|
|
176
|
+
Manages the lifecycle of a notebook worker process, providing a clean
|
|
177
|
+
interface to the main application and ensuring robust cleanup.
|
|
178
|
+
"""
|
|
179
|
+
def __init__(self, workspace: WorkspaceService, timeout: int):
|
|
180
|
+
self.task_queue = Queue()
|
|
181
|
+
self.result_queue = Queue()
|
|
182
|
+
self.worker_process = Process(
|
|
183
|
+
target=notebook_worker,
|
|
184
|
+
args=(self.task_queue, self.result_queue, workspace, timeout),
|
|
185
|
+
daemon=True # Set as daemon process to prevent main process from hanging
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def start(self):
|
|
189
|
+
"""Starts the worker process."""
|
|
190
|
+
logger.info("Starting process-isolated notebook worker...")
|
|
191
|
+
self.worker_process.start()
|
|
192
|
+
|
|
193
|
+
def stop(self, timeout: int = 10):
|
|
194
|
+
"""
|
|
195
|
+
Stops the worker process gracefully, with a forceful termination fallback.
|
|
196
|
+
Note: Since this is a daemon process, it will be automatically terminated
|
|
197
|
+
when the parent process exits, providing an additional safety mechanism.
|
|
198
|
+
"""
|
|
199
|
+
if not self.worker_process.is_alive():
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
logger.info("Stopping process-isolated notebook worker...")
|
|
203
|
+
try:
|
|
204
|
+
# Send shutdown signal
|
|
205
|
+
self.task_queue.put(None)
|
|
206
|
+
# Wait for graceful shutdown
|
|
207
|
+
self.worker_process.join(timeout=timeout)
|
|
208
|
+
|
|
209
|
+
# If still alive, it's hung. Terminate it.
|
|
210
|
+
if self.worker_process.is_alive():
|
|
211
|
+
logger.warning(
|
|
212
|
+
f"Notebook worker did not exit gracefully within {timeout}s. Terminating."
|
|
213
|
+
)
|
|
214
|
+
self.worker_process.terminate()
|
|
215
|
+
self.worker_process.join() # Wait for termination to complete
|
|
216
|
+
finally:
|
|
217
|
+
self.task_queue.close()
|
|
218
|
+
self.result_queue.close()
|
|
219
|
+
logger.info("Notebook worker stopped.")
|
|
220
|
+
|
|
221
|
+
async def execute_cell(self, code: str) -> ExecutionResult:
|
|
222
|
+
"""
|
|
223
|
+
Sends code to the worker process and waits for the result.
|
|
224
|
+
This is async to fit into the main application's event loop, but the
|
|
225
|
+
underlying queue.get() is blocking. We use run_in_executor to avoid
|
|
226
|
+
blocking the main event loop.
|
|
227
|
+
"""
|
|
228
|
+
if not self.worker_process.is_alive():
|
|
229
|
+
raise RuntimeError("Worker process is not running.")
|
|
230
|
+
|
|
231
|
+
loop = asyncio.get_running_loop()
|
|
232
|
+
|
|
233
|
+
# Send the task asynchronously
|
|
234
|
+
await loop.run_in_executor(None, self.task_queue.put, code)
|
|
235
|
+
|
|
236
|
+
# Wait for the result asynchronously
|
|
237
|
+
result = await loop.run_in_executor(None, self.result_queue.get)
|
|
238
|
+
|
|
239
|
+
return result
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ==============================================================================
|
|
243
|
+
# == MODIFIED: SANDBOX SERVICE INTEGRATION ==
|
|
244
|
+
# ==============================================================================
|
|
245
|
+
class SandboxService:
|
|
246
|
+
"""
|
|
247
|
+
Provides unified access to isolated script and notebook code execution environments.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
workspace: Workspace service for managing files
|
|
251
|
+
timeout: Default timeout for script execution (seconds)
|
|
252
|
+
auto_matplotlib: Automatically inject matplotlib backend (default: False).
|
|
253
|
+
Set to True for Web UI environments that need visualization.
|
|
254
|
+
Set to False for standalone package usage.
|
|
255
|
+
"""
|
|
256
|
+
def __init__(
|
|
257
|
+
self,
|
|
258
|
+
workspace: WorkspaceService,
|
|
259
|
+
timeout: int = 600,
|
|
260
|
+
auto_matplotlib: bool = False
|
|
261
|
+
):
|
|
262
|
+
self.workspace = workspace
|
|
263
|
+
self.timeout = timeout
|
|
264
|
+
self.auto_matplotlib = auto_matplotlib # Store matplotlib injection preference
|
|
265
|
+
self.execution_history: List[Dict[str, Any]] = []
|
|
266
|
+
|
|
267
|
+
def run_script(self, script_code: str, timeout: Optional[int] = None) -> ExecutionResult:
|
|
268
|
+
"""
|
|
269
|
+
Runs a Python script within the sandbox workspace.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
script_code: Python code to execute
|
|
273
|
+
timeout: Optional timeout override (uses self.timeout if None)
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
ExecutionResult with stdout, stderr, success status, etc.
|
|
277
|
+
"""
|
|
278
|
+
# Optionally inject matplotlib non-interactive backend
|
|
279
|
+
# This is only done if auto_matplotlib=True (used by Web UI for visualization)
|
|
280
|
+
if self.auto_matplotlib:
|
|
281
|
+
# Force non-interactive backend for matplotlib to prevent blocking plt.show()
|
|
282
|
+
fixed_code = "import matplotlib\nmatplotlib.use('Agg')\n" + script_code
|
|
283
|
+
logger.debug("Auto-injected matplotlib non-interactive backend")
|
|
284
|
+
else:
|
|
285
|
+
# Use code as-is without modification (default for DSLighting package)
|
|
286
|
+
fixed_code = script_code
|
|
287
|
+
|
|
288
|
+
script_name = f"_sandbox_script_{uuid.uuid4().hex}.py"
|
|
289
|
+
script_path = self.workspace.run_dir / script_name
|
|
290
|
+
execution_id = uuid.uuid4().hex
|
|
291
|
+
started_at = datetime.utcnow()
|
|
292
|
+
perf_start = time.perf_counter()
|
|
293
|
+
execution_result: ExecutionResult = ExecutionResult(success=False, stdout="", stderr="", exc_type=None)
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
script_path.write_text(fixed_code, encoding="utf-8")
|
|
297
|
+
logger.info(f"Executing script '{script_name}' in sandbox (timeout: {self.timeout}s)...")
|
|
298
|
+
completed_process = subprocess.run(
|
|
299
|
+
[sys.executable, str(script_path)],
|
|
300
|
+
capture_output=True, text=True, timeout=self.timeout,
|
|
301
|
+
cwd=self.workspace.get_path("sandbox_workdir"),
|
|
302
|
+
encoding='utf-8', errors='replace'
|
|
303
|
+
)
|
|
304
|
+
success = completed_process.returncode == 0
|
|
305
|
+
exc_type = None
|
|
306
|
+
if not success:
|
|
307
|
+
stderr_lines = completed_process.stderr.strip().split('\n')
|
|
308
|
+
if stderr_lines:
|
|
309
|
+
match = re.search(r"^(\w+(?:Error|Exception)):", stderr_lines[-1])
|
|
310
|
+
if match: exc_type = match.group(1)
|
|
311
|
+
status = "succeeded" if success else f"failed (exit code {completed_process.returncode})"
|
|
312
|
+
logger.info(f"Script execution finished: {status}.")
|
|
313
|
+
if not success:
|
|
314
|
+
logger.error(f"=== SCRIPT EXECUTION FAILED ===")
|
|
315
|
+
logger.error(f"Exit Code: {completed_process.returncode}")
|
|
316
|
+
logger.error(f"Exception Type: {exc_type}")
|
|
317
|
+
if completed_process.stdout: logger.error(f"STDOUT:\n{completed_process.stdout}")
|
|
318
|
+
if completed_process.stderr: logger.error(f"STDERR:\n{completed_process.stderr}")
|
|
319
|
+
logger.error(f"=== END ERROR LOG ===")
|
|
320
|
+
execution_result = ExecutionResult(
|
|
321
|
+
success=success, stdout=completed_process.stdout,
|
|
322
|
+
stderr=completed_process.stderr, exc_type=exc_type
|
|
323
|
+
)
|
|
324
|
+
except subprocess.TimeoutExpired as e:
|
|
325
|
+
logger.warning("Script execution timed out. Process was terminated.")
|
|
326
|
+
execution_result = ExecutionResult(
|
|
327
|
+
success=False, stdout=e.stdout or "",
|
|
328
|
+
stderr=e.stderr or f"TimeoutError: Execution exceeded {self.timeout} seconds.",
|
|
329
|
+
exc_type="TimeoutError"
|
|
330
|
+
)
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.error(f"An unexpected error occurred during sandbox setup: {e}", exc_info=True)
|
|
333
|
+
execution_result = ExecutionResult(success=False, stderr=str(e), exc_type=e.__class__.__name__)
|
|
334
|
+
finally:
|
|
335
|
+
ended_at = datetime.utcnow()
|
|
336
|
+
duration = round(time.perf_counter() - perf_start, 4)
|
|
337
|
+
|
|
338
|
+
copied_script_path = None
|
|
339
|
+
scripts_dir = self.workspace.get_path("artifacts") / "sandbox_scripts"
|
|
340
|
+
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
341
|
+
if script_path.exists():
|
|
342
|
+
try:
|
|
343
|
+
copied_script_path = scripts_dir / script_name
|
|
344
|
+
shutil.copy2(script_path, copied_script_path)
|
|
345
|
+
except Exception as copy_error:
|
|
346
|
+
logger.error(f"Failed to copy sandbox script '{script_name}' to artifacts: {copy_error}", exc_info=True)
|
|
347
|
+
|
|
348
|
+
execution_metadata = {
|
|
349
|
+
"execution_id": execution_id,
|
|
350
|
+
"script_filename": script_name,
|
|
351
|
+
"original_script_path": str(script_path) if script_path.exists() else None,
|
|
352
|
+
"copied_script_path": str(copied_script_path) if copied_script_path else None,
|
|
353
|
+
"sandbox_cwd": str(self.workspace.get_path("sandbox_workdir")),
|
|
354
|
+
"started_at_utc": started_at.isoformat() + "Z",
|
|
355
|
+
"ended_at_utc": ended_at.isoformat() + "Z",
|
|
356
|
+
"duration_seconds": duration,
|
|
357
|
+
}
|
|
358
|
+
execution_result.metadata = execution_metadata
|
|
359
|
+
|
|
360
|
+
history_entry = {
|
|
361
|
+
**execution_metadata,
|
|
362
|
+
"success": execution_result.success,
|
|
363
|
+
"exc_type": execution_result.exc_type,
|
|
364
|
+
"stdout": execution_result.stdout,
|
|
365
|
+
"stderr": execution_result.stderr,
|
|
366
|
+
"code": fixed_code,
|
|
367
|
+
}
|
|
368
|
+
self.execution_history.append(history_entry)
|
|
369
|
+
return execution_result
|
|
370
|
+
|
|
371
|
+
@asynccontextmanager
|
|
372
|
+
async def notebook_executor(self) -> AsyncGenerator[ProcessIsolatedNotebookExecutor, None]:
|
|
373
|
+
"""
|
|
374
|
+
Provides a process-isolated notebook executor as an asynchronous context
|
|
375
|
+
manager to ensure proper startup and cleanup of the worker process.
|
|
376
|
+
"""
|
|
377
|
+
executor = ProcessIsolatedNotebookExecutor(self.workspace, self.timeout)
|
|
378
|
+
executor.start()
|
|
379
|
+
try:
|
|
380
|
+
yield executor
|
|
381
|
+
finally:
|
|
382
|
+
executor.stop()
|
|
383
|
+
|
|
384
|
+
def get_execution_history(self) -> List[Dict[str, Any]]:
|
|
385
|
+
"""Return a copy of sandbox execution history for telemetry persistence."""
|
|
386
|
+
return list(self.execution_history)
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# dsat/services/states/autokaggle_state.py
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
from dsat.models.formats import TaskContract
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AttemptMemory(BaseModel):
|
|
10
|
+
"""Stores the memory of a single attempt within a phase."""
|
|
11
|
+
attempt_number: int
|
|
12
|
+
plan: str
|
|
13
|
+
code: str
|
|
14
|
+
execution_output: str
|
|
15
|
+
execution_error: Optional[str]
|
|
16
|
+
validation_result: Dict[str, Any]
|
|
17
|
+
review_score: float
|
|
18
|
+
review_suggestion: str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PhaseMemory(BaseModel):
|
|
22
|
+
"""Stores the history of all attempts for a single successful phase."""
|
|
23
|
+
phase_goal: str
|
|
24
|
+
attempts: List[AttemptMemory] = Field(default_factory=list)
|
|
25
|
+
final_report: str = ""
|
|
26
|
+
is_successful: bool = False
|
|
27
|
+
# Keep track of artifacts produced *in this phase*. Paths are relative to the sandbox workdir.
|
|
28
|
+
output_artifacts: Dict[str, str] = Field(default_factory=dict, description="Mapping of artifact filenames to their descriptions.")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AutoKaggleState(BaseModel):
|
|
32
|
+
"""Manages the entire state of a dynamic AutoKaggle workflow run."""
|
|
33
|
+
contract: TaskContract
|
|
34
|
+
dynamic_phases: List[str] = Field(default_factory=list)
|
|
35
|
+
phase_history: List[PhaseMemory] = Field(default_factory=list)
|
|
36
|
+
io_instructions: str = Field(default="", description="Standardized I/O instructions for the agents.")
|
|
37
|
+
full_task_description: str = Field(
|
|
38
|
+
description="The original, complete task description including the data analysis report from DataAnalyzer."
|
|
39
|
+
)
|
|
40
|
+
# The central registry for all artifacts created during the run.
|
|
41
|
+
# This provides state continuity between phases.
|
|
42
|
+
# Maps artifact filename to a description.
|
|
43
|
+
global_artifacts: Dict[str, str] = Field(default_factory=dict)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Defines the abstract base class for all state management services in DSAT.
|
|
3
|
+
"""
|
|
4
|
+
from abc import ABC
|
|
5
|
+
|
|
6
|
+
class State(ABC):
|
|
7
|
+
"""
|
|
8
|
+
Abstract base class for state managers.
|
|
9
|
+
|
|
10
|
+
This class serves as a common interface and type hint for different state
|
|
11
|
+
management strategies used by various agent paradigms, such as journaling for
|
|
12
|
+
tree-based search or linear phase logs for sequential workflows.
|
|
13
|
+
"""
|
|
14
|
+
pass
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Implements DSAgentState, which manages the running log for the DS-Agent workflow.
|
|
3
|
+
"""
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from dsat.services.states.base import State
|
|
6
|
+
|
|
7
|
+
class DSAgentState(State, BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Holds the state for a DS-Agent workflow execution, primarily the running log
|
|
10
|
+
which accumulates summaries of each experimental step.
|
|
11
|
+
"""
|
|
12
|
+
running_log: str = ""
|
|
13
|
+
final_code: str = ""
|