letta-nightly 0.7.29.dev20250602104315__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 +16 -9
- 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/jobs/llm_batch_job_polling.py +1 -1
- letta/jobs/scheduler.py +1 -1
- 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.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +138 -112
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.29.dev20250602104315.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
letta/functions/ast_parsers.py
CHANGED
@@ -5,23 +5,13 @@ import typing
|
|
5
5
|
from typing import Dict, Optional, Tuple
|
6
6
|
|
7
7
|
from letta.errors import LettaToolCreateError
|
8
|
-
|
9
|
-
# Registry of known types for annotation resolution
|
10
|
-
BUILTIN_TYPES = {
|
11
|
-
"int": int,
|
12
|
-
"float": float,
|
13
|
-
"str": str,
|
14
|
-
"dict": dict,
|
15
|
-
"list": list,
|
16
|
-
"set": set,
|
17
|
-
"tuple": tuple,
|
18
|
-
"bool": bool,
|
19
|
-
}
|
8
|
+
from letta.types import JsonDict
|
20
9
|
|
21
10
|
|
22
11
|
def resolve_type(annotation: str):
|
23
12
|
"""
|
24
13
|
Resolve a type annotation string into a Python type.
|
14
|
+
Previously, primitive support for int, float, str, dict, list, set, tuple, bool.
|
25
15
|
|
26
16
|
Args:
|
27
17
|
annotation (str): The annotation string (e.g., 'int', 'list[int]', 'dict[str, int]').
|
@@ -32,24 +22,19 @@ def resolve_type(annotation: str):
|
|
32
22
|
Raises:
|
33
23
|
ValueError: If the annotation is unsupported or invalid.
|
34
24
|
"""
|
35
|
-
|
36
|
-
|
25
|
+
python_types = {**vars(typing), **vars(builtins)}
|
26
|
+
|
27
|
+
if annotation in python_types:
|
28
|
+
return python_types[annotation]
|
37
29
|
|
38
30
|
try:
|
39
31
|
# Allow use of typing and builtins in a safe eval context
|
40
|
-
|
41
|
-
**vars(typing),
|
42
|
-
**vars(builtins),
|
43
|
-
"list": list,
|
44
|
-
"dict": dict,
|
45
|
-
"tuple": tuple,
|
46
|
-
"set": set,
|
47
|
-
}
|
48
|
-
return eval(annotation, namespace)
|
32
|
+
return eval(annotation, python_types)
|
49
33
|
except Exception:
|
50
34
|
raise ValueError(f"Unsupported annotation: {annotation}")
|
51
35
|
|
52
36
|
|
37
|
+
# TODO :: THIS MUST BE EDITED TO HANDLE THINGS
|
53
38
|
def get_function_annotations_from_source(source_code: str, function_name: str) -> Dict[str, str]:
|
54
39
|
"""
|
55
40
|
Parse the source code to extract annotations for a given function name.
|
@@ -76,7 +61,8 @@ def get_function_annotations_from_source(source_code: str, function_name: str) -
|
|
76
61
|
raise ValueError(f"Function '{function_name}' not found in the provided source code.")
|
77
62
|
|
78
63
|
|
79
|
-
|
64
|
+
# NOW json_loads -> ast.literal_eval -> typing.get_origin
|
65
|
+
def coerce_dict_args_by_annotations(function_args: JsonDict, annotations: Dict[str, str]) -> dict:
|
80
66
|
coerced_args = dict(function_args) # Shallow copy
|
81
67
|
|
82
68
|
for arg_name, value in coerced_args.items():
|
@@ -110,8 +96,8 @@ def coerce_dict_args_by_annotations(function_args: dict, annotations: Dict[str,
|
|
110
96
|
return coerced_args
|
111
97
|
|
112
98
|
|
113
|
-
def
|
114
|
-
"""Gets the name and
|
99
|
+
def get_function_name_and_docstring(source_code: str, name: Optional[str] = None) -> Tuple[str, str]:
|
100
|
+
"""Gets the name and docstring for a given function source code by parsing the AST.
|
115
101
|
|
116
102
|
Args:
|
117
103
|
source_code: The source code to parse
|
@@ -147,11 +133,8 @@ def get_function_name_and_description(source_code: str, name: Optional[str] = No
|
|
147
133
|
|
148
134
|
return function_name, docstring
|
149
135
|
|
150
|
-
except Exception as e:
|
151
|
-
raise LettaToolCreateError(f"Failed to parse function name and docstring: {str(e)}")
|
152
|
-
|
153
136
|
except Exception as e:
|
154
137
|
import traceback
|
155
138
|
|
156
139
|
traceback.print_exc()
|
157
|
-
raise LettaToolCreateError(f"
|
140
|
+
raise LettaToolCreateError(f"Failed to parse function name and docstring: {str(e)}")
|
@@ -288,7 +288,9 @@ def memory_insert(agent_state: "AgentState", label: str, new_str: str, insert_li
|
|
288
288
|
n_lines = len(current_value_lines)
|
289
289
|
|
290
290
|
# Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line)
|
291
|
-
if insert_line
|
291
|
+
if insert_line == -1:
|
292
|
+
insert_line = n_lines
|
293
|
+
elif insert_line < 0 or insert_line > n_lines:
|
292
294
|
raise ValueError(
|
293
295
|
f"Invalid `insert_line` parameter: {insert_line}. It should be within the range of lines of the memory block: {[0, n_lines]}, or -1 to append to the end of the memory block."
|
294
296
|
)
|
letta/functions/functions.py
CHANGED
@@ -12,6 +12,8 @@ from letta.functions.schema_generator import generate_schema
|
|
12
12
|
def derive_openai_json_schema(source_code: str, name: Optional[str] = None) -> dict:
|
13
13
|
"""Derives the OpenAI JSON schema for a given function source code.
|
14
14
|
|
15
|
+
# TODO (cliandy): I don't think we need to or should execute here
|
16
|
+
# TODO (cliandy): CONFIRM THIS BEFORE MERGING.
|
15
17
|
First, attempts to execute the source code in a custom environment with only the necessary imports.
|
16
18
|
Then, it generates the schema from the function's docstring and signature.
|
17
19
|
"""
|
@@ -1,102 +1,156 @@
|
|
1
|
-
import asyncio
|
2
|
-
from typing import List, Optional, Tuple
|
3
|
-
|
4
|
-
from mcp import ClientSession
|
5
|
-
from mcp.types import TextContent
|
6
|
-
|
7
|
-
from letta.functions.mcp_client.exceptions import MCPTimeoutError
|
8
|
-
from letta.functions.mcp_client.types import BaseServerConfig, MCPTool
|
9
1
|
from letta.log import get_logger
|
10
|
-
from letta.settings import tool_settings
|
11
2
|
|
12
3
|
logger = get_logger(__name__)
|
13
4
|
|
14
5
|
|
15
|
-
class BaseMCPClient:
|
16
|
-
def __init__(self, server_config: BaseServerConfig):
|
17
|
-
self.server_config = server_config
|
18
|
-
self.session: Optional[ClientSession] = None
|
19
|
-
self.stdio = None
|
20
|
-
self.write = None
|
21
|
-
self.initialized = False
|
22
|
-
self.loop = asyncio.new_event_loop()
|
23
|
-
self.cleanup_funcs = []
|
24
|
-
|
25
|
-
def connect_to_server(self):
|
26
|
-
asyncio.set_event_loop(self.loop)
|
27
|
-
success = self._initialize_connection(self.server_config, timeout=tool_settings.mcp_connect_to_server_timeout)
|
28
|
-
|
29
|
-
if success:
|
30
|
-
try:
|
31
|
-
self.loop.run_until_complete(
|
32
|
-
asyncio.wait_for(self.session.initialize(), timeout=tool_settings.mcp_connect_to_server_timeout)
|
33
|
-
)
|
34
|
-
self.initialized = True
|
35
|
-
except asyncio.TimeoutError:
|
36
|
-
raise MCPTimeoutError("initializing session", self.server_config.server_name, tool_settings.mcp_connect_to_server_timeout)
|
37
|
-
else:
|
38
|
-
raise RuntimeError(
|
39
|
-
f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}"
|
40
|
-
)
|
41
|
-
|
42
|
-
def _initialize_connection(self, server_config: BaseServerConfig, timeout: float) -> bool:
|
43
|
-
raise NotImplementedError("Subclasses must implement _initialize_connection")
|
44
|
-
|
45
|
-
def list_tools(self) -> List[MCPTool]:
|
46
|
-
self._check_initialized()
|
47
|
-
try:
|
48
|
-
response = self.loop.run_until_complete(
|
49
|
-
asyncio.wait_for(self.session.list_tools(), timeout=tool_settings.mcp_list_tools_timeout)
|
50
|
-
)
|
51
|
-
return response.tools
|
52
|
-
except asyncio.TimeoutError:
|
53
|
-
logger.error(
|
54
|
-
f"Timed out while listing tools for MCP server {self.server_config.server_name} (timeout={tool_settings.mcp_list_tools_timeout}s)."
|
55
|
-
)
|
56
|
-
raise MCPTimeoutError("listing tools", self.server_config.server_name, tool_settings.mcp_list_tools_timeout)
|
57
|
-
|
58
|
-
def execute_tool(self, tool_name: str, tool_args: dict) -> Tuple[str, bool]:
|
59
|
-
self._check_initialized()
|
60
|
-
try:
|
61
|
-
result = self.loop.run_until_complete(
|
62
|
-
asyncio.wait_for(self.session.call_tool(tool_name, tool_args), timeout=tool_settings.mcp_execute_tool_timeout)
|
63
|
-
)
|
64
|
-
|
65
|
-
parsed_content = []
|
66
|
-
for content_piece in result.content:
|
67
|
-
if isinstance(content_piece, TextContent):
|
68
|
-
parsed_content.append(content_piece.text)
|
69
|
-
print("parsed_content (text)", parsed_content)
|
70
|
-
else:
|
71
|
-
parsed_content.append(str(content_piece))
|
72
|
-
print("parsed_content (other)", parsed_content)
|
73
|
-
|
74
|
-
if len(parsed_content) > 0:
|
75
|
-
final_content = " ".join(parsed_content)
|
76
|
-
else:
|
77
|
-
# TODO move hardcoding to constants
|
78
|
-
final_content = "Empty response from tool"
|
79
|
-
|
80
|
-
return final_content, result.isError
|
81
|
-
except asyncio.TimeoutError:
|
82
|
-
logger.error(
|
83
|
-
f"Timed out while executing tool '{tool_name}' for MCP server {self.server_config.server_name} (timeout={tool_settings.mcp_execute_tool_timeout}s)."
|
84
|
-
)
|
85
|
-
raise MCPTimeoutError(f"executing tool '{tool_name}'", self.server_config.server_name, tool_settings.mcp_execute_tool_timeout)
|
86
|
-
|
87
|
-
def _check_initialized(self):
|
88
|
-
if not self.initialized:
|
89
|
-
logger.error("MCPClient has not been initialized")
|
90
|
-
raise RuntimeError("MCPClient has not been initialized")
|
91
|
-
|
92
|
-
def cleanup(self):
|
93
|
-
try:
|
94
|
-
for cleanup_func in self.cleanup_funcs:
|
95
|
-
cleanup_func()
|
96
|
-
self.initialized = False
|
97
|
-
if not self.loop.is_closed():
|
98
|
-
self.loop.close()
|
99
|
-
except Exception as e:
|
100
|
-
logger.warning(e)
|
101
|
-
finally:
|
102
|
-
logger.info("Cleaned up MCP clients on shutdown.")
|
6
|
+
# class BaseMCPClient:
|
7
|
+
# def __init__(self, server_config: BaseServerConfig):
|
8
|
+
# self.server_config = server_config
|
9
|
+
# self.session: Optional[ClientSession] = None
|
10
|
+
# self.stdio = None
|
11
|
+
# self.write = None
|
12
|
+
# self.initialized = False
|
13
|
+
# self.loop = asyncio.new_event_loop()
|
14
|
+
# self.cleanup_funcs = []
|
15
|
+
#
|
16
|
+
# def connect_to_server(self):
|
17
|
+
# asyncio.set_event_loop(self.loop)
|
18
|
+
# success = self._initialize_connection(self.server_config, timeout=tool_settings.mcp_connect_to_server_timeout)
|
19
|
+
#
|
20
|
+
# if success:
|
21
|
+
# try:
|
22
|
+
# self.loop.run_until_complete(
|
23
|
+
# asyncio.wait_for(self.session.initialize(), timeout=tool_settings.mcp_connect_to_server_timeout)
|
24
|
+
# )
|
25
|
+
# self.initialized = True
|
26
|
+
# except asyncio.TimeoutError:
|
27
|
+
# raise MCPTimeoutError("initializing session", self.server_config.server_name, tool_settings.mcp_connect_to_server_timeout)
|
28
|
+
# else:
|
29
|
+
# raise RuntimeError(
|
30
|
+
# f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}"
|
31
|
+
# )
|
32
|
+
#
|
33
|
+
# def _initialize_connection(self, server_config: BaseServerConfig, timeout: float) -> bool:
|
34
|
+
# raise NotImplementedError("Subclasses must implement _initialize_connection")
|
35
|
+
#
|
36
|
+
# def list_tools(self) -> List[MCPTool]:
|
37
|
+
# self._check_initialized()
|
38
|
+
# try:
|
39
|
+
# response = self.loop.run_until_complete(
|
40
|
+
# asyncio.wait_for(self.session.list_tools(), timeout=tool_settings.mcp_list_tools_timeout)
|
41
|
+
# )
|
42
|
+
# return response.tools
|
43
|
+
# except asyncio.TimeoutError:
|
44
|
+
# logger.error(
|
45
|
+
# f"Timed out while listing tools for MCP server {self.server_config.server_name} (timeout={tool_settings.mcp_list_tools_timeout}s)."
|
46
|
+
# )
|
47
|
+
# raise MCPTimeoutError("listing tools", self.server_config.server_name, tool_settings.mcp_list_tools_timeout)
|
48
|
+
#
|
49
|
+
# def execute_tool(self, tool_name: str, tool_args: dict) -> Tuple[str, bool]:
|
50
|
+
# self._check_initialized()
|
51
|
+
# try:
|
52
|
+
# result = self.loop.run_until_complete(
|
53
|
+
# asyncio.wait_for(self.session.call_tool(tool_name, tool_args), timeout=tool_settings.mcp_execute_tool_timeout)
|
54
|
+
# )
|
55
|
+
#
|
56
|
+
# parsed_content = []
|
57
|
+
# for content_piece in result.content:
|
58
|
+
# if isinstance(content_piece, TextContent):
|
59
|
+
# parsed_content.append(content_piece.text)
|
60
|
+
# print("parsed_content (text)", parsed_content)
|
61
|
+
# else:
|
62
|
+
# parsed_content.append(str(content_piece))
|
63
|
+
# print("parsed_content (other)", parsed_content)
|
64
|
+
#
|
65
|
+
# if len(parsed_content) > 0:
|
66
|
+
# final_content = " ".join(parsed_content)
|
67
|
+
# else:
|
68
|
+
# # TODO move hardcoding to constants
|
69
|
+
# final_content = "Empty response from tool"
|
70
|
+
#
|
71
|
+
# return final_content, result.isError
|
72
|
+
# except asyncio.TimeoutError:
|
73
|
+
# logger.error(
|
74
|
+
# f"Timed out while executing tool '{tool_name}' for MCP server {self.server_config.server_name} (timeout={tool_settings.mcp_execute_tool_timeout}s)."
|
75
|
+
# )
|
76
|
+
# raise MCPTimeoutError(f"executing tool '{tool_name}'", self.server_config.server_name, tool_settings.mcp_execute_tool_timeout)
|
77
|
+
#
|
78
|
+
# def _check_initialized(self):
|
79
|
+
# if not self.initialized:
|
80
|
+
# logger.error("MCPClient has not been initialized")
|
81
|
+
# raise RuntimeError("MCPClient has not been initialized")
|
82
|
+
#
|
83
|
+
# def cleanup(self):
|
84
|
+
# try:
|
85
|
+
# for cleanup_func in self.cleanup_funcs:
|
86
|
+
# cleanup_func()
|
87
|
+
# self.initialized = False
|
88
|
+
# if not self.loop.is_closed():
|
89
|
+
# self.loop.close()
|
90
|
+
# except Exception as e:
|
91
|
+
# logger.warning(e)
|
92
|
+
# finally:
|
93
|
+
# logger.info("Cleaned up MCP clients on shutdown.")
|
94
|
+
#
|
95
|
+
#
|
96
|
+
# class BaseAsyncMCPClient:
|
97
|
+
# def __init__(self, server_config: BaseServerConfig):
|
98
|
+
# self.server_config = server_config
|
99
|
+
# self.session: Optional[ClientSession] = None
|
100
|
+
# self.stdio = None
|
101
|
+
# self.write = None
|
102
|
+
# self.initialized = False
|
103
|
+
# self.cleanup_funcs = []
|
104
|
+
#
|
105
|
+
# async def connect_to_server(self):
|
106
|
+
#
|
107
|
+
# success = await self._initialize_connection(self.server_config, timeout=tool_settings.mcp_connect_to_server_timeout)
|
108
|
+
#
|
109
|
+
# if success:
|
110
|
+
# self.initialized = True
|
111
|
+
# else:
|
112
|
+
# raise RuntimeError(
|
113
|
+
# f"Connecting to MCP server failed. Please review your server config: {self.server_config.model_dump_json(indent=4)}"
|
114
|
+
# )
|
115
|
+
#
|
116
|
+
# async def list_tools(self) -> List[MCPTool]:
|
117
|
+
# self._check_initialized()
|
118
|
+
# response = await self.session.list_tools()
|
119
|
+
# return response.tools
|
120
|
+
#
|
121
|
+
# async def execute_tool(self, tool_name: str, tool_args: dict) -> Tuple[str, bool]:
|
122
|
+
# self._check_initialized()
|
123
|
+
# result = await self.session.call_tool(tool_name, tool_args)
|
124
|
+
#
|
125
|
+
# parsed_content = []
|
126
|
+
# for content_piece in result.content:
|
127
|
+
# if isinstance(content_piece, TextContent):
|
128
|
+
# parsed_content.append(content_piece.text)
|
129
|
+
# else:
|
130
|
+
# parsed_content.append(str(content_piece))
|
131
|
+
#
|
132
|
+
# if len(parsed_content) > 0:
|
133
|
+
# final_content = " ".join(parsed_content)
|
134
|
+
# else:
|
135
|
+
# # TODO move hardcoding to constants
|
136
|
+
# final_content = "Empty response from tool"
|
137
|
+
#
|
138
|
+
# return final_content, result.isError
|
139
|
+
#
|
140
|
+
# def _check_initialized(self):
|
141
|
+
# if not self.initialized:
|
142
|
+
# logger.error("MCPClient has not been initialized")
|
143
|
+
# raise RuntimeError("MCPClient has not been initialized")
|
144
|
+
#
|
145
|
+
# async def cleanup(self):
|
146
|
+
# try:
|
147
|
+
# for cleanup_func in self.cleanup_funcs:
|
148
|
+
# cleanup_func()
|
149
|
+
# self.initialized = False
|
150
|
+
# if not self.loop.is_closed():
|
151
|
+
# self.loop.close()
|
152
|
+
# except Exception as e:
|
153
|
+
# logger.warning(e)
|
154
|
+
# finally:
|
155
|
+
# logger.info("Cleaned up MCP clients on shutdown.")
|
156
|
+
#
|
@@ -1,33 +1,51 @@
|
|
1
|
-
import asyncio
|
1
|
+
# import asyncio
|
2
|
+
#
|
3
|
+
# from mcp import ClientSession
|
4
|
+
# from mcp.client.sse import sse_client
|
5
|
+
#
|
6
|
+
# from letta.functions.mcp_client.base_client import BaseAsyncMCPClient, BaseMCPClient
|
7
|
+
# from letta.functions.mcp_client.types import SSEServerConfig
|
8
|
+
# from letta.log import get_logger
|
9
|
+
#
|
10
|
+
## see: https://modelcontextprotocol.io/quickstart/user
|
11
|
+
#
|
12
|
+
# logger = get_logger(__name__)
|
2
13
|
|
3
|
-
from mcp import ClientSession
|
4
|
-
from mcp.client.sse import sse_client
|
5
14
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
#
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
15
|
+
# class SSEMCPClient(BaseMCPClient):
|
16
|
+
# def _initialize_connection(self, server_config: SSEServerConfig, timeout: float) -> bool:
|
17
|
+
# try:
|
18
|
+
# sse_cm = sse_client(url=server_config.server_url)
|
19
|
+
# sse_transport = self.loop.run_until_complete(asyncio.wait_for(sse_cm.__aenter__(), timeout=timeout))
|
20
|
+
# self.stdio, self.write = sse_transport
|
21
|
+
# self.cleanup_funcs.append(lambda: self.loop.run_until_complete(sse_cm.__aexit__(None, None, None)))
|
22
|
+
#
|
23
|
+
# session_cm = ClientSession(self.stdio, self.write)
|
24
|
+
# self.session = self.loop.run_until_complete(asyncio.wait_for(session_cm.__aenter__(), timeout=timeout))
|
25
|
+
# self.cleanup_funcs.append(lambda: self.loop.run_until_complete(session_cm.__aexit__(None, None, None)))
|
26
|
+
# return True
|
27
|
+
# except asyncio.TimeoutError:
|
28
|
+
# logger.error(f"Timed out while establishing SSE connection (timeout={timeout}s).")
|
29
|
+
# return False
|
30
|
+
# except Exception:
|
31
|
+
# logger.exception("Exception occurred while initializing SSE client session.")
|
32
|
+
# return False
|
33
|
+
#
|
34
|
+
#
|
35
|
+
# class AsyncSSEMCPClient(BaseAsyncMCPClient):
|
36
|
+
#
|
37
|
+
# async def _initialize_connection(self, server_config: SSEServerConfig, timeout: float) -> bool:
|
38
|
+
# try:
|
39
|
+
# sse_cm = sse_client(url=server_config.server_url)
|
40
|
+
# sse_transport = await sse_cm.__aenter__()
|
41
|
+
# self.stdio, self.write = sse_transport
|
42
|
+
# self.cleanup_funcs.append(lambda: sse_cm.__aexit__(None, None, None))
|
43
|
+
#
|
44
|
+
# session_cm = ClientSession(self.stdio, self.write)
|
45
|
+
# self.session = await session_cm.__aenter__()
|
46
|
+
# self.cleanup_funcs.append(lambda: session_cm.__aexit__(None, None, None))
|
47
|
+
# return True
|
48
|
+
# except Exception:
|
49
|
+
# logger.exception("Exception occurred while initializing SSE client session.")
|
50
|
+
# return False
|
51
|
+
#
|