Skip to content

Commit b6d8ce2

Browse files
committed
refactor: Simplify file handling in programmatic execution and enhance timeout management
- Updated `ProgrammaticExecutionRequest` to accept only `CodeEnvFile` references, removing legacy inline file support. - Refined timeout conversion logic in `programmatic.py` for clarity. - Removed unused state restoration fields from `FileInfo` model and related services to streamline state management. - Adjusted `ProgrammaticService` to focus solely on mounting referenced files, enhancing code readability. - Cleaned up various comments and documentation to reflect the current state of the codebase.
1 parent d7ac15c commit b6d8ce2

19 files changed

Lines changed: 135 additions & 1508 deletions

docs/PROGRAMMATIC_TOOL_CALLING.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ interface ProgrammaticExecutionRequest {
5959
tools: LCTool[]; // Filtered tool definitions
6060
session_id?: string; // Optional: for file persistence
6161
timeout?: number; // 1000-300000ms (default: 60000)
62-
files?: Array<CodeEnvFile | InlinePTCFile>; // Optional: initial sandbox files
62+
files?: CodeEnvFile[]; // Optional: initial sandbox files
6363
}
6464

6565
interface LCTool {
@@ -73,11 +73,6 @@ interface CodeEnvFile {
7373
name: string; // Original filename
7474
session_id: string; // Source session
7575
}
76-
77-
interface InlinePTCFile {
78-
filename: string;
79-
content: string; // Legacy inline payload, still accepted
80-
}
8176
```
8277

8378
The public timeout contract is milliseconds. The backend converts that value to
@@ -249,7 +244,7 @@ Unlike `/exec`, sandboxes for programmatic execution must:
249244
- Have **longer TTL** (match request timeout, up to 5 minutes)
250245
- Be **cleaned up** on completion, error, or timeout
251246
- Support **session file access** at `/mnt/data/`
252-
- Accept LibreChat `CodeEnvFile` references without requiring inline file blobs
247+
- Accept only LibreChat `CodeEnvFile` references for prior-session files
253248

254249
### 4. Round-Trip Limits
255250

src/api/programmatic.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Optional
99

1010
import structlog
11-
from fastapi import APIRouter, Request
11+
from fastapi import APIRouter
1212

1313
from ..models.programmatic import (
1414
ProgrammaticExecRequest,
@@ -38,13 +38,12 @@ def _timeout_ms_to_seconds(timeout_ms: Optional[int]) -> Optional[int]:
3838
"""Convert the public millisecond timeout contract to internal seconds."""
3939
if timeout_ms is None:
4040
return None
41-
return max(1, math.ceil(timeout_ms / 1000))
41+
return math.ceil(timeout_ms / 1000)
4242

4343

4444
@router.post("/exec/programmatic", response_model=ProgrammaticExecResponse)
4545
async def execute_programmatic(
4646
request: ProgrammaticExecRequest,
47-
http_request: Request,
4847
session_service: SessionServiceDep,
4948
) -> ProgrammaticExecResponse:
5049
"""Execute code with programmatic tool calling support.
@@ -55,7 +54,6 @@ async def execute_programmatic(
5554
5655
Args:
5756
request: PTC execution request
58-
http_request: HTTP request for auth state
5957
session_service: Session service for session management
6058
6159
Returns:

src/middleware/security.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,9 @@ async def _validate_request(self, request: Request):
120120
"""Validate request content type."""
121121
# Only validate content type for non-file upload requests.
122122
# File uploads are handled by the files API with specific validation.
123-
# Keep /state/* exempt so legacy paths return router-level 404 rather
124-
# than a middleware 415 when clients send binary content.
125123
if (
126124
request.method in ["POST", "PUT", "PATCH"]
127125
and not request.url.path.startswith("/upload")
128-
and not request.url.path.startswith("/state/")
129126
):
130127
content_type = request.headers.get("content-type", "")
131128
allowed_types = [

src/models/files.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,6 @@ class FileInfo(BaseModel):
4040
content_type: str
4141
created_at: datetime
4242
path: str = Field(..., description="File path in the session")
43-
# State restoration fields (for Python state-file linking)
44-
execution_id: Optional[str] = Field(
45-
default=None, description="ID of the execution that created/last used this file"
46-
)
47-
state_hash: Optional[str] = Field(
48-
default=None,
49-
description="SHA256 hash of the Python state when this file was last used",
50-
)
51-
last_used_at: Optional[datetime] = Field(
52-
default=None,
53-
description="Timestamp of when this file was last used in an execution",
54-
)
5543

5644
class Config:
5745
json_encoders = {datetime: lambda v: v.isoformat()}

src/models/programmatic.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,13 @@ class PTCToolResult(BaseModel):
4848
class PTCFileInput(BaseModel):
4949
"""File payload for PTC initial execution.
5050
51-
Supports both:
52-
- LibreChat-style file references: {session_id, id, name}
53-
- Legacy inline files: {filename, content}
51+
Matches the LibreChat/librechat-agents CodeEnvFile shape:
52+
{session_id, id, name}
5453
"""
5554

56-
id: Optional[str] = Field(default=None, description="File identifier")
57-
name: Optional[str] = Field(
58-
default=None, description="Filename for a referenced session file"
59-
)
60-
session_id: Optional[str] = Field(
61-
default=None, description="Source session for a referenced file"
62-
)
63-
filename: Optional[str] = Field(
64-
default=None, description="Filename for inline file content"
65-
)
66-
content: Optional[Any] = Field(
67-
default=None,
68-
description="Inline file content for non-reference callers",
69-
)
55+
id: str = Field(..., description="File identifier")
56+
name: str = Field(..., description="Original filename for the referenced file")
57+
session_id: str = Field(..., description="Source session for a referenced file")
7058

7159

7260
class ProgrammaticExecRequest(BaseModel):
@@ -102,7 +90,8 @@ class ProgrammaticExecRequest(BaseModel):
10290
le=300000,
10391
)
10492
files: List[PTCFileInput] = Field(
105-
default_factory=list, description="Files to mount in sandbox"
93+
default_factory=list,
94+
description="Referenced prior-session files to mount in the sandbox",
10695
)
10796

10897
# Continuation fields

src/services/file.py

Lines changed: 0 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -287,24 +287,13 @@ async def get_file_info(self, session_id: str, file_id: str) -> Optional[FileInf
287287
if not metadata:
288288
return None
289289

290-
# Parse last_used_at if present
291-
last_used_at = None
292-
if metadata.get("last_used_at"):
293-
try:
294-
last_used_at = datetime.fromisoformat(metadata["last_used_at"])
295-
except (ValueError, TypeError):
296-
pass
297-
298290
return FileInfo(
299291
file_id=file_id,
300292
filename=metadata["filename"],
301293
size=metadata["size"],
302294
content_type=metadata["content_type"],
303295
created_at=metadata["created_at"],
304296
path=metadata["path"],
305-
execution_id=metadata.get("execution_id"),
306-
state_hash=metadata.get("state_hash"),
307-
last_used_at=last_used_at,
308297
)
309298

310299
async def list_files(self, session_id: str) -> List[FileInfo]:
@@ -454,17 +443,13 @@ async def store_execution_output_file(
454443
session_id: str,
455444
filename: str,
456445
content: bytes,
457-
execution_id: Optional[str] = None,
458-
state_hash: Optional[str] = None,
459446
) -> str:
460447
"""Store a file generated during code execution.
461448
462449
Args:
463450
session_id: Session identifier
464451
filename: Name of the file
465452
content: File content as bytes
466-
execution_id: Optional ID of the execution that created this file
467-
state_hash: Optional SHA256 hash of the Python state at creation time
468453
469454
Returns:
470455
The generated file_id
@@ -496,7 +481,6 @@ async def store_execution_output_file(
496481

497482
now = datetime.utcnow()
498483

499-
# Store metadata including state restoration fields
500484
metadata = {
501485
"file_id": file_id,
502486
"filename": filename,
@@ -509,13 +493,6 @@ async def store_execution_output_file(
509493
"type": "output", # Mark as execution output
510494
}
511495

512-
# Add state restoration fields if provided
513-
if execution_id:
514-
metadata["execution_id"] = execution_id
515-
if state_hash:
516-
metadata["state_hash"] = state_hash
517-
metadata["last_used_at"] = now.isoformat()
518-
519496
await self._store_file_metadata(session_id, file_id, metadata)
520497

521498
logger.debug(
@@ -822,84 +799,11 @@ async def cleanup_orphan_objects(self, batch_limit: int = 1000) -> int:
822799
logger.error("Orphan MinIO objects cleanup failed", error=str(e))
823800
return 0
824801

825-
async def get_file_state_hash(self, session_id: str, file_id: str) -> Optional[str]:
826-
"""Get the state hash associated with a file.
827-
828-
Args:
829-
session_id: Session identifier
830-
file_id: File identifier
831-
832-
Returns:
833-
SHA256 hash of the state when this file was last used, or None
834-
"""
835-
try:
836-
metadata_key = self.get_file_metadata_key(session_id, file_id)
837-
state_hash = await self.redis_client.hget(metadata_key, "state_hash")
838-
return state_hash
839-
except Exception as e:
840-
logger.error(
841-
"Failed to get file state hash",
842-
error=str(e),
843-
session_id=session_id,
844-
file_id=file_id,
845-
)
846-
return None
847-
848-
async def update_file_state_hash(
849-
self,
850-
session_id: str,
851-
file_id: str,
852-
state_hash: str,
853-
execution_id: Optional[str] = None,
854-
) -> bool:
855-
"""Update the state hash for a file (called when file is used in execution).
856-
857-
Args:
858-
session_id: Session identifier
859-
file_id: File identifier
860-
state_hash: New SHA256 hash of the Python state
861-
execution_id: Optional ID of the execution that used this file
862-
863-
Returns:
864-
True if update was successful
865-
"""
866-
try:
867-
metadata_key = self.get_file_metadata_key(session_id, file_id)
868-
now = datetime.utcnow().isoformat()
869-
870-
# Update multiple fields atomically
871-
updates = {
872-
"state_hash": state_hash,
873-
"last_used_at": now,
874-
}
875-
if execution_id:
876-
updates["execution_id"] = execution_id
877-
878-
await self.redis_client.hset(metadata_key, mapping=updates)
879-
880-
logger.debug(
881-
"Updated file state hash",
882-
session_id=session_id[:12],
883-
file_id=file_id,
884-
state_hash=state_hash[:12],
885-
)
886-
return True
887-
except Exception as e:
888-
logger.error(
889-
"Failed to update file state hash",
890-
error=str(e),
891-
session_id=session_id,
892-
file_id=file_id,
893-
)
894-
return False
895-
896802
async def update_file_content(
897803
self,
898804
session_id: str,
899805
file_id: str,
900806
content: bytes,
901-
state_hash: Optional[str] = None,
902-
execution_id: Optional[str] = None,
903807
) -> bool:
904808
"""Update the content of an existing file.
905809
@@ -910,8 +814,6 @@ async def update_file_content(
910814
session_id: Session identifier
911815
file_id: File identifier
912816
content: New file content as bytes
913-
state_hash: Optional SHA256 hash of the Python state
914-
execution_id: Optional ID of the execution that modified this file
915817
916818
Returns:
917819
True if update was successful
@@ -955,15 +857,9 @@ async def update_file_content(
955857
)
956858

957859
# Update metadata
958-
now = datetime.utcnow().isoformat()
959860
updates = {
960861
"size": len(content),
961-
"last_used_at": now,
962862
}
963-
if state_hash:
964-
updates["state_hash"] = state_hash
965-
if execution_id:
966-
updates["execution_id"] = execution_id
967863

968864
metadata_key = self.get_file_metadata_key(session_id, file_id)
969865
await self.redis_client.hset(metadata_key, mapping=updates)

0 commit comments

Comments
 (0)