letta-nightly 0.6.2.dev20241210104242__py3-none-any.whl → 0.6.3.dev20241211050151__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (44) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +32 -43
  3. letta/agent_store/db.py +12 -54
  4. letta/agent_store/storage.py +10 -9
  5. letta/cli/cli.py +1 -0
  6. letta/client/client.py +3 -2
  7. letta/config.py +2 -2
  8. letta/data_sources/connectors.py +4 -3
  9. letta/embeddings.py +29 -9
  10. letta/functions/function_sets/base.py +36 -11
  11. letta/metadata.py +13 -2
  12. letta/o1_agent.py +2 -3
  13. letta/offline_memory_agent.py +2 -1
  14. letta/orm/__init__.py +1 -0
  15. letta/orm/file.py +1 -0
  16. letta/orm/mixins.py +12 -2
  17. letta/orm/organization.py +3 -0
  18. letta/orm/passage.py +72 -0
  19. letta/orm/sqlalchemy_base.py +36 -7
  20. letta/orm/sqlite_functions.py +140 -0
  21. letta/orm/user.py +1 -1
  22. letta/schemas/agent.py +4 -3
  23. letta/schemas/letta_message.py +5 -1
  24. letta/schemas/letta_request.py +3 -3
  25. letta/schemas/passage.py +6 -4
  26. letta/schemas/sandbox_config.py +1 -0
  27. letta/schemas/tool_rule.py +0 -3
  28. letta/server/rest_api/app.py +34 -12
  29. letta/server/rest_api/routers/v1/agents.py +19 -6
  30. letta/server/server.py +182 -43
  31. letta/server/static_files/assets/{index-4848e3d7.js → index-048c9598.js} +1 -1
  32. letta/server/static_files/assets/{index-43ab4d62.css → index-0e31b727.css} +1 -1
  33. letta/server/static_files/index.html +2 -2
  34. letta/services/passage_manager.py +225 -0
  35. letta/services/source_manager.py +2 -1
  36. letta/services/tool_execution_sandbox.py +18 -6
  37. letta/settings.py +2 -0
  38. letta_nightly-0.6.3.dev20241211050151.dist-info/METADATA +375 -0
  39. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/RECORD +42 -40
  40. letta/agent_store/chroma.py +0 -297
  41. letta_nightly-0.6.2.dev20241210104242.dist-info/METADATA +0 -212
  42. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/LICENSE +0 -0
  43. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/WHEEL +0 -0
  44. {letta_nightly-0.6.2.dev20241210104242.dist-info → letta_nightly-0.6.3.dev20241211050151.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,225 @@
1
+ from typing import List, Optional, Dict, Tuple
2
+ from letta.constants import MAX_EMBEDDING_DIM
3
+ from datetime import datetime
4
+ import numpy as np
5
+
6
+ from letta.orm.errors import NoResultFound
7
+ from letta.utils import enforce_types
8
+
9
+ from letta.embeddings import embedding_model, parse_and_chunk_text
10
+ from letta.schemas.embedding_config import EmbeddingConfig
11
+
12
+ from letta.orm.passage import Passage as PassageModel
13
+ from letta.orm.sqlalchemy_base import AccessType
14
+ from letta.schemas.agent import AgentState
15
+ from letta.schemas.passage import Passage as PydanticPassage
16
+ from letta.schemas.user import User as PydanticUser
17
+
18
+ class PassageManager:
19
+ """Manager class to handle business logic related to Passages."""
20
+
21
+ def __init__(self):
22
+ from letta.server.server import db_context
23
+ self.session_maker = db_context
24
+
25
+ @enforce_types
26
+ def get_passage_by_id(self, passage_id: str, actor: PydanticUser) -> Optional[PydanticPassage]:
27
+ """Fetch a passage by ID."""
28
+ with self.session_maker() as session:
29
+ try:
30
+ passage = PassageModel.read(db_session=session, identifier=passage_id, actor=actor)
31
+ return passage.to_pydantic()
32
+ except NoResultFound:
33
+ return None
34
+
35
+ @enforce_types
36
+ def create_passage(self, pydantic_passage: PydanticPassage, actor: PydanticUser) -> PydanticPassage:
37
+ """Create a new passage."""
38
+ with self.session_maker() as session:
39
+ passage = PassageModel(**pydantic_passage.model_dump())
40
+ passage.create(session, actor=actor)
41
+ return passage.to_pydantic()
42
+
43
+ @enforce_types
44
+ def create_many_passages(self, passages: List[PydanticPassage], actor: PydanticUser) -> List[PydanticPassage]:
45
+ """Create multiple passages."""
46
+ return [self.create_passage(p, actor) for p in passages]
47
+
48
+ @enforce_types
49
+ def insert_passage(self,
50
+ agent_state: AgentState,
51
+ agent_id: str,
52
+ text: str,
53
+ actor: PydanticUser,
54
+ return_ids: bool = False
55
+ ) -> List[PydanticPassage]:
56
+ """ Insert passage(s) into archival memory """
57
+
58
+ embedding_chunk_size = agent_state.embedding_config.embedding_chunk_size
59
+ embed_model = embedding_model(agent_state.embedding_config)
60
+
61
+ passages = []
62
+
63
+ try:
64
+ # breakup string into passages
65
+ for text in parse_and_chunk_text(text, embedding_chunk_size):
66
+ embedding = embed_model.get_text_embedding(text)
67
+ if isinstance(embedding, dict):
68
+ try:
69
+ embedding = embedding["data"][0]["embedding"]
70
+ except (KeyError, IndexError):
71
+ # TODO as a fallback, see if we can find any lists in the payload
72
+ raise TypeError(
73
+ f"Got back an unexpected payload from text embedding function, type={type(embedding)}, value={embedding}"
74
+ )
75
+ passage = self.create_passage(
76
+ PydanticPassage(
77
+ organization_id=actor.organization_id,
78
+ agent_id=agent_id,
79
+ text=text,
80
+ embedding=embedding,
81
+ embedding_config=agent_state.embedding_config
82
+ ),
83
+ actor=actor
84
+ )
85
+ passages.append(passage)
86
+
87
+ ids = [str(p.id) for p in passages]
88
+
89
+ if return_ids:
90
+ return ids
91
+
92
+ return passages
93
+
94
+ except Exception as e:
95
+ raise e
96
+
97
+ @enforce_types
98
+ def update_passage_by_id(self, passage_id: str, passage: PydanticPassage, actor: PydanticUser, **kwargs) -> Optional[PydanticPassage]:
99
+ """Update a passage."""
100
+ if not passage_id:
101
+ raise ValueError("Passage ID must be provided.")
102
+
103
+ with self.session_maker() as session:
104
+ try:
105
+ # Fetch existing message from database
106
+ curr_passage = PassageModel.read(
107
+ db_session=session,
108
+ identifier=passage_id,
109
+ actor=actor,
110
+ )
111
+ if not curr_passage:
112
+ raise ValueError(f"Passage with id {passage_id} does not exist.")
113
+
114
+ # Update the database record with values from the provided record
115
+ update_data = passage.model_dump(exclude_unset=True, exclude_none=True)
116
+ for key, value in update_data.items():
117
+ setattr(curr_passage, key, value)
118
+
119
+ # Commit changes
120
+ curr_passage.update(session, actor=actor)
121
+ return curr_passage.to_pydantic()
122
+ except NoResultFound:
123
+ return None
124
+
125
+ @enforce_types
126
+ def delete_passage_by_id(self, passage_id: str, actor: PydanticUser) -> bool:
127
+ """Delete a passage."""
128
+ if not passage_id:
129
+ raise ValueError("Passage ID must be provided.")
130
+
131
+ with self.session_maker() as session:
132
+ try:
133
+ passage = PassageModel.read(db_session=session, identifier=passage_id, actor=actor)
134
+ passage.hard_delete(session, actor=actor)
135
+ except NoResultFound:
136
+ raise ValueError(f"Passage with id {passage_id} not found.")
137
+
138
+ @enforce_types
139
+ def list_passages(self,
140
+ actor : PydanticUser,
141
+ agent_id : Optional[str] = None,
142
+ file_id : Optional[str] = None,
143
+ cursor : Optional[str] = None,
144
+ limit : Optional[int] = 50,
145
+ query_text : Optional[str] = None,
146
+ start_date : Optional[datetime] = None,
147
+ end_date : Optional[datetime] = None,
148
+ source_id : Optional[str] = None,
149
+ embed_query : bool = False,
150
+ embedding_config: Optional[EmbeddingConfig] = None
151
+ ) -> List[PydanticPassage]:
152
+ """List passages with pagination."""
153
+ with self.session_maker() as session:
154
+ filters = {"organization_id": actor.organization_id}
155
+ if agent_id:
156
+ filters["agent_id"] = agent_id
157
+ if file_id:
158
+ filters["file_id"] = file_id
159
+ if source_id:
160
+ filters["source_id"] = source_id
161
+
162
+ embedded_text = None
163
+ if embed_query:
164
+ assert embedding_config is not None
165
+
166
+ # Embed the text
167
+ embedded_text = embedding_model(embedding_config).get_text_embedding(query_text)
168
+
169
+ # Pad the embedding with zeros
170
+ embedded_text = np.array(embedded_text)
171
+ embedded_text = np.pad(embedded_text, (0, MAX_EMBEDDING_DIM - embedded_text.shape[0]), mode="constant").tolist()
172
+
173
+ results = PassageModel.list(
174
+ db_session=session,
175
+ cursor=cursor,
176
+ start_date=start_date,
177
+ end_date=end_date,
178
+ limit=limit,
179
+ query_text=query_text if not embedded_text else None,
180
+ query_embedding=embedded_text,
181
+ **filters
182
+ )
183
+ return [p.to_pydantic() for p in results]
184
+
185
+ @enforce_types
186
+ def size(
187
+ self,
188
+ actor : PydanticUser,
189
+ agent_id : Optional[str] = None,
190
+ **kwargs
191
+ ) -> int:
192
+ """Get the total count of messages with optional filters.
193
+
194
+ Args:
195
+ actor : The user requesting the count
196
+ agent_id: The agent ID
197
+ """
198
+ with self.session_maker() as session:
199
+ return PassageModel.size(db_session=session, actor=actor, agent_id=agent_id, **kwargs)
200
+
201
+ def delete_passages(self,
202
+ actor: PydanticUser,
203
+ agent_id: Optional[str] = None,
204
+ file_id: Optional[str] = None,
205
+ start_date: Optional[datetime] = None,
206
+ end_date: Optional[datetime] = None,
207
+ limit: Optional[int] = 50,
208
+ cursor: Optional[str] = None,
209
+ query_text: Optional[str] = None,
210
+ source_id: Optional[str] = None
211
+ ) -> bool:
212
+
213
+ passages = self.list_passages(
214
+ actor=actor,
215
+ agent_id=agent_id,
216
+ file_id=file_id,
217
+ cursor=cursor,
218
+ limit=limit,
219
+ start_date=start_date,
220
+ end_date=end_date,
221
+ query_text=query_text,
222
+ source_id=source_id)
223
+
224
+ for passage in passages:
225
+ self.delete_passage_by_id(passage_id=passage.id, actor=actor)
@@ -64,7 +64,7 @@ class SourceManager:
64
64
  return source.to_pydantic()
65
65
 
66
66
  @enforce_types
67
- def list_sources(self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticSource]:
67
+ def list_sources(self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50, **kwargs) -> List[PydanticSource]:
68
68
  """List all sources with optional pagination."""
69
69
  with self.session_maker() as session:
70
70
  sources = SourceModel.list(
@@ -72,6 +72,7 @@ class SourceManager:
72
72
  cursor=cursor,
73
73
  limit=limit,
74
74
  organization_id=actor.organization_id,
75
+ **kwargs,
75
76
  )
76
77
  return [source.to_pydantic() for source in sources]
77
78
 
@@ -127,11 +127,12 @@ class ToolExecutionSandbox:
127
127
 
128
128
  # Save the old stdout
129
129
  old_stdout = sys.stdout
130
+ old_stderr = sys.stderr
130
131
  try:
131
132
  if local_configs.use_venv:
132
133
  return self.run_local_dir_sandbox_venv(sbx_config, env, temp_file_path)
133
134
  else:
134
- return self.run_local_dir_sandbox_runpy(sbx_config, env_vars, temp_file_path, old_stdout)
135
+ return self.run_local_dir_sandbox_runpy(sbx_config, env_vars, temp_file_path, old_stdout, old_stderr)
135
136
  except Exception as e:
136
137
  logger.error(f"Executing tool {self.tool_name} has an unexpected error: {e}")
137
138
  logger.error(f"Logging out tool {self.tool_name} auto-generated code for debugging: \n\n{code}")
@@ -139,6 +140,7 @@ class ToolExecutionSandbox:
139
140
  finally:
140
141
  # Clean up the temp file and restore stdout
141
142
  sys.stdout = old_stdout
143
+ sys.stderr = old_stderr
142
144
  os.remove(temp_file_path)
143
145
 
144
146
  def run_local_dir_sandbox_venv(self, sbx_config: SandboxConfig, env: Dict[str, str], temp_file_path: str) -> SandboxRunResult:
@@ -201,7 +203,11 @@ class ToolExecutionSandbox:
201
203
  func_result, stdout = self.parse_out_function_results_markers(result.stdout)
202
204
  func_return, agent_state = self.parse_best_effort(func_result)
203
205
  return SandboxRunResult(
204
- func_return=func_return, agent_state=agent_state, stdout=[stdout], sandbox_config_fingerprint=sbx_config.fingerprint()
206
+ func_return=func_return,
207
+ agent_state=agent_state,
208
+ stdout=[stdout],
209
+ stderr=[result.stderr],
210
+ sandbox_config_fingerprint=sbx_config.fingerprint(),
205
211
  )
206
212
  except subprocess.TimeoutExpired:
207
213
  raise TimeoutError(f"Executing tool {self.tool_name} has timed out.")
@@ -213,11 +219,13 @@ class ToolExecutionSandbox:
213
219
  raise e
214
220
 
215
221
  def run_local_dir_sandbox_runpy(
216
- self, sbx_config: SandboxConfig, env_vars: Dict[str, str], temp_file_path: str, old_stdout: TextIO
222
+ self, sbx_config: SandboxConfig, env_vars: Dict[str, str], temp_file_path: str, old_stdout: TextIO, old_stderr: TextIO
217
223
  ) -> SandboxRunResult:
218
- # Redirect stdout to capture script output
224
+ # Redirect stdout and stderr to capture script output
219
225
  captured_stdout = io.StringIO()
226
+ captured_stderr = io.StringIO()
220
227
  sys.stdout = captured_stdout
228
+ sys.stderr = captured_stderr
221
229
 
222
230
  # Execute the temp file
223
231
  with self.temporary_env_vars(env_vars):
@@ -227,14 +235,17 @@ class ToolExecutionSandbox:
227
235
  func_result = result.get(self.LOCAL_SANDBOX_RESULT_VAR_NAME)
228
236
  func_return, agent_state = self.parse_best_effort(func_result)
229
237
 
230
- # Restore stdout and collect captured output
238
+ # Restore stdout and stderr and collect captured output
231
239
  sys.stdout = old_stdout
240
+ sys.stderr = old_stderr
232
241
  stdout_output = captured_stdout.getvalue()
242
+ stderr_output = captured_stderr.getvalue()
233
243
 
234
244
  return SandboxRunResult(
235
245
  func_return=func_return,
236
246
  agent_state=agent_state,
237
247
  stdout=[stdout_output],
248
+ stderr=[stderr_output],
238
249
  sandbox_config_fingerprint=sbx_config.fingerprint(),
239
250
  )
240
251
 
@@ -294,7 +305,8 @@ class ToolExecutionSandbox:
294
305
  return SandboxRunResult(
295
306
  func_return=func_return,
296
307
  agent_state=agent_state,
297
- stdout=execution.logs.stdout + execution.logs.stderr,
308
+ stdout=execution.logs.stdout,
309
+ stderr=execution.logs.stderr,
298
310
  sandbox_config_fingerprint=sbx_config.fingerprint(),
299
311
  )
300
312
 
letta/settings.py CHANGED
@@ -17,6 +17,8 @@ class ToolSettings(BaseSettings):
17
17
 
18
18
  class ModelSettings(BaseSettings):
19
19
 
20
+ model_config = SettingsConfigDict(env_file='.env')
21
+
20
22
  # env_prefix='my_prefix_'
21
23
 
22
24
  # when we use /completions APIs (instead of /chat/completions), we need to specify a model wrapper