letta-nightly 0.6.27.dev20250220104103__py3-none-any.whl → 0.6.29.dev20250221033538__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 (66) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +19 -2
  3. letta/client/client.py +2 -0
  4. letta/constants.py +2 -0
  5. letta/functions/schema_generator.py +6 -6
  6. letta/helpers/converters.py +153 -0
  7. letta/helpers/tool_rule_solver.py +11 -1
  8. letta/llm_api/anthropic.py +10 -5
  9. letta/llm_api/aws_bedrock.py +1 -1
  10. letta/llm_api/deepseek.py +303 -0
  11. letta/llm_api/helpers.py +20 -10
  12. letta/llm_api/llm_api_tools.py +85 -2
  13. letta/llm_api/openai.py +16 -1
  14. letta/local_llm/chat_completion_proxy.py +15 -2
  15. letta/local_llm/lmstudio/api.py +75 -1
  16. letta/orm/__init__.py +2 -0
  17. letta/orm/agent.py +11 -4
  18. letta/orm/custom_columns.py +31 -110
  19. letta/orm/identities_agents.py +13 -0
  20. letta/orm/identity.py +60 -0
  21. letta/orm/organization.py +2 -0
  22. letta/orm/sqlalchemy_base.py +4 -0
  23. letta/schemas/agent.py +11 -1
  24. letta/schemas/identity.py +67 -0
  25. letta/schemas/llm_config.py +2 -0
  26. letta/schemas/message.py +1 -1
  27. letta/schemas/openai/chat_completion_response.py +2 -0
  28. letta/schemas/providers.py +72 -1
  29. letta/schemas/tool_rule.py +9 -1
  30. letta/serialize_schemas/__init__.py +1 -0
  31. letta/serialize_schemas/agent.py +36 -0
  32. letta/serialize_schemas/base.py +12 -0
  33. letta/serialize_schemas/custom_fields.py +69 -0
  34. letta/serialize_schemas/message.py +15 -0
  35. letta/server/db.py +111 -0
  36. letta/server/rest_api/app.py +8 -0
  37. letta/server/rest_api/chat_completions_interface.py +45 -21
  38. letta/server/rest_api/interface.py +114 -9
  39. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +98 -24
  40. letta/server/rest_api/routers/v1/__init__.py +2 -0
  41. letta/server/rest_api/routers/v1/agents.py +14 -3
  42. letta/server/rest_api/routers/v1/identities.py +121 -0
  43. letta/server/rest_api/utils.py +183 -4
  44. letta/server/server.py +23 -117
  45. letta/services/agent_manager.py +53 -6
  46. letta/services/block_manager.py +1 -1
  47. letta/services/identity_manager.py +156 -0
  48. letta/services/job_manager.py +1 -1
  49. letta/services/message_manager.py +1 -1
  50. letta/services/organization_manager.py +1 -1
  51. letta/services/passage_manager.py +1 -1
  52. letta/services/provider_manager.py +1 -1
  53. letta/services/sandbox_config_manager.py +1 -1
  54. letta/services/source_manager.py +1 -1
  55. letta/services/step_manager.py +1 -1
  56. letta/services/tool_manager.py +1 -1
  57. letta/services/user_manager.py +1 -1
  58. letta/settings.py +3 -0
  59. letta/streaming_interface.py +6 -2
  60. letta/tracing.py +205 -0
  61. letta/utils.py +4 -0
  62. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/METADATA +9 -2
  63. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/RECORD +66 -52
  64. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/LICENSE +0 -0
  65. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/WHEEL +0 -0
  66. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/entry_points.txt +0 -0
letta/server/server.py CHANGED
@@ -18,6 +18,7 @@ import letta.server.utils as server_utils
18
18
  import letta.system as system
19
19
  from letta.agent import Agent, save_agent
20
20
  from letta.chat_only_agent import ChatOnlyAgent
21
+ from letta.config import LettaConfig
21
22
  from letta.data_sources.connectors import DataConnector, load_data
22
23
  from letta.helpers.datetime_helpers import get_utc_time
23
24
  from letta.helpers.json_helpers import json_dumps, json_loads
@@ -27,7 +28,6 @@ from letta.interface import AgentInterface # abstract
27
28
  from letta.interface import CLIInterface # for printing to terminal
28
29
  from letta.log import get_logger
29
30
  from letta.offline_memory_agent import OfflineMemoryAgent
30
- from letta.orm import Base
31
31
  from letta.orm.errors import NoResultFound
32
32
  from letta.schemas.agent import AgentState, AgentType, CreateAgent
33
33
  from letta.schemas.block import BlockUpdate
@@ -48,6 +48,7 @@ from letta.schemas.providers import (
48
48
  AnthropicBedrockProvider,
49
49
  AnthropicProvider,
50
50
  AzureProvider,
51
+ DeepSeekProvider,
51
52
  GoogleAIProvider,
52
53
  GoogleVertexProvider,
53
54
  GroqProvider,
@@ -70,6 +71,7 @@ from letta.server.rest_api.interface import StreamingServerInterface
70
71
  from letta.server.rest_api.utils import sse_async_generator
71
72
  from letta.services.agent_manager import AgentManager
72
73
  from letta.services.block_manager import BlockManager
74
+ from letta.services.identity_manager import IdentityManager
73
75
  from letta.services.job_manager import JobManager
74
76
  from letta.services.message_manager import MessageManager
75
77
  from letta.services.organization_manager import OrganizationManager
@@ -82,8 +84,11 @@ from letta.services.step_manager import StepManager
82
84
  from letta.services.tool_execution_sandbox import ToolExecutionSandbox
83
85
  from letta.services.tool_manager import ToolManager
84
86
  from letta.services.user_manager import UserManager
87
+ from letta.settings import model_settings, settings, tool_settings
88
+ from letta.tracing import trace_method
85
89
  from letta.utils import get_friendly_error_msg
86
90
 
91
+ config = LettaConfig.load()
87
92
  logger = get_logger(__name__)
88
93
 
89
94
 
@@ -145,118 +150,6 @@ class Server(object):
145
150
  raise NotImplementedError
146
151
 
147
152
 
148
- from contextlib import contextmanager
149
-
150
- from rich.console import Console
151
- from rich.panel import Panel
152
- from rich.text import Text
153
- from sqlalchemy import create_engine
154
- from sqlalchemy.orm import sessionmaker
155
-
156
- from letta.config import LettaConfig
157
-
158
- # NOTE: hack to see if single session management works
159
- from letta.settings import model_settings, settings, tool_settings
160
-
161
- config = LettaConfig.load()
162
-
163
-
164
- def print_sqlite_schema_error():
165
- """Print a formatted error message for SQLite schema issues"""
166
- console = Console()
167
- error_text = Text()
168
- error_text.append("Existing SQLite DB schema is invalid, and schema migrations are not supported for SQLite. ", style="bold red")
169
- error_text.append("To have migrations supported between Letta versions, please run Letta with Docker (", style="white")
170
- error_text.append("https://docs.letta.com/server/docker", style="blue underline")
171
- error_text.append(") or use Postgres by setting ", style="white")
172
- error_text.append("LETTA_PG_URI", style="yellow")
173
- error_text.append(".\n\n", style="white")
174
- error_text.append("If you wish to keep using SQLite, you can reset your database by removing the DB file with ", style="white")
175
- error_text.append("rm ~/.letta/sqlite.db", style="yellow")
176
- error_text.append(" or downgrade to your previous version of Letta.", style="white")
177
-
178
- console.print(Panel(error_text, border_style="red"))
179
-
180
-
181
- @contextmanager
182
- def db_error_handler():
183
- """Context manager for handling database errors"""
184
- try:
185
- yield
186
- except Exception as e:
187
- # Handle other SQLAlchemy errors
188
- print(e)
189
- print_sqlite_schema_error()
190
- # raise ValueError(f"SQLite DB error: {str(e)}")
191
- exit(1)
192
-
193
-
194
- if settings.letta_pg_uri_no_default:
195
- print("Creating postgres engine")
196
- config.recall_storage_type = "postgres"
197
- config.recall_storage_uri = settings.letta_pg_uri_no_default
198
- config.archival_storage_type = "postgres"
199
- config.archival_storage_uri = settings.letta_pg_uri_no_default
200
-
201
- # create engine
202
- engine = create_engine(
203
- settings.letta_pg_uri,
204
- pool_size=settings.pg_pool_size,
205
- max_overflow=settings.pg_max_overflow,
206
- pool_timeout=settings.pg_pool_timeout,
207
- pool_recycle=settings.pg_pool_recycle,
208
- echo=settings.pg_echo,
209
- )
210
- else:
211
- # TODO: don't rely on config storage
212
- engine_path = "sqlite:///" + os.path.join(config.recall_storage_path, "sqlite.db")
213
- logger.info("Creating sqlite engine " + engine_path)
214
-
215
- engine = create_engine(engine_path)
216
-
217
- # Store the original connect method
218
- original_connect = engine.connect
219
-
220
- def wrapped_connect(*args, **kwargs):
221
- with db_error_handler():
222
- # Get the connection
223
- connection = original_connect(*args, **kwargs)
224
-
225
- # Store the original execution method
226
- original_execute = connection.execute
227
-
228
- # Wrap the execute method of the connection
229
- def wrapped_execute(*args, **kwargs):
230
- with db_error_handler():
231
- return original_execute(*args, **kwargs)
232
-
233
- # Replace the connection's execute method
234
- connection.execute = wrapped_execute
235
-
236
- return connection
237
-
238
- # Replace the engine's connect method
239
- engine.connect = wrapped_connect
240
-
241
- Base.metadata.create_all(bind=engine)
242
-
243
- SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
244
-
245
-
246
- # Dependency
247
- def get_db():
248
- db = SessionLocal()
249
- try:
250
- yield db
251
- finally:
252
- db.close()
253
-
254
-
255
- from contextlib import contextmanager
256
-
257
- db_context = contextmanager(get_db)
258
-
259
-
260
153
  class SyncServer(Server):
261
154
  """Simple single-threaded / blocking server process"""
262
155
 
@@ -304,6 +197,7 @@ class SyncServer(Server):
304
197
  self.agent_manager = AgentManager()
305
198
  self.provider_manager = ProviderManager()
306
199
  self.step_manager = StepManager()
200
+ self.identity_manager = IdentityManager()
307
201
 
308
202
  # Managers that interface with parallelism
309
203
  self.per_agent_lock_manager = PerAgentLockManager()
@@ -415,6 +309,8 @@ class SyncServer(Server):
415
309
  else model_settings.lmstudio_base_url + "/v1"
416
310
  )
417
311
  self._enabled_providers.append(LMStudioOpenAIProvider(base_url=lmstudio_url))
312
+ if model_settings.deepseek_api_key:
313
+ self._enabled_providers.append(DeepSeekProvider(api_key=model_settings.deepseek_api_key))
418
314
 
419
315
  def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
420
316
  """Updated method to load agents from persisted storage"""
@@ -440,6 +336,7 @@ class SyncServer(Server):
440
336
  agent_id: str,
441
337
  input_messages: Union[Message, List[Message]],
442
338
  interface: Union[AgentInterface, None] = None, # needed to getting responses
339
+ put_inner_thoughts_first: bool = True,
443
340
  # timestamp: Optional[datetime],
444
341
  ) -> LettaUsageStatistics:
445
342
  """Send the input message through the agent"""
@@ -472,6 +369,7 @@ class SyncServer(Server):
472
369
  stream=token_streaming,
473
370
  skip_verify=True,
474
371
  metadata=metadata,
372
+ put_inner_thoughts_first=put_inner_thoughts_first,
475
373
  )
476
374
 
477
375
  except Exception as e:
@@ -729,6 +627,7 @@ class SyncServer(Server):
729
627
  wrap_system_message: bool = True,
730
628
  interface: Union[AgentInterface, ChatCompletionsStreamingInterface, None] = None, # needed to getting responses
731
629
  metadata: Optional[dict] = None, # Pass through metadata to interface
630
+ put_inner_thoughts_first: bool = True,
732
631
  ) -> LettaUsageStatistics:
733
632
  """Send a list of messages to the agent
734
633
 
@@ -779,7 +678,13 @@ class SyncServer(Server):
779
678
  interface.metadata = metadata
780
679
 
781
680
  # Run the agent state forward
782
- return self._step(actor=actor, agent_id=agent_id, input_messages=message_objects, interface=interface)
681
+ return self._step(
682
+ actor=actor,
683
+ agent_id=agent_id,
684
+ input_messages=message_objects,
685
+ interface=interface,
686
+ put_inner_thoughts_first=put_inner_thoughts_first,
687
+ )
783
688
 
784
689
  # @LockingServer.agent_lock_decorator
785
690
  def run_command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
@@ -1256,6 +1161,7 @@ class SyncServer(Server):
1256
1161
  actions = self.get_composio_client(api_key=api_key).actions.get(apps=[composio_app_name])
1257
1162
  return actions
1258
1163
 
1164
+ @trace_method("Send Message")
1259
1165
  async def send_message_to_agent(
1260
1166
  self,
1261
1167
  agent_id: str,
@@ -1273,7 +1179,6 @@ class SyncServer(Server):
1273
1179
  metadata: Optional[dict] = None,
1274
1180
  ) -> Union[StreamingResponse, LettaResponse]:
1275
1181
  """Split off into a separate function so that it can be imported in the /chat/completion proxy."""
1276
-
1277
1182
  # TODO: @charles is this the correct way to handle?
1278
1183
  include_final_message = True
1279
1184
 
@@ -1292,11 +1197,12 @@ class SyncServer(Server):
1292
1197
  # Disable token streaming if not OpenAI or Anthropic
1293
1198
  # TODO: cleanup this logic
1294
1199
  llm_config = letta_agent.agent_state.llm_config
1200
+ supports_token_streaming = ["openai", "anthropic", "deepseek"]
1295
1201
  if stream_tokens and (
1296
- llm_config.model_endpoint_type not in ["openai", "anthropic"] or "inference.memgpt.ai" in llm_config.model_endpoint
1202
+ llm_config.model_endpoint_type not in supports_token_streaming or "inference.memgpt.ai" in llm_config.model_endpoint
1297
1203
  ):
1298
1204
  warnings.warn(
1299
- f"Token streaming is only supported for models with type 'openai' or 'anthropic' in the model_endpoint: agent has endpoint type {llm_config.model_endpoint_type} and {llm_config.model_endpoint}. Setting stream_tokens to False."
1205
+ f"Token streaming is only supported for models with type {' or '.join(supports_token_streaming)} in the model_endpoint: agent has endpoint type {llm_config.model_endpoint_type} and {llm_config.model_endpoint}. Setting stream_tokens to False."
1300
1206
  )
1301
1207
  stream_tokens = False
1302
1208
 
@@ -11,6 +11,7 @@ from letta.log import get_logger
11
11
  from letta.orm import Agent as AgentModel
12
12
  from letta.orm import AgentPassage, AgentsTags
13
13
  from letta.orm import Block as BlockModel
14
+ from letta.orm import Identity as IdentityModel
14
15
  from letta.orm import Source as SourceModel
15
16
  from letta.orm import SourcePassage, SourcesAgents
16
17
  from letta.orm import Tool as ToolModel
@@ -27,8 +28,11 @@ from letta.schemas.message import MessageCreate
27
28
  from letta.schemas.passage import Passage as PydanticPassage
28
29
  from letta.schemas.source import Source as PydanticSource
29
30
  from letta.schemas.tool import Tool as PydanticTool
31
+ from letta.schemas.tool_rule import ContinueToolRule as PydanticContinueToolRule
32
+ from letta.schemas.tool_rule import TerminalToolRule as PydanticTerminalToolRule
30
33
  from letta.schemas.tool_rule import ToolRule as PydanticToolRule
31
34
  from letta.schemas.user import User as PydanticUser
35
+ from letta.serialize_schemas import SerializedAgentSchema
32
36
  from letta.services.block_manager import BlockManager
33
37
  from letta.services.helpers.agent_manager_helper import (
34
38
  _process_relationship,
@@ -39,6 +43,7 @@ from letta.services.helpers.agent_manager_helper import (
39
43
  initialize_message_sequence,
40
44
  package_initial_message_sequence,
41
45
  )
46
+ from letta.services.identity_manager import IdentityManager
42
47
  from letta.services.message_manager import MessageManager
43
48
  from letta.services.source_manager import SourceManager
44
49
  from letta.services.tool_manager import ToolManager
@@ -53,13 +58,14 @@ class AgentManager:
53
58
  """Manager class to handle business logic related to Agents."""
54
59
 
55
60
  def __init__(self):
56
- from letta.server.server import db_context
61
+ from letta.server.db import db_context
57
62
 
58
63
  self.session_maker = db_context
59
64
  self.block_manager = BlockManager()
60
65
  self.tool_manager = ToolManager()
61
66
  self.source_manager = SourceManager()
62
67
  self.message_manager = MessageManager()
68
+ self.identity_manager = IdentityManager()
63
69
 
64
70
  # ======================================================================================================================
65
71
  # Basic CRUD operations
@@ -75,10 +81,6 @@ class AgentManager:
75
81
  if not agent_create.llm_config or not agent_create.embedding_config:
76
82
  raise ValueError("llm_config and embedding_config are required")
77
83
 
78
- # Check tool rules are valid
79
- if agent_create.tool_rules:
80
- check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=agent_create.tool_rules)
81
-
82
84
  # create blocks (note: cannot be linked into the agent_id is created)
83
85
  block_ids = list(agent_create.block_ids or []) # Create a local copy to avoid modifying the original
84
86
  if agent_create.memory_blocks:
@@ -98,6 +100,25 @@ class AgentManager:
98
100
  # Remove duplicates
99
101
  tool_names = list(set(tool_names))
100
102
 
103
+ # add default tool rules
104
+ if agent_create.include_base_tool_rules:
105
+ if not agent_create.tool_rules:
106
+ tool_rules = []
107
+ else:
108
+ tool_rules = agent_create.tool_rules
109
+
110
+ # apply default tool rules
111
+ for tool_name in tool_names:
112
+ if tool_name == "send_message" or tool_name == "send_message_to_agent_async":
113
+ tool_rules.append(PydanticTerminalToolRule(tool_name=tool_name))
114
+ elif tool_name in BASE_TOOLS:
115
+ tool_rules.append(PydanticContinueToolRule(tool_name=tool_name))
116
+ else:
117
+ tool_rules = agent_create.tool_rules
118
+ # Check tool rules are valid
119
+ if agent_create.tool_rules:
120
+ check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=agent_create.tool_rules)
121
+
101
122
  tool_ids = agent_create.tool_ids or []
102
123
  for tool_name in tool_names:
103
124
  tool = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
@@ -117,9 +138,10 @@ class AgentManager:
117
138
  tool_ids=tool_ids,
118
139
  source_ids=agent_create.source_ids or [],
119
140
  tags=agent_create.tags or [],
141
+ identity_ids=agent_create.identity_ids or [],
120
142
  description=agent_create.description,
121
143
  metadata=agent_create.metadata,
122
- tool_rules=agent_create.tool_rules,
144
+ tool_rules=tool_rules,
123
145
  actor=actor,
124
146
  project_id=agent_create.project_id,
125
147
  template_id=agent_create.template_id,
@@ -181,6 +203,7 @@ class AgentManager:
181
203
  tool_ids: List[str],
182
204
  source_ids: List[str],
183
205
  tags: List[str],
206
+ identity_ids: List[str],
184
207
  description: Optional[str] = None,
185
208
  metadata: Optional[Dict] = None,
186
209
  tool_rules: Optional[List[PydanticToolRule]] = None,
@@ -214,6 +237,8 @@ class AgentManager:
214
237
  _process_relationship(session, new_agent, "sources", SourceModel, source_ids, replace=True)
215
238
  _process_relationship(session, new_agent, "core_memory", BlockModel, block_ids, replace=True)
216
239
  _process_tags(new_agent, tags, replace=True)
240
+ _process_relationship(session, new_agent, "identities", IdentityModel, identity_ids, replace=True)
241
+
217
242
  new_agent.create(session, actor=actor)
218
243
 
219
244
  # Convert to PydanticAgentState and return
@@ -286,6 +311,8 @@ class AgentManager:
286
311
  _process_relationship(session, agent, "core_memory", BlockModel, agent_update.block_ids, replace=True)
287
312
  if agent_update.tags is not None:
288
313
  _process_tags(agent, agent_update.tags, replace=True)
314
+ if agent_update.identity_ids is not None:
315
+ _process_relationship(session, agent, "identities", IdentityModel, agent_update.identity_ids, replace=True)
289
316
 
290
317
  # Commit and refresh the agent
291
318
  agent.update(session, actor=actor)
@@ -303,6 +330,7 @@ class AgentManager:
303
330
  tags: Optional[List[str]] = None,
304
331
  match_all_tags: bool = False,
305
332
  query_text: Optional[str] = None,
333
+ identifier_keys: Optional[List[str]] = None,
306
334
  **kwargs,
307
335
  ) -> List[PydanticAgentState]:
308
336
  """
