diff --git a/docs/src/llms.txt b/docs/src/llms.txt
index bc988247e..85e3d3a84 100644
--- a/docs/src/llms.txt
+++ b/docs/src/llms.txt
@@ -422,8 +422,8 @@ The SDK docs below are for generated-code editing and reference. They are not th
### File Storage
- [FileStorage](https://docs.notte.cc/sdk-reference/manual/file_storage.md): Upload and download files for your automations
-- [download](https://docs.notte.cc/sdk-reference/remotefilestorage/download.md): Stores a file that has been downloaded from a website in the current session
-- [upload](https://docs.notte.cc/sdk-reference/remotefilestorage/upload.md): Upload a file from your local machine to storage
+- [download](https://docs.notte.cc/sdk-reference/remotefilestorage/download.md): No description available
+- [upload](https://docs.notte.cc/sdk-reference/remotefilestorage/upload.md): No description available
- [list_downloaded_files](https://docs.notte.cc/sdk-reference/remotefilestorage/list_downloaded_files.md): List all files in the download_dir
- [list_uploaded_files](https://docs.notte.cc/sdk-reference/remotefilestorage/list_uploaded_files.md): List all files from the upload_dir
diff --git a/docs/src/sdk-reference/manual/session.mdx b/docs/src/sdk-reference/manual/session.mdx
index f5ea05a75..65e5ff07c 100644
--- a/docs/src/sdk-reference/manual/session.mdx
+++ b/docs/src/sdk-reference/manual/session.mdx
@@ -111,10 +111,6 @@ You can use the default parameters to create your session, or customize them:
The CDP URL of another remote session provider.
-
- Whether FileStorage should be attached to the session.
-
-
The type of screenshot to use for the session.
diff --git a/docs/src/sdk-reference/misc/filesource.mdx b/docs/src/sdk-reference/misc/filesource.mdx
new file mode 100644
index 000000000..a9483c6d0
--- /dev/null
+++ b/docs/src/sdk-reference/misc/filesource.mdx
@@ -0,0 +1,18 @@
+---
+title: "FileSource"
+description: "Enum where members are also (and must be) strings"
+---
+
+
+
+## Methods
+
+
+## Inheritance
+
+Inherits from: StrEnum
+
+
+## Module
+
+`notte_sdk.types`
diff --git a/docs/src/sdk-reference/misc/listfilesresponse.mdx b/docs/src/sdk-reference/misc/listfilesresponse.mdx
new file mode 100644
index 000000000..e76b30c1c
--- /dev/null
+++ b/docs/src/sdk-reference/misc/listfilesresponse.mdx
@@ -0,0 +1,26 @@
+---
+title: "ListFilesResponse"
+description: ""
+---
+
+
+
+## Fields
+
+
+ List of non-expired session files
+
+
+
+
+
+
+
+
+
+
+
+
+## Module
+
+`notte_sdk.types`
diff --git a/docs/src/sdk-reference/misc/remotefilestorage.mdx b/docs/src/sdk-reference/misc/remotefilestorage.mdx
index 505d8a16b..f6e438f60 100644
--- a/docs/src/sdk-reference/misc/remotefilestorage.mdx
+++ b/docs/src/sdk-reference/misc/remotefilestorage.mdx
@@ -13,7 +13,7 @@ description: "Base class for storage implementations that handle upload and down
alist_downloaded_files() -> list[FileInfo]
```
-List files that have been downloaded into storage by the agents
+List all files in the download_dir
**Returns:**
@@ -27,7 +27,7 @@ List files that have been downloaded into storage by the agents
alist_uploaded_files() -> list[FileInfo]
```
-List files that have been uploaded to storage
+List all files from the upload_dir
**Returns:**
@@ -35,37 +35,37 @@ List files that have been uploaded to storage
---
-### download
+### delete
```python
-download(file_name: str, local_dir: str, force: bool = False) -> bool
+delete(file_id: str) -> None
```
-Stores a file that has been downloaded from a website in the current session
+---
+
+### download
+
+```python
+download(file_id: str, local_dir: str = ., force: bool = False) -> str
+```
**Returns:**
-`bool`
+`str`
---
-### download_uploaded_file
+### for_session
```python
-download_uploaded_file(file_name: str, local_dir: str, force: bool = False) -> bool
+for_session(session_id: str) -> RemoteFileStorage
```
-Downloads a user-uploaded file to the local filesystem without requiring a session attachment
-
-**Parameters:**
-
-- `file_name`: The name of the uploaded file to download.
-- `local_dir`: The directory to download the file to.
-- `force`: Overwrite an existing destination file. Defaults to ``False``; set ``True`` to replace it.
+Bind this storage once, cloning it when another session already owns it
**Returns:**
-`bool`
+[`RemoteFileStorage`](/sdk-reference/misc/remotefilestorage)[`RemoteFileStorage`](/sdk-reference/misc/remotefilestorage.md)
---
@@ -97,6 +97,18 @@ Return LLM instructions to append to the prompt
---
+### list
+
+```python
+list(source: FileSource | str | None = None, limit: int = 100, offset: int = 0) -> SessionFilesPage
+```
+
+**Returns:**
+
+[`SessionFilesPage`](/sdk-reference/misc/sessionfilespage)[`SessionFilesPage`](/sdk-reference/misc/sessionfilespage.md)
+
+---
+
### list_downloaded_files
```python
@@ -142,7 +154,7 @@ Stores a file from the local path
### set_session_id
```python
-set_session_id(id: str) -> None
+set_session_id(session_id: str) -> None
```
---
@@ -150,14 +162,12 @@ set_session_id(id: str) -> None
### upload
```python
-upload(file_path: str, upload_file_name: str | None = None) -> bool
+upload(file_path: str, upload_file_name: str | None = None) -> SessionFile
```
-Upload a file from your local machine to storage
-
**Returns:**
-`bool`
+[`SessionFile`](/sdk-reference/misc/sessionfile)[`SessionFile`](/sdk-reference/misc/sessionfile.md)
---
diff --git a/docs/src/sdk-reference/misc/sessionfile.mdx b/docs/src/sdk-reference/misc/sessionfile.mdx
new file mode 100644
index 000000000..1c23d92b6
--- /dev/null
+++ b/docs/src/sdk-reference/misc/sessionfile.mdx
@@ -0,0 +1,40 @@
+---
+title: "SessionFile"
+description: ""
+---
+
+
+
+## Fields
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Module
+
+`notte_sdk.types`
diff --git a/docs/src/sdk-reference/misc/sessionfilespage.mdx b/docs/src/sdk-reference/misc/sessionfilespage.mdx
new file mode 100644
index 000000000..17140a7ce
--- /dev/null
+++ b/docs/src/sdk-reference/misc/sessionfilespage.mdx
@@ -0,0 +1,26 @@
+---
+title: "SessionFilesPage"
+description: ""
+---
+
+
+
+## Fields
+
+
+ List of non-expired session files
+
+
+
+
+
+
+
+
+
+
+
+
+## Module
+
+`notte_sdk.types`
diff --git a/docs/src/sdk-reference/misc/sessionresponse.mdx b/docs/src/sdk-reference/misc/sessionresponse.mdx
index 26111a793..b76e0c261 100644
--- a/docs/src/sdk-reference/misc/sessionresponse.mdx
+++ b/docs/src/sdk-reference/misc/sessionresponse.mdx
@@ -58,10 +58,6 @@ description: ""
-
- Whether FileStorage was attached to the session.
-
-
Total byte usage for network requests.
diff --git a/docs/src/sdk-reference/remotefilestorage/alist_downloaded_files.mdx b/docs/src/sdk-reference/remotefilestorage/alist_downloaded_files.mdx
index e1bbd018b..dea117f08 100644
--- a/docs/src/sdk-reference/remotefilestorage/alist_downloaded_files.mdx
+++ b/docs/src/sdk-reference/remotefilestorage/alist_downloaded_files.mdx
@@ -1,16 +1,10 @@
---
title: "alist_downloaded_files"
-description: "List files that have been downloaded into storage by the agents"
+description: "List all files in the download_dir"
---
-```python
-storage = notte.FileStorage(session_id="")
-# list the files that have been downloaded from storage
-files = storage.list_downloaded_files()
-```
-
## Returns
diff --git a/docs/src/sdk-reference/remotefilestorage/alist_uploaded_files.mdx b/docs/src/sdk-reference/remotefilestorage/alist_uploaded_files.mdx
index 53987f1f1..7db34b70c 100644
--- a/docs/src/sdk-reference/remotefilestorage/alist_uploaded_files.mdx
+++ b/docs/src/sdk-reference/remotefilestorage/alist_uploaded_files.mdx
@@ -1,16 +1,10 @@
---
title: "alist_uploaded_files"
-description: "List files that have been uploaded to storage"
+description: "List all files from the upload_dir"
---
-```python
-storage = notte.FileStorage()
-# list the files that have been uploaded to storage
-files = storage.list_uploaded_files()
-```
-
## Returns
diff --git a/docs/src/sdk-reference/remotefilestorage/delete.mdx b/docs/src/sdk-reference/remotefilestorage/delete.mdx
new file mode 100644
index 000000000..be28c6f74
--- /dev/null
+++ b/docs/src/sdk-reference/remotefilestorage/delete.mdx
@@ -0,0 +1,16 @@
+---
+title: "delete"
+description: "No description available"
+---
+
+
+
+
+## Parameters
+
+
+
+
+## Returns
+
+`None`
diff --git a/docs/src/sdk-reference/remotefilestorage/download.mdx b/docs/src/sdk-reference/remotefilestorage/download.mdx
index f9d8e5b0a..7a332dfbd 100644
--- a/docs/src/sdk-reference/remotefilestorage/download.mdx
+++ b/docs/src/sdk-reference/remotefilestorage/download.mdx
@@ -1,25 +1,17 @@
---
title: "download"
-description: "Stores a file that has been downloaded from a website in the current session"
+description: "No description available"
---
import AgentMdNotice from '/partials/agent-md-notice.mdx';
-```python
-file_storage = notte.FileStorage("")
-# file.pdf has been downloaded by an agent in the session
-# you can download it to your local machine using:
-file_storage.download(file_name="file.pdf", local_dir="")
-```
-
-
## Parameters
-
+
-
+
@@ -27,4 +19,4 @@ file_storage.download(file_name="file.pdf", local_dir="")
## Returns
-`bool`
+`str`
diff --git a/docs/src/sdk-reference/remotefilestorage/for_session.mdx b/docs/src/sdk-reference/remotefilestorage/for_session.mdx
new file mode 100644
index 000000000..4d35e5d5e
--- /dev/null
+++ b/docs/src/sdk-reference/remotefilestorage/for_session.mdx
@@ -0,0 +1,16 @@
+---
+title: "for_session"
+description: "Bind this storage once, cloning it when another session already owns it"
+---
+
+
+
+
+## Parameters
+
+
+
+
+## Returns
+
+[`RemoteFileStorage`](/sdk-reference/misc/remotefilestorage)[`RemoteFileStorage`](/sdk-reference/misc/remotefilestorage.md)
diff --git a/docs/src/sdk-reference/remotefilestorage/index.mdx b/docs/src/sdk-reference/remotefilestorage/index.mdx
index d310bfbe8..1ed55e961 100644
--- a/docs/src/sdk-reference/remotefilestorage/index.mdx
+++ b/docs/src/sdk-reference/remotefilestorage/index.mdx
@@ -34,7 +34,7 @@ Base class for storage implementations that handle upload and download file stor
icon="function"
href="/sdk-reference/remotefilestorage/alist_downloaded_files"
>
- List files that have been downloaded into storage by the agents
+ List all files in the download_dir
@@ -43,7 +43,7 @@ Base class for storage implementations that handle upload and download file stor
icon="function"
href="/sdk-reference/remotefilestorage/alist_downloaded_files.md"
>
- List files that have been downloaded into storage by the agents
+ List all files in the download_dir
@@ -52,7 +52,7 @@ Base class for storage implementations that handle upload and download file stor
icon="function"
href="/sdk-reference/remotefilestorage/alist_uploaded_files"
>
- List files that have been uploaded to storage
+ List all files from the upload_dir
@@ -61,7 +61,25 @@ Base class for storage implementations that handle upload and download file stor
icon="function"
href="/sdk-reference/remotefilestorage/alist_uploaded_files.md"
>
- List files that have been uploaded to storage
+ List all files from the upload_dir
+
+
+
+
+ No description available
+
+
+
+
+ No description available
@@ -70,7 +88,7 @@ Base class for storage implementations that handle upload and download file stor
icon="function"
href="/sdk-reference/remotefilestorage/download"
>
- Stores a file that has been downloaded from a website in the current session
+ No description available
@@ -79,25 +97,25 @@ Base class for storage implementations that handle upload and download file stor
icon="function"
href="/sdk-reference/remotefilestorage/download.md"
>
- Stores a file that has been downloaded from a website in the current session
+ No description available
- Downloads a user-uploaded file to the local filesystem without requiring a session attachment
+ Bind this storage once, cloning it when another session already owns it
- Downloads a user-uploaded file to the local filesystem without requiring a session attachment
+ Bind this storage once, cloning it when another session already owns it
@@ -136,6 +154,24 @@ Base class for storage implementations that handle upload and download file stor
Return LLM instructions to append to the prompt
+
+
+ No description available
+
+
+
+
+ No description available
+
+
- Upload a file from your local machine to storage
+ No description available
@@ -223,7 +259,7 @@ Base class for storage implementations that handle upload and download file stor
icon="function"
href="/sdk-reference/remotefilestorage/upload.md"
>
- Upload a file from your local machine to storage
+ No description available
diff --git a/docs/src/sdk-reference/remotefilestorage/list.mdx b/docs/src/sdk-reference/remotefilestorage/list.mdx
new file mode 100644
index 000000000..5052ec3f3
--- /dev/null
+++ b/docs/src/sdk-reference/remotefilestorage/list.mdx
@@ -0,0 +1,22 @@
+---
+title: "list"
+description: "No description available"
+---
+
+
+
+
+## Parameters
+
+
+
+
+
+
+
+
+
+
+## Returns
+
+[`SessionFilesPage`](/sdk-reference/misc/sessionfilespage)[`SessionFilesPage`](/sdk-reference/misc/sessionfilespage.md)
diff --git a/docs/src/sdk-reference/remotefilestorage/set_session_id.mdx b/docs/src/sdk-reference/remotefilestorage/set_session_id.mdx
index 8c5af2ac6..a04cfa705 100644
--- a/docs/src/sdk-reference/remotefilestorage/set_session_id.mdx
+++ b/docs/src/sdk-reference/remotefilestorage/set_session_id.mdx
@@ -8,7 +8,7 @@ description: "No description available"
## Parameters
-
+
## Returns
diff --git a/docs/src/sdk-reference/remotefilestorage/upload.mdx b/docs/src/sdk-reference/remotefilestorage/upload.mdx
index 833abd2c6..e45b31927 100644
--- a/docs/src/sdk-reference/remotefilestorage/upload.mdx
+++ b/docs/src/sdk-reference/remotefilestorage/upload.mdx
@@ -1,21 +1,11 @@
---
title: "upload"
-description: "Upload a file from your local machine to storage"
+description: "No description available"
---
import AgentMdNotice from '/partials/agent-md-notice.mdx';
-This file will then be available to the agent in the current session.
-
-```python
-storage = notte.FileStorage()
-with notte.Session(storage=storage) as session:
- # make the file available to the agent in the current session
- storage.upload(file_path="")
-```
-
-
## Parameters
@@ -26,4 +16,4 @@ with notte.Session(storage=storage) as session:
## Returns
-`bool`
+[`SessionFile`](/sdk-reference/misc/sessionfile)[`SessionFile`](/sdk-reference/misc/sessionfile.md)
diff --git a/docs/src/sdk-reference/remotesession/__init__.mdx b/docs/src/sdk-reference/remotesession/__init__.mdx
index 9332cfd12..bc9a9c77b 100644
--- a/docs/src/sdk-reference/remotesession/__init__.mdx
+++ b/docs/src/sdk-reference/remotesession/__init__.mdx
@@ -80,10 +80,6 @@ RemoteSession instance configured with the specified parameters.
The CDP URL of another remote session provider.
-
- Whether FileStorage should be attached to the session.
-
-
The type of screenshot to use for the session.
diff --git a/docs/src/snippets/file-storage/attach_before_starting.mdx b/docs/src/snippets/file-storage/attach_before_starting.mdx
index 8cb4f38e9..ca00761fd 100644
--- a/docs/src/snippets/file-storage/attach_before_starting.mdx
+++ b/docs/src/snippets/file-storage/attach_before_starting.mdx
@@ -6,11 +6,7 @@ from notte_sdk import NotteClient
client = NotteClient()
-# Correct
-storage = client.FileStorage()
-storage.upload("file.pdf")
-
-with client.Session(storage=storage) as session:
- # Storage is available
- pass
+with client.Session() as session:
+ # Storage is always available and scoped to the session.
+ session.storage.upload("file.pdf")
```
diff --git a/docs/src/snippets/file-storage/check_downloads.mdx b/docs/src/snippets/file-storage/check_downloads.mdx
index 38fcbb6ff..40762a9c6 100644
--- a/docs/src/snippets/file-storage/check_downloads.mdx
+++ b/docs/src/snippets/file-storage/check_downloads.mdx
@@ -5,17 +5,15 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
agent = client.Agent(session=session)
agent.run(task="Download all invoices")
# Check what was downloaded
-files = storage.list_downloaded_files()
+files = session.storage.list("session_download").files
if not files:
print("No files were downloaded")
else:
for f in files:
- _ = storage.download(file_name=f.name, local_dir="./invoices")
+ _ = session.storage.download(f.id, local_dir="./invoices")
```
diff --git a/docs/src/snippets/file-storage/descriptive_filenames.mdx b/docs/src/snippets/file-storage/descriptive_filenames.mdx
index f468ab25c..451343e0e 100644
--- a/docs/src/snippets/file-storage/descriptive_filenames.mdx
+++ b/docs/src/snippets/file-storage/descriptive_filenames.mdx
@@ -7,8 +7,7 @@ from datetime import datetime
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-storage.upload("report.pdf", upload_file_name=f"report_{timestamp}.pdf")
+with client.Session() as session:
+ session.storage.upload("report.pdf", upload_file_name=f"report_{timestamp}.pdf")
```
diff --git a/docs/src/snippets/file-storage/downloading_files.mdx b/docs/src/snippets/file-storage/downloading_files.mdx
index 7af7a3531..1111af10c 100644
--- a/docs/src/snippets/file-storage/downloading_files.mdx
+++ b/docs/src/snippets/file-storage/downloading_files.mdx
@@ -5,17 +5,15 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
agent = client.Agent(session=session)
agent.run(task="Download the invoice from the account page")
# List downloaded files
-downloaded = storage.list_downloaded_files()
+downloaded = session.storage.list("session_download").files
print(f"Downloaded: {downloaded}")
# Download to local directory
for file in downloaded:
- storage.download(file_name=file.name, local_dir="./invoices")
+ session.storage.download(file.id, local_dir="./invoices")
```
diff --git a/docs/src/snippets/file-storage/force_overwrite.mdx b/docs/src/snippets/file-storage/force_overwrite.mdx
index 9395c6e60..6e8ad5c13 100644
--- a/docs/src/snippets/file-storage/force_overwrite.mdx
+++ b/docs/src/snippets/file-storage/force_overwrite.mdx
@@ -5,11 +5,6 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-storage.download(
- file_name="report.pdf",
- local_dir="./downloads",
- force=True, # Overwrite if exists
-)
+storage = client.FileStorage("session-id")
+storage.download("file-id", local_dir="./downloads", force=True)
```
diff --git a/docs/src/snippets/file-storage/quickstart.mdx b/docs/src/snippets/file-storage/quickstart.mdx
index c34d9d8e5..e0cb0504e 100644
--- a/docs/src/snippets/file-storage/quickstart.mdx
+++ b/docs/src/snippets/file-storage/quickstart.mdx
@@ -5,13 +5,9 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload a file before the session
-storage.upload("document.pdf")
-
-# Create session with storage attached
-with client.Session(storage=storage) as session:
+with client.Session() as session:
+ # Every file belongs to this session.
+ session.storage.upload("document.pdf")
agent = client.Agent(session=session)
result = agent.run(
@@ -19,6 +15,6 @@ with client.Session(storage=storage) as session:
)
# Download files the agent retrieved
-for file in storage.list_downloaded_files():
- storage.download(file_name=file.name, local_dir="./downloads")
+for file in session.storage.list("session_download").files:
+ session.storage.download(file.id, local_dir="./downloads")
```
diff --git a/docs/src/snippets/file-storage/uploading_files.mdx b/docs/src/snippets/file-storage/uploading_files.mdx
index d62ece2bc..0ed9cdec1 100644
--- a/docs/src/snippets/file-storage/uploading_files.mdx
+++ b/docs/src/snippets/file-storage/uploading_files.mdx
@@ -5,15 +5,8 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload a file (uses the original filename)
-storage.upload("report.pdf")
-
-# Upload with a custom name
-storage.upload("report.pdf", upload_file_name="quarterly_report.pdf")
-
-# List uploaded files
-files = storage.list_uploaded_files()
-print(f"Uploaded: {files}")
+with client.Session() as session:
+ session.storage.upload("report.pdf")
+ session.storage.upload("report.pdf", upload_file_name="quarterly_report.pdf")
+ print(session.storage.list(source="user_upload").files)
```
diff --git a/docs/src/snippets/file-storage/using_with_agents.mdx b/docs/src/snippets/file-storage/using_with_agents.mdx
index 02d818e42..d9fce2cb6 100644
--- a/docs/src/snippets/file-storage/using_with_agents.mdx
+++ b/docs/src/snippets/file-storage/using_with_agents.mdx
@@ -5,13 +5,9 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload files for the agent to use
-storage.upload("contract.pdf")
-storage.upload("signature.png")
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
+ session.storage.upload("contract.pdf")
+ session.storage.upload("signature.png")
agent = client.Agent(session=session, max_steps=15)
result = agent.run(
@@ -25,6 +21,6 @@ with client.Session(storage=storage) as session:
)
# Get the confirmation the agent downloaded
-for file in storage.list_downloaded_files():
- storage.download(file_name=file.name, local_dir="./signed")
+for file in session.storage.list(source="session_download").files:
+ session.storage.download(file.id, local_dir="./signed")
```
diff --git a/docs/src/snippets/file-storage/using_with_sessions.mdx b/docs/src/snippets/file-storage/using_with_sessions.mdx
index af5562b1b..9e654f2e9 100644
--- a/docs/src/snippets/file-storage/using_with_sessions.mdx
+++ b/docs/src/snippets/file-storage/using_with_sessions.mdx
@@ -5,12 +5,8 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload file
-storage.upload("data.csv")
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
+ session.storage.upload("data.csv")
session.execute(type="goto", url="https://example.com/import")
# Upload using the upload_file action
@@ -19,6 +15,6 @@ with client.Session(storage=storage) as session:
session.execute(type="click", selector="button.submit")
# Download any files
-for file in storage.list_downloaded_files():
- storage.download(file_name=file.name, local_dir="./results")
+for file in session.storage.list(source="session_download").files:
+ session.storage.download(file.id, local_dir="./results")
```
diff --git a/docs/src/testers/file-storage/attach_before_starting.py b/docs/src/testers/file-storage/attach_before_starting.py
index 3a9487741..7e79bd5aa 100644
--- a/docs/src/testers/file-storage/attach_before_starting.py
+++ b/docs/src/testers/file-storage/attach_before_starting.py
@@ -3,10 +3,6 @@
client = NotteClient()
-# Correct
-storage = client.FileStorage()
-storage.upload("file.pdf")
-
-with client.Session(storage=storage) as session:
- # Storage is available
- pass
+with client.Session() as session:
+ # Storage is always available and scoped to the session.
+ session.storage.upload("file.pdf")
diff --git a/docs/src/testers/file-storage/check_downloads.py b/docs/src/testers/file-storage/check_downloads.py
index dbc00e0fa..467c5fe1b 100644
--- a/docs/src/testers/file-storage/check_downloads.py
+++ b/docs/src/testers/file-storage/check_downloads.py
@@ -2,16 +2,14 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
agent = client.Agent(session=session)
agent.run(task="Download all invoices")
# Check what was downloaded
-files = storage.list_downloaded_files()
+files = session.storage.list("session_download").files
if not files:
print("No files were downloaded")
else:
for f in files:
- _ = storage.download(file_name=f.name, local_dir="./invoices")
+ _ = session.storage.download(f.id, local_dir="./invoices")
diff --git a/docs/src/testers/file-storage/descriptive_filenames.py b/docs/src/testers/file-storage/descriptive_filenames.py
index a756b60fe..1485d1982 100644
--- a/docs/src/testers/file-storage/descriptive_filenames.py
+++ b/docs/src/testers/file-storage/descriptive_filenames.py
@@ -4,7 +4,6 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
-storage.upload("report.pdf", upload_file_name=f"report_{timestamp}.pdf")
+with client.Session() as session:
+ session.storage.upload("report.pdf", upload_file_name=f"report_{timestamp}.pdf")
diff --git a/docs/src/testers/file-storage/downloading_files.py b/docs/src/testers/file-storage/downloading_files.py
index 6861df3de..905f03bde 100644
--- a/docs/src/testers/file-storage/downloading_files.py
+++ b/docs/src/testers/file-storage/downloading_files.py
@@ -2,16 +2,14 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
agent = client.Agent(session=session)
agent.run(task="Download the invoice from the account page")
# List downloaded files
-downloaded = storage.list_downloaded_files()
+downloaded = session.storage.list("session_download").files
print(f"Downloaded: {downloaded}")
# Download to local directory
for file in downloaded:
- storage.download(file_name=file.name, local_dir="./invoices")
+ session.storage.download(file.id, local_dir="./invoices")
diff --git a/docs/src/testers/file-storage/force_overwrite.py b/docs/src/testers/file-storage/force_overwrite.py
index 0b16ae5a8..8f70cdfae 100644
--- a/docs/src/testers/file-storage/force_overwrite.py
+++ b/docs/src/testers/file-storage/force_overwrite.py
@@ -2,10 +2,5 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-storage.download(
- file_name="report.pdf",
- local_dir="./downloads",
- force=True, # Overwrite if exists
-)
+storage = client.FileStorage("session-id")
+storage.download("file-id", local_dir="./downloads", force=True)
diff --git a/docs/src/testers/file-storage/quickstart.py b/docs/src/testers/file-storage/quickstart.py
index d7454379b..33ad96831 100644
--- a/docs/src/testers/file-storage/quickstart.py
+++ b/docs/src/testers/file-storage/quickstart.py
@@ -2,13 +2,9 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload a file before the session
-storage.upload("document.pdf")
-
-# Create session with storage attached
-with client.Session(storage=storage) as session:
+with client.Session() as session:
+ # Every file belongs to this session.
+ session.storage.upload("document.pdf")
agent = client.Agent(session=session)
result = agent.run(
@@ -16,5 +12,5 @@
)
# Download files the agent retrieved
-for file in storage.list_downloaded_files():
- storage.download(file_name=file.name, local_dir="./downloads")
+for file in session.storage.list("session_download").files:
+ session.storage.download(file.id, local_dir="./downloads")
diff --git a/docs/src/testers/file-storage/uploading_files.py b/docs/src/testers/file-storage/uploading_files.py
index 444d4fb49..752262453 100644
--- a/docs/src/testers/file-storage/uploading_files.py
+++ b/docs/src/testers/file-storage/uploading_files.py
@@ -2,14 +2,7 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload a file (uses the original filename)
-storage.upload("report.pdf")
-
-# Upload with a custom name
-storage.upload("report.pdf", upload_file_name="quarterly_report.pdf")
-
-# List uploaded files
-files = storage.list_uploaded_files()
-print(f"Uploaded: {files}")
+with client.Session() as session:
+ session.storage.upload("report.pdf")
+ session.storage.upload("report.pdf", upload_file_name="quarterly_report.pdf")
+ print(session.storage.list(source="user_upload").files)
diff --git a/docs/src/testers/file-storage/using_with_agents.py b/docs/src/testers/file-storage/using_with_agents.py
index 69cdb4d5d..78ddb98dd 100644
--- a/docs/src/testers/file-storage/using_with_agents.py
+++ b/docs/src/testers/file-storage/using_with_agents.py
@@ -2,13 +2,9 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload files for the agent to use
-storage.upload("contract.pdf")
-storage.upload("signature.png")
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
+ session.storage.upload("contract.pdf")
+ session.storage.upload("signature.png")
agent = client.Agent(session=session, max_steps=15)
result = agent.run(
@@ -22,5 +18,5 @@
)
# Get the confirmation the agent downloaded
-for file in storage.list_downloaded_files():
- storage.download(file_name=file.name, local_dir="./signed")
+for file in session.storage.list(source="session_download").files:
+ session.storage.download(file.id, local_dir="./signed")
diff --git a/docs/src/testers/file-storage/using_with_sessions.py b/docs/src/testers/file-storage/using_with_sessions.py
index d446674a4..de89c5df4 100644
--- a/docs/src/testers/file-storage/using_with_sessions.py
+++ b/docs/src/testers/file-storage/using_with_sessions.py
@@ -2,12 +2,8 @@
from notte_sdk import NotteClient
client = NotteClient()
-storage = client.FileStorage()
-
-# Upload file
-storage.upload("data.csv")
-
-with client.Session(storage=storage) as session:
+with client.Session() as session:
+ session.storage.upload("data.csv")
session.execute(type="goto", url="https://example.com/import")
# Upload using the upload_file action
@@ -16,5 +12,5 @@
session.execute(type="click", selector="button.submit")
# Download any files
-for file in storage.list_downloaded_files():
- storage.download(file_name=file.name, local_dir="./results")
+for file in session.storage.list(source="session_download").files:
+ session.storage.download(file.id, local_dir="./results")
diff --git a/packages/notte-browser/src/notte_browser/controller.py b/packages/notte-browser/src/notte_browser/controller.py
index d5230d88c..73a306980 100644
--- a/packages/notte-browser/src/notte_browser/controller.py
+++ b/packages/notte-browser/src/notte_browser/controller.py
@@ -551,10 +551,10 @@ async def execute_interaction_action(
with open(file_path, "wb") as f:
_ = f.write(file_bytes)
- res = await self.storage.set_file(str(file_path))
-
- if not res:
- raise FailedToDownloadFileError()
+ if not self.storage.captures_browser_downloads:
+ res = await self.storage.set_file(str(file_path))
+ if not res:
+ raise FailedToDownloadFileError()
case _:
raise ValueError(f"Unsupported action type: {type(action)}")
diff --git a/packages/notte-core/src/notte_core/storage.py b/packages/notte-core/src/notte_core/storage.py
index 676a9ed08..10b568557 100644
--- a/packages/notte-core/src/notte_core/storage.py
+++ b/packages/notte-core/src/notte_core/storage.py
@@ -44,6 +44,11 @@ def is_remote(self) -> bool:
"""Whether this storage implementation requires a remote server."""
return False
+ @property
+ def captures_browser_downloads(self) -> bool:
+ """Whether browser-native downloads are persisted out of band."""
+ return False
+
@abstractmethod
async def get_file(self, name: str) -> str | None:
"""Returns the local path for a file"""
diff --git a/packages/notte-sdk/src/notte_sdk/endpoints/base.py b/packages/notte-sdk/src/notte_sdk/endpoints/base.py
index 06255ee9a..5279b6858 100644
--- a/packages/notte-sdk/src/notte_sdk/endpoints/base.py
+++ b/packages/notte-sdk/src/notte_sdk/endpoints/base.py
@@ -436,7 +436,7 @@ def _request(
params=params,
timeout=timeout or self.DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
- if response.status_code != 200:
+ if not 200 <= response.status_code < 300:
# Check for 422 status code with Pydantic validation errors first
if response.status_code == 422:
should_upgrade, cached_version = self._should_suggest_upgrade()
@@ -464,6 +464,8 @@ def _request(
raise NotteAPIExecutionError(path=f"{self.base_endpoint_path}/{endpoint.path}", response=response)
raise NotteAPIError(path=f"{self.base_endpoint_path}/{endpoint.path}", response=response)
+ if response.status_code == 204 or getattr(response, "content", None) == b"":
+ return {}
response_dict: Any = response.json()
if "detail" in response_dict:
raise NotteAPIError(path=f"{self.base_endpoint_path}/{endpoint.path}", response=response)
diff --git a/packages/notte-sdk/src/notte_sdk/endpoints/files.py b/packages/notte-sdk/src/notte_sdk/endpoints/files.py
index ee15b9840..5bb4bb361 100644
--- a/packages/notte-sdk/src/notte_sdk/endpoints/files.py
+++ b/packages/notte-sdk/src/notte_sdk/endpoints/files.py
@@ -1,238 +1,139 @@
from __future__ import annotations
import os
+import tempfile
from pathlib import Path, PureWindowsPath
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, final
+import requests
from notte_core.common.cache import CacheDirectory, ensure_cache_directory
from notte_core.common.telemetry import track_usage
-from notte_core.storage import BaseStorage
-from typing_extensions import final, override
+from notte_core.storage import BaseStorage, FileInfo
+from typing_extensions import override
from notte_sdk.endpoints.base import BaseClient, NotteEndpoint
-from notte_sdk.types import (
- DownloadFileRequest,
- FileInfo,
- FileLinkResponse,
- FileUploadResponse,
- ListFilesResponse,
-)
+from notte_sdk.errors import NotteAPIError
+from notte_sdk.types import FileSource, SessionFile, SessionFilesPage
if TYPE_CHECKING:
from notte_sdk.client import NotteClient
def _get_cache_dir() -> Path:
- """Get cache directory with NOTTE_CACHE_DIR override support."""
- # Support NOTTE_CACHE_DIR override for backward compatibility
- env_cache_dir = os.getenv("NOTTE_CACHE_DIR")
- if env_cache_dir:
- return Path(env_cache_dir)
- # Use centralized cache directory
- return ensure_cache_directory(CacheDirectory.FILES)
+ configured = os.getenv("NOTTE_CACHE_DIR")
+ return Path(configured) if configured else ensure_cache_directory(CacheDirectory.FILES)
-NOTTE_CACHE_DIR: Path = _get_cache_dir()
+NOTTE_CACHE_DIR = _get_cache_dir()
@final
class FileStorageClient(BaseClient):
- """
- Client for Notte Storage API.
- """
-
- STORAGE_UPLOAD = "uploads/{file_name}"
- STORAGE_UPLOAD_LIST = "uploads"
- STORAGE_UPLOADED_FILE_DOWNLOAD = "uploads/{file_name}"
- STORAGE_DOWNLOAD = "{session_id}/downloads/{file_name}"
- STORAGE_UPLOAD_DOWNLOADED_FILE = "{session_id}/downloads/{file_name}"
- STORAGE_DOWNLOAD_LIST = "{session_id}/downloads"
+ """Client for session-owned uploads and browser downloads."""
def __init__(
self, root_client: NotteClient, api_key: str | None = None, server_url: str | None = None, verbose: bool = False
):
- """
- Initialize a FileStorageClient instance.
-
- Initializes the client with an optional API key and server URL,
- setting the base endpoint to "storage".
- """
- super().__init__(
- root_client=root_client,
- base_endpoint_path="storage",
- server_url=server_url,
- api_key=api_key,
- verbose=verbose,
- )
+ super().__init__(root_client, "sessions", server_url=server_url, api_key=api_key, verbose=verbose)
@staticmethod
- def _storage_upload_endpoint(file_name: str | None = None) -> NotteEndpoint[FileUploadResponse]:
- """
- Returns a NotteEndpoint for uploading files to storage.
- """
- path = FileStorageClient.STORAGE_UPLOAD
- if file_name is not None:
- path = path.format(file_name=file_name)
- return NotteEndpoint(path=path, response=FileUploadResponse, method="POST")
-
- @staticmethod
- def _storage_upload_list_endpoint() -> NotteEndpoint[ListFilesResponse]:
- """
- Returns a NotteEndpoint for listing upload files from storage.
- """
- path = FileStorageClient.STORAGE_UPLOAD_LIST
- return NotteEndpoint(path=path, response=ListFilesResponse, method="GET")
-
- @staticmethod
- def _storage_uploaded_file_download_endpoint(file_name: str) -> NotteEndpoint[FileLinkResponse]:
- """
- Returns a NotteEndpoint for getting a user-uploaded file link.
- """
- path = FileStorageClient.STORAGE_UPLOADED_FILE_DOWNLOAD.format(file_name=file_name)
- return NotteEndpoint(path=path, response=FileLinkResponse, method="GET")
-
- @staticmethod
- def _storage_download_endpoint(
- session_id: str | None = None, file_name: str | None = None
- ) -> NotteEndpoint[FileLinkResponse]:
- """
- Returns a NotteEndpoint for getting a file link for download from storage.
- """
- path = FileStorageClient.STORAGE_DOWNLOAD
- if session_id is not None and file_name is not None:
- path = path.format(session_id=session_id, file_name=file_name)
- return NotteEndpoint(path=path, response=FileLinkResponse, method="GET")
-
- @staticmethod
- def _storage_upload_downloaded_file_endpoint(
- session_id: str | None = None, file_name: str | None = None
- ) -> NotteEndpoint[FileUploadResponse]:
- """
- Returns a NotteEndpoint for getting a file link for download from storage.
- """
- path = FileStorageClient.STORAGE_UPLOAD_DOWNLOADED_FILE
- if session_id is not None and file_name is not None:
- path = path.format(session_id=session_id, file_name=file_name)
- return NotteEndpoint(path=path, response=FileUploadResponse, method="POST")
-
- @staticmethod
- def _storage_download_list_endpoint(session_id: str | None = None) -> NotteEndpoint[ListFilesResponse]:
- """
- Returns a NotteEndpoint for listing download files from storage.
- """
- path = FileStorageClient.STORAGE_DOWNLOAD_LIST
- if session_id is not None:
- path = path.format(session_id=session_id)
- return NotteEndpoint(path=path, response=ListFilesResponse, method="GET")
-
- def _upload_file(self, file_path: str, endpoint: NotteEndpoint[FileUploadResponse]):
- if not Path(file_path).exists():
- raise FileNotFoundError(
- f"Cannot upload file {file_path} because it does not exist in the local file system."
- )
- return self.request(endpoint.with_file(file_path))
-
- def _download_file(
- self,
- file_name: str,
- local_dir: str,
- endpoint: NotteEndpoint[FileLinkResponse],
- force: bool = False,
- ) -> bool:
- if Path(file_name).name != file_name or PureWindowsPath(file_name).name != file_name:
- raise ValueError("file_name must be a filename, not a path")
-
- local_dir_path = Path(local_dir)
- if not local_dir_path.exists():
- local_dir_path.mkdir(parents=True, exist_ok=True)
-
- file_path = local_dir_path / file_name
-
- if file_path.exists() and not force:
- raise ValueError(f"A file with name '{file_name}' is already at the path! Use force=True to overwrite.")
-
- _ = DownloadFileRequest.model_validate({"filename": file_name})
- resp: FileLinkResponse = self.request(endpoint)
- return self.request_download(resp.url, str(file_path))
+ def _file_endpoint(session_id: str, file_id: str | None = None) -> str:
+ path = f"{session_id}/files"
+ return f"{path}/{file_id}" if file_id is not None else path
@track_usage("cloud.files.upload")
- def upload(self, file_path: str, upload_file_name: str | None = None) -> FileUploadResponse:
- """
- Upload a file to storage.
-
- Args:
- file_path: The path to the file to upload.
- upload_file_name: The name of the file to upload. If not provided, the file name will be the same as the file path.
- """
- file_name = upload_file_name or Path(file_path).name
- return self._upload_file(file_path=file_path, endpoint=self._storage_upload_endpoint(file_name=file_name))
-
- @track_usage("cloud.files.upload_downloaded_file")
- def upload_downloaded_file(
- self, session_id: str, file_path: str, upload_file_name: str | None = None
- ) -> FileUploadResponse:
- """
- Upload a file to storage.
-
- Args:
- file_path: The path to the file to upload.
- upload_file_name: The name of the file to upload. If not provided, the file name will be the same as the file path.
- """
- file_name = upload_file_name or Path(file_path).name
- return self._upload_file(
- file_path=file_path,
- endpoint=self._storage_upload_downloaded_file_endpoint(session_id=session_id, file_name=file_name),
+ def upload(self, session_id: str, file_path: str, upload_file_name: str | None = None) -> SessionFile:
+ path = Path(file_path)
+ if not path.is_file():
+ raise FileNotFoundError(f"Cannot upload file {file_path}: it is not a file")
+ endpoint = NotteEndpoint(path=self._file_endpoint(session_id), response=SessionFile, method="POST")
+ with path.open("rb") as payload:
+ files = {"file": (upload_file_name or path.name, payload)}
+ return self.request(endpoint.model_copy(update={"files": files}))
+
+ @track_usage("cloud.files.list")
+ def list(
+ self,
+ session_id: str,
+ *,
+ source: FileSource | str | None = None,
+ limit: int = 100,
+ offset: int = 0,
+ ) -> SessionFilesPage:
+ params: dict[str, str | int] = {"limit": limit, "offset": offset}
+ if source is not None:
+ params["source"] = source.value if isinstance(source, FileSource) else source
+ endpoint = NotteEndpoint(path=self._file_endpoint(session_id), response=SessionFilesPage, method="GET")
+ response = requests.get(
+ self.request_path(endpoint),
+ headers=self.headers(),
+ params=params,
+ timeout=self.DEFAULT_REQUEST_TIMEOUT_SECONDS,
)
+ if not response.ok:
+ raise NotteAPIError(path=f"sessions/{session_id}/files", response=response)
+ return SessionFilesPage.model_validate(response.json())
+
+ def metadata(self, session_id: str, file_id: str) -> SessionFile:
+ offset = 0
+ while True:
+ page = self.list(session_id, limit=1000, offset=offset)
+ match = next((item for item in page.files if item.id == file_id), None)
+ if match is not None:
+ return match
+ offset += len(page.files)
+ if offset >= page.total or not page.files:
+ raise FileNotFoundError(f"File {file_id} was not found in session {session_id}")
@track_usage("cloud.files.download")
- def download(self, session_id: str, file_name: str, local_dir: str, force: bool = False) -> bool:
- """
- Downloads a file from storage for the current session.
-
- Args:
- file_name: The name of the file to download.
- local_dir: The directory to download the file to.
- force: Whether to overwrite the file if it already exists.
-
- Returns:
- True if the file was downloaded successfully, False otherwise.
- """
-
- endpoint = self._storage_download_endpoint(session_id=session_id, file_name=file_name)
- return self._download_file(file_name=file_name, local_dir=local_dir, endpoint=endpoint, force=force)
-
- @track_usage("cloud.files.download_uploaded_file")
- def download_uploaded_file(self, file_name: str, local_dir: str, force: bool = False) -> bool:
- """
- Downloads a user-uploaded file from storage.
-
- Args:
- file_name: The name of the uploaded file to download.
- local_dir: The directory to download the file to.
- force: Whether to overwrite the file if it already exists.
-
- Returns:
- True if the file was downloaded successfully, False otherwise.
- """
- endpoint = self._storage_uploaded_file_download_endpoint(file_name=file_name)
- return self._download_file(file_name=file_name, local_dir=local_dir, endpoint=endpoint, force=force)
-
- def list_uploaded_files(self) -> list[FileInfo]:
- """
- List files in storage. 'type' can be 'uploads' or 'downloads'.
- """
- endpoint = self._storage_upload_list_endpoint()
- resp: ListFilesResponse = self.request(endpoint)
- return resp.files
-
- def list_downloaded_files(self, session_id: str) -> list[FileInfo]:
- """
- List files in storage. 'type' can be 'uploads' or 'downloads'.
- """
-
- endpoint = self._storage_download_list_endpoint(session_id=session_id)
- resp_dl: ListFilesResponse = self.request(endpoint)
- return resp_dl.files
+ def download(self, session_id: str, file_id: str, local_dir: str = ".", *, force: bool = False) -> str:
+ metadata = self.metadata(session_id, file_id)
+ directory = Path(local_dir)
+ _ = directory.mkdir(parents=True, exist_ok=True)
+ safe_name = Path(PureWindowsPath(metadata.filename).name).name
+ if safe_name in {"", ".", ".."}:
+ raise ValueError(f"Unsafe filename returned for file {file_id}: {metadata.filename!r}")
+ destination = directory / safe_name
+ if destination.exists() and not force:
+ raise FileExistsError(f"{destination} already exists; pass force=True to overwrite it")
+ endpoint = NotteEndpoint(path=self._file_endpoint(session_id, file_id), response=SessionFile, method="GET")
+ response = requests.get(
+ self.request_path(endpoint),
+ headers=self.headers(),
+ timeout=self.DEFAULT_REQUEST_TIMEOUT_SECONDS,
+ stream=True,
+ )
+ if not response.ok:
+ raise NotteAPIError(path=f"sessions/{session_id}/files/{file_id}", response=response)
+ temporary: Path | None = None
+ try:
+ with tempfile.NamedTemporaryFile(
+ mode="wb",
+ dir=directory,
+ prefix=f".{destination.name}.",
+ suffix=".part",
+ delete=False,
+ ) as output:
+ temporary = Path(output.name)
+ for chunk in response.iter_content(self.DEFAULT_FILE_CHUNK_SIZE):
+ if chunk:
+ _ = output.write(chunk)
+ _ = temporary.replace(destination)
+ temporary = None
+ finally:
+ response.close()
+ if temporary is not None:
+ temporary.unlink(missing_ok=True)
+ return str(destination)
+
+ @track_usage("cloud.files.delete")
+ def delete(self, session_id: str, file_id: str) -> None:
+ endpoint = NotteEndpoint(path=self._file_endpoint(session_id, file_id), response=SessionFile, method="DELETE")
+ response = requests.delete(
+ self.request_path(endpoint), headers=self.headers(), timeout=self.DEFAULT_REQUEST_TIMEOUT_SECONDS
+ )
+ if not response.ok:
+ raise NotteAPIError(path=f"sessions/{session_id}/files/{file_id}", response=response)
class RemoteFileStorage(BaseStorage):
@@ -240,108 +141,67 @@ def __init__(self, session_id: str | None = None, *, _client: FileStorageClient
if _client is None:
raise ValueError("FileStorageClient is required")
self.client: FileStorageClient = _client
- super().__init__(upload_dir=str(NOTTE_CACHE_DIR / "uploads"), download_dir=str(NOTTE_CACHE_DIR / "downloads"))
self._session_id: str | None = session_id
+ super().__init__(upload_dir=str(NOTTE_CACHE_DIR / "uploads"), download_dir=str(NOTTE_CACHE_DIR / "downloads"))
@property
@override
def is_remote(self) -> bool:
return True
- def set_session_id(self, id: str) -> None:
- self._session_id = id
+ def set_session_id(self, session_id: str) -> None:
+ self._session_id = session_id
+
+ def for_session(self, session_id: str) -> RemoteFileStorage:
+ """Bind this storage once, cloning it when another session already owns it."""
+ if self._session_id is None:
+ self._session_id = session_id
+ return self
+ if self._session_id == session_id:
+ return self
+ return RemoteFileStorage(session_id, _client=self.client)
@property
def session_id(self) -> str:
if self._session_id is None:
- raise ValueError("Session ID is not set. Call set_session_id() to set the session ID.")
+ raise ValueError("A session ID is required for every file operation")
return self._session_id
- def download(self, file_name: str, local_dir: str, force: bool = False) -> bool:
- """
- Stores a file that has been downloaded from a website in the current session.
-
- ```python
- file_storage = notte.FileStorage("")
- # file.pdf has been downloaded by an agent in the session
- # you can download it to your local machine using:
- file_storage.download(file_name="file.pdf", local_dir="")
- ```
-
- """
- return self.client.download(session_id=self.session_id, file_name=file_name, local_dir=local_dir, force=force)
-
- def download_uploaded_file(self, file_name: str, local_dir: str, force: bool = False) -> bool:
- """
- Downloads a user-uploaded file to the local filesystem without requiring a session attachment.
-
- ```python
- storage = notte.FileStorage()
- storage.upload(file_path="")
- storage.download_uploaded_file(file_name="", local_dir="")
- ```
-
- Args:
- file_name: The name of the uploaded file to download.
- local_dir: The directory to download the file to.
- force: Overwrite an existing destination file. Defaults to ``False``; set ``True`` to replace it.
- """
- return self.client.download_uploaded_file(file_name=file_name, local_dir=local_dir, force=force)
-
- def upload(self, file_path: str, upload_file_name: str | None = None) -> bool:
- """
- Upload a file from your local machine to storage.
-
- This file will then be available to the agent in the current session.
-
- ```python
- storage = notte.FileStorage()
- with notte.Session(storage=storage) as session:
- # make the file available to the agent in the current session
- storage.upload(file_path="")
- ```
-
- """
- response = self.client.upload(file_path=file_path, upload_file_name=upload_file_name)
- return response.success
+ def upload(self, file_path: str, upload_file_name: str | None = None) -> SessionFile:
+ return self.client.upload(self.session_id, file_path, upload_file_name)
+
+ def list(self, source: FileSource | str | None = None, *, limit: int = 100, offset: int = 0) -> SessionFilesPage:
+ return self.client.list(self.session_id, source=source, limit=limit, offset=offset)
+
+ def download(self, file_id: str, local_dir: str = ".", *, force: bool = False) -> str:
+ return self.client.download(self.session_id, file_id, local_dir, force=force)
+
+ def delete(self, file_id: str) -> None:
+ self.client.delete(self.session_id, file_id)
@override
async def get_file(self, name: str) -> str | None:
- assert self.download_dir is not None
- _ = Path(self.download_dir).mkdir(parents=True, exist_ok=True)
-
- status = self.client.download(session_id=self.session_id, file_name=name, local_dir=self.download_dir)
- if not status:
- return None
- return str(Path(self.download_dir) / name)
+ assert self.upload_dir is not None
+ match = next(
+ (file for file in self.list(FileSource.USER_UPLOAD, limit=1000).files if file.filename == name), None
+ )
+ return None if match is None else self.download(match.id, self.upload_dir, force=True)
@override
async def set_file(self, path: str) -> bool:
- response = self.client.upload_downloaded_file(session_id=self.session_id, file_path=path)
- return response.success
+ _ = self.upload(path)
+ return True
+
+ @staticmethod
+ def _file_info(file: SessionFile) -> FileInfo:
+ return FileInfo(
+ name=file.filename, size=file.size, file_ext=Path(file.filename).suffix, updated_at=file.created_at
+ )
@override
async def alist_uploaded_files(self) -> list[FileInfo]:
- """
- List files that have been uploaded to storage.
-
- ```python
- storage = notte.FileStorage()
- # list the files that have been uploaded to storage
- files = storage.list_uploaded_files()
- ```
- """
- return self.client.list_uploaded_files()
+ return [self._file_info(file) for file in self.list(FileSource.USER_UPLOAD, limit=1000).files]
@override
async def alist_downloaded_files(self) -> list[FileInfo]:
- """
- List files that have been downloaded into storage by the agents.
-
- ```python
- storage = notte.FileStorage(session_id="")
- # list the files that have been downloaded from storage
- files = storage.list_downloaded_files()
- ```
- """
- return self.client.list_downloaded_files(session_id=self.session_id)
+ return [self._file_info(file) for file in self.list(FileSource.SESSION_DOWNLOAD, limit=1000).files]
diff --git a/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py b/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py
index dcf86cf4d..2a035d243 100644
--- a/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py
+++ b/packages/notte-sdk/src/notte_sdk/endpoints/sessions.py
@@ -649,21 +649,18 @@ def __init__(
request_data = {k: v for k, v in data.items() if k != "open_viewer"}
request = SessionStartRequest.model_validate(request_data)
- if storage is not None:
- request.use_file_storage = True
-
response: SessionResponse | None = None
if session_id is not None:
response = _client.status(session_id=session_id)
- if storage is not None:
- storage.set_session_id(session_id)
# init attributes
self.request: SessionStartRequest = request
self._open_viewer: bool = open_viewer
self.client: SessionsClient = _client
self.response: SessionResponse | None = response
- self.storage: RemoteFileStorage | None = storage
+ self.storage: RemoteFileStorage = storage or RemoteFileStorage(_client=_client.root_client.files)
+ if session_id is not None:
+ self.storage = self.storage.for_session(session_id)
self.default_perception_type: PerceptionType = perception_type
self.default_raise_on_failure: bool = raise_on_failure
self._cookie_file: Path | None = Path(cookie_file) if cookie_file is not None else None
@@ -676,12 +673,6 @@ def __init__(
self._async_playwright_browser: "BrowserAsync | None" = None
self._async_playwright_page: "PageAsync | None" = None
- if self.storage is not None and not self.request.use_file_storage:
- logger.warning(
- "Storage is provided but `use_file_storage=False` in session start request. Overriding `use_file_storage=True`."
- )
- self.request.use_file_storage = True
-
@override
def __exit__( # pyright: ignore [reportMissingSuperCall]
self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None
@@ -813,8 +804,7 @@ def start(self, tries: int = 3) -> None:
else:
logger.warning(f"Failed to start session: retrying ({retry_str})")
- if self.storage is not None:
- self.storage.set_session_id(self.session_id)
+ self.storage = self.storage.for_session(self.session_id)
logger.info(f"[Session] {self.session_id} started with request: {self.request.model_dump(exclude_none=True)}")
if self._open_viewer:
diff --git a/packages/notte-sdk/src/notte_sdk/types.py b/packages/notte-sdk/src/notte_sdk/types.py
index 660a0f0c8..787077440 100644
--- a/packages/notte-sdk/src/notte_sdk/types.py
+++ b/packages/notte-sdk/src/notte_sdk/types.py
@@ -750,7 +750,6 @@ class SessionStartRequestDict(TypedDict, total=False):
viewport_height: The height of the viewport
aspect_ratio: Viewport shape preset ("5:4" or "16:9"). Cannot be combined with viewport_width/viewport_height.
cdp_url: The CDP URL of another remote session provider.
- use_file_storage: Whether FileStorage should be attached to the session.
screenshot_type: The type of screenshot to use for the session.
profile: Browser profile configuration for state persistence.
auth_ids: Up to 10 unique Managed Auth connection IDs to authenticate before the session is returned.
@@ -773,7 +772,6 @@ class SessionStartRequestDict(TypedDict, total=False):
viewport_height: int | None
aspect_ratio: AspectRatio | None
cdp_url: str | None
- use_file_storage: bool
screenshot_type: ScreenshotType
profile: SessionProfileDict | SessionProfile | None
web_bot_auth: bool
@@ -834,10 +832,6 @@ class SessionStartRequest(SdkRequest):
config.cdp_url
)
- use_file_storage: Annotated[bool, Field(description="Whether FileStorage should be attached to the session.")] = (
- True
- )
-
screenshot_type: Annotated[ScreenshotType, Field(description="The type of screenshot to use for the session.")] = (
config.screenshot_type
)
@@ -1109,7 +1103,6 @@ class SessionResponse(SdkResponse):
] = False
# remaining args
browser_type: BrowserType = "chromium"
- use_file_storage: Annotated[bool, Field(description="Whether FileStorage was attached to the session.")] = False
network_request_bytes: Annotated[int, Field(description="Total byte usage for network requests.")] = 0
network_response_bytes: Annotated[int, Field(description="Total byte usage for network responses.")] = 0
user_agent: Annotated[str | None, Field(description="The user agent to use for the session")] = None
@@ -1171,10 +1164,36 @@ class SessionStopRequest(BaseModel):
] = "manual"
+class FileSource(StrEnum):
+ USER_UPLOAD = "user_upload"
+ SESSION_DOWNLOAD = "session_download"
+
+
+class SessionFile(BaseModel):
+ id: str
+ session_id: str
+ filename: str
+ mime_type: str
+ size: int
+ checksum: str
+ created_at: dt.datetime
+ expires_at: dt.datetime
+ source: FileSource
+
+
class ListFilesResponse(SdkResponse):
+ """Legacy file-list response retained for import compatibility."""
+
files: Annotated[list[FileInfo], Field(description="List of files with metadata")]
+class SessionFilesPage(SdkResponse):
+ files: Annotated[list[SessionFile], Field(description="List of non-expired session files")]
+ total: int
+ limit: int
+ offset: int
+
+
class FileUploadResponse(SdkResponse):
success: Annotated[bool, Field(description="Whether the upload was successful")]
diff --git a/tests/integration/sdk/file_storage/test_download.py b/tests/integration/sdk/file_storage/test_download.py
index f1e984f39..e800f15bd 100644
--- a/tests/integration/sdk/file_storage/test_download.py
+++ b/tests/integration/sdk/file_storage/test_download.py
@@ -6,6 +6,8 @@
from notte_browser.errors import NoStorageObjectProvidedError
from notte_core.actions import DownloadFileAction
from notte_sdk import NotteClient
+from notte_sdk.errors import NotteAPIError
+from notte_sdk.types import FileSource
from pydantic import BaseModel, Field
import notte
@@ -104,16 +106,20 @@ def test_download_against_local_fixture(case: FixtureDownloadCase):
_ = session.execute(type="goto", url=case.url)
_ = session.execute(type="download_file", selector=case.selector)
- downloaded = storage.list_downloaded_files()
- names = [f.name for f in downloaded]
- matching = [n for n in names if n.endswith(case.expected_filename_suffix)]
+ try:
+ downloaded = storage.list(FileSource.SESSION_DOWNLOAD, limit=1000).files
+ except NotteAPIError as exc:
+ if exc.status_code == 404:
+ pytest.skip("Session-file API is not deployed to the integration environment yet")
+ raise
+ names = [f.filename for f in downloaded]
+ matching = [f for f in downloaded if f.filename.endswith(case.expected_filename_suffix)]
assert len(matching) == 1, (
f"expected exactly one file ending with {case.expected_filename_suffix!r}, got {names}"
)
- stored_name = matching[0]
+ stored = matching[0]
with tempfile.TemporaryDirectory() as tmp_dir:
- assert storage.download(file_name=stored_name, local_dir=tmp_dir)
- local_path = Path(tmp_dir) / stored_name
+ local_path = Path(storage.download(file_id=stored.id, local_dir=tmp_dir))
assert local_path.exists(), f"{local_path} missing after storage.download"
- assert local_path.read_bytes() == case.expected_bytes, f"byte mismatch for {stored_name}"
+ assert local_path.read_bytes() == case.expected_bytes, f"byte mismatch for {stored.filename}"
diff --git a/tests/integration/sdk/file_storage/test_readonly_robust.py b/tests/integration/sdk/file_storage/test_readonly_robust.py
index 856614a27..877754a83 100644
--- a/tests/integration/sdk/file_storage/test_readonly_robust.py
+++ b/tests/integration/sdk/file_storage/test_readonly_robust.py
@@ -12,6 +12,8 @@
os.environ["DISABLE_TELEMETRY"] = "true"
from notte_sdk import NotteClient
+from notte_sdk.errors import NotteAPIError
+from notte_sdk.types import FileSource
@contextlib.contextmanager
@@ -115,8 +117,13 @@ def test_download_file_action_is_strictly_readonly():
# Verify that accessing the file locally triggers a permission error
# This can trigger either "Filesystem modification denied" (mkdir) or "Write access denied" (open)
with pytest.raises(PermissionError, match="Filesystem modification denied|Write access denied"):
- files = storage.list_downloaded_files()
- _ = asyncio.run(storage.get_file(files[0].name))
+ try:
+ files = storage.list(FileSource.SESSION_DOWNLOAD, limit=1000).files
+ except NotteAPIError as exc:
+ if exc.status_code == 404:
+ pytest.skip("Session-file API is not deployed to the integration environment yet")
+ raise
+ _ = asyncio.run(storage.get_file(files[0].filename))
except PermissionError as e:
pytest.fail(f"Read-only violation detected: {e}")
diff --git a/tests/integration/sdk/file_storage/test_upload.py b/tests/integration/sdk/file_storage/test_upload.py
index 235fc5b77..cbd651072 100644
--- a/tests/integration/sdk/file_storage/test_upload.py
+++ b/tests/integration/sdk/file_storage/test_upload.py
@@ -3,6 +3,7 @@
import pytest
from dotenv import load_dotenv
from notte_sdk import NotteClient
+from notte_sdk.errors import NotteAPIError
from pydantic import BaseModel
_ = load_dotenv()
@@ -13,7 +14,7 @@
def test_upload_non_existent_file_should_raise_error():
notte = NotteClient()
- storage = notte.FileStorage()
+ storage = notte.FileStorage("session-id")
with pytest.raises(FileNotFoundError):
_ = storage.upload(str(DATA_DIR / "non_existent_file.txt"))
@@ -57,7 +58,13 @@ def test_upload_against_local_fixture(case: FixtureUploadCase):
storage = notte.FileStorage()
with notte.Session(storage=storage) as session:
- assert storage.upload(str(DATA_DIR / case.file_name))
+ try:
+ uploaded = storage.upload(str(DATA_DIR / case.file_name))
+ except NotteAPIError as exc:
+ if exc.status_code == 404:
+ pytest.skip("Session-file API is not deployed to the integration environment yet")
+ raise
+ assert uploaded.filename == case.file_name
_ = session.execute(type="goto", url=UPLOAD_FIXTURE_URL)
_ = session.execute(type="upload_file", selector="#file-input", file_path=case.file_name)
diff --git a/tests/sdk/test_client.py b/tests/sdk/test_client.py
index de5345370..0583dcdd4 100644
--- a/tests/sdk/test_client.py
+++ b/tests/sdk/test_client.py
@@ -10,6 +10,7 @@
from notte_core.browser.observation import ExecutionResult, Observation
from notte_core.space import SpaceCategory
from notte_sdk.client import NotteClient
+from notte_sdk.endpoints.base import NotteEndpoint
from notte_sdk.errors import AuthenticationError
from notte_sdk.types import (
DEFAULT_SESSION_IDLE_TIMEOUT_IN_MINUTES,
@@ -69,6 +70,14 @@ def test_client_initialization_without_api_key() -> None:
_ = NotteClient()
+def test_base_client_accepts_empty_success_response(client: NotteClient) -> None:
+ response = MagicMock(status_code=204, content=b"")
+ with patch("requests.get", return_value=response):
+ result = client.sessions._request(NotteEndpoint(path="empty", response=SessionResponse, method="GET"))
+
+ assert result == {}
+
+
@pytest.fixture
def session_id() -> str:
return "test-session-123"
@@ -173,7 +182,6 @@ def test_start_session(mock_post: MagicMock, client: NotteClient, session_id: st
"browser_type": "chromium",
"viewport_width": 1920,
"viewport_height": 1080,
- "use_file_storage": True,
}
response = _start_session(mock_post=mock_post, client=client, session_id=session_id)
assert response.session_id == session_id
diff --git a/tests/sdk/test_file_storage.py b/tests/sdk/test_file_storage.py
index acc7d3b5a..14a5a3c22 100644
--- a/tests/sdk/test_file_storage.py
+++ b/tests/sdk/test_file_storage.py
@@ -3,95 +3,124 @@
import pytest
from notte_sdk.endpoints.files import FileStorageClient, RemoteFileStorage
-from notte_sdk.types import FileLinkResponse
+from notte_sdk.types import FileSource, SessionFile, SessionFilesPage
@pytest.fixture
def files_client() -> FileStorageClient:
with patch.object(FileStorageClient, "check_and_warn_version_mismatch"):
return FileStorageClient(
- root_client=Mock(),
+ Mock(),
api_key="test-api-key", # pragma: allowlist secret
server_url="https://api.notte.test",
)
-def test_uploaded_file_download_endpoint() -> None:
- endpoint = FileStorageClient._storage_uploaded_file_download_endpoint("input.txt")
+def file_metadata() -> SessionFile:
+ return SessionFile.model_validate(
+ {
+ "id": "file-id",
+ "session_id": "session-id",
+ "filename": "input.txt",
+ "mime_type": "text/plain",
+ "size": 5,
+ "checksum": "a" * 64,
+ "created_at": "2026-08-21T00:00:00Z",
+ "expires_at": "2026-08-22T00:00:00Z",
+ "source": "user_upload",
+ }
+ )
+
+
+def test_every_operation_requires_a_session(files_client: FileStorageClient) -> None:
+ storage = RemoteFileStorage(_client=files_client)
+ with pytest.raises(ValueError, match="session ID"):
+ storage.list()
+
- assert endpoint.path == "uploads/input.txt"
- assert endpoint.method == "GET"
- assert endpoint.response is FileLinkResponse
+def test_list_uses_session_endpoint(files_client: FileStorageClient) -> None:
+ response = Mock(ok=True)
+ response.json.return_value = {
+ "files": [file_metadata().model_dump(mode="json")],
+ "total": 1,
+ "limit": 100,
+ "offset": 0,
+ }
+ with patch("notte_sdk.endpoints.files.requests.get", return_value=response) as get:
+ result = files_client.list("session-id", source=FileSource.USER_UPLOAD)
+ assert isinstance(result, SessionFilesPage)
+ assert result.files[0].id == "file-id"
+ assert get.call_args.args[0] == "https://api.notte.test/sessions/session-id/files"
+ assert get.call_args.kwargs["params"]["source"] == "user_upload"
-def test_download_uploaded_file(files_client: FileStorageClient, tmp_path: Path) -> None:
- response = FileLinkResponse(url="https://storage.notte.test/input.txt")
+def test_download_is_id_based(files_client: FileStorageClient, tmp_path: Path) -> None:
+ response = Mock(ok=True)
+ response.iter_content.return_value = [b"hello"]
with (
- patch.object(files_client, "request", return_value=response) as request,
- patch.object(files_client, "request_download", return_value=True) as request_download,
+ patch.object(files_client, "metadata", return_value=file_metadata()),
+ patch("notte_sdk.endpoints.files.requests.get", return_value=response) as get,
):
- result = files_client.download_uploaded_file(file_name="input.txt", local_dir=str(tmp_path))
+ destination = files_client.download("session-id", "file-id", str(tmp_path))
- assert result is True
- endpoint = request.call_args.args[0]
- assert endpoint.path == "uploads/input.txt"
- assert endpoint.method == "GET"
- request_download.assert_called_once_with(response.url, str(tmp_path / "input.txt"))
+ assert Path(destination).read_bytes() == b"hello"
+ assert get.call_args.args[0] == "https://api.notte.test/sessions/session-id/files/file-id"
+ response.close.assert_called_once()
-def test_download_uploaded_file_refuses_to_overwrite(files_client: FileStorageClient, tmp_path: Path) -> None:
- file_path = tmp_path / "input.txt"
- _ = file_path.write_text("existing")
-
+@pytest.mark.parametrize("filename", ["../../secret.txt", "..\\..\\secret.txt", "/tmp/secret.txt"])
+def test_download_sanitizes_server_filename(files_client: FileStorageClient, tmp_path: Path, filename: str) -> None:
+ response = Mock(ok=True)
+ response.iter_content.return_value = [b"hello"]
+ metadata = file_metadata().model_copy(update={"filename": filename})
with (
- patch.object(files_client, "request") as request,
- pytest.raises(ValueError, match="force=True"),
+ patch.object(files_client, "metadata", return_value=metadata),
+ patch("notte_sdk.endpoints.files.requests.get", return_value=response),
):
- files_client.download_uploaded_file(file_name=file_path.name, local_dir=str(tmp_path))
-
- request.assert_not_called()
-
-
-@pytest.mark.parametrize(
- "file_name",
- [
- "../outside.txt",
- "/absolute/outside.txt",
- r"..\outside.txt",
- r"C:\tmp\outside.txt",
- ],
-)
-def test_download_uploaded_file_rejects_paths(
- files_client: FileStorageClient,
- tmp_path: Path,
- file_name: str,
+ destination = files_client.download("session-id", "file-id", str(tmp_path))
+
+ assert Path(destination) == tmp_path / "secret.txt"
+ assert Path(destination).read_bytes() == b"hello"
+
+
+def test_download_does_not_follow_predictable_temporary_symlink(
+ files_client: FileStorageClient, tmp_path: Path
) -> None:
+ outside = tmp_path.parent / "outside.txt"
+ outside.write_text("safe")
+ (tmp_path / ".input.txt.part").symlink_to(outside)
+ response = Mock(ok=True)
+ response.iter_content.return_value = [b"download"]
with (
- patch.object(files_client, "request") as request,
- patch.object(files_client, "request_download") as request_download,
- pytest.raises(ValueError, match="filename, not a path"),
+ patch.object(files_client, "metadata", return_value=file_metadata()),
+ patch("notte_sdk.endpoints.files.requests.get", return_value=response),
):
- files_client.download_uploaded_file(
- file_name=file_name,
- local_dir=str(tmp_path),
- force=True,
- )
+ destination = files_client.download("session-id", "file-id", str(tmp_path))
- request.assert_not_called()
- request_download.assert_not_called()
+ assert Path(destination).read_bytes() == b"download"
+ assert outside.read_text() == "safe"
-def test_remote_storage_downloads_uploaded_file_without_session(tmp_path: Path) -> None:
- client = Mock(spec=FileStorageClient)
- client.download_uploaded_file.return_value = True
- storage = RemoteFileStorage(_client=client)
+def test_remote_storage_upload_is_session_scoped(files_client: FileStorageClient, tmp_path: Path) -> None:
+ local = tmp_path / "input.txt"
+ local.write_text("hello")
+ metadata = file_metadata()
+ with patch.object(files_client, "upload", return_value=metadata) as upload:
+ result = RemoteFileStorage("session-id", _client=files_client).upload(str(local))
- result = storage.download_uploaded_file(file_name="input.txt", local_dir=str(tmp_path), force=True)
+ assert result == metadata
+ upload.assert_called_once_with("session-id", str(local), None)
- assert result is True
- client.download_uploaded_file.assert_called_once_with(
- file_name="input.txt",
- local_dir=str(tmp_path),
- force=True,
- )
+
+def test_remote_storage_is_cloned_when_reused_across_sessions(files_client: FileStorageClient) -> None:
+ storage = RemoteFileStorage(_client=files_client)
+
+ first = storage.for_session("session-a")
+ second = storage.for_session("session-b")
+
+ assert first is storage
+ assert first.session_id == "session-a"
+ assert second is not storage
+ assert second.session_id == "session-b"
+ assert second.client is storage.client
diff --git a/tests/sdk/test_no_orphan_models.py b/tests/sdk/test_no_orphan_models.py
index 1b952fabf..faf88fab3 100644
--- a/tests/sdk/test_no_orphan_models.py
+++ b/tests/sdk/test_no_orphan_models.py
@@ -8,9 +8,12 @@
SDK_SRC = Path(inspect.getfile(notte_sdk)).parent
-# Models intentionally defined but referenced nowhere else in the SDK.
-# Every entry needs a non-empty reason. This dict should stay empty.
-ALLOWED_UNREFERENCED: dict[str, str] = {}
+# Models intentionally retained as import-compatible legacy API types.
+ALLOWED_UNREFERENCED: dict[str, str] = {
+ "DownloadFileRequest": "Deprecated global-storage request retained for import compatibility",
+ "FileUploadResponse": "Deprecated global-storage response retained for import compatibility",
+ "ListFilesResponse": "Deprecated global-storage response retained for import compatibility",
+}
def _models() -> set[str]: