letta-nightly 0.4.1.dev20241014104152__py3-none-any.whl → 0.5.0.dev20241015014828__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.
- letta/__init__.py +2 -2
- letta/agent_store/db.py +18 -7
- letta/agent_store/lancedb.py +2 -2
- letta/agent_store/milvus.py +1 -1
- letta/agent_store/qdrant.py +1 -1
- letta/agent_store/storage.py +12 -10
- letta/cli/cli_load.py +1 -1
- letta/client/client.py +51 -0
- letta/data_sources/connectors.py +124 -124
- letta/data_sources/connectors_helper.py +97 -0
- letta/llm_api/mistral.py +47 -0
- letta/metadata.py +58 -0
- letta/providers.py +44 -0
- letta/schemas/file.py +31 -0
- letta/schemas/job.py +1 -1
- letta/schemas/letta_request.py +3 -3
- letta/schemas/llm_config.py +1 -0
- letta/schemas/message.py +6 -2
- letta/schemas/passage.py +3 -3
- letta/schemas/source.py +2 -2
- letta/server/rest_api/routers/v1/agents.py +10 -16
- letta/server/rest_api/routers/v1/jobs.py +17 -1
- letta/server/rest_api/routers/v1/sources.py +7 -9
- letta/server/server.py +86 -13
- letta/server/static_files/assets/{index-9a9c449b.js → index-dc228d4a.js} +4 -4
- letta/server/static_files/index.html +1 -1
- {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/METADATA +1 -1
- {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/RECORD +31 -29
- letta/schemas/document.py +0 -21
- {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/LICENSE +0 -0
- {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015014828.dist-info}/WHEEL +0 -0
- {letta_nightly-0.4.1.dev20241014104152.dist-info → letta_nightly-0.5.0.dev20241015014828.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
|
letta/llm_api/mistral.py
ADDED
|
@@ -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
|
|
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.
|
letta/schemas/letta_request.py
CHANGED
|
@@ -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(
|
letta/schemas/llm_config.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
#
|
|
23
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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}/
|
|
190
|
-
def
|
|
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
|
|
197
|
+
List paginated files associated with a data source.
|
|
197
198
|
"""
|
|
198
|
-
|
|
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):
|