letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604201135__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 +7 -1
- letta/agent.py +14 -7
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/RECORD +136 -110
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/entry_points.txt +0 -0
@@ -192,6 +192,15 @@ class SandboxConfigManager:
|
|
192
192
|
sandbox.hard_delete(db_session=session, actor=actor)
|
193
193
|
return sandbox.to_pydantic()
|
194
194
|
|
195
|
+
@enforce_types
|
196
|
+
@trace_method
|
197
|
+
async def delete_sandbox_config_async(self, sandbox_config_id: str, actor: PydanticUser) -> PydanticSandboxConfig:
|
198
|
+
"""Delete a sandbox configuration by its ID."""
|
199
|
+
async with db_registry.async_session() as session:
|
200
|
+
sandbox = await SandboxConfigModel.read_async(db_session=session, identifier=sandbox_config_id, actor=actor)
|
201
|
+
await sandbox.hard_delete_async(db_session=session, actor=actor)
|
202
|
+
return sandbox.to_pydantic()
|
203
|
+
|
195
204
|
@enforce_types
|
196
205
|
@trace_method
|
197
206
|
def list_sandbox_configs(
|
@@ -305,6 +314,34 @@ class SandboxConfigManager:
|
|
305
314
|
env_var.create(session, actor=actor)
|
306
315
|
return env_var.to_pydantic()
|
307
316
|
|
317
|
+
@enforce_types
|
318
|
+
@trace_method
|
319
|
+
async def create_sandbox_env_var_async(
|
320
|
+
self, env_var_create: SandboxEnvironmentVariableCreate, sandbox_config_id: str, actor: PydanticUser
|
321
|
+
) -> PydanticEnvVar:
|
322
|
+
"""Create a new sandbox environment variable."""
|
323
|
+
env_var = PydanticEnvVar(**env_var_create.model_dump(), sandbox_config_id=sandbox_config_id, organization_id=actor.organization_id)
|
324
|
+
|
325
|
+
db_env_var = await self.get_sandbox_env_var_by_key_and_sandbox_config_id_async(env_var.key, env_var.sandbox_config_id, actor=actor)
|
326
|
+
if db_env_var:
|
327
|
+
update_data = env_var.model_dump(exclude_unset=True, exclude_none=True)
|
328
|
+
update_data = {key: value for key, value in update_data.items() if getattr(db_env_var, key) != value}
|
329
|
+
# If there are changes, update the environment variable
|
330
|
+
if update_data:
|
331
|
+
db_env_var = await self.update_sandbox_env_var_async(db_env_var.id, SandboxEnvironmentVariableUpdate(**update_data), actor)
|
332
|
+
else:
|
333
|
+
printd(
|
334
|
+
f"`create_or_update_sandbox_env_var` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
335
|
+
f"key={env_var.key}, but found existing variable with nothing to update."
|
336
|
+
)
|
337
|
+
|
338
|
+
return db_env_var
|
339
|
+
else:
|
340
|
+
async with db_registry.async_session() as session:
|
341
|
+
env_var = SandboxEnvVarModel(**env_var.model_dump(to_orm=True, exclude_none=True))
|
342
|
+
await env_var.create_async(session, actor=actor)
|
343
|
+
return env_var.to_pydantic()
|
344
|
+
|
308
345
|
@enforce_types
|
309
346
|
@trace_method
|
310
347
|
def update_sandbox_env_var(
|
@@ -327,6 +364,28 @@ class SandboxConfigManager:
|
|
327
364
|
)
|
328
365
|
return env_var.to_pydantic()
|
329
366
|
|
367
|
+
@enforce_types
|
368
|
+
@trace_method
|
369
|
+
async def update_sandbox_env_var_async(
|
370
|
+
self, env_var_id: str, env_var_update: SandboxEnvironmentVariableUpdate, actor: PydanticUser
|
371
|
+
) -> PydanticEnvVar:
|
372
|
+
"""Update an existing sandbox environment variable."""
|
373
|
+
async with db_registry.async_session() as session:
|
374
|
+
env_var = await SandboxEnvVarModel.read_async(db_session=session, identifier=env_var_id, actor=actor)
|
375
|
+
update_data = env_var_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
376
|
+
update_data = {key: value for key, value in update_data.items() if getattr(env_var, key) != value}
|
377
|
+
|
378
|
+
if update_data:
|
379
|
+
for key, value in update_data.items():
|
380
|
+
setattr(env_var, key, value)
|
381
|
+
await env_var.update_async(db_session=session, actor=actor)
|
382
|
+
else:
|
383
|
+
printd(
|
384
|
+
f"`update_sandbox_env_var` called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
385
|
+
f"key={env_var.key}, but nothing to update."
|
386
|
+
)
|
387
|
+
return env_var.to_pydantic()
|
388
|
+
|
330
389
|
@enforce_types
|
331
390
|
@trace_method
|
332
391
|
def delete_sandbox_env_var(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
|
@@ -336,6 +395,15 @@ class SandboxConfigManager:
|
|
336
395
|
env_var.hard_delete(db_session=session, actor=actor)
|
337
396
|
return env_var.to_pydantic()
|
338
397
|
|
398
|
+
@enforce_types
|
399
|
+
@trace_method
|
400
|
+
async def delete_sandbox_env_var_async(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
|
401
|
+
"""Delete a sandbox environment variable by its ID."""
|
402
|
+
async with db_registry.async_session() as session:
|
403
|
+
env_var = await SandboxEnvVarModel.read_async(db_session=session, identifier=env_var_id, actor=actor)
|
404
|
+
await env_var.hard_delete_async(db_session=session, actor=actor)
|
405
|
+
return env_var.to_pydantic()
|
406
|
+
|
339
407
|
@enforce_types
|
340
408
|
@trace_method
|
341
409
|
def list_sandbox_env_vars(
|
@@ -392,6 +460,22 @@ class SandboxConfigManager:
|
|
392
460
|
)
|
393
461
|
return [env_var.to_pydantic() for env_var in env_vars]
|
394
462
|
|
463
|
+
@enforce_types
|
464
|
+
@trace_method
|
465
|
+
async def list_sandbox_env_vars_by_key_async(
|
466
|
+
self, key: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
|
467
|
+
) -> List[PydanticEnvVar]:
|
468
|
+
"""List all sandbox environment variables with optional pagination."""
|
469
|
+
async with db_registry.async_session() as session:
|
470
|
+
env_vars = await SandboxEnvVarModel.list_async(
|
471
|
+
db_session=session,
|
472
|
+
after=after,
|
473
|
+
limit=limit,
|
474
|
+
organization_id=actor.organization_id,
|
475
|
+
key=key,
|
476
|
+
)
|
477
|
+
return [env_var.to_pydantic() for env_var in env_vars]
|
478
|
+
|
395
479
|
@enforce_types
|
396
480
|
@trace_method
|
397
481
|
def get_sandbox_env_vars_as_dict(
|
@@ -434,3 +518,24 @@ class SandboxConfigManager:
|
|
434
518
|
return None
|
435
519
|
except NoResultFound:
|
436
520
|
return None
|
521
|
+
|
522
|
+
@enforce_types
|
523
|
+
@trace_method
|
524
|
+
async def get_sandbox_env_var_by_key_and_sandbox_config_id_async(
|
525
|
+
self, key: str, sandbox_config_id: str, actor: Optional[PydanticUser] = None
|
526
|
+
) -> Optional[PydanticEnvVar]:
|
527
|
+
"""Retrieve a sandbox environment variable by its key and sandbox_config_id."""
|
528
|
+
async with db_registry.async_session() as session:
|
529
|
+
try:
|
530
|
+
env_var = await SandboxEnvVarModel.list_async(
|
531
|
+
db_session=session,
|
532
|
+
key=key,
|
533
|
+
sandbox_config_id=sandbox_config_id,
|
534
|
+
organization_id=actor.organization_id,
|
535
|
+
limit=1,
|
536
|
+
)
|
537
|
+
if env_var:
|
538
|
+
return env_var[0].to_pydantic()
|
539
|
+
return None
|
540
|
+
except NoResultFound:
|
541
|
+
return None
|
letta/services/source_manager.py
CHANGED
@@ -31,7 +31,7 @@ class SourceManager:
|
|
31
31
|
source.organization_id = actor.organization_id
|
32
32
|
source = SourceModel(**source.model_dump(to_orm=True, exclude_none=True))
|
33
33
|
await source.create_async(session, actor=actor)
|
34
|
-
|
34
|
+
return source.to_pydantic()
|
35
35
|
|
36
36
|
@enforce_types
|
37
37
|
@trace_method
|
@@ -48,7 +48,7 @@ class SourceManager:
|
|
48
48
|
if update_data:
|
49
49
|
for key, value in update_data.items():
|
50
50
|
setattr(source, key, value)
|
51
|
-
source.
|
51
|
+
await source.update_async(db_session=session, actor=actor)
|
52
52
|
else:
|
53
53
|
printd(
|
54
54
|
f"`update_source` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={source.name}, but found existing source with nothing to update."
|
@@ -83,7 +83,7 @@ class SourceManager:
|
|
83
83
|
|
84
84
|
@enforce_types
|
85
85
|
@trace_method
|
86
|
-
async def
|
86
|
+
async def size_async(self, actor: PydanticUser) -> int:
|
87
87
|
"""
|
88
88
|
Get the total count of sources for the given user.
|
89
89
|
"""
|
@@ -152,7 +152,7 @@ class SourceManager:
|
|
152
152
|
file_metadata.organization_id = actor.organization_id
|
153
153
|
file_metadata = FileMetadataModel(**file_metadata.model_dump(to_orm=True, exclude_none=True))
|
154
154
|
await file_metadata.create_async(session, actor=actor)
|
155
|
-
|
155
|
+
return file_metadata.to_pydantic()
|
156
156
|
|
157
157
|
# TODO: We make actor optional for now, but should most likely be enforced due to security reasons
|
158
158
|
@enforce_types
|
@@ -173,7 +173,6 @@ class SourceManager:
|
|
173
173
|
) -> List[PydanticFileMetadata]:
|
174
174
|
"""List all files with optional pagination."""
|
175
175
|
async with db_registry.async_session() as session:
|
176
|
-
files_all = await FileMetadataModel.list_async(db_session=session, organization_id=actor.organization_id, source_id=source_id)
|
177
176
|
files = await FileMetadataModel.list_async(
|
178
177
|
db_session=session, after=after, limit=limit, organization_id=actor.organization_id, source_id=source_id
|
179
178
|
)
|
letta/services/step_manager.py
CHANGED
@@ -22,7 +22,7 @@ class StepManager:
|
|
22
22
|
|
23
23
|
@enforce_types
|
24
24
|
@trace_method
|
25
|
-
def
|
25
|
+
async def list_steps_async(
|
26
26
|
self,
|
27
27
|
actor: PydanticUser,
|
28
28
|
before: Optional[str] = None,
|
@@ -33,16 +33,19 @@ class StepManager:
|
|
33
33
|
order: Optional[str] = None,
|
34
34
|
model: Optional[str] = None,
|
35
35
|
agent_id: Optional[str] = None,
|
36
|
+
trace_ids: Optional[list[str]] = None,
|
36
37
|
) -> List[PydanticStep]:
|
37
38
|
"""List all jobs with optional pagination and status filter."""
|
38
|
-
with db_registry.
|
39
|
+
async with db_registry.async_session() as session:
|
39
40
|
filter_kwargs = {"organization_id": actor.organization_id}
|
40
41
|
if model:
|
41
42
|
filter_kwargs["model"] = model
|
42
43
|
if agent_id:
|
43
44
|
filter_kwargs["agent_id"] = agent_id
|
45
|
+
if trace_ids:
|
46
|
+
filter_kwargs["trace_id"] = trace_ids
|
44
47
|
|
45
|
-
steps = StepModel.
|
48
|
+
steps = await StepModel.list_async(
|
46
49
|
db_session=session,
|
47
50
|
before=before,
|
48
51
|
after=after,
|
@@ -142,9 +145,9 @@ class StepManager:
|
|
142
145
|
|
143
146
|
@enforce_types
|
144
147
|
@trace_method
|
145
|
-
def
|
146
|
-
with db_registry.
|
147
|
-
step = StepModel.
|
148
|
+
async def get_step_async(self, step_id: str, actor: PydanticUser) -> PydanticStep:
|
149
|
+
async with db_registry.async_session() as session:
|
150
|
+
step = await StepModel.read_async(db_session=session, identifier=step_id, actor=actor)
|
148
151
|
return step.to_pydantic()
|
149
152
|
|
150
153
|
@enforce_types
|
@@ -1,14 +1,16 @@
|
|
1
1
|
import asyncio
|
2
2
|
import json
|
3
3
|
import traceback
|
4
|
-
from typing import List, Optional, Tuple
|
4
|
+
from typing import List, Optional, Tuple, Union
|
5
5
|
|
6
|
+
from letta.agents.ephemeral_summary_agent import EphemeralSummaryAgent
|
6
7
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
7
8
|
from letta.log import get_logger
|
8
9
|
from letta.schemas.enums import MessageRole
|
9
10
|
from letta.schemas.letta_message_content import TextContent
|
10
11
|
from letta.schemas.message import Message, MessageCreate
|
11
12
|
from letta.services.summarizer.enums import SummarizationMode
|
13
|
+
from letta.tracing import trace_method
|
12
14
|
|
13
15
|
logger = get_logger(__name__)
|
14
16
|
|
@@ -23,7 +25,7 @@ class Summarizer:
|
|
23
25
|
def __init__(
|
24
26
|
self,
|
25
27
|
mode: SummarizationMode,
|
26
|
-
summarizer_agent: Optional["VoiceSleeptimeAgent"] = None,
|
28
|
+
summarizer_agent: Optional[Union[EphemeralSummaryAgent, "VoiceSleeptimeAgent"]] = None,
|
27
29
|
message_buffer_limit: int = 10,
|
28
30
|
message_buffer_min: int = 3,
|
29
31
|
):
|
@@ -35,7 +37,10 @@ class Summarizer:
|
|
35
37
|
self.summarizer_agent = summarizer_agent
|
36
38
|
# TODO: Move this to config
|
37
39
|
|
38
|
-
|
40
|
+
@trace_method
|
41
|
+
def summarize(
|
42
|
+
self, in_context_messages: List[Message], new_letta_messages: List[Message], force: bool = False, clear: bool = False
|
43
|
+
) -> Tuple[List[Message], bool]:
|
39
44
|
"""
|
40
45
|
Summarizes or trims in_context_messages according to the chosen mode,
|
41
46
|
and returns the updated messages plus any optional "summary message".
|
@@ -43,6 +48,7 @@ class Summarizer:
|
|
43
48
|
Args:
|
44
49
|
in_context_messages: The existing messages in the conversation's context.
|
45
50
|
new_letta_messages: The newly added Letta messages (just appended).
|
51
|
+
force: Force summarize even if the criteria is not met
|
46
52
|
|
47
53
|
Returns:
|
48
54
|
(updated_messages, summary_message)
|
@@ -51,7 +57,7 @@ class Summarizer:
|
|
51
57
|
(could be appended to the conversation if desired)
|
52
58
|
"""
|
53
59
|
if self.mode == SummarizationMode.STATIC_MESSAGE_BUFFER:
|
54
|
-
return self._static_buffer_summarization(in_context_messages, new_letta_messages)
|
60
|
+
return self._static_buffer_summarization(in_context_messages, new_letta_messages, force=force, clear=clear)
|
55
61
|
else:
|
56
62
|
# Fallback or future logic
|
57
63
|
return in_context_messages, False
|
@@ -69,42 +75,48 @@ class Summarizer:
|
|
69
75
|
return task
|
70
76
|
|
71
77
|
def _static_buffer_summarization(
|
72
|
-
self, in_context_messages: List[Message], new_letta_messages: List[Message]
|
78
|
+
self, in_context_messages: List[Message], new_letta_messages: List[Message], force: bool = False, clear: bool = False
|
73
79
|
) -> Tuple[List[Message], bool]:
|
74
80
|
all_in_context_messages = in_context_messages + new_letta_messages
|
75
81
|
|
76
|
-
if len(all_in_context_messages) <= self.message_buffer_limit:
|
82
|
+
if len(all_in_context_messages) <= self.message_buffer_limit and not force:
|
77
83
|
logger.info(
|
78
84
|
f"Nothing to evict, returning in context messages as is. Current buffer length is {len(all_in_context_messages)}, limit is {self.message_buffer_limit}."
|
79
85
|
)
|
80
86
|
return all_in_context_messages, False
|
81
87
|
|
82
|
-
|
88
|
+
retain_count = 0 if clear else self.message_buffer_min
|
89
|
+
|
90
|
+
if not force:
|
91
|
+
logger.info(f"Buffer length hit {self.message_buffer_limit}, evicting until we retain only {retain_count} messages.")
|
92
|
+
else:
|
93
|
+
logger.info(f"Requested force summarization, evicting until we retain only {retain_count} messages.")
|
83
94
|
|
84
|
-
target_trim_index = len(all_in_context_messages) -
|
95
|
+
target_trim_index = max(1, len(all_in_context_messages) - retain_count)
|
85
96
|
|
86
97
|
while target_trim_index < len(all_in_context_messages) and all_in_context_messages[target_trim_index].role != MessageRole.user:
|
87
98
|
target_trim_index += 1
|
88
99
|
|
89
|
-
|
100
|
+
evicted_messages = all_in_context_messages[1:target_trim_index] # everything except sys msg
|
101
|
+
updated_in_context_messages = all_in_context_messages[target_trim_index:] # may be empty
|
90
102
|
|
91
|
-
#
|
92
|
-
if not
|
93
|
-
logger.info("Nothing to evict, returning in
|
103
|
+
# If *no* messages were evicted we really have nothing to do
|
104
|
+
if not evicted_messages:
|
105
|
+
logger.info("Nothing to evict, returning in-context messages as-is.")
|
94
106
|
return all_in_context_messages, False
|
95
107
|
|
96
108
|
if self.summarizer_agent:
|
97
109
|
# Only invoke if summarizer agent is passed in
|
98
|
-
|
99
|
-
evicted_messages = all_in_context_messages[1:target_trim_index]
|
100
|
-
|
101
110
|
# Format
|
102
111
|
formatted_evicted_messages = format_transcript(evicted_messages)
|
103
112
|
formatted_in_context_messages = format_transcript(updated_in_context_messages)
|
104
113
|
|
105
114
|
# TODO: This is hyperspecific to voice, generalize!
|
106
115
|
# Update the message transcript of the memory agent
|
107
|
-
self.summarizer_agent
|
116
|
+
if not isinstance(self.summarizer_agent, EphemeralSummaryAgent):
|
117
|
+
self.summarizer_agent.update_message_transcript(
|
118
|
+
message_transcripts=formatted_evicted_messages + formatted_in_context_messages
|
119
|
+
)
|
108
120
|
|
109
121
|
# Add line numbers to the formatted messages
|
110
122
|
offset = len(formatted_evicted_messages)
|
@@ -113,14 +125,28 @@ class Summarizer:
|
|
113
125
|
|
114
126
|
evicted_messages_str = "\n".join(formatted_evicted_messages)
|
115
127
|
in_context_messages_str = "\n".join(formatted_in_context_messages)
|
116
|
-
|
128
|
+
# Base prompt
|
129
|
+
prompt_header = (
|
130
|
+
f"You’re a memory-recall helper for an AI that can only keep the last {retain_count} messages. "
|
131
|
+
"Scan the conversation history, focusing on messages about to drop out of that window, "
|
132
|
+
"and write crisp notes that capture any important facts or insights about the conversation history so they aren’t lost."
|
133
|
+
)
|
117
134
|
|
118
|
-
|
119
|
-
|
135
|
+
# Sections
|
136
|
+
evicted_section = f"\n\n(Older) Evicted Messages:\n{evicted_messages_str}" if evicted_messages_str.strip() else ""
|
137
|
+
in_context_section = ""
|
138
|
+
|
139
|
+
if retain_count > 0 and in_context_messages_str.strip():
|
140
|
+
in_context_section = f"\n\n(Newer) In-Context Messages:\n{in_context_messages_str}"
|
141
|
+
elif retain_count == 0:
|
142
|
+
prompt_header = (
|
143
|
+
"You’re a memory-recall helper for an AI that is about to forget all prior messages. "
|
144
|
+
"Scan the conversation history and write crisp notes that capture any important facts or insights about the conversation history."
|
145
|
+
)
|
146
|
+
|
147
|
+
# Compose final prompt
|
148
|
+
summary_request_text = prompt_header + evicted_section + in_context_section
|
120
149
|
|
121
|
-
(Newer) In-Context Messages:\n
|
122
|
-
{in_context_messages_str}
|
123
|
-
"""
|
124
150
|
# Fire-and-forget the summarization task
|
125
151
|
self.fire_and_forget(
|
126
152
|
self.summarizer_agent.step([MessageCreate(role=MessageRole.user, content=[TextContent(text=summary_request_text)])])
|
@@ -156,7 +182,8 @@ def format_transcript(messages: List[Message], include_system: bool = False) ->
|
|
156
182
|
# Skip tool messages where the name is "send_message"
|
157
183
|
if msg.role == MessageRole.tool and msg.name == DEFAULT_MESSAGE_TOOL:
|
158
184
|
continue
|
159
|
-
|
185
|
+
|
186
|
+
text = "".join(c.text for c in msg.content if isinstance(c, TextContent)).strip()
|
160
187
|
|
161
188
|
# 2) Otherwise, try extracting from function calls
|
162
189
|
elif msg.tool_calls:
|
@@ -38,6 +38,13 @@ class TelemetryManager:
|
|
38
38
|
def create_provider_trace(self, actor: PydanticUser, provider_trace_create: ProviderTraceCreate) -> PydanticProviderTrace:
|
39
39
|
with db_registry.session() as session:
|
40
40
|
provider_trace = ProviderTraceModel(**provider_trace_create.model_dump())
|
41
|
+
if provider_trace_create.request_json:
|
42
|
+
request_json_str = json_dumps(provider_trace_create.request_json)
|
43
|
+
provider_trace.request_json = json_loads(request_json_str)
|
44
|
+
|
45
|
+
if provider_trace_create.response_json:
|
46
|
+
response_json_str = json_dumps(provider_trace_create.response_json)
|
47
|
+
provider_trace.response_json = json_loads(response_json_str)
|
41
48
|
provider_trace.create(session, actor=actor)
|
42
49
|
return provider_trace.to_pydantic()
|
43
50
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import traceback
|
2
2
|
from typing import Any, Dict, Optional, Type
|
3
3
|
|
4
|
+
from letta.constants import FUNCTION_RETURN_VALUE_TRUNCATED
|
4
5
|
from letta.log import get_logger
|
5
6
|
from letta.orm.enums import ToolType
|
6
7
|
from letta.schemas.agent import AgentState
|
@@ -68,8 +69,8 @@ class ToolExecutionManager:
|
|
68
69
|
agent_manager: AgentManager,
|
69
70
|
block_manager: BlockManager,
|
70
71
|
passage_manager: PassageManager,
|
71
|
-
agent_state: AgentState,
|
72
72
|
actor: User,
|
73
|
+
agent_state: Optional[AgentState] = None,
|
73
74
|
sandbox_config: Optional[SandboxConfig] = None,
|
74
75
|
sandbox_env_vars: Optional[Dict[str, Any]] = None,
|
75
76
|
):
|
@@ -83,50 +84,6 @@ class ToolExecutionManager:
|
|
83
84
|
self.sandbox_config = sandbox_config
|
84
85
|
self.sandbox_env_vars = sandbox_env_vars
|
85
86
|
|
86
|
-
def execute_tool(self, function_name: str, function_args: dict, tool: Tool) -> ToolExecutionResult:
|
87
|
-
"""
|
88
|
-
Execute a tool and persist any state changes.
|
89
|
-
|
90
|
-
Args:
|
91
|
-
function_name: Name of the function to execute
|
92
|
-
function_args: Arguments to pass to the function
|
93
|
-
tool: Tool object containing metadata about the tool
|
94
|
-
|
95
|
-
Returns:
|
96
|
-
Tuple containing the function response and sandbox run result (if applicable)
|
97
|
-
"""
|
98
|
-
try:
|
99
|
-
executor = ToolExecutorFactory.get_executor(
|
100
|
-
tool.tool_type,
|
101
|
-
message_manager=self.message_manager,
|
102
|
-
agent_manager=self.agent_manager,
|
103
|
-
block_manager=self.block_manager,
|
104
|
-
passage_manager=self.passage_manager,
|
105
|
-
actor=self.actor,
|
106
|
-
)
|
107
|
-
return executor.execute(
|
108
|
-
function_name,
|
109
|
-
function_args,
|
110
|
-
self.agent_state,
|
111
|
-
tool,
|
112
|
-
self.actor,
|
113
|
-
self.sandbox_config,
|
114
|
-
self.sandbox_env_vars,
|
115
|
-
)
|
116
|
-
|
117
|
-
except Exception as e:
|
118
|
-
self.logger.error(f"Error executing tool {function_name}: {str(e)}")
|
119
|
-
error_message = get_friendly_error_msg(
|
120
|
-
function_name=function_name,
|
121
|
-
exception_name=type(e).__name__,
|
122
|
-
exception_message=str(e),
|
123
|
-
)
|
124
|
-
return ToolExecutionResult(
|
125
|
-
status="error",
|
126
|
-
func_return=error_message,
|
127
|
-
stderr=[traceback.format_exc()],
|
128
|
-
)
|
129
|
-
|
130
87
|
@trace_method
|
131
88
|
async def execute_tool_async(self, function_name: str, function_args: dict, tool: Tool) -> ToolExecutionResult:
|
132
89
|
"""
|
@@ -141,13 +98,15 @@ class ToolExecutionManager:
|
|
141
98
|
passage_manager=self.passage_manager,
|
142
99
|
actor=self.actor,
|
143
100
|
)
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
101
|
+
result = await executor.execute(
|
102
|
+
function_name, function_args, tool, self.actor, self.agent_state, self.sandbox_config, self.sandbox_env_vars
|
103
|
+
)
|
104
|
+
|
105
|
+
# trim result
|
106
|
+
return_str = str(result.func_return)
|
107
|
+
if len(return_str) > tool.return_char_limit:
|
108
|
+
# TODO: okay that this become a string?
|
109
|
+
result.func_return = FUNCTION_RETURN_VALUE_TRUNCATED(return_str, len(return_str), tool.return_char_limit)
|
151
110
|
return result
|
152
111
|
|
153
112
|
except Exception as e:
|
@@ -1,4 +1,3 @@
|
|
1
|
-
import ast
|
2
1
|
import base64
|
3
2
|
import io
|
4
3
|
import os
|
@@ -23,6 +22,7 @@ from letta.services.helpers.tool_execution_helper import (
|
|
23
22
|
find_python_executable,
|
24
23
|
install_pip_requirements_for_sandbox,
|
25
24
|
)
|
25
|
+
from letta.services.helpers.tool_parser_helper import convert_param_to_str_value, parse_function_arguments
|
26
26
|
from letta.services.organization_manager import OrganizationManager
|
27
27
|
from letta.services.sandbox_config_manager import SandboxConfigManager
|
28
28
|
from letta.services.tool_manager import ToolManager
|
@@ -52,7 +52,6 @@ class ToolExecutionSandbox:
|
|
52
52
|
self.tool_name = tool_name
|
53
53
|
self.args = args
|
54
54
|
self.user = user
|
55
|
-
# get organization
|
56
55
|
self.organization = OrganizationManager().get_organization_by_id(self.user.organization_id)
|
57
56
|
self.privileged_tools = self.organization.privileged_tools
|
58
57
|
|
@@ -476,16 +475,6 @@ class ToolExecutionSandbox:
|
|
476
475
|
agent_state = result["agent_state"]
|
477
476
|
return result["results"], agent_state
|
478
477
|
|
479
|
-
def parse_function_arguments(self, source_code: str, tool_name: str):
|
480
|
-
"""Get arguments of a function from its source code"""
|
481
|
-
tree = ast.parse(source_code)
|
482
|
-
args = []
|
483
|
-
for node in ast.walk(tree):
|
484
|
-
if isinstance(node, ast.FunctionDef) and node.name == tool_name:
|
485
|
-
for arg in node.args.args:
|
486
|
-
args.append(arg.arg)
|
487
|
-
return args
|
488
|
-
|
489
478
|
def generate_execution_script(self, agent_state: AgentState, wrap_print_with_markers: bool = False) -> str:
|
490
479
|
"""
|
491
480
|
Generate code to run inside of execution sandbox.
|
@@ -498,7 +487,7 @@ class ToolExecutionSandbox:
|
|
498
487
|
Returns:
|
499
488
|
code (str): The generated code strong
|
500
489
|
"""
|
501
|
-
if "agent_state" in
|
490
|
+
if "agent_state" in parse_function_arguments(self.tool.source_code, self.tool.name):
|
502
491
|
inject_agent_state = True
|
503
492
|
else:
|
504
493
|
inject_agent_state = False
|
@@ -546,7 +535,7 @@ class ToolExecutionSandbox:
|
|
546
535
|
code += (
|
547
536
|
self.LOCAL_SANDBOX_RESULT_VAR_NAME
|
548
537
|
+ ' = {"results": '
|
549
|
-
+ self.invoke_function_call(inject_agent_state=inject_agent_state)
|
538
|
+
+ self.invoke_function_call(inject_agent_state=inject_agent_state) # this inject_agent_state is the main difference
|
550
539
|
+ ', "agent_state": agent_state}\n'
|
551
540
|
)
|
552
541
|
code += (
|
@@ -562,24 +551,6 @@ class ToolExecutionSandbox:
|
|
562
551
|
|
563
552
|
return code
|
564
553
|
|
565
|
-
def _convert_param_to_value(self, param_type: str, raw_value: str) -> str:
|
566
|
-
|
567
|
-
if param_type == "string":
|
568
|
-
value = "pickle.loads(" + str(pickle.dumps(raw_value)) + ")"
|
569
|
-
|
570
|
-
elif param_type == "integer" or param_type == "boolean" or param_type == "number":
|
571
|
-
value = raw_value
|
572
|
-
|
573
|
-
elif param_type == "array":
|
574
|
-
value = raw_value
|
575
|
-
|
576
|
-
elif param_type == "object":
|
577
|
-
value = raw_value
|
578
|
-
|
579
|
-
else:
|
580
|
-
raise TypeError(f"Unsupported type: {param_type}, raw_value={raw_value}")
|
581
|
-
return str(value)
|
582
|
-
|
583
554
|
def initialize_param(self, name: str, raw_value: str) -> str:
|
584
555
|
params = self.tool.json_schema["parameters"]["properties"]
|
585
556
|
spec = params.get(name)
|
@@ -591,8 +562,7 @@ class ToolExecutionSandbox:
|
|
591
562
|
if param_type is None and spec.get("parameters"):
|
592
563
|
param_type = spec["parameters"].get("type")
|
593
564
|
|
594
|
-
value =
|
595
|
-
|
565
|
+
value = convert_param_to_str_value(param_type, raw_value)
|
596
566
|
return name + " = " + value + "\n"
|
597
567
|
|
598
568
|
def invoke_function_call(self, inject_agent_state: bool) -> str:
|