letta-nightly 0.6.0.dev20241204213946__py3-none-any.whl → 0.6.0.dev20241205104308__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/client/client.py CHANGED
@@ -2859,8 +2859,12 @@ class LocalClient(AbstractClient):
2859
2859
  Returns:
2860
2860
  job (Job): Data loading job including job status and metadata
2861
2861
  """
2862
- metadata_ = {"type": "embedding", "filename": filename, "source_id": source_id}
2863
- job = self.server.create_job(user_id=self.user_id, metadata=metadata_)
2862
+ job = Job(
2863
+ user_id=self.user_id,
2864
+ status=JobStatus.created,
2865
+ metadata_={"type": "embedding", "filename": filename, "source_id": source_id},
2866
+ )
2867
+ job = self.server.job_manager.create_job(pydantic_job=job, actor=self.user)
2864
2868
 
2865
2869
  # TODO: implement blocking vs. non-blocking
2866
2870
  self.server.load_file_to_source(source_id=source_id, file_path=filename, job_id=job.id)
@@ -2870,16 +2874,16 @@ class LocalClient(AbstractClient):
2870
2874
  self.server.source_manager.delete_file(file_id, actor=self.user)
2871
2875
 
2872
2876
  def get_job(self, job_id: str):
2873
- return self.server.get_job(job_id=job_id)
2877
+ return self.server.job_manager.get_job_by_id(job_id=job_id, actor=self.user)
2874
2878
 
2875
2879
  def delete_job(self, job_id: str):
2876
- return self.server.delete_job(job_id)
2880
+ return self.server.job_manager.delete_job(job_id=job_id, actor=self.user)
2877
2881
 
2878
2882
  def list_jobs(self):
2879
- return self.server.list_jobs(user_id=self.user_id)
2883
+ return self.server.job_manager.list_jobs(actor=self.user)
2880
2884
 
2881
2885
  def list_active_jobs(self):
2882
- return self.server.list_active_jobs(user_id=self.user_id)
2886
+ return self.server.job_manager.list_jobs(actor=self.user, statuses=[JobStatus.created, JobStatus.running])
2883
2887
 
2884
2888
  def create_source(self, name: str, embedding_config: Optional[EmbeddingConfig] = None) -> Source:
2885
2889
  """
letta/metadata.py CHANGED
@@ -12,15 +12,14 @@ from letta.orm.base import Base
12
12
  from letta.schemas.agent import PersistedAgentState
13
13
  from letta.schemas.api_key import APIKey
14
14
  from letta.schemas.embedding_config import EmbeddingConfig
15
- from letta.schemas.enums import JobStatus, ToolRuleType
16
- from letta.schemas.job import Job
15
+ from letta.schemas.enums import ToolRuleType
17
16
  from letta.schemas.llm_config import LLMConfig
18
17
  from letta.schemas.openai.chat_completions import ToolCall, ToolCallFunction
19
18
  from letta.schemas.tool_rule import ChildToolRule, InitToolRule, TerminalToolRule
20
19
  from letta.schemas.user import User
21
20
  from letta.services.per_agent_lock_manager import PerAgentLockManager
22
21
  from letta.settings import settings
23
- from letta.utils import enforce_types, get_utc_time, printd
22
+ from letta.utils import enforce_types, printd
24
23
 
25
24
 
26
25
  class LLMConfigColumn(TypeDecorator):
@@ -258,31 +257,6 @@ class AgentSourceMappingModel(Base):
258
257
  return f"<AgentSourceMapping(user_id='{self.user_id}', agent_id='{self.agent_id}', source_id='{self.source_id}')>"
259
258
 
260
259
 
261
- class JobModel(Base):
262
- __tablename__ = "jobs"
263
- __table_args__ = {"extend_existing": True}
264
-
265
- id = Column(String, primary_key=True)
266
- user_id = Column(String)
267
- status = Column(String, default=JobStatus.pending)
268
- created_at = Column(DateTime(timezone=True), server_default=func.now())
269
- completed_at = Column(DateTime(timezone=True), onupdate=func.now())
270
- metadata_ = Column(JSON)
271
-
272
- def __repr__(self) -> str:
273
- return f"<Job(id='{self.id}', status='{self.status}')>"
274
-
275
- def to_record(self):
276
- return Job(
277
- id=self.id,
278
- user_id=self.user_id,
279
- status=self.status,
280
- created_at=self.created_at,
281
- completed_at=self.completed_at,
282
- metadata_=self.metadata_,
283
- )
284
-
285
-
286
260
  class MetadataStore:
287
261
  uri: Optional[str] = None
288
262
 
@@ -455,40 +429,3 @@ class MetadataStore:
455
429
  AgentSourceMappingModel.agent_id == agent_id, AgentSourceMappingModel.source_id == source_id
456
430
  ).delete()
457
431
  session.commit()
458
-
459
- @enforce_types
460
- def create_job(self, job: Job):
461
- with self.session_maker() as session:
462
- session.add(JobModel(**vars(job)))
463
- session.commit()
464
-
465
- def delete_job(self, job_id: str):
466
- with self.session_maker() as session:
467
- session.query(JobModel).filter(JobModel.id == job_id).delete()
468
- session.commit()
469
-
470
- def get_job(self, job_id: str) -> Optional[Job]:
471
- with self.session_maker() as session:
472
- results = session.query(JobModel).filter(JobModel.id == job_id).all()
473
- if len(results) == 0:
474
- return None
475
- assert len(results) == 1, f"Expected 1 result, got {len(results)}"
476
- return results[0].to_record()
477
-
478
- def list_jobs(self, user_id: str) -> List[Job]:
479
- with self.session_maker() as session:
480
- results = session.query(JobModel).filter(JobModel.user_id == user_id).all()
481
- return [r.to_record() for r in results]
482
-
483
- def update_job(self, job: Job) -> Job:
484
- with self.session_maker() as session:
485
- session.query(JobModel).filter(JobModel.id == job.id).update(vars(job))
486
- session.commit()
487
- return Job
488
-
489
- def update_job_status(self, job_id: str, status: JobStatus):
490
- with self.session_maker() as session:
491
- session.query(JobModel).filter(JobModel.id == job_id).update({"status": status})
492
- if status == JobStatus.COMPLETED:
493
- session.query(JobModel).filter(JobModel.id == job_id).update({"completed_at": get_utc_time()})
494
- session.commit()
letta/orm/__init__.py CHANGED
@@ -2,6 +2,7 @@ from letta.orm.base import Base
2
2
  from letta.orm.block import Block
