letta-nightly 0.5.2.dev20241112104101__py3-none-any.whl → 0.5.2.dev20241113104112__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 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 = ms.get_source(source_id=source_id)
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/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
- payload = {"name": name}
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(id=source_id, name=name)
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}")
@@ -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
- request = SourceCreate(name=name)
2467
- return self.server.create_source(request=request, user_id=self.user_id)
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, user_id=self.user_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.get_source(source_id=source_id, user_id=self.user_id)
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.get_source_id(source_name=source_name, user_id=self.user_id)
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(user_id=self.user_id)
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
  """
@@ -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(id=source_id, name=name)
2576
- return self.server.update_source(request=request, user_id=self.user_id)
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
 
@@ -47,7 +47,7 @@ def load_data(
47
47
  passage_store: StorageConnector,
48
48
  file_metadata_store: StorageConnector,
49
49
  ):
50
- """Load data from a connector (generates file and passages) into a specified source_id, associatedw with a user_id."""
50
+ """Load data from a connector (generates file and passages) into a specified source_id, associated with a user_id."""
51
51
  embedding_config = source.embedding_config
52
52
 
53
53
  # embedding model
@@ -88,7 +88,7 @@ def load_data(
88
88
  file_id=file_metadata.id,
89
89
  source_id=source.id,
90
90
  metadata_=passage_metadata,
91
- user_id=source.user_id,
91
+ user_id=source.created_by_id,
92
92
  embedding_config=source.embedding_config,
93
93
  embedding=embedding,
94
94
  )
@@ -155,7 +155,7 @@ class DirectoryConnector(DataConnector):
155
155
 
156
156
  for metadata in extract_metadata_from_files(files):
157
157
  yield FileMetadata(
158
- user_id=source.user_id,
158
+ user_id=source.created_by_id,
159
159
  source_id=source.id,
160
160
  file_name=metadata.get("file_name"),
161
161
  file_path=metadata.get("file_path"),
@@ -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="system", text=summary_prompt))
74
- if insert_acknowledgement_assistant_message:
75
- message_sequence.append(Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role="assistant", text=MESSAGE_SUMMARY_REQUEST_ACK))
76
- message_sequence.append(Message(user_id=dummy_user_id, agent_id=dummy_agent_id, role="user", text=summary_input))
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=agent_state.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
@@ -29,7 +29,6 @@ from letta.schemas.job import Job
29
29
  from letta.schemas.llm_config import LLMConfig
30
30
  from letta.schemas.memory import Memory
31
31
  from letta.schemas.openai.chat_completions import ToolCall, ToolCallFunction
32
- from letta.schemas.source import Source
33
32
  from letta.schemas.tool_rule import (
34
33
  BaseToolRule,
35
34
  InitToolRule,
@@ -292,40 +291,6 @@ class AgentModel(Base):
292
291
  return agent_state
293
292
 
294
293
 
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
294
  class AgentSourceMappingModel(Base):
330
295
  """Stores mapping between agent -> source"""
331
296
 
@@ -497,14 +462,6 @@ class MetadataStore:
497
462
  session.add(AgentModel(**fields))
498
463
  session.commit()
499
464
 
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
465
  @enforce_types
509
466
  def create_block(self, block: Block):
510
467
  with self.session_maker() as session:
@@ -522,6 +479,7 @@ class MetadataStore:
522
479
  ):
523
480
 
524
481
  raise ValueError(f"Block with name {block.template_name} already exists")
482
+
525
483
  session.add(BlockModel(**vars(block)))
526
484
  session.commit()
527
485
 
@@ -536,12 +494,6 @@ class MetadataStore:
536
494
  session.query(AgentModel).filter(AgentModel.id == agent.id).update(fields)
537
495
  session.commit()
538
496
 
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
497
  @enforce_types
546
498
  def update_block(self, block: Block):
547
499
  with self.session_maker() as session:
@@ -591,29 +543,12 @@ class MetadataStore:
591
543
 
592
544
  session.commit()
593
545
 
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
546
  @enforce_types
606
547
  def list_agents(self, user_id: str) -> List[AgentState]:
607
548
  with self.session_maker() as session:
608
549
  results = session.query(AgentModel).filter(AgentModel.user_id == user_id).all()
609
550
  return [r.to_record() for r in results]
610
551
 
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
552
  @enforce_types
618
553
  def get_agent(
619
554
  self, agent_id: Optional[str] = None, agent_name: Optional[str] = None, user_id: Optional[str] = None
@@ -630,21 +565,6 @@ class MetadataStore:
630
565
  assert len(results) == 1, f"Expected 1 result, got {len(results)}" # should only be one result
631
566
  return results[0].to_record()
632
567
 
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
568
  @enforce_types
649
569
  def get_block(self, block_id: str) -> Optional[Block]:
650
570
  with self.session_maker() as session:
@@ -699,19 +619,10 @@ class MetadataStore:
699
619
  session.commit()
700
620
 
701
621
  @enforce_types
702
- def list_attached_sources(self, agent_id: str) -> List[Source]:
622
+ def list_attached_source_ids(self, agent_id: str) -> List[str]:
703
623
  with self.session_maker() as session:
704
624
  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
625
+ return [r.source_id for r in results]
715
626
 
716
627
  @enforce_types
717
628
  def list_attached_agents(self, source_id: str) -> List[str]:
letta/orm/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from letta.orm.base import Base
2
2
  from letta.orm.organization import Organization
3
+ from letta.orm.source import Source
3
4
  from letta.orm.tool import Tool
4
5
  from letta.orm.user import User
letta/orm/organization.py CHANGED
@@ -21,13 +21,13 @@ class Organization(SqlalchemyBase):
21
21
  id: Mapped[str] = mapped_column(String, primary_key=True)
22
22
  name: Mapped[str] = mapped_column(doc="The display name of the organization.")
23
23
 
24
+ # relationships
24
25
  users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")
25
26
  tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="organization", cascade="all, delete-orphan")
27
+ sources: Mapped[List["Source"]] = relationship("Source", back_populates="organization", cascade="all, delete-orphan")
26
28
  agents_tags: Mapped[List["AgentsTags"]] = relationship("AgentsTags", back_populates="organization", cascade="all, delete-orphan")
27
-
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")
letta/orm/source.py ADDED
@@ -0,0 +1,50 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ from sqlalchemy import JSON, TypeDecorator
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from letta.orm.mixins import OrganizationMixin
7
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
8
+ from letta.schemas.embedding_config import EmbeddingConfig
9
+ from letta.schemas.source import Source as PydanticSource
10
+
11
+ if TYPE_CHECKING:
12
+ from letta.orm.organization import Organization
13
+
14
+
15
+ class EmbeddingConfigColumn(TypeDecorator):
16
+ """Custom type for storing EmbeddingConfig as JSON"""
17
+
18
+ impl = JSON
19
+ cache_ok = True
20
+
21
+ def load_dialect_impl(self, dialect):
22
+ return dialect.type_descriptor(JSON())
23
+
24
+ def process_bind_param(self, value, dialect):
25
+ if value:
26
+ # return vars(value)
27
+ if isinstance(value, EmbeddingConfig):
28
+ return value.model_dump()
29
+ return value
30
+
31
+ def process_result_value(self, value, dialect):
32
+ if value:
33
+ return EmbeddingConfig(**value)
34
+ return value
35
+
36
+
37
+ class Source(SqlalchemyBase, OrganizationMixin):
38
+ """A source represents an embedded text passage"""
39
+
40
+ __tablename__ = "sources"
41
+ __pydantic_model__ = PydanticSource
42
+
43
+ name: Mapped[str] = mapped_column(doc="the name of the source, must be unique within the org", nullable=False)
44
+ description: Mapped[str] = mapped_column(nullable=True, doc="a human-readable description of the source")
45
+ embedding_config: Mapped[EmbeddingConfig] = mapped_column(EmbeddingConfigColumn, doc="Configuration settings for embedding.")
46
+ metadata_: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="metadata for the source.")
47
+
48
+ # relationships
49
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="sources")
50
+ # agents: Mapped[List["Agent"]] = relationship("Agent", secondary="sources_agents", back_populates="sources")
letta/providers.py CHANGED
@@ -462,7 +462,6 @@ class VLLMChatCompletionsProvider(Provider):
462
462
  response = openai_get_model_list(self.base_url, api_key=None)
463
463
 
464
464
  configs = []
465
- print(response)
466
465
  for model in response["data"]:
467
466
  configs.append(
468
467
  LLMConfig(
letta/schemas/source.py CHANGED
@@ -1,12 +1,10 @@
1
1
  from datetime import datetime
2
2
  from typing import Optional
3
3
 
4
- from fastapi import UploadFile
5
- from pydantic import BaseModel, Field
4
+ from pydantic import Field
6
5
 
7
6
  from letta.schemas.embedding_config import EmbeddingConfig
8
7
  from letta.schemas.letta_base import LettaBase
9
- from letta.utils import get_utc_time
10
8
 
11
9
 
12
10
  class BaseSource(LettaBase):
@@ -15,15 +13,6 @@ class BaseSource(LettaBase):
15
13
  """
