|
11 | 11 | from __future__ import annotations |
12 | 12 |
|
13 | 13 | import json |
| 14 | +import threading |
14 | 15 | from concurrent.futures import ThreadPoolExecutor, as_completed |
15 | 16 | from datetime import datetime |
16 | 17 | from pathlib import Path |
|
30 | 31 | from bluebox.data_models.llms.vendors import LLMModel, OpenAIModel |
31 | 32 | from bluebox.data_models.routine.routine import RoutineExecutionRequest, RoutineInfo |
32 | 33 | from bluebox.utils.code_execution_sandbox import execute_python_sandboxed |
| 34 | +from bluebox.utils.infra_utils import read_file_lines |
33 | 35 | from bluebox.utils.llm_utils import token_optimized |
34 | 36 | from bluebox.utils.logger import get_logger |
35 | 37 |
|
@@ -71,7 +73,7 @@ class BlueBoxAgent(AbstractAgent): |
71 | 73 |
|
72 | 74 | ## Inspecting the Workspace |
73 | 75 | - Use `list_workspace_files` to see all files in the workspace (raw/, outputs/, etc.). |
74 | | - - Use `read_workspace_file` to read any file by relative path (e.g. "raw/routine_results_2024-01-15_143052_abc.json" or "outputs/results.csv"). Use optional start_line/end_line to read specific line ranges for large files. |
| 76 | + - Use `read_workspace_file` to read any file by relative path (e.g. "raw/25-01-15-143052-routine_result_1.json" or "outputs/results.csv"). Use optional start_line/end_line to read specific line ranges for large files. |
75 | 77 |
|
76 | 78 | ## Important Rules |
77 | 79 | - You ONLY have routine tools, code execution, and file inspection tools. Do not tell the user you can browse, click, type, or interact with web pages directly. |
@@ -117,6 +119,8 @@ def __init__( |
117 | 119 | self._raw_dir = self._workspace_dir / "raw" |
118 | 120 | self._outputs_dir = self._workspace_dir / "outputs" |
119 | 121 | self._routine_cache: dict[str, RoutineInfo] = {} |
| 122 | + self._execution_counter: int = 0 |
| 123 | + self._counter_lock = threading.Lock() |
120 | 124 |
|
121 | 125 | super().__init__( |
122 | 126 | emit_message_callable=emit_message_callable, |
@@ -245,9 +249,11 @@ def save_result(result: dict[str, Any]) -> dict[str, Any]: |
245 | 249 | """Save a single routine result to a JSON file in raw/.""" |
246 | 250 | try: |
247 | 251 | self._raw_dir.mkdir(parents=True, exist_ok=True) |
248 | | - rid = result.get("routine_id", "unknown") |
249 | | - timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S") |
250 | | - output_path = self._raw_dir / f"routine_results_{timestamp}_{rid}.json" |
| 252 | + with self._counter_lock: |
| 253 | + self._execution_counter += 1 |
| 254 | + idx = self._execution_counter |
| 255 | + timestamp = datetime.now().strftime("%y-%m-%d-%H%M%S") |
| 256 | + output_path = self._raw_dir / f"{timestamp}-routine_result_{idx}.json" |
251 | 257 | output_path.write_text(json.dumps(result, indent=2, default=str)) |
252 | 258 | result["output_file"] = str(output_path) |
253 | 259 | logger.info("Routine result saved to %s", output_path) |
@@ -445,37 +451,11 @@ def _read_workspace_file( |
445 | 451 | # Resolve and validate path stays within workspace |
446 | 452 | resolved = (self._workspace_dir / path).resolve() |
447 | 453 | workspace_resolved = self._workspace_dir.resolve() |
448 | | - if not str(resolved).startswith(str(workspace_resolved) + "/") and resolved != workspace_resolved: |
449 | | - return {"error": f"Access denied: '{path}' is outside the workspace directory"} |
450 | | - |
451 | | - if not resolved.exists(): |
452 | | - return {"error": f"File not found: {path}"} |
453 | | - if not resolved.is_file(): |
454 | | - return {"error": f"Not a file: {path}"} |
455 | | - |
456 | 454 | try: |
457 | | - lines = resolved.read_text().splitlines() |
458 | | - except OSError as e: |
459 | | - return {"error": f"Failed to read file: {e}"} |
460 | | - |
461 | | - total_lines = len(lines) |
462 | | - |
463 | | - # Apply line range |
464 | | - if start_line is not None or end_line is not None: |
465 | | - s = (start_line or 1) - 1 # Convert to 0-based |
466 | | - e = end_line or total_lines |
467 | | - lines = lines[s:e] |
468 | | - line_range = f"lines {s + 1}-{min(e, total_lines)} of {total_lines}" |
469 | | - else: |
470 | | - # Cap output at 200 lines to avoid blowing up context |
471 | | - if total_lines > 200: |
472 | | - lines = lines[:200] |
473 | | - line_range = f"lines 1-200 of {total_lines} (truncated, use start_line/end_line for more)" |
474 | | - else: |
475 | | - line_range = f"all {total_lines} lines" |
| 455 | + resolved.relative_to(workspace_resolved) |
| 456 | + except ValueError: |
| 457 | + return {"error": f"Access denied: '{path}' is outside the workspace directory"} |
476 | 458 |
|
477 | | - return { |
478 | | - "path": path, |
479 | | - "line_range": line_range, |
480 | | - "content": "\n".join(lines), |
481 | | - } |
| 459 | + result = read_file_lines(resolved, start_line=start_line, end_line=end_line) |
| 460 | + result["path"] = path |
| 461 | + return result |
0 commit comments