3
3
  from letta.orm.blocks_agents import BlocksAgents
4
4
  from letta.orm.file import FileMetadata
5
+ from letta.orm.job import Job
5
6
  from letta.orm.organization import Organization
6
7
  from letta.orm.sandbox_config import SandboxConfig, SandboxEnvironmentVariable
7
8
  from letta.orm.source import Source
letta/orm/job.py ADDED
@@ -0,0 +1,29 @@
1
+ from datetime import datetime
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ from sqlalchemy import JSON, String
5
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
+
7
+ from letta.orm.mixins import UserMixin
8
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
9
+ from letta.schemas.enums import JobStatus
10
+ from letta.schemas.job import Job as PydanticJob
11
+
12
+ if TYPE_CHECKING:
13
+ from letta.orm.user import User
14
+
15
+
16
+ class Job(SqlalchemyBase, UserMixin):
17
+ """Jobs run in the background and are owned by a user.
18
+ Typical jobs involve loading and processing sources etc.
19
+ """
20
+
21
+ __tablename__ = "jobs"
22
+ __pydantic_model__ = PydanticJob
23
+
24
+ status: Mapped[JobStatus] = mapped_column(String, default=JobStatus.created, doc="The current status of the job.")
25
+ completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the job was completed.")
26
+ metadata_: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="The metadata of the job.")
27
+
28
+ # relationships
29
+ user: Mapped["User"] = relationship("User", back_populates="jobs")
@@ -31,24 +31,43 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
31
31
  def list(
32
32
  cls, *, db_session: "Session", cursor: Optional[str] = None, limit: Optional[int] = 50, **kwargs
33
33
  ) -> List[Type["SqlalchemyBase"]]:
34
- """List records with optional cursor (for pagination) and limit."""
35
- logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
36
- with db_session as session:
37
- # Start with the base query filtered by kwargs
38
- query = select(cls).filter_by(**kwargs)
34
+ """
35
+ List records with optional cursor (for pagination), limit, and automatic filtering.
36
+
37
+ Args:
38
+ db_session: The database session to use.
39
+ cursor: Optional ID to start pagination from.
40
+ limit: Maximum number of records to return.
41
+ **kwargs: Filters passed as equality conditions or iterable for IN filtering.
39
42
 
40
- # Add a cursor condition if provided
43
+ Returns:
44
+ A list of model instances matching the filters.
45
+ """
46
+ logger.debug(f"Listing {cls.__name__} with filters {kwargs}")
47
+ with db_session as session:
48
+ # Start with a base query
49
+ query = select(cls)
50
+
51
+ # Apply filtering logic
52
+ for key, value in kwargs.items():
53
+ column = getattr(cls, key)
54
+ if isinstance(value, (list, tuple, set)): # Check for iterables
55
+ query = query.where(column.in_(value))
56
+ else: # Single value for equality filtering
57
+ query = query.where(column == value)
58
+
59
+ # Apply cursor for pagination
41
60
  if cursor:
42
61
  query = query.where(cls.id > cursor)
43
62
 
44
- # Add a limit to the query if provided
45
- query = query.order_by(cls.id).limit(limit)
46
-
47
63
  # Handle soft deletes if the class has the 'is_deleted' attribute
48
64
  if hasattr(cls, "is_deleted"):
49
65
  query = query.where(cls.is_deleted == False)
50
66
 
51
- # Execute the query and return the results as a list of model instances
67
+ # Add ordering and limit
68
+ query = query.order_by(cls.id).limit(limit)
69
+
70
+ # Execute the query and return results as model instances
52
71
  return list(session.execute(query).scalars())
53
72
 
54
73
  @classmethod
letta/orm/user.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, List
2
2
 
3
3
  from sqlalchemy.orm import Mapped, mapped_column, relationship
4
4
 
@@ -7,7 +7,7 @@ from letta.orm.sqlalchemy_base import SqlalchemyBase
7
7
  from letta.schemas.user import User as PydanticUser
8
8
 
9
9
  if TYPE_CHECKING:
10
- from letta.orm.organization import Organization
10
+ from letta.orm import Job, Organization
11
11
 
12
12
 
13
13
  class User(SqlalchemyBase, OrganizationMixin):
@@ -20,10 +20,10 @@ class User(SqlalchemyBase, OrganizationMixin):
20
20
 
21
21
  # relationships
22
22
  organization: Mapped["Organization"] = relationship("Organization", back_populates="users")
23
+ jobs: Mapped[List["Job"]] = relationship("Job", back_populates="user", doc="the jobs associated with this user.")
23
24
 
24
25
  # TODO: Add this back later potentially
25
26
  # agents: Mapped[List["Agent"]] = relationship(
26
27
  # "Agent", secondary="users_agents", back_populates="users", doc="the agents associated with this user."
27
28
  # )
28
29
  # tokens: Mapped[List["Token"]] = relationship("Token", back_populates="user", doc="the tokens associated with this user.")
29
- # jobs: Mapped[List["Job"]] = relationship("Job", back_populates="user", doc="the jobs associated with this user.")
letta/schemas/job.py CHANGED
@@ -4,12 +4,13 @@ from typing import Optional
4
4
  from pydantic import Field
5
5
 
6
6
  from letta.schemas.enums import JobStatus