16
14
 
17
15
  __id_prefix__ = "source"
18
- description: Optional[str] = Field(None, description="The description of the source.")
19
- embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the passage.")
20
- # NOTE: .metadata is a reserved attribute on SQLModel
21
- metadata_: Optional[dict] = Field(None, description="Metadata associated with the source.")
22
-
23
-
24
- class SourceCreate(BaseSource):
25
- name: str = Field(..., description="The name of the source.")
26
- description: Optional[str] = Field(None, description="The description of the source.")
27
16
 
28
17
 
29
18
  class Source(BaseSource):
@@ -34,7 +23,6 @@ class Source(BaseSource):
34
23
  id (str): The ID of the source
35
24
  name (str): The name of the source.
36
25
  embedding_config (EmbeddingConfig): The embedding configuration used by the source.
37
- created_at (datetime): The creation date of the source.
38
26
  user_id (str): The ID of the user that created the source.
39
27
  metadata_ (dict): Metadata associated with the source.
40
28
  description (str): The description of the source.
@@ -42,21 +30,39 @@ class Source(BaseSource):
42
30
 
43
31
  id: str = BaseSource.generate_id_field()
44
32
  name: str = Field(..., description="The name of the source.")
33
+ description: Optional[str] = Field(None, description="The description of the source.")
45
34
  embedding_config: EmbeddingConfig = Field(..., description="The embedding configuration used by the source.")
46
- created_at: datetime = Field(default_factory=get_utc_time, description="The creation date of the source.")
47
- user_id: str = Field(..., description="The ID of the user that created the source.")
35
+ organization_id: Optional[str] = Field(None, description="The ID of the organization that created the source.")
36
+ metadata_: Optional[dict] = Field(None, description="Metadata associated with the source.")
48
37
 
38
+ # metadata fields
39
+ created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
40
+ last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
41
+ created_at: Optional[datetime] = Field(None, description="The timestamp when the source was created.")
42
+ updated_at: Optional[datetime] = Field(None, description="The timestamp when the source was last updated.")
49
43
 
50
- class SourceUpdate(BaseSource):
51
- id: str = Field(..., description="The ID of the source.")
52
- name: Optional[str] = Field(None, description="The name of the source.")
53
44
 
45
+ class SourceCreate(BaseSource):
46
+ """
47
+ Schema for creating a new Source.
48
+ """
49
+
50
+ # required
51
+ name: str = Field(..., description="The name of the source.")
52
+ # TODO: @matt, make this required after shub makes the FE changes
53
+ embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the source.")
54
+
55
+ # optional
56
+ description: Optional[str] = Field(None, description="The description of the source.")
57
+ metadata_: Optional[dict] = Field(None, description="Metadata associated with the source.")
54
58
 
55
- class UploadFileToSourceRequest(BaseModel):
56
- file: UploadFile = Field(..., description="The file to upload.")
57
59
 
60
+ class SourceUpdate(BaseSource):
61
+ """
62
+ Schema for updating an existing Source.
63
+ """
58
64
 
59
- class UploadFileToSourceResponse(BaseModel):
60
- source: Source = Field(..., description="The source the file was uploaded to.")
61
- added_passages: int = Field(..., description="The number of passages added to the source.")
62
- added_documents: int = Field(..., description="The number of files added to the source.")
65
+ name: Optional[str] = Field(None, description="The name of the source.")
66
+ description: Optional[str] = Field(None, description="The description of the source.")
67
+ metadata_: Optional[dict] = Field(None, description="Metadata associated with the source.")
68
+ embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the source.")
@@ -6,6 +6,8 @@ from typing import Optional
6
6
 
7
7
  import uvicorn
8
8
  from fastapi import FastAPI
9
+ from fastapi.responses import JSONResponse
10
+ from starlette.middleware.base import BaseHTTPMiddleware
9
11
  from starlette.middleware.cors import CORSMiddleware
10
12
 
11
13
  from letta.__init__ import __version__
@@ -94,6 +96,27 @@ def generate_openapi_schema(app: FastAPI):
94
96
  Path(f"openapi_{name}.json").write_text(json.dumps(docs, indent=2))
95
97
 
96
98
 
99
+ # middleware that only allows requests to pass through if user provides a password thats randomly generated and stored in memory
100
+ def generate_password():
101
+ import secrets
102
+
103
+ return secrets.token_urlsafe(16)
104
+
105
+
106
+ random_password = generate_password()
107
+
108
+
109
+ class CheckPasswordMiddleware(BaseHTTPMiddleware):
110
+ async def dispatch(self, request, call_next):
111
+ if request.headers.get("X-BARE-PASSWORD") == f"password {random_password}":
112
+ return await call_next(request)
113
+
114
+ return JSONResponse(
115
+ content={"detail": "Unauthorized"},
116
+ status_code=401,
117
+ )
118
+
119
+
97
120
  def create_application() -> "FastAPI":
