letta-nightly 0.7.0.dev20250423003112__py3-none-any.whl → 0.7.2.dev20250423222439__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/agent.py +113 -81
- letta/agents/letta_agent.py +2 -2
- letta/agents/letta_agent_batch.py +38 -34
- letta/client/client.py +10 -2
- letta/constants.py +4 -3
- letta/functions/function_sets/multi_agent.py +1 -3
- letta/functions/helpers.py +3 -3
- letta/groups/dynamic_multi_agent.py +58 -59
- letta/groups/round_robin_multi_agent.py +43 -49
- letta/groups/sleeptime_multi_agent.py +28 -18
- letta/groups/supervisor_multi_agent.py +21 -20
- letta/helpers/composio_helpers.py +1 -1
- letta/helpers/converters.py +29 -0
- letta/helpers/datetime_helpers.py +9 -0
- letta/helpers/message_helper.py +1 -0
- letta/helpers/tool_execution_helper.py +3 -3
- letta/jobs/llm_batch_job_polling.py +2 -1
- letta/llm_api/anthropic.py +10 -6
- letta/llm_api/anthropic_client.py +2 -2
- letta/llm_api/cohere.py +2 -2
- letta/llm_api/google_ai_client.py +2 -2
- letta/llm_api/google_vertex_client.py +2 -2
- letta/llm_api/openai.py +11 -4
- letta/llm_api/openai_client.py +34 -2
- letta/local_llm/chat_completion_proxy.py +2 -2
- letta/orm/agent.py +8 -1
- letta/orm/custom_columns.py +15 -0
- letta/schemas/agent.py +6 -0
- letta/schemas/letta_message_content.py +2 -1
- letta/schemas/llm_config.py +12 -2
- letta/schemas/message.py +18 -0
- letta/schemas/openai/chat_completion_response.py +52 -3
- letta/schemas/response_format.py +78 -0
- letta/schemas/tool_execution_result.py +14 -0
- letta/server/rest_api/chat_completions_interface.py +2 -2
- letta/server/rest_api/interface.py +3 -2
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
- letta/server/rest_api/routers/v1/agents.py +4 -4
- letta/server/rest_api/routers/v1/groups.py +2 -2
- letta/server/rest_api/routers/v1/messages.py +41 -19
- letta/server/server.py +24 -57
- letta/services/agent_manager.py +6 -1
- letta/services/llm_batch_manager.py +28 -26
- letta/services/tool_executor/tool_execution_manager.py +37 -28
- letta/services/tool_executor/tool_execution_sandbox.py +35 -16
- letta/services/tool_executor/tool_executor.py +299 -68
- letta/services/tool_sandbox/base.py +3 -2
- letta/services/tool_sandbox/e2b_sandbox.py +5 -4
- letta/services/tool_sandbox/local_sandbox.py +11 -6
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/METADATA +1 -1
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/RECORD +55 -53
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.0.dev20250423003112.dist-info → letta_nightly-0.7.2.dev20250423222439.dist-info}/entry_points.txt +0 -0
@@ -39,9 +39,10 @@ class Message(BaseModel):
|
|
39
39
|
tool_calls: Optional[List[ToolCall]] = None
|
40
40
|
role: str
|
41
41
|
function_call: Optional[FunctionCall] = None # Deprecated
|
42
|
-
reasoning_content: Optional[str] = None # Used in newer reasoning APIs
|
42
|
+
reasoning_content: Optional[str] = None # Used in newer reasoning APIs, e.g. DeepSeek
|
43
43
|
reasoning_content_signature: Optional[str] = None # NOTE: for Anthropic
|
44
44
|
redacted_reasoning_content: Optional[str] = None # NOTE: for Anthropic
|
45
|
+
ommitted_reasoning_content: bool = False # NOTE: for OpenAI o1/o3
|
45
46
|
|
46
47
|
|
47
48
|
class Choice(BaseModel):
|
@@ -52,16 +53,64 @@ class Choice(BaseModel):
|
|
52
53
|
seed: Optional[int] = None # found in TogetherAI
|
53
54
|
|
54
55
|
|
56
|
+
class UsageStatisticsPromptTokenDetails(BaseModel):
|
57
|
+
cached_tokens: int = 0
|
58
|
+
# NOTE: OAI specific
|
59
|
+
# audio_tokens: int = 0
|
60
|
+
|
61
|
+
def __add__(self, other: "UsageStatisticsPromptTokenDetails") -> "UsageStatisticsPromptTokenDetails":
|
62
|
+
return UsageStatisticsPromptTokenDetails(
|
63
|
+
cached_tokens=self.cached_tokens + other.cached_tokens,
|
64
|
+
)
|
65
|
+
|
66
|
+
|
67
|
+
class UsageStatisticsCompletionTokenDetails(BaseModel):
|
68
|
+
reasoning_tokens: int = 0
|
69
|
+
# NOTE: OAI specific
|
70
|
+
# audio_tokens: int = 0
|
71
|
+
# accepted_prediction_tokens: int = 0
|
72
|
+
# rejected_prediction_tokens: int = 0
|
73
|
+
|
74
|
+
def __add__(self, other: "UsageStatisticsCompletionTokenDetails") -> "UsageStatisticsCompletionTokenDetails":
|
75
|
+
return UsageStatisticsCompletionTokenDetails(
|
76
|
+
reasoning_tokens=self.reasoning_tokens + other.reasoning_tokens,
|
77
|
+
)
|
78
|
+
|
79
|
+
|
55
80
|
class UsageStatistics(BaseModel):
|
56
81
|
completion_tokens: int = 0
|
57
82
|
prompt_tokens: int = 0
|
58
83
|
total_tokens: int = 0
|
59
84
|
|
85
|
+
prompt_tokens_details: Optional[UsageStatisticsPromptTokenDetails] = None
|
86
|
+
completion_tokens_details: Optional[UsageStatisticsCompletionTokenDetails] = None
|
87
|
+
|
60
88
|
def __add__(self, other: "UsageStatistics") -> "UsageStatistics":
|
89
|
+
|
90
|
+
if self.prompt_tokens_details is None and other.prompt_tokens_details is None:
|
91
|
+
total_prompt_tokens_details = None
|
92
|
+
elif self.prompt_tokens_details is None:
|
93
|
+
total_prompt_tokens_details = other.prompt_tokens_details
|
94
|
+
elif other.prompt_tokens_details is None:
|
95
|
+
total_prompt_tokens_details = self.prompt_tokens_details
|
96
|
+
else:
|
97
|
+
total_prompt_tokens_details = self.prompt_tokens_details + other.prompt_tokens_details
|
98
|
+
|
99
|
+
if self.completion_tokens_details is None and other.completion_tokens_details is None:
|
100
|
+
total_completion_tokens_details = None
|
101
|
+
elif self.completion_tokens_details is None:
|
102
|
+
total_completion_tokens_details = other.completion_tokens_details
|
103
|
+
elif other.completion_tokens_details is None:
|
104
|
+
total_completion_tokens_details = self.completion_tokens_details
|
105
|
+
else:
|
106
|
+
total_completion_tokens_details = self.completion_tokens_details + other.completion_tokens_details
|
107
|
+
|
61
108
|
return UsageStatistics(
|
62
109
|
completion_tokens=self.completion_tokens + other.completion_tokens,
|
63
110
|
prompt_tokens=self.prompt_tokens + other.prompt_tokens,
|
64
111
|
total_tokens=self.total_tokens + other.total_tokens,
|
112
|
+
prompt_tokens_details=total_prompt_tokens_details,
|
113
|
+
completion_tokens_details=total_completion_tokens_details,
|
65
114
|
)
|
66
115
|
|
67
116
|
|
@@ -70,7 +119,7 @@ class ChatCompletionResponse(BaseModel):
|
|
70
119
|
|
71
120
|
id: str
|
72
121
|
choices: List[Choice]
|
73
|
-
created: datetime.datetime
|
122
|
+
created: Union[datetime.datetime, int]
|
74
123
|
model: Optional[str] = None # NOTE: this is not consistent with OpenAI API standard, however is necessary to support local LLMs
|
75
124
|
# system_fingerprint: str # docs say this is mandatory, but in reality API returns None
|
76
125
|
system_fingerprint: Optional[str] = None
|
@@ -138,7 +187,7 @@ class ChatCompletionChunkResponse(BaseModel):
|
|
138
187
|
|
139
188
|
id: str
|
140
189
|
choices: List[ChunkChoice]
|
141
|
-
created: Union[datetime.datetime,
|
190
|
+
created: Union[datetime.datetime, int]
|
142
191
|
model: str
|
143
192
|
# system_fingerprint: str # docs say this is mandatory, but in reality API returns None
|
144
193
|
system_fingerprint: Optional[str] = None
|
@@ -0,0 +1,78 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Annotated, Any, Dict, Literal, Union
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field, validator
|
5
|
+
|
6
|
+
|
7
|
+
class ResponseFormatType(str, Enum):
|
8
|
+
"""Enum defining the possible response format types."""
|
9
|
+
|
10
|
+
text = "text"
|
11
|
+
json_schema = "json_schema"
|
12
|
+
json_object = "json_object"
|
13
|
+
|
14
|
+
|
15
|
+
class ResponseFormat(BaseModel):
|
16
|
+
"""Base class for all response formats."""
|
17
|
+
|
18
|
+
type: ResponseFormatType = Field(
|
19
|
+
...,
|
20
|
+
description="The type of the response format.",
|
21
|
+
# why use this?
|
22
|
+
example=ResponseFormatType.text,
|
23
|
+
)
|
24
|
+
|
25
|
+
|
26
|
+
# ---------------------
|
27
|
+
# Response Format Types
|
28
|
+
# ---------------------
|
29
|
+
|
30
|
+
# SQLAlchemy type for database mapping
|
31
|
+
ResponseFormatDict = Dict[str, Any]
|
32
|
+
|
33
|
+
|
34
|
+
class TextResponseFormat(ResponseFormat):
|
35
|
+
"""Response format for plain text responses."""
|
36
|
+
|
37
|
+
type: Literal[ResponseFormatType.text] = Field(
|
38
|
+
ResponseFormatType.text,
|
39
|
+
description="The type of the response format.",
|
40
|
+
)
|
41
|
+
|
42
|
+
|
43
|
+
class JsonSchemaResponseFormat(ResponseFormat):
|
44
|
+
"""Response format for JSON schema-based responses."""
|
45
|
+
|
46
|
+
type: Literal[ResponseFormatType.json_schema] = Field(
|
47
|
+
ResponseFormatType.json_schema,
|
48
|
+
description="The type of the response format.",
|
49
|
+
)
|
50
|
+
json_schema: Dict[str, Any] = Field(
|
51
|
+
...,
|
52
|
+
description="The JSON schema of the response.",
|
53
|
+
)
|
54
|
+
|
55
|
+
@validator("json_schema")
|
56
|
+
def validate_json_schema(cls, v: Dict[str, Any]) -> Dict[str, Any]:
|
57
|
+
"""Validate that the provided schema is a valid JSON schema."""
|
58
|
+
if not isinstance(v, dict):
|
59
|
+
raise ValueError("JSON schema must be a dictionary")
|
60
|
+
if "schema" not in v:
|
61
|
+
raise ValueError("JSON schema should include a $schema property")
|
62
|
+
return v
|
63
|
+
|
64
|
+
|
65
|
+
class JsonObjectResponseFormat(ResponseFormat):
|
66
|
+
"""Response format for JSON object responses."""
|
67
|
+
|
68
|
+
type: Literal[ResponseFormatType.json_object] = Field(
|
69
|
+
ResponseFormatType.json_object,
|
70
|
+
description="The type of the response format.",
|
71
|
+
)
|
72
|
+
|
73
|
+
|
74
|
+
# Pydantic type for validation
|
75
|
+
ResponseFormatUnion = Annotated[
|
76
|
+
Union[TextResponseFormat | JsonSchemaResponseFormat | JsonObjectResponseFormat],
|
77
|
+
Field(discriminator="type"),
|
78
|
+
]
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from typing import Any, List, Literal, Optional
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from letta.schemas.agent import AgentState
|
6
|
+
|
7
|
+
|
8
|
+
class ToolExecutionResult(BaseModel):
|
9
|
+
status: Literal["success", "error"] = Field(..., description="The status of the tool execution and return object")
|
10
|
+
func_return: Optional[Any] = Field(None, description="The function return object")
|
11
|
+
agent_state: Optional[AgentState] = Field(None, description="The agent state")
|
12
|
+
stdout: Optional[List[str]] = Field(None, description="Captured stdout (prints, logs) from function invocation")
|
13
|
+
stderr: Optional[List[str]] = Field(None, description="Captured stderr from the function invocation")
|
14
|
+
sandbox_config_fingerprint: Optional[str] = Field(None, description="The fingerprint of the config for the sandbox")
|
@@ -238,7 +238,7 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
|
|
238
238
|
return ChatCompletionChunk(
|
239
239
|
id=chunk.id,
|
240
240
|
object=chunk.object,
|
241
|
-
created=chunk.created
|
241
|
+
created=chunk.created,
|
242
242
|
model=chunk.model,
|
243
243
|
choices=[
|
244
244
|
Choice(
|
@@ -256,7 +256,7 @@ class ChatCompletionsStreamingInterface(AgentChunkStreamingInterface):
|
|
256
256
|
return ChatCompletionChunk(
|
257
257
|
id=chunk.id,
|
258
258
|
object=chunk.object,
|
259
|
-
created=chunk.created
|
259
|
+
created=chunk.created,
|
260
260
|
model=chunk.model,
|
261
261
|
choices=[
|
262
262
|
Choice(
|
@@ -1001,7 +1001,7 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
1001
1001
|
# Example case that would trigger here:
|
1002
1002
|
# id='chatcmpl-AKtUvREgRRvgTW6n8ZafiKuV0mxhQ'
|
1003
1003
|
# choices=[ChunkChoice(finish_reason=None, index=0, delta=MessageDelta(content=None, tool_calls=None, function_call=None), logprobs=None)]
|
1004
|
-
# created=
|
1004
|
+
# created=1713216662
|
1005
1005
|
# model='gpt-4o-mini-2024-07-18'
|
1006
1006
|
# object='chat.completion.chunk'
|
1007
1007
|
warnings.warn(f"Couldn't find delta in chunk: {chunk}")
|
@@ -1240,10 +1240,11 @@ class StreamingServerInterface(AgentChunkStreamingInterface):
|
|
1240
1240
|
and function_call.function.name == self.assistant_message_tool_name
|
1241
1241
|
and self.assistant_message_tool_kwarg in func_args
|
1242
1242
|
):
|
1243
|
+
# Coerce content to `str` in cases where it's a JSON due to `response_format` being a JSON
|
1243
1244
|
processed_chunk = AssistantMessage(
|
1244
1245
|
id=msg_obj.id,
|
1245
1246
|
date=msg_obj.created_at,
|
1246
|
-
content=func_args[self.assistant_message_tool_kwarg],
|
1247
|
+
content=str(func_args[self.assistant_message_tool_kwarg]),
|
1247
1248
|
name=msg_obj.name,
|
1248
1249
|
otid=Message.generate_otid_from_id(msg_obj.id, chunk_index) if chunk_index is not None else None,
|
1249
1250
|
)
|
@@ -111,7 +111,7 @@ async def send_message_to_agent_chat_completions(
|
|
111
111
|
server.send_messages,
|
112
112
|
actor=actor,
|
113
113
|
agent_id=letta_agent.agent_state.id,
|
114
|
-
|
114
|
+
input_messages=messages,
|
115
115
|
interface=streaming_interface,
|
116
116
|
put_inner_thoughts_first=False,
|
117
117
|
)
|
@@ -412,7 +412,7 @@ def list_blocks(
|
|
412
412
|
"""
|
413
413
|
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
414
414
|
try:
|
415
|
-
agent = server.agent_manager.get_agent_by_id(agent_id, actor
|
415
|
+
agent = server.agent_manager.get_agent_by_id(agent_id, actor)
|
416
416
|
return agent.memory.blocks
|
417
417
|
except NoResultFound as e:
|
418
418
|
raise HTTPException(status_code=404, detail=str(e))
|
@@ -640,7 +640,7 @@ async def send_message(
|
|
640
640
|
result = await server.send_message_to_agent(
|
641
641
|
agent_id=agent_id,
|
642
642
|
actor=actor,
|
643
|
-
|
643
|
+
input_messages=request.messages,
|
644
644
|
stream_steps=False,
|
645
645
|
stream_tokens=False,
|
646
646
|
# Support for AssistantMessage
|
@@ -703,7 +703,7 @@ async def send_message_streaming(
|
|
703
703
|
result = await server.send_message_to_agent(
|
704
704
|
agent_id=agent_id,
|
705
705
|
actor=actor,
|
706
|
-
|
706
|
+
input_messages=request.messages,
|
707
707
|
stream_steps=True,
|
708
708
|
stream_tokens=request.stream_tokens,
|
709
709
|
# Support for AssistantMessage
|
@@ -730,7 +730,7 @@ async def process_message_background(
|
|
730
730
|
result = await server.send_message_to_agent(
|
731
731
|
agent_id=agent_id,
|
732
732
|
actor=actor,
|
733
|
-
|
733
|
+
input_messages=messages,
|
734
734
|
stream_steps=False, # NOTE(matt)
|
735
735
|
stream_tokens=False,
|
736
736
|
use_assistant_message=use_assistant_message,
|
@@ -128,7 +128,7 @@ async def send_group_message(
|
|
128
128
|
result = await server.send_group_message_to_agent(
|
129
129
|
group_id=group_id,
|
130
130
|
actor=actor,
|
131
|
-
|
131
|
+
input_messages=request.messages,
|
132
132
|
stream_steps=False,
|
133
133
|
stream_tokens=False,
|
134
134
|
# Support for AssistantMessage
|
@@ -167,7 +167,7 @@ async def send_group_message_streaming(
|
|
167
167
|
result = await server.send_group_message_to_agent(
|
168
168
|
group_id=group_id,
|
169
169
|
actor=actor,
|
170
|
-
|
170
|
+
input_messages=request.messages,
|
171
171
|
stream_steps=True,
|
172
172
|
stream_tokens=request.stream_tokens,
|
173
173
|
# Support for AssistantMessage
|
@@ -1,16 +1,17 @@
|
|
1
1
|
from typing import List, Optional
|
2
2
|
|
3
|
-
from fastapi import APIRouter, Body, Depends, Header
|
3
|
+
from fastapi import APIRouter, Body, Depends, Header, status
|
4
4
|
from fastapi.exceptions import HTTPException
|
5
5
|
from starlette.requests import Request
|
6
6
|
|
7
7
|
from letta.agents.letta_agent_batch import LettaAgentBatch
|
8
8
|
from letta.log import get_logger
|
9
9
|
from letta.orm.errors import NoResultFound
|
10
|
-
from letta.schemas.job import BatchJob, JobStatus, JobType
|
10
|
+
from letta.schemas.job import BatchJob, JobStatus, JobType, JobUpdate
|
11
11
|
from letta.schemas.letta_request import CreateBatch
|
12
12
|
from letta.server.rest_api.utils import get_letta_server
|
13
13
|
from letta.server.server import SyncServer
|
14
|
+
from letta.settings import settings
|
14
15
|
|
15
16
|
router = APIRouter(prefix="/messages", tags=["messages"])
|
16
17
|
|
@@ -43,19 +44,26 @@ async def create_messages_batch(
|
|
43
44
|
if length > max_bytes:
|
44
45
|
raise HTTPException(status_code=413, detail=f"Request too large ({length} bytes). Max is {max_bytes} bytes.")
|
45
46
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
user_id=actor.id,
|
52
|
-
status=JobStatus.created,
|
53
|
-
metadata={
|
54
|
-
"job_type": "batch_messages",
|
55
|
-
},
|
56
|
-
callback_url=str(payload.callback_url),
|
47
|
+
# Reject request if env var is not set
|
48
|
+
if not settings.enable_batch_job_polling:
|
49
|
+
raise HTTPException(
|
50
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
51
|
+
detail=f"Server misconfiguration: LETTA_ENABLE_BATCH_JOB_POLLING is set to False.",
|
57
52
|
)
|
58
53
|
|
54
|
+
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
55
|
+
batch_job = BatchJob(
|
56
|
+
user_id=actor.id,
|
57
|
+
status=JobStatus.running,
|
58
|
+
metadata={
|
59
|
+
"job_type": "batch_messages",
|
60
|
+
},
|
61
|
+
callback_url=str(payload.callback_url),
|
62
|
+
)
|
63
|
+
|
64
|
+
try:
|
65
|
+
batch_job = server.job_manager.create_job(pydantic_job=batch_job, actor=actor)
|
66
|
+
|
59
67
|
# create the batch runner
|
60
68
|
batch_runner = LettaAgentBatch(
|
61
69
|
message_manager=server.message_manager,
|
@@ -67,14 +75,17 @@ async def create_messages_batch(
|
|
67
75
|
job_manager=server.job_manager,
|
68
76
|
actor=actor,
|
69
77
|
)
|
70
|
-
|
78
|
+
await batch_runner.step_until_request(batch_requests=payload.requests, letta_batch_job_id=batch_job.id)
|
71
79
|
|
72
80
|
# TODO: update run metadata
|
73
|
-
|
74
|
-
except Exception:
|
81
|
+
except Exception as e:
|
75
82
|
import traceback
|
76
83
|
|
84
|
+
print("Error creating batch job", e)
|
77
85
|
traceback.print_exc()
|
86
|
+
|
87
|
+
# mark job as failed
|
88
|
+
server.job_manager.update_job_by_id(job_id=batch_job.id, job=BatchJob(status=JobStatus.failed), actor=actor)
|
78
89
|
raise
|
79
90
|
return batch_job
|
80
91
|
|
@@ -125,8 +136,19 @@ async def cancel_batch_run(
|
|
125
136
|
|
126
137
|
try:
|
127
138
|
job = server.job_manager.get_job_by_id(job_id=batch_id, actor=actor)
|
128
|
-
job.
|
129
|
-
|
130
|
-
#
|
139
|
+
job = server.job_manager.update_job_by_id(job_id=job.id, job_update=JobUpdate(status=JobStatus.cancelled), actor=actor)
|
140
|
+
|
141
|
+
# Get related llm batch jobs
|
142
|
+
llm_batch_jobs = server.batch_manager.list_llm_batch_jobs(letta_batch_id=job.id, actor=actor)
|
143
|
+
for llm_batch_job in llm_batch_jobs:
|
144
|
+
if llm_batch_job.status in {JobStatus.running, JobStatus.created}:
|
145
|
+
# TODO: Extend to providers beyond anthropic
|
146
|
+
# TODO: For now, we only support anthropic
|
147
|
+
# Cancel the job
|
148
|
+
anthropic_batch_id = llm_batch_job.create_batch_response.id
|
149
|
+
await server.anthropic_async_client.messages.batches.cancel(anthropic_batch_id)
|
150
|
+
|
151
|
+
# Update all the batch_job statuses
|
152
|
+
server.batch_manager.update_llm_batch_status(llm_batch_id=llm_batch_job.id, status=JobStatus.cancelled, actor=actor)
|
131
153
|
except NoResultFound:
|
132
154
|
raise HTTPException(status_code=404, detail="Run not found")
|
letta/server/server.py
CHANGED
@@ -28,7 +28,6 @@ from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerCo
|
|
28
28
|
from letta.groups.helpers import load_multi_agent
|
29
29
|
from letta.helpers.datetime_helpers import get_utc_time
|
30
30
|
from letta.helpers.json_helpers import json_dumps, json_loads
|
31
|
-
from letta.helpers.message_helper import prepare_input_message_create
|
32
31
|
|
33
32
|
# TODO use custom interface
|
34
33
|
from letta.interface import AgentInterface # abstract
|
@@ -148,7 +147,7 @@ class Server(object):
|
|
148
147
|
raise NotImplementedError
|
149
148
|
|
150
149
|
@abstractmethod
|
151
|
-
def send_messages(self, user_id: str, agent_id: str,
|
150
|
+
def send_messages(self, user_id: str, agent_id: str, input_messages: List[MessageCreate]) -> None:
|
152
151
|
"""Send a list of messages to the agent"""
|
153
152
|
raise NotImplementedError
|
154
153
|
|
@@ -372,19 +371,13 @@ class SyncServer(Server):
|
|
372
371
|
self,
|
373
372
|
actor: User,
|
374
373
|
agent_id: str,
|
375
|
-
input_messages:
|
374
|
+
input_messages: List[MessageCreate],
|
376
375
|
interface: Union[AgentInterface, None] = None, # needed to getting responses
|
377
376
|
put_inner_thoughts_first: bool = True,
|
378
377
|
# timestamp: Optional[datetime],
|
379
378
|
) -> LettaUsageStatistics:
|
380
379
|
"""Send the input message through the agent"""
|
381
380
|
# TODO: Thread actor directly through this function, since the top level caller most likely already retrieved the user
|
382
|
-
# Input validation
|
383
|
-
if isinstance(input_messages, Message):
|
384
|
-
input_messages = [input_messages]
|
385
|
-
if not all(isinstance(m, Message) for m in input_messages):
|
386
|
-
raise ValueError(f"messages should be a Message or a list of Message, got {type(input_messages)}")
|
387
|
-
|
388
381
|
logger.debug(f"Got input messages: {input_messages}")
|
389
382
|
letta_agent = None
|
390
383
|
try:
|
@@ -400,8 +393,9 @@ class SyncServer(Server):
|
|
400
393
|
metadata = interface.metadata if hasattr(interface, "metadata") else None
|
401
394
|
else:
|
402
395
|
metadata = None
|
396
|
+
|
403
397
|
usage_stats = letta_agent.step(
|
404
|
-
|
398
|
+
input_messages=input_messages,
|
405
399
|
chaining=self.chaining,
|
406
400
|
max_chaining_steps=self.max_chaining_steps,
|
407
401
|
stream=token_streaming,
|
@@ -572,23 +566,14 @@ class SyncServer(Server):
|
|
572
566
|
)
|
573
567
|
|
574
568
|
# NOTE: eventually deprecate and only allow passing Message types
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
content=[TextContent(text=packaged_user_message)],
|
581
|
-
created_at=timestamp,
|
582
|
-
)
|
583
|
-
else:
|
584
|
-
message = Message(
|
585
|
-
agent_id=agent_id,
|
586
|
-
role="user",
|
587
|
-
content=[TextContent(text=packaged_user_message)],
|
588
|
-
)
|
569
|
+
message = MessageCreate(
|
570
|
+
agent_id=agent_id,
|
571
|
+
role="user",
|
572
|
+
content=[TextContent(text=packaged_user_message)],
|
573
|
+
)
|
589
574
|
|
590
575
|
# Run the agent state forward
|
591
|
-
usage = self._step(actor=actor, agent_id=agent_id, input_messages=message)
|
576
|
+
usage = self._step(actor=actor, agent_id=agent_id, input_messages=[message])
|
592
577
|
return usage
|
593
578
|
|
594
579
|
def system_message(
|
@@ -660,23 +645,14 @@ class SyncServer(Server):
|
|
660
645
|
self,
|
661
646
|
actor: User,
|
662
647
|
agent_id: str,
|
663
|
-
|
648
|
+
input_messages: List[MessageCreate],
|
664
649
|
wrap_user_message: bool = True,
|
665
650
|
wrap_system_message: bool = True,
|
666
651
|
interface: Union[AgentInterface, ChatCompletionsStreamingInterface, None] = None, # needed for responses
|
667
652
|
metadata: Optional[dict] = None, # Pass through metadata to interface
|
668
653
|
put_inner_thoughts_first: bool = True,
|
669
654
|
) -> LettaUsageStatistics:
|
670
|
-
"""Send a list of messages to the agent.
|
671
|
-
|
672
|
-
If messages are of type MessageCreate, convert them to Message objects before sending.
|
673
|
-
"""
|
674
|
-
if all(isinstance(m, MessageCreate) for m in messages):
|
675
|
-
message_objects = [prepare_input_message_create(m, agent_id, wrap_user_message, wrap_system_message) for m in messages]
|
676
|
-
elif all(isinstance(m, Message) for m in messages):
|
677
|
-
message_objects = messages
|
678
|
-
else:
|
679
|
-
raise ValueError(f"All messages must be of type Message or MessageCreate, got {[type(m) for m in messages]}")
|
655
|
+
"""Send a list of messages to the agent."""
|
680
656
|
|
681
657
|
# Store metadata in interface if provided
|
682
658
|
if metadata and hasattr(interface, "metadata"):
|
@@ -686,7 +662,7 @@ class SyncServer(Server):
|
|
686
662
|
return self._step(
|
687
663
|
actor=actor,
|
688
664
|
agent_id=agent_id,
|
689
|
-
input_messages=
|
665
|
+
input_messages=input_messages,
|
690
666
|
interface=interface,
|
691
667
|
put_inner_thoughts_first=put_inner_thoughts_first,
|
692
668
|
)
|
@@ -703,8 +679,6 @@ class SyncServer(Server):
|
|
703
679
|
@trace_method
|
704
680
|
def get_cached_llm_config(self, **kwargs):
|
705
681
|
key = make_key(**kwargs)
|
706
|
-
print(self._llm_config_cache)
|
707
|
-
print("KEY", key)
|
708
682
|
if key not in self._llm_config_cache:
|
709
683
|
self._llm_config_cache[key] = self.get_llm_config_from_handle(**kwargs)
|
710
684
|
return self._llm_config_cache[key]
|
@@ -1019,12 +993,8 @@ class SyncServer(Server):
|
|
1019
993
|
agent = self.load_agent(agent_id=sleeptime_agent.id, actor=actor)
|
1020
994
|
for passage in self.list_data_source_passages(source_id=source.id, user_id=actor.id):
|
1021
995
|
agent.step(
|
1022
|
-
|
1023
|
-
|
1024
|
-
role="user",
|
1025
|
-
content=[TextContent(text=passage.text)],
|
1026
|
-
agent_id=sleeptime_agent.id,
|
1027
|
-
),
|
996
|
+
input_messages=[
|
997
|
+
MessageCreate(role="user", content=passage.text),
|
1028
998
|
]
|
1029
999
|
)
|
1030
1000
|
self.agent_manager.delete_agent(agent_id=sleeptime_agent.id, actor=actor)
|
@@ -1182,7 +1152,6 @@ class SyncServer(Server):
|
|
1182
1152
|
provider = self.get_provider_from_name(provider_name)
|
1183
1153
|
|
1184
1154
|
llm_configs = [config for config in provider.list_llm_models() if config.handle == handle]
|
1185
|
-
print("LLM CONFIGS", llm_configs)
|
1186
1155
|
if not llm_configs:
|
1187
1156
|
llm_configs = [config for config in provider.list_llm_models() if config.model == model_name]
|
1188
1157
|
if not llm_configs:
|
@@ -1195,8 +1164,6 @@ class SyncServer(Server):
|
|
1195
1164
|
if not llm_configs:
|
1196
1165
|
raise e
|
1197
1166
|
|
1198
|
-
print("CONFIGS", llm_configs)
|
1199
|
-
|
1200
1167
|
if len(llm_configs) == 1:
|
1201
1168
|
llm_config = llm_configs[0]
|
1202
1169
|
elif len(llm_configs) > 1:
|
@@ -1343,17 +1310,17 @@ class SyncServer(Server):
|
|
1343
1310
|
|
1344
1311
|
# Next, attempt to run the tool with the sandbox
|
1345
1312
|
try:
|
1346
|
-
|
1313
|
+
tool_execution_result = ToolExecutionSandbox(tool.name, tool_args, actor, tool_object=tool).run(
|
1347
1314
|
agent_state=agent_state, additional_env_vars=tool_env_vars
|
1348
1315
|
)
|
1349
1316
|
return ToolReturnMessage(
|
1350
1317
|
id="null",
|
1351
1318
|
tool_call_id="null",
|
1352
1319
|
date=get_utc_time(),
|
1353
|
-
status=
|
1354
|
-
tool_return=str(
|
1355
|
-
stdout=
|
1356
|
-
stderr=
|
1320
|
+
status=tool_execution_result.status,
|
1321
|
+
tool_return=str(tool_execution_result.func_return),
|
1322
|
+
stdout=tool_execution_result.stdout,
|
1323
|
+
stderr=tool_execution_result.stderr,
|
1357
1324
|
)
|
1358
1325
|
|
1359
1326
|
except Exception as e:
|
@@ -1567,7 +1534,7 @@ class SyncServer(Server):
|
|
1567
1534
|
agent_id: str,
|
1568
1535
|
actor: User,
|
1569
1536
|
# role: MessageRole,
|
1570
|
-
|
1537
|
+
input_messages: List[MessageCreate],
|
1571
1538
|
stream_steps: bool,
|
1572
1539
|
stream_tokens: bool,
|
1573
1540
|
# related to whether or not we return `LettaMessage`s or `Message`s
|
@@ -1647,7 +1614,7 @@ class SyncServer(Server):
|
|
1647
1614
|
self.send_messages,
|
1648
1615
|
actor=actor,
|
1649
1616
|
agent_id=agent_id,
|
1650
|
-
|
1617
|
+
input_messages=input_messages,
|
1651
1618
|
interface=streaming_interface,
|
1652
1619
|
metadata=metadata,
|
1653
1620
|
)
|
@@ -1701,7 +1668,7 @@ class SyncServer(Server):
|
|
1701
1668
|
self,
|
1702
1669
|
group_id: str,
|
1703
1670
|
actor: User,
|
1704
|
-
|
1671
|
+
input_messages: Union[List[Message], List[MessageCreate]],
|
1705
1672
|
stream_steps: bool,
|
1706
1673
|
stream_tokens: bool,
|
1707
1674
|
chat_completion_mode: bool = False,
|
@@ -1751,7 +1718,7 @@ class SyncServer(Server):
|
|
1751
1718
|
task = asyncio.create_task(
|
1752
1719
|
asyncio.to_thread(
|
1753
1720
|
letta_multi_agent.step,
|
1754
|
-
|
1721
|
+
input_messages=input_messages,
|
1755
1722
|
chaining=self.chaining,
|
1756
1723
|
max_chaining_steps=self.max_chaining_steps,
|
1757
1724
|
)
|
letta/services/agent_manager.py
CHANGED
@@ -161,7 +161,7 @@ class AgentManager:
|
|
161
161
|
# Basic CRUD operations
|
162
162
|
# ======================================================================================================================
|
163
163
|
@trace_method
|
164
|
-
def create_agent(self, agent_create: CreateAgent, actor: PydanticUser) -> PydanticAgentState:
|
164
|
+
def create_agent(self, agent_create: CreateAgent, actor: PydanticUser, _test_only_force_id: Optional[str] = None) -> PydanticAgentState:
|
165
165
|
# validate required configs
|
166
166
|
if not agent_create.llm_config or not agent_create.embedding_config:
|
167
167
|
raise ValueError("llm_config and embedding_config are required")
|
@@ -239,6 +239,10 @@ class AgentManager:
|
|
239
239
|
created_by_id=actor.id,
|
240
240
|
last_updated_by_id=actor.id,
|
241
241
|
)
|
242
|
+
|
243
|
+
if _test_only_force_id:
|
244
|
+
new_agent.id = _test_only_force_id
|
245
|
+
|
242
246
|
session.add(new_agent)
|
243
247
|
session.flush()
|
244
248
|
aid = new_agent.id
|
@@ -364,6 +368,7 @@ class AgentManager:
|
|
364
368
|
"base_template_id": agent_update.base_template_id,
|
365
369
|
"message_buffer_autoclear": agent_update.message_buffer_autoclear,
|
366
370
|
"enable_sleeptime": agent_update.enable_sleeptime,
|
371
|
+
"response_format": agent_update.response_format,
|
367
372
|
}
|
368
373
|
for col, val in scalar_updates.items():
|
369
374
|
if val is not None:
|