7
- from letta.schemas.letta_base import LettaBase
8
- from letta.utils import get_utc_time
7
+ from letta.schemas.letta_base import OrmMetadataBase
9
8
 
10
9
 
11
- class JobBase(LettaBase):
10
+ class JobBase(OrmMetadataBase):
12
11
  __id_prefix__ = "job"
12
+ status: JobStatus = Field(default=JobStatus.created, description="The status of the job.")
13
+ completed_at: Optional[datetime] = Field(None, description="The unix timestamp of when the job was completed.")
13
14
  metadata_: Optional[dict] = Field(None, description="The metadata of the job.")
14
15
 
15
16
 
@@ -27,12 +28,11 @@ class Job(JobBase):
27
28
  """
28
29
 
29
30
  id: str = JobBase.generate_id_field()
30
- status: JobStatus = Field(default=JobStatus.created, description="The status of the job.")
31
- created_at: datetime = Field(default_factory=get_utc_time, description="The unix timestamp of when the job was created.")
32
- completed_at: Optional[datetime] = Field(None, description="The unix timestamp of when the job was completed.")
33
- user_id: str = Field(..., description="The unique identifier of the user associated with the job.")
31
+ user_id: Optional[str] = Field(None, description="The unique identifier of the user associated with the job.")
34
32
 
35
33
 
36
34
  class JobUpdate(JobBase):
37
- id: str = Field(..., description="The unique identifier of the job.")
38
- status: Optional[JobStatus] = Field(..., description="The status of the job.")
35
+ status: Optional[JobStatus] = Field(None, description="The status of the job.")
36
+
37
+ class Config:
38
+ extra = "ignore" # Ignores extra fields
@@ -2,6 +2,8 @@ from typing import List, Optional
2
2
 
3
3
  from fastapi import APIRouter, Depends, Header, HTTPException, Query
4
4
 
5
+ from letta.orm.errors import NoResultFound
6
+ from letta.schemas.enums import JobStatus
5
7
  from letta.schemas.job import Job
6
8
  from letta.server.rest_api.utils import get_letta_server
7
9
  from letta.server.server import SyncServer
@@ -21,12 +23,11 @@ def list_jobs(
21
23
  actor = server.get_user_or_default(user_id=user_id)
22
24
 
23
25
  # TODO: add filtering by status
24
- jobs = server.list_jobs(user_id=actor.id)
26
+ jobs = server.job_manager.list_jobs(actor=actor)
25
27
 
26
- # TODO: eventually use ORM
27
- # results = session.query(JobModel).filter(JobModel.user_id == user_id, JobModel.metadata_["source_id"].astext == sourced_id).all()
28
28
  if source_id:
29
29
  # can't be in the ORM since we have source_id stored in the metadata_
30
+ # TODO: Probably change this
30
31
  jobs = [job for job in jobs if job.metadata_.get("source_id") == source_id]
31
32
  return jobs
32
33
 
@@ -41,32 +42,39 @@ def list_active_jobs(
41
42
  """
42
43
  actor = server.get_user_or_default(user_id=user_id)
43
44
 
44
- return server.list_active_jobs(user_id=actor.id)
45
+ return server.job_manager.list_jobs(actor=actor, statuses=[JobStatus.created, JobStatus.running])
45
46
 
46
47
 
47
48
  @router.get("/{job_id}", response_model=Job, operation_id="get_job")
48
49
  def get_job(
49
50
  job_id: str,
51
+ user_id: Optional[str] = Header(None, alias="user_id"),
50
52
  server: "SyncServer" = Depends(get_letta_server),
51
53
  ):
52
54
  """
53
55
  Get the status of a job.
54
56
  """
57
+ actor = server.get_user_or_default(user_id=user_id)
55
58
 
56
- return server.get_job(job_id=job_id)
59
+ try:
60
+ return server.job_manager.get_job_by_id(job_id=job_id, actor=actor)
61
+ except NoResultFound:
62
+ raise HTTPException(status_code=404, detail="Job not found")
57
63
 
58
64
 
59
65
  @router.delete("/{job_id}", response_model=Job, operation_id="delete_job")
60
66
  def delete_job(
61
67
  job_id: str,
68
+ user_id: Optional[str] = Header(None, alias="user_id"),
62
69
  server: "SyncServer" = Depends(get_letta_server),
63
70
  ):
64
71
  """
65
72
  Delete a job by its job_id.
66
73
  """
67
- job = server.get_job(job_id=job_id)
68
- if not job:
69
- raise HTTPException(status_code=404, detail="Job not found")
74
+ actor = server.get_user_or_default(user_id=user_id)
70
75
 
71
- server.delete_job(job_id=job_id)
72
- return job
76
+ try:
77
+ job = server.job_manager.delete_job_by_id(job_id=job_id, actor=actor)
78
+ return job
79
+ except NoResultFound:
80
+ raise HTTPException(status_code=404, detail="Job not found")
@@ -16,6 +16,7 @@ from letta.schemas.file import FileMetadata
16
16
  from letta.schemas.job import Job
17
17
  from letta.schemas.passage import Passage
18
18
  from letta.schemas.source import Source, SourceCreate, SourceUpdate
19
+ from letta.schemas.user import User
19
20
  from letta.server.rest_api.utils import get_letta_server
20
21
  from letta.server.server import SyncServer
21
22
  from letta.utils import sanitize_filename
@@ -175,13 +176,14 @@ def upload_file_to_source(
175
176
  completed_at=None,
176
177
  )
177
178
  job_id = job.id
178
- server.ms.create_job(job)
179
+ server.job_manager.create_job(job, actor=actor)
179
180
 
180
181
  # create background task
181
- background_tasks.add_task(load_file_to_source_async, server, source_id=source.id, file=file, job_id=job.id, bytes=bytes)
182
+ background_tasks.add_task(load_file_to_source_async, server, source_id=source.id, file=file, job_id=job.id, bytes=bytes, actor=actor)
182
183
 
