letta-nightly 0.4.1.dev20241014104152__py3-none-any.whl → 0.5.0.dev20241015104156__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (32) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent_store/db.py +18 -7
  3. letta/agent_store/lancedb.py +2 -2
  4. letta/agent_store/milvus.py +1 -1
  5. letta/agent_store/qdrant.py +1 -1
  6. letta/agent_store/storage.py +12 -10
  7. letta/cli/cli_load.py +1 -1
  8. letta/client/client.py +51 -0
  9. letta/data_sources/connectors.py +124 -124
  10. letta/data_sources/connectors_helper.py +97 -0
  11. letta/llm_api/mistral.py +47 -0
  12. letta/metadata.py +58 -0
  13. letta/providers.py +44 -0
  14. letta/schemas/file.py +31 -0
  15. letta/schemas/job.py +1 -1
  16. letta/schemas/letta_request.py +3 -3
  17. letta/schemas/llm_config.py +1 -0
  18. letta/schemas/message.py +6 -2
  19. letta/schemas/passage.py +3 -3
  20. letta/schemas/source.py +2 -2
  21. letta/server/rest_api/routers/v1/agents.py +10 -16
  22. letta/server/rest_api/routers/v1/jobs.py +17 -1
  23. letta/server/rest_api/routers/v1/sources.py +7 -9
  24. letta/server/server.py +86 -13
  25. letta/server/static_files/assets/{index-9a9c449b.js → index-dc228d4a.js} +4 -4
  26. letta/server/static_files/index.html +1 -1
  27. {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015104156.dist-info}/METADATA +1 -1
  28. {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015104156.dist-info}/RECORD +31 -29
  29. letta/schemas/document.py +0 -21
  30. {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015104156.dist-info}/LICENSE +0 -0
  31. {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015104156.dist-info}/WHEEL +0 -0
  32. {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015104156.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,97 @@
1
+ import mimetypes
2
+ import os
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+
8
+ def extract_file_metadata(file_path) -> dict:
9
+ """Extracts metadata from a single file."""
10
+ if not os.path.exists(file_path):
11
+ raise FileNotFoundError(file_path)
12
+
13
+ file_metadata = {
14
+ "file_name": os.path.basename(file_path),
15
+ "file_path": file_path,
16
+ "file_type": mimetypes.guess_type(file_path)[0] or "unknown",
17
+ "file_size": os.path.getsize(file_path),
18
+ "file_creation_date": datetime.fromtimestamp(os.path.getctime(file_path)).strftime("%Y-%m-%d"),
19
+ "file_last_modified_date": datetime.fromtimestamp(os.path.getmtime(file_path)).strftime("%Y-%m-%d"),
20
+ }
21
+ return file_metadata
22
+
23
+
24
+ def extract_metadata_from_files(file_list):
25
+ """Extracts metadata for a list of files."""
26
+ metadata = []
27
+ for file_path in file_list:
28
+ file_metadata = extract_file_metadata(file_path)
29
+ if file_metadata:
30
+ metadata.append(file_metadata)
31
+ return metadata
32
+
33
+
34
+ def get_filenames_in_dir(
35
+ input_dir: str, recursive: bool = True, required_exts: Optional[List[str]] = None, exclude: Optional[List[str]] = None
36
+ ):
37
+ """
38
+ Recursively reads files from the directory, applying required_exts and exclude filters.
39
+ Ensures that required_exts and exclude do not overlap.
40
+
41
+ Args:
42
+ input_dir (str): The directory to scan for files.
43
+ recursive (bool): Whether to scan directories recursively.
44
+ required_exts (list): List of file extensions to include (e.g., ['pdf', 'txt']).
45
+ If None or empty, matches any file extension.
46
+ exclude (list): List of file patterns to exclude (e.g., ['*png', '*jpg']).
47
+
48
+ Returns:
49
+ list: A list of matching file paths.
50
+ """
51
+ required_exts = required_exts or []
52
+ exclude = exclude or []
53
+
54
+ # Ensure required_exts and exclude do not overlap
55
+ ext_set = set(required_exts)
56
+ exclude_set = set(exclude)
57
+ overlap = ext_set & exclude_set
58
+ if overlap:
59
+ raise ValueError(f"Extensions in required_exts and exclude overlap: {overlap}")
60
+
61
+ def is_excluded(file_name):
62
+ """Check if a file matches any pattern in the exclude list."""
63
+ for pattern in exclude:
64
+ if Path(file_name).match(pattern):
65
+ return True
66
+ return False
67
+
68
+ files = []
69
+ search_pattern = "**/*" if recursive else "*"
70
+
71
+ for file_path in Path(input_dir).glob(search_pattern):
72
+ if file_path.is_file() and not is_excluded(file_path.name):
73
+ ext = file_path.suffix.lstrip(".")
74
+ # If required_exts is empty, match any file
75
+ if not required_exts or ext in required_exts:
76
+ files.append(file_path)
77
+
78
+ return files
79
+
80
+
81
+ def assert_all_files_exist_locally(file_paths: List[str]) -> bool:
82
+ """
83
+ Checks if all file paths in the provided list exist locally.
84
+ Raises a FileNotFoundError with a list of missing files if any do not exist.
85
+
86
+ Args:
87
+ file_paths (List[str]): List of file paths to check.
88
+
89
+ Returns:
90
+ bool: True if all files exist, raises FileNotFoundError if any file is missing.
91
+ """
92
+ missing_files = [file_path for file_path in file_paths if not Path(file_path).exists()]
93
+
94
+ if missing_files:
95
+ raise FileNotFoundError(missing_files)
96
+
97
+ return True
@@ -0,0 +1,47 @@
1
+ import requests
2
+
3
+ from letta.utils import printd, smart_urljoin
4
+
5
+
6
+ def mistral_get_model_list(url: str, api_key: str) -> dict:
7
+ url = smart_urljoin(url, "models")
8
+
9
+ headers = {"Content-Type": "application/json"}
10
+ if api_key is not None:
11
+ headers["Authorization"] = f"Bearer {api_key}"
12
+
13
+ printd(f"Sending request to {url}")
14
+ response = None
15
+ try:
16
+ # TODO add query param "tool" to be true
17
+ response = requests.get(url, headers=headers)
18
+ response.raise_for_status() # Raises HTTPError for 4XX/5XX status
19
+ response_json = response.json() # convert to dict from string
20
+ return response_json
21
+ except requests.exceptions.HTTPError as http_err:
22
+ # Handle HTTP errors (e.g., response 4XX, 5XX)
23
+ try:
24
+ if response:
25
+ response = response.json()
26
+ except:
27
+ pass
28
+ printd(f"Got HTTPError, exception={http_err}, response={response}")
29
+ raise http_err
30
+ except requests.exceptions.RequestException as req_err:
31
+ # Handle other requests-related errors (e.g., connection error)
32
+ try:
33
+ if response:
34
+ response = response.json()
35
+ except:
36
+ pass
37
+ printd(f"Got RequestException, exception={req_err}, response={response}")
38
+ raise req_err
39
+ except Exception as e:
40
+ # Handle other potential errors
41
+ try:
42
+ if response:
43
+ response = response.json()
44
+ except:
45
+ pass
46
+ printd(f"Got unknown Exception, exception={e}, response={response}")
47
+ raise e
letta/metadata.py CHANGED
@@ -11,6 +11,7 @@ from sqlalchemy import (
11
11
  Column,
12
12
  DateTime,
13
13
  Index,
14
+ Integer,
14
15
  String,
15
16
  TypeDecorator,
16
17
  desc,
@@ -24,6 +25,7 @@ from letta.schemas.api_key import APIKey
24
25
  from letta.schemas.block import Block, Human, Persona
25
26
  from letta.schemas.embedding_config import EmbeddingConfig
26
27
  from letta.schemas.enums import JobStatus
28
+ from letta.schemas.file import FileMetadata
27
29
  from letta.schemas.job import Job
28
30
  from letta.schemas.llm_config import LLMConfig
29
31
  from letta.schemas.memory import Memory
@@ -38,6 +40,41 @@ from letta.settings import settings
38
40
  from letta.utils import enforce_types, get_utc_time, printd
39
41
 
40
42
 
43
+ class FileMetadataModel(Base):
44
+ __tablename__ = "files"
45
+ __table_args__ = {"extend_existing": True}
46
+
47
+ id = Column(String, primary_key=True, nullable=False)
48
+ user_id = Column(String, nullable=False)
49
+ # TODO: Investigate why this breaks during table creation due to FK
50
+ # source_id = Column(String, ForeignKey("sources.id"), nullable=False)
51
+ source_id = Column(String, nullable=False)
52
+ file_name = Column(String, nullable=True)
53
+ file_path = Column(String, nullable=True)
54
+ file_type = Column(String, nullable=True)
55
+ file_size = Column(Integer, nullable=True)
56
+ file_creation_date = Column(String, nullable=True)
57
+ file_last_modified_date = Column(String, nullable=True)
58
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
59
+
60
+ def __repr__(self):
61
+ return f"<FileMetadata(id='{self.id}', source_id='{self.source_id}', file_name='{self.file_name}')>"
62
+
63
+ def to_record(self):
64
+ return FileMetadata(
65
+ id=self.id,
66
+ user_id=self.user_id,
67
+ source_id=self.source_id,
68
+ file_name=self.file_name,
69
+ file_path=self.file_path,
70
+ file_type=self.file_type,
71
+ file_size=self.file_size,
72
+ file_creation_date=self.file_creation_date,
73
+ file_last_modified_date=self.file_last_modified_date,
74
+ created_at=self.created_at,
75
+ )
76
+
77
+
41
78
  class LLMConfigColumn(TypeDecorator):
42
79
  """Custom type for storing LLMConfig as JSON"""
43
80
 
@@ -865,6 +902,27 @@ class MetadataStore:
865
902
  session.add(JobModel(**vars(job)))
866
903
  session.commit()
867
904
 
905
+ @enforce_types
906
+ def list_files_from_source(self, source_id: str, limit: int, cursor: Optional[str]):
907
+ with self.session_maker() as session:
908
+ # Start with the basic query filtered by source_id
909
+ query = session.query(FileMetadataModel).filter(FileMetadataModel.source_id == source_id)
910
+
911
+ if cursor:
912
+ # Assuming cursor is the ID of the last file in the previous page
913
+ query = query.filter(FileMetadataModel.id > cursor)
914
+
915
+ # Order by ID or other ordering criteria to ensure correct pagination
916
+ query = query.order_by(FileMetadataModel.id)
917
+
918
+ # Limit the number of results returned
919
+ results = query.limit(limit).all()
920
+
921
+ # Convert the results to the required FileMetadata objects
922
+ files = [r.to_record() for r in results]
923
+
924
+ return files
925
+
868
926
  def delete_job(self, job_id: str):
869
927
  with self.session_maker() as session:
870
928
  session.query(JobModel).filter(JobModel.id == job_id).delete()
letta/providers.py CHANGED
@@ -139,6 +139,50 @@ class AnthropicProvider(Provider):
139
139
  return []
140
140
 
141
141
 
142
+ class MistralProvider(Provider):
143
+ name: str = "mistral"
144
+ api_key: str = Field(..., description="API key for the Mistral API.")
145
+ base_url: str = "https://api.mistral.ai/v1"
146
+
147
+ def list_llm_models(self) -> List[LLMConfig]:
148
+ from letta.llm_api.mistral import mistral_get_model_list
149
+
150
+ # Some hardcoded support for OpenRouter (so that we only get models with tool calling support)...
151
+ # See: https://openrouter.ai/docs/requests
152
+ response = mistral_get_model_list(self.base_url, api_key=self.api_key)
153
+
154
+ assert "data" in response, f"Mistral model query response missing 'data' field: {response}"
155
+
156
+ configs = []
157
+ for model in response["data"]:
158
+ # If model has chat completions and function calling enabled
159
+ if model["capabilities"]["completion_chat"] and model["capabilities"]["function_calling"]:
160
+ configs.append(
161
+ LLMConfig(
162
+ model=model["id"],
163
+ model_endpoint_type="openai",
164
+ model_endpoint=self.base_url,
165
+ context_window=model["max_context_length"],
166
+ )
167
+ )
168
+
169
+ return configs
170
+
171
+ def list_embedding_models(self) -> List[EmbeddingConfig]:
172
+ # Not supported for mistral
173
+ return []
174
+
175
+ def get_model_context_window(self, model_name: str) -> Optional[int]:
176
+ # Redoing this is fine because it's a pretty lightweight call
177
+ models = self.list_llm_models()
178
+
179
+ for m in models:
180
+ if model_name in m["id"]:
181
+ return int(m["max_context_length"])
182
+
183
+ return None
184
+
185
+
142
186
  class OllamaProvider(OpenAIProvider):
143
187
  """Ollama provider that uses the native /api/generate endpoint
144
188
 
letta/schemas/file.py ADDED
@@ -0,0 +1,31 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from pydantic import Field
5
+
6
+ from letta.schemas.letta_base import LettaBase
7
+ from letta.utils import get_utc_time
8
+
9
+
10
+ class FileMetadataBase(LettaBase):
11
+ """Base class for FileMetadata schemas"""
12
+
13
+ __id_prefix__ = "file"
14
+
15
+
16
+ class FileMetadata(FileMetadataBase):
17
+ """Representation of a single FileMetadata"""
18
+
19
+ id: str = FileMetadataBase.generate_id_field()
20
+ user_id: str = Field(description="The unique identifier of the user associated with the document.")
21
+ source_id: str = Field(..., description="The unique identifier of the source associated with the document.")
22
+ file_name: Optional[str] = Field(None, description="The name of the file.")
23
+ file_path: Optional[str] = Field(None, description="The path to the file.")
24
+ file_type: Optional[str] = Field(None, description="The type of the file (MIME type).")
25
+ file_size: Optional[int] = Field(None, description="The size of the file in bytes.")
26
+ file_creation_date: Optional[str] = Field(None, description="The creation date of the file.")
27
+ file_last_modified_date: Optional[str] = Field(None, description="The last modified date of the file.")
28
+ created_at: datetime = Field(default_factory=get_utc_time, description="The creation date of this file metadata object.")
29
+
30
+ class Config:
31
+ extra = "allow"
letta/schemas/job.py CHANGED
@@ -15,7 +15,7 @@ class JobBase(LettaBase):
15
15
 
16
16
  class Job(JobBase):
17
17
  """
18
- Representation of offline jobs, used for tracking status of data loading tasks (involving parsing and embedding documents).
18
+ Representation of offline jobs, used for tracking status of data loading tasks (involving parsing and embedding files).
19
19
 
20
20
  Parameters:
21
21
  id (str): The unique identifier of the job.
@@ -1,13 +1,13 @@
1
- from typing import List
1
+ from typing import List, Union
2
2
 
3
3
  from pydantic import BaseModel, Field
4
4
 
5
5
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
6
- from letta.schemas.message import MessageCreate
6
+ from letta.schemas.message import Message, MessageCreate
7
7
 
8
8
 
9
9
  class LettaRequest(BaseModel):
10
- messages: List[MessageCreate] = Field(..., description="The messages to be sent to the agent.")
10
+ messages: Union[List[MessageCreate], List[Message]] = Field(..., description="The messages to be sent to the agent.")
11
11
  run_async: bool = Field(default=False, description="Whether to asynchronously send the messages to the agent.") # TODO: implement
12
12
 
13
13
  stream_steps: bool = Field(
@@ -33,6 +33,7 @@ class LLMConfig(BaseModel):
33
33
  "koboldcpp",
34
34
  "vllm",
35
35
  "hugging-face",
36
+ "mistral",
36
37
  ] = Field(..., description="The endpoint type for the model.")
37
38
  model_endpoint: Optional[str] = Field(None, description="The endpoint for the model.")
38
39
  model_wrapper: Optional[str] = Field(None, description="The wrapper for the model.")
letta/schemas/message.py CHANGED
@@ -2,7 +2,7 @@ import copy
2
2
  import json
3
3
  import warnings
4
4
  from datetime import datetime, timezone
5
- from typing import List, Optional
5
+ from typing import List, Literal, Optional
6
6
 
7
7
  from pydantic import Field, field_validator
8
8
 
@@ -57,7 +57,11 @@ class BaseMessage(LettaBase):
57
57
  class MessageCreate(BaseMessage):
58
58
  """Request to create a message"""
59
59
 
60
- role: MessageRole = Field(..., description="The role of the participant.")
60
+ # In the simplified format, only allow simple roles
61
+ role: Literal[
62
+ MessageRole.user,
63
+ MessageRole.system,
64
+ ] = Field(..., description="The role of the participant.")
61
65
  text: str = Field(..., description="The text of the message.")
62
66
  name: Optional[str] = Field(None, description="The name of the participant.")
63
67
 
letta/schemas/passage.py CHANGED
@@ -19,8 +19,8 @@ class PassageBase(LettaBase):
19
19
  # origin data source
20
20
  source_id: Optional[str] = Field(None, description="The data source of the passage.")
21
21
 
22
- # document association
23
- doc_id: Optional[str] = Field(None, description="The unique identifier of the document associated with the passage.")
22
+ # file association
23
+ file_id: Optional[str] = Field(None, description="The unique identifier of the file associated with the passage.")
24
24
  metadata_: Optional[Dict] = Field({}, description="The metadata of the passage.")
25
25
 
26
26
 
@@ -36,7 +36,7 @@ class Passage(PassageBase):
36
36
  user_id (str): The unique identifier of the user associated with the passage.
37
37
  agent_id (str): The unique identifier of the agent associated with the passage.
38
38
  source_id (str): The data source of the passage.
39
- doc_id (str): The unique identifier of the document associated with the passage.
39
+ file_id (str): The unique identifier of the file associated with the passage.
40
40
  """
41
41
 
42
42
  id: str = PassageBase.generate_id_field()
letta/schemas/source.py CHANGED
@@ -28,7 +28,7 @@ class SourceCreate(BaseSource):
28
28
 
29
29
  class Source(BaseSource):
30
30
  """
31
- Representation of a source, which is a collection of documents and passages.
31
+ Representation of a source, which is a collection of files and passages.
32
32
 
33
33
  Parameters:
34
34
  id (str): The ID of the source
@@ -59,4 +59,4 @@ class UploadFileToSourceRequest(BaseModel):
59
59
  class UploadFileToSourceResponse(BaseModel):
60
60
  source: Source = Field(..., description="The source the file was uploaded to.")
61
61
  added_passages: int = Field(..., description="The number of passages added to the source.")
62
- added_documents: int = Field(..., description="The number of documents added to the source.")
62
+ added_documents: int = Field(..., description="The number of files added to the source.")
@@ -8,7 +8,7 @@ from starlette.responses import StreamingResponse
8
8
 
9
9
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
10
10
  from letta.schemas.agent import AgentState, CreateAgent, UpdateAgentState
11
- from letta.schemas.enums import MessageRole, MessageStreamStatus
11
+ from letta.schemas.enums import MessageStreamStatus
12
12
  from letta.schemas.letta_message import (
13
13
  LegacyLettaMessage,
14
14
  LettaMessage,
@@ -23,7 +23,7 @@ from letta.schemas.memory import (
23
23
  Memory,
24
24
  RecallMemorySummary,
25
25
  )
26
- from letta.schemas.message import Message, UpdateMessage
26
+ from letta.schemas.message import Message, MessageCreate, UpdateMessage
27
27
  from letta.schemas.passage import Passage
28
28
  from letta.schemas.source import Source
29
29
  from letta.server.rest_api.interface import StreamingServerInterface
@@ -326,14 +326,15 @@ async def send_message(
326
326
 
327
327
  # TODO(charles): support sending multiple messages
328
328
  assert len(request.messages) == 1, f"Multiple messages not supported: {request.messages}"
329
- message = request.messages[0]
329
+ request.messages[0]
330
330
 
331
331
  return await send_message_to_agent(
332
332
  server=server,
333
333
  agent_id=agent_id,
334
334
  user_id=actor.id,
335
- role=message.role,
336
- message=message.text,
335
+ # role=message.role,
336
+ # message=message.text,
337
+ messages=request.messages,
337
338
  stream_steps=request.stream_steps,
338
339
  stream_tokens=request.stream_tokens,
339
340
  return_message_object=request.return_message_object,
@@ -349,8 +350,8 @@ async def send_message_to_agent(
349
350
  server: SyncServer,
350
351
  agent_id: str,
351
352
  user_id: str,
352
- role: MessageRole,
353
- message: str,
353
+ # role: MessageRole,
354
+ messages: Union[List[Message], List[MessageCreate]],
354
355
  stream_steps: bool,
355
356
  stream_tokens: bool,
356
357
  # related to whether or not we return `LettaMessage`s or `Message`s
@@ -367,14 +368,6 @@ async def send_message_to_agent(
367
368
  # TODO: @charles is this the correct way to handle?
368
369
  include_final_message = True
369
370
 
370
- # determine role
371
- if role == MessageRole.user:
372
- message_func = server.user_message
373
- elif role == MessageRole.system:
374
- message_func = server.system_message
375
- else:
376
- raise HTTPException(status_code=500, detail=f"Bad role {role}")
377
-
378
371
  if not stream_steps and stream_tokens:
379
372
  raise HTTPException(status_code=400, detail="stream_steps must be 'true' if stream_tokens is 'true'")
380
373
 
@@ -413,7 +406,8 @@ async def send_message_to_agent(
413
406
  # Offload the synchronous message_func to a separate thread
414
407
  streaming_interface.stream_start()
415
408
  task = asyncio.create_task(
416
- asyncio.to_thread(message_func, user_id=user_id, agent_id=agent_id, message=message, timestamp=timestamp)
409
+ # asyncio.to_thread(message_func, user_id=user_id, agent_id=agent_id, message=message, timestamp=timestamp)
410
+ asyncio.to_thread(server.send_messages, user_id=user_id, agent_id=agent_id, messages=messages)
417
411
  )
418
412
 
419
413
  if stream_steps:
@@ -1,6 +1,6 @@
1
1
  from typing import List, Optional
2
2
 
3
- from fastapi import APIRouter, Depends, Header, Query
3
+ from fastapi import APIRouter, Depends, Header, HTTPException, Query
4
4
 
5
5
  from letta.schemas.job import Job
6
6
  from letta.server.rest_api.utils import get_letta_server
@@ -54,3 +54,19 @@ def get_job(
54
54
  """
55
55
 
56
56
  return server.get_job(job_id=job_id)
57
+
58
+
59
+ @router.delete("/{job_id}", response_model=Job, operation_id="delete_job")
60
+ def delete_job(
61
+ job_id: str,
62
+ server: "SyncServer" = Depends(get_letta_server),
63
+ ):
64
+ """
65
+ Delete a job by its job_id.
66
+ """
67
+ job = server.get_job(job_id=job_id)
68
+ if not job:
69
+ raise HTTPException(status_code=404, detail="Job not found")
70
+
71
+ server.delete_job(job_id=job_id)
72
+ return job
@@ -4,7 +4,7 @@ from typing import List, Optional
4
4
 
5
5
  from fastapi import APIRouter, BackgroundTasks, Depends, Header, Query, UploadFile
6
6
 
7
- from letta.schemas.document import Document
7
+ from letta.schemas.file import FileMetadata
8
8
  from letta.schemas.job import Job
9
9
  from letta.schemas.passage import Passage
10
10
  from letta.schemas.source import Source, SourceCreate, SourceUpdate
@@ -186,19 +186,17 @@ def list_passages(
186
186
  return passages
187
187
 
188
188
 
189
- @router.get("/{source_id}/documents", response_model=List[Document], operation_id="list_source_documents")
190
- def list_documents(
189
+ @router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_files_from_source")
190
+ def list_files_from_source(
191
191
  source_id: str,
192
+ limit: int = Query(1000, description="Number of files to return"),
193
+ cursor: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
192
194
  server: "SyncServer" = Depends(get_letta_server),
193
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
194
195
  ):
195
196
  """
196
- List all documents associated with a data source.
197
+ List paginated files associated with a data source.
197
198
  """
198
- actor = server.get_user_or_default(user_id=user_id)
199
-
200
- documents = server.list_data_source_documents(user_id=actor.id, source_id=source_id)
201
- return documents
199
+ return server.list_files_from_source(source_id=source_id, limit=limit, cursor=cursor)
202
200
 
203
201
 
204
202
  def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, file: UploadFile, bytes: bytes):