@@ -318,6 +346,7 @@ class AgentManager:
318
346
  match_all_tags=match_all_tags,
319
347
  organization_id=actor.organization_id if actor else None,
320
348
  query_text=query_text,
349
+ identifier_keys=identifier_keys,
321
350
  **kwargs,
322
351
  )
323
352
 
@@ -355,6 +384,24 @@ class AgentManager:
355
384
  agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
356
385
  agent.hard_delete(session)
357
386
 
387
+ @enforce_types
388
+ def serialize(self, agent_id: str, actor: PydanticUser) -> dict:
389
+ with self.session_maker() as session:
390
+ # Retrieve the agent
391
+ agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
392
+ schema = SerializedAgentSchema(session=session)
393
+ return schema.dump(agent)
394
+
395
+ @enforce_types
396
+ def deserialize(self, serialized_agent: dict, actor: PydanticUser) -> PydanticAgentState:
397
+ # TODO: Use actor to override fields
398
+ with self.session_maker() as session:
399
+ schema = SerializedAgentSchema(session=session)
400
+ agent = schema.load(serialized_agent, session=session)
401
+ agent.organization_id = actor.organization_id
402
+ agent = agent.create(session, actor=actor)
403
+ return agent.to_pydantic()
404
+
358
405
  # ======================================================================================================================