183
184
  # return job information
184
- job = server.ms.get_job(job_id=job_id)
185
+ # Is this necessary? Can we just return the job from create_job?
186
+ job = server.job_manager.get_job_by_id(job_id=job_id, actor=actor)
185
187
  assert job is not None, "Job not found"
186
188
  return job
187
189
 
@@ -234,7 +236,7 @@ def delete_file_from_source(
234
236
  raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
235
237
 
236
238
 
237
- def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, file: UploadFile, bytes: bytes):
239
+ def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, file: UploadFile, bytes: bytes, actor: User):
238
240
  # Create a temporary directory (deleted after the context manager exits)
239
241
  with tempfile.TemporaryDirectory() as tmpdirname:
240
242
  # Sanitize the filename
@@ -246,4 +248,4 @@ def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, f
246
248
  buffer.write(bytes)
247
249
 
248
250
  # Pass the file to load_file_to_source
249
- server.load_file_to_source(source_id, file_path, job_id)
251
+ server.load_file_to_source(source_id, file_path, job_id, actor)
letta/server/server.py CHANGED
@@ -6,7 +6,7 @@ import warnings
6
6
  from abc import abstractmethod
7
7
  from asyncio import Lock
8
8
  from datetime import datetime
9
- from typing import Callable, Dict, List, Optional, Tuple, Union
9
+ from typing import Callable, List, Optional, Tuple, Union
10
10
 
11
11
  from composio.client import Composio
12
12
  from composio.client.collections import ActionModel, AppModel
@@ -56,7 +56,7 @@ from letta.schemas.embedding_config import EmbeddingConfig
56
56
 
57
57
  # openai schemas
58
58
  from letta.schemas.enums import JobStatus
59
- from letta.schemas.job import Job
59
+ from letta.schemas.job import Job, JobUpdate
60
60
  from letta.schemas.letta_message import FunctionReturn, LettaMessage
61
61
  from letta.schemas.llm_config import LLMConfig
