letta-nightly 0.6.34.dev20250302104001__py3-none-any.whl → 0.6.34.dev20250303230404__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +1 -1
- letta/agent.py +40 -15
- letta/agents/__init__.py +0 -0
- letta/agents/base_agent.py +51 -0
- letta/agents/ephemeral_agent.py +72 -0
- letta/agents/low_latency_agent.py +315 -0
- letta/constants.py +3 -1
- letta/functions/ast_parsers.py +50 -1
- letta/functions/helpers.py +79 -2
- letta/functions/schema_generator.py +3 -0
- letta/helpers/converters.py +3 -3
- letta/interfaces/__init__.py +0 -0
- letta/interfaces/openai_chat_completions_streaming_interface.py +109 -0
- letta/interfaces/utils.py +11 -0
- letta/llm_api/anthropic.py +9 -1
- letta/llm_api/azure_openai.py +3 -0
- letta/llm_api/google_ai.py +3 -0
- letta/llm_api/google_vertex.py +4 -0
- letta/llm_api/llm_api_tools.py +1 -1
- letta/llm_api/openai.py +6 -0
- letta/local_llm/chat_completion_proxy.py +6 -1
- letta/log.py +2 -2
- letta/orm/step.py +1 -0
- letta/orm/tool.py +1 -1
- letta/prompts/system/memgpt_convo_only.txt +3 -5
- letta/prompts/system/memgpt_memory_only.txt +29 -0
- letta/schemas/agent.py +0 -1
- letta/schemas/step.py +1 -1
- letta/schemas/tool.py +16 -2
- letta/server/rest_api/app.py +5 -1
- letta/server/rest_api/routers/v1/agents.py +32 -21
- letta/server/rest_api/routers/v1/identities.py +9 -1
- letta/server/rest_api/routers/v1/runs.py +49 -0
- letta/server/rest_api/routers/v1/tools.py +1 -0
- letta/server/rest_api/routers/v1/voice.py +19 -255
- letta/server/rest_api/utils.py +3 -2
- letta/server/server.py +15 -7
- letta/services/agent_manager.py +10 -6
- letta/services/helpers/agent_manager_helper.py +0 -2
- letta/services/helpers/tool_execution_helper.py +18 -0
- letta/services/job_manager.py +98 -0
- letta/services/step_manager.py +2 -0
- letta/services/summarizer/__init__.py +0 -0
- letta/services/summarizer/enums.py +9 -0
- letta/services/summarizer/summarizer.py +102 -0
- letta/services/tool_execution_sandbox.py +20 -3
- letta/services/tool_manager.py +1 -1
- letta/settings.py +2 -0
- letta/tracing.py +176 -156
- {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/METADATA +6 -5
- {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/RECORD +54 -44
- letta/chat_only_agent.py +0 -101
- {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/entry_points.txt +0 -0
letta/services/agent_manager.py
CHANGED
|
@@ -50,6 +50,7 @@ from letta.services.message_manager import MessageManager
|
|
|
50
50
|
from letta.services.source_manager import SourceManager
|
|
51
51
|
from letta.services.tool_manager import ToolManager
|
|
52
52
|
from letta.settings import settings
|
|
53
|
+
from letta.tracing import trace_method
|
|
53
54
|
from letta.utils import enforce_types, united_diff
|
|
54
55
|
|
|
55
56
|
logger = get_logger(__name__)
|
|
@@ -72,6 +73,7 @@ class AgentManager:
|
|
|
72
73
|
# ======================================================================================================================
|
|
73
74
|
# Basic CRUD operations
|
|
74
75
|
# ======================================================================================================================
|
|
76
|
+
@trace_method
|
|
75
77
|
@enforce_types
|
|
76
78
|
def create_agent(
|
|
77
79
|
self,
|
|
@@ -368,6 +370,7 @@ class AgentManager:
|
|
|
368
370
|
agent = AgentModel.read(db_session=session, name=agent_name, actor=actor)
|
|
369
371
|
return agent.to_pydantic()
|
|
370
372
|
|
|
373
|
+
@trace_method
|
|
371
374
|
@enforce_types
|
|
372
375
|
def delete_agent(self, agent_id: str, actor: PydanticUser) -> None:
|
|
373
376
|
"""
|
|
@@ -529,42 +532,43 @@ class AgentManager:
|
|
|
529
532
|
model=agent_state.llm_config.model,
|
|
530
533
|
openai_message_dict={"role": "system", "content": new_system_message_str},
|
|
531
534
|
)
|
|
535
|
+
# TODO: This seems kind of silly, why not just update the message?
|
|
532
536
|
message = self.message_manager.create_message(message, actor=actor)
|
|
533
537
|
message_ids = [message.id] + agent_state.message_ids[1:] # swap index 0 (system)
|
|
534
|
-
return self.
|
|
538
|
+
return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
|
|
535
539
|
else:
|
|
536
540
|
return agent_state
|
|
537
541
|
|
|
538
542
|
@enforce_types
|
|
539
|
-
def
|
|
543
|
+
def set_in_context_messages(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
|
|
540
544
|
return self.update_agent(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
|
|
541
545
|
|
|
542
546
|
@enforce_types
|
|
543
547
|
def trim_older_in_context_messages(self, num: int, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
544
548
|
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
|
|
545
549
|
new_messages = [message_ids[0]] + message_ids[num:] # 0 is system message
|
|
546
|
-
return self.
|
|
550
|
+
return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
|
|
547
551
|
|
|
548
552
|
@enforce_types
|
|
549
553
|
def trim_all_in_context_messages_except_system(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
550
554
|
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
|
|
551
555
|
# TODO: How do we know this?
|
|
552
556
|
new_messages = [message_ids[0]] # 0 is system message
|
|
553
|
-
return self.
|
|
557
|
+
return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
|
|
554
558
|
|
|
555
559
|
@enforce_types
|
|
556
560
|
def prepend_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
557
561
|
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
|
|
558
562
|
new_messages = self.message_manager.create_many_messages(messages, actor=actor)
|
|
559
563
|
message_ids = [message_ids[0]] + [m.id for m in new_messages] + message_ids[1:]
|
|
560
|
-
return self.
|
|
564
|
+
return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
|
|
561
565
|
|
|
562
566
|
@enforce_types
|
|
563
567
|
def append_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
564
568
|
messages = self.message_manager.create_many_messages(messages, actor=actor)
|
|
565
569
|
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids or []
|
|
566
570
|
message_ids += [m.id for m in messages]
|
|
567
|
-
return self.
|
|
571
|
+
return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
|
|
568
572
|
|
|
569
573
|
@enforce_types
|
|
570
574
|
def reset_messages(self, agent_id: str, actor: PydanticUser, add_default_initial_messages: bool = False) -> PydanticAgentState:
|
|
@@ -91,8 +91,6 @@ def derive_system_message(agent_type: AgentType, system: Optional[str] = None):
|
|
|
91
91
|
system = gpt_system.get_system_text("memgpt_chat")
|
|
92
92
|
elif agent_type == AgentType.offline_memory_agent:
|
|
93
93
|
system = gpt_system.get_system_text("memgpt_offline_memory")
|
|
94
|
-
elif agent_type == AgentType.chat_only_agent:
|
|
95
|
-
system = gpt_system.get_system_text("memgpt_convo_only")
|
|
96
94
|
else:
|
|
97
95
|
raise ValueError(f"Invalid agent type: {agent_type}")
|
|
98
96
|
|
|
@@ -4,6 +4,10 @@ import subprocess
|
|
|
4
4
|
import venv
|
|
5
5
|
from typing import Dict, Optional
|
|
6
6
|
|
|
7
|
+
from datamodel_code_generator import DataModelType, PythonVersion
|
|
8
|
+
from datamodel_code_generator.model import get_data_model_types
|
|
9
|
+
from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
|
|
10
|
+
|
|
7
11
|
from letta.log import get_logger
|
|
8
12
|
from letta.schemas.sandbox_config import LocalSandboxConfig
|
|
9
13
|
|
|
@@ -153,3 +157,17 @@ def create_venv_for_local_sandbox(sandbox_dir_path: str, venv_path: str, env: Di
|
|
|
153
157
|
except subprocess.CalledProcessError as e:
|
|
154
158
|
logger.error(f"Error while setting up the virtual environment: {e}")
|
|
155
159
|
raise RuntimeError(f"Failed to set up the virtual environment: {e}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def add_imports_and_pydantic_schemas_for_args(args_json_schema: dict) -> str:
|
|
163
|
+
data_model_types = get_data_model_types(DataModelType.PydanticV2BaseModel, target_python_version=PythonVersion.PY_311)
|
|
164
|
+
parser = JsonSchemaParser(
|
|
165
|
+
str(args_json_schema),
|
|
166
|
+
data_model_type=data_model_types.data_model,
|
|
167
|
+
data_model_root_type=data_model_types.root_model,
|
|
168
|
+
data_model_field_type=data_model_types.field_model,
|
|
169
|
+
data_type_manager_type=data_model_types.data_type_manager,
|
|
170
|
+
dump_resolve_reference_action=data_model_types.dump_resolve_reference_action,
|
|
171
|
+
)
|
|
172
|
+
result = parser.parse()
|
|
173
|
+
return result
|
letta/services/job_manager.py
CHANGED
|
@@ -13,12 +13,14 @@ from letta.orm.job_messages import JobMessage
|
|
|
13
13
|
from letta.orm.message import Message as MessageModel
|
|
14
14
|
from letta.orm.sqlalchemy_base import AccessType
|
|
15
15
|
from letta.orm.step import Step
|
|
16
|
+
from letta.orm.step import Step as StepModel
|
|
16
17
|
from letta.schemas.enums import JobStatus, MessageRole
|
|
17
18
|
from letta.schemas.job import Job as PydanticJob
|
|
18
19
|
from letta.schemas.job import JobUpdate, LettaRequestConfig
|
|
19
20
|
from letta.schemas.letta_message import LettaMessage
|
|
20
21
|
from letta.schemas.message import Message as PydanticMessage
|
|
21
22
|
from letta.schemas.run import Run as PydanticRun
|
|
23
|
+
from letta.schemas.step import Step as PydanticStep
|
|
22
24
|
from letta.schemas.usage import LettaUsageStatistics
|
|
23
25
|
from letta.schemas.user import User as PydanticUser
|
|
24
26
|
from letta.utils import enforce_types
|
|
@@ -161,6 +163,51 @@ class JobManager:
|
|
|
161
163
|
|
|
162
164
|
return [message.to_pydantic() for message in messages]
|
|
163
165
|
|
|
166
|
+
@enforce_types
|
|
167
|
+
def get_job_steps(
|
|
168
|
+
self,
|
|
169
|
+
job_id: str,
|
|
170
|
+
actor: PydanticUser,
|
|
171
|
+
before: Optional[str] = None,
|
|
172
|
+
after: Optional[str] = None,
|
|
173
|
+
limit: Optional[int] = 100,
|
|
174
|
+
ascending: bool = True,
|
|
175
|
+
) -> List[PydanticStep]:
|
|
176
|
+
"""
|
|
177
|
+
Get all steps associated with a job.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
job_id: The ID of the job to get steps for
|
|
181
|
+
actor: The user making the request
|
|
182
|
+
before: Cursor for pagination
|
|
183
|
+
after: Cursor for pagination
|
|
184
|
+
limit: Maximum number of steps to return
|
|
185
|
+
ascending: Optional flag to sort in ascending order
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of steps associated with the job
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
NoResultFound: If the job does not exist or user does not have access
|
|
192
|
+
"""
|
|
193
|
+
with self.session_maker() as session:
|
|
194
|
+
# Build filters
|
|
195
|
+
filters = {}
|
|
196
|
+
filters["job_id"] = job_id
|
|
197
|
+
|
|
198
|
+
# Get steps
|
|
199
|
+
steps = StepModel.list(
|
|
200
|
+
db_session=session,
|
|
201
|
+
before=before,
|
|
202
|
+
after=after,
|
|
203
|
+
ascending=ascending,
|
|
204
|
+
limit=limit,
|
|
205
|
+
actor=actor,
|
|
206
|
+
**filters,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return [step.to_pydantic() for step in steps]
|
|
210
|
+
|
|
164
211
|
@enforce_types
|
|
165
212
|
def add_message_to_job(self, job_id: str, message_id: str, actor: PydanticUser) -> None:
|
|
166
213
|
"""
|
|
@@ -312,6 +359,57 @@ class JobManager:
|
|
|
312
359
|
|
|
313
360
|
return messages
|
|
314
361
|
|
|
362
|
+
@enforce_types
|
|
363
|
+
def get_step_messages(
|
|
364
|
+
self,
|
|
365
|
+
run_id: str,
|
|
366
|
+
actor: PydanticUser,
|
|
367
|
+
before: Optional[str] = None,
|
|
368
|
+
after: Optional[str] = None,
|
|
369
|
+
limit: Optional[int] = 100,
|
|
370
|
+
role: Optional[MessageRole] = None,
|
|
371
|
+
ascending: bool = True,
|
|
372
|
+
) -> List[LettaMessage]:
|
|
373
|
+
"""
|
|
374
|
+
Get steps associated with a job using cursor-based pagination.
|
|
375
|
+
This is a wrapper around get_job_messages that provides cursor-based pagination.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
run_id: The ID of the run to get steps for
|
|
379
|
+
actor: The user making the request
|
|
380
|
+
before: Message ID to get messages after
|
|
381
|
+
after: Message ID to get messages before
|
|
382
|
+
limit: Maximum number of messages to return
|
|
383
|
+
ascending: Whether to return messages in ascending order
|
|
384
|
+
role: Optional role filter
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
List of Steps associated with the job
|
|
388
|
+
|
|
389
|
+
Raises:
|
|
390
|
+
NoResultFound: If the job does not exist or user does not have access
|
|
391
|
+
"""
|
|
392
|
+
messages = self.get_job_messages(
|
|
393
|
+
job_id=run_id,
|
|
394
|
+
actor=actor,
|
|
395
|
+
before=before,
|
|
396
|
+
after=after,
|
|
397
|
+
limit=limit,
|
|
398
|
+
role=role,
|
|
399
|
+
ascending=ascending,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
request_config = self._get_run_request_config(run_id)
|
|
403
|
+
|
|
404
|
+
messages = PydanticMessage.to_letta_messages_from_list(
|
|
405
|
+
messages=messages,
|
|
406
|
+
use_assistant_message=request_config["use_assistant_message"],
|
|
407
|
+
assistant_message_tool_name=request_config["assistant_message_tool_name"],
|
|
408
|
+
assistant_message_tool_kwarg=request_config["assistant_message_tool_kwarg"],
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
return messages
|
|
412
|
+
|
|
315
413
|
def _verify_job_access(
|
|
316
414
|
self,
|
|
317
415
|
session: Session,
|
letta/services/step_manager.py
CHANGED
|
@@ -11,6 +11,7 @@ from letta.orm.step import Step as StepModel
|
|
|
11
11
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
12
12
|
from letta.schemas.step import Step as PydanticStep
|
|
13
13
|
from letta.schemas.user import User as PydanticUser
|
|
14
|
+
from letta.tracing import get_trace_id
|
|
14
15
|
from letta.utils import enforce_types
|
|
15
16
|
|
|
16
17
|
|
|
@@ -75,6 +76,7 @@ class StepManager:
|
|
|
75
76
|
"job_id": job_id,
|
|
76
77
|
"tags": [],
|
|
77
78
|
"tid": None,
|
|
79
|
+
"trace_id": get_trace_id(), # Get the current trace ID
|
|
78
80
|
}
|
|
79
81
|
with self.session_maker() as session:
|
|
80
82
|
if job_id:
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from json import JSONDecodeError
|
|
3
|
+
from typing import List, Tuple
|
|
4
|
+
|
|
5
|
+
from letta.agents.base_agent import BaseAgent
|
|
6
|
+
from letta.schemas.enums import MessageRole
|
|
7
|
+
from letta.schemas.message import Message
|
|
8
|
+
from letta.schemas.openai.chat_completion_request import UserMessage
|
|
9
|
+
from letta.services.summarizer.enums import SummarizationMode
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Summarizer:
|
|
13
|
+
"""
|
|
14
|
+
Handles summarization or trimming of conversation messages based on
|
|
15
|
+
the specified SummarizationMode. For now, we demonstrate a simple
|
|
16
|
+
static buffer approach but leave room for more advanced strategies.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, mode: SummarizationMode, summarizer_agent: BaseAgent, message_buffer_limit: int = 10, message_buffer_min: int = 3):
|
|
20
|
+
self.mode = mode
|
|
21
|
+
|
|
22
|
+
# Need to do validation on this
|
|
23
|
+
self.message_buffer_limit = message_buffer_limit
|
|
24
|
+
self.message_buffer_min = message_buffer_min
|
|
25
|
+
self.summarizer_agent = summarizer_agent
|
|
26
|
+
# TODO: Move this to config
|
|
27
|
+
self.summary_prefix = "Out of context message summarization:\n"
|
|
28
|
+
|
|
29
|
+
async def summarize(
|
|
30
|
+
self, in_context_messages: List[Message], new_letta_messages: List[Message], previous_summary: str
|
|
31
|
+
) -> Tuple[List[Message], str, bool]:
|
|
32
|
+
"""
|
|
33
|
+
Summarizes or trims in_context_messages according to the chosen mode,
|
|
34
|
+
and returns the updated messages plus any optional "summary message".
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
in_context_messages: The existing messages in the conversation's context.
|
|
38
|
+
new_letta_messages: The newly added Letta messages (just appended).
|
|
39
|
+
previous_summary: The previous summary string.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
(updated_messages, summary_message)
|
|
43
|
+
updated_messages: The new context after trimming/summary
|
|
44
|
+
summary_message: Optional summarization message that was created
|
|
45
|
+
(could be appended to the conversation if desired)
|
|
46
|
+
"""
|
|
47
|
+
if self.mode == SummarizationMode.STATIC_MESSAGE_BUFFER:
|
|
48
|
+
return await self._static_buffer_summarization(in_context_messages, new_letta_messages, previous_summary)
|
|
49
|
+
else:
|
|
50
|
+
# Fallback or future logic
|
|
51
|
+
return in_context_messages, "", False
|
|
52
|
+
|
|
53
|
+
async def _static_buffer_summarization(
|
|
54
|
+
self, in_context_messages: List[Message], new_letta_messages: List[Message], previous_summary: str
|
|
55
|
+
) -> Tuple[List[Message], str, bool]:
|
|
56
|
+
previous_summary = previous_summary[: len(self.summary_prefix)]
|
|
57
|
+
all_in_context_messages = in_context_messages + new_letta_messages
|
|
58
|
+
|
|
59
|
+
# Only summarize if we exceed `message_buffer_limit`
|
|
60
|
+
if len(all_in_context_messages) <= self.message_buffer_limit:
|
|
61
|
+
return all_in_context_messages, previous_summary, False
|
|
62
|
+
|
|
63
|
+
# Aim to trim down to `message_buffer_min`
|
|
64
|
+
target_trim_index = len(all_in_context_messages) - self.message_buffer_min + 1
|
|
65
|
+
|
|
66
|
+
# Move the trim index forward until it's at a `MessageRole.user`
|
|
67
|
+
while target_trim_index < len(all_in_context_messages) and all_in_context_messages[target_trim_index].role != MessageRole.user:
|
|
68
|
+
target_trim_index += 1
|
|
69
|
+
|
|
70
|
+
# TODO: Assuming system message is always at index 0
|
|
71
|
+
updated_in_context_messages = [all_in_context_messages[0]] + all_in_context_messages[target_trim_index:]
|
|
72
|
+
out_of_context_messages = all_in_context_messages[:target_trim_index]
|
|
73
|
+
|
|
74
|
+
formatted_messages = []
|
|
75
|
+
for m in out_of_context_messages:
|
|
76
|
+
if m.content:
|
|
77
|
+
try:
|
|
78
|
+
message = json.loads(m.content[0].text).get("message")
|
|
79
|
+
except JSONDecodeError:
|
|
80
|
+
continue
|
|
81
|
+
if message:
|
|
82
|
+
formatted_messages.append(f"{m.role.value}: {message}")
|
|
83
|
+
|
|
84
|
+
# If we didn't trim any messages, return as-is
|
|
85
|
+
if not formatted_messages:
|
|
86
|
+
return all_in_context_messages, previous_summary, False
|
|
87
|
+
|
|
88
|
+
# Generate summarization request
|
|
89
|
+
summary_request_text = (
|
|
90
|
+
"These are messages that are soon to be removed from the context window:\n"
|
|
91
|
+
f"{formatted_messages}\n\n"
|
|
92
|
+
"This is the current memory:\n"
|
|
93
|
+
f"{previous_summary}\n\n"
|
|
94
|
+
"Your task is to integrate any relevant updates from the messages into the memory."
|
|
95
|
+
"It should be in note-taking format in natural English. You are to return the new, updated memory only."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
messages = await self.summarizer_agent.step(UserMessage(content=summary_request_text))
|
|
99
|
+
current_summary = "\n".join([m.text for m in messages])
|
|
100
|
+
current_summary = f"{self.summary_prefix}{current_summary}"
|
|
101
|
+
|
|
102
|
+
return updated_in_context_messages, current_summary, True
|
|
@@ -11,12 +11,14 @@ import traceback
|
|
|
11
11
|
import uuid
|
|
12
12
|
from typing import Any, Dict, Optional
|
|
13
13
|
|
|
14
|
+
from letta.functions.helpers import generate_model_from_args_json_schema
|
|
14
15
|
from letta.log import get_logger
|
|
15
16
|
from letta.schemas.agent import AgentState
|
|
16
17
|
from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult, SandboxType
|
|
17
18
|
from letta.schemas.tool import Tool
|
|
18
19
|
from letta.schemas.user import User
|
|
19
20
|
from letta.services.helpers.tool_execution_helper import (
|
|
21
|
+
add_imports_and_pydantic_schemas_for_args,
|
|
20
22
|
create_venv_for_local_sandbox,
|
|
21
23
|
find_python_executable,
|
|
22
24
|
install_pip_requirements_for_sandbox,
|
|
@@ -408,20 +410,35 @@ class ToolExecutionSandbox:
|
|
|
408
410
|
code += "import sys\n"
|
|
409
411
|
code += "import base64\n"
|
|
410
412
|
|
|
411
|
-
#
|
|
413
|
+
# imports to support agent state
|
|
412
414
|
if agent_state:
|
|
413
415
|
code += "import letta\n"
|
|
414
416
|
code += "from letta import * \n"
|
|
415
417
|
import pickle
|
|
416
418
|
|
|
419
|
+
if self.tool.args_json_schema:
|
|
420
|
+
schema_code = add_imports_and_pydantic_schemas_for_args(self.tool.args_json_schema)
|
|
421
|
+
if "from __future__ import annotations" in schema_code:
|
|
422
|
+
schema_code = schema_code.replace("from __future__ import annotations", "").lstrip()
|
|
423
|
+
code = "from __future__ import annotations\n\n" + code
|
|
424
|
+
code += schema_code + "\n"
|
|
425
|
+
|
|
426
|
+
# load the agent state
|
|
427
|
+
if agent_state:
|
|
417
428
|
agent_state_pickle = pickle.dumps(agent_state)
|
|
418
429
|
code += f"agent_state = pickle.loads({agent_state_pickle})\n"
|
|
419
430
|
else:
|
|
420
431
|
# agent state is None
|
|
421
432
|
code += "agent_state = None\n"
|
|
422
433
|
|
|
423
|
-
|
|
424
|
-
|
|
434
|
+
if self.tool.args_json_schema:
|
|
435
|
+
args_schema = generate_model_from_args_json_schema(self.tool.args_json_schema)
|
|
436
|
+
code += f"args_object = {args_schema.__name__}(**{self.args})\n"
|
|
437
|
+
for param in self.args:
|
|
438
|
+
code += f"{param} = args_object.{param}\n"
|
|
439
|
+
else:
|
|
440
|
+
for param in self.args:
|
|
441
|
+
code += self.initialize_param(param, self.args[param])
|
|
425
442
|
|
|
426
443
|
if "agent_state" in self.parse_function_arguments(self.tool.source_code, self.tool.name):
|
|
427
444
|
inject_agent_state = True
|
letta/services/tool_manager.py
CHANGED
|
@@ -42,7 +42,7 @@ class ToolManager:
|
|
|
42
42
|
tool = self.get_tool_by_name(tool_name=pydantic_tool.name, actor=actor)
|
|
43
43
|
if tool:
|
|
44
44
|
# Put to dict and remove fields that should not be reset
|
|
45
|
-
update_data = pydantic_tool.model_dump(
|
|
45
|
+
update_data = pydantic_tool.model_dump(exclude_unset=True, exclude_none=True)
|
|
46
46
|
|
|
47
47
|
# If there's anything to update
|
|
48
48
|
if update_data:
|
letta/settings.py
CHANGED
|
@@ -50,6 +50,8 @@ class ModelSettings(BaseSettings):
|
|
|
50
50
|
|
|
51
51
|
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
|
52
52
|
|
|
53
|
+
global_max_context_window_limit: int = 32000
|
|
54
|
+
|
|
53
55
|
# env_prefix='my_prefix_'
|
|
54
56
|
|
|
55
57
|
# when we use /completions APIs (instead of /chat/completions), we need to specify a model wrapper
|