letta-nightly 0.8.4.dev20250618104304__py3-none-any.whl → 0.8.5.dev20250619180801__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/__init__.py +1 -1
- letta/agents/letta_agent.py +54 -20
- letta/agents/voice_agent.py +47 -31
- letta/constants.py +1 -1
- letta/data_sources/redis_client.py +11 -6
- letta/functions/function_sets/builtin.py +35 -11
- letta/functions/prompts.py +26 -0
- letta/functions/types.py +6 -0
- letta/interfaces/openai_chat_completions_streaming_interface.py +0 -1
- letta/llm_api/anthropic.py +9 -1
- letta/llm_api/anthropic_client.py +22 -3
- letta/llm_api/aws_bedrock.py +10 -6
- letta/llm_api/llm_api_tools.py +3 -0
- letta/llm_api/openai_client.py +1 -1
- letta/orm/agent.py +14 -1
- letta/orm/job.py +3 -0
- letta/orm/provider.py +3 -1
- letta/schemas/agent.py +7 -0
- letta/schemas/embedding_config.py +8 -0
- letta/schemas/enums.py +0 -1
- letta/schemas/job.py +1 -0
- letta/schemas/providers.py +13 -5
- letta/server/rest_api/routers/v1/agents.py +76 -35
- letta/server/rest_api/routers/v1/providers.py +7 -7
- letta/server/rest_api/routers/v1/sources.py +39 -19
- letta/server/rest_api/routers/v1/tools.py +96 -31
- letta/services/agent_manager.py +8 -2
- letta/services/file_processor/chunker/llama_index_chunker.py +89 -1
- letta/services/file_processor/embedder/openai_embedder.py +6 -1
- letta/services/file_processor/parser/mistral_parser.py +2 -2
- letta/services/helpers/agent_manager_helper.py +44 -16
- letta/services/job_manager.py +35 -17
- letta/services/mcp/base_client.py +26 -1
- letta/services/mcp_manager.py +33 -18
- letta/services/provider_manager.py +30 -0
- letta/services/tool_executor/builtin_tool_executor.py +335 -43
- letta/services/tool_manager.py +25 -1
- letta/services/user_manager.py +1 -1
- letta/settings.py +3 -0
- {letta_nightly-0.8.4.dev20250618104304.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/METADATA +4 -3
- {letta_nightly-0.8.4.dev20250618104304.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/RECORD +44 -42
- {letta_nightly-0.8.4.dev20250618104304.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.4.dev20250618104304.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.4.dev20250618104304.dist-info → letta_nightly-0.8.5.dev20250619180801.dist-info}/entry_points.txt +0 -0
letta/schemas/agent.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
from datetime import datetime
|
1
2
|
from enum import Enum
|
2
3
|
from typing import Dict, List, Optional
|
3
4
|
|
@@ -105,6 +106,10 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
|
|
105
106
|
|
106
107
|
multi_agent_group: Optional[Group] = Field(None, description="The multi-agent group that this agent manages")
|
107
108
|
|
109
|
+
# Run metrics
|
110
|
+
last_run_completion: Optional[datetime] = Field(None, description="The timestamp when the agent last completed a run.")
|
111
|
+
last_run_duration_ms: Optional[int] = Field(None, description="The duration in milliseconds of the agent's last run.")
|
112
|
+
|
108
113
|
def get_agent_env_vars_as_dict(self) -> Dict[str, str]:
|
109
114
|
# Get environment variables for this agent specifically
|
110
115
|
per_agent_env_vars = {}
|
@@ -279,6 +284,8 @@ class UpdateAgent(BaseModel):
|
|
279
284
|
)
|
280
285
|
enable_sleeptime: Optional[bool] = Field(None, description="If set to True, memory management will move to a background agent thread.")
|
281
286
|
response_format: Optional[ResponseFormatUnion] = Field(None, description="The response format for the agent.")
|
287
|
+
last_run_completion: Optional[datetime] = Field(None, description="The timestamp when the agent last completed a run.")
|
288
|
+
last_run_duration_ms: Optional[int] = Field(None, description="The duration in milliseconds of the agent's last run.")
|
282
289
|
|
283
290
|
class Config:
|
284
291
|
extra = "ignore" # Ignores extra fields
|
@@ -63,6 +63,14 @@ class EmbeddingConfig(BaseModel):
|
|
63
63
|
embedding_dim=1536,
|
64
64
|
embedding_chunk_size=300,
|
65
65
|
)
|
66
|
+
if model_name == "text-embedding-3-small" and provider == "openai":
|
67
|
+
return cls(
|
68
|
+
embedding_model="text-embedding-3-small",
|
69
|
+
embedding_endpoint_type="openai",
|
70
|
+
embedding_endpoint="https://api.openai.com/v1",
|
71
|
+
embedding_dim=2000,
|
72
|
+
embedding_chunk_size=300,
|
73
|
+
)
|
66
74
|
elif model_name == "letta":
|
67
75
|
return cls(
|
68
76
|
embedding_endpoint="https://embeddings.memgpt.ai",
|
letta/schemas/enums.py
CHANGED
letta/schemas/job.py
CHANGED
@@ -19,6 +19,7 @@ class JobBase(OrmMetadataBase):
|
|
19
19
|
callback_url: Optional[str] = Field(None, description="If set, POST to this URL when the job completes.")
|
20
20
|
callback_sent_at: Optional[datetime] = Field(None, description="Timestamp when the callback was last attempted.")
|
21
21
|
callback_status_code: Optional[int] = Field(None, description="HTTP status code returned by the callback endpoint.")
|
22
|
+
callback_error: Optional[str] = Field(None, description="Optional error message from attempting to POST the callback endpoint.")
|
22
23
|
|
23
24
|
|
24
25
|
class Job(JobBase):
|
letta/schemas/providers.py
CHANGED
@@ -27,8 +27,10 @@ class Provider(ProviderBase):
|
|
27
27
|
name: str = Field(..., description="The name of the provider")
|
28
28
|
provider_type: ProviderType = Field(..., description="The type of the provider")
|
29
29
|
provider_category: ProviderCategory = Field(..., description="The category of the provider (base or byok)")
|
30
|
-
api_key: Optional[str] = Field(None, description="API key used for requests to the provider.")
|
30
|
+
api_key: Optional[str] = Field(None, description="API key or secret key used for requests to the provider.")
|
31
31
|
base_url: Optional[str] = Field(None, description="Base URL for the provider.")
|
32
|
+
access_key: Optional[str] = Field(None, description="Access key used for requests to the provider.")
|
33
|
+
region: Optional[str] = Field(None, description="Region used for requests to the provider.")
|
32
34
|
organization_id: Optional[str] = Field(None, description="The organization id of the user")
|
33
35
|
updated_at: Optional[datetime] = Field(None, description="The last update timestamp of the provider.")
|
34
36
|
|
@@ -95,7 +97,7 @@ class Provider(ProviderBase):
|
|
95
97
|
return OpenAIProvider(**self.model_dump(exclude_none=True))
|
96
98
|
case ProviderType.anthropic:
|
97
99
|
return AnthropicProvider(**self.model_dump(exclude_none=True))
|
98
|
-
case ProviderType.
|
100
|
+
case ProviderType.bedrock:
|
99
101
|
return AnthropicBedrockProvider(**self.model_dump(exclude_none=True))
|
100
102
|
case ProviderType.ollama:
|
101
103
|
return OllamaProvider(**self.model_dump(exclude_none=True))
|
@@ -122,16 +124,22 @@ class Provider(ProviderBase):
|
|
122
124
|
class ProviderCreate(ProviderBase):
|
123
125
|
name: str = Field(..., description="The name of the provider.")
|
124
126
|
provider_type: ProviderType = Field(..., description="The type of the provider.")
|
125
|
-
api_key: str = Field(..., description="API key used for requests to the provider.")
|
127
|
+
api_key: str = Field(..., description="API key or secret key used for requests to the provider.")
|
128
|
+
access_key: Optional[str] = Field(None, description="Access key used for requests to the provider.")
|
129
|
+
region: Optional[str] = Field(None, description="Region used for requests to the provider.")
|
126
130
|
|
127
131
|
|
128
132
|
class ProviderUpdate(ProviderBase):
|
129
|
-
api_key: str = Field(..., description="API key used for requests to the provider.")
|
133
|
+
api_key: str = Field(..., description="API key or secret key used for requests to the provider.")
|
134
|
+
access_key: Optional[str] = Field(None, description="Access key used for requests to the provider.")
|
135
|
+
region: Optional[str] = Field(None, description="Region used for requests to the provider.")
|
130
136
|
|
131
137
|
|
132
138
|
class ProviderCheck(BaseModel):
|
133
139
|
provider_type: ProviderType = Field(..., description="The type of the provider.")
|
134
|
-
api_key: str = Field(..., description="API key used for requests to the provider.")
|
140
|
+
api_key: str = Field(..., description="API key or secret key used for requests to the provider.")
|
141
|
+
access_key: Optional[str] = Field(None, description="Access key used for requests to the provider.")
|
142
|
+
region: Optional[str] = Field(None, description="Region used for requests to the provider.")
|
135
143
|
|
136
144
|
|
137
145
|
class LettaProvider(Provider):
|
@@ -1,9 +1,10 @@
|
|
1
|
+
import asyncio
|
1
2
|
import json
|
2
3
|
import traceback
|
3
4
|
from datetime import datetime, timezone
|
4
5
|
from typing import Annotated, Any, List, Optional
|
5
6
|
|
6
|
-
from fastapi import APIRouter,
|
7
|
+
from fastapi import APIRouter, Body, Depends, File, Header, HTTPException, Query, Request, UploadFile, status
|
7
8
|
from fastapi.responses import JSONResponse
|
8
9
|
from marshmallow import ValidationError
|
9
10
|
from orjson import orjson
|
@@ -79,6 +80,10 @@ async def list_agents(
|
|
79
80
|
False,
|
80
81
|
description="Whether to sort agents oldest to newest (True) or newest to oldest (False, default)",
|
81
82
|
),
|
83
|
+
sort_by: Optional[str] = Query(
|
84
|
+
"created_at",
|
85
|
+
description="Field to sort by. Options: 'created_at' (default), 'last_run_completion'",
|
86
|
+
),
|
82
87
|
):
|
83
88
|
"""
|
84
89
|
List all agents associated with a given user.
|
@@ -107,6 +112,7 @@ async def list_agents(
|
|
107
112
|
identifier_keys=identifier_keys,
|
108
113
|
include_relationships=include_relationships,
|
109
114
|
ascending=ascending,
|
115
|
+
sort_by=sort_by,
|
110
116
|
)
|
111
117
|
|
112
118
|
|
@@ -847,29 +853,63 @@ async def process_message_background(
|
|
847
853
|
include_return_message_types: Optional[List[MessageType]] = None,
|
848
854
|
) -> None:
|
849
855
|
"""Background task to process the message and update job status."""
|
856
|
+
request_start_timestamp_ns = get_utc_timestamp_ns()
|
850
857
|
try:
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
858
|
+
agent = await server.agent_manager.get_agent_by_id_async(agent_id, actor, include_relationships=["multi_agent_group"])
|
859
|
+
agent_eligible = agent.multi_agent_group is None or agent.multi_agent_group.manager_type in ["sleeptime", "voice_sleeptime"]
|
860
|
+
model_compatible = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "together", "google_ai", "google_vertex"]
|
861
|
+
if agent_eligible and model_compatible:
|
862
|
+
if agent.enable_sleeptime and agent.agent_type != AgentType.voice_convo_agent:
|
863
|
+
agent_loop = SleeptimeMultiAgentV2(
|
864
|
+
agent_id=agent_id,
|
865
|
+
message_manager=server.message_manager,
|
866
|
+
agent_manager=server.agent_manager,
|
867
|
+
block_manager=server.block_manager,
|
868
|
+
passage_manager=server.passage_manager,
|
869
|
+
group_manager=server.group_manager,
|
870
|
+
job_manager=server.job_manager,
|
871
|
+
actor=actor,
|
872
|
+
group=agent.multi_agent_group,
|
873
|
+
)
|
874
|
+
else:
|
875
|
+
agent_loop = LettaAgent(
|
876
|
+
agent_id=agent_id,
|
877
|
+
message_manager=server.message_manager,
|
878
|
+
agent_manager=server.agent_manager,
|
879
|
+
block_manager=server.block_manager,
|
880
|
+
passage_manager=server.passage_manager,
|
881
|
+
actor=actor,
|
882
|
+
step_manager=server.step_manager,
|
883
|
+
telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
|
884
|
+
)
|
885
|
+
|
886
|
+
result = await agent_loop.step(
|
887
|
+
messages,
|
888
|
+
max_steps=max_steps,
|
889
|
+
use_assistant_message=use_assistant_message,
|
890
|
+
request_start_timestamp_ns=request_start_timestamp_ns,
|
891
|
+
include_return_message_types=include_return_message_types,
|
892
|
+
)
|
893
|
+
else:
|
894
|
+
result = await server.send_message_to_agent(
|
895
|
+
agent_id=agent_id,
|
896
|
+
actor=actor,
|
897
|
+
input_messages=messages,
|
898
|
+
stream_steps=False,
|
899
|
+
stream_tokens=False,
|
900
|
+
# Support for AssistantMessage
|
901
|
+
use_assistant_message=use_assistant_message,
|
902
|
+
assistant_message_tool_name=assistant_message_tool_name,
|
903
|
+
assistant_message_tool_kwarg=assistant_message_tool_kwarg,
|
904
|
+
include_return_message_types=include_return_message_types,
|
905
|
+
)
|
865
906
|
|
866
|
-
# Update job status to completed
|
867
907
|
job_update = JobUpdate(
|
868
908
|
status=JobStatus.completed,
|
869
909
|
completed_at=datetime.now(timezone.utc),
|
870
|
-
metadata={"result": result.model_dump(mode="json")},
|
910
|
+
metadata={"result": result.model_dump(mode="json")},
|
871
911
|
)
|
872
|
-
server.job_manager.
|
912
|
+
await server.job_manager.update_job_by_id_async(job_id=job_id, job_update=job_update, actor=actor)
|
873
913
|
|
874
914
|
except Exception as e:
|
875
915
|
# Update job status to failed
|
@@ -878,8 +918,7 @@ async def process_message_background(
|
|
878
918
|
completed_at=datetime.now(timezone.utc),
|
879
919
|
metadata={"error": str(e)},
|
880
920
|
)
|
881
|
-
server.job_manager.
|
882
|
-
raise
|
921
|
+
await server.job_manager.update_job_by_id_async(job_id=job_id, job_update=job_update, actor=actor)
|
883
922
|
|
884
923
|
|
885
924
|
@router.post(
|
@@ -889,10 +928,10 @@ async def process_message_background(
|
|
889
928
|
)
|
890
929
|
async def send_message_async(
|
891
930
|
agent_id: str,
|
892
|
-
background_tasks: BackgroundTasks,
|
893
931
|
server: SyncServer = Depends(get_letta_server),
|
894
932
|
request: LettaRequest = Body(...),
|
895
933
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
934
|
+
callback_url: Optional[str] = Query(None, description="Optional callback URL to POST to when the job completes"),
|
896
935
|
):
|
897
936
|
"""
|
898
937
|
Asynchronously process a user message and return a run object.
|
@@ -905,6 +944,7 @@ async def send_message_async(
|
|
905
944
|
run = Run(
|
906
945
|
user_id=actor.id,
|
907
946
|
status=JobStatus.created,
|
947
|
+
callback_url=callback_url,
|
908
948
|
metadata={
|
909
949
|
"job_type": "send_message_async",
|
910
950
|
"agent_id": agent_id,
|
@@ -915,21 +955,22 @@ async def send_message_async(
|
|
915
955
|
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
916
956
|
),
|
917
957
|
)
|
918
|
-
run = server.job_manager.
|
958
|
+
run = await server.job_manager.create_job_async(pydantic_job=run, actor=actor)
|
919
959
|
|
920
|
-
#
|
921
|
-
|
922
|
-
process_message_background
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
960
|
+
# Create asyncio task for background processing
|
961
|
+
asyncio.create_task(
|
962
|
+
process_message_background(
|
963
|
+
job_id=run.id,
|
964
|
+
server=server,
|
965
|
+
actor=actor,
|
966
|
+
agent_id=agent_id,
|
967
|
+
messages=request.messages,
|
968
|
+
use_assistant_message=request.use_assistant_message,
|
969
|
+
assistant_message_tool_name=request.assistant_message_tool_name,
|
970
|
+
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
971
|
+
max_steps=request.max_steps,
|
972
|
+
include_return_message_types=request.include_return_message_types,
|
973
|
+
)
|
933
974
|
)
|
934
975
|
|
935
976
|
return run
|
@@ -66,20 +66,20 @@ async def modify_provider(
|
|
66
66
|
"""
|
67
67
|
Update an existing custom provider
|
68
68
|
"""
|
69
|
-
actor = server.user_manager.
|
70
|
-
return server.provider_manager.
|
69
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
70
|
+
return await server.provider_manager.update_provider_async(provider_id=provider_id, provider_update=request, actor=actor)
|
71
71
|
|
72
72
|
|
73
73
|
@router.get("/check", response_model=None, operation_id="check_provider")
|
74
74
|
def check_provider(
|
75
|
-
|
76
|
-
api_key: str = Header(..., alias="x-api-key"),
|
75
|
+
request: ProviderCheck = Body(...),
|
77
76
|
server: "SyncServer" = Depends(get_letta_server),
|
78
77
|
):
|
79
78
|
try:
|
80
|
-
provider_check
|
81
|
-
|
82
|
-
|
79
|
+
server.provider_manager.check_provider_api_key(provider_check=request)
|
80
|
+
return JSONResponse(
|
81
|
+
status_code=status.HTTP_200_OK, content={"message": f"Valid api key for provider_type={request.provider_type.value}"}
|
82
|
+
)
|
83
83
|
except LLMAuthenticationError as e:
|
84
84
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"{e.message}")
|
85
85
|
except Exception as e:
|
@@ -11,6 +11,7 @@ from starlette import status
|
|
11
11
|
import letta.constants as constants
|
12
12
|
from letta.log import get_logger
|
13
13
|
from letta.schemas.agent import AgentState
|
14
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
14
15
|
from letta.schemas.file import FileMetadata
|
15
16
|
from letta.schemas.job import Job
|
16
17
|
from letta.schemas.passage import Passage
|
@@ -21,9 +22,14 @@ from letta.server.server import SyncServer
|
|
21
22
|
from letta.services.file_processor.chunker.llama_index_chunker import LlamaIndexChunker
|
22
23
|
from letta.services.file_processor.embedder.openai_embedder import OpenAIEmbedder
|
23
24
|
from letta.services.file_processor.file_processor import FileProcessor
|
24
|
-
from letta.services.file_processor.file_types import
|
25
|
+
from letta.services.file_processor.file_types import (
|
26
|
+
get_allowed_media_types,
|
27
|
+
get_extension_to_mime_type_map,
|
28
|
+
is_simple_text_mime_type,
|
29
|
+
register_mime_types,
|
30
|
+
)
|
25
31
|
from letta.services.file_processor.parser.mistral_parser import MistralFileParser
|
26
|
-
from letta.settings import
|
32
|
+
from letta.settings import settings
|
27
33
|
from letta.utils import safe_create_task, sanitize_filename
|
28
34
|
|
29
35
|
logger = get_logger(__name__)
|
@@ -184,7 +190,7 @@ async def upload_file_to_source(
|
|
184
190
|
raw_ct = file.content_type or ""
|
185
191
|
media_type = raw_ct.split(";", 1)[0].strip().lower()
|
186
192
|
|
187
|
-
# If client didn
|
193
|
+
# If client didn't supply a Content-Type or it's not one of the allowed types,
|
188
194
|
# attempt to infer from filename extension.
|
189
195
|
if media_type not in allowed_media_types and file.filename:
|
190
196
|
guessed, _ = mimetypes.guess_type(file.filename)
|
@@ -211,6 +217,7 @@ async def upload_file_to_source(
|
|
211
217
|
source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
212
218
|
if source is None:
|
213
219
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Source with id={source_id} not found.")
|
220
|
+
|
214
221
|
content = await file.read()
|
215
222
|
|
216
223
|
# sanitize filename
|
@@ -228,20 +235,26 @@ async def upload_file_to_source(
|
|
228
235
|
agent_states = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
|
229
236
|
|
230
237
|
# NEW: Cloud based file processing
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
logger=logger,
|
243
|
-
label="load_file_to_source_async",
|
238
|
+
# Determine file's MIME type
|
239
|
+
file_mime_type = mimetypes.guess_type(file.filename)[0] or "application/octet-stream"
|
240
|
+
|
241
|
+
# Check if it's a simple text file
|
242
|
+
is_simple_file = is_simple_text_mime_type(file_mime_type)
|
243
|
+
|
244
|
+
# For complex files, require Mistral API key
|
245
|
+
if not is_simple_file and not settings.mistral_api_key:
|
246
|
+
raise HTTPException(
|
247
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
248
|
+
detail=f"Mistral API key is required to process this file type {file_mime_type}. Please configure your Mistral API key to upload complex file formats.",
|
244
249
|
)
|
250
|
+
|
251
|
+
# Use cloud processing for all files (simple files always, complex files with Mistral key)
|
252
|
+
logger.info("Running experimental cloud based file processing...")
|
253
|
+
safe_create_task(
|
254
|
+
load_file_to_source_cloud(server, agent_states, content, file, job, source_id, actor, source.embedding_config),
|
255
|
+
logger=logger,
|
256
|
+
label="file_processor.process",
|
257
|
+
)
|
245
258
|
safe_create_task(sleeptime_document_ingest_async(server, source_id, actor), logger=logger, label="sleeptime_document_ingest_async")
|
246
259
|
|
247
260
|
return job
|
@@ -336,10 +349,17 @@ async def sleeptime_document_ingest_async(server: SyncServer, source_id: str, ac
|
|
336
349
|
|
337
350
|
|
338
351
|
async def load_file_to_source_cloud(
|
339
|
-
server: SyncServer,
|
352
|
+
server: SyncServer,
|
353
|
+
agent_states: List[AgentState],
|
354
|
+
content: bytes,
|
355
|
+
file: UploadFile,
|
356
|
+
job: Job,
|
357
|
+
source_id: str,
|
358
|
+
actor: User,
|
359
|
+
embedding_config: EmbeddingConfig,
|
340
360
|
):
|
341
361
|
file_processor = MistralFileParser()
|
342
|
-
text_chunker = LlamaIndexChunker()
|
343
|
-
embedder = OpenAIEmbedder()
|
362
|
+
text_chunker = LlamaIndexChunker(chunk_size=embedding_config.embedding_chunk_size)
|
363
|
+
embedder = OpenAIEmbedder(embedding_config=embedding_config)
|
344
364
|
file_processor = FileProcessor(file_parser=file_processor, text_chunker=text_chunker, embedder=embedder, actor=actor)
|
345
365
|
await file_processor.process(server=server, agent_states=agent_states, source_id=source_id, content=content, file=file, job=job)
|
@@ -13,11 +13,12 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
|
13
13
|
|
14
14
|
from letta.errors import LettaToolCreateError
|
15
15
|
from letta.functions.mcp_client.exceptions import MCPTimeoutError
|
16
|
-
from letta.functions.mcp_client.types import MCPTool, SSEServerConfig, StdioServerConfig
|
16
|
+
from letta.functions.mcp_client.types import MCPTool, SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig
|
17
17
|
from letta.helpers.composio_helpers import get_composio_api_key
|
18
18
|
from letta.log import get_logger
|
19
19
|
from letta.orm.errors import UniqueConstraintViolationError
|
20
20
|
from letta.schemas.letta_message import ToolReturnMessage
|
21
|
+
from letta.schemas.mcp import UpdateSSEMCPServer, UpdateStreamableHTTPMCPServer
|
21
22
|
from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
|
22
23
|
from letta.server.rest_api.utils import get_letta_server
|
23
24
|
from letta.server.server import SyncServer
|
@@ -91,6 +92,8 @@ async def list_tools(
|
|
91
92
|
if name is not None:
|
92
93
|
tool = await server.tool_manager.get_tool_by_name_async(tool_name=name, actor=actor)
|
93
94
|
return [tool] if tool else []
|
95
|
+
|
96
|
+
# Get the list of tools
|
94
97
|
return await server.tool_manager.list_tools_async(actor=actor, after=after, limit=limit)
|
95
98
|
except Exception as e:
|
96
99
|
# Log or print the full exception here for debugging
|
@@ -338,20 +341,20 @@ async def add_composio_tool(
|
|
338
341
|
"composio_action_name": composio_action_name,
|
339
342
|
},
|
340
343
|
)
|
341
|
-
except
|
344
|
+
except ApiKeyNotProvidedError as e:
|
342
345
|
raise HTTPException(
|
343
346
|
status_code=400, # Bad Request
|
344
347
|
detail={
|
345
|
-
"code": "
|
348
|
+
"code": "ApiKeyNotProvidedError",
|
346
349
|
"message": str(e),
|
347
350
|
"composio_action_name": composio_action_name,
|
348
351
|
},
|
349
352
|
)
|
350
|
-
except
|
353
|
+
except ComposioClientError as e:
|
351
354
|
raise HTTPException(
|
352
355
|
status_code=400, # Bad Request
|
353
356
|
detail={
|
354
|
-
"code": "
|
357
|
+
"code": "ComposioClientError",
|
355
358
|
"message": str(e),
|
356
359
|
"composio_action_name": composio_action_name,
|
357
360
|
},
|
@@ -368,7 +371,11 @@ async def add_composio_tool(
|
|
368
371
|
|
369
372
|
|
370
373
|
# Specific routes for MCP
|
371
|
-
@router.get(
|
374
|
+
@router.get(
|
375
|
+
"/mcp/servers",
|
376
|
+
response_model=dict[str, Union[SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig]],
|
377
|
+
operation_id="list_mcp_servers",
|
378
|
+
)
|
372
379
|
async def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
|
373
380
|
"""
|
374
381
|
Get a list of all configured MCP servers
|
@@ -479,44 +486,102 @@ async def add_mcp_tool(
|
|
479
486
|
return await server.mcp_manager.add_tool_from_mcp_server(mcp_server_name=mcp_server_name, mcp_tool_name=mcp_tool_name, actor=actor)
|
480
487
|
|
481
488
|
|
482
|
-
@router.put(
|
489
|
+
@router.put(
|
490
|
+
"/mcp/servers",
|
491
|
+
response_model=List[Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig]],
|
492
|
+
operation_id="add_mcp_server",
|
493
|
+
)
|
483
494
|
async def add_mcp_server_to_config(
|
484
|
-
request: Union[StdioServerConfig, SSEServerConfig] = Body(...),
|
495
|
+
request: Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig] = Body(...),
|
485
496
|
server: SyncServer = Depends(get_letta_server),
|
486
497
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
487
498
|
):
|
488
499
|
"""
|
489
500
|
Add a new MCP server to the Letta MCP server config
|
490
501
|
"""
|
502
|
+
try:
|
503
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
491
504
|
|
492
|
-
|
505
|
+
if tool_settings.mcp_read_from_config:
|
506
|
+
# write to config file
|
507
|
+
return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
|
508
|
+
else:
|
509
|
+
# log to DB
|
510
|
+
from letta.schemas.mcp import MCPServer
|
511
|
+
|
512
|
+
if isinstance(request, StdioServerConfig):
|
513
|
+
mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, stdio_config=request)
|
514
|
+
# don't allow stdio servers
|
515
|
+
if tool_settings.mcp_disable_stdio: # protected server
|
516
|
+
raise HTTPException(
|
517
|
+
status_code=400,
|
518
|
+
detail="stdio is not supported in the current environment, please use a self-hosted Letta server in order to add a stdio MCP server",
|
519
|
+
)
|
520
|
+
elif isinstance(request, SSEServerConfig):
|
521
|
+
mapped_request = MCPServer(
|
522
|
+
server_name=request.server_name, server_type=request.type, server_url=request.server_url, token=request.resolve_token()
|
523
|
+
)
|
524
|
+
elif isinstance(request, StreamableHTTPServerConfig):
|
525
|
+
mapped_request = MCPServer(
|
526
|
+
server_name=request.server_name, server_type=request.type, server_url=request.server_url, token=request.resolve_token()
|
527
|
+
)
|
528
|
+
|
529
|
+
await server.mcp_manager.create_mcp_server(mapped_request, actor=actor)
|
530
|
+
|
531
|
+
# TODO: don't do this in the future (just return MCPServer)
|
532
|
+
all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
|
533
|
+
return [server.to_config() for server in all_servers]
|
534
|
+
except UniqueConstraintViolationError:
|
535
|
+
# If server name already exists, throw 409 conflict error
|
536
|
+
raise HTTPException(
|
537
|
+
status_code=409,
|
538
|
+
detail={
|
539
|
+
"code": "MCPServerNameAlreadyExistsError",
|
540
|
+
"message": f"MCP server with name '{request.server_name}' already exists",
|
541
|
+
"server_name": request.server_name,
|
542
|
+
},
|
543
|
+
)
|
544
|
+
except Exception as e:
|
545
|
+
print(f"Unexpected error occurred while adding MCP server: {e}")
|
546
|
+
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
493
547
|
|
494
|
-
if tool_settings.mcp_read_from_config:
|
495
|
-
# write to config file
|
496
|
-
return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
|
497
|
-
else:
|
498
|
-
# log to DB
|
499
|
-
from letta.schemas.mcp import MCPServer
|
500
|
-
|
501
|
-
if isinstance(request, StdioServerConfig):
|
502
|
-
mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, stdio_config=request)
|
503
|
-
# don't allow stdio servers
|
504
|
-
if tool_settings.mcp_disable_stdio: # protected server
|
505
|
-
raise HTTPException(status_code=400, detail="StdioServerConfig is not supported")
|
506
|
-
elif isinstance(request, SSEServerConfig):
|
507
|
-
mapped_request = MCPServer(
|
508
|
-
server_name=request.server_name, server_type=request.type, server_url=request.server_url, token=request.resolve_token()
|
509
|
-
)
|
510
|
-
# TODO: add HTTP streaming
|
511
|
-
mcp_server = await server.mcp_manager.create_or_update_mcp_server(mapped_request, actor=actor)
|
512
548
|
|
513
|
-
|
514
|
-
|
515
|
-
|
549
|
+
@router.patch(
|
550
|
+
"/mcp/servers/{mcp_server_name}",
|
551
|
+
response_model=Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig],
|
552
|
+
operation_id="update_mcp_server",
|
553
|
+
)
|
554
|
+
async def update_mcp_server(
|
555
|
+
mcp_server_name: str,
|
556
|
+
request: Union[UpdateSSEMCPServer, UpdateStreamableHTTPMCPServer] = Body(...),
|
557
|
+
server: SyncServer = Depends(get_letta_server),
|
558
|
+
actor_id: Optional[str] = Header(None, alias="user_id"),
|
559
|
+
):
|
560
|
+
"""
|
561
|
+
Update an existing MCP server configuration
|
562
|
+
"""
|
563
|
+
try:
|
564
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
565
|
+
|
566
|
+
if tool_settings.mcp_read_from_config:
|
567
|
+
raise HTTPException(status_code=501, detail="Update not implemented for config file mode, config files to be deprecated.")
|
568
|
+
else:
|
569
|
+
updated_server = await server.mcp_manager.update_mcp_server_by_name(
|
570
|
+
mcp_server_name=mcp_server_name, mcp_server_update=request, actor=actor
|
571
|
+
)
|
572
|
+
return updated_server.to_config()
|
573
|
+
except HTTPException:
|
574
|
+
# Re-raise HTTP exceptions (like 404)
|
575
|
+
raise
|
576
|
+
except Exception as e:
|
577
|
+
print(f"Unexpected error occurred while updating MCP server: {e}")
|
578
|
+
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
516
579
|
|
517
580
|
|
518
581
|
@router.delete(
|
519
|
-
"/mcp/servers/{mcp_server_name}",
|
582
|
+
"/mcp/servers/{mcp_server_name}",
|
583
|
+
response_model=List[Union[StdioServerConfig, SSEServerConfig, StreamableHTTPServerConfig]],
|
584
|
+
operation_id="delete_mcp_server",
|
520
585
|
)
|
521
586
|
async def delete_mcp_server_from_config(
|
522
587
|
mcp_server_name: str,
|
letta/services/agent_manager.py
CHANGED
@@ -622,6 +622,8 @@ class AgentManager:
|
|
622
622
|
"message_buffer_autoclear": agent_update.message_buffer_autoclear,
|
623
623
|
"enable_sleeptime": agent_update.enable_sleeptime,
|
624
624
|
"response_format": agent_update.response_format,
|
625
|
+
"last_run_completion": agent_update.last_run_completion,
|
626
|
+
"last_run_duration_ms": agent_update.last_run_duration_ms,
|
625
627
|
}
|
626
628
|
for col, val in scalar_updates.items():
|
627
629
|
if val is not None:
|
@@ -742,6 +744,8 @@ class AgentManager:
|
|
742
744
|
"message_buffer_autoclear": agent_update.message_buffer_autoclear,
|
743
745
|
"enable_sleeptime": agent_update.enable_sleeptime,
|
744
746
|
"response_format": agent_update.response_format,
|
747
|
+
"last_run_completion": agent_update.last_run_completion,
|
748
|
+
"last_run_duration_ms": agent_update.last_run_duration_ms,
|
745
749
|
}
|
746
750
|
for col, val in scalar_updates.items():
|
747
751
|
if val is not None:
|
@@ -844,6 +848,7 @@ class AgentManager:
|
|
844
848
|
identifier_keys: Optional[List[str]] = None,
|
845
849
|
include_relationships: Optional[List[str]] = None,
|
846
850
|
ascending: bool = True,
|
851
|
+
sort_by: Optional[str] = "created_at",
|
847
852
|
) -> List[PydanticAgentState]:
|
848
853
|
"""
|
849
854
|
Retrieves agents with optimized filtering and optional field selection.
|
@@ -876,7 +881,7 @@ class AgentManager:
|
|
876
881
|
query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
|
877
882
|
query = _apply_identity_filters(query, identity_id, identifier_keys)
|
878
883
|
query = _apply_tag_filter(query, tags, match_all_tags)
|
879
|
-
query = _apply_pagination(query, before, after, session, ascending=ascending)
|
884
|
+
query = _apply_pagination(query, before, after, session, ascending=ascending, sort_by=sort_by)
|
880
885
|
|
881
886
|
if limit:
|
882
887
|
query = query.limit(limit)
|
@@ -903,6 +908,7 @@ class AgentManager:
|
|
903
908
|
identifier_keys: Optional[List[str]] = None,
|
904
909
|
include_relationships: Optional[List[str]] = None,
|
905
910
|
ascending: bool = True,
|
911
|
+
sort_by: Optional[str] = "created_at",
|
906
912
|
) -> List[PydanticAgentState]:
|
907
913
|
"""
|
908
914
|
Retrieves agents with optimized filtering and optional field selection.
|
@@ -935,7 +941,7 @@ class AgentManager:
|
|
935
941
|
query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
|
936
942
|
query = _apply_identity_filters(query, identity_id, identifier_keys)
|
937
943
|
query = _apply_tag_filter(query, tags, match_all_tags)
|
938
|
-
query = await _apply_pagination_async(query, before, after, session, ascending=ascending)
|
944
|
+
query = await _apply_pagination_async(query, before, after, session, ascending=ascending, sort_by=sort_by)
|
939
945
|
|
940
946
|
if limit:
|
941
947
|
query = query.limit(limit)
|