98
121
  """the application start routine"""
99
122
  # global server
@@ -113,6 +136,10 @@ def create_application() -> "FastAPI":
113
136
  settings.cors_origins.append("https://app.letta.com")
114
137
  print(f"▶ View using ADE at: https://app.letta.com/local-project/agents")
115
138
 
139
+ if "--secure" in sys.argv:
140
+ print(f"▶ Using secure mode with password: {random_password}")
141
+ app.add_middleware(CheckPasswordMiddleware)
142
+
116
143
  app.add_middleware(
117
144
  CORSMiddleware,
118
145
  allow_origins=settings.cors_origins,
@@ -36,7 +36,7 @@ def get_source(
36
36
  """
37
37
  actor = server.get_user_or_default(user_id=user_id)
38
38
 
39
- return server.get_source(source_id=source_id, user_id=actor.id)
39
+ return server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
40
40
 
41
41
 
42
42
  @router.get("/name/{source_name}", response_model=str, operation_id="get_source_id_by_name")
@@ -50,8 +50,8 @@ def get_source_id_by_name(
50
50
  """
51
51
  actor = server.get_user_or_default(user_id=user_id)
52
52
 
53
- source_id = server.get_source_id(source_name=source_name, user_id=actor.id)
54
- return source_id
53
+ source = server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
54
+ return source.id
55
55
 
56
56
 
57
57
  @router.get("/", response_model=List[Source], operation_id="list_sources")
@@ -64,12 +64,12 @@ def list_sources(
64
64
  """
65
65
  actor = server.get_user_or_default(user_id=user_id)
66
66
 
67
- return server.list_all_sources(user_id=actor.id)
67
+ return server.list_all_sources(actor=actor)
68
68
 
69
69
 
70
70
  @router.post("/", response_model=Source, operation_id="create_source")
71
71
  def create_source(
72
- source: SourceCreate,
72
+ source_create: SourceCreate,
73
73
  server: "SyncServer" = Depends(get_letta_server),
74
74
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
75
75
  ):
@@ -77,8 +77,9 @@ def create_source(
77
77
  Create a new data source.
78
78
  """
79
79
  actor = server.get_user_or_default(user_id=user_id)
80
+ source = Source(**source_create.model_dump())
80
81
 
81
- return server.create_source(request=source, user_id=actor.id)
82
+ return server.source_manager.create_source(source=source, actor=actor)
82
83
 
83
84
 
84
85
  @router.patch("/{source_id}", response_model=Source, operation_id="update_source")
@@ -92,10 +93,7 @@ def update_source(
92
93
  Update the name or documentation of an existing data source.
93
94
  """
94
95
  actor = server.get_user_or_default(user_id=user_id)
95
-
96
- assert source.id == source_id, "Source ID in path must match ID in request body"
97
-
98
- return server.update_source(request=source, user_id=actor.id)
96
+ return server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
99
97
 
100
98
 
101
99
  @router.delete("/{source_id}", response_model=None, operation_id="delete_source")
@@ -109,7 +107,7 @@ def delete_source(
109
107
  """
110
108
  actor = server.get_user_or_default(user_id=user_id)
111
109
 
112
- server.delete_source(source_id=source_id, user_id=actor.id)
110
+ server.delete_source(source_id=source_id, actor=actor)
113
111
 
114
112
 
115
113
  @router.post("/{source_id}/attach", response_model=Source, operation_id="attach_agent_to_source")
@@ -124,7 +122,7 @@ def attach_source_to_agent(
124
122
  """
125
123
  actor = server.get_user_or_default(user_id=user_id)
126
124
 
127
- source = server.ms.get_source(source_id=source_id, user_id=actor.id)
125
+ source = server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
128
126
  assert source is not None, f"Source with id={source_id} not found."
129
127
  source = server.attach_source_to_agent(source_id=source.id, agent_id=agent_id, user_id=actor.id)
130
128
  return source
@@ -158,7 +156,7 @@ def upload_file_to_source(
158
156
  """
159
157
  actor = server.get_user_or_default(user_id=user_id)
160
158
 
161
- source = server.ms.get_source(source_id=source_id, user_id=actor.id)
159
+ source = server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
162
160
  assert source is not None, f"Source with id={source_id} not found."
163
161
  bytes = file.file.read()
164
162
 
letta/server/server.py CHANGED
@@ -78,12 +78,13 @@ from letta.schemas.memory import (
78
78
  from letta.schemas.message import Message, MessageCreate, MessageRole, UpdateMessage
79
79
  from letta.schemas.organization import Organization
80
80
  from letta.schemas.passage import Passage
81
- from letta.schemas.source import Source, SourceCreate, SourceUpdate
81
+ from letta.schemas.source import Source
82
82
  from letta.schemas.tool import Tool, ToolCreate
83
83
  from letta.schemas.usage import LettaUsageStatistics
84
84
  from letta.schemas.user import User
85
85
  from letta.services.agents_tags_manager import AgentsTagsManager
86
86
  from letta.services.organization_manager import OrganizationManager
87
+ from letta.services.source_manager import SourceManager
87
88
  from letta.services.tool_manager import ToolManager
88
89
  from letta.services.user_manager import UserManager
89
90
  from letta.utils import create_random_username, json_dumps, json_loads
@@ -249,6 +250,7 @@ class SyncServer(Server):
249
250
  self.organization_manager = OrganizationManager()
250
251
  self.user_manager = UserManager()
251
252
  self.tool_manager = ToolManager()
253
+ self.source_manager = SourceManager()
252
254
  self.agents_tags_manager = AgentsTagsManager()
253
255
 
254
256
  # Make default user and org
@@ -1560,44 +1562,12 @@ class SyncServer(Server):
1560
1562
  self.ms.delete_api_key(api_key=api_key)
1561
1563
  return api_key_obj
1562
1564
 
1563
- def create_source(self, request: SourceCreate, user_id: str) -> Source: # TODO: add other fields
1564
- """Create a new data source"""
1565
- source = Source(
1566
- name=request.name,
1567
- user_id=user_id,
1568
- embedding_config=self.list_embedding_models()[0], # TODO: require providing this
1569
- )
1570
- self.ms.create_source(source)
1571
- assert self.ms.get_source(source_name=request.name, user_id=user_id) is not None, f"Failed to create source {request.name}"
1572
- return source
1573
-
1574
- def update_source(self, request: SourceUpdate, user_id: str) -> Source:
1575
- """Update an existing data source"""
1576
- if not request.id:
1577
- existing_source = self.ms.get_source(source_name=request.name, user_id=user_id)
1578
- else:
1579
- existing_source = self.ms.get_source(source_id=request.id)
1580
- if not existing_source:
1581
- raise ValueError("Source does not exist")
1582
-
1583
- # override updated fields
1584
- if request.name:
1585
- existing_source.name = request.name
1586
- if request.metadata_:
1587
- existing_source.metadata_ = request.metadata_
1588
- if request.description:
1589
- existing_source.description = request.description
1590
-
1591
- self.ms.update_source(existing_source)
1592
- return existing_source
1593
-
1594
- def delete_source(self, source_id: str, user_id: str):
1565
+ def delete_source(self, source_id: str, actor: User):
1595
1566
  """Delete a data source"""
1596
- source = self.ms.get_source(source_id=source_id, user_id=user_id)
1597
- self.ms.delete_source(source_id)
1567
+ self.source_manager.delete_source(source_id=source_id, actor=actor)
1598
1568
 
1599
1569
  # delete data from passage store
1600
- passage_store = StorageConnector.get_storage_connector(TableType.PASSAGES, self.config, user_id=user_id)
1570
+ passage_store = StorageConnector.get_storage_connector(TableType.PASSAGES, self.config, user_id=actor.id)
1601
1571
  passage_store.delete({"source_id": source_id})
1602
1572
 
1603
1573
  # TODO: delete data from agent passage stores (?)
@@ -1639,9 +1609,9 @@ class SyncServer(Server):
1639
1609
  # try:
1640
1610
  from letta.data_sources.connectors import DirectoryConnector
1641
1611
 
1642
- source = self.ms.get_source(source_id=source_id)
1612
+ source = self.source_manager.get_source_by_id(source_id=source_id)
1643
1613
  connector = DirectoryConnector(input_files=[file_path])
1644
- num_passages, num_documents = self.load_data(user_id=source.user_id, source_name=source.name, connector=connector)
1614
+ num_passages, num_documents = self.load_data(user_id=source.created_by_id, source_name=source.name, connector=connector)
1645
1615
  # except Exception as e:
1646
1616
  # # job failed with error
1647
1617
  # error = str(e)
@@ -1675,7 +1645,8 @@ class SyncServer(Server):
1675
1645
  # TODO: this should be implemented as a batch job or at least async, since it may take a long time
1676
1646
 
1677
1647
  # load data from a data source into the document store
1678
- source = self.ms.get_source(source_name=source_name, user_id=user_id)
1648
+ user = self.user_manager.get_user_by_id(user_id=user_id)
1649
+ source = self.source_manager.get_source_by_name(source_name=source_name, actor=user)
1679
1650
  if source is None:
1680
1651
  raise ValueError(f"Data source {source_name} does not exist for user {user_id}")
1681
1652
 
@@ -1696,9 +1667,13 @@ class SyncServer(Server):
1696
1667
  source_name: Optional[str] = None,
1697
1668
  ) -> Source:
1698
1669
  # attach a data source to an agent
1699
- data_source = self.ms.get_source(source_id=source_id, user_id=user_id, source_name=source_name)
1700
- if data_source is None:
1701
- raise ValueError(f"Data source id={source_id} name={source_name} does not exist for user_id {user_id}")
1670
+ user = self.user_manager.get_user_by_id(user_id=user_id)
1671
+ if source_id:
1672
+ data_source = self.source_manager.get_source_by_id(source_id=source_id, actor=user)
1673
+ elif source_name:
1674
+ data_source = self.source_manager.get_source_by_name(source_name=source_name, actor=user)
1675
+ else:
1676
+ raise ValueError(f"Need to provide at least source_id or source_name to find the source.")
1702
1677
 
1703
1678
  # get connection to data source storage
1704
1679
  source_connector = StorageConnector.get_storage_connector(TableType.PASSAGES, self.config, user_id=user_id)
@@ -1719,12 +1694,14 @@ class SyncServer(Server):
1719
1694
  source_id: Optional[str] = None,
1720
1695
  source_name: Optional[str] = None,
1721
1696
  ) -> Source:
1722
- if not source_id:
1723
- assert source_name is not None, "source_name must be provided if source_id is not"
1724
- source = self.ms.get_source(source_name=source_name, user_id=user_id)
1725
- source_id = source.id
1697
+ user = self.user_manager.get_user_by_id(user_id=user_id)
1698
+ if source_id:
1699
+ source = self.source_manager.get_source_by_id(source_id=source_id, actor=user)
1700
+ elif source_name:
1701
+ source = self.source_manager.get_source_by_name(source_name=source_name, actor=user)
1726
1702
  else:
1727
- source = self.ms.get_source(source_id=source_id)
1703
+ raise ValueError(f"Need to provide at least source_id or source_name to find the source.")
1704
+ source_id = source.id
1728
1705
 
1729
1706
  # delete all Passage objects with source_id==source_id from agent's archival memory
1730
1707
  agent = self._get_or_load_agent(agent_id=agent_id)
@@ -1739,7 +1716,9 @@ class SyncServer(Server):
1739
1716
 
1740
1717
  def list_attached_sources(self, agent_id: str) -> List[Source]:
1741
1718
  # list all attached sources to an agent
1742
- return self.ms.list_attached_sources(agent_id)
1719
+ source_ids = self.ms.list_attached_source_ids(agent_id)
1720
+
1721
+ return [self.source_manager.get_source_by_id(source_id=id) for id in source_ids]
1743
1722
 
1744
1723
  def list_files_from_source(self, source_id: str, limit: int = 1000, cursor: Optional[str] = None) -> List[FileMetadata]:
1745
1724
  # list all attached sources to an agent
@@ -1749,17 +1728,17 @@ class SyncServer(Server):
1749
1728
  warnings.warn("list_data_source_passages is not yet implemented, returning empty list.", category=UserWarning)
1750
1729
  return []
1751
1730
 
1752
- def list_all_sources(self, user_id: str) -> List[Source]:
1731
+ def list_all_sources(self, actor: User) -> List[Source]:
1753
1732
  """List all sources (w/ extra metadata) belonging to a user"""
1754
1733
 
1755
- sources = self.ms.list_sources(user_id=user_id)
1734
+ sources = self.source_manager.list_sources(actor=actor)
1756
1735
 
1757
1736
  # Add extra metadata to the sources
1758
1737
  sources_with_metadata = []
1759
1738
  for source in sources:
1760
1739
 
1761
1740
  # count number of passages
1762
- passage_conn = StorageConnector.get_storage_connector(TableType.PASSAGES, self.config, user_id=user_id)
1741
+ passage_conn = StorageConnector.get_storage_connector(TableType.PASSAGES, self.config, user_id=actor.id)
1763
1742
  num_passages = passage_conn.size({"source_id": source.id})
1764
1743
 
1765
1744
  # TODO: add when files table implemented
@@ -1773,7 +1752,7 @@ class SyncServer(Server):
1773
1752
  attached_agents = [
1774
1753
  {
1775
1754
  "id": str(a_id),
1776
- "name": self.ms.get_agent(user_id=user_id, agent_id=a_id).name,
1755
+ "name": self.ms.get_agent(user_id=actor.id, agent_id=a_id).name,
1777
1756
  }
1778
1757
  for a_id in agent_ids
1779
1758
  ]
@@ -27,18 +27,26 @@ class OrganizationManager:
27
27
  return self.get_organization_by_id(self.DEFAULT_ORG_ID)
28
28
 
29
29
  @enforce_types
30
- def get_organization_by_id(self, org_id: str) -> PydanticOrganization:
30
+ def get_organization_by_id(self, org_id: str) -> Optional[PydanticOrganization]:
31
31
  """Fetch an organization by ID."""
32
32
  with self.session_maker() as session:
33
33
  try:
34
34
  organization = OrganizationModel.read(db_session=session, identifier=org_id)
35
35
  return organization.to_pydantic()
36
36
  except NoResultFound:
37
- raise ValueError(f"Organization with id {org_id} not found.")
37
+ return None
38
38
 
39
39
  @enforce_types
40
40
  def create_organization(self, pydantic_org: PydanticOrganization) -> PydanticOrganization:
41
41
  """Create a new organization. If a name is provided, it is used, otherwise, a random one is generated."""
42
+ org = self.get_organization_by_id(pydantic_org.id)
43
+ if org:
44
+ return org
45
+ else:
46
+ return self._create_organization(pydantic_org=pydantic_org)
47
+
48
+ @enforce_types
49
+ def _create_organization(self, pydantic_org: PydanticOrganization) -> PydanticOrganization:
42
50
  with self.session_maker() as session:
43
51
  org = OrganizationModel(**pydantic_org.model_dump())
44
52
  org.create(session)
@@ -47,16 +55,7 @@ class OrganizationManager:
47
55
  @enforce_types
48
56
  def create_default_organization(self) -> PydanticOrganization:
49
57
  """Create the default organization."""
50
- with self.session_maker() as session:
51
- # Try to get it first
52
- try:
53
- org = OrganizationModel.read(db_session=session, identifier=self.DEFAULT_ORG_ID)
54
- # If it doesn't exist, make it
55
- except NoResultFound:
56
- org = OrganizationModel(name=self.DEFAULT_ORG_NAME, id=self.DEFAULT_ORG_ID)
57
- org.create(session)
58
-
59
- return org.to_pydantic()
58
+ return self.create_organization(PydanticOrganization(name=self.DEFAULT_ORG_NAME, id=self.DEFAULT_ORG_ID))
60
59
 
61
60
  @enforce_types
62
61
  def update_organization_name_using_id(self, org_id: str, name: Optional[str] = None) -> PydanticOrganization:
@@ -73,7 +72,7 @@ class OrganizationManager:
73
72
  """Delete an organization by marking it as deleted."""
74
73
  with self.session_maker() as session:
75
74
  organization = OrganizationModel.read(db_session=session, identifier=org_id)
76
- organization.delete(session)
75
+ organization.hard_delete(session)
77
76
 
78
77
  @enforce_types
79
78
  def list_organizations(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticOrganization]:
@@ -0,0 +1,100 @@
1
+ from typing import List, Optional
2
+
3
+ from letta.orm.errors import NoResultFound
4
+ from letta.orm.source import Source as SourceModel
5
+ from letta.schemas.source import Source as PydanticSource
6
+ from letta.schemas.source import SourceUpdate
7
+ from letta.schemas.user import User as PydanticUser
8
+ from letta.utils import enforce_types, printd
9
+
10
+
11
+ class SourceManager:
12
+ """Manager class to handle business logic related to Sources."""
13
+
14
+ def __init__(self):
15
+ from letta.server.server import db_context
16
+
17
+ self.session_maker = db_context
18
+
19
+ @enforce_types
20
+ def create_source(self, source: PydanticSource, actor: PydanticUser) -> PydanticSource:
21
+ """Create a new source based on the PydanticSource schema."""
22
+ # Try getting the source first by id
23
+ db_source = self.get_source_by_id(source.id, actor=actor)
24
+ if db_source:
25
+ return db_source
26
+ else:
27
+ with self.session_maker() as session:
28
+ # Provide default embedding config if not given
29
+ source.organization_id = actor.organization_id
30
+ source = SourceModel(**source.model_dump(exclude_none=True))
31
+ source.create(session, actor=actor)
32
+ return source.to_pydantic()
33
+
34
+ @enforce_types
35
+ def update_source(self, source_id: str, source_update: SourceUpdate, actor: PydanticUser) -> PydanticSource:
36
+ """Update a source by its ID with the given SourceUpdate object."""
37
+ with self.session_maker() as session:
38
+ source = SourceModel.read(db_session=session, identifier=source_id, actor=actor)
39
+
40
+ # get update dictionary
41
+ update_data = source_update.model_dump(exclude_unset=True, exclude_none=True)
42
+ # Remove redundant update fields
43
+ update_data = {key: value for key, value in update_data.items() if getattr(source, key) != value}
44
+
45
+ if update_data:
46
+ for key, value in update_data.items():
47
+ setattr(source, key, value)
48
+ source.update(db_session=session, actor=actor)
49
+ else:
50
+ printd(
51
+ f"`update_source` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={source.name}, but found existing source with nothing to update."
52
+ )
53
+
54
+ return source.to_pydantic()
55
+
56
+ @enforce_types
57
+ def delete_source(self, source_id: str, actor: PydanticUser) -> PydanticSource:
58
+ """Delete a source by its ID."""
59
+ with self.session_maker() as session:
60
+ source = SourceModel.read(db_session=session, identifier=source_id)
61
+ source.delete(db_session=session, actor=actor)
62
+ return source.to_pydantic()
63
+
64
+ @enforce_types
65
+ def list_sources(self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticSource]:
66
+ """List all sources with optional pagination."""
67
+ with self.session_maker() as session:
68
+ sources = SourceModel.list(
69
+ db_session=session,
70
+ cursor=cursor,
71
+ limit=limit,
72
+ organization_id=actor.organization_id,
73
+ )
74
+ return [source.to_pydantic() for source in sources]
75
+
76
+ # TODO: We make actor optional for now, but should most likely be enforced due to security reasons
77
+ @enforce_types
78
+ def get_source_by_id(self, source_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSource]:
79
+ """Retrieve a source by its ID."""
80
+ with self.session_maker() as session:
81
+ try:
82
+ source = SourceModel.read(db_session=session, identifier=source_id, actor=actor)
83
+ return source.to_pydantic()
84
+ except NoResultFound:
85
+ return None
86
+
87
+ @enforce_types
88
+ def get_source_by_name(self, source_name: str, actor: PydanticUser) -> Optional[PydanticSource]:
89
+ """Retrieve a source by its name."""
90
+ with self.session_maker() as session:
91
+ sources = SourceModel.list(
92
+ db_session=session,
93
+ name=source_name,
94
+ organization_id=actor.organization_id,
95
+ limit=1,
96
+ )
97
+ if not sources:
98
+ return None
99
+ else:
100
+ return sources[0].to_pydantic()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.5.2.dev20241112104101
3
+ Version: 0.5.2.dev20241113104112
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -1,6 +1,6 @@
1
1
  letta/__init__.py,sha256=IMLtpH5HlbVUa1mmPpSyBpTZqVz1rsS7lbuqT7viBQ0,1014
2
2
  letta/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
- letta/agent.py,sha256=5eP4H5DBJEUQT3QlZIAIdB3Dtmh5XnW1I3TJievv56Q,76744
3
+ letta/agent.py,sha256=mQxkYsG80P_KCzXN6EDmwi6tiXebabHpcYsgs7ugWP0,76952
4
4
  letta/agent_store/chroma.py,sha256=upR5zGnGs6I6btulEYbiZdGG87BgKjxUJOQZ4Y-RQ_M,12492
5
5
  letta/agent_store/db.py,sha256=iBdP9IxvuenKRF7SVRhFlC8yvRwNn2xxOfBFvadBcNA,23383
6
6
  letta/agent_store/lancedb.py,sha256=i63d4VZwj9UIOTNs5f0JZ_r5yZD-jKWz4FAH4RMpXOE,5104
@@ -9,17 +9,17 @@ letta/agent_store/qdrant.py,sha256=6_33V-FEDpT9LG5zmr6-3y9slw1YFLswxpahiyMkvHA,7
9
9
  letta/agent_store/storage.py,sha256=4gKvMRYBGm9cwyaDOzljxDKgqr4MxGXcC4yGhAdKcAA,6693
10
10
  letta/benchmark/benchmark.py,sha256=ebvnwfp3yezaXOQyGXkYCDYpsmre-b9hvNtnyx4xkG0,3701
11
11
  letta/benchmark/constants.py,sha256=aXc5gdpMGJT327VuxsT5FngbCK2J41PQYeICBO7g_RE,536
12
- letta/cli/cli.py,sha256=D6fnMecQ2BuVBlpAWt1F5QKDN-W3Sx30Ynuq-sdi-Hk,16811
12
+ letta/cli/cli.py,sha256=oREx2gmpFvw7CdmXjhzoj_4iypM27WAK5vePESIgcHo,16898
13
13
  letta/cli/cli_config.py,sha256=D-CpSZI1cDvdSQr3-zhGuDrJnZo1Ko7bi_wuxcBxxqo,8555
14
14
  letta/cli/cli_load.py,sha256=x4L8s15GwIW13xrhKYFWHo_y-IVGtoPDHWWKcHDRP10,4587
15
15
  letta/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- letta/client/client.py,sha256=rInVYoGTibIUmu7ProGeHfTbVF_zu70OfQxY8zX5l2w,97430
16
+ letta/client/client.py,sha256=nnd0n4NWKDpBnt41LgcQYxfNuOGYQ2CuQ4SpHbKArZo,97883
17
17
  letta/client/streaming.py,sha256=Hh5pjlyrdCuO2V75ZCxSSOCPd3BmHdKFGaIUJC6fBp0,4775
18
18
  letta/client/utils.py,sha256=OJlAKWrldc4I6M1WpcTWNtPJ4wfxlzlZqWLfCozkFtI,2872
19
19
  letta/config.py,sha256=eK-ip06ELHNYriInkgfidDvJxQ2tD1u49I-VLXB87nE,18929
20
20
  letta/constants.py,sha256=c8pEfIhtpqFGunyzGObnfEeRJNkunfmq9Pfiau8YYfA,6544
21
21
  letta/credentials.py,sha256=D9mlcPsdDWlIIXQQD8wSPE9M_QvsRrb0p3LB5i9OF5Q,5806
22
- letta/data_sources/connectors.py,sha256=qO81ASB6V-vDPthfHYtZiyqcQDQPTT0NuD8hVwC6xI0,9907
22
+ letta/data_sources/connectors.py,sha256=gEZCpNwGPR5MCnAofmFmQMuLbiJ9I44HJ2rSNRGMqP4,9918
23
23
  letta/data_sources/connectors_helper.py,sha256=2TQjCt74fCgT5sw1AP8PalDEk06jPBbhrPG4HVr-WLs,3371
24
24
  letta/embeddings.py,sha256=qPt8kB-wmuRIg1py7DHnQGJpw3DmQHJ505FJvc0K6Yk,8873
25
25
  letta/errors.py,sha256=cDOo4cSYL-LA0w0b0GdsxXd5k2I1LLOY8nhtXk9YqYs,2875
@@ -40,11 +40,11 @@ letta/llm_api/anthropic.py,sha256=GZFMAGgertU7GrGK_ahz051Ce0MHXqPzh8f7AlsPslg,12
40
40
  letta/llm_api/azure_openai.py,sha256=Y1HKPog1XzM_f7ujUK_Gv2zQkoy5pU-1bKiUnvSxSrs,6297
41
41
  letta/llm_api/azure_openai_constants.py,sha256=oXtKrgBFHf744gyt5l1thILXgyi8NDNUrKEa2GGGpjw,278
42
42
  letta/llm_api/cohere.py,sha256=vDRd-SUGp1t_JUIdwC3RkIhwMl0OY7n-tAU9uPORYkY,14826
43
- letta/llm_api/google_ai.py,sha256=3xZ074nSOCC22c15yerA5ngWzh0ex4wxeI-6faNbHPE,17708
43
+ letta/llm_api/google_ai.py,sha256=xKz9JDZs3m6yzSfcgCAAUD_rjI20BBIINoiSvlcnOw0,17621
44
44
  letta/llm_api/helpers.py,sha256=KqkdjZWYghx4OPwLcHEC6ruc_z9DScbysw3VH4x9A0Q,9887
45
45
  letta/llm_api/llm_api_tools.py,sha256=KFG2miI7KrDOIcOSgm2jwBIb3qvzYt2O_5UNjTbTsm8,14786
46
46
  letta/llm_api/mistral.py,sha256=fHdfD9ug-rQIk2qn8tRKay1U6w9maF11ryhKi91FfXM,1593
47
- letta/llm_api/openai.py,sha256=o2I7yNyBMsW33MwQJMo2EWYTC5dLDdxfGmg-mQle6nA,23844
47
+ letta/llm_api/openai.py,sha256=p1tKbfCfhWOLnx2u-Vt67rq12uVUbo_zyslHHlmuTyU,23845
48
48
  letta/local_llm/README.md,sha256=hFJyw5B0TU2jrh9nb0zGZMgdH-Ei1dSRfhvPQG_NSoU,168
49
49
  letta/local_llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
50
  letta/local_llm/chat_completion_proxy.py,sha256=SiohxsjGTku4vOryOZx7I0t0xoO_sUuhXgoe62fKq3c,12995
@@ -84,19 +84,20 @@ letta/local_llm/webui/legacy_settings.py,sha256=BLmd3TSx5StnY3ibjwaxYATPt_Lvq-o1
84
84
  letta/local_llm/webui/settings.py,sha256=gmLHfiOl1u4JmlAZU2d2O8YKF9lafdakyjwR_ftVPh8,552
85
85
  letta/log.py,sha256=Oy5b71AXfrnQShxI_4ULo5U3kmZJG01bXbP_64Nr4Fk,2105
86
86
  letta/main.py,sha256=h-qPQn_Ok5wf2cf54RFPfe8yp6sCmE-Kp9mBk_HZf7o,18797
87
- letta/memory.py,sha256=6q1x3-PY-PeXzAt6hvP-UF1ajvroPZ7XW-5nLy-JhMo,17657
88
- letta/metadata.py,sha256=XI9nL2JwFAvkpTZHq8EGoJnwhq9fZZen8gwkjFJxz4E,29408
87
+ letta/memory.py,sha256=YupXOvzVJXH59RW4XWBrd7qMNEYaMbtWXCheKeWZwpU,17873
88
+ letta/metadata.py,sha256=2bfM0cJk78Aw3j1aAbxld1mHwwhd8vrnFHQJj1Ts3LI,25750
89
89
  letta/o1_agent.py,sha256=LqATgTpkc02-nCH_F87EOvgxLjdjT9F07kdzj3zSdQg,3118
90
90
  letta/openai_backcompat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
91
91
  letta/openai_backcompat/openai_object.py,sha256=Y1ZS1sATP60qxJiOsjOP3NbwSzuzvkNAvb3DeuhM5Uk,13490
92
92
  letta/orm/__all__.py,sha256=2gh2MZTkA3Hw67VWVKK3JIStJOqTeLdpCvYSVYNeEDA,692
93
- letta/orm/__init__.py,sha256=J2GZpfXQunxU0ChavjkhoaSruluRFrLYknXD2m0BP_g,144
93
+ letta/orm/__init__.py,sha256=mR6vCTKJk4QzmqIFQozBoGo95RPEy3og5a-jQNKOpzk,180
94
94
  letta/orm/agents_tags.py,sha256=Qa7Yt9imL8xbGP57fflccAMy7Z32CQiU_7eZKSSPngc,1119
95
95
  letta/orm/base.py,sha256=K_LpNUURbsj44ycHbzvNXG_n8pBOjf1YvDaikIPDpQA,2716
96
96
  letta/orm/enums.py,sha256=KfHcFt_fR6GUmSlmfsa-TetvmuRxGESNve8MStRYW64,145
97
97
  letta/orm/errors.py,sha256=somsGtotFlb3SDM6tKdZ5TDGwEEP3ppx47ICAvNMnkg,225
98
98
  letta/orm/mixins.py,sha256=fW4oa1cUFbgVE46KSQlW_hwzsZSqEBSSV-U3xJC6fyw,749
99
- letta/orm/organization.py,sha256=ChQ3KuWfZLTnveI6Z6pvCV_b2gjUf1m7Pn17wXPheaI,1695
99
+ letta/orm/organization.py,sha256=_2HRrc1jCRNcZj-G5V70Xw_eEy21bBW7_f4c_6g_668,1712
100
+ letta/orm/source.py,sha256=Vr6WkD26BbVeZMHmcJFY6h2upChDW5OFNZEnlGvl9Q4,1895
100
101
  letta/orm/sqlalchemy_base.py,sha256=QZ_b2jxNjXSvK-bJGxEHQiZqRn8tKNSuJRDorYCDCvE,7369
101
102
  letta/orm/tool.py,sha256=7FIeldPJTEOLA5ygasTOVXqUcm2aouYYARUOJXdaC4Y,2151
102
103
  letta/orm/user.py,sha256=-KNdpfnKRVJtSw0NrGRrVoSBBv7ASDchVempKtUuk-A,1229
@@ -123,7 +124,7 @@ letta/prompts/system/memgpt_gpt35_extralong.txt,sha256=FheNhYoIzNz6qnJKhVquZVSMj
123
124
  letta/prompts/system/memgpt_intuitive_knowledge.txt,sha256=sA7c3urYqREVnSBI81nTGImXAekqC0Fxc7RojFqud1g,2966
124
125
  letta/prompts/system/memgpt_modified_chat.txt,sha256=HOaPVurEftD8KsuwsclDgE2afIfklMjxhuSO96q1-6I,4656
125
126
  letta/prompts/system/memgpt_modified_o1.txt,sha256=AxxYVjYLZwpZ6yfifh1SuPtwlJGWTcTVzw53QbkN-Ao,5492
126
- letta/providers.py,sha256=nWvTvVsZH1EE2aHAwihJvCIDJpgfeWAOUE_YK1x5Zj4,19635
127
+ letta/providers.py,sha256=miNxSXDMKDZqYjrYtE2N9ecw9ltccDXSJBlJqxOhEws,19611
127
128
  letta/pytest.ini,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
128
129
  letta/schemas/agent.py,sha256=f0khTBWIRGZva4_C15Nm_tkmn1cwaVQlWa7_7laRbEE,7866
129
130
  letta/schemas/agents_tags.py,sha256=9DGr8fN2DHYdWvZ_qcXmrKI0w7YKCGz2lfEcrX2KAkI,1130
@@ -148,7 +149,7 @@ letta/schemas/openai/embedding_response.py,sha256=WKIZpXab1Av7v6sxKG8feW3ZtpQUNo
148
149
  letta/schemas/openai/openai.py,sha256=Hilo5BiLAGabzxCwnwfzK5QrWqwYD8epaEKFa4Pwndk,7970
149
150
  letta/schemas/organization.py,sha256=d2oN3IK2HeruEHKXwIzCbJ3Fxdi_BEe9JZ8J9aDbHwQ,698
150
151
  letta/schemas/passage.py,sha256=eYQMxD_XjHAi72jmqcGBU4wM4VZtSU0XK8uhQxxN3Ug,3563
151
- letta/schemas/source.py,sha256=hB4Ai6Nj8dFdbxv5_Qaf4uN_cmdGmnzgc-4QnHXcV3o,2562
152
+ letta/schemas/source.py,sha256=B1VbaDJV-EGPv1nQXwCx_RAzeAJd50UqP_1m1cIRT8c,2854
152
153
  letta/schemas/tool.py,sha256=HCUW4RXUS9RtvfpTPeNhauFN3s23BojSOceaIxv9bKI,9596
153
154
  letta/schemas/tool_rule.py,sha256=zv4jE0b8LW78idP4UbJARnrZcnmaqjGNUk_YV99Y0c0,884
154
155
  letta/schemas/usage.py,sha256=lvn1ooHwLEdv6gwQpw5PBUbcwn_gwdT6HA-fCiix6sY,817
@@ -157,7 +158,7 @@ letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
158
  letta/server/constants.py,sha256=yAdGbLkzlOU_dLTx0lKDmAnj0ZgRXCEaIcPJWO69eaE,92
158
159
  letta/server/generate_openapi_schema.sh,sha256=0OtBhkC1g6CobVmNEd_m2B6sTdppjbJLXaM95icejvE,371
159
160
  letta/server/rest_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
160
- letta/server/rest_api/app.py,sha256=YwBC6ox8CY9XH9joiX8J--9zGYXSaGGH2OR7EeozbTU,6697
161
+ letta/server/rest_api/app.py,sha256=QbCgYeyzA86iefwomROBAaxwvSWYQT10fhHcNZsyjH4,7555
161
162
  letta/server/rest_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
162
163
  letta/server/rest_api/auth/index.py,sha256=fQBGyVylGSRfEMLQ17cZzrHd5Y1xiVylvPqH5Rl-lXQ,1378
163
164
  letta/server/rest_api/auth_token.py,sha256=725EFEIiNj4dh70hrSd94UysmFD8vcJLrTRfNHkzxDo,774
@@ -177,12 +178,12 @@ letta/server/rest_api/routers/v1/health.py,sha256=pKCuVESlVOhGIb4VC4K-H82eZqfghm
177
178
  letta/server/rest_api/routers/v1/jobs.py,sha256=a-j0v-5A0un0pVCOHpfeWnzpOWkVDQO6ti42k_qAlZY,2272
178
179
  letta/server/rest_api/routers/v1/llms.py,sha256=TcyvSx6MEM3je5F4DysL7ligmssL_pFlJaaO4uL95VY,877
179
180
  letta/server/rest_api/routers/v1/organizations.py,sha256=tyqVzXTpMtk3sKxI3Iz4aS6RhbGEbXDzFBB_CpW18v4,2080
180
- letta/server/rest_api/routers/v1/sources.py,sha256=eY_pk9jRL2Y9yIZdsTjH6EuKsfH1neaTU15MKNL0dvw,8749
181
+ letta/server/rest_api/routers/v1/sources.py,sha256=u6THgcSbSx2r0Sszr0v_6NV1U9AeMUDzZj9n3_kIXr8,8809
181
182
  letta/server/rest_api/routers/v1/tools.py,sha256=Bkb9oKswOycj5S3fBeim7LpDrZf37SybGwV6fyi3BFs,4296
182
183
  letta/server/rest_api/routers/v1/users.py,sha256=M1wEr2IyHzuRwINYxLXTkrbAH3osLe_cWjzrWrzR1aw,3729
183
184
  letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
184
185
  letta/server/rest_api/utils.py,sha256=GdHYCzXtbM5VCAYDPR0z5gnNZpRhwPld2BGZV7xT6cU,2924
185
- letta/server/server.py,sha256=fOhR-Do3_56R6x5BTV9U1ZbgedSd2oR6Gu76cwaj6cg,80783
186
+ letta/server/server.py,sha256=Bk-FPS0aODYFckEAgr5aPWVVizbf2gfcESnRK0aWdfQ,80038
186
187
  letta/server/startup.sh,sha256=wTOQOJJZw_Iec57WIu0UW0AVflk0ZMWYZWg8D3T_gSQ,698
187
188
  letta/server/static_files/assets/index-3ab03d5b.css,sha256=OrA9W4iKJ5h2Wlr7GwdAT4wow0CM8hVit1yOxEL49Qw,54295
188
189
  letta/server/static_files/assets/index-9fa459a2.js,sha256=j2oMcDJO9dWJaH5e-tsflbVpWK20gLWpZKJk4-Kuy6A,1815592
@@ -197,7 +198,8 @@ letta/server/ws_api/protocol.py,sha256=M_-gM5iuDBwa1cuN2IGNCG5GxMJwU2d3XW93XALv9
197
198
  letta/server/ws_api/server.py,sha256=C2Kv48PCwl46DQFb0ZP30s86KJLQ6dZk2AhWQEZn9pY,6004
198
199
  letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
199
200
  letta/services/agents_tags_manager.py,sha256=zNqeXDpaf4dQ77jrRHiQfITdk4FawBzcND-9tWrj8gw,3127
200
- letta/services/organization_manager.py,sha256=2RMmA8TRE9OFkomnT2NptEuFL4Y1lU3H2w1YjacI-o8,3613
201
+ letta/services/organization_manager.py,sha256=OfE2_NMmhqXURX4sg7hCOiFQVQpV5ZiPu7J3sboCSYc,3555
202
+ letta/services/source_manager.py,sha256=-02XYBsWa3Dt64Bq-anZ-US_b0XFlqrRyy3nNzlgoBs,4332
201
203
  letta/services/tool_manager.py,sha256=z3nnUDQWuqB5RYk_y78EvIH6SMx-KJy7qeHqclZHonw,7897
202
204
  letta/services/user_manager.py,sha256=UJa0hqCjz0yXtvrCR8OVBqlSR5lC_Ejn-uG__58zLds,4398
203
205
  letta/settings.py,sha256=yiYNmnYKj_BdTm0cBEIvQKYGU-lCmFntqsyVfRUy3_k,3411
@@ -205,8 +207,8 @@ letta/streaming_interface.py,sha256=_FPUWy58j50evHcpXyd7zB1wWqeCc71NCFeWh_TBvnw,
205
207
  letta/streaming_utils.py,sha256=329fsvj1ZN0r0LpQtmMPZ2vSxkDBIUUwvGHZFkjm2I8,11745
206
208
  letta/system.py,sha256=buKYPqG5n2x41hVmWpu6JUpyd7vTWED9Km2_M7dLrvk,6960
207
209
  letta/utils.py,sha256=SXLEYhyp3gHyIjrxNIKNZZ5ittKo3KOj6zxgC_Trex0,31012
208
- letta_nightly-0.5.2.dev20241112104101.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
209
- letta_nightly-0.5.2.dev20241112104101.dist-info/METADATA,sha256=iPQkItVGNBgehma7soFHdycDK4oqjHETvfw2Ul-ror8,11024
210
- letta_nightly-0.5.2.dev20241112104101.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
211
- letta_nightly-0.5.2.dev20241112104101.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
212
- letta_nightly-0.5.2.dev20241112104101.dist-info/RECORD,,
210
+ letta_nightly-0.5.2.dev20241113104112.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
211
+ letta_nightly-0.5.2.dev20241113104112.dist-info/METADATA,sha256=R-hEIbQCiq8wTmE3Qayy_unZscQ1_6jmhoIzwIGZx_g,11024
212
+ letta_nightly-0.5.2.dev20241113104112.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
213
+ letta_nightly-0.5.2.dev20241113104112.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
214
+ letta_nightly-0.5.2.dev20241113104112.dist-info/RECORD,,