359
406
  # Per Agent Environment Variable Management
360
407
  # ======================================================================================================================
@@ -16,7 +16,7 @@ class BlockManager:
16
16
 
17
17
  def __init__(self):
18
18
  # Fetching the db_context similarly as in ToolManager
19
- from letta.server.server import db_context
19
+ from letta.server.db import db_context
20
20
 
21
21
  self.session_maker = db_context
22
22
 
@@ -0,0 +1,156 @@
1
+ from typing import List, Optional
2
+
3
+ from fastapi import HTTPException
4
+ from sqlalchemy.exc import NoResultFound
5
+ from sqlalchemy.orm import Session
6
+
7
+ from letta.orm.agent import Agent as AgentModel
8
+ from letta.orm.identity import Identity as IdentityModel
9
+ from letta.schemas.identity import Identity as PydanticIdentity
10
+ from letta.schemas.identity import IdentityCreate, IdentityType, IdentityUpdate
11
+ from letta.schemas.user import User as PydanticUser
12
+ from letta.utils import enforce_types
13
+
14
+
15
+ class IdentityManager:
16
+
17
+ def __init__(self):
18
+ from letta.server.db import db_context
19
+
20
+ self.session_maker = db_context
21
+
22
+ @enforce_types
23
+ def list_identities(
24
+ self,
25
+ name: Optional[str] = None,
26
+ project_id: Optional[str] = None,
27
+ identifier_key: Optional[str] = None,
28
+ identity_type: Optional[IdentityType] = None,
29
+ before: Optional[str] = None,
30
+ after: Optional[str] = None,
31
+ limit: Optional[int] = 50,
32
+ actor: PydanticUser = None,
33
+ ) -> list[PydanticIdentity]:
34
+ with self.session_maker() as session:
35
+ filters = {"organization_id": actor.organization_id}
36
+ if project_id:
37
+ filters["project_id"] = project_id
38
+ if identifier_key:
39
+ filters["identifier_key"] = identifier_key
40
+ if identity_type:
41
+ filters["identity_type"] = identity_type
42
+ identities = IdentityModel.list(
43
+ db_session=session,
44
+ query_text=name,
45
+ before=before,
46
+ after=after,
47
+ limit=limit,
48
+ **filters,
49
+ )
50
+ return [identity.to_pydantic() for identity in identities]
51
+
52
+ @enforce_types
53
+ def get_identity(self, identity_id: str, actor: PydanticUser) -> PydanticIdentity:
54
+ with self.session_maker() as session:
55
+ identity = IdentityModel.read(db_session=session, identifier=identity_id, actor=actor)
56
+ return identity.to_pydantic()
57
+
58
+ @enforce_types
59
+ def create_identity(self, identity: IdentityCreate, actor: PydanticUser) -> PydanticIdentity:
60
+ with self.session_maker() as session:
61
+ new_identity = IdentityModel(**identity.model_dump(exclude={"agent_ids"}, exclude_unset=True))
62
+ new_identity.organization_id = actor.organization_id
63
+ self._process_agent_relationship(session=session, identity=new_identity, agent_ids=identity.agent_ids, allow_partial=False)
64
+ new_identity.create(session, actor=actor)
65
+ return new_identity.to_pydantic()
66
+
67
+ @enforce_types
68
+ def upsert_identity(self, identity: IdentityCreate, actor: PydanticUser) -> PydanticIdentity:
69
+ with self.session_maker() as session:
70
+ existing_identity = IdentityModel.read(
71
+ db_session=session,
72
+ identifier_key=identity.identifier_key,
73
+ project_id=identity.project_id,
74
+ organization_id=actor.organization_id,
75
+ actor=actor,
76
+ )
77
+
78
+ if existing_identity is None:
79
+ return self.create_identity(identity=identity, actor=actor)
80
+ else:
81
+ identity_update = IdentityUpdate(name=identity.name, identity_type=identity.identity_type, agent_ids=identity.agent_ids)
82
+ return self._update_identity(
83
+ session=session, existing_identity=existing_identity, identity=identity_update, actor=actor, replace=True
84
+ )
85
+
86
+ @enforce_types
87
+ def update_identity(self, identity_id: str, identity: IdentityUpdate, actor: PydanticUser, replace: bool = False) -> PydanticIdentity:
88
+ with self.session_maker() as session:
89
+ try:
90
+ existing_identity = IdentityModel.read(db_session=session, identifier=identity_id, actor=actor)
91
+ except NoResultFound:
92
+ raise HTTPException(status_code=404, detail="Identity not found")
93
+ if existing_identity.organization_id != actor.organization_id:
94
+ raise HTTPException(status_code=403, detail="Forbidden")
95
+
96
+ return self._update_identity(
97
+ session=session, existing_identity=existing_identity, identity=identity, actor=actor, replace=replace
98
+ )
99
+
100
+ def _update_identity(
101
+ self,
102
+ session: Session,
103
+ existing_identity: IdentityModel,
104
+ identity: IdentityUpdate,
105
+ actor: PydanticUser,
106
+ replace: bool = False,
107
+ ) -> PydanticIdentity:
108
+ if identity.identifier_key is not None:
109
+ existing_identity.identifier_key = identity.identifier_key
110
+ if identity.name is not None:
111
+ existing_identity.name = identity.name
112
+ if identity.identity_type is not None:
113
+ existing_identity.identity_type = identity.identity_type
114
+
115
+ self._process_agent_relationship(
116
+ session=session, identity=existing_identity, agent_ids=identity.agent_ids, allow_partial=False, replace=replace
117
+ )
118
+ existing_identity.update(session, actor=actor)
119
+ return existing_identity.to_pydantic()
120
+
121
+ @enforce_types
122
+ def delete_identity(self, identity_id: str, actor: PydanticUser) -> None:
123
+ with self.session_maker() as session:
124
+ identity = IdentityModel.read(db_session=session, identifier=identity_id)
125
+ if identity is None:
126
+ raise HTTPException(status_code=404, detail="Identity not found")
127
+ if identity.organization_id != actor.organization_id:
128
+ raise HTTPException(status_code=403, detail="Forbidden")
129
+ session.delete(identity)
130
+ session.commit()
131
+
132
+ def _process_agent_relationship(
133
+ self, session: Session, identity: IdentityModel, agent_ids: List[str], allow_partial=False, replace=True
134
+ ):
135
+ current_relationship = getattr(identity, "agents", [])
136
+ if not agent_ids:
137
+ if replace:
138
+ setattr(identity, "agents", [])
139
+ return
140
+
141
+ # Retrieve models for the provided IDs
142
+ found_items = session.query(AgentModel).filter(AgentModel.id.in_(agent_ids)).all()
143
+
144
+ # Validate all items are found if allow_partial is False
145
+ if not allow_partial and len(found_items) != len(agent_ids):
146
+ missing = set(agent_ids) - {item.id for item in found_items}
147
+ raise NoResultFound(f"Items not found in agents: {missing}")
148
+
149
+ if replace:
150
+ # Replace the relationship
151
+ setattr(identity, "agents", found_items)
152
+ else:
153
+ # Extend the relationship (only add new items)
154
+ current_ids = {item.id for item in current_relationship}
155
+ new_items = [item for item in found_items if item.id not in current_ids]
156
+ current_relationship.extend(new_items)
@@ -29,7 +29,7 @@ class JobManager:
29
29
 