62
62
  from letta.schemas.memory import (
@@ -75,6 +75,7 @@ from letta.schemas.user import User
75
75
  from letta.services.agents_tags_manager import AgentsTagsManager
76
76
  from letta.services.block_manager import BlockManager
77
77
  from letta.services.blocks_agents_manager import BlocksAgentsManager
78
+ from letta.services.job_manager import JobManager
78
79
  from letta.services.organization_manager import OrganizationManager
79
80
  from letta.services.per_agent_lock_manager import PerAgentLockManager
80
81
  from letta.services.sandbox_config_manager import SandboxConfigManager
@@ -256,6 +257,7 @@ class SyncServer(Server):
256
257
  self.agents_tags_manager = AgentsTagsManager()
257
258
  self.sandbox_config_manager = SandboxConfigManager(tool_settings)
258
259
  self.blocks_agents_manager = BlocksAgentsManager()
260
+ self.job_manager = JobManager()
259
261
 
260
262
  # Managers that interface with parallelism
261
263
  self.per_agent_lock_manager = PerAgentLockManager()
@@ -1469,39 +1471,12 @@ class SyncServer(Server):
1469
1471
 
1470
1472
  # TODO: delete data from agent passage stores (?)
1471
1473
 
1472
- def create_job(self, user_id: str, metadata: Optional[Dict] = None) -> Job:
1473
- """Create a new job"""
1474
- job = Job(
1475
- user_id=user_id,
1476
- status=JobStatus.created,
1477
- metadata_=metadata,
1478
- )
1479
- self.ms.create_job(job)
1480
- return job
1481
-
1482
- def delete_job(self, job_id: str):
1483
- """Delete a job"""
1484
- self.ms.delete_job(job_id)
1485
-
1486
- def get_job(self, job_id: str) -> Job:
1487
- """Get a job"""
1488
- return self.ms.get_job(job_id)
1489
-
1490
- def list_jobs(self, user_id: str) -> List[Job]:
1491
- """List all jobs for a user"""
1492
- return self.ms.list_jobs(user_id=user_id)
1493
-
1494
- def list_active_jobs(self, user_id: str) -> List[Job]:
1495
- """List all active jobs for a user"""
1496
- jobs = self.ms.list_jobs(user_id=user_id)
1497
- return [job for job in jobs if job.status in [JobStatus.created, JobStatus.running]]
1498
-
1499
- def load_file_to_source(self, source_id: str, file_path: str, job_id: str) -> Job:
1474
+ def load_file_to_source(self, source_id: str, file_path: str, job_id: str, actor: User) -> Job:
1500
1475
 
1501
1476
  # update job
1502
- job = self.ms.get_job(job_id)
1477
+ job = self.job_manager.get_job_by_id(job_id, actor=actor)
1503
1478
  job.status = JobStatus.running
1504
- self.ms.update_job(job)
1479
+ self.job_manager.update_job_by_id(job_id=job_id, job_update=JobUpdate(**job.model_dump()), actor=actor)
1505
1480
 
1506
1481
  # try:
1507
1482
  from letta.data_sources.connectors import DirectoryConnector
@@ -1509,23 +1484,12 @@ class SyncServer(Server):
1509
1484
  source = self.source_manager.get_source_by_id(source_id=source_id)
1510
1485
  connector = DirectoryConnector(input_files=[file_path])
1511
1486
  num_passages, num_documents = self.load_data(user_id=source.created_by_id, source_name=source.name, connector=connector)
1512
- # except Exception as e:
1513
- # # job failed with error
1514
- # error = str(e)
1515
- # print(error)
1516
- # job.status = JobStatus.failed
1517
- # job.metadata_["error"] = error
1518
- # self.ms.update_job(job)
1519
- # # TODO: delete any associated passages/files?
1520
-
1521
- # # return failed job
1522
- # return job
1523
1487
 
1524
1488
  # update job status
1525
1489
  job.status = JobStatus.completed
1526
1490
  job.metadata_["num_passages"] = num_passages
1527
1491
  job.metadata_["num_documents"] = num_documents
1528
- self.ms.update_job(job)
1492
+ self.job_manager.update_job_by_id(job_id=job_id, job_update=JobUpdate(**job.model_dump()), actor=actor)
1529
1493
 
1530
1494
  return job
1531
1495
 
@@ -0,0 +1,85 @@
1
+ from typing import List, Optional
2
+
3
+ from letta.orm.job import Job as JobModel
4
+ from letta.schemas.enums import JobStatus
5
+ from letta.schemas.job import Job as PydanticJob
6
+ from letta.schemas.job import JobUpdate
7
+ from letta.schemas.user import User as PydanticUser
8
+ from letta.utils import enforce_types, get_utc_time
9
+
10
+
11
+ class JobManager:
12
+ """Manager class to handle business logic related to Jobs."""
13
+
14
+ def __init__(self):
15
+ # Fetching the db_context similarly as in OrganizationManager
16
+ from letta.server.server import db_context
17
+
18
+ self.session_maker = db_context
19
+
20
+ @enforce_types
21
+ def create_job(self, pydantic_job: PydanticJob, actor: PydanticUser) -> PydanticJob:
22
+ """Create a new job based on the JobCreate schema."""
23
+ with self.session_maker() as session:
24
+ # Associate the job with the user
25
+ pydantic_job.user_id = actor.id
26
+ job_data = pydantic_job.model_dump()
27
+ job = JobModel(**job_data)
28
+ job.create(session, actor=actor) # Save job in the database
29
+ return job.to_pydantic()
30
+
31
+ @enforce_types
32
+ def update_job_by_id(self, job_id: str, job_update: JobUpdate, actor: PydanticUser) -> PydanticJob:
33
+ """Update a job by its ID with the given JobUpdate object."""
34
+ with self.session_maker() as session:
35
+ # Fetch the job by ID
36
+ job = JobModel.read(db_session=session, identifier=job_id) # TODO: Add this later , actor=actor)
37
+
38
+ # Update job attributes with only the fields that were explicitly set
39
+ update_data = job_update.model_dump(exclude_unset=True, exclude_none=True)
40
+
41
+ # Automatically update the completion timestamp if status is set to 'completed'
42
+ if update_data.get("status") == JobStatus.completed and not job.completed_at:
43
+ job.completed_at = get_utc_time()
44
+
45
+ for key, value in update_data.items():
46
+ setattr(job, key, value)
47
+
48
+ # Save the updated job to the database
49
+ return job.update(db_session=session) # TODO: Add this later , actor=actor)
50
+
51
+ @enforce_types
52
+ def get_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
53
+ """Fetch a job by its ID."""
54
+ with self.session_maker() as session:
55
+ # Retrieve job by ID using the Job model's read method
56
+ job = JobModel.read(db_session=session, identifier=job_id) # TODO: Add this later , actor=actor)
57
+ return job.to_pydantic()
58
+
59
+ @enforce_types
60
+ def list_jobs(
61
+ self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50, statuses: Optional[List[JobStatus]] = None
62
+ ) -> List[PydanticJob]:
63
+ """List all jobs with optional pagination and status filter."""
64
+ with self.session_maker() as session:
65
+ filter_kwargs = {"user_id": actor.id}
66
+
67
+ # Add status filter if provided
68
+ if statuses:
69
+ filter_kwargs["status"] = statuses
70
+
71
+ jobs = JobModel.list(
72
+ db_session=session,
73
+ cursor=cursor,
74
+ limit=limit,
75
+ **filter_kwargs,
76
+ )
77
+ return [job.to_pydantic() for job in jobs]
78
+
79
+ @enforce_types
80
+ def delete_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
81
+ """Delete a job by its ID."""
82
+ with self.session_maker() as session:
83
+ job = JobModel.read(db_session=session, identifier=job_id) # TODO: Add this later , actor=actor)
84
+ job.hard_delete(db_session=session) # TODO: Add this later , actor=actor)
85
+ return job.to_pydantic()
@@ -159,6 +159,8 @@ class ToolExecutionSandbox:
159
159
  # Set up env for venv
160
160
  env["VIRTUAL_ENV"] = venv_path
161
161
  env["PATH"] = os.path.join(venv_path, "bin") + ":" + env["PATH"]
162
+ # Suppress all warnings
163
+ env["PYTHONWARNINGS"] = "ignore"
162
164
 
163
165
  # Execute the code in a restricted subprocess
164
166
  try:
@@ -170,9 +172,31 @@ class ToolExecutionSandbox:
170
172
  capture_output=True,
171
173
  text=True,
172
174
  )
173
- if result.stderr:
174
- logger.error(f"Sandbox execution error: {result.stderr}")
175
- raise RuntimeError(f"Sandbox execution error: {result.stderr}")
175
+
176
+ # Handle error with optimistic error parsing from the string
177
+ # This is very brittle, so we fall back to a RuntimeError if parsing fails
178
+ if result.returncode != 0:
179
+ # Log the error
180
+ logger.error(f"Sandbox execution error:\n{result.stderr}")
181
+
182
+ # Parse and raise the actual error from stderr
183
+ tb_lines = result.stderr.strip().splitlines()
184
+ exception_line = tb_lines[-1] # The last line contains the exception
185
+
186
+ try:
187
+ # Split exception type and message
188
+ exception_type, exception_message = exception_line.split(": ", 1)
189
+ exception_type = exception_type.strip()
190
+ exception_message = exception_message.strip()
191
+
192
+ # Dynamically raise the exception
193
+ exception_class = eval(exception_type) # Look up the exception type
194
+
195
+ except Exception:
196
+ # Fallback to RuntimeError if parsing fails
197
+ raise RuntimeError(result.stderr)
198
+
199
+ raise exception_class(exception_message)
176
200
 
177
201
  func_result, stdout = self.parse_out_function_results_markers(result.stdout)
