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.
- letta/__init__.py +1 -1
- letta/agent.py +19 -2
- letta/client/client.py +2 -0
- letta/constants.py +2 -0
- letta/functions/schema_generator.py +6 -6
- letta/helpers/converters.py +153 -0
- letta/helpers/tool_rule_solver.py +11 -1
- letta/llm_api/anthropic.py +10 -5
- letta/llm_api/aws_bedrock.py +1 -1
- letta/llm_api/deepseek.py +303 -0
- letta/llm_api/helpers.py +20 -10
- letta/llm_api/llm_api_tools.py +85 -2
- letta/llm_api/openai.py +16 -1
- letta/local_llm/chat_completion_proxy.py +15 -2
- letta/local_llm/lmstudio/api.py +75 -1
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +11 -4
- letta/orm/custom_columns.py +31 -110
- letta/orm/identities_agents.py +13 -0
- letta/orm/identity.py +60 -0
- letta/orm/organization.py +2 -0
- letta/orm/sqlalchemy_base.py +4 -0
- letta/schemas/agent.py +11 -1
- letta/schemas/identity.py +67 -0
- letta/schemas/llm_config.py +2 -0
- letta/schemas/message.py +1 -1
- letta/schemas/openai/chat_completion_response.py +2 -0
- letta/schemas/providers.py +72 -1
- letta/schemas/tool_rule.py +9 -1
- letta/serialize_schemas/__init__.py +1 -0
- letta/serialize_schemas/agent.py +36 -0
- letta/serialize_schemas/base.py +12 -0
- letta/serialize_schemas/custom_fields.py +69 -0
- letta/serialize_schemas/message.py +15 -0
- letta/server/db.py +111 -0
- letta/server/rest_api/app.py +8 -0
- letta/server/rest_api/chat_completions_interface.py +45 -21
- letta/server/rest_api/interface.py +114 -9
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +98 -24
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +14 -3
- letta/server/rest_api/routers/v1/identities.py +121 -0
- letta/server/rest_api/utils.py +183 -4
- letta/server/server.py +23 -117
- letta/services/agent_manager.py +53 -6
- letta/services/block_manager.py +1 -1
- letta/services/identity_manager.py +156 -0
- letta/services/job_manager.py +1 -1
- letta/services/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +1 -1
- letta/services/provider_manager.py +1 -1
- letta/services/sandbox_config_manager.py +1 -1
- letta/services/source_manager.py +1 -1
- letta/services/step_manager.py +1 -1
- letta/services/tool_manager.py +1 -1
- letta/services/user_manager.py +1 -1
- letta/settings.py +3 -0
- letta/streaming_interface.py +6 -2
- letta/tracing.py +205 -0
- letta/utils.py +4 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/METADATA +9 -2
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/RECORD +66 -52
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/WHEEL +0 -0
- {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(
|
|
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
|
|
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 '
|
|
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
|
|
letta/services/agent_manager.py
CHANGED
|
@@ -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.
|
|
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=
|
|
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
|
# ======================================================================================================================
|
letta/services/block_manager.py
CHANGED
|
@@ -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)
|
letta/services/job_manager.py
CHANGED
|
@@ -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.
|
|
19
|
+
from letta.server.db import db_context
|
|
20
20
|
|
|
21
21
|
self.session_maker = db_context
|
|
22
22
|
|
|
@@ -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.
|
|
23
|
+
from letta.server.db import db_context
|
|
24
24
|
|
|
25
25
|
self.session_maker = db_context
|
|
26
26
|
|
letta/services/source_manager.py
CHANGED
letta/services/step_manager.py
CHANGED
letta/services/tool_manager.py
CHANGED
letta/services/user_manager.py
CHANGED
letta/settings.py
CHANGED