Skip to content

Commit 7567f3e

Browse files
committed
feat: Enhance file upload and listing with resource kind and version support
Updated the upload_files_batch and list_files functions to include support for resource kind and version parameters. The upload logic now marks files as agent files based on the kind provided. Additionally, the RequestFile and FileRef models have been updated to accept resource_id, kind, and version fields, with corresponding tests added to validate these changes.
1 parent 16bb6b6 commit 7567f3e

5 files changed

Lines changed: 115 additions & 1 deletion

File tree

src/api/files.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,10 @@ async def upload_files_batch(
250250
entity_id: Optional[str] = (
251251
entity_id_raw if isinstance(entity_id_raw, str) and entity_id_raw else None
252252
)
253-
is_agent_file = entity_id is not None
253+
kind_raw = form.get("kind")
254+
is_agent_file = entity_id is not None or (
255+
isinstance(kind_raw, str) and kind_raw in ("skill", "agent")
256+
)
254257

255258
read_only_raw = form.get("read_only")
256259
is_read_only = isinstance(read_only_raw, str) and read_only_raw.lower() in (
@@ -355,6 +358,18 @@ async def list_files(
355358
None,
356359
description="Detail level: 'simple' for basic info, otherwise full details",
357360
),
361+
kind: Optional[str] = Query(
362+
None,
363+
description="Resource kind filter: 'skill', 'agent', or 'user'",
364+
),
365+
id: Optional[str] = Query(
366+
None,
367+
description="Resource id for scoped file listing",
368+
),
369+
version: Optional[int] = Query(
370+
None,
371+
description="Resource version (only meaningful when kind=skill)",
372+
),
358373
file_service: FileServiceDep = None,
359374
):
360375
"""List all files in a session with optional detail parameter - LibreChat compatible."""

src/models/exec.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class FileRef(BaseModel):
1919
session_id: Optional[str] = None
2020
inherited: Optional[bool] = None
2121
entity_id: Optional[str] = None
22+
resource_id: Optional[str] = None
23+
kind: Optional[str] = None
24+
version: Optional[int] = None
2225
modified_from: Optional[Dict[str, str]] = None
2326

2427
@computed_field # type: ignore[prop-decorator]
@@ -38,6 +41,9 @@ class RequestFile(BaseModel):
3841
)
3942
name: str
4043
entity_id: Optional[str] = None
44+
resource_id: Optional[str] = None
45+
kind: Optional[str] = None
46+
version: Optional[int] = None
4147

4248

4349
class ExecRequest(BaseModel):

src/models/programmatic.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ class PTCFileInput(BaseModel):
6363
description="Source session for a referenced file",
6464
validation_alias=AliasChoices("storage_session_id", "session_id"),
6565
)
66+
resource_id: Optional[str] = None
67+
kind: Optional[str] = None
68+
version: Optional[int] = None
6669

6770

6871
class ProgrammaticExecRequest(BaseModel):

tests/integration/test_librechat_compat.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2041,6 +2041,50 @@ def test_nested_filename_preserved_in_response(
20412041
# The stored filename also preserves the path so S3/sandbox round-trip works.
20422042
assert "skills/weather_lookup/SKILL.md" in setup_mocks["stored"]
20432043

2044+
def test_kind_skill_marks_files_as_agent(self, client, auth_headers, setup_mocks):
2045+
"""kind=skill bypasses extension whitelist for skill-priming uploads."""
2046+
files = [
2047+
("file", ("schema.xsd", io.BytesIO(b"<xs:schema/>"), "application/xml"))
2048+
]
2049+
data = {"kind": "skill", "id": "skill_abc123", "version": "3"}
2050+
response = client.post(
2051+
"/upload/batch", files=files, data=data, headers=auth_headers
2052+
)
2053+
2054+
assert response.status_code == 200
2055+
store = setup_mocks["file_service"].store_uploaded_file
2056+
assert store.await_count == 1
2057+
kwargs = store.await_args.kwargs
2058+
assert kwargs["is_agent_file"] is True
2059+
2060+
def test_kind_agent_marks_files_as_agent(self, client, auth_headers, setup_mocks):
2061+
"""kind=agent also bypasses extension whitelist."""
2062+
files = [
2063+
("file", ("config.toml", io.BytesIO(b"[tool]"), "application/toml"))
2064+
]
2065+
data = {"kind": "agent", "id": "agent_xyz"}
2066+
response = client.post(
2067+
"/upload/batch", files=files, data=data, headers=auth_headers
2068+
)
2069+
2070+
assert response.status_code == 200
2071+
store = setup_mocks["file_service"].store_uploaded_file
2072+
assert store.await_count == 1
2073+
kwargs = store.await_args.kwargs
2074+
assert kwargs["is_agent_file"] is True
2075+
2076+
def test_batch_response_includes_storage_session_id(
2077+
self, client, auth_headers, setup_mocks
2078+
):
2079+
"""LibreChat validates storage_session_id in batch upload response."""
2080+
files = [("file", ("data.csv", io.BytesIO(b"a,b"), "text/csv"))]
2081+
response = client.post("/upload/batch", files=files, headers=auth_headers)
2082+
2083+
assert response.status_code == 200
2084+
body = response.json()
2085+
assert "storage_session_id" in body
2086+
assert body["storage_session_id"] == body["session_id"]
2087+
20442088

20452089
# =============================================================================
20462090
# GET /sessions/{session_id}/objects/{file_id} — liveness probe

tests/unit/test_exec_models.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,52 @@ def test_fileref_emits_both_session_id_and_storage_session_id(self):
9191
assert dumped["session_id"] == "sess-1"
9292

9393

94+
class TestCodeEnvFileFields:
95+
"""RequestFile and FileRef accept resource_id, kind, and version
96+
fields sent by the librechat-agents CodeEnvFile type."""
97+
98+
def test_request_file_accepts_code_env_file_shape(self):
99+
rf = RequestFile(
100+
id="fid",
101+
storage_session_id="sess",
102+
name="data.csv",
103+
resource_id="res-1",
104+
kind="skill",
105+
version=3,
106+
)
107+
assert rf.session_id == "sess"
108+
assert rf.resource_id == "res-1"
109+
assert rf.kind == "skill"
110+
assert rf.version == 3
111+
112+
def test_request_file_code_env_fields_optional(self):
113+
rf = RequestFile(id="fid", session_id="sess", name="data.csv")
114+
assert rf.resource_id is None
115+
assert rf.kind is None
116+
assert rf.version is None
117+
118+
def test_fileref_resource_id_kind_version(self):
119+
ref = FileRef(
120+
id="fid",
121+
name="out.png",
122+
session_id="sess-1",
123+
resource_id="res-1",
124+
kind="skill",
125+
version=2,
126+
)
127+
dumped = ref.model_dump(exclude_none=True)
128+
assert dumped["resource_id"] == "res-1"
129+
assert dumped["kind"] == "skill"
130+
assert dumped["version"] == 2
131+
132+
def test_fileref_code_env_fields_excluded_when_none(self):
133+
ref = FileRef(id="fid", name="out.png", session_id="sess-1")
134+
dumped = ref.model_dump(exclude_none=True)
135+
assert "resource_id" not in dumped
136+
assert "kind" not in dumped
137+
assert "version" not in dumped
138+
139+
94140
class TestExecRequestTimeout:
95141
"""ExecRequest.timeout: optional, milliseconds, range 1000-300000."""
96142

0 commit comments

Comments
 (0)