letta-nightly 0.11.7.dev20251006104136__py3-none-any.whl → 0.11.7.dev20251008104128__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.
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +899 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +126 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +123 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +1 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +504 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +86 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +126 -85
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +85 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +301 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
letta/server/server.py
CHANGED
@@ -10,19 +10,16 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
10
10
|
|
11
11
|
import httpx
|
12
12
|
from anthropic import AsyncAnthropic
|
13
|
-
from composio.client import Composio
|
14
|
-
from composio.client.collections import ActionModel, AppModel
|
15
13
|
from fastapi import HTTPException
|
16
14
|
from fastapi.responses import StreamingResponse
|
17
15
|
|
18
16
|
import letta.constants as constants
|
19
17
|
import letta.server.utils as server_utils
|
20
18
|
import letta.system as system
|
21
|
-
from letta.agent import Agent, save_agent
|
22
19
|
from letta.config import LettaConfig
|
23
20
|
from letta.constants import LETTA_TOOL_EXECUTION_DIR
|
24
21
|
from letta.data_sources.connectors import DataConnector, load_data
|
25
|
-
from letta.errors import HandleNotFoundError
|
22
|
+
from letta.errors import HandleNotFoundError, LettaInvalidArgumentError, LettaMCPConnectionError, LettaMCPTimeoutError
|
26
23
|
from letta.functions.mcp_client.types import MCPServerType, MCPTool, MCPToolHealth, SSEServerConfig, StdioServerConfig
|
27
24
|
from letta.functions.schema_validator import validate_complete_json_schema
|
28
25
|
from letta.groups.helpers import load_multi_agent
|
@@ -68,6 +65,7 @@ from letta.schemas.providers import (
|
|
68
65
|
LMStudioOpenAIProvider,
|
69
66
|
OllamaProvider,
|
70
67
|
OpenAIProvider,
|
68
|
+
OpenRouterProvider,
|
71
69
|
Provider,
|
72
70
|
TogetherProvider,
|
73
71
|
VLLMProvider,
|
@@ -100,6 +98,7 @@ from letta.services.message_manager import MessageManager
|
|
100
98
|
from letta.services.organization_manager import OrganizationManager
|
101
99
|
from letta.services.passage_manager import PassageManager
|
102
100
|
from letta.services.provider_manager import ProviderManager
|
101
|
+
from letta.services.run_manager import RunManager
|
103
102
|
from letta.services.sandbox_config_manager import SandboxConfigManager
|
104
103
|
from letta.services.source_manager import SourceManager
|
105
104
|
from letta.services.step_manager import StepManager
|
@@ -115,65 +114,7 @@ config = LettaConfig.load()
|
|
115
114
|
logger = get_logger(__name__)
|
116
115
|
|
117
116
|
|
118
|
-
class
|
119
|
-
"""Abstract server class that supports multi-agent multi-user"""
|
120
|
-
|
121
|
-
@abstractmethod
|
122
|
-
def list_agents(self, user_id: str) -> dict:
|
123
|
-
"""List all available agents to a user"""
|
124
|
-
raise NotImplementedError
|
125
|
-
|
126
|
-
@abstractmethod
|
127
|
-
def get_agent_memory(self, user_id: str, agent_id: str) -> dict:
|
128
|
-
"""Return the memory of an agent (core memory + non-core statistics)"""
|
129
|
-
raise NotImplementedError
|
130
|
-
|
131
|
-
@abstractmethod
|
132
|
-
def get_server_config(self, user_id: str) -> dict:
|
133
|
-
"""Return the base config"""
|
134
|
-
raise NotImplementedError
|
135
|
-
|
136
|
-
@abstractmethod
|
137
|
-
def update_agent_core_memory(self, user_id: str, agent_id: str, label: str, actor: User) -> Memory:
|
138
|
-
"""Update the agents core memory block, return the new state"""
|
139
|
-
raise NotImplementedError
|
140
|
-
|
141
|
-
@abstractmethod
|
142
|
-
def create_agent(
|
143
|
-
self,
|
144
|
-
request: CreateAgent,
|
145
|
-
actor: User,
|
146
|
-
# interface
|
147
|
-
interface: Union[AgentInterface, None] = None,
|
148
|
-
) -> AgentState:
|
149
|
-
"""Create a new agent using a config"""
|
150
|
-
raise NotImplementedError
|
151
|
-
|
152
|
-
@abstractmethod
|
153
|
-
def user_message(self, user_id: str, agent_id: str, message: str) -> None:
|
154
|
-
"""Process a message from the user, internally calls step"""
|
155
|
-
raise NotImplementedError
|
156
|
-
|
157
|
-
@abstractmethod
|
158
|
-
def system_message(self, user_id: str, agent_id: str, message: str) -> None:
|
159
|
-
"""Process a message from the system, internally calls step"""
|
160
|
-
raise NotImplementedError
|
161
|
-
|
162
|
-
@abstractmethod
|
163
|
-
def send_messages(self, user_id: str, agent_id: str, input_messages: List[MessageCreate]) -> None:
|
164
|
-
"""Send a list of messages to the agent"""
|
165
|
-
raise NotImplementedError
|
166
|
-
|
167
|
-
@abstractmethod
|
168
|
-
def run_command(self, user_id: str, agent_id: str, command: str) -> Union[str, None]:
|
169
|
-
"""Run a command on the agent, e.g. /memory
|
170
|
-
|
171
|
-
May return a string with a message generated by the command
|
172
|
-
"""
|
173
|
-
raise NotImplementedError
|
174
|
-
|
175
|
-
|
176
|
-
class SyncServer(Server):
|
117
|
+
class SyncServer(object):
|
177
118
|
"""Simple single-threaded / blocking server process"""
|
178
119
|
|
179
120
|
def __init__(
|
@@ -218,6 +159,7 @@ class SyncServer(Server):
|
|
218
159
|
self.sandbox_config_manager = SandboxConfigManager()
|
219
160
|
self.message_manager = MessageManager()
|
220
161
|
self.job_manager = JobManager()
|
162
|
+
self.run_manager = RunManager()
|
221
163
|
self.agent_manager = AgentManager()
|
222
164
|
self.archive_manager = ArchiveManager()
|
223
165
|
self.provider_manager = ProviderManager()
|
@@ -246,58 +188,17 @@ class SyncServer(Server):
|
|
246
188
|
limits = httpx.Limits(max_connections=100, max_keepalive_connections=80, keepalive_expiry=300)
|
247
189
|
self.httpx_client = httpx.AsyncClient(timeout=timeout, follow_redirects=True, limits=limits)
|
248
190
|
|
249
|
-
#
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
self.tool_manager.upsert_base_tools(actor=self.default_user)
|
254
|
-
|
255
|
-
# Add composio keys to the tool sandbox env vars of the org
|
256
|
-
if tool_settings.composio_api_key:
|
257
|
-
manager = SandboxConfigManager()
|
258
|
-
sandbox_config = manager.get_or_create_default_sandbox_config(sandbox_type=SandboxType.LOCAL, actor=self.default_user)
|
259
|
-
|
260
|
-
manager.create_sandbox_env_var(
|
261
|
-
SandboxEnvironmentVariableCreate(key="COMPOSIO_API_KEY", value=tool_settings.composio_api_key),
|
262
|
-
sandbox_config_id=sandbox_config.id,
|
263
|
-
actor=self.default_user,
|
264
|
-
)
|
265
|
-
|
266
|
-
# For OSS users, create a local sandbox config
|
267
|
-
oss_default_user = self.user_manager.get_default_user()
|
268
|
-
use_venv = False if not tool_settings.tool_exec_venv_name else True
|
269
|
-
venv_name = tool_settings.tool_exec_venv_name or "venv"
|
270
|
-
tool_dir = tool_settings.tool_exec_dir or LETTA_TOOL_EXECUTION_DIR
|
271
|
-
|
272
|
-
venv_dir = Path(tool_dir) / venv_name
|
273
|
-
tool_path = Path(tool_dir)
|
274
|
-
|
275
|
-
if tool_path.exists() and not tool_path.is_dir():
|
276
|
-
logger.error(f"LETTA_TOOL_SANDBOX_DIR exists but is not a directory: {tool_dir}")
|
277
|
-
else:
|
278
|
-
if not tool_path.exists():
|
279
|
-
logger.warning(f"LETTA_TOOL_SANDBOX_DIR does not exist, creating now: {tool_dir}")
|
280
|
-
tool_path.mkdir(parents=True, exist_ok=True)
|
281
|
-
|
282
|
-
if tool_settings.tool_exec_venv_name and not venv_dir.is_dir():
|
283
|
-
logger.warning(
|
284
|
-
f"Provided LETTA_TOOL_SANDBOX_VENV_NAME is not a valid venv ({venv_dir}), one will be created for you during tool execution."
|
285
|
-
)
|
191
|
+
# For MCP
|
192
|
+
# TODO: remove this
|
193
|
+
"""Initialize the MCP clients (there may be multiple)"""
|
194
|
+
self.mcp_clients: Dict[str, AsyncBaseMCPClient] = {}
|
286
195
|
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
sandbox_config = self.sandbox_config_manager.create_or_update_sandbox_config(
|
291
|
-
sandbox_config_create=sandbox_config_create, actor=oss_default_user
|
292
|
-
)
|
293
|
-
logger.info(f"Successfully created default local sandbox config:\n{sandbox_config.get_local_config().model_dump()}")
|
196
|
+
# TODO: Remove these in memory caches
|
197
|
+
self._llm_config_cache = {}
|
198
|
+
self._embedding_config_cache = {}
|
294
199
|
|
295
|
-
|
296
|
-
|
297
|
-
sandbox_config.get_local_config(),
|
298
|
-
env=os.environ.copy(),
|
299
|
-
force_recreate=True,
|
300
|
-
)
|
200
|
+
# TODO: Replace this with the Anthropic client we have in house
|
201
|
+
self.anthropic_async_client = AsyncAnthropic()
|
301
202
|
|
302
203
|
# collect providers (always has Letta as a default)
|
303
204
|
self._enabled_providers: List[Provider] = [LettaProvider(name="letta")]
|
@@ -375,6 +276,7 @@ class SyncServer(Server):
|
|
375
276
|
name="vllm",
|
376
277
|
base_url=model_settings.vllm_api_base,
|
377
278
|
default_prompt_formatter=model_settings.default_prompt_formatter,
|
279
|
+
handle_base=model_settings.vllm_handle_base,
|
378
280
|
)
|
379
281
|
)
|
380
282
|
|
@@ -398,18 +300,58 @@ class SyncServer(Server):
|
|
398
300
|
self._enabled_providers.append(DeepSeekProvider(name="deepseek", api_key=model_settings.deepseek_api_key))
|
399
301
|
if model_settings.xai_api_key:
|
400
302
|
self._enabled_providers.append(XAIProvider(name="xai", api_key=model_settings.xai_api_key))
|
303
|
+
if model_settings.openrouter_api_key:
|
304
|
+
self._enabled_providers.append(
|
305
|
+
OpenRouterProvider(
|
306
|
+
name="openrouter",
|
307
|
+
api_key=model_settings.openrouter_api_key,
|
308
|
+
handle_base=model_settings.openrouter_handle_base,
|
309
|
+
)
|
310
|
+
)
|
401
311
|
|
402
|
-
|
403
|
-
#
|
404
|
-
|
405
|
-
|
312
|
+
async def init_async(self, init_with_default_org_and_user: bool = True):
|
313
|
+
# Make default user and org
|
314
|
+
if init_with_default_org_and_user:
|
315
|
+
self.default_org = await self.organization_manager.create_default_organization_async()
|
316
|
+
self.default_user = await self.user_manager.create_default_actor_async()
|
317
|
+
print(f"Default user: {self.default_user} and org: {self.default_org}")
|
318
|
+
await self.tool_manager.upsert_base_tools_async(actor=self.default_user)
|
406
319
|
|
407
|
-
|
408
|
-
|
409
|
-
|
320
|
+
# For OSS users, create a local sandbox config
|
321
|
+
oss_default_user = await self.user_manager.get_default_actor_async()
|
322
|
+
use_venv = False if not tool_settings.tool_exec_venv_name else True
|
323
|
+
venv_name = tool_settings.tool_exec_venv_name or "venv"
|
324
|
+
tool_dir = tool_settings.tool_exec_dir or LETTA_TOOL_EXECUTION_DIR
|
410
325
|
|
411
|
-
|
412
|
-
|
326
|
+
venv_dir = Path(tool_dir) / venv_name
|
327
|
+
tool_path = Path(tool_dir)
|
328
|
+
|
329
|
+
if tool_path.exists() and not tool_path.is_dir():
|
330
|
+
logger.error(f"LETTA_TOOL_SANDBOX_DIR exists but is not a directory: {tool_dir}")
|
331
|
+
else:
|
332
|
+
if not tool_path.exists():
|
333
|
+
logger.warning(f"LETTA_TOOL_SANDBOX_DIR does not exist, creating now: {tool_dir}")
|
334
|
+
tool_path.mkdir(parents=True, exist_ok=True)
|
335
|
+
|
336
|
+
if tool_settings.tool_exec_venv_name and not venv_dir.is_dir():
|
337
|
+
logger.warning(
|
338
|
+
f"Provided LETTA_TOOL_SANDBOX_VENV_NAME is not a valid venv ({venv_dir}), one will be created for you during tool execution."
|
339
|
+
)
|
340
|
+
|
341
|
+
sandbox_config_create = SandboxConfigCreate(
|
342
|
+
config=LocalSandboxConfig(sandbox_dir=tool_settings.tool_exec_dir, use_venv=use_venv, venv_name=venv_name)
|
343
|
+
)
|
344
|
+
sandbox_config = await self.sandbox_config_manager.create_or_update_sandbox_config_async(
|
345
|
+
sandbox_config_create=sandbox_config_create, actor=oss_default_user
|
346
|
+
)
|
347
|
+
logger.debug(f"Successfully created default local sandbox config:\n{sandbox_config.get_local_config().model_dump()}")
|
348
|
+
|
349
|
+
if use_venv and tool_settings.tool_exec_autoreload_venv:
|
350
|
+
prepare_local_sandbox(
|
351
|
+
sandbox_config.get_local_config(),
|
352
|
+
env=os.environ.copy(),
|
353
|
+
force_recreate=True,
|
354
|
+
)
|
413
355
|
|
414
356
|
async def init_mcp_clients(self):
|
415
357
|
# TODO: remove this
|
@@ -421,7 +363,7 @@ class SyncServer(Server):
|
|
421
363
|
elif server_config.type == MCPServerType.STDIO:
|
422
364
|
self.mcp_clients[server_name] = AsyncStdioMCPClient(server_config)
|
423
365
|
else:
|
424
|
-
raise
|
366
|
+
raise LettaInvalidArgumentError(f"Invalid MCP server config: {server_config}", argument_name="server_config")
|
425
367
|
|
426
368
|
try:
|
427
369
|
await self.mcp_clients[server_name].connect_to_server()
|
@@ -436,329 +378,6 @@ class SyncServer(Server):
|
|
436
378
|
logger.info(f"MCP tools connected: {', '.join([t.name for t in mcp_tools])}")
|
437
379
|
logger.debug(f"MCP tools: {', '.join([str(t) for t in mcp_tools])}")
|
438
380
|
|
439
|
-
def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
|
440
|
-
"""Updated method to load agents from persisted storage"""
|
441
|
-
agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
|
442
|
-
# TODO: Think about how to integrate voice sleeptime into sleeptime
|
443
|
-
# TODO: Voice sleeptime agents turn into normal agents when being messaged
|
444
|
-
if agent_state.multi_agent_group and agent_state.multi_agent_group.manager_type != ManagerType.voice_sleeptime:
|
445
|
-
return load_multi_agent(
|
446
|
-
group=agent_state.multi_agent_group, agent_state=agent_state, actor=actor, interface=interface, mcp_clients=self.mcp_clients
|
447
|
-
)
|
448
|
-
|
449
|
-
interface = interface or self.default_interface_factory()
|
450
|
-
return Agent(agent_state=agent_state, interface=interface, user=actor, mcp_clients=self.mcp_clients)
|
451
|
-
|
452
|
-
def _step(
|
453
|
-
self,
|
454
|
-
actor: User,
|
455
|
-
agent_id: str,
|
456
|
-
input_messages: List[MessageCreate],
|
457
|
-
interface: Union[AgentInterface, None] = None, # needed to getting responses
|
458
|
-
put_inner_thoughts_first: bool = True,
|
459
|
-
# timestamp: Optional[datetime],
|
460
|
-
) -> LettaUsageStatistics:
|
461
|
-
"""Send the input message through the agent"""
|
462
|
-
# TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
|
463
|
-
logger.debug(f"Got input messages: {input_messages}")
|
464
|
-
letta_agent = None
|
465
|
-
try:
|
466
|
-
letta_agent = self.load_agent(agent_id=agent_id, interface=interface, actor=actor)
|
467
|
-
if letta_agent is None:
|
468
|
-
raise KeyError(f"Agent (user={actor.id}, agent={agent_id}) is not loaded")
|
469
|
-
|
470
|
-
# Determine whether or not to token stream based on the capability of the interface
|
471
|
-
token_streaming = letta_agent.interface.streaming_mode if hasattr(letta_agent.interface, "streaming_mode") else False
|
472
|
-
|
473
|
-
logger.debug("Starting agent step")
|
474
|
-
if interface:
|
475
|
-
metadata = interface.metadata if hasattr(interface, "metadata") else None
|
476
|
-
else:
|
477
|
-
metadata = None
|
478
|
-
|
479
|
-
usage_stats = letta_agent.step(
|
480
|
-
input_messages=input_messages,
|
481
|
-
chaining=self.chaining,
|
482
|
-
max_chaining_steps=self.max_chaining_steps,
|
483
|
-
stream=token_streaming,
|
484
|
-
skip_verify=True,
|
485
|
-
metadata=metadata,
|
486
|
-
put_inner_thoughts_first=put_inner_thoughts_first,
|
487
|
-
)
|
488
|
-
|
489
|
-
except Exception as e:
|
490
|
-
logger.error(f"Error in server._step: {e}")
|
491
|
-
print(traceback.print_exc())
|
492
|
-
raise
|
493
|
-
finally:
|
494
|
-
logger.debug("Calling step_yield()")
|
495
|
-
if letta_agent:
|
496
|
-
letta_agent.interface.step_yield()
|
497
|
-
|
498
|
-
return usage_stats
|
499
|
-
|
500
|
-
def _command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
|
501
|
-
"""Process a CLI command"""
|
502
|
-
# TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
|
503
|
-
actor = self.user_manager.get_user_or_default(user_id=user_id)
|
504
|
-
|
505
|
-
logger.debug(f"Got command: {command}")
|
506
|
-
|
507
|
-
# Get the agent object (loaded in memory)
|
508
|
-
letta_agent = self.load_agent(agent_id=agent_id, actor=actor)
|
509
|
-
usage = None
|
510
|
-
|
511
|
-
if command.lower() == "exit":
|
512
|
-
# exit not supported on server.py
|
513
|
-
raise ValueError(command)
|
514
|
-
|
515
|
-
elif command.lower() == "save" or command.lower() == "savechat":
|
516
|
-
save_agent(letta_agent)
|
517
|
-
|
518
|
-
elif command.lower() == "attach":
|
519
|
-
# Different from CLI, we extract the data source name from the command
|
520
|
-
command = command.strip().split()
|
521
|
-
try:
|
522
|
-
data_source = int(command[1])
|
523
|
-
except:
|
524
|
-
raise ValueError(command)
|
525
|
-
|
526
|
-
# attach data to agent from source
|
527
|
-
letta_agent.attach_source(
|
528
|
-
user=self.user_manager.get_user_by_id(user_id=user_id),
|
529
|
-
source_id=data_source,
|
530
|
-
source_manager=self.source_manager,
|
531
|
-
agent_manager=self.agent_manager,
|
532
|
-
)
|
533
|
-
|
534
|
-
elif command.lower() == "dump" or command.lower().startswith("dump "):
|
535
|
-
# Check if there's an additional argument that's an integer
|
536
|
-
command = command.strip().split()
|
537
|
-
amount = int(command[1]) if len(command) > 1 and command[1].isdigit() else 0
|
538
|
-
if amount == 0:
|
539
|
-
letta_agent.interface.print_messages(letta_agent.messages, dump=True)
|
540
|
-
else:
|
541
|
-
letta_agent.interface.print_messages(letta_agent.messages[-min(amount, len(letta_agent.messages)) :], dump=True)
|
542
|
-
|
543
|
-
elif command.lower() == "dumpraw":
|
544
|
-
letta_agent.interface.print_messages_raw(letta_agent.messages)
|
545
|
-
|
546
|
-
elif command.lower() == "memory":
|
547
|
-
ret_str = "\nDumping memory contents:\n" + f"\n{str(letta_agent.agent_state.memory)}" + f"\n{str(letta_agent.passage_manager)}"
|
548
|
-
return ret_str
|
549
|
-
|
550
|
-
elif command.lower() == "pop" or command.lower().startswith("pop "):
|
551
|
-
# Check if there's an additional argument that's an integer
|
552
|
-
command = command.strip().split()
|
553
|
-
pop_amount = int(command[1]) if len(command) > 1 and command[1].isdigit() else 3
|
554
|
-
n_messages = len(letta_agent.messages)
|
555
|
-
MIN_MESSAGES = 2
|
556
|
-
if n_messages <= MIN_MESSAGES:
|
557
|
-
logger.debug(f"Agent only has {n_messages} messages in stack, none left to pop")
|
558
|
-
elif n_messages - pop_amount < MIN_MESSAGES:
|
559
|
-
logger.debug(f"Agent only has {n_messages} messages in stack, cannot pop more than {n_messages - MIN_MESSAGES}")
|
560
|
-
else:
|
561
|
-
logger.debug(f"Popping last {pop_amount} messages from stack")
|
562
|
-
for _ in range(min(pop_amount, len(letta_agent.messages))):
|
563
|
-
letta_agent.messages.pop()
|
564
|
-
|
565
|
-
elif command.lower() == "retry":
|
566
|
-
# TODO this needs to also modify the persistence manager
|
567
|
-
logger.debug("Retrying for another answer")
|
568
|
-
while len(letta_agent.messages) > 0:
|
569
|
-
if letta_agent.messages[-1].get("role") == "user":
|
570
|
-
# we want to pop up to the last user message and send it again
|
571
|
-
letta_agent.messages[-1].get("content")
|
572
|
-
letta_agent.messages.pop()
|
573
|
-
break
|
574
|
-
letta_agent.messages.pop()
|
575
|
-
|
576
|
-
elif command.lower() == "rethink" or command.lower().startswith("rethink "):
|
577
|
-
# TODO this needs to also modify the persistence manager
|
578
|
-
if len(command) < len("rethink "):
|
579
|
-
logger.warning("Missing text after the command")
|
580
|
-
else:
|
581
|
-
for x in range(len(letta_agent.messages) - 1, 0, -1):
|
582
|
-
if letta_agent.messages[x].get("role") == "assistant":
|
583
|
-
text = command[len("rethink ") :].strip()
|
584
|
-
letta_agent.messages[x].update({"content": text})
|
585
|
-
break
|
586
|
-
|
587
|
-
elif command.lower() == "rewrite" or command.lower().startswith("rewrite "):
|
588
|
-
# TODO this needs to also modify the persistence manager
|
589
|
-
if len(command) < len("rewrite "):
|
590
|
-
logger.warning("Missing text after the command")
|
591
|
-
else:
|
592
|
-
for x in range(len(letta_agent.messages) - 1, 0, -1):
|
593
|
-
if letta_agent.messages[x].get("role") == "assistant":
|
594
|
-
text = command[len("rewrite ") :].strip()
|
595
|
-
args = json_loads(letta_agent.messages[x].get("function_call").get("arguments"))
|
596
|
-
args["message"] = text
|
597
|
-
letta_agent.messages[x].get("function_call").update({"arguments": json_dumps(args)})
|
598
|
-
break
|
599
|
-
|
600
|
-
# No skip options
|
601
|
-
elif command.lower() == "wipe":
|
602
|
-
# exit not supported on server.py
|
603
|
-
raise ValueError(command)
|
604
|
-
|
605
|
-
elif command.lower() == "heartbeat":
|
606
|
-
input_message = system.get_heartbeat()
|
607
|
-
usage = self._step(actor=actor, agent_id=agent_id, input_message=input_message)
|
608
|
-
|
609
|
-
elif command.lower() == "memorywarning":
|
610
|
-
input_message = system.get_token_limit_warning()
|
611
|
-
usage = self._step(actor=actor, agent_id=agent_id, input_message=input_message)
|
612
|
-
|
613
|
-
if not usage:
|
614
|
-
usage = LettaUsageStatistics()
|
615
|
-
|
616
|
-
return usage
|
617
|
-
|
618
|
-
def user_message(
|
619
|
-
self,
|
620
|
-
user_id: str,
|
621
|
-
agent_id: str,
|
622
|
-
message: Union[str, Message],
|
623
|
-
timestamp: Optional[datetime] = None,
|
624
|
-
) -> LettaUsageStatistics:
|
625
|
-
"""Process an incoming user message and feed it through the Letta agent"""
|
626
|
-
try:
|
627
|
-
actor = self.user_manager.get_user_by_id(user_id=user_id)
|
628
|
-
except NoResultFound:
|
629
|
-
raise ValueError(f"User user_id={user_id} does not exist")
|
630
|
-
|
631
|
-
try:
|
632
|
-
agent = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
|
633
|
-
except NoResultFound:
|
634
|
-
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
635
|
-
|
636
|
-
# Basic input sanitization
|
637
|
-
if isinstance(message, str):
|
638
|
-
if len(message) == 0:
|
639
|
-
raise ValueError(f"Invalid input: '{message}'")
|
640
|
-
|
641
|
-
# If the input begins with a command prefix, reject
|
642
|
-
elif message.startswith("/"):
|
643
|
-
raise ValueError(f"Invalid input: '{message}'")
|
644
|
-
|
645
|
-
packaged_user_message = system.package_user_message(
|
646
|
-
user_message=message,
|
647
|
-
timezone=agent.timezone,
|
648
|
-
)
|
649
|
-
|
650
|
-
# NOTE: eventually deprecate and only allow passing Message types
|
651
|
-
message = MessageCreate(
|
652
|
-
agent_id=agent_id,
|
653
|
-
role="user",
|
654
|
-
content=[TextContent(text=packaged_user_message)],
|
655
|
-
)
|
656
|
-
|
657
|
-
# Run the agent state forward
|
658
|
-
usage = self._step(actor=actor, agent_id=agent_id, input_messages=[message])
|
659
|
-
return usage
|
660
|
-
|
661
|
-
def system_message(
|
662
|
-
self,
|
663
|
-
user_id: str,
|
664
|
-
agent_id: str,
|
665
|
-
message: Union[str, Message],
|
666
|
-
timestamp: Optional[datetime] = None,
|
667
|
-
) -> LettaUsageStatistics:
|
668
|
-
"""Process an incoming system message and feed it through the Letta agent"""
|
669
|
-
try:
|
670
|
-
actor = self.user_manager.get_user_by_id(user_id=user_id)
|
671
|
-
except NoResultFound:
|
672
|
-
raise ValueError(f"User user_id={user_id} does not exist")
|
673
|
-
|
674
|
-
try:
|
675
|
-
agent = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
|
676
|
-
except NoResultFound:
|
677
|
-
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
678
|
-
|
679
|
-
# Basic input sanitization
|
680
|
-
if isinstance(message, str):
|
681
|
-
if len(message) == 0:
|
682
|
-
raise ValueError(f"Invalid input: '{message}'")
|
683
|
-
|
684
|
-
# If the input begins with a command prefix, reject
|
685
|
-
elif message.startswith("/"):
|
686
|
-
raise ValueError(f"Invalid input: '{message}'")
|
687
|
-
|
688
|
-
packaged_system_message = system.package_system_message(system_message=message)
|
689
|
-
|
690
|
-
# NOTE: eventually deprecate and only allow passing Message types
|
691
|
-
# Convert to a Message object
|
692
|
-
|
693
|
-
if timestamp:
|
694
|
-
message = Message(
|
695
|
-
agent_id=agent_id,
|
696
|
-
role="system",
|
697
|
-
content=[TextContent(text=packaged_system_message)],
|
698
|
-
created_at=timestamp,
|
699
|
-
)
|
700
|
-
else:
|
701
|
-
message = Message(
|
702
|
-
agent_id=agent_id,
|
703
|
-
role="system",
|
704
|
-
content=[TextContent(text=packaged_system_message)],
|
705
|
-
)
|
706
|
-
|
707
|
-
if isinstance(message, Message):
|
708
|
-
# Can't have a null text field
|
709
|
-
message_text = message.content[0].text
|
710
|
-
if message_text is None or len(message_text) == 0:
|
711
|
-
raise ValueError(f"Invalid input: '{message_text}'")
|
712
|
-
# If the input begins with a command prefix, reject
|
713
|
-
elif message_text.startswith("/"):
|
714
|
-
raise ValueError(f"Invalid input: '{message_text}'")
|
715
|
-
|
716
|
-
else:
|
717
|
-
raise TypeError(f"Invalid input: '{message}' - type {type(message)}")
|
718
|
-
|
719
|
-
if timestamp:
|
720
|
-
# Override the timestamp with what the caller provided
|
721
|
-
message.created_at = timestamp
|
722
|
-
|
723
|
-
# Run the agent state forward
|
724
|
-
return self._step(actor=actor, agent_id=agent_id, input_messages=message)
|
725
|
-
|
726
|
-
# TODO: Deprecate this
|
727
|
-
def send_messages(
|
728
|
-
self,
|
729
|
-
actor: User,
|
730
|
-
agent_id: str,
|
731
|
-
input_messages: List[MessageCreate],
|
732
|
-
wrap_user_message: bool = True,
|
733
|
-
wrap_system_message: bool = True,
|
734
|
-
interface: Union[AgentInterface, ChatCompletionsStreamingInterface, None] = None, # needed for responses
|
735
|
-
metadata: Optional[dict] = None, # Pass through metadata to interface
|
736
|
-
put_inner_thoughts_first: bool = True,
|
737
|
-
) -> LettaUsageStatistics:
|
738
|
-
"""Send a list of messages to the agent."""
|
739
|
-
|
740
|
-
# Store metadata in interface if provided
|
741
|
-
if metadata and hasattr(interface, "metadata"):
|
742
|
-
interface.metadata = metadata
|
743
|
-
|
744
|
-
# Run the agent state forward
|
745
|
-
return self._step(
|
746
|
-
actor=actor,
|
747
|
-
agent_id=agent_id,
|
748
|
-
input_messages=input_messages,
|
749
|
-
interface=interface,
|
750
|
-
put_inner_thoughts_first=put_inner_thoughts_first,
|
751
|
-
)
|
752
|
-
|
753
|
-
# @LockingServer.agent_lock_decorator
|
754
|
-
def run_command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
|
755
|
-
"""Run a command on the agent"""
|
756
|
-
# If the input begins with a command prefix, attempt to process it as a command
|
757
|
-
if command.startswith("/"):
|
758
|
-
if len(command) > 1:
|
759
|
-
command = command[1:] # strip the prefix
|
760
|
-
return self._command(user_id=user_id, agent_id=agent_id, command=command)
|
761
|
-
|
762
381
|
@trace_method
|
763
382
|
def get_cached_llm_config(self, actor: User, **kwargs):
|
764
383
|
key = make_key(**kwargs)
|
@@ -788,54 +407,6 @@ class SyncServer(Server):
|
|
788
407
|
self._embedding_config_cache[key] = await self.get_embedding_config_from_handle_async(actor=actor, **kwargs)
|
789
408
|
return self._embedding_config_cache[key]
|
790
409
|
|
791
|
-
@trace_method
|
792
|
-
def create_agent(
|
793
|
-
self,
|
794
|
-
request: CreateAgent,
|
795
|
-
actor: User,
|
796
|
-
interface: AgentInterface | None = None,
|
797
|
-
) -> AgentState:
|
798
|
-
warnings.warn("This method is deprecated, use create_agent_async where possible.", DeprecationWarning, stacklevel=2)
|
799
|
-
if request.llm_config is None:
|
800
|
-
if request.model is None:
|
801
|
-
raise ValueError("Must specify either model or llm_config in request")
|
802
|
-
config_params = {
|
803
|
-
"handle": request.model,
|
804
|
-
"context_window_limit": request.context_window_limit,
|
805
|
-
"max_tokens": request.max_tokens,
|
806
|
-
"max_reasoning_tokens": request.max_reasoning_tokens,
|
807
|
-
"enable_reasoner": request.enable_reasoner,
|
808
|
-
}
|
809
|
-
log_event(name="start get_cached_llm_config", attributes=config_params)
|
810
|
-
request.llm_config = self.get_cached_llm_config(actor=actor, **config_params)
|
811
|
-
log_event(name="end get_cached_llm_config", attributes=config_params)
|
812
|
-
|
813
|
-
if request.embedding_config is None:
|
814
|
-
if request.embedding is None:
|
815
|
-
raise ValueError("Must specify either embedding or embedding_config in request")
|
816
|
-
embedding_config_params = {
|
817
|
-
"handle": request.embedding,
|
818
|
-
"embedding_chunk_size": request.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE,
|
819
|
-
}
|
820
|
-
log_event(name="start get_cached_embedding_config", attributes=embedding_config_params)
|
821
|
-
request.embedding_config = self.get_cached_embedding_config(actor=actor, **embedding_config_params)
|
822
|
-
log_event(name="end get_cached_embedding_config", attributes=embedding_config_params)
|
823
|
-
|
824
|
-
log_event(name="start create_agent db")
|
825
|
-
main_agent = self.agent_manager.create_agent(
|
826
|
-
agent_create=request,
|
827
|
-
actor=actor,
|
828
|
-
)
|
829
|
-
log_event(name="end create_agent db")
|
830
|
-
|
831
|
-
if request.enable_sleeptime:
|
832
|
-
if request.agent_type == AgentType.voice_convo_agent:
|
833
|
-
main_agent = self.create_voice_sleeptime_agent(main_agent=main_agent, actor=actor)
|
834
|
-
else:
|
835
|
-
main_agent = self.create_sleeptime_agent(main_agent=main_agent, actor=actor)
|
836
|
-
|
837
|
-
return main_agent
|
838
|
-
|
839
410
|
@trace_method
|
840
411
|
async def create_agent_async(
|
841
412
|
self,
|
@@ -845,7 +416,7 @@ class SyncServer(Server):
|
|
845
416
|
if request.llm_config is None:
|
846
417
|
if request.model is None:
|
847
418
|
if settings.default_llm_handle is None:
|
848
|
-
raise
|
419
|
+
raise LettaInvalidArgumentError("Must specify either model or llm_config in request", argument_name="model")
|
849
420
|
else:
|
850
421
|
request.model = settings.default_llm_handle
|
851
422
|
config_params = {
|
@@ -865,7 +436,9 @@ class SyncServer(Server):
|
|
865
436
|
if request.embedding_config is None:
|
866
437
|
if request.embedding is None:
|
867
438
|
if settings.default_embedding_handle is None:
|
868
|
-
raise
|
439
|
+
raise LettaInvalidArgumentError(
|
440
|
+
"Must specify either embedding or embedding_config in request", argument_name="embedding"
|
441
|
+
)
|
869
442
|
else:
|
870
443
|
request.embedding = settings.default_embedding_handle
|
871
444
|
embedding_config_params = {
|
@@ -903,32 +476,6 @@ class SyncServer(Server):
|
|
903
476
|
|
904
477
|
return main_agent
|
905
478
|
|
906
|
-
def update_agent(
|
907
|
-
self,
|
908
|
-
agent_id: str,
|
909
|
-
request: UpdateAgent,
|
910
|
-
actor: User,
|
911
|
-
) -> AgentState:
|
912
|
-
if request.model is not None:
|
913
|
-
request.llm_config = self.get_llm_config_from_handle(handle=request.model, actor=actor)
|
914
|
-
|
915
|
-
if request.embedding is not None:
|
916
|
-
request.embedding_config = self.get_embedding_config_from_handle(handle=request.embedding, actor=actor)
|
917
|
-
|
918
|
-
if request.enable_sleeptime:
|
919
|
-
agent = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
|
920
|
-
if agent.multi_agent_group is None:
|
921
|
-
if agent.agent_type == AgentType.voice_convo_agent:
|
922
|
-
self.create_voice_sleeptime_agent(main_agent=agent, actor=actor)
|
923
|
-
else:
|
924
|
-
self.create_sleeptime_agent(main_agent=agent, actor=actor)
|
925
|
-
|
926
|
-
return self.agent_manager.update_agent(
|
927
|
-
agent_id=agent_id,
|
928
|
-
agent_update=request,
|
929
|
-
actor=actor,
|
930
|
-
)
|
931
|
-
|
932
479
|
async def update_agent_async(
|
933
480
|
self,
|
934
481
|
agent_id: str,
|
@@ -955,38 +502,6 @@ class SyncServer(Server):
|
|
955
502
|
actor=actor,
|
956
503
|
)
|
957
504
|
|
958
|
-
def create_sleeptime_agent(self, main_agent: AgentState, actor: User) -> AgentState:
|
959
|
-
request = CreateAgent(
|
960
|
-
name=main_agent.name + "-sleeptime",
|
961
|
-
agent_type=AgentType.sleeptime_agent,
|
962
|
-
block_ids=[block.id for block in main_agent.memory.blocks],
|
963
|
-
memory_blocks=[
|
964
|
-
CreateBlock(
|
965
|
-
label="memory_persona",
|
966
|
-
value=get_persona_text("sleeptime_memory_persona"),
|
967
|
-
),
|
968
|
-
],
|
969
|
-
llm_config=main_agent.llm_config,
|
970
|
-
embedding_config=main_agent.embedding_config,
|
971
|
-
project_id=main_agent.project_id,
|
972
|
-
)
|
973
|
-
sleeptime_agent = self.agent_manager.create_agent(
|
974
|
-
agent_create=request,
|
975
|
-
actor=actor,
|
976
|
-
)
|
977
|
-
self.group_manager.create_group(
|
978
|
-
group=GroupCreate(
|
979
|
-
description="",
|
980
|
-
agent_ids=[sleeptime_agent.id],
|
981
|
-
manager_config=SleeptimeManager(
|
982
|
-
manager_agent_id=main_agent.id,
|
983
|
-
sleeptime_agent_frequency=5,
|
984
|
-
),
|
985
|
-
),
|
986
|
-
actor=actor,
|
987
|
-
)
|
988
|
-
return self.agent_manager.get_agent_by_id(agent_id=main_agent.id, actor=actor)
|
989
|
-
|
990
505
|
async def create_sleeptime_agent_async(self, main_agent: AgentState, actor: User) -> AgentState:
|
991
506
|
request = CreateAgent(
|
992
507
|
name=main_agent.name + "-sleeptime",
|
@@ -1019,40 +534,6 @@ class SyncServer(Server):
|
|
1019
534
|
)
|
1020
535
|
return await self.agent_manager.get_agent_by_id_async(agent_id=main_agent.id, actor=actor)
|
1021
536
|
|
1022
|
-
def create_voice_sleeptime_agent(self, main_agent: AgentState, actor: User) -> AgentState:
|
1023
|
-
# TODO: Inject system
|
1024
|
-
request = CreateAgent(
|
1025
|
-
name=main_agent.name + "-sleeptime",
|
1026
|
-
agent_type=AgentType.voice_sleeptime_agent,
|
1027
|
-
block_ids=[block.id for block in main_agent.memory.blocks],
|
1028
|
-
memory_blocks=[
|
1029
|
-
CreateBlock(
|
1030
|
-
label="memory_persona",
|
1031
|
-
value=get_persona_text("voice_memory_persona"),
|
1032
|
-
),
|
1033
|
-
],
|
1034
|
-
llm_config=LLMConfig.default_config("gpt-4.1"),
|
1035
|
-
embedding_config=main_agent.embedding_config,
|
1036
|
-
project_id=main_agent.project_id,
|
1037
|
-
)
|
1038
|
-
voice_sleeptime_agent = self.agent_manager.create_agent(
|
1039
|
-
agent_create=request,
|
1040
|
-
actor=actor,
|
1041
|
-
)
|
1042
|
-
self.group_manager.create_group(
|
1043
|
-
group=GroupCreate(
|
1044
|
-
description="Low latency voice chat with async memory management.",
|
1045
|
-
agent_ids=[voice_sleeptime_agent.id],
|
1046
|
-
manager_config=VoiceSleeptimeManager(
|
1047
|
-
manager_agent_id=main_agent.id,
|
1048
|
-
max_message_buffer_length=constants.DEFAULT_MAX_MESSAGE_BUFFER_LENGTH,
|
1049
|
-
min_message_buffer_length=constants.DEFAULT_MIN_MESSAGE_BUFFER_LENGTH,
|
1050
|
-
),
|
1051
|
-
),
|
1052
|
-
actor=actor,
|
1053
|
-
)
|
1054
|
-
return self.agent_manager.get_agent_by_id(agent_id=main_agent.id, actor=actor)
|
1055
|
-
|
1056
537
|
async def create_voice_sleeptime_agent_async(self, main_agent: AgentState, actor: User) -> AgentState:
|
1057
538
|
# TODO: Inject system
|
1058
539
|
request = CreateAgent(
|
@@ -1087,24 +568,11 @@ class SyncServer(Server):
|
|
1087
568
|
)
|
1088
569
|
return await self.agent_manager.get_agent_by_id_async(agent_id=main_agent.id, actor=actor)
|
1089
570
|
|
1090
|
-
# convert name->id
|
1091
|
-
|
1092
|
-
# TODO: These can be moved to agent_manager
|
1093
|
-
def get_agent_memory(self, agent_id: str, actor: User) -> Memory:
|
1094
|
-
"""Return the memory of an agent (core memory)"""
|
1095
|
-
return self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor).memory
|
1096
|
-
|
1097
571
|
async def get_agent_memory_async(self, agent_id: str, actor: User) -> Memory:
|
1098
572
|
"""Return the memory of an agent (core memory)"""
|
1099
573
|
agent = await self.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
1100
574
|
return agent.memory
|
1101
575
|
|
1102
|
-
def get_archival_memory_summary(self, agent_id: str, actor: User) -> ArchivalMemorySummary:
|
1103
|
-
return ArchivalMemorySummary(size=self.agent_manager.passage_size(actor=actor, agent_id=agent_id))
|
1104
|
-
|
1105
|
-
def get_recall_memory_summary(self, agent_id: str, actor: User) -> RecallMemorySummary:
|
1106
|
-
return RecallMemorySummary(size=self.message_manager.size(actor=actor, agent_id=agent_id))
|
1107
|
-
|
1108
576
|
async def get_agent_archival_async(
|
1109
577
|
self,
|
1110
578
|
agent_id: str,
|
@@ -1149,7 +617,7 @@ class SyncServer(Server):
|
|
1149
617
|
# delete the passage
|
1150
618
|
await self.passage_manager.delete_passage_by_id_async(passage_id=memory_id, actor=actor)
|
1151
619
|
|
1152
|
-
def get_agent_recall(
|
620
|
+
async def get_agent_recall(
|
1153
621
|
self,
|
1154
622
|
user_id: str,
|
1155
623
|
agent_id: str,
|
@@ -1165,9 +633,9 @@ class SyncServer(Server):
|
|
1165
633
|
) -> Union[List[Message], List[LettaMessage]]:
|
1166
634
|
# TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
|
1167
635
|
|
1168
|
-
actor = self.user_manager.
|
636
|
+
actor = await self.user_manager.get_actor_or_default_async(actor_id=user_id)
|
1169
637
|
|
1170
|
-
records = self.message_manager.
|
638
|
+
records = await self.message_manager.list_messages(
|
1171
639
|
agent_id=agent_id,
|
1172
640
|
actor=actor,
|
1173
641
|
after=after,
|
@@ -1206,7 +674,7 @@ class SyncServer(Server):
|
|
1206
674
|
assistant_message_tool_kwarg: str = constants.DEFAULT_MESSAGE_TOOL_KWARG,
|
1207
675
|
include_err: Optional[bool] = None,
|
1208
676
|
) -> Union[List[Message], List[LettaMessage]]:
|
1209
|
-
records = await self.message_manager.
|
677
|
+
records = await self.message_manager.list_messages(
|
1210
678
|
agent_id=agent_id,
|
1211
679
|
actor=actor,
|
1212
680
|
after=after,
|
@@ -1218,6 +686,10 @@ class SyncServer(Server):
|
|
1218
686
|
)
|
1219
687
|
|
1220
688
|
if not return_message_object:
|
689
|
+
# Get agent state to determine if it's a react agent
|
690
|
+
agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
691
|
+
text_is_assistant_message = agent_state.agent_type == AgentType.letta_v1_agent
|
692
|
+
|
1221
693
|
records = Message.to_letta_messages_from_list(
|
1222
694
|
messages=records,
|
1223
695
|
use_assistant_message=use_assistant_message,
|
@@ -1225,6 +697,7 @@ class SyncServer(Server):
|
|
1225
697
|
assistant_message_tool_kwarg=assistant_message_tool_kwarg,
|
1226
698
|
reverse=reverse,
|
1227
699
|
include_err=include_err,
|
700
|
+
text_is_assistant_message=text_is_assistant_message,
|
1228
701
|
)
|
1229
702
|
|
1230
703
|
if reverse:
|
@@ -1289,7 +762,7 @@ class SyncServer(Server):
|
|
1289
762
|
# TODO: move this into a thread
|
1290
763
|
source = await self.source_manager.get_source_by_id(source_id=source_id)
|
1291
764
|
if source is None:
|
1292
|
-
raise
|
765
|
+
raise NoResultFound(f"Source {source_id} does not exist")
|
1293
766
|
connector = DirectoryConnector(input_files=[file_path])
|
1294
767
|
num_passages, num_documents = await self.load_data(user_id=source.created_by_id, source_name=source.name, connector=connector)
|
1295
768
|
|
@@ -1423,78 +896,12 @@ class SyncServer(Server):
|
|
1423
896
|
actor = await self.user_manager.get_actor_by_id_async(actor_id=user_id)
|
1424
897
|
source = await self.source_manager.get_source_by_name(source_name=source_name, actor=actor)
|
1425
898
|
if source is None:
|
1426
|
-
raise
|
899
|
+
raise NoResultFound(f"Data source {source_name} does not exist for user {user_id}")
|
1427
900
|
|
1428
901
|
# load data into the document store
|
1429
902
|
passage_count, document_count = await load_data(connector, source, self.passage_manager, self.file_manager, actor=actor)
|
1430
903
|
return passage_count, document_count
|
1431
904
|
|
1432
|
-
def list_all_sources(self, actor: User) -> List[Source]:
|
1433
|
-
# TODO: legacy: remove
|
1434
|
-
"""List all sources (w/ extra metadata) belonging to a user"""
|
1435
|
-
|
1436
|
-
sources = self.source_manager.list_sources(actor=actor)
|
1437
|
-
|
1438
|
-
# Add extra metadata to the sources
|
1439
|
-
sources_with_metadata = []
|
1440
|
-
for source in sources:
|
1441
|
-
# count number of passages
|
1442
|
-
num_passages = self.agent_manager.passage_size(actor=actor, source_id=source.id)
|
1443
|
-
|
1444
|
-
# TODO: add when files table implemented
|
1445
|
-
## count number of files
|
1446
|
-
# document_conn = StorageConnector.get_storage_connector(TableType.FILES, self.config, user_id=user_id)
|
1447
|
-
# num_documents = document_conn.size({"data_source": source.name})
|
1448
|
-
num_documents = 0
|
1449
|
-
|
1450
|
-
agents = self.source_manager.list_attached_agents(source_id=source.id, actor=actor)
|
1451
|
-
# add the agent name information
|
1452
|
-
attached_agents = [{"id": agent.id, "name": agent.name} for agent in agents]
|
1453
|
-
|
1454
|
-
# Overwrite metadata field, should be empty anyways
|
1455
|
-
source.metadata = dict(
|
1456
|
-
num_documents=num_documents,
|
1457
|
-
num_passages=num_passages,
|
1458
|
-
attached_agents=attached_agents,
|
1459
|
-
)
|
1460
|
-
|
1461
|
-
sources_with_metadata.append(source)
|
1462
|
-
|
1463
|
-
return sources_with_metadata
|
1464
|
-
|
1465
|
-
def update_agent_message(self, message_id: str, request: MessageUpdate, actor: User) -> Message:
|
1466
|
-
"""Update the details of a message associated with an agent"""
|
1467
|
-
|
1468
|
-
# Get the current message
|
1469
|
-
return self.message_manager.update_message_by_id(message_id=message_id, message_update=request, actor=actor)
|
1470
|
-
|
1471
|
-
def list_llm_models(
|
1472
|
-
self,
|
1473
|
-
actor: User,
|
1474
|
-
provider_category: Optional[List[ProviderCategory]] = None,
|
1475
|
-
provider_name: Optional[str] = None,
|
1476
|
-
provider_type: Optional[ProviderType] = None,
|
1477
|
-
) -> List[LLMConfig]:
|
1478
|
-
"""List available models"""
|
1479
|
-
llm_models = []
|
1480
|
-
for provider in self.get_enabled_providers(
|
1481
|
-
provider_category=provider_category,
|
1482
|
-
provider_name=provider_name,
|
1483
|
-
provider_type=provider_type,
|
1484
|
-
actor=actor,
|
1485
|
-
):
|
1486
|
-
try:
|
1487
|
-
llm_models.extend(provider.list_llm_models())
|
1488
|
-
except Exception as e:
|
1489
|
-
import traceback
|
1490
|
-
|
1491
|
-
traceback.print_exc()
|
1492
|
-
warnings.warn(f"An error occurred while listing LLM models for provider {provider}: {e}")
|
1493
|
-
|
1494
|
-
llm_models.extend(self.get_local_llm_configs())
|
1495
|
-
|
1496
|
-
return llm_models
|
1497
|
-
|
1498
905
|
@trace_method
|
1499
906
|
async def list_llm_models_async(
|
1500
907
|
self,
|
@@ -1548,16 +955,6 @@ class SyncServer(Server):
|
|
1548
955
|
|
1549
956
|
return unique_models
|
1550
957
|
|
1551
|
-
def list_embedding_models(self, actor: User) -> List[EmbeddingConfig]:
|
1552
|
-
"""List available embedding models"""
|
1553
|
-
embedding_models = []
|
1554
|
-
for provider in self.get_enabled_providers(actor):
|
1555
|
-
try:
|
1556
|
-
embedding_models.extend(provider.list_embedding_models())
|
1557
|
-
except Exception as e:
|
1558
|
-
warnings.warn(f"An error occurred while listing embedding models for provider {provider}: {e}")
|
1559
|
-
return embedding_models
|
1560
|
-
|
1561
958
|
async def list_embedding_models_async(self, actor: User) -> List[EmbeddingConfig]:
|
1562
959
|
"""Asynchronously list available embedding models with maximum concurrency"""
|
1563
960
|
import asyncio
|
@@ -1587,35 +984,6 @@ class SyncServer(Server):
|
|
1587
984
|
|
1588
985
|
return embedding_models
|
1589
986
|
|
1590
|
-
def get_enabled_providers(
|
1591
|
-
self,
|
1592
|
-
actor: User,
|
1593
|
-
provider_category: Optional[List[ProviderCategory]] = None,
|
1594
|
-
provider_name: Optional[str] = None,
|
1595
|
-
provider_type: Optional[ProviderType] = None,
|
1596
|
-
) -> List[Provider]:
|
1597
|
-
providers = []
|
1598
|
-
if not provider_category or ProviderCategory.base in provider_category:
|
1599
|
-
providers_from_env = [p for p in self._enabled_providers]
|
1600
|
-
providers.extend(providers_from_env)
|
1601
|
-
|
1602
|
-
if not provider_category or ProviderCategory.byok in provider_category:
|
1603
|
-
providers_from_db = self.provider_manager.list_providers(
|
1604
|
-
name=provider_name,
|
1605
|
-
provider_type=provider_type,
|
1606
|
-
actor=actor,
|
1607
|
-
)
|
1608
|
-
providers_from_db = [p.cast_to_subtype() for p in providers_from_db]
|
1609
|
-
providers.extend(providers_from_db)
|
1610
|
-
|
1611
|
-
if provider_name is not None:
|
1612
|
-
providers = [p for p in providers if p.name == provider_name]
|
1613
|
-
|
1614
|
-
if provider_type is not None:
|
1615
|
-
providers = [p for p in providers if p.provider_type == provider_type]
|
1616
|
-
|
1617
|
-
return providers
|
1618
|
-
|
1619
987
|
async def get_enabled_providers_async(
|
1620
988
|
self,
|
1621
989
|
actor: User,
|
@@ -1645,60 +1013,6 @@ class SyncServer(Server):
|
|
1645
1013
|
|
1646
1014
|
return providers
|
1647
1015
|
|
1648
|
-
@trace_method
|
1649
|
-
def get_llm_config_from_handle(
|
1650
|
-
self,
|
1651
|
-
actor: User,
|
1652
|
-
handle: str,
|
1653
|
-
context_window_limit: Optional[int] = None,
|
1654
|
-
max_tokens: Optional[int] = None,
|
1655
|
-
max_reasoning_tokens: Optional[int] = None,
|
1656
|
-
enable_reasoner: Optional[bool] = None,
|
1657
|
-
) -> LLMConfig:
|
1658
|
-
try:
|
1659
|
-
provider_name, model_name = handle.split("/", 1)
|
1660
|
-
provider = self.get_provider_from_name(provider_name, actor)
|
1661
|
-
|
1662
|
-
llm_configs = [config for config in provider.list_llm_models() if config.handle == handle]
|
1663
|
-
if not llm_configs:
|
1664
|
-
llm_configs = [config for config in provider.list_llm_models() if config.model == model_name]
|
1665
|
-
if not llm_configs:
|
1666
|
-
available_handles = [config.handle for config in provider.list_llm_models()]
|
1667
|
-
raise HandleNotFoundError(handle, available_handles)
|
1668
|
-
except ValueError as e:
|
1669
|
-
llm_configs = [config for config in self.get_local_llm_configs() if config.handle == handle]
|
1670
|
-
if not llm_configs:
|
1671
|
-
llm_configs = [config for config in self.get_local_llm_configs() if config.model == model_name]
|
1672
|
-
if not llm_configs:
|
1673
|
-
raise e
|
1674
|
-
|
1675
|
-
if len(llm_configs) == 1:
|
1676
|
-
llm_config = llm_configs[0]
|
1677
|
-
elif len(llm_configs) > 1:
|
1678
|
-
raise ValueError(f"Multiple LLM models with name {model_name} supported by {provider_name}")
|
1679
|
-
else:
|
1680
|
-
llm_config = llm_configs[0]
|
1681
|
-
|
1682
|
-
if context_window_limit is not None:
|
1683
|
-
if context_window_limit > llm_config.context_window:
|
1684
|
-
raise ValueError(f"Context window limit ({context_window_limit}) is greater than maximum of ({llm_config.context_window})")
|
1685
|
-
llm_config.context_window = context_window_limit
|
1686
|
-
else:
|
1687
|
-
llm_config.context_window = min(llm_config.context_window, model_settings.global_max_context_window_limit)
|
1688
|
-
|
1689
|
-
if max_tokens is not None:
|
1690
|
-
llm_config.max_tokens = max_tokens
|
1691
|
-
if max_reasoning_tokens is not None:
|
1692
|
-
if not max_tokens or max_reasoning_tokens > max_tokens:
|
1693
|
-
raise ValueError(f"Max reasoning tokens ({max_reasoning_tokens}) must be less than max tokens ({max_tokens})")
|
1694
|
-
llm_config.max_reasoning_tokens = max_reasoning_tokens
|
1695
|
-
if enable_reasoner is not None:
|
1696
|
-
llm_config.enable_reasoner = enable_reasoner
|
1697
|
-
if enable_reasoner and llm_config.model_endpoint_type == "anthropic":
|
1698
|
-
llm_config.put_inner_thoughts_in_kwargs = False
|
1699
|
-
|
1700
|
-
return llm_config
|
1701
|
-
|
1702
1016
|
@trace_method
|
1703
1017
|
async def get_llm_config_from_handle_async(
|
1704
1018
|
self,
|
@@ -1730,13 +1044,18 @@ class SyncServer(Server):
|
|
1730
1044
|
if len(llm_configs) == 1:
|
1731
1045
|
llm_config = llm_configs[0]
|
1732
1046
|
elif len(llm_configs) > 1:
|
1733
|
-
raise
|
1047
|
+
raise LettaInvalidArgumentError(
|
1048
|
+
f"Multiple LLM models with name {model_name} supported by {provider_name}", argument_name="model_name"
|
1049
|
+
)
|
1734
1050
|
else:
|
1735
1051
|
llm_config = llm_configs[0]
|
1736
1052
|
|
1737
1053
|
if context_window_limit is not None:
|
1738
1054
|
if context_window_limit > llm_config.context_window:
|
1739
|
-
raise
|
1055
|
+
raise LettaInvalidArgumentError(
|
1056
|
+
f"Context window limit ({context_window_limit}) is greater than maximum of ({llm_config.context_window})",
|
1057
|
+
argument_name="context_window_limit",
|
1058
|
+
)
|
1740
1059
|
llm_config.context_window = context_window_limit
|
1741
1060
|
else:
|
1742
1061
|
llm_config.context_window = min(llm_config.context_window, model_settings.global_max_context_window_limit)
|
@@ -1745,7 +1064,10 @@ class SyncServer(Server):
|
|
1745
1064
|
llm_config.max_tokens = max_tokens
|
1746
1065
|
if max_reasoning_tokens is not None:
|
1747
1066
|
if not max_tokens or max_reasoning_tokens > max_tokens:
|
1748
|
-
raise
|
1067
|
+
raise LettaInvalidArgumentError(
|
1068
|
+
f"Max reasoning tokens ({max_reasoning_tokens}) must be less than max tokens ({max_tokens})",
|
1069
|
+
argument_name="max_reasoning_tokens",
|
1070
|
+
)
|
1749
1071
|
llm_config.max_reasoning_tokens = max_reasoning_tokens
|
1750
1072
|
if enable_reasoner is not None:
|
1751
1073
|
llm_config.enable_reasoner = enable_reasoner
|
@@ -1754,35 +1076,6 @@ class SyncServer(Server):
|
|
1754
1076
|
|
1755
1077
|
return llm_config
|
1756
1078
|
|
1757
|
-
@trace_method
|
1758
|
-
def get_embedding_config_from_handle(
|
1759
|
-
self, actor: User, handle: str, embedding_chunk_size: int = constants.DEFAULT_EMBEDDING_CHUNK_SIZE
|
1760
|
-
) -> EmbeddingConfig:
|
1761
|
-
try:
|
1762
|
-
provider_name, model_name = handle.split("/", 1)
|
1763
|
-
provider = self.get_provider_from_name(provider_name, actor)
|
1764
|
-
|
1765
|
-
embedding_configs = [config for config in provider.list_embedding_models() if config.handle == handle]
|
1766
|
-
if not embedding_configs:
|
1767
|
-
raise ValueError(f"Embedding model {model_name} is not supported by {provider_name}")
|
1768
|
-
except ValueError as e:
|
1769
|
-
# search local configs
|
1770
|
-
embedding_configs = [config for config in self.get_local_embedding_configs() if config.handle == handle]
|
1771
|
-
if not embedding_configs:
|
1772
|
-
raise e
|
1773
|
-
|
1774
|
-
if len(embedding_configs) == 1:
|
1775
|
-
embedding_config = embedding_configs[0]
|
1776
|
-
elif len(embedding_configs) > 1:
|
1777
|
-
raise ValueError(f"Multiple embedding models with name {model_name} supported by {provider_name}")
|
1778
|
-
else:
|
1779
|
-
embedding_config = embedding_configs[0]
|
1780
|
-
|
1781
|
-
if embedding_chunk_size:
|
1782
|
-
embedding_config.embedding_chunk_size = embedding_chunk_size
|
1783
|
-
|
1784
|
-
return embedding_config
|
1785
|
-
|
1786
1079
|
@trace_method
|
1787
1080
|
async def get_embedding_config_from_handle_async(
|
1788
1081
|
self, actor: User, handle: str, embedding_chunk_size: int = constants.DEFAULT_EMBEDDING_CHUNK_SIZE
|
@@ -1794,8 +1087,10 @@ class SyncServer(Server):
|
|
1794
1087
|
all_embedding_configs = await provider.list_embedding_models_async()
|
1795
1088
|
embedding_configs = [config for config in all_embedding_configs if config.handle == handle]
|
1796
1089
|
if not embedding_configs:
|
1797
|
-
raise
|
1798
|
-
|
1090
|
+
raise LettaInvalidArgumentError(
|
1091
|
+
f"Embedding model {model_name} is not supported by {provider_name}", argument_name="model_name"
|
1092
|
+
)
|
1093
|
+
except LettaInvalidArgumentError as e:
|
1799
1094
|
# search local configs
|
1800
1095
|
embedding_configs = [config for config in self.get_local_embedding_configs() if config.handle == handle]
|
1801
1096
|
if not embedding_configs:
|
@@ -1804,7 +1099,9 @@ class SyncServer(Server):
|
|
1804
1099
|
if len(embedding_configs) == 1:
|
1805
1100
|
embedding_config = embedding_configs[0]
|
1806
1101
|
elif len(embedding_configs) > 1:
|
1807
|
-
raise
|
1102
|
+
raise LettaInvalidArgumentError(
|
1103
|
+
f"Multiple embedding models with name {model_name} supported by {provider_name}", argument_name="model_name"
|
1104
|
+
)
|
1808
1105
|
else:
|
1809
1106
|
embedding_config = embedding_configs[0]
|
1810
1107
|
|
@@ -1813,28 +1110,16 @@ class SyncServer(Server):
|
|
1813
1110
|
|
1814
1111
|
return embedding_config
|
1815
1112
|
|
1816
|
-
def get_provider_from_name(self, provider_name: str, actor: User) -> Provider:
|
1817
|
-
providers = [provider for provider in self.get_enabled_providers(actor) if provider.name == provider_name]
|
1818
|
-
if not providers:
|
1819
|
-
raise ValueError(
|
1820
|
-
f"Provider {provider_name} is not supported (supported providers: {', '.join([provider.name for provider in self._enabled_providers])})"
|
1821
|
-
)
|
1822
|
-
elif len(providers) > 1:
|
1823
|
-
raise ValueError(f"Multiple providers with name {provider_name} supported")
|
1824
|
-
else:
|
1825
|
-
provider = providers[0]
|
1826
|
-
|
1827
|
-
return provider
|
1828
|
-
|
1829
1113
|
async def get_provider_from_name_async(self, provider_name: str, actor: User) -> Provider:
|
1830
1114
|
all_providers = await self.get_enabled_providers_async(actor)
|
1831
1115
|
providers = [provider for provider in all_providers if provider.name == provider_name]
|
1832
1116
|
if not providers:
|
1833
|
-
raise
|
1834
|
-
f"Provider {provider_name} is not supported (supported providers: {', '.join([provider.name for provider in self._enabled_providers])})"
|
1117
|
+
raise LettaInvalidArgumentError(
|
1118
|
+
f"Provider {provider_name} is not supported (supported providers: {', '.join([provider.name for provider in self._enabled_providers])})",
|
1119
|
+
argument_name="provider_name",
|
1835
1120
|
)
|
1836
1121
|
elif len(providers) > 1:
|
1837
|
-
raise
|
1122
|
+
raise LettaInvalidArgumentError(f"Multiple providers with name {provider_name} supported", argument_name="provider_name")
|
1838
1123
|
else:
|
1839
1124
|
provider = providers[0]
|
1840
1125
|
|
@@ -1842,40 +1127,42 @@ class SyncServer(Server):
|
|
1842
1127
|
|
1843
1128
|
def get_local_llm_configs(self):
|
1844
1129
|
llm_models = []
|
1845
|
-
|
1846
|
-
|
1847
|
-
|
1848
|
-
|
1849
|
-
|
1850
|
-
|
1851
|
-
|
1852
|
-
|
1853
|
-
|
1854
|
-
|
1855
|
-
|
1856
|
-
|
1857
|
-
|
1858
|
-
|
1859
|
-
|
1130
|
+
# NOTE: deprecated
|
1131
|
+
# try:
|
1132
|
+
# llm_configs_dir = os.path.expanduser("~/.letta/llm_configs")
|
1133
|
+
# if os.path.exists(llm_configs_dir):
|
1134
|
+
# for filename in os.listdir(llm_configs_dir):
|
1135
|
+
# if filename.endswith(".json"):
|
1136
|
+
# filepath = os.path.join(llm_configs_dir, filename)
|
1137
|
+
# try:
|
1138
|
+
# with open(filepath, "r") as f:
|
1139
|
+
# config_data = json.load(f)
|
1140
|
+
# llm_config = LLMConfig(**config_data)
|
1141
|
+
# llm_models.append(llm_config)
|
1142
|
+
# except (json.JSONDecodeError, ValueError) as e:
|
1143
|
+
# warnings.warn(f"Error parsing LLM config file {filename}: {e}")
|
1144
|
+
# except Exception as e:
|
1145
|
+
# warnings.warn(f"Error reading LLM configs directory: {e}")
|
1860
1146
|
return llm_models
|
1861
1147
|
|
1862
1148
|
def get_local_embedding_configs(self):
|
1863
1149
|
embedding_models = []
|
1864
|
-
|
1865
|
-
|
1866
|
-
|
1867
|
-
|
1868
|
-
|
1869
|
-
|
1870
|
-
|
1871
|
-
|
1872
|
-
|
1873
|
-
|
1874
|
-
|
1875
|
-
|
1876
|
-
|
1877
|
-
|
1878
|
-
|
1150
|
+
# NOTE: deprecated
|
1151
|
+
# try:
|
1152
|
+
# embedding_configs_dir = os.path.expanduser("~/.letta/embedding_configs")
|
1153
|
+
# if os.path.exists(embedding_configs_dir):
|
1154
|
+
# for filename in os.listdir(embedding_configs_dir):
|
1155
|
+
# if filename.endswith(".json"):
|
1156
|
+
# filepath = os.path.join(embedding_configs_dir, filename)
|
1157
|
+
# try:
|
1158
|
+
# with open(filepath, "r") as f:
|
1159
|
+
# config_data = json.load(f)
|
1160
|
+
# embedding_config = EmbeddingConfig(**config_data)
|
1161
|
+
# embedding_models.append(embedding_config)
|
1162
|
+
# except (json.JSONDecodeError, ValueError) as e:
|
1163
|
+
# warnings.warn(f"Error parsing embedding config file {filename}: {e}")
|
1164
|
+
# except Exception as e:
|
1165
|
+
# warnings.warn(f"Error reading embedding configs directory: {e}")
|
1879
1166
|
return embedding_models
|
1880
1167
|
|
1881
1168
|
def add_llm_model(self, request: LLMConfig) -> LLMConfig:
|
@@ -1898,8 +1185,12 @@ class SyncServer(Server):
|
|
1898
1185
|
) -> ToolReturnMessage:
|
1899
1186
|
"""Run a tool from source code"""
|
1900
1187
|
|
1188
|
+
from letta.services.tool_schema_generator import generate_schema_for_tool_creation, generate_schema_for_tool_update
|
1189
|
+
|
1901
1190
|
if tool_source_type not in (None, ToolSourceType.python, ToolSourceType.typescript):
|
1902
|
-
raise
|
1191
|
+
raise LettaInvalidArgumentError(
|
1192
|
+
f"Tool source type is not supported at this time. Found {tool_source_type}", argument_name="tool_source_type"
|
1193
|
+
)
|
1903
1194
|
|
1904
1195
|
# If tools_json_schema is explicitly passed in, override it on the created Tool object
|
1905
1196
|
if tool_json_schema:
|
@@ -1920,6 +1211,11 @@ class SyncServer(Server):
|
|
1920
1211
|
source_type=tool_source_type,
|
1921
1212
|
)
|
1922
1213
|
|
1214
|
+
# try to get the schema
|
1215
|
+
if not tool.name:
|
1216
|
+
if not tool.json_schema:
|
1217
|
+
tool.json_schema = generate_schema_for_tool_creation(tool)
|
1218
|
+
tool.name = tool.json_schema.get("name")
|
1923
1219
|
assert tool.name is not None, "Failed to create tool object"
|
1924
1220
|
|
1925
1221
|
# TODO eventually allow using agent state in tools
|
@@ -1932,7 +1228,7 @@ class SyncServer(Server):
|
|
1932
1228
|
message_manager=self.message_manager,
|
1933
1229
|
agent_manager=self.agent_manager,
|
1934
1230
|
block_manager=self.block_manager,
|
1935
|
-
|
1231
|
+
run_manager=self.run_manager,
|
1936
1232
|
passage_manager=self.passage_manager,
|
1937
1233
|
actor=actor,
|
1938
1234
|
sandbox_env_vars=tool_env_vars,
|
@@ -1965,33 +1261,6 @@ class SyncServer(Server):
|
|
1965
1261
|
stderr=[traceback.format_exc()],
|
1966
1262
|
)
|
1967
1263
|
|
1968
|
-
# Composio wrappers
|
1969
|
-
@staticmethod
|
1970
|
-
def get_composio_client(api_key: Optional[str] = None):
|
1971
|
-
if api_key:
|
1972
|
-
return Composio(api_key=api_key)
|
1973
|
-
elif tool_settings.composio_api_key:
|
1974
|
-
return Composio(api_key=tool_settings.composio_api_key)
|
1975
|
-
else:
|
1976
|
-
return Composio()
|
1977
|
-
|
1978
|
-
@staticmethod
|
1979
|
-
def get_composio_apps(api_key: Optional[str] = None) -> List["AppModel"]:
|
1980
|
-
"""Get a list of all Composio apps with actions"""
|
1981
|
-
apps = SyncServer.get_composio_client(api_key=api_key).apps.get()
|
1982
|
-
apps_with_actions = []
|
1983
|
-
for app in apps:
|
1984
|
-
# A bit of hacky logic until composio patches this
|
1985
|
-
if app.meta["actionsCount"] > 0 and not app.name.lower().endswith("_beta"):
|
1986
|
-
apps_with_actions.append(app)
|
1987
|
-
|
1988
|
-
return apps_with_actions
|
1989
|
-
|
1990
|
-
def get_composio_actions_from_app_name(self, composio_app_name: str, api_key: Optional[str] = None) -> List["ActionModel"]:
|
1991
|
-
actions = self.get_composio_client(api_key=api_key).actions.get(apps=[composio_app_name])
|
1992
|
-
# Filter out deprecated composio actions
|
1993
|
-
return [action for action in actions if "deprecated" not in action.description.lower()]
|
1994
|
-
|
1995
1264
|
# MCP wrappers
|
1996
1265
|
# TODO support both command + SSE servers (via config)
|
1997
1266
|
def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, StdioServerConfig]]:
|
@@ -2055,7 +1324,7 @@ class SyncServer(Server):
|
|
2055
1324
|
async def get_tools_from_mcp_server(self, mcp_server_name: str) -> List[MCPTool]:
|
2056
1325
|
"""List the tools in an MCP server. Requires a client to be created."""
|
2057
1326
|
if mcp_server_name not in self.mcp_clients:
|
2058
|
-
raise
|
1327
|
+
raise LettaInvalidArgumentError(f"No client was created for MCP server: {mcp_server_name}", argument_name="mcp_server_name")
|
2059
1328
|
|
2060
1329
|
tools = await self.mcp_clients[mcp_server_name].list_tools()
|
2061
1330
|
# Add health information to each tool
|
@@ -2087,11 +1356,13 @@ class SyncServer(Server):
|
|
2087
1356
|
except Exception as e:
|
2088
1357
|
# Raise an error telling the user to fix the config file
|
2089
1358
|
logger.error(f"Failed to parse MCP config file at {mcp_config_path}: {e}")
|
2090
|
-
raise
|
1359
|
+
raise LettaInvalidArgumentError(f"Failed to parse MCP config file {mcp_config_path}")
|
2091
1360
|
|
2092
1361
|
# Check if the server name is already in the config
|
2093
1362
|
if server_config.server_name in current_mcp_servers and not allow_upsert:
|
2094
|
-
raise
|
1363
|
+
raise LettaInvalidArgumentError(
|
1364
|
+
f"Server name {server_config.server_name} is already in the config file", argument_name="server_name"
|
1365
|
+
)
|
2095
1366
|
|
2096
1367
|
# Attempt to initialize the connection to the server
|
2097
1368
|
if server_config.type == MCPServerType.SSE:
|
@@ -2099,7 +1370,7 @@ class SyncServer(Server):
|
|
2099
1370
|
elif server_config.type == MCPServerType.STDIO:
|
2100
1371
|
new_mcp_client = AsyncStdioMCPClient(server_config)
|
2101
1372
|
else:
|
2102
|
-
raise
|
1373
|
+
raise LettaInvalidArgumentError(f"Invalid MCP server config: {server_config}", argument_name="server_config")
|
2103
1374
|
try:
|
2104
1375
|
await new_mcp_client.connect_to_server()
|
2105
1376
|
except:
|
@@ -2124,7 +1395,7 @@ class SyncServer(Server):
|
|
2124
1395
|
json.dump(new_mcp_file, f, indent=4)
|
2125
1396
|
except Exception as e:
|
2126
1397
|
logger.error(f"Failed to write MCP config file at {mcp_config_path}: {e}")
|
2127
|
-
raise
|
1398
|
+
raise LettaInvalidArgumentError(f"Failed to write MCP config file {mcp_config_path}")
|
2128
1399
|
|
2129
1400
|
return list(current_mcp_servers.values())
|
2130
1401
|
|
@@ -2147,12 +1418,12 @@ class SyncServer(Server):
|
|
2147
1418
|
except Exception as e:
|
2148
1419
|
# Raise an error telling the user to fix the config file
|
2149
1420
|
logger.error(f"Failed to parse MCP config file at {mcp_config_path}: {e}")
|
2150
|
-
raise
|
1421
|
+
raise LettaInvalidArgumentError(f"Failed to parse MCP config file {mcp_config_path}")
|
2151
1422
|
|
2152
1423
|
# Check if the server name is already in the config
|
2153
1424
|
# If it's not, throw an error
|
2154
1425
|
if server_name not in current_mcp_servers:
|
2155
|
-
raise
|
1426
|
+
raise LettaInvalidArgumentError(f"Server name {server_name} not found in MCP config file", argument_name="server_name")
|
2156
1427
|
|
2157
1428
|
# Remove from the server file
|
2158
1429
|
del current_mcp_servers[server_name]
|
@@ -2164,7 +1435,7 @@ class SyncServer(Server):
|
|
2164
1435
|
json.dump(new_mcp_file, f, indent=4)
|
2165
1436
|
except Exception as e:
|
2166
1437
|
logger.error(f"Failed to write MCP config file at {mcp_config_path}: {e}")
|
2167
|
-
raise
|
1438
|
+
raise LettaInvalidArgumentError(f"Failed to write MCP config file {mcp_config_path}")
|
2168
1439
|
|
2169
1440
|
return list(current_mcp_servers.values())
|
2170
1441
|
|
@@ -2226,7 +1497,9 @@ class SyncServer(Server):
|
|
2226
1497
|
)
|
2227
1498
|
streaming_interface = letta_agent.interface
|
2228
1499
|
if not isinstance(streaming_interface, StreamingServerInterface):
|
2229
|
-
raise
|
1500
|
+
raise LettaInvalidArgumentError(
|
1501
|
+
f"Agent has wrong type of interface: {type(streaming_interface)}", argument_name="interface"
|
1502
|
+
)
|
2230
1503
|
|
2231
1504
|
# Enable token-streaming within the request if desired
|
2232
1505
|
streaming_interface.streaming_mode = stream_tokens
|
@@ -2331,7 +1604,7 @@ class SyncServer(Server):
|
|
2331
1604
|
) -> Union[StreamingResponse, LettaResponse]:
|
2332
1605
|
include_final_message = True
|
2333
1606
|
if not stream_steps and stream_tokens:
|
2334
|
-
raise
|
1607
|
+
raise LettaInvalidArgumentError("stream_steps must be 'true' if stream_tokens is 'true'", argument_name="stream_steps")
|
2335
1608
|
|
2336
1609
|
group = await self.group_manager.retrieve_group_async(group_id=group_id, actor=actor)
|
2337
1610
|
agent_state_id = group.manager_agent_id or (group.agent_ids[0] if len(group.agent_ids) > 0 else None)
|
@@ -2357,7 +1630,7 @@ class SyncServer(Server):
|
|
2357
1630
|
)
|
2358
1631
|
streaming_interface = letta_multi_agent.interface
|
2359
1632
|
if not isinstance(streaming_interface, StreamingServerInterface):
|
2360
|
-
raise
|
1633
|
+
raise LettaInvalidArgumentError(f"Agent has wrong type of interface: {type(streaming_interface)}", argument_name="interface")
|
2361
1634
|
streaming_interface.streaming_mode = stream_tokens
|
2362
1635
|
streaming_interface.streaming_chat_completion_mode = chat_completion_mode
|
2363
1636
|
if metadata and hasattr(streaming_interface, "metadata"):
|