letta-nightly 0.6.9.dev20250115104021__py3-none-any.whl → 0.6.9.dev20250116195713__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 -0
- letta/agent.py +24 -0
- letta/client/client.py +274 -11
- letta/constants.py +5 -0
- letta/functions/function_sets/multi_agent.py +96 -0
- letta/functions/helpers.py +105 -1
- letta/functions/schema_generator.py +8 -0
- letta/llm_api/openai.py +18 -2
- letta/local_llm/utils.py +4 -0
- letta/orm/__init__.py +1 -0
- letta/orm/enums.py +6 -0
- letta/orm/job.py +24 -2
- letta/orm/job_messages.py +33 -0
- letta/orm/job_usage_statistics.py +30 -0
- letta/orm/message.py +10 -0
- letta/orm/sqlalchemy_base.py +28 -4
- letta/orm/tool.py +0 -3
- letta/schemas/agent.py +10 -4
- letta/schemas/job.py +2 -0
- letta/schemas/letta_base.py +6 -1
- letta/schemas/letta_request.py +6 -4
- letta/schemas/llm_config.py +1 -1
- letta/schemas/message.py +2 -4
- letta/schemas/providers.py +1 -1
- letta/schemas/run.py +61 -0
- letta/schemas/tool.py +9 -17
- letta/server/rest_api/interface.py +3 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +6 -12
- letta/server/rest_api/routers/v1/__init__.py +4 -0
- letta/server/rest_api/routers/v1/agents.py +47 -151
- letta/server/rest_api/routers/v1/runs.py +137 -0
- letta/server/rest_api/routers/v1/tags.py +27 -0
- letta/server/rest_api/utils.py +5 -3
- letta/server/server.py +139 -2
- letta/services/agent_manager.py +101 -6
- letta/services/job_manager.py +274 -9
- letta/services/tool_execution_sandbox.py +1 -1
- letta/services/tool_manager.py +30 -25
- letta/utils.py +3 -4
- {letta_nightly-0.6.9.dev20250115104021.dist-info → letta_nightly-0.6.9.dev20250116195713.dist-info}/METADATA +4 -3
- {letta_nightly-0.6.9.dev20250115104021.dist-info → letta_nightly-0.6.9.dev20250116195713.dist-info}/RECORD +44 -38
- {letta_nightly-0.6.9.dev20250115104021.dist-info → letta_nightly-0.6.9.dev20250116195713.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.9.dev20250115104021.dist-info → letta_nightly-0.6.9.dev20250116195713.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.9.dev20250115104021.dist-info → letta_nightly-0.6.9.dev20250116195713.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
letta/agent.py
CHANGED
|
@@ -12,6 +12,7 @@ from letta.constants import (
|
|
|
12
12
|
FIRST_MESSAGE_ATTEMPTS,
|
|
13
13
|
FUNC_FAILED_HEARTBEAT_MESSAGE,
|
|
14
14
|
LETTA_CORE_TOOL_MODULE_NAME,
|
|
15
|
+
LETTA_MULTI_AGENT_TOOL_MODULE_NAME,
|
|
15
16
|
LLM_MAX_TOKENS,
|
|
16
17
|
MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST,
|
|
17
18
|
MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC,
|
|
@@ -25,6 +26,7 @@ from letta.interface import AgentInterface
|
|
|
25
26
|
from letta.llm_api.helpers import is_context_overflow_error
|
|
26
27
|
from letta.llm_api.llm_api_tools import create
|
|
27
28
|
from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_messages
|
|
29
|
+
from letta.log import get_logger
|
|
28
30
|
from letta.memory import summarize_messages
|
|
29
31
|
from letta.orm import User
|
|
30
32
|
from letta.orm.enums import ToolType
|
|
@@ -44,6 +46,7 @@ from letta.schemas.usage import LettaUsageStatistics
|
|
|
44
46
|
from letta.services.agent_manager import AgentManager
|
|
45
47
|
from letta.services.block_manager import BlockManager
|
|
46
48
|
from letta.services.helpers.agent_manager_helper import check_supports_structured_output, compile_memory_metadata_block
|
|
49
|
+
from letta.services.job_manager import JobManager
|
|
47
50
|
from letta.services.message_manager import MessageManager
|
|
48
51
|
from letta.services.passage_manager import PassageManager
|
|
49
52
|
from letta.services.tool_execution_sandbox import ToolExecutionSandbox
|
|
@@ -128,6 +131,7 @@ class Agent(BaseAgent):
|
|
|
128
131
|
self.message_manager = MessageManager()
|
|
129
132
|
self.passage_manager = PassageManager()
|
|
130
133
|
self.agent_manager = AgentManager()
|
|
134
|
+
self.job_manager = JobManager()
|
|
131
135
|
|
|
132
136
|
# State needed for heartbeat pausing
|
|
133
137
|
|
|
@@ -141,6 +145,9 @@ class Agent(BaseAgent):
|
|
|
141
145
|
# Load last function response from message history
|
|
142
146
|
self.last_function_response = self.load_last_function_response()
|
|
143
147
|
|
|
148
|
+
# Logger that the Agent specifically can use, will also report the agent_state ID with the logs
|
|
149
|
+
self.logger = get_logger(agent_state.id)
|
|
150
|
+
|
|
144
151
|
def load_last_function_response(self):
|
|
145
152
|
"""Load the last function response from message history"""
|
|
146
153
|
in_context_messages = self.agent_manager.get_in_context_messages(agent_id=self.agent_state.id, actor=self.user)
|
|
@@ -205,6 +212,10 @@ class Agent(BaseAgent):
|
|
|
205
212
|
callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name)
|
|
206
213
|
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
207
214
|
function_response = callable_func(**function_args)
|
|
215
|
+
elif target_letta_tool.tool_type == ToolType.LETTA_MULTI_AGENT_CORE:
|
|
216
|
+
callable_func = get_function_from_module(LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name)
|
|
217
|
+
function_args["self"] = self # need to attach self to arg since it's dynamically linked
|
|
218
|
+
function_response = callable_func(**function_args)
|
|
208
219
|
elif target_letta_tool.tool_type == ToolType.LETTA_MEMORY_CORE:
|
|
209
220
|
callable_func = get_function_from_module(LETTA_CORE_TOOL_MODULE_NAME, function_name)
|
|
210
221
|
agent_state_copy = self.agent_state.__deepcopy__()
|
|
@@ -675,11 +686,15 @@ class Agent(BaseAgent):
|
|
|
675
686
|
skip_verify: bool = False,
|
|
676
687
|
stream: bool = False, # TODO move to config?
|
|
677
688
|
step_count: Optional[int] = None,
|
|
689
|
+
metadata: Optional[dict] = None,
|
|
678
690
|
) -> AgentStepResponse:
|
|
679
691
|
"""Runs a single step in the agent loop (generates at most one LLM call)"""
|
|
680
692
|
|
|
681
693
|
try:
|
|
682
694
|
|
|
695
|
+
# Extract job_id from metadata if present
|
|
696
|
+
job_id = metadata.get("job_id") if metadata else None
|
|
697
|
+
|
|
683
698
|
# Step 0: update core memory
|
|
684
699
|
# only pulling latest block data if shared memory is being used
|
|
685
700
|
current_persisted_memory = Memory(
|
|
@@ -754,9 +769,17 @@ class Agent(BaseAgent):
|
|
|
754
769
|
f"last response total_tokens ({current_total_tokens}) < {MESSAGE_SUMMARY_WARNING_FRAC * int(self.agent_state.llm_config.context_window)}"
|
|
755
770
|
)
|
|
756
771
|
|
|
772
|
+
# Persisting into Messages
|
|
757
773
|
self.agent_state = self.agent_manager.append_to_in_context_messages(
|
|
758
774
|
all_new_messages, agent_id=self.agent_state.id, actor=self.user
|
|
759
775
|
)
|
|
776
|
+
if job_id:
|
|
777
|
+
for message in all_new_messages:
|
|
778
|
+
self.job_manager.add_message_to_job(
|
|
779
|
+
job_id=job_id,
|
|
780
|
+
message_id=message.id,
|
|
781
|
+
actor=self.user,
|
|
782
|
+
)
|
|
760
783
|
|
|
761
784
|
return AgentStepResponse(
|
|
762
785
|
messages=all_new_messages,
|
|
@@ -784,6 +807,7 @@ class Agent(BaseAgent):
|
|
|
784
807
|
first_message_retry_limit=first_message_retry_limit,
|
|
785
808
|
skip_verify=skip_verify,
|
|
786
809
|
stream=stream,
|
|
810
|
+
metadata=metadata,
|
|
787
811
|
)
|
|
788
812
|
|
|
789
813
|
else:
|
letta/client/client.py
CHANGED
|
@@ -22,14 +22,17 @@ from letta.schemas.environment_variables import (
|
|
|
22
22
|
)
|
|
23
23
|
from letta.schemas.file import FileMetadata
|
|
24
24
|
from letta.schemas.job import Job
|
|
25
|
+
from letta.schemas.letta_message import LettaMessage, LettaMessageUnion
|
|
25
26
|
from letta.schemas.letta_request import LettaRequest, LettaStreamingRequest
|
|
26
27
|
from letta.schemas.letta_response import LettaResponse, LettaStreamingResponse
|
|
27
28
|
from letta.schemas.llm_config import LLMConfig
|
|
28
29
|
from letta.schemas.memory import ArchivalMemorySummary, ChatMemory, CreateArchivalMemory, Memory, RecallMemorySummary
|
|
29
30
|
from letta.schemas.message import Message, MessageCreate, MessageUpdate
|
|
31
|
+
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
30
32
|
from letta.schemas.openai.chat_completions import ToolCall
|
|
31
33
|
from letta.schemas.organization import Organization
|
|
32
34
|
from letta.schemas.passage import Passage
|
|
35
|
+
from letta.schemas.run import Run
|
|
33
36
|
from letta.schemas.sandbox_config import E2BSandboxConfig, LocalSandboxConfig, SandboxConfig, SandboxConfigCreate, SandboxConfigUpdate
|
|
34
37
|
from letta.schemas.source import Source, SourceCreate, SourceUpdate
|
|
35
38
|
from letta.schemas.tool import Tool, ToolCreate, ToolUpdate
|
|
@@ -433,11 +436,22 @@ class RESTClient(AbstractClient):
|
|
|
433
436
|
self._default_llm_config = default_llm_config
|
|
434
437
|
self._default_embedding_config = default_embedding_config
|
|
435
438
|
|
|
436
|
-
def list_agents(
|
|
437
|
-
|
|
439
|
+
def list_agents(
|
|
440
|
+
self, tags: Optional[List[str]] = None, query_text: Optional[str] = None, limit: int = 50, cursor: Optional[str] = None
|
|
441
|
+
) -> List[AgentState]:
|
|
442
|
+
params = {"limit": limit}
|
|
438
443
|
if tags:
|
|
439
444
|
params["tags"] = tags
|
|
445
|
+
params["match_all_tags"] = False
|
|
446
|
+
|
|
447
|
+
if query_text:
|
|
448
|
+
params["query_text"] = query_text
|
|
449
|
+
|
|
450
|
+
if cursor:
|
|
451
|
+
params["cursor"] = cursor
|
|
452
|
+
|
|
440
453
|
response = requests.get(f"{self.base_url}/{self.api_prefix}/agents", headers=self.headers, params=params)
|
|
454
|
+
print(f"\nLIST RESPONSE\n{response.json()}\n")
|
|
441
455
|
return [AgentState(**agent) for agent in response.json()]
|
|
442
456
|
|
|
443
457
|
def agent_exists(self, agent_id: str) -> bool:
|
|
@@ -543,6 +557,7 @@ class RESTClient(AbstractClient):
|
|
|
543
557
|
"embedding_config": embedding_config if embedding_config else self._default_embedding_config,
|
|
544
558
|
"initial_message_sequence": initial_message_sequence,
|
|
545
559
|
"tags": tags,
|
|
560
|
+
"include_base_tools": include_base_tools,
|
|
546
561
|
}
|
|
547
562
|
|
|
548
563
|
# Only add name if it's not None
|
|
@@ -983,7 +998,7 @@ class RESTClient(AbstractClient):
|
|
|
983
998
|
role: str,
|
|
984
999
|
agent_id: Optional[str] = None,
|
|
985
1000
|
name: Optional[str] = None,
|
|
986
|
-
) ->
|
|
1001
|
+
) -> Run:
|
|
987
1002
|
"""
|
|
988
1003
|
Send a message to an agent (async, returns a job)
|
|
989
1004
|
|
|
@@ -1006,7 +1021,7 @@ class RESTClient(AbstractClient):
|
|
|
1006
1021
|
)
|
|
1007
1022
|
if response.status_code != 200:
|
|
1008
1023
|
raise ValueError(f"Failed to send message: {response.text}")
|
|
1009
|
-
response =
|
|
1024
|
+
response = Run(**response.json())
|
|
1010
1025
|
|
|
1011
1026
|
return response
|
|
1012
1027
|
|
|
@@ -1980,6 +1995,153 @@ class RESTClient(AbstractClient):
|
|
|
1980
1995
|
raise ValueError(f"Failed to update block: {response.text}")
|
|
1981
1996
|
return Block(**response.json())
|
|
1982
1997
|
|
|
1998
|
+
def get_run_messages(
|
|
1999
|
+
self,
|
|
2000
|
+
run_id: str,
|
|
2001
|
+
cursor: Optional[str] = None,
|
|
2002
|
+
limit: Optional[int] = 100,
|
|
2003
|
+
ascending: bool = True,
|
|
2004
|
+
role: Optional[MessageRole] = None,
|
|
2005
|
+
) -> List[LettaMessageUnion]:
|
|
2006
|
+
"""
|
|
2007
|
+
Get messages associated with a job with filtering options.
|
|
2008
|
+
|
|
2009
|
+
Args:
|
|
2010
|
+
job_id: ID of the job
|
|
2011
|
+
cursor: Cursor for pagination
|
|
2012
|
+
limit: Maximum number of messages to return
|
|
2013
|
+
ascending: Sort order by creation time
|
|
2014
|
+
role: Filter by message role (user/assistant/system/tool)
|
|
2015
|
+
Returns:
|
|
2016
|
+
List of messages matching the filter criteria
|
|
2017
|
+
"""
|
|
2018
|
+
params = {
|
|
2019
|
+
"cursor": cursor,
|
|
2020
|
+
"limit": limit,
|
|
2021
|
+
"ascending": ascending,
|
|
2022
|
+
"role": role,
|
|
2023
|
+
}
|
|
2024
|
+
# Remove None values
|
|
2025
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
2026
|
+
|
|
2027
|
+
response = requests.get(f"{self.base_url}/{self.api_prefix}/runs/{run_id}/messages", params=params)
|
|
2028
|
+
if response.status_code != 200:
|
|
2029
|
+
raise ValueError(f"Failed to get run messages: {response.text}")
|
|
2030
|
+
return [LettaMessage(**message) for message in response.json()]
|
|
2031
|
+
|
|
2032
|
+
def get_run_usage(
|
|
2033
|
+
self,
|
|
2034
|
+
run_id: str,
|
|
2035
|
+
) -> List[UsageStatistics]:
|
|
2036
|
+
"""
|
|
2037
|
+
Get usage statistics associated with a job.
|
|
2038
|
+
|
|
2039
|
+
Args:
|
|
2040
|
+
job_id (str): ID of the job
|
|
2041
|
+
|
|
2042
|
+
Returns:
|
|
2043
|
+
List[UsageStatistics]: List of usage statistics associated with the job
|
|
2044
|
+
"""
|
|
2045
|
+
response = requests.get(
|
|
2046
|
+
f"{self.base_url}/{self.api_prefix}/runs/{run_id}/usage",
|
|
2047
|
+
headers=self.headers,
|
|
2048
|
+
)
|
|
2049
|
+
if response.status_code != 200:
|
|
2050
|
+
raise ValueError(f"Failed to get run usage statistics: {response.text}")
|
|
2051
|
+
return [UsageStatistics(**stat) for stat in [response.json()]]
|
|
2052
|
+
|
|
2053
|
+
def get_run(self, run_id: str) -> Run:
|
|
2054
|
+
"""
|
|
2055
|
+
Get a run by ID.
|
|
2056
|
+
|
|
2057
|
+
Args:
|
|
2058
|
+
run_id (str): ID of the run
|
|
2059
|
+
|
|
2060
|
+
Returns:
|
|
2061
|
+
run (Run): Run
|
|
2062
|
+
"""
|
|
2063
|
+
response = requests.get(
|
|
2064
|
+
f"{self.base_url}/{self.api_prefix}/runs/{run_id}",
|
|
2065
|
+
headers=self.headers,
|
|
2066
|
+
)
|
|
2067
|
+
if response.status_code != 200:
|
|
2068
|
+
raise ValueError(f"Failed to get run: {response.text}")
|
|
2069
|
+
return Run(**response.json())
|
|
2070
|
+
|
|
2071
|
+
def delete_run(self, run_id: str) -> None:
|
|
2072
|
+
"""
|
|
2073
|
+
Delete a run by ID.
|
|
2074
|
+
|
|
2075
|
+
Args:
|
|
2076
|
+
run_id (str): ID of the run
|
|
2077
|
+
"""
|
|
2078
|
+
response = requests.delete(
|
|
2079
|
+
f"{self.base_url}/{self.api_prefix}/runs/{run_id}",
|
|
2080
|
+
headers=self.headers,
|
|
2081
|
+
)
|
|
2082
|
+
if response.status_code != 200:
|
|
2083
|
+
raise ValueError(f"Failed to delete run: {response.text}")
|
|
2084
|
+
|
|
2085
|
+
def list_runs(self) -> List[Run]:
|
|
2086
|
+
"""
|
|
2087
|
+
List all runs.
|
|
2088
|
+
|
|
2089
|
+
Returns:
|
|
2090
|
+
runs (List[Run]): List of runs
|
|
2091
|
+
"""
|
|
2092
|
+
response = requests.get(
|
|
2093
|
+
f"{self.base_url}/{self.api_prefix}/runs",
|
|
2094
|
+
headers=self.headers,
|
|
2095
|
+
)
|
|
2096
|
+
if response.status_code != 200:
|
|
2097
|
+
raise ValueError(f"Failed to list runs: {response.text}")
|
|
2098
|
+
return [Run(**run) for run in response.json()]
|
|
2099
|
+
|
|
2100
|
+
def list_active_runs(self) -> List[Run]:
|
|
2101
|
+
"""
|
|
2102
|
+
List all active runs.
|
|
2103
|
+
|
|
2104
|
+
Returns:
|
|
2105
|
+
runs (List[Run]): List of active runs
|
|
2106
|
+
"""
|
|
2107
|
+
response = requests.get(
|
|
2108
|
+
f"{self.base_url}/{self.api_prefix}/runs/active",
|
|
2109
|
+
headers=self.headers,
|
|
2110
|
+
)
|
|
2111
|
+
if response.status_code != 200:
|
|
2112
|
+
raise ValueError(f"Failed to list active runs: {response.text}")
|
|
2113
|
+
return [Run(**run) for run in response.json()]
|
|
2114
|
+
|
|
2115
|
+
def get_tags(
|
|
2116
|
+
self,
|
|
2117
|
+
cursor: Optional[str] = None,
|
|
2118
|
+
limit: Optional[int] = None,
|
|
2119
|
+
query_text: Optional[str] = None,
|
|
2120
|
+
) -> List[str]:
|
|
2121
|
+
"""
|
|
2122
|
+
Get a list of all unique tags.
|
|
2123
|
+
|
|
2124
|
+
Args:
|
|
2125
|
+
cursor: Optional cursor for pagination (last tag seen)
|
|
2126
|
+
limit: Optional maximum number of tags to return
|
|
2127
|
+
query_text: Optional text to filter tags
|
|
2128
|
+
|
|
2129
|
+
Returns:
|
|
2130
|
+
List[str]: List of unique tags
|
|
2131
|
+
"""
|
|
2132
|
+
params = {}
|
|
2133
|
+
if cursor:
|
|
2134
|
+
params["cursor"] = cursor
|
|
2135
|
+
if limit:
|
|
2136
|
+
params["limit"] = limit
|
|
2137
|
+
if query_text:
|
|
2138
|
+
params["query_text"] = query_text
|
|
2139
|
+
|
|
2140
|
+
response = requests.get(f"{self.base_url}/{self.api_prefix}/tags", headers=self.headers, params=params)
|
|
2141
|
+
if response.status_code != 200:
|
|
2142
|
+
raise ValueError(f"Failed to get tags: {response.text}")
|
|
2143
|
+
return response.json()
|
|
2144
|
+
|
|
1983
2145
|
|
|
1984
2146
|
class LocalClient(AbstractClient):
|
|
1985
2147
|
"""
|
|
@@ -2038,10 +2200,12 @@ class LocalClient(AbstractClient):
|
|
|
2038
2200
|
self.organization = self.server.get_organization_or_default(self.org_id)
|
|
2039
2201
|
|
|
2040
2202
|
# agents
|
|
2041
|
-
def list_agents(
|
|
2203
|
+
def list_agents(
|
|
2204
|
+
self, query_text: Optional[str] = None, tags: Optional[List[str]] = None, limit: int = 100, cursor: Optional[str] = None
|
|
2205
|
+
) -> List[AgentState]:
|
|
2042
2206
|
self.interface.clear()
|
|
2043
2207
|
|
|
2044
|
-
return self.server.agent_manager.list_agents(actor=self.user, tags=tags,
|
|
2208
|
+
return self.server.agent_manager.list_agents(actor=self.user, tags=tags, query_text=query_text, limit=limit, cursor=cursor)
|
|
2045
2209
|
|
|
2046
2210
|
def agent_exists(self, agent_id: Optional[str] = None, agent_name: Optional[str] = None) -> bool:
|
|
2047
2211
|
"""
|
|
@@ -2087,6 +2251,7 @@ class LocalClient(AbstractClient):
|
|
|
2087
2251
|
tool_ids: Optional[List[str]] = None,
|
|
2088
2252
|
tool_rules: Optional[List[BaseToolRule]] = None,
|
|
2089
2253
|
include_base_tools: Optional[bool] = True,
|
|
2254
|
+
include_multi_agent_tools: bool = False,
|
|
2090
2255
|
# metadata
|
|
2091
2256
|
metadata: Optional[Dict] = {"human:": DEFAULT_HUMAN, "persona": DEFAULT_PERSONA},
|
|
2092
2257
|
description: Optional[str] = None,
|
|
@@ -2104,6 +2269,7 @@ class LocalClient(AbstractClient):
|
|
|
2104
2269
|
tools (List[str]): List of tools
|
|
2105
2270
|
tool_rules (Optional[List[BaseToolRule]]): List of tool rules
|
|
2106
2271
|
include_base_tools (bool): Include base tools
|
|
2272
|
+
include_multi_agent_tools (bool): Include multi agent tools
|
|
2107
2273
|
metadata (Dict): Metadata
|
|
2108
2274
|
description (str): Description
|
|
2109
2275
|
tags (List[str]): Tags for filtering agents
|
|
@@ -2113,11 +2279,6 @@ class LocalClient(AbstractClient):
|
|
|
2113
2279
|
"""
|
|
2114
2280
|
# construct list of tools
|
|
2115
2281
|
tool_ids = tool_ids or []
|
|
2116
|
-
tool_names = []
|
|
2117
|
-
if include_base_tools:
|
|
2118
|
-
tool_names += BASE_TOOLS
|
|
2119
|
-
tool_names += BASE_MEMORY_TOOLS
|
|
2120
|
-
tool_ids += [self.server.tool_manager.get_tool_by_name(tool_name=name, actor=self.user).id for name in tool_names]
|
|
2121
2282
|
|
|
2122
2283
|
# check if default configs are provided
|
|
2123
2284
|
assert embedding_config or self._default_embedding_config, f"Embedding config must be provided"
|
|
@@ -2140,6 +2301,7 @@ class LocalClient(AbstractClient):
|
|
|
2140
2301
|
"tool_ids": tool_ids,
|
|
2141
2302
|
"tool_rules": tool_rules,
|
|
2142
2303
|
"include_base_tools": include_base_tools,
|
|
2304
|
+
"include_multi_agent_tools": include_multi_agent_tools,
|
|
2143
2305
|
"system": system,
|
|
2144
2306
|
"agent_type": agent_type,
|
|
2145
2307
|
"llm_config": llm_config if llm_config else self._default_llm_config,
|
|
@@ -3433,3 +3595,104 @@ class LocalClient(AbstractClient):
|
|
|
3433
3595
|
if label:
|
|
3434
3596
|
data["label"] = label
|
|
3435
3597
|
return self.server.block_manager.update_block(block_id, actor=self.user, block_update=BlockUpdate(**data))
|
|
3598
|
+
|
|
3599
|
+
def get_run_messages(
|
|
3600
|
+
self,
|
|
3601
|
+
run_id: str,
|
|
3602
|
+
cursor: Optional[str] = None,
|
|
3603
|
+
limit: Optional[int] = 100,
|
|
3604
|
+
ascending: bool = True,
|
|
3605
|
+
role: Optional[MessageRole] = None,
|
|
3606
|
+
) -> List[LettaMessageUnion]:
|
|
3607
|
+
"""
|
|
3608
|
+
Get messages associated with a job with filtering options.
|
|
3609
|
+
|
|
3610
|
+
Args:
|
|
3611
|
+
run_id: ID of the run
|
|
3612
|
+
cursor: Cursor for pagination
|
|
3613
|
+
limit: Maximum number of messages to return
|
|
3614
|
+
ascending: Sort order by creation time
|
|
3615
|
+
role: Filter by message role (user/assistant/system/tool)
|
|
3616
|
+
|
|
3617
|
+
Returns:
|
|
3618
|
+
List of messages matching the filter criteria
|
|
3619
|
+
"""
|
|
3620
|
+
params = {
|
|
3621
|
+
"cursor": cursor,
|
|
3622
|
+
"limit": limit,
|
|
3623
|
+
"ascending": ascending,
|
|
3624
|
+
"role": role,
|
|
3625
|
+
}
|
|
3626
|
+
return self.server.job_manager.get_run_messages_cursor(run_id=run_id, actor=self.user, **params)
|
|
3627
|
+
|
|
3628
|
+
def get_run_usage(
|
|
3629
|
+
self,
|
|
3630
|
+
run_id: str,
|
|
3631
|
+
) -> List[UsageStatistics]:
|
|
3632
|
+
"""
|
|
3633
|
+
Get usage statistics associated with a job.
|
|
3634
|
+
|
|
3635
|
+
Args:
|
|
3636
|
+
run_id (str): ID of the run
|
|
3637
|
+
|
|
3638
|
+
Returns:
|
|
3639
|
+
List[UsageStatistics]: List of usage statistics associated with the run
|
|
3640
|
+
"""
|
|
3641
|
+
usage = self.server.job_manager.get_job_usage(job_id=run_id, actor=self.user)
|
|
3642
|
+
return [
|
|
3643
|
+
UsageStatistics(completion_tokens=stat.completion_tokens, prompt_tokens=stat.prompt_tokens, total_tokens=stat.total_tokens)
|
|
3644
|
+
for stat in usage
|
|
3645
|
+
]
|
|
3646
|
+
|
|
3647
|
+
def get_run(self, run_id: str) -> Run:
|
|
3648
|
+
"""
|
|
3649
|
+
Get a run by ID.
|
|
3650
|
+
|
|
3651
|
+
Args:
|
|
3652
|
+
run_id (str): ID of the run
|
|
3653
|
+
|
|
3654
|
+
Returns:
|
|
3655
|
+
run (Run): Run
|
|
3656
|
+
"""
|
|
3657
|
+
return self.server.job_manager.get_job_by_id(job_id=run_id, actor=self.user)
|
|
3658
|
+
|
|
3659
|
+
def delete_run(self, run_id: str) -> None:
|
|
3660
|
+
"""
|
|
3661
|
+
Delete a run by ID.
|
|
3662
|
+
|
|
3663
|
+
Args:
|
|
3664
|
+
run_id (str): ID of the run
|
|
3665
|
+
"""
|
|
3666
|
+
return self.server.job_manager.delete_job_by_id(job_id=run_id, actor=self.user)
|
|
3667
|
+
|
|
3668
|
+
def list_runs(self) -> List[Run]:
|
|
3669
|
+
"""
|
|
3670
|
+
List all runs.
|
|
3671
|
+
|
|
3672
|
+
Returns:
|
|
3673
|
+
runs (List[Run]): List of runs
|
|
3674
|
+
"""
|
|
3675
|
+
return self.server.job_manager.list_jobs(actor=self.user, job_type=JobType.RUN)
|
|
3676
|
+
|
|
3677
|
+
def list_active_runs(self) -> List[Run]:
|
|
3678
|
+
"""
|
|
3679
|
+
List all active runs.
|
|
3680
|
+
|
|
3681
|
+
Returns:
|
|
3682
|
+
runs (List[Run]): List of active runs
|
|
3683
|
+
"""
|
|
3684
|
+
return self.server.job_manager.list_jobs(actor=self.user, job_type=JobType.RUN, statuses=[JobStatus.created, JobStatus.running])
|
|
3685
|
+
|
|
3686
|
+
def get_tags(
|
|
3687
|
+
self,
|
|
3688
|
+
cursor: str = None,
|
|
3689
|
+
limit: int = 100,
|
|
3690
|
+
query_text: str = None,
|
|
3691
|
+
) -> List[str]:
|
|
3692
|
+
"""
|
|
3693
|
+
Get all tags.
|
|
3694
|
+
|
|
3695
|
+
Returns:
|
|
3696
|
+
tags (List[str]): List of tags
|
|
3697
|
+
"""
|
|
3698
|
+
return self.server.agent_manager.list_tags(actor=self.user, cursor=cursor, limit=limit, query_text=query_text)
|
letta/constants.py
CHANGED
|
@@ -12,6 +12,7 @@ COMPOSIO_ENTITY_ENV_VAR_KEY = "COMPOSIO_ENTITY"
|
|
|
12
12
|
COMPOSIO_TOOL_TAG_NAME = "composio"
|
|
13
13
|
|
|
14
14
|
LETTA_CORE_TOOL_MODULE_NAME = "letta.functions.function_sets.base"
|
|
15
|
+
LETTA_MULTI_AGENT_TOOL_MODULE_NAME = "letta.functions.function_sets.multi_agent"
|
|
15
16
|
|
|
16
17
|
# String in the error message for when the context window is too large
|
|
17
18
|
# Example full message:
|
|
@@ -48,6 +49,10 @@ DEFAULT_PRESET = "memgpt_chat"
|
|
|
48
49
|
BASE_TOOLS = ["send_message", "conversation_search", "archival_memory_insert", "archival_memory_search"]
|
|
49
50
|
# Base memory tools CAN be edited, and are added by default by the server
|
|
50
51
|
BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace"]
|
|
52
|
+
# Multi agent tools
|
|
53
|
+
MULTI_AGENT_TOOLS = ["send_message_to_specific_agent", "send_message_to_agents_matching_all_tags"]
|
|
54
|
+
MULTI_AGENT_SEND_MESSAGE_MAX_RETRIES = 3
|
|
55
|
+
MULTI_AGENT_SEND_MESSAGE_TIMEOUT = 20 * 60
|
|
51
56
|
|
|
52
57
|
# The name of the tool used to send message to the user
|
|
53
58
|
# May not be relevant in cases where the agent has multiple ways to message to user (send_imessage, send_discord_mesasge, ...)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
3
|
+
|
|
4
|
+
from letta.constants import MULTI_AGENT_SEND_MESSAGE_MAX_RETRIES, MULTI_AGENT_SEND_MESSAGE_TIMEOUT
|
|
5
|
+
from letta.functions.helpers import async_send_message_with_retries
|
|
6
|
+
from letta.orm.errors import NoResultFound
|
|
7
|
+
from letta.server.rest_api.utils import get_letta_server
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from letta.agent import Agent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def send_message_to_specific_agent(self: "Agent", message: str, other_agent_id: str) -> Optional[str]:
|
|
14
|
+
"""
|
|
15
|
+
Send a message to a specific Letta agent within the same organization.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
message (str): The message to be sent to the target Letta agent.
|
|
19
|
+
other_agent_id (str): The identifier of the target Letta agent.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Optional[str]: The response from the Letta agent. It's possible that the agent does not respond.
|
|
23
|
+
"""
|
|
24
|
+
server = get_letta_server()
|
|
25
|
+
|
|
26
|
+
# Ensure the target agent is in the same org
|
|
27
|
+
try:
|
|
28
|
+
server.agent_manager.get_agent_by_id(agent_id=other_agent_id, actor=self.user)
|
|
29
|
+
except NoResultFound:
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"The passed-in agent_id {other_agent_id} either does not exist, "
|
|
32
|
+
f"or does not belong to the same org ({self.user.organization_id})."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Async logic to send a message with retries and timeout
|
|
36
|
+
async def async_send_single_agent():
|
|
37
|
+
return await async_send_message_with_retries(
|
|
38
|
+
server=server,
|
|
39
|
+
sender_agent=self,
|
|
40
|
+
target_agent_id=other_agent_id,
|
|
41
|
+
message_text=message,
|
|
42
|
+
max_retries=MULTI_AGENT_SEND_MESSAGE_MAX_RETRIES, # or your chosen constants
|
|
43
|
+
timeout=MULTI_AGENT_SEND_MESSAGE_TIMEOUT, # e.g., 1200 for 20 min
|
|
44
|
+
logging_prefix="[send_message_to_specific_agent]",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Run in the current event loop or create one if needed
|
|
48
|
+
try:
|
|
49
|
+
return asyncio.run(async_send_single_agent())
|
|
50
|
+
except RuntimeError:
|
|
51
|
+
# e.g., in case there's already an active loop
|
|
52
|
+
loop = asyncio.get_event_loop()
|
|
53
|
+
if loop.is_running():
|
|
54
|
+
return loop.run_until_complete(async_send_single_agent())
|
|
55
|
+
else:
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def send_message_to_agents_matching_all_tags(self: "Agent", message: str, tags: List[str]) -> List[str]:
|
|
60
|
+
"""
|
|
61
|
+
Send a message to all agents in the same organization that match ALL of the given tags.
|
|
62
|
+
|
|
63
|
+
Messages are sent in parallel for improved performance, with retries on flaky calls and timeouts for long-running requests.
|
|
64
|
+
This function does not use a cursor (pagination) and enforces a limit of 100 agents.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
message (str): The message to be sent to each matching agent.
|
|
68
|
+
tags (List[str]): The list of tags that each agent must have (match_all_tags=True).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List[str]: A list of responses from the agents that match all tags.
|
|
72
|
+
Each response corresponds to one agent.
|
|
73
|
+
"""
|
|
74
|
+
server = get_letta_server()
|
|
75
|
+
|
|
76
|
+
# Retrieve agents that match ALL specified tags
|
|
77
|
+
matching_agents = server.agent_manager.list_agents(actor=self.user, tags=tags, match_all_tags=True, cursor=None, limit=100)
|
|
78
|
+
|
|
79
|
+
async def send_messages_to_all_agents():
|
|
80
|
+
tasks = [
|
|
81
|
+
async_send_message_with_retries(
|
|
82
|
+
server=server,
|
|
83
|
+
sender_agent=self,
|
|
84
|
+
target_agent_id=agent_state.id,
|
|
85
|
+
message_text=message,
|
|
86
|
+
max_retries=MULTI_AGENT_SEND_MESSAGE_MAX_RETRIES,
|
|
87
|
+
timeout=MULTI_AGENT_SEND_MESSAGE_TIMEOUT,
|
|
88
|
+
logging_prefix="[send_message_to_agents_matching_all_tags]",
|
|
89
|
+
)
|
|
90
|
+
for agent_state in matching_agents
|
|
91
|
+
]
|
|
92
|
+
# Run all tasks in parallel
|
|
93
|
+
return await asyncio.gather(*tasks)
|
|
94
|
+
|
|
95
|
+
# Run the async function and return results
|
|
96
|
+
return asyncio.run(send_messages_to_all_agents())
|