letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604104349__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.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -1,108 +1,109 @@
|
|
1
|
-
import asyncio
|
2
|
-
import sys
|
3
|
-
from contextlib import asynccontextmanager
|
1
|
+
# import asyncio
|
2
|
+
# import sys
|
3
|
+
# from contextlib import asynccontextmanager
|
4
|
+
#
|
5
|
+
# import anyio
|
6
|
+
# import anyio.lowlevel
|
7
|
+
# import mcp.types as types
|
8
|
+
# from anyio.streams.text import TextReceiveStream
|
9
|
+
# from mcp import ClientSession, StdioServerParameters
|
10
|
+
# from mcp.client.stdio import get_default_environment
|
11
|
+
#
|
12
|
+
# from letta.functions.mcp_client.base_client import BaseMCPClient
|
13
|
+
# from letta.functions.mcp_client.types import StdioServerConfig
|
14
|
+
# from letta.log import get_logger
|
15
|
+
#
|
16
|
+
# logger = get_logger(__name__)
|
4
17
|
|
5
|
-
import anyio
|
6
|
-
import anyio.lowlevel
|
7
|
-
import mcp.types as types
|
8
|
-
from anyio.streams.text import TextReceiveStream
|
9
|
-
from mcp import ClientSession, StdioServerParameters
|
10
|
-
from mcp.client.stdio import get_default_environment
|
11
18
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
tg.start_soon(watch_process_exit)
|
104
|
-
|
105
|
-
with anyio.move_on_after(0.2):
|
106
|
-
await anyio.sleep_forever()
|
107
|
-
|
108
|
-
yield read_stream, write_stream
|
19
|
+
# class StdioMCPClient(BaseMCPClient):
|
20
|
+
# def _initialize_connection(self, server_config: StdioServerConfig, timeout: float) -> bool:
|
21
|
+
# try:
|
22
|
+
# server_params = StdioServerParameters(command=server_config.command, args=server_config.args, env=server_config.env)
|
23
|
+
# stdio_cm = forked_stdio_client(server_params)
|
24
|
+
# stdio_transport = self.loop.run_until_complete(asyncio.wait_for(stdio_cm.__aenter__(), timeout=timeout))
|
25
|
+
# self.stdio, self.write = stdio_transport
|
26
|
+
# self.cleanup_funcs.append(lambda: self.loop.run_until_complete(stdio_cm.__aexit__(None, None, None)))
|
27
|
+
#
|
28
|
+
# session_cm = ClientSession(self.stdio, self.write)
|
29
|
+
# self.session = self.loop.run_until_complete(asyncio.wait_for(session_cm.__aenter__(), timeout=timeout))
|
30
|
+
# self.cleanup_funcs.append(lambda: self.loop.run_until_complete(session_cm.__aexit__(None, None, None)))
|
31
|
+
# return True
|
32
|
+
# except asyncio.TimeoutError:
|
33
|
+
# logger.error(f"Timed out while establishing stdio connection (timeout={timeout}s).")
|
34
|
+
# return False
|
35
|
+
# except Exception:
|
36
|
+
# logger.exception("Exception occurred while initializing stdio client session.")
|
37
|
+
# return False
|
38
|
+
#
|
39
|
+
#
|
40
|
+
# @asynccontextmanager
|
41
|
+
# async def forked_stdio_client(server: StdioServerParameters):
|
42
|
+
# """
|
43
|
+
# Client transport for stdio: this will connect to a server by spawning a
|
44
|
+
# process and communicating with it over stdin/stdout.
|
45
|
+
# """
|
46
|
+
# read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
|
47
|
+
# write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
|
48
|
+
#
|
49
|
+
# try:
|
50
|
+
# process = await anyio.open_process(
|
51
|
+
# [server.command, *server.args],
|
52
|
+
# env=server.env or get_default_environment(),
|
53
|
+
# stderr=sys.stderr, # Consider logging stderr somewhere instead of silencing it
|
54
|
+
# )
|
55
|
+
# except OSError as exc:
|
56
|
+
# raise RuntimeError(f"Failed to spawn process: {server.command} {server.args}") from exc
|
57
|
+
#
|
58
|
+
# async def stdout_reader():
|
59
|
+
# assert process.stdout, "Opened process is missing stdout"
|
60
|
+
# buffer = ""
|
61
|
+
# try:
|
62
|
+
# async with read_stream_writer:
|
63
|
+
# async for chunk in TextReceiveStream(
|
64
|
+
# process.stdout,
|
65
|
+
# encoding=server.encoding,
|
66
|
+
# errors=server.encoding_error_handler,
|
67
|
+
# ):
|
68
|
+
# lines = (buffer + chunk).split("\n")
|
69
|
+
# buffer = lines.pop()
|
70
|
+
# for line in lines:
|
71
|
+
# try:
|
72
|
+
# message = types.JSONRPCMessage.model_validate_json(line)
|
73
|
+
# except Exception as exc:
|
74
|
+
# await read_stream_writer.send(exc)
|
75
|
+
# continue
|
76
|
+
# await read_stream_writer.send(message)
|
77
|
+
# except anyio.ClosedResourceError:
|
78
|
+
# await anyio.lowlevel.checkpoint()
|
79
|
+
#
|
80
|
+
# async def stdin_writer():
|
81
|
+
# assert process.stdin, "Opened process is missing stdin"
|
82
|
+
# try:
|
83
|
+
# async with write_stream_reader:
|
84
|
+
# async for message in write_stream_reader:
|
85
|
+
# json = message.model_dump_json(by_alias=True, exclude_none=True)
|
86
|
+
# await process.stdin.send(
|
87
|
+
# (json + "\n").encode(
|
88
|
+
# encoding=server.encoding,
|
89
|
+
# errors=server.encoding_error_handler,
|
90
|
+
# )
|
91
|
+
# )
|
92
|
+
# except anyio.ClosedResourceError:
|
93
|
+
# await anyio.lowlevel.checkpoint()
|
94
|
+
#
|
95
|
+
# async def watch_process_exit():
|
96
|
+
# returncode = await process.wait()
|
97
|
+
# if returncode != 0:
|
98
|
+
# raise RuntimeError(f"Subprocess exited with code {returncode}. Command: {server.command} {server.args}")
|
99
|
+
#
|
100
|
+
# async with anyio.create_task_group() as tg, process:
|
101
|
+
# tg.start_soon(stdout_reader)
|
102
|
+
# tg.start_soon(stdin_writer)
|
103
|
+
# tg.start_soon(watch_process_exit)
|
104
|
+
#
|
105
|
+
# with anyio.move_on_after(0.2):
|
106
|
+
# await anyio.sleep_forever()
|
107
|
+
#
|
108
|
+
# yield read_stream, write_stream
|
109
|
+
#
|
@@ -7,6 +7,7 @@ from docstring_parser import parse
|
|
7
7
|
from pydantic import BaseModel
|
8
8
|
from typing_extensions import Literal
|
9
9
|
|
10
|
+
from letta.constants import REQUEST_HEARTBEAT_DESCRIPTION, REQUEST_HEARTBEAT_PARAM
|
10
11
|
from letta.functions.mcp_client.types import MCPTool
|
11
12
|
|
12
13
|
|
@@ -143,7 +144,10 @@ def pydantic_model_to_open_ai(model: Type[BaseModel]) -> dict:
|
|
143
144
|
parameters["required"] = sorted(k for k, v in parameters["properties"].items() if "default" not in v)
|
144
145
|
|
145
146
|
if "description" not in schema:
|
146
|
-
|
147
|
+
# Support multiline docstrings for complex functions, TODO (cliandy): consider having this as a setting
|
148
|
+
if docstring.long_description:
|
149
|
+
schema["description"] = docstring.long_description
|
150
|
+
elif docstring.short_description:
|
147
151
|
schema["description"] = docstring.short_description
|
148
152
|
else:
|
149
153
|
raise ValueError(f"No description found in docstring or description field (model: {model}, docstring: {docstring})")
|
@@ -330,10 +334,17 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
|
|
330
334
|
# Parse the docstring
|
331
335
|
docstring = parse(function.__doc__)
|
332
336
|
|
337
|
+
if not description:
|
338
|
+
# Support multiline docstrings for complex functions, TODO (cliandy): consider having this as a setting
|
339
|
+
if docstring.long_description:
|
340
|
+
description = docstring.long_description
|
341
|
+
else:
|
342
|
+
description = docstring.short_description
|
343
|
+
|
333
344
|
# Prepare the schema dictionary
|
334
345
|
schema = {
|
335
346
|
"name": function.__name__ if name is None else name,
|
336
|
-
"description":
|
347
|
+
"description": description,
|
337
348
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
338
349
|
}
|
339
350
|
|
@@ -412,17 +423,6 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
|
|
412
423
|
# TODO is this not duplicating the other append directly above?
|
413
424
|
if param.annotation == inspect.Parameter.empty:
|
414
425
|
schema["parameters"]["required"].append(param.name)
|
415
|
-
|
416
|
-
# append the heartbeat
|
417
|
-
# TODO: don't hard-code
|
418
|
-
# TODO: if terminal, don't include this
|
419
|
-
# if function.__name__ not in ["send_message"]:
|
420
|
-
schema["parameters"]["properties"]["request_heartbeat"] = {
|
421
|
-
"type": "boolean",
|
422
|
-
"description": "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function.",
|
423
|
-
}
|
424
|
-
schema["parameters"]["required"].append("request_heartbeat")
|
425
|
-
|
426
426
|
return schema
|
427
427
|
|
428
428
|
|
@@ -445,11 +445,11 @@ def generate_schema_from_args_schema_v2(
|
|
445
445
|
}
|
446
446
|
|
447
447
|
if append_heartbeat:
|
448
|
-
function_call_json["parameters"]["properties"][
|
448
|
+
function_call_json["parameters"]["properties"][REQUEST_HEARTBEAT_PARAM] = {
|
449
449
|
"type": "boolean",
|
450
|
-
"description":
|
450
|
+
"description": REQUEST_HEARTBEAT_DESCRIPTION,
|
451
451
|
}
|
452
|
-
function_call_json["parameters"]["required"].append(
|
452
|
+
function_call_json["parameters"]["required"].append(REQUEST_HEARTBEAT_PARAM)
|
453
453
|
|
454
454
|
return function_call_json
|
455
455
|
|
@@ -476,11 +476,11 @@ def generate_tool_schema_for_mcp(
|
|
476
476
|
|
477
477
|
# Add the optional heartbeat parameter
|
478
478
|
if append_heartbeat:
|
479
|
-
parameters_schema["properties"][
|
479
|
+
parameters_schema["properties"][REQUEST_HEARTBEAT_PARAM] = {
|
480
480
|
"type": "boolean",
|
481
|
-
"description":
|
481
|
+
"description": REQUEST_HEARTBEAT_DESCRIPTION,
|
482
482
|
}
|
483
|
-
parameters_schema["required"].append(
|
483
|
+
parameters_schema["required"].append(REQUEST_HEARTBEAT_PARAM)
|
484
484
|
|
485
485
|
# Return the final schema
|
486
486
|
if strict:
|
@@ -538,11 +538,11 @@ def generate_tool_schema_for_composio(
|
|
538
538
|
|
539
539
|
# Add the optional heartbeat parameter
|
540
540
|
if append_heartbeat:
|
541
|
-
properties_json[
|
541
|
+
properties_json[REQUEST_HEARTBEAT_PARAM] = {
|
542
542
|
"type": "boolean",
|
543
|
-
"description":
|
543
|
+
"description": REQUEST_HEARTBEAT_DESCRIPTION,
|
544
544
|
}
|
545
|
-
required_fields.append(
|
545
|
+
required_fields.append(REQUEST_HEARTBEAT_PARAM)
|
546
546
|
|
547
547
|
# Return the final schema
|
548
548
|
if strict:
|
letta/groups/helpers.py
CHANGED
@@ -2,13 +2,13 @@ import json
|
|
2
2
|
from typing import Dict, Optional, Union
|
3
3
|
|
4
4
|
from letta.agent import Agent
|
5
|
-
from letta.functions.mcp_client.base_client import BaseMCPClient
|
6
5
|
from letta.interface import AgentInterface
|
7
6
|
from letta.orm.group import Group
|
8
7
|
from letta.orm.user import User
|
9
8
|
from letta.schemas.agent import AgentState
|
10
9
|
from letta.schemas.group import ManagerType
|
11
10
|
from letta.schemas.message import Message
|
11
|
+
from letta.services.mcp.base_client import AsyncBaseMCPClient
|
12
12
|
|
13
13
|
|
14
14
|
def load_multi_agent(
|
@@ -16,7 +16,7 @@ def load_multi_agent(
|
|
16
16
|
agent_state: Optional[AgentState],
|
17
17
|
actor: User,
|
18
18
|
interface: Union[AgentInterface, None] = None,
|
19
|
-
mcp_clients: Optional[Dict[str,
|
19
|
+
mcp_clients: Optional[Dict[str, AsyncBaseMCPClient]] = None,
|
20
20
|
) -> Agent:
|
21
21
|
if len(group.agent_ids) == 0:
|
22
22
|
raise ValueError("Empty group: group must have at least one agent")
|
@@ -76,7 +76,6 @@ def load_multi_agent(
|
|
76
76
|
agent_state=agent_state,
|
77
77
|
interface=interface,
|
78
78
|
user=actor,
|
79
|
-
mcp_clients=mcp_clients,
|
80
79
|
group_id=group.id,
|
81
80
|
agent_ids=group.agent_ids,
|
82
81
|
description=group.description,
|
@@ -108,7 +107,7 @@ def stringify_message(message: Message, use_assistant_name: bool = False) -> str
|
|
108
107
|
elif message.role == "tool":
|
109
108
|
if message.content:
|
110
109
|
content = json.loads(message.content[0].text)
|
111
|
-
if content["message"] != "None"
|
110
|
+
if str(content["message"]) != "None":
|
112
111
|
return f"{assistant_name}: Tool call returned {content['message']}"
|
113
112
|
return None
|
114
113
|
elif message.role == "system":
|
@@ -1,10 +1,9 @@
|
|
1
1
|
import asyncio
|
2
2
|
import threading
|
3
3
|
from datetime import datetime, timezone
|
4
|
-
from typing import
|
4
|
+
from typing import List, Optional
|
5
5
|
|
6
6
|
from letta.agent import Agent, AgentState
|
7
|
-
from letta.functions.mcp_client.base_client import BaseMCPClient
|
8
7
|
from letta.groups.helpers import stringify_message
|
9
8
|
from letta.interface import AgentInterface
|
10
9
|
from letta.orm import User
|
@@ -27,7 +26,7 @@ class SleeptimeMultiAgent(Agent):
|
|
27
26
|
interface: AgentInterface,
|
28
27
|
agent_state: AgentState,
|
29
28
|
user: User,
|
30
|
-
mcp_clients: Optional[Dict[str, BaseMCPClient]] = None,
|
29
|
+
# mcp_clients: Optional[Dict[str, BaseMCPClient]] = None,
|
31
30
|
# custom
|
32
31
|
group_id: str = "",
|
33
32
|
agent_ids: List[str] = [],
|
@@ -42,7 +41,8 @@ class SleeptimeMultiAgent(Agent):
|
|
42
41
|
self.group_manager = GroupManager()
|
43
42
|
self.message_manager = MessageManager()
|
44
43
|
self.job_manager = JobManager()
|
45
|
-
|
44
|
+
# TODO: add back MCP support with new agent loop
|
45
|
+
self.mcp_clients = {}
|
46
46
|
|
47
47
|
def _run_async_in_new_thread(self, coro):
|
48
48
|
"""Run an async coroutine in a new thread with its own event loop"""
|
@@ -21,6 +21,7 @@ from letta.services.message_manager import MessageManager
|
|
21
21
|
from letta.services.passage_manager import PassageManager
|
22
22
|
from letta.services.step_manager import NoopStepManager, StepManager
|
23
23
|
from letta.services.telemetry_manager import NoopTelemetryManager, TelemetryManager
|
24
|
+
from letta.tracing import trace_method
|
24
25
|
|
25
26
|
|
26
27
|
class SleeptimeMultiAgentV2(BaseAgent):
|
@@ -55,11 +56,13 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
55
56
|
assert group.manager_type == ManagerType.sleeptime, f"Expected group manager type to be 'sleeptime', got {group.manager_type}"
|
56
57
|
self.group = group
|
57
58
|
|
59
|
+
@trace_method
|
58
60
|
async def step(
|
59
61
|
self,
|
60
62
|
input_messages: List[MessageCreate],
|
61
63
|
max_steps: int = 10,
|
62
64
|
use_assistant_message: bool = True,
|
65
|
+
request_start_timestamp_ns: Optional[int] = None,
|
63
66
|
) -> LettaResponse:
|
64
67
|
run_ids = []
|
65
68
|
|
@@ -119,6 +122,22 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
119
122
|
response.usage.run_ids = run_ids
|
120
123
|
return response
|
121
124
|
|
125
|
+
@trace_method
|
126
|
+
async def step_stream_no_tokens(
|
127
|
+
self,
|
128
|
+
input_messages: List[MessageCreate],
|
129
|
+
max_steps: int = 10,
|
130
|
+
use_assistant_message: bool = True,
|
131
|
+
request_start_timestamp_ns: Optional[int] = None,
|
132
|
+
):
|
133
|
+
response = await self.step(input_messages, max_steps, use_assistant_message)
|
134
|
+
|
135
|
+
for message in response.messages:
|
136
|
+
yield f"data: {message.model_dump_json()}\n\n"
|
137
|
+
|
138
|
+
yield f"data: {response.usage.model_dump_json()}\n\n"
|
139
|
+
|
140
|
+
@trace_method
|
122
141
|
async def step_stream(
|
123
142
|
self,
|
124
143
|
input_messages: List[MessageCreate],
|
@@ -256,6 +275,9 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
256
275
|
actor=self.actor,
|
257
276
|
step_manager=self.step_manager,
|
258
277
|
telemetry_manager=self.telemetry_manager,
|
278
|
+
message_buffer_limit=20, # TODO: Make this configurable
|
279
|
+
message_buffer_min=8, # TODO: Make this configurable
|
280
|
+
enable_summarization=False, # TODO: Make this configurable
|
259
281
|
)
|
260
282
|
|
261
283
|
# Perform sleeptime agent step
|
@@ -20,3 +20,19 @@ def get_composio_api_key(actor: User, logger: Optional[Logger] = None) -> Option
|
|
20
20
|
# Ideally, not tied to a specific sandbox, but for now we just get the first one
|
21
21
|
# Theoretically possible for someone to have different composio api keys per sandbox
|
22
22
|
return api_keys[0].value
|
23
|
+
|
24
|
+
|
25
|
+
async def get_composio_api_key_async(actor: User, logger: Optional[Logger] = None) -> Optional[str]:
|
26
|
+
api_keys = await SandboxConfigManager().list_sandbox_env_vars_by_key_async(key="COMPOSIO_API_KEY", actor=actor)
|
27
|
+
if not api_keys:
|
28
|
+
if logger:
|
29
|
+
logger.debug(f"No API keys found for Composio. Defaulting to the environment variable...")
|
30
|
+
if tool_settings.composio_api_key:
|
31
|
+
return tool_settings.composio_api_key
|
32
|
+
else:
|
33
|
+
return None
|
34
|
+
else:
|
35
|
+
# TODO: Add more protections around this
|
36
|
+
# Ideally, not tied to a specific sandbox, but for now we just get the first one
|
37
|
+
# Theoretically possible for someone to have different composio api keys per sandbox
|
38
|
+
return api_keys[0].value
|
letta/helpers/converters.py
CHANGED
@@ -7,6 +7,7 @@ from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMe
|
|
7
7
|
from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
|
8
8
|
from sqlalchemy import Dialect
|
9
9
|
|
10
|
+
from letta.functions.mcp_client.types import StdioServerConfig
|
10
11
|
from letta.schemas.agent import AgentStepState
|
11
12
|
from letta.schemas.embedding_config import EmbeddingConfig
|
12
13
|
from letta.schemas.enums import ProviderType, ToolRuleType
|
@@ -400,3 +401,22 @@ def deserialize_response_format(data: Optional[Dict]) -> Optional[ResponseFormat
|
|
400
401
|
return JsonSchemaResponseFormat(**data)
|
401
402
|
if data["type"] == ResponseFormatType.json_object:
|
402
403
|
return JsonObjectResponseFormat(**data)
|
404
|
+
|
405
|
+
|
406
|
+
# --------------------------
|
407
|
+
# MCP Stdio Server Config Serialization
|
408
|
+
# --------------------------
|
409
|
+
|
410
|
+
|
411
|
+
def serialize_mcp_stdio_config(config: Union[Optional[StdioServerConfig], Dict]) -> Optional[Dict]:
|
412
|
+
"""Convert an StdioServerConfig object into a JSON-serializable dictionary."""
|
413
|
+
if config and isinstance(config, StdioServerConfig):
|
414
|
+
return config.to_dict()
|
415
|
+
return config
|
416
|
+
|
417
|
+
|
418
|
+
def deserialize_mcp_stdio_config(data: Optional[Dict]) -> Optional[StdioServerConfig]:
|
419
|
+
"""Convert a dictionary back into an StdioServerConfig object."""
|
420
|
+
if not data:
|
421
|
+
return None
|
422
|
+
return StdioServerConfig(**data)
|
@@ -16,11 +16,6 @@ def datetime_to_timestamp(dt):
|
|
16
16
|
return int(dt.timestamp())
|
17
17
|
|
18
18
|
|
19
|
-
def timestamp_to_datetime(ts):
|
20
|
-
# convert integer timestamp to datetime object
|
21
|
-
return datetime.fromtimestamp(ts)
|
22
|
-
|
23
|
-
|
24
19
|
def get_local_time_military():
|
25
20
|
# Get the current time in UTC
|
26
21
|
current_time_utc = datetime.now(pytz.utc)
|
@@ -36,7 +31,7 @@ def get_local_time_military():
|
|
36
31
|
|
37
32
|
|
38
33
|
def get_local_time_fast():
|
39
|
-
formatted_time = strftime("%Y-%m-%d %
|
34
|
+
formatted_time = strftime("%Y-%m-%d %I:%M:%S %p %Z%z")
|
40
35
|
|
41
36
|
return formatted_time
|
42
37
|
|
@@ -141,7 +141,8 @@ class ToolRulesSolver(BaseModel):
|
|
141
141
|
"""Check if the tool is defined as a continue tool in the tool rules."""
|
142
142
|
return any(rule.tool_name == tool_name for rule in self.continue_tool_rules)
|
143
143
|
|
144
|
-
|
144
|
+
@staticmethod
|
145
|
+
def validate_conditional_tool(rule: ConditionalToolRule):
|
145
146
|
"""
|
146
147
|
Validate a conditional tool rule
|
147
148
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import json
|
2
2
|
from datetime import datetime, timezone
|
3
3
|
from enum import Enum
|
4
|
-
from typing import AsyncGenerator, List, Union
|
4
|
+
from typing import AsyncGenerator, List, Optional, Union
|
5
5
|
|
6
6
|
from anthropic import AsyncStream
|
7
7
|
from anthropic.types.beta import (
|
@@ -23,6 +23,7 @@ from anthropic.types.beta import (
|
|
23
23
|
)
|
24
24
|
|
25
25
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
26
|
+
from letta.helpers.datetime_helpers import get_utc_timestamp_ns
|
26
27
|
from letta.local_llm.constants import INNER_THOUGHTS_KWARG
|
27
28
|
from letta.log import get_logger
|
28
29
|
from letta.schemas.letta_message import (
|
@@ -115,12 +116,26 @@ class AnthropicStreamingInterface:
|
|
115
116
|
logger.error("Error checking inner thoughts: %s", e)
|
116
117
|
raise
|
117
118
|
|
118
|
-
async def process(
|
119
|
+
async def process(
|
120
|
+
self,
|
121
|
+
stream: AsyncStream[BetaRawMessageStreamEvent],
|
122
|
+
ttft_span: Optional["Span"] = None,
|
123
|
+
provider_request_start_timestamp_ns: Optional[int] = None,
|
124
|
+
) -> AsyncGenerator[LettaMessage, None]:
|
119
125
|
prev_message_type = None
|
120
126
|
message_index = 0
|
127
|
+
first_chunk = True
|
121
128
|
try:
|
122
129
|
async with stream:
|
123
130
|
async for event in stream:
|
131
|
+
if first_chunk and ttft_span is not None and provider_request_start_timestamp_ns is not None:
|
132
|
+
now = get_utc_timestamp_ns()
|
133
|
+
ttft_ns = now - provider_request_start_timestamp_ns
|
134
|
+
ttft_span.add_event(
|
135
|
+
name="anthropic_time_to_first_token_ms", attributes={"anthropic_time_to_first_token_ms": ttft_ns // 1_000_000}
|
136
|
+
)
|
137
|
+
first_chunk = False
|
138
|
+
|
124
139
|
# TODO: Support BetaThinkingBlock, BetaRedactedThinkingBlock
|
125
140
|
if isinstance(event, BetaRawContentBlockStartEvent):
|
126
141
|
content = event.content_block
|
@@ -16,6 +16,7 @@ class OpenAIChatCompletionsStreamingInterface:
|
|
16
16
|
"""
|
17
17
|
|
18
18
|
def __init__(self, stream_pre_execution_message: bool = True):
|
19
|
+
print("CHAT COMPLETITION INTERFACE")
|
19
20
|
self.optimistic_json_parser: OptimisticJSONParser = OptimisticJSONParser()
|
20
21
|
self.stream_pre_execution_message: bool = stream_pre_execution_message
|
21
22
|
|
@@ -5,6 +5,7 @@ from openai import AsyncStream
|
|
5
5
|
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
6
6
|
|
7
7
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
|
8
|
+
from letta.helpers.datetime_helpers import get_utc_timestamp_ns
|
8
9
|
from letta.schemas.letta_message import AssistantMessage, LettaMessage, ReasoningMessage, ToolCallDelta, ToolCallMessage
|
9
10
|
from letta.schemas.letta_message_content import TextContent
|
10
11
|
from letta.schemas.message import Message
|
@@ -26,7 +27,7 @@ class OpenAIStreamingInterface:
|
|
26
27
|
self.assistant_message_tool_kwarg = DEFAULT_MESSAGE_TOOL_KWARG
|
27
28
|
|
28
29
|
self.optimistic_json_parser: OptimisticJSONParser = OptimisticJSONParser()
|
29
|
-
self.function_args_reader = JSONInnerThoughtsExtractor(wait_for_first_key=True) # TODO: pass in
|
30
|
+
self.function_args_reader = JSONInnerThoughtsExtractor(wait_for_first_key=True) # TODO: pass in kwarg
|
30
31
|
self.function_name_buffer = None
|
31
32
|
self.function_args_buffer = None
|
32
33
|
self.function_id_buffer = None
|
@@ -64,15 +65,30 @@ class OpenAIStreamingInterface:
|
|
64
65
|
function=FunctionCall(arguments=self.current_function_arguments, name=function_name),
|
65
66
|
)
|
66
67
|
|
67
|
-
async def process(
|
68
|
+
async def process(
|
69
|
+
self,
|
70
|
+
stream: AsyncStream[ChatCompletionChunk],
|
71
|
+
ttft_span: Optional["Span"] = None,
|
72
|
+
provider_request_start_timestamp_ns: Optional[int] = None,
|
73
|
+
) -> AsyncGenerator[LettaMessage, None]:
|
68
74
|
"""
|
69
75
|
Iterates over the OpenAI stream, yielding SSE events.
|
70
76
|
It also collects tokens and detects if a tool call is triggered.
|
71
77
|
"""
|
78
|
+
first_chunk = True
|
79
|
+
|
72
80
|
async with stream:
|
73
81
|
prev_message_type = None
|
74
82
|
message_index = 0
|
75
83
|
async for chunk in stream:
|
84
|
+
if first_chunk and ttft_span is not None and provider_request_start_timestamp_ns is not None:
|
85
|
+
now = get_utc_timestamp_ns()
|
86
|
+
ttft_ns = now - provider_request_start_timestamp_ns
|
87
|
+
ttft_span.add_event(
|
88
|
+
name="openai_time_to_first_token_ms", attributes={"openai_time_to_first_token_ms": ttft_ns // 1_000_000}
|
89
|
+
)
|
90
|
+
first_chunk = False
|
91
|
+
|
76
92
|
if not self.model or not self.message_id:
|
77
93
|
self.model = chunk.model
|
78
94
|
self.message_id = chunk.id
|