30
30
  def __init__(self):
31
31
  # Fetching the db_context similarly as in OrganizationManager
32
- from letta.server.server import db_context
32
+ from letta.server.db import db_context
33
33
 
34
34
  self.session_maker = db_context
35
35
 
@@ -16,7 +16,7 @@ class MessageManager:
16
16
  """Manager class to handle business logic related to Messages."""
17
17
 
18
18
  def __init__(self):
19
- from letta.server.server import db_context
19
+ from letta.server.db import db_context
20
20
 
21
21
  self.session_maker = db_context
22
22
 
@@ -16,7 +16,7 @@ class OrganizationManager:
16
16
  # TODO: Please refactor this out
17
17
  # I am currently working on a ORM refactor and would like to make a more minimal set of changes
18
18
  # - Matt
19
- from letta.server.server import db_context
19
+ from letta.server.db import db_context
20
20
 
21
21
  self.session_maker = db_context
22
22
 
@@ -16,7 +16,7 @@ class PassageManager:
16
16
  """Manager class to handle business logic related to Passages."""
17
17
 
18
18
  def __init__(self):
19
- from letta.server.server import db_context
19
+ from letta.server.db import db_context
20
20
 
21
21
  self.session_maker = db_context
22
22
 
@@ -10,7 +10,7 @@ from letta.utils import enforce_types
10
10
  class ProviderManager:
