Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/api/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ async def upload_file(
# Note: Production API returns different format with fileId instead of id
return {
"message": "success",
"storage_session_id": session_id,
"session_id": session_id,
"files": [
{"filename": file["name"], "fileId": file["id"]}
Expand Down Expand Up @@ -249,7 +250,10 @@ async def upload_files_batch(
entity_id: Optional[str] = (
entity_id_raw if isinstance(entity_id_raw, str) and entity_id_raw else None
)
is_agent_file = entity_id is not None
kind_raw = form.get("kind")
is_agent_file = entity_id is not None or (
isinstance(kind_raw, str) and kind_raw in ("skill", "agent")
)

read_only_raw = form.get("read_only")
is_read_only = isinstance(read_only_raw, str) and read_only_raw.lower() in (
Expand Down Expand Up @@ -339,6 +343,7 @@ async def upload_files_batch(

return {
"message": message,
"storage_session_id": session_id,
"session_id": session_id,
"files": results,
"succeeded": succeeded,
Expand All @@ -353,6 +358,18 @@ async def list_files(
None,
description="Detail level: 'simple' for basic info, otherwise full details",
),
kind: Optional[str] = Query(
None,
description="Resource kind filter: 'skill', 'agent', or 'user'",
),
id: Optional[str] = Query(
None,
description="Resource id for scoped file listing",
),
version: Optional[int] = Query(
None,
description="Resource version (only meaningful when kind=skill)",
),
file_service: FileServiceDep = None,
):
"""List all files in a session with optional detail parameter - LibreChat compatible."""
Expand Down Expand Up @@ -408,6 +425,7 @@ async def list_files(
{
"name": f"{session_id}/{file_info.file_id}",
"id": file_info.file_id,
"storage_session_id": session_id,
"session_id": session_id,
"content": None, # Not returned in list
"size": file_info.size,
Expand Down
25 changes: 21 additions & 4 deletions src/models/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,45 @@
from typing import Dict, List, Optional, Any

# Third-party imports
from pydantic import BaseModel, Field
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field


class FileRef(BaseModel):
"""File reference model for execution response."""

model_config = ConfigDict(populate_by_name=True)

id: str
name: str
path: Optional[str] = None # Make path optional
session_id: Optional[str] = None # Session ID for cross-message file persistence
path: Optional[str] = None
session_id: Optional[str] = None
inherited: Optional[bool] = None
entity_id: Optional[str] = None
resource_id: Optional[str] = None
kind: Optional[str] = None
version: Optional[int] = None
modified_from: Optional[Dict[str, str]] = None

@computed_field # type: ignore[prop-decorator]
@property
def storage_session_id(self) -> Optional[str]:
return self.session_id


class RequestFile(BaseModel):
"""Request file model."""

model_config = ConfigDict(populate_by_name=True)

id: str
session_id: str
session_id: str = Field(
validation_alias=AliasChoices("storage_session_id", "session_id"),
)
name: str
entity_id: Optional[str] = None
resource_id: Optional[str] = None
kind: Optional[str] = None
version: Optional[int] = None


class ExecRequest(BaseModel):
Expand Down
15 changes: 12 additions & 3 deletions src/models/programmatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field, validator
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, validator

SUPPORTED_PTC_LANGUAGES = {"py", "bash"}

Expand Down Expand Up @@ -51,12 +51,21 @@ class PTCFileInput(BaseModel):
"""File payload for PTC initial execution.

Matches the LibreChat/librechat-agents CodeEnvFile shape:
{session_id, id, name}
{storage_session_id, id, name}
"""

model_config = ConfigDict(populate_by_name=True)

id: str = Field(..., description="File identifier")
name: str = Field(..., description="Original filename for the referenced file")
session_id: str = Field(..., description="Source session for a referenced file")
session_id: str = Field(
...,
description="Source session for a referenced file",
validation_alias=AliasChoices("storage_session_id", "session_id"),
)
resource_id: Optional[str] = None
kind: Optional[str] = None
version: Optional[int] = None


class ProgrammaticExecRequest(BaseModel):
Expand Down
3 changes: 2 additions & 1 deletion src/services/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,8 @@ async def _handle_generated_files(self, ctx: ExecutionContext) -> List[FileRef]:
if meta.get("modified_from_id"):
file_ref.modified_from = {
"id": meta["modified_from_id"],
"session_id": meta.get("modified_from_session_id") or "",
"storage_session_id": meta.get("modified_from_session_id")
or "",
}
generated.append(file_ref)
logger.debug(
Expand Down
10 changes: 5 additions & 5 deletions tests/functional/test_client_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def _normalize_artifact_files(result: dict) -> list[dict]:
session_id = result["session_id"]
return [
{
"session_id": file_info.get("session_id") or session_id,
"storage_session_id": file_info.get("storage_session_id") or session_id,
"id": file_info["id"],
"name": file_info["name"],
}
Expand All @@ -36,7 +36,7 @@ async def _fetch_runtime_file_refs(
file_id = name_parts[1].split(".")[0] if len(name_parts) > 1 else ""
file_references.append(
{
"session_id": session_id,
"storage_session_id": session_id,
"id": file_id,
"name": file_info["metadata"]["original-filename"],
}
Expand Down Expand Up @@ -128,7 +128,7 @@ async def test_uploaded_files_follow_runtime_session_when_first_exec_has_no_outp
data={"entity_id": unique_entity_id},
)
assert upload.status_code == 200, upload.text
upload_session_id = upload.json()["session_id"]
upload_session_id = upload.json()["storage_session_id"]
upload_refs = await _fetch_runtime_file_refs(
async_client, auth_headers, upload_session_id
)
Expand Down Expand Up @@ -164,7 +164,7 @@ async def test_uploaded_files_survive_runtime_fallback_after_outputs_are_generat
data={"entity_id": unique_entity_id},
)
assert upload.status_code == 200, upload.text
upload_session_id = upload.json()["session_id"]
upload_session_id = upload.json()["storage_session_id"]
upload_refs = await _fetch_runtime_file_refs(
async_client, auth_headers, upload_session_id
)
Expand Down Expand Up @@ -298,7 +298,7 @@ async def test_ptc_fallback_refs_preserve_files_after_continuation(
)
assert upload.status_code == 200, upload.text
upload_result = upload.json()
session_id = upload_result["session_id"]
session_id = upload_result["storage_session_id"]

initial = await _start_ptc_like_runtime(
async_client,
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/test_concurrent_file_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def test_large_file_exec_does_not_block_concurrent_requests(
assert upload_resp.status_code == 200, f"Upload failed: {upload_resp.text}"

result = upload_resp.json()
session_id = result["session_id"]
session_id = result["storage_session_id"]
file_id = result["files"][0]["fileId"]
filename = result["files"][0]["filename"]

Expand All @@ -72,7 +72,7 @@ async def exec_with_file(idx: int) -> tuple:
"lang": "py",
"session_id": session_id,
"files": [
{"id": file_id, "session_id": session_id, "name": filename}
{"id": file_id, "storage_session_id": session_id, "name": filename}
],
},
timeout=60.0,
Expand Down
6 changes: 3 additions & 3 deletions tests/functional/test_exec_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ async def test_file_ref_does_not_leak_session_across_users(
)
assert upload.status_code == 200
upload_data = upload.json()
upload_session = upload_data["session_id"]
upload_session = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -187,7 +187,7 @@ async def test_file_ref_does_not_leak_session_across_users(
"files": [
{
"id": file_id,
"session_id": upload_session,
"storage_session_id": upload_session,
"name": filename,
}
],
Expand All @@ -208,7 +208,7 @@ async def test_file_ref_does_not_leak_session_across_users(
"files": [
{
"id": file_id,
"session_id": upload_session,
"storage_session_id": upload_session,
"name": filename,
}
],
Expand Down
34 changes: 17 additions & 17 deletions tests/functional/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async def test_upload_single_file(
result = response.json()

assert result["message"] == "success"
assert "session_id" in result
assert "storage_session_id" in result
assert len(result["files"]) == 1
assert "fileId" in result["files"][0]
assert "filename" in result["files"][0]
Expand All @@ -50,10 +50,10 @@ async def test_librechat_upload_format(
assert response.json()["message"] == "success"

@pytest.mark.asyncio
async def test_upload_returns_session_id(
async def test_upload_returns_storage_session_id(
self, async_client, auth_headers, unique_entity_id
):
"""Upload response includes session_id."""
"""Upload response includes storage_session_id."""
files = {"files": ("test.txt", b"content", "text/plain")}
data = {"entity_id": unique_entity_id}

Expand All @@ -65,8 +65,8 @@ async def test_upload_returns_session_id(
)

result = response.json()
assert "session_id" in result
assert len(result["session_id"]) > 0
assert "storage_session_id" in result
assert len(result["storage_session_id"]) > 0

@pytest.mark.asyncio
async def test_upload_returns_file_info(
Expand Down Expand Up @@ -120,7 +120,7 @@ async def test_list_files_after_upload(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# List files
response = await async_client.get(
Expand All @@ -145,7 +145,7 @@ async def test_list_files_detail_simple(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# List with simple detail
response = await async_client.get(
Expand All @@ -170,7 +170,7 @@ async def test_list_files_detail_summary(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# List with summary detail
response = await async_client.get(
Expand Down Expand Up @@ -204,7 +204,7 @@ async def test_detail_full_has_original_filename_metadata(
data={"entity_id": unique_entity_id},
)
assert upload.status_code == 200
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# Get full detail
response = await async_client.get(
Expand Down Expand Up @@ -237,7 +237,7 @@ async def test_detail_full_has_required_fields(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

response = await async_client.get(
f"/files/{session_id}?detail=full",
Expand Down Expand Up @@ -278,7 +278,7 @@ async def test_download_uploaded_file(
data={"entity_id": unique_entity_id},
)

session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]
file_id = upload.json()["files"][0]["fileId"]

response = await async_client.get(
Expand Down Expand Up @@ -322,7 +322,7 @@ async def test_uploaded_file_readable_at_mnt_data(
)
assert upload.status_code == 200
upload_data = upload.json()
session_id = upload_data["session_id"]
session_id = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -341,7 +341,7 @@ async def test_uploaded_file_readable_at_mnt_data(
),
"lang": "py",
"session_id": session_id,
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
},
)

Expand All @@ -366,7 +366,7 @@ async def test_uploaded_file_readable_via_relative_path(
data={"entity_id": unique_entity_id},
)
upload_data = upload.json()
session_id = upload_data["session_id"]
session_id = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -377,7 +377,7 @@ async def test_uploaded_file_readable_via_relative_path(
"code": f"print(open('{filename}').read())",
"lang": "py",
"session_id": session_id,
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
},
)

Expand All @@ -400,7 +400,7 @@ async def test_upload_execute_generate_download(
data={"entity_id": unique_entity_id},
)
upload_data = upload.json()
session_id = upload_data["session_id"]
session_id = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -424,7 +424,7 @@ async def test_upload_execute_generate_download(
),
"lang": "py",
"session_id": session_id,
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
},
)

Expand Down
2 changes: 1 addition & 1 deletion tests/functional/test_generated_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def test_generated_file_is_reused_on_follow_up_execution(
"files": [
{
"id": generated_file["id"],
"session_id": generate_result["session_id"],
"storage_session_id": generate_result["session_id"],
"name": generated_file["name"],
}
],
Expand Down
Loading
Loading