letta-nightly 0.5.2.dev20241112104101__py3-none-any.whl → 0.5.2.dev20241113234401__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/agent.py +4 -2
- letta/agent_store/db.py +2 -1
- letta/cli/cli.py +1 -0
- letta/client/client.py +19 -16
- letta/data_sources/connectors.py +5 -10
- letta/llm_api/google_ai.py +0 -2
- letta/llm_api/openai.py +1 -0
- letta/memory.py +10 -6
- letta/metadata.py +3 -165
- letta/orm/__init__.py +2 -0
- letta/orm/file.py +29 -0
- letta/orm/mixins.py +8 -0
- letta/orm/organization.py +4 -4
- letta/orm/source.py +51 -0
- letta/orm/tool.py +0 -1
- letta/orm/user.py +0 -2
- letta/providers.py +0 -1
- letta/schemas/file.py +5 -5
- letta/schemas/source.py +30 -24
- letta/server/rest_api/app.py +27 -0
- letta/server/rest_api/routers/v1/sources.py +15 -15
- letta/server/server.py +38 -65
- letta/services/organization_manager.py +12 -13
- letta/services/source_manager.py +145 -0
- {letta_nightly-0.5.2.dev20241112104101.dist-info → letta_nightly-0.5.2.dev20241113234401.dist-info}/METADATA +1 -1
- {letta_nightly-0.5.2.dev20241112104101.dist-info → letta_nightly-0.5.2.dev20241113234401.dist-info}/RECORD +29 -26
- {letta_nightly-0.5.2.dev20241112104101.dist-info → letta_nightly-0.5.2.dev20241113234401.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.2.dev20241112104101.dist-info → letta_nightly-0.5.2.dev20241113234401.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.2.dev20241112104101.dist-info → letta_nightly-0.5.2.dev20241113234401.dist-info}/entry_points.txt +0 -0
letta/agent.py
CHANGED
|
@@ -46,6 +46,8 @@ from letta.schemas.passage import Passage
|
|
|
46
46
|
from letta.schemas.tool import Tool
|
|
47
47
|
from letta.schemas.tool_rule import TerminalToolRule
|
|
48
48
|
from letta.schemas.usage import LettaUsageStatistics
|
|
49
|
+
from letta.services.source_manager import SourceManager
|
|
50
|
+
from letta.services.user_manager import UserManager
|
|
49
51
|
from letta.system import (
|
|
50
52
|
get_heartbeat,
|
|
51
53
|
get_initial_boot_messages,
|
|
@@ -1311,7 +1313,7 @@ class Agent(BaseAgent):
|
|
|
1311
1313
|
def attach_source(self, source_id: str, source_connector: StorageConnector, ms: MetadataStore):
|
|
1312
1314
|
"""Attach data with name `source_name` to the agent from source_connector."""
|
|
1313
1315
|
# TODO: eventually, adding a data source should just give access to the retriever the source table, rather than modifying archival memory
|
|
1314
|
-
|
|
1316
|
+
user = UserManager().get_user_by_id(self.agent_state.user_id)
|
|
1315
1317
|
filters = {"user_id": self.agent_state.user_id, "source_id": source_id}
|
|
1316
1318
|
size = source_connector.size(filters)
|
|
1317
1319
|
page_size = 100
|
|
@@ -1339,7 +1341,7 @@ class Agent(BaseAgent):
|
|
|
1339
1341
|
self.persistence_manager.archival_memory.storage.save()
|
|
1340
1342
|
|
|
1341
1343
|
# attach to agent
|
|
1342
|
-
source =
|
|
1344
|
+
source = SourceManager().get_source_by_id(source_id=source_id, actor=user)
|
|
1343
1345
|
assert source is not None, f"Source {source_id} not found in metadata store"
|
|
1344
1346
|
ms.attach_source(agent_id=self.agent_state.id, source_id=source_id, user_id=self.agent_state.user_id)
|
|
1345
1347
|
|
letta/agent_store/db.py
CHANGED
|
@@ -27,8 +27,9 @@ from tqdm import tqdm
|
|
|
27
27
|
from letta.agent_store.storage import StorageConnector, TableType
|
|
28
28
|
from letta.config import LettaConfig
|
|
29
29
|
from letta.constants import MAX_EMBEDDING_DIM
|
|
30
|
-
from letta.metadata import EmbeddingConfigColumn,
|
|
30
|
+
from letta.metadata import EmbeddingConfigColumn, ToolCallColumn
|
|
31
31
|
from letta.orm.base import Base
|
|
32
|
+
from letta.orm.file import FileMetadata as FileMetadataModel
|
|
32
33
|
|
|
33
34
|
# from letta.schemas.message import Message, Passage, Record, RecordType, ToolCall
|
|
34
35
|
from letta.schemas.message import Message
|
letta/cli/cli.py
CHANGED
|
@@ -47,6 +47,7 @@ def server(
|
|
|
47
47
|
host: Annotated[Optional[str], typer.Option(help="Host to run the server on (default to localhost)")] = None,
|
|
48
48
|
debug: Annotated[bool, typer.Option(help="Turn debugging output on")] = False,
|
|
49
49
|
ade: Annotated[bool, typer.Option(help="Allows remote access")] = False,
|
|
50
|
+
secure: Annotated[bool, typer.Option(help="Adds simple security access")] = False,
|
|
50
51
|
):
|
|
51
52
|
"""Launch a Letta server process"""
|
|
52
53
|
if type == ServerChoice.rest_api:
|
letta/client/client.py
CHANGED
|
@@ -238,7 +238,7 @@ class AbstractClient(object):
|
|
|
238
238
|
def delete_file_from_source(self, source_id: str, file_id: str) -> None:
|
|
239
239
|
raise NotImplementedError
|
|
240
240
|
|
|
241
|
-
def create_source(self, name: str) -> Source:
|
|
241
|
+
def create_source(self, name: str, embedding_config: Optional[EmbeddingConfig] = None) -> Source:
|
|
242
242
|
raise NotImplementedError
|
|
243
243
|
|
|
244
244
|
def delete_source(self, source_id: str):
|
|
@@ -1188,7 +1188,7 @@ class RESTClient(AbstractClient):
|
|
|
1188
1188
|
if response.status_code not in [200, 204]:
|
|
1189
1189
|
raise ValueError(f"Failed to delete tool: {response.text}")
|
|
1190
1190
|
|
|
1191
|
-
def create_source(self, name: str) -> Source:
|
|
1191
|
+
def create_source(self, name: str, embedding_config: Optional[EmbeddingConfig] = None) -> Source:
|
|
1192
1192
|
"""
|
|
1193
1193
|
Create a source
|
|
1194
1194
|
|
|
@@ -1198,7 +1198,8 @@ class RESTClient(AbstractClient):
|
|
|
1198
1198
|
Returns:
|
|
1199
1199
|
source (Source): Created source
|
|
1200
1200
|
"""
|
|
1201
|
-
|
|
1201
|
+
source_create = SourceCreate(name=name, embedding_config=embedding_config or self._default_embedding_config)
|
|
1202
|
+
payload = source_create.model_dump()
|
|
1202
1203
|
response = requests.post(f"{self.base_url}/{self.api_prefix}/sources", json=payload, headers=self.headers)
|
|
1203
1204
|
response_json = response.json()
|
|
1204
1205
|
return Source(**response_json)
|
|
@@ -1253,7 +1254,7 @@ class RESTClient(AbstractClient):
|
|
|
1253
1254
|
Returns:
|
|
1254
1255
|
source (Source): Updated source
|
|
1255
1256
|
"""
|
|
1256
|
-
request = SourceUpdate(
|
|
1257
|
+
request = SourceUpdate(name=name)
|
|
1257
1258
|
response = requests.patch(f"{self.base_url}/{self.api_prefix}/sources/{source_id}", json=request.model_dump(), headers=self.headers)
|
|
1258
1259
|
if response.status_code != 200:
|
|
1259
1260
|
raise ValueError(f"Failed to update source: {response.text}")
|
|
@@ -2394,7 +2395,7 @@ class LocalClient(AbstractClient):
|
|
|
2394
2395
|
Args:
|
|
2395
2396
|
id (str): ID of the tool
|
|
2396
2397
|
"""
|
|
2397
|
-
return self.server.tool_manager.delete_tool_by_id(id,
|
|
2398
|
+
return self.server.tool_manager.delete_tool_by_id(id, actor=self.user)
|
|
2398
2399
|
|
|
2399
2400
|
def get_tool_id(self, name: str) -> Optional[str]:
|
|
2400
2401
|
"""
|
|
@@ -2439,7 +2440,7 @@ class LocalClient(AbstractClient):
|
|
|
2439
2440
|
return job
|
|
2440
2441
|
|
|
2441
2442
|
def delete_file_from_source(self, source_id: str, file_id: str):
|
|
2442
|
-
self.server.
|
|
2443
|
+
self.server.source_manager.delete_file(file_id, actor=self.user)
|
|
2443
2444
|
|
|
2444
2445
|
def get_job(self, job_id: str):
|
|
2445
2446
|
return self.server.get_job(job_id=job_id)
|
|
@@ -2453,7 +2454,7 @@ class LocalClient(AbstractClient):
|
|
|
2453
2454
|
def list_active_jobs(self):
|
|
2454
2455
|
return self.server.list_active_jobs(user_id=self.user_id)
|
|
2455
2456
|
|
|
2456
|
-
def create_source(self, name: str) -> Source:
|
|
2457
|
+
def create_source(self, name: str, embedding_config: Optional[EmbeddingConfig] = None) -> Source:
|
|
2457
2458
|
"""
|
|
2458
2459
|
Create a source
|
|
2459
2460
|
|
|
@@ -2463,8 +2464,10 @@ class LocalClient(AbstractClient):
|
|
|
2463
2464
|
Returns:
|
|
2464
2465
|
source (Source): Created source
|
|
2465
2466
|
"""
|
|
2466
|
-
|
|
2467
|
-
|
|
2467
|
+
source = Source(
|
|
2468
|
+
name=name, embedding_config=embedding_config or self._default_embedding_config, organization_id=self.user.organization_id
|
|
2469
|
+
)
|
|
2470
|
+
return self.server.source_manager.create_source(source=source, actor=self.user)
|
|
2468
2471
|
|
|
2469
2472
|
def delete_source(self, source_id: str):
|
|
2470
2473
|
"""
|
|
@@ -2475,7 +2478,7 @@ class LocalClient(AbstractClient):
|
|
|
2475
2478
|
"""
|
|
2476
2479
|
|
|
2477
2480
|
# TODO: delete source data
|
|
2478
|
-
self.server.delete_source(source_id=source_id,
|
|
2481
|
+
self.server.delete_source(source_id=source_id, actor=self.user)
|
|
2479
2482
|
|
|
2480
2483
|
def get_source(self, source_id: str) -> Source:
|
|
2481
2484
|
"""
|
|
@@ -2487,7 +2490,7 @@ class LocalClient(AbstractClient):
|
|
|
2487
2490
|
Returns:
|
|
2488
2491
|
source (Source): Source
|
|
2489
2492
|
"""
|
|
2490
|
-
return self.server.
|
|
2493
|
+
return self.server.source_manager.get_source_by_id(source_id=source_id, actor=self.user)
|
|
2491
2494
|
|
|
2492
2495
|
def get_source_id(self, source_name: str) -> str:
|
|
2493
2496
|
"""
|
|
@@ -2499,7 +2502,7 @@ class LocalClient(AbstractClient):
|
|
|
2499
2502
|
Returns:
|
|
2500
2503
|
source_id (str): ID of the source
|
|
2501
2504
|
"""
|
|
2502
|
-
return self.server.
|
|
2505
|
+
return self.server.source_manager.get_source_by_name(source_name=source_name, actor=self.user).id
|
|
2503
2506
|
|
|
2504
2507
|
def attach_source_to_agent(self, agent_id: str, source_id: Optional[str] = None, source_name: Optional[str] = None):
|
|
2505
2508
|
"""
|
|
@@ -2532,7 +2535,7 @@ class LocalClient(AbstractClient):
|
|
|
2532
2535
|
sources (List[Source]): List of sources
|
|
2533
2536
|
"""
|
|
2534
2537
|
|
|
2535
|
-
return self.server.list_all_sources(
|
|
2538
|
+
return self.server.list_all_sources(actor=self.user)
|
|
2536
2539
|
|
|
2537
2540
|
def list_attached_sources(self, agent_id: str) -> List[Source]:
|
|
2538
2541
|
"""
|
|
@@ -2558,7 +2561,7 @@ class LocalClient(AbstractClient):
|
|
|
2558
2561
|
Returns:
|
|
2559
2562
|
files (List[FileMetadata]): List of files
|
|
2560
2563
|
"""
|
|
2561
|
-
return self.server.
|
|
2564
|
+
return self.server.source_manager.list_files(source_id=source_id, limit=limit, cursor=cursor, actor=self.user)
|
|
2562
2565
|
|
|
2563
2566
|
def update_source(self, source_id: str, name: Optional[str] = None) -> Source:
|
|
2564
2567
|
"""
|
|
@@ -2572,8 +2575,8 @@ class LocalClient(AbstractClient):
|
|
|
2572
2575
|
source (Source): Updated source
|
|
2573
2576
|
"""
|
|
2574
2577
|
# TODO should the arg here just be "source_update: Source"?
|
|
2575
|
-
request = SourceUpdate(
|
|
2576
|
-
return self.server.update_source(
|
|
2578
|
+
request = SourceUpdate(name=name)
|
|
2579
|
+
return self.server.source_manager.update_source(source_id=source_id, source_update=request, actor=self.user)
|
|
2577
2580
|
|
|
2578
2581
|
# archival memory
|
|
2579
2582
|
|
letta/data_sources/connectors.py
CHANGED
|
@@ -12,6 +12,7 @@ from letta.embeddings import embedding_model
|
|
|
12
12
|
from letta.schemas.file import FileMetadata
|
|
13
13
|
from letta.schemas.passage import Passage
|
|
14
14
|
from letta.schemas.source import Source
|
|
15
|
+
from letta.services.source_manager import SourceManager
|
|
15
16
|
from letta.utils import create_uuid_from_string
|
|
16
17
|
|
|
17
18
|
|
|
@@ -41,13 +42,8 @@ class DataConnector:
|
|
|
41
42
|
"""
|
|
42
43
|
|
|
43
44
|
|
|
44
|
-
def load_data(
|
|
45
|
-
connector
|
|
46
|
-
source: Source,
|
|
47
|
-
passage_store: StorageConnector,
|
|
48
|
-
file_metadata_store: StorageConnector,
|
|
49
|
-
):
|
|
50
|
-
"""Load data from a connector (generates file and passages) into a specified source_id, associatedw with a user_id."""
|
|
45
|
+
def load_data(connector: DataConnector, source: Source, passage_store: StorageConnector, source_manager: SourceManager, actor: "User"):
|
|
46
|
+
"""Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id."""
|
|
51
47
|
embedding_config = source.embedding_config
|
|
52
48
|
|
|
53
49
|
# embedding model
|
|
@@ -60,7 +56,7 @@ def load_data(
|
|
|
60
56
|
file_count = 0
|
|
61
57
|
for file_metadata in connector.find_files(source):
|
|
62
58
|
file_count += 1
|
|
63
|
-
|
|
59
|
+
source_manager.create_file(file_metadata, actor)
|
|
64
60
|
|
|
65
61
|
# generate passages
|
|
66
62
|
for passage_text, passage_metadata in connector.generate_passages(file_metadata, chunk_size=embedding_config.embedding_chunk_size):
|
|
@@ -88,7 +84,7 @@ def load_data(
|
|
|
88
84
|
file_id=file_metadata.id,
|
|
89
85
|
source_id=source.id,
|
|
90
86
|
metadata_=passage_metadata,
|
|
91
|
-
user_id=source.
|
|
87
|
+
user_id=source.created_by_id,
|
|
92
88
|
embedding_config=source.embedding_config,
|
|
93
89
|
embedding=embedding,
|
|
94
90
|
)
|
|
@@ -155,7 +151,6 @@ class DirectoryConnector(DataConnector):
|
|
|
155
151
|
|
|
156
152
|
for metadata in extract_metadata_from_files(files):
|
|
157
153
|
yield FileMetadata(
|
|
158
|
-
user_id=source.user_id,
|
|
159
154
|
source_id=source.id,
|
|
160
155
|
file_name=metadata.get("file_name"),
|
|
161
156
|
file_path=metadata.get("file_path"),
|
letta/llm_api/google_ai.py
CHANGED
|
@@ -95,10 +95,8 @@ def google_ai_get_model_list(base_url: str, api_key: str, key_in_header: bool =
|
|
|
95
95
|
|
|
96
96
|
try:
|
|
97
97
|
response = requests.get(url, headers=headers)
|
|
98
|
-
printd(f"response = {response}")
|
|
99
98
|
response.raise_for_status() # Raises HTTPError for 4XX/5XX status
|
|
100
99
|
response = response.json() # convert to dict from string
|
|
101
|
-
printd(f"response.json = {response}")
|
|
102
100
|
|
|
103
101
|
# Grab the models out
|
|
104
102
|
model_list = response["models"]
|
letta/llm_api/openai.py
CHANGED
|
@@ -126,6 +126,7 @@ def build_openai_chat_completions_request(
|
|
|
126
126
|
openai_message_list = [
|
|
127
127
|
cast_message_to_subtype(m.to_openai_dict(put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs)) for m in messages
|
|
128
128
|
]
|
|
129
|
+
|
|
129
130
|
if llm_config.model:
|
|
130
131
|
model = llm_config.model
|
|
131
132
|
else:
|
letta/memory.py
CHANGED
|
@@ -7,6 +7,7 @@ from letta.embeddings import embedding_model, parse_and_chunk_text, query_embedd
|
|
|
7
7
|
from letta.llm_api.llm_api_tools import create
|
|
8
8
|
from letta.prompts.gpt_summarize import SYSTEM as SUMMARY_PROMPT_SYSTEM
|
|
9
9
|
from letta.schemas.agent import AgentState
|
|
10
|
+
from letta.schemas.enums import MessageRole
|
|
10
11
|
from letta.schemas.memory import Memory
|
|
11
12
|
from letta.schemas.message import Message
|
|
12
13
|
from letta.schemas.passage import Passage
|
|
@@ -50,7 +51,6 @@ def _format_summary_history(message_history: List[Message]):
|
|
|
50
51
|
def summarize_messages(
|
|
51
52
|
agent_state: AgentState,
|
|
52
53
|
message_sequence_to_summarize: List[Message],
|
|
53
|
-
insert_acknowledgement_assistant_message: bool = True,
|
|
54
54
|
):
|
|
55
55
|
"""Summarize a message sequence using GPT"""
|
|
56
56
|
# we need the context_window
|
|
@@ -70,13 +70,17 @@ def summarize_messages(
|
|
|
70
70
|
dummy_user_id = agent_state.user_id
|
|
71
71
|
dummy_agent_id = agent_state.id
|
|
72
72
|
message_sequence = []
|
|
73
|
-
message_sequence.append(Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role=
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
message_sequence.append(Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role=MessageRole.system, text=summary_prompt))
|
|
74
|
+
message_sequence.append(
|
|
75
|
+
Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role=MessageRole.assistant, text=MESSAGE_SUMMARY_REQUEST_ACK)
|
|
76
|
+
)
|
|
77
|
+
message_sequence.append(Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role=MessageRole.user, text=summary_input))
|
|
77
78
|
|
|
79
|
+
# TODO: We need to eventually have a separate LLM config for the summarizer LLM
|
|
80
|
+
llm_config_no_inner_thoughts = agent_state.llm_config.model_copy(deep=True)
|
|
81
|
+
llm_config_no_inner_thoughts.put_inner_thoughts_in_kwargs = False
|
|
78
82
|
response = create(
|
|
79
|
-
llm_config=
|
|
83
|
+
llm_config=llm_config_no_inner_thoughts,
|
|
80
84
|
user_id=agent_state.user_id,
|
|
81
85
|
messages=message_sequence,
|
|
82
86
|
stream=False,
|
letta/metadata.py
CHANGED
|
@@ -11,7 +11,6 @@ from sqlalchemy import (
|
|
|
11
11
|
Column,
|
|
12
12
|
DateTime,
|
|
13
13
|
Index,
|
|
14
|
-
Integer,
|
|
15
14
|
String,
|
|
16
15
|
TypeDecorator,
|
|
17
16
|
)
|
|
@@ -24,12 +23,10 @@ from letta.schemas.api_key import APIKey
|
|
|
24
23
|
from letta.schemas.block import Block, Human, Persona
|
|
25
24
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
26
25
|
from letta.schemas.enums import JobStatus
|
|
27
|
-
from letta.schemas.file import FileMetadata
|
|
28
26
|
from letta.schemas.job import Job
|
|
29
27
|
from letta.schemas.llm_config import LLMConfig
|
|
30
28
|
from letta.schemas.memory import Memory
|
|
31
29
|
from letta.schemas.openai.chat_completions import ToolCall, ToolCallFunction
|
|
32
|
-
from letta.schemas.source import Source
|
|
33
30
|
from letta.schemas.tool_rule import (
|
|
34
31
|
BaseToolRule,
|
|
35
32
|
InitToolRule,
|
|
@@ -41,41 +38,6 @@ from letta.settings import settings
|
|
|
41
38
|
from letta.utils import enforce_types, get_utc_time, printd
|
|
42
39
|
|
|
43
40
|
|
|
44
|
-
class FileMetadataModel(Base):
|
|
45
|
-
__tablename__ = "files"
|
|
46
|
-
__table_args__ = {"extend_existing": True}
|
|
47
|
-
|
|
48
|
-
id = Column(String, primary_key=True, nullable=False)
|
|
49
|
-
user_id = Column(String, nullable=False)
|
|
50
|
-
# TODO: Investigate why this breaks during table creation due to FK
|
|
51
|
-
# source_id = Column(String, ForeignKey("sources.id"), nullable=False)
|
|
52
|
-
source_id = Column(String, nullable=False)
|
|
53
|
-
file_name = Column(String, nullable=True)
|
|
54
|
-
file_path = Column(String, nullable=True)
|
|
55
|
-
file_type = Column(String, nullable=True)
|
|
56
|
-
file_size = Column(Integer, nullable=True)
|
|
57
|
-
file_creation_date = Column(String, nullable=True)
|
|
58
|
-
file_last_modified_date = Column(String, nullable=True)
|
|
59
|
-
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
60
|
-
|
|
61
|
-
def __repr__(self):
|
|
62
|
-
return f"<FileMetadata(id='{self.id}', source_id='{self.source_id}', file_name='{self.file_name}')>"
|
|
63
|
-
|
|
64
|
-
def to_record(self):
|
|
65
|
-
return FileMetadata(
|
|
66
|
-
id=self.id,
|
|
67
|
-
user_id=self.user_id,
|
|
68
|
-
source_id=self.source_id,
|
|
69
|
-
file_name=self.file_name,
|
|
70
|
-
file_path=self.file_path,
|
|
71
|
-
file_type=self.file_type,
|
|
72
|
-
file_size=self.file_size,
|
|
73
|
-
file_creation_date=self.file_creation_date,
|
|
74
|
-
file_last_modified_date=self.file_last_modified_date,
|
|
75
|
-
created_at=self.created_at,
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
|
|
79
41
|
class LLMConfigColumn(TypeDecorator):
|
|
80
42
|
"""Custom type for storing LLMConfig as JSON"""
|
|
81
43
|
|
|
@@ -292,40 +254,6 @@ class AgentModel(Base):
|
|
|
292
254
|
return agent_state
|
|
293
255
|
|
|
294
256
|
|
|
295
|
-
class SourceModel(Base):
|
|
296
|
-
"""Defines data model for storing Passages (consisting of text, embedding)"""
|
|
297
|
-
|
|
298
|
-
__tablename__ = "sources"
|
|
299
|
-
__table_args__ = {"extend_existing": True}
|
|
300
|
-
|
|
301
|
-
# Assuming passage_id is the primary key
|
|
302
|
-
# id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
303
|
-
id = Column(String, primary_key=True)
|
|
304
|
-
user_id = Column(String, nullable=False)
|
|
305
|
-
name = Column(String, nullable=False)
|
|
306
|
-
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
|
307
|
-
embedding_config = Column(EmbeddingConfigColumn)
|
|
308
|
-
description = Column(String)
|
|
309
|
-
metadata_ = Column(JSON)
|
|
310
|
-
Index(__tablename__ + "_idx_user", user_id),
|
|
311
|
-
|
|
312
|
-
# TODO: add num passages
|
|
313
|
-
|
|
314
|
-
def __repr__(self) -> str:
|
|
315
|
-
return f"<Source(passage_id='{self.id}', name='{self.name}')>"
|
|
316
|
-
|
|
317
|
-
def to_record(self) -> Source:
|
|
318
|
-
return Source(
|
|
319
|
-
id=self.id,
|
|
320
|
-
user_id=self.user_id,
|
|
321
|
-
name=self.name,
|
|
322
|
-
created_at=self.created_at,
|
|
323
|
-
embedding_config=self.embedding_config,
|
|
324
|
-
description=self.description,
|
|
325
|
-
metadata_=self.metadata_,
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
|
|
329
257
|
class AgentSourceMappingModel(Base):
|
|
330
258
|
"""Stores mapping between agent -> source"""
|
|
331
259
|
|
|
@@ -497,14 +425,6 @@ class MetadataStore:
|
|
|
497
425
|
session.add(AgentModel(**fields))
|
|
498
426
|
session.commit()
|
|
499
427
|
|
|
500
|
-
@enforce_types
|
|
501
|
-
def create_source(self, source: Source):
|
|
502
|
-
with self.session_maker() as session:
|
|
503
|
-
if session.query(SourceModel).filter(SourceModel.name == source.name).filter(SourceModel.user_id == source.user_id).count() > 0:
|
|
504
|
-
raise ValueError(f"Source with name {source.name} already exists for user {source.user_id}")
|
|
505
|
-
session.add(SourceModel(**vars(source)))
|
|
506
|
-
session.commit()
|
|
507
|
-
|
|
508
428
|
@enforce_types
|
|
509
429
|
def create_block(self, block: Block):
|
|
510
430
|
with self.session_maker() as session:
|
|
@@ -522,6 +442,7 @@ class MetadataStore:
|
|
|
522
442
|
):
|
|
523
443
|
|
|
524
444
|
raise ValueError(f"Block with name {block.template_name} already exists")
|
|
445
|
+
|
|
525
446
|
session.add(BlockModel(**vars(block)))
|
|
526
447
|
session.commit()
|
|
527
448
|
|
|
@@ -536,12 +457,6 @@ class MetadataStore:
|
|
|
536
457
|
session.query(AgentModel).filter(AgentModel.id == agent.id).update(fields)
|
|
537
458
|
session.commit()
|
|
538
459
|
|
|
539
|
-
@enforce_types
|
|
540
|
-
def update_source(self, source: Source):
|
|
541
|
-
with self.session_maker() as session:
|
|
542
|
-
session.query(SourceModel).filter(SourceModel.id == source.id).update(vars(source))
|
|
543
|
-
session.commit()
|
|
544
|
-
|
|
545
460
|
@enforce_types
|
|
546
461
|
def update_block(self, block: Block):
|
|
547
462
|
with self.session_maker() as session:
|
|
@@ -558,21 +473,6 @@ class MetadataStore:
|
|
|
558
473
|
session.add(BlockModel(**vars(block)))
|
|
559
474
|
session.commit()
|
|
560
475
|
|
|
561
|
-
@enforce_types
|
|
562
|
-
def delete_file_from_source(self, source_id: str, file_id: str, user_id: Optional[str]):
|
|
563
|
-
with self.session_maker() as session:
|
|
564
|
-
file_metadata = (
|
|
565
|
-
session.query(FileMetadataModel)
|
|
566
|
-
.filter(FileMetadataModel.source_id == source_id, FileMetadataModel.id == file_id, FileMetadataModel.user_id == user_id)
|
|
567
|
-
.first()
|
|
568
|
-
)
|
|
569
|
-
|
|
570
|
-
if file_metadata:
|
|
571
|
-
session.delete(file_metadata)
|
|
572
|
-
session.commit()
|
|
573
|
-
|
|
574
|
-
return file_metadata
|
|
575
|
-
|
|
576
476
|
@enforce_types
|
|
577
477
|
def delete_block(self, block_id: str):
|
|
578
478
|
with self.session_maker() as session:
|
|
@@ -591,29 +491,12 @@ class MetadataStore:
|
|
|
591
491
|
|
|
592
492
|
session.commit()
|
|
593
493
|
|
|
594
|
-
@enforce_types
|
|
595
|
-
def delete_source(self, source_id: str):
|
|
596
|
-
with self.session_maker() as session:
|
|
597
|
-
# delete from sources table
|
|
598
|
-
session.query(SourceModel).filter(SourceModel.id == source_id).delete()
|
|
599
|
-
|
|
600
|
-
# delete any mappings
|
|
601
|
-
session.query(AgentSourceMappingModel).filter(AgentSourceMappingModel.source_id == source_id).delete()
|
|
602
|
-
|
|
603
|
-
session.commit()
|
|
604
|
-
|
|
605
494
|
@enforce_types
|
|
606
495
|
def list_agents(self, user_id: str) -> List[AgentState]:
|
|
607
496
|
with self.session_maker() as session:
|
|
608
497
|
results = session.query(AgentModel).filter(AgentModel.user_id == user_id).all()
|
|
609
498
|
return [r.to_record() for r in results]
|
|
610
499
|
|
|
611
|
-
@enforce_types
|
|
612
|
-
def list_sources(self, user_id: str) -> List[Source]:
|
|
613
|
-
with self.session_maker() as session:
|
|
614
|
-
results = session.query(SourceModel).filter(SourceModel.user_id == user_id).all()
|
|
615
|
-
return [r.to_record() for r in results]
|
|
616
|
-
|
|
617
500
|
@enforce_types
|
|
618
501
|
def get_agent(
|
|
619
502
|
self, agent_id: Optional[str] = None, agent_name: Optional[str] = None, user_id: Optional[str] = None
|
|
@@ -630,21 +513,6 @@ class MetadataStore:
|
|
|
630
513
|
assert len(results) == 1, f"Expected 1 result, got {len(results)}" # should only be one result
|
|
631
514
|
return results[0].to_record()
|
|
632
515
|
|
|
633
|
-
@enforce_types
|
|
634
|
-
def get_source(
|
|
635
|
-
self, source_id: Optional[str] = None, user_id: Optional[str] = None, source_name: Optional[str] = None
|
|
636
|
-
) -> Optional[Source]:
|
|
637
|
-
with self.session_maker() as session:
|
|
638
|
-
if source_id:
|
|
639
|
-
results = session.query(SourceModel).filter(SourceModel.id == source_id).all()
|
|
640
|
-
else:
|
|
641
|
-
assert user_id is not None and source_name is not None
|
|
642
|
-
results = session.query(SourceModel).filter(SourceModel.name == source_name).filter(SourceModel.user_id == user_id).all()
|
|
643
|
-
if len(results) == 0:
|
|
644
|
-
return None
|
|
645
|
-
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
|
|
646
|
-
return results[0].to_record()
|
|
647
|
-
|
|
648
516
|
@enforce_types
|
|
649
517
|
def get_block(self, block_id: str) -> Optional[Block]:
|
|
650
518
|
with self.session_maker() as session:
|
|
@@ -699,19 +567,10 @@ class MetadataStore:
|
|
|
699
567
|
session.commit()
|
|
700
568
|
|
|
701
569
|
@enforce_types
|
|
702
|
-
def
|
|
570
|
+
def list_attached_source_ids(self, agent_id: str) -> List[str]:
|
|
703
571
|
with self.session_maker() as session:
|
|
704
572
|
results = session.query(AgentSourceMappingModel).filter(AgentSourceMappingModel.agent_id == agent_id).all()
|
|
705
|
-
|
|
706
|
-
sources = []
|
|
707
|
-
# make sure source exists
|
|
708
|
-
for r in results:
|
|
709
|
-
source = self.get_source(source_id=r.source_id)
|
|
710
|
-
if source:
|
|
711
|
-
sources.append(source)
|
|
712
|
-
else:
|
|
713
|
-
printd(f"Warning: source {r.source_id} does not exist but exists in mapping database. This should never happen.")
|
|
714
|
-
return sources
|
|
573
|
+
return [r.source_id for r in results]
|
|
715
574
|
|
|
716
575
|
@enforce_types
|
|
717
576
|
def list_attached_agents(self, source_id: str) -> List[str]:
|
|
@@ -742,27 +601,6 @@ class MetadataStore:
|
|
|
742
601
|
session.add(JobModel(**vars(job)))
|
|
743
602
|
session.commit()
|
|
744
603
|
|
|
745
|
-
@enforce_types
|
|
746
|
-
def list_files_from_source(self, source_id: str, limit: int, cursor: Optional[str]):
|
|
747
|
-
with self.session_maker() as session:
|
|
748
|
-
# Start with the basic query filtered by source_id
|
|
749
|
-
query = session.query(FileMetadataModel).filter(FileMetadataModel.source_id == source_id)
|
|
750
|
-
|
|
751
|
-
if cursor:
|
|
752
|
-
# Assuming cursor is the ID of the last file in the previous page
|
|
753
|
-
query = query.filter(FileMetadataModel.id > cursor)
|
|
754
|
-
|
|
755
|
-
# Order by ID or other ordering criteria to ensure correct pagination
|
|
756
|
-
query = query.order_by(FileMetadataModel.id)
|
|
757
|
-
|
|
758
|
-
# Limit the number of results returned
|
|
759
|
-
results = query.limit(limit).all()
|
|
760
|
-
|
|
761
|
-
# Convert the results to the required FileMetadata objects
|
|
762
|
-
files = [r.to_record() for r in results]
|
|
763
|
-
|
|
764
|
-
return files
|
|
765
|
-
|
|
766
604
|
def delete_job(self, job_id: str):
|
|
767
605
|
with self.session_maker() as session:
|
|
768
606
|
session.query(JobModel).filter(JobModel.id == job_id).delete()
|
letta/orm/__init__.py
CHANGED
letta/orm/file.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import Integer, String
|
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
5
|
+
|
|
6
|
+
from letta.orm.mixins import OrganizationMixin, SourceMixin
|
|
7
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
8
|
+
from letta.schemas.file import FileMetadata as PydanticFileMetadata
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from letta.orm.organization import Organization
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FileMetadata(SqlalchemyBase, OrganizationMixin, SourceMixin):
|
|
15
|
+
"""Represents metadata for an uploaded file."""
|
|
16
|
+
|
|
17
|
+
__tablename__ = "files"
|
|
18
|
+
__pydantic_model__ = PydanticFileMetadata
|
|
19
|
+
|
|
20
|
+
file_name: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The name of the file.")
|
|
21
|
+
file_path: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The file path on the system.")
|
|
22
|
+
file_type: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The type of the file.")
|
|
23
|
+
file_size: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, doc="The size of the file in bytes.")
|
|
24
|
+
file_creation_date: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The creation date of the file.")
|
|
25
|
+
file_last_modified_date: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The last modified date of the file.")
|
|
26
|
+
|
|
27
|
+
# relationships
|
|
28
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="files", lazy="selectin")
|
|
29
|
+
source: Mapped["Source"] = relationship("Source", back_populates="files", lazy="selectin")
|
letta/orm/mixins.py
CHANGED
|
@@ -29,3 +29,11 @@ class UserMixin(Base):
|
|
|
29
29
|
__abstract__ = True
|
|
30
30
|
|
|
31
31
|
user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SourceMixin(Base):
|
|
35
|
+
"""Mixin for models (e.g. file) that belong to a source."""
|
|
36
|
+
|
|
37
|
+
__abstract__ = True
|
|
38
|
+
|
|
39
|
+
source_id: Mapped[str] = mapped_column(String, ForeignKey("sources.id"))
|
letta/orm/organization.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, List
|
|
2
2
|
|
|
3
|
-
from sqlalchemy import String
|
|
4
3
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
5
4
|
|
|
5
|
+
from letta.orm.file import FileMetadata
|
|
6
6
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
7
7
|
from letta.schemas.organization import Organization as PydanticOrganization
|
|
8
8
|
|
|
@@ -18,16 +18,16 @@ class Organization(SqlalchemyBase):
|
|
|
18
18
|
__tablename__ = "organizations"
|
|
19
19
|
__pydantic_model__ = PydanticOrganization
|
|
20
20
|
|
|
21
|
-
id: Mapped[str] = mapped_column(String, primary_key=True)
|
|
22
21
|
name: Mapped[str] = mapped_column(doc="The display name of the organization.")
|
|
23
22
|
|
|
23
|
+
# relationships
|
|
24
24
|
users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")
|
|
25
25
|
tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="organization", cascade="all, delete-orphan")
|
|
26
|
+
sources: Mapped[List["Source"]] = relationship("Source", back_populates="organization", cascade="all, delete-orphan")
|
|
26
27
|
agents_tags: Mapped[List["AgentsTags"]] = relationship("AgentsTags", back_populates="organization", cascade="all, delete-orphan")
|
|
27
|
-
|
|
28
|
+
files: Mapped[List["FileMetadata"]] = relationship("FileMetadata", back_populates="organization", cascade="all, delete-orphan")
|
|
28
29
|
# TODO: Map these relationships later when we actually make these models
|
|
29
30
|
# below is just a suggestion
|
|
30
31
|
# agents: Mapped[List["Agent"]] = relationship("Agent", back_populates="organization", cascade="all, delete-orphan")
|
|
31
|
-
# sources: Mapped[List["Source"]] = relationship("Source", back_populates="organization", cascade="all, delete-orphan")
|
|
32
32
|
# tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="organization", cascade="all, delete-orphan")
|
|
33
33
|
# documents: Mapped[List["Document"]] = relationship("Document", back_populates="organization", cascade="all, delete-orphan")
|