11
11
 
12
12
  def __init__(self):
13
- from letta.server.server import db_context
13
+ from letta.server.db import db_context
14
14
 
15
15
  self.session_maker = db_context
16
16
 
@@ -20,7 +20,7 @@ class SandboxConfigManager:
20
20
  """Manager class to handle business logic related to SandboxConfig and SandboxEnvironmentVariable."""
21
21
 
22
22
  def __init__(self):
23
- from letta.server.server import db_context
23
+ from letta.server.db import db_context
24
24
 
25
25
  self.session_maker = db_context
26
26
 
@@ -15,7 +15,7 @@ class SourceManager:
15
15
  """Manager class to handle business logic related to Sources."""
16
16
 
17
17
  def __init__(self):
18
- from letta.server.server import db_context
18
+ from letta.server.db import db_context
19
19
 
20
20
  self.session_maker = db_context
21
21
 
@@ -17,7 +17,7 @@ from letta.utils import enforce_types
17
17
  class StepManager:
18
18
 
19
19
  def __init__(self):
20
- from letta.server.server import db_context
20
+ from letta.server.db import db_context
21
21
 
22
22
  self.session_maker = db_context
23
23
 
@@ -31,7 +31,7 @@ class ToolManager:
31
31
 
32
32
  def __init__(self):
33
33
  # Fetching the db_context similarly as in OrganizationManager
34
- from letta.server.server import db_context
34
+ from letta.server.db import db_context
35
35
 
36
36
  self.session_maker = db_context
37
37
 
@@ -17,7 +17,7 @@ class UserManager:
17
17
 
18
18
  def __init__(self):
19
19
  # Fetching the db_context similarly as in OrganizationManager
20
- from letta.server.server import db_context
20
+ from letta.server.db import db_context
21
21
 
22
22
  self.session_maker = db_context
23
23
 
letta/settings.py CHANGED
@@ -60,6 +60,9 @@ class ModelSettings(BaseSettings):
60
60
  openai_api_key: Optional[str] = None
61
61
  openai_api_base: str = "https://api.openai.com/v1"
62
62
 
63
+ # deepseek
64
+ deepseek_api_key: Optional[str] = None
65
+
63
66
  # groq
64
67
  groq_api_key: Optional[str] = None
65
68