178
202
  func_return, agent_state = self.parse_best_effort(func_result)
@@ -182,9 +206,11 @@ class ToolExecutionSandbox:
182
206
  except subprocess.TimeoutExpired:
183
207
  raise TimeoutError(f"Executing tool {self.tool_name} has timed out.")
184
208
  except subprocess.CalledProcessError as e:
185
- raise RuntimeError(f"Executing tool {self.tool_name} has process error: {e}")
209
+ logger.error(f"Executing tool {self.tool_name} has process error: {e}")
210
+ raise e
186
211
  except Exception as e:
187
- raise RuntimeError(f"Executing tool {self.tool_name} has an unexpected error: {e}")
212
+ logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}")
213
+ raise e
188
214
 
189
215
  def run_local_dir_sandbox_runpy(
190
216
  self, sbx_config: SandboxConfig, env_vars: Dict[str, str], temp_file_path: str, old_stdout: TextIO
letta/utils.py CHANGED
@@ -15,7 +15,7 @@ import uuid
15
15
  from contextlib import contextmanager
16
16
  from datetime import datetime, timedelta, timezone
17
17
  from functools import wraps
18
- from typing import List, Union, _GenericAlias, get_type_hints
18
+ from typing import List, Union, _GenericAlias, get_args, get_origin, get_type_hints
19
19
  from urllib.parse import urljoin, urlparse
20
20
 
21
21
  import demjson3 as demjson
@@ -529,16 +529,32 @@ def enforce_types(func):
529
529
  # Pair each argument with its corresponding type hint
530
530
  args_with_hints = dict(zip(arg_names[1:], args[1:])) # Skipping 'self'
531
531
 
532
+ # Function to check if a value matches a given type hint
533
+ def matches_type(value, hint):
534
+ origin = get_origin(hint)
535
+ args = get_args(hint)
536
+
537
+ if origin is list and isinstance(value, list): # Handle List[T]
538
+ element_type = args[0] if args else None
539
+ return all(isinstance(v, element_type) for v in value) if element_type else True
540
+ elif origin is Union and type(None) in args: # Handle Optional[T]
541
+ non_none_type = next(arg for arg in args if arg is not type(None))
542
+ return value is None or matches_type(value, non_none_type)
543
+ elif origin: # Handle other generics like Dict, Tuple, etc.
544
+ return isinstance(value, origin)
545
+ else: # Handle non-generic types
546
+ return isinstance(value, hint)
547
+
532
548
  # Check types of arguments
533
549
  for arg_name, arg_value in args_with_hints.items():
534
550
  hint = hints.get(arg_name)
535
- if hint and not isinstance(arg_value, hint) and not (is_optional_type(hint) and arg_value is None):
551
+ if hint and not matches_type(arg_value, hint):
536
552
  raise ValueError(f"Argument {arg_name} does not match type {hint}")
537
553
 
538
554
  # Check types of keyword arguments
539
555
  for arg_name, arg_value in kwargs.items():
540
556
  hint = hints.get(arg_name)
541
- if hint and not isinstance(arg_value, hint) and not (is_optional_type(hint) and arg_value is None):
557
+ if hint and not matches_type(arg_value, hint):
542
558
  raise ValueError(f"Argument {arg_name} does not match type {hint}")
543
559
 
544
560
  return func(*args, **kwargs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.6.0.dev20241204213946
3
+ Version: 0.6.0.dev20241205104308
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -13,7 +13,7 @@ letta/cli/cli.py,sha256=fpcpBKEAzKU6o9gSS5pe6YRTkiybIik5CC9mCAVl_bA,16928
13
13
  letta/cli/cli_config.py,sha256=tB0Wgz3O9j6KiCsU1HWfsKmhNM9RqLsAxzxEDFQFGnM,8565
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=uUEpADbWwm7bhwv2mA4kllyDC6MpnfRKd8CLpP8kEt0,123907
16
+ letta/client/client.py,sha256=hQiTmkmddoGONuSlNG0Yrbo1Y2k9VRTRDFAOtHRbOPI,124143
17
17
  letta/client/streaming.py,sha256=Hh5pjlyrdCuO2V75ZCxSSOCPd3BmHdKFGaIUJC6fBp0,4775
18
18
  letta/client/utils.py,sha256=OJlAKWrldc4I6M1WpcTWNtPJ4wfxlzlZqWLfCozkFtI,2872
19
19
  letta/config.py,sha256=AF4XY6grcu87OLjrWXh1ufnyKWsCL0qER-_9jQCAlU0,18947
@@ -85,12 +85,12 @@ letta/local_llm/webui/settings.py,sha256=gmLHfiOl1u4JmlAZU2d2O8YKF9lafdakyjwR_ft
85
85
  letta/log.py,sha256=FxkAk2f8Bl-u9dfImSj1DYnjAsmV6PL3tjTSnEiNP48,2218
86
86
  letta/main.py,sha256=5guUzYyxID3FDlegk3dNUev7vjPMglcIw-xqdyHdhKI,19175
87
87
  letta/memory.py,sha256=YupXOvzVJXH59RW4XWBrd7qMNEYaMbtWXCheKeWZwpU,17873
88
- letta/metadata.py,sha256=IipAhhpLXve8mVRAf4LNQhZxexwt-5lmf7bqCr8hE3E,18643
88
+ letta/metadata.py,sha256=jv_s2Y6M8aE3bpOOTr-xwBBu58zZHx9tYtAekVa_Dz4,16233
89
89
  letta/o1_agent.py,sha256=jTMlP7LxR4iUDWaGHMy8SiZtlzn6_RqP0H1HaEWXydQ,3078
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=3n3rOtlDySptVPM3H1LpT3Ey5mdvQZjK0V0TJwDmaoI,382
93
+ letta/orm/__init__.py,sha256=yXCse66-Va1tuVhoDcHbaLIffBiJKLKF4JR6SAFiIZU,412
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/block.py,sha256=xymYeCTJJFkzADW6wjfP2LXNZZN9yg4mCSybbvEEMMM,2356
@@ -98,13 +98,14 @@ letta/orm/blocks_agents.py,sha256=o6cfblODja7so4444npW0vusqKcvDPp8YJdsWsOePus,11
98
98
  letta/orm/enums.py,sha256=KfHcFt_fR6GUmSlmfsa-TetvmuRxGESNve8MStRYW64,145
99
99
  letta/orm/errors.py,sha256=nv1HnF3z4-u9m_g7SO5TO5u2nmJN677_n8F0iIjluUI,460
100
100
  letta/orm/file.py,sha256=FtfZlJLXfac4ntaw3kC0N9VRoD255m8EK4p-pC2lcHk,1519
101
+ letta/orm/job.py,sha256=If-qSTJW4t5h-6Jolw3tS3-xMZEaPIbXe3S0GMf_FXI,1102
101
102
  letta/orm/mixins.py,sha256=LfwePamGyOwCtAEUm-sZpIBJjODIMe4MnA_JTUcppLs,1155
102
103
  letta/orm/organization.py,sha256=mliw4q7-SRfRcGIG8paNfCNn6ITTjR7nFalZzlRszqU,2272
103
104
  letta/orm/sandbox_config.py,sha256=PCMHE-eJPzBT-90OYtXjEMRF4f9JB8AJIGETE7bu-f0,2870
104
105
  letta/orm/source.py,sha256=Ib0XHCMt345RjBSC30A398rZ21W5mA4PXX00XNXyd24,2021
105
- letta/orm/sqlalchemy_base.py,sha256=PGkbEb7nz3aNMScQEu55KXxE48Xw7DA01tV9famFmO0,9980
106
+ letta/orm/sqlalchemy_base.py,sha256=CmCwVCHVyhzDAd-xIUFh72bzSX-bA0mwvMUD7FrIlUk,10672
106
107
  letta/orm/tool.py,sha256=d0GclU_7qg8Z6ZE6kkH1kmrUAMCiV-ZM8BGaT1mnBU4,2089
107
- letta/orm/user.py,sha256=bB4qGIT-ZoECZeeVqG-z3Z7WFXGqpC-GPcoYQoJZOuc,1137
108
+ letta/orm/user.py,sha256=bUZzyBQXfR0w7mZAkThPAQE6kKx6W--Rw6uAiPEUW3s,1133
108
109
  letta/persistence_manager.py,sha256=sEnNdNisK7StqIzG8eIY0YMag6S9hZFbkDfmY7L2Ikc,5268
109
110
  letta/personas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
111
  letta/personas/examples/anna_pa.txt,sha256=zgiNdSNhy1HQy58cF_6RFPzcg2i37F9v38YuL1CW40A,1849
@@ -139,7 +140,7 @@ letta/schemas/embedding_config.py,sha256=1kD6NpiXeH4roVumxqDAKk7xt8SpXGWNhZs_XXU
139
140
  letta/schemas/enums.py,sha256=F396hXs57up4Jqj1vwWVknMpoVo7MkccVBALvKGHPpE,1032
140
141
  letta/schemas/file.py,sha256=ChN2CWzLI2TT9WLItcfElEH0E8b7kzPylF2OQBr6Beg,1550
141
142
  letta/schemas/health.py,sha256=zT6mYovvD17iJRuu2rcaQQzbEEYrkwvAE9TB7iU824c,139
142
- letta/schemas/job.py,sha256=605TWjUdNy5Rgoc7en_DX3Giz-I7sVTXiSRZqxL__d8,1543
143
+ letta/schemas/job.py,sha256=vRVVpMCHTxot9uaalLS8RARnqzJWvcLB1XP5XRBioPc,1398
143
144
  letta/schemas/letta_base.py,sha256=QlCY5BBSjEPNpUvSHDl_P0TXQIPlr7xfhK_2SqKqadQ,3567
144
145
  letta/schemas/letta_message.py,sha256=RuVVtwFbi85yP3dXQxowofQ6cI2cO_CdGtgpHGQzgHc,6563
145
146
  letta/schemas/letta_request.py,sha256=E6OwKiceNffpdGdQMI1qc0jEfpL_e7O9BTzklOkbt6Y,1019
@@ -181,16 +182,16 @@ letta/server/rest_api/routers/v1/__init__.py,sha256=RZc0fIHNN4BGretjU6_TGK7q49Ry
181
182
  letta/server/rest_api/routers/v1/agents.py,sha256=Tj7QyHjem_tOgzDzTyEJREDH2rzDgpE6S5azBDrhY_o,24884
182
183
  letta/server/rest_api/routers/v1/blocks.py,sha256=UCVfMbb8hzOXI6a8OYWKuXyOropIxw6PYKZkwwAh1v0,4880
183
184
  letta/server/rest_api/routers/v1/health.py,sha256=pKCuVESlVOhGIb4VC4K-H82eZqfghmT6kvj2iOkkKuc,401
184
- letta/server/rest_api/routers/v1/jobs.py,sha256=a-j0v-5A0un0pVCOHpfeWnzpOWkVDQO6ti42k_qAlZY,2272
185
+ letta/server/rest_api/routers/v1/jobs.py,sha256=gnu__rjcd9SpKdQkSD_sci-xJYH0fw828PuHMcYwsCw,2678
185
186
  letta/server/rest_api/routers/v1/llms.py,sha256=TcyvSx6MEM3je5F4DysL7ligmssL_pFlJaaO4uL95VY,877
186
187
  letta/server/rest_api/routers/v1/organizations.py,sha256=tyqVzXTpMtk3sKxI3Iz4aS6RhbGEbXDzFBB_CpW18v4,2080
187
188
  letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=4tkTH8z9vpuBiGzxrS_wxkFdznnWZx-U-9F08czHMP8,5004
188
- letta/server/rest_api/routers/v1/sources.py,sha256=HUbcBENk4RZDzxvP9tRANiWLX60nOkMdUCZ48jFGfk8,9630
189
+ letta/server/rest_api/routers/v1/sources.py,sha256=1zIkopcyHDMarOGJISy5yXpi9-yeBRBitJ6_yiIJGdY,9818
189
190
  letta/server/rest_api/routers/v1/tools.py,sha256=TP16cpuTF2HYLFZVmabExw9gziB-PtkExtWVkjxrRes,9553
190
191
  letta/server/rest_api/routers/v1/users.py,sha256=M1wEr2IyHzuRwINYxLXTkrbAH3osLe_cWjzrWrzR1aw,3729
191
192
  letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
192
193
  letta/server/rest_api/utils.py,sha256=6c5a_-ZFTlwZ1IuzpRQtqxSG1eD56nNhKhWlrdgBYWk,3103
193
- letta/server/server.py,sha256=tfEnHSTnWKWZslRP-bXYUbM2A2-XiAkbjnGrHQu6NAw,81341
194
+ letta/server/server.py,sha256=ag3y5efyi0zzNC_TTJwJ9-xCbzzIgI2Orz36UDb8zoY,80381
194
195
  letta/server/startup.sh,sha256=wTOQOJJZw_Iec57WIu0UW0AVflk0ZMWYZWg8D3T_gSQ,698
195
196
  letta/server/static_files/assets/index-3ab03d5b.css,sha256=OrA9W4iKJ5h2Wlr7GwdAT4wow0CM8hVit1yOxEL49Qw,54295
196
197
  letta/server/static_files/assets/index-9fa459a2.js,sha256=wtfkyHnEIMACHKL3UgN_jZNOKWEcOFjmWoeRHLngPwk,1815584
@@ -207,11 +208,12 @@ letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
207
208
  letta/services/agents_tags_manager.py,sha256=zNqeXDpaf4dQ77jrRHiQfITdk4FawBzcND-9tWrj8gw,3127
208
209
  letta/services/block_manager.py,sha256=TrbStwHAREwnybA6jZSkNPe-EYUa5rdiuliPR2PTV-M,5426
209
210
  letta/services/blocks_agents_manager.py,sha256=mfO3EMW9os_E1_r4SRlC2wmBFFLpt8p-yhdOH_Iotaw,5627
211
+ letta/services/job_manager.py,sha256=FrkSXloke48CZKuzlYdysxM5gKWoTu7FRigPrs_YW4A,3645
210
212
  letta/services/organization_manager.py,sha256=OfE2_NMmhqXURX4sg7hCOiFQVQpV5ZiPu7J3sboCSYc,3555
211
213
  letta/services/per_agent_lock_manager.py,sha256=porM0cKKANQ1FvcGXOO_qM7ARk5Fgi1HVEAhXsAg9-4,546
212
214
  letta/services/sandbox_config_manager.py,sha256=9BCu59nHR4nIMFXgFyEMOY2UTmZvBMS3GlDBWWCHB4I,12648
213
215
  letta/services/source_manager.py,sha256=StX5Wfd7XSCKJet8qExIu3GMoI-eMIbEarAeTv2gq0s,6555
214
- letta/services/tool_execution_sandbox.py,sha256=HY8W6W0-AlYOc6597WzF-eF_TB-OHhATf2-DT2-4lk4,20215
216
+ letta/services/tool_execution_sandbox.py,sha256=GTWdfAKIMIuODEFbmReyEYkOnE62uzDF-3FHWee1s3A,21295
215
217
  letta/services/tool_manager.py,sha256=FVCB9R3NFahh-KE5jROzf6J9WEgqhqGoDk5RpWjlgjg,7835
216
218
  letta/services/tool_sandbox_env/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
217
219
  letta/services/user_manager.py,sha256=UJa0hqCjz0yXtvrCR8OVBqlSR5lC_Ejn-uG__58zLds,4398
@@ -219,9 +221,9 @@ letta/settings.py,sha256=ZcUcwvl7hStawZ0JOA0133jNk3j5qBd7qlFAAaIPsU8,3608
219
221
  letta/streaming_interface.py,sha256=_FPUWy58j50evHcpXyd7zB1wWqeCc71NCFeWh_TBvnw,15736
220
222
  letta/streaming_utils.py,sha256=329fsvj1ZN0r0LpQtmMPZ2vSxkDBIUUwvGHZFkjm2I8,11745
221
223
  letta/system.py,sha256=buKYPqG5n2x41hVmWpu6JUpyd7vTWED9Km2_M7dLrvk,6960
222
- letta/utils.py,sha256=iELiiJhSnijGDmwyk_T4NBJIqFUnEw_Flv9ZpSBUPFA,32136
223
- letta_nightly-0.6.0.dev20241204213946.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
224
- letta_nightly-0.6.0.dev20241204213946.dist-info/METADATA,sha256=iNEBlkI4KVar7ahBDGxq6tBLveBGuL7qySYFN2aCcqk,11413
225
- letta_nightly-0.6.0.dev20241204213946.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
226
- letta_nightly-0.6.0.dev20241204213946.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
227
- letta_nightly-0.6.0.dev20241204213946.dist-info/RECORD,,
224
+ letta/utils.py,sha256=7FGZYp8JCEtP0PBSfV03Zj4kW3pwV7qpNki4AOU3a94,32913
225
+ letta_nightly-0.6.0.dev20241205104308.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
226
+ letta_nightly-0.6.0.dev20241205104308.dist-info/METADATA,sha256=gz6eo82cNek6syp2o1gS4FSmOQTzVty0a41NPRc13Hs,11413
227
+ letta_nightly-0.6.0.dev20241205104308.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
228
+ letta_nightly-0.6.0.dev20241205104308.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
229
+ letta_nightly-0.6.0.dev20241205104308.dist-info/RECORD,,