letta-nightly 0.5.0.dev20241015104156__py3-none-any.whl → 0.5.0.dev20241017104103__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/agent.py +170 -16
- letta/client/client.py +186 -42
- letta/client/utils.py +15 -0
- letta/constants.py +1 -1
- letta/functions/functions.py +1 -1
- letta/functions/schema_generator.py +3 -2
- letta/main.py +6 -4
- letta/metadata.py +27 -3
- letta/schemas/agent.py +7 -3
- letta/schemas/memory.py +37 -0
- letta/schemas/tool.py +4 -0
- letta/server/rest_api/routers/openai/assistants/threads.py +1 -1
- letta/server/rest_api/routers/v1/agents.py +43 -0
- letta/server/rest_api/routers/v1/sources.py +28 -1
- letta/server/rest_api/routers/v1/tools.py +1 -1
- letta/server/server.py +157 -94
- letta/server/static_files/assets/{index-dc228d4a.js → index-d6b3669a.js} +59 -59
- letta/server/static_files/index.html +1 -1
- letta_nightly-0.5.0.dev20241017104103.dist-info/METADATA +203 -0
- {letta_nightly-0.5.0.dev20241015104156.dist-info → letta_nightly-0.5.0.dev20241017104103.dist-info}/RECORD +23 -23
- letta_nightly-0.5.0.dev20241015104156.dist-info/METADATA +0 -105
- {letta_nightly-0.5.0.dev20241015104156.dist-info → letta_nightly-0.5.0.dev20241017104103.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.0.dev20241015104156.dist-info → letta_nightly-0.5.0.dev20241017104103.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.0.dev20241015104156.dist-info → letta_nightly-0.5.0.dev20241017104103.dist-info}/entry_points.txt +0 -0
letta/server/server.py
CHANGED
|
@@ -16,6 +16,7 @@ import letta.system as system
|
|
|
16
16
|
from letta.agent import Agent, save_agent
|
|
17
17
|
from letta.agent_store.db import attach_base
|
|
18
18
|
from letta.agent_store.storage import StorageConnector, TableType
|
|
19
|
+
from letta.client.utils import derive_function_name_regex
|
|
19
20
|
from letta.credentials import LettaCredentials
|
|
20
21
|
from letta.data_sources.connectors import DataConnector, load_data
|
|
21
22
|
|
|
@@ -72,9 +73,13 @@ from letta.schemas.file import FileMetadata
|
|
|
72
73
|
from letta.schemas.job import Job
|
|
73
74
|
from letta.schemas.letta_message import LettaMessage
|
|
74
75
|
from letta.schemas.llm_config import LLMConfig
|
|
75
|
-
from letta.schemas.memory import
|
|
76
|
+
from letta.schemas.memory import (
|
|
77
|
+
ArchivalMemorySummary,
|
|
78
|
+
ContextWindowOverview,
|
|
79
|
+
Memory,
|
|
80
|
+
RecallMemorySummary,
|
|
81
|
+
)
|
|
76
82
|
from letta.schemas.message import Message, MessageCreate, MessageRole, UpdateMessage
|
|
77
|
-
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
78
83
|
from letta.schemas.organization import Organization, OrganizationCreate
|
|
79
84
|
from letta.schemas.passage import Passage
|
|
80
85
|
from letta.schemas.source import Source, SourceCreate, SourceUpdate
|
|
@@ -411,6 +416,7 @@ class SyncServer(Server):
|
|
|
411
416
|
raise ValueError(f"messages should be a Message or a list of Message, got {type(input_messages)}")
|
|
412
417
|
|
|
413
418
|
logger.debug(f"Got input messages: {input_messages}")
|
|
419
|
+
letta_agent = None
|
|
414
420
|
try:
|
|
415
421
|
|
|
416
422
|
# Get the agent object (loaded in memory)
|
|
@@ -422,83 +428,14 @@ class SyncServer(Server):
|
|
|
422
428
|
token_streaming = letta_agent.interface.streaming_mode if hasattr(letta_agent.interface, "streaming_mode") else False
|
|
423
429
|
|
|
424
430
|
logger.debug(f"Starting agent step")
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
first_message=False,
|
|
434
|
-
skip_verify=no_verify,
|
|
435
|
-
return_dicts=False,
|
|
436
|
-
stream=token_streaming,
|
|
437
|
-
# timestamp=timestamp,
|
|
438
|
-
ms=self.ms,
|
|
439
|
-
)
|
|
440
|
-
step_response.messages
|
|
441
|
-
heartbeat_request = step_response.heartbeat_request
|
|
442
|
-
function_failed = step_response.function_failed
|
|
443
|
-
token_warning = step_response.in_context_memory_warning
|
|
444
|
-
usage = step_response.usage
|
|
445
|
-
|
|
446
|
-
step_count += 1
|
|
447
|
-
total_usage += usage
|
|
448
|
-
counter += 1
|
|
449
|
-
letta_agent.interface.step_complete()
|
|
450
|
-
|
|
451
|
-
logger.debug("Saving agent state")
|
|
452
|
-
# save updated state
|
|
453
|
-
save_agent(letta_agent, self.ms)
|
|
454
|
-
|
|
455
|
-
# Chain stops
|
|
456
|
-
if not self.chaining:
|
|
457
|
-
logger.debug("No chaining, stopping after one step")
|
|
458
|
-
break
|
|
459
|
-
elif self.max_chaining_steps is not None and counter > self.max_chaining_steps:
|
|
460
|
-
logger.debug(f"Hit max chaining steps, stopping after {counter} steps")
|
|
461
|
-
break
|
|
462
|
-
# Chain handlers
|
|
463
|
-
elif token_warning:
|
|
464
|
-
assert letta_agent.agent_state.user_id is not None
|
|
465
|
-
next_input_message = Message.dict_to_message(
|
|
466
|
-
agent_id=letta_agent.agent_state.id,
|
|
467
|
-
user_id=letta_agent.agent_state.user_id,
|
|
468
|
-
model=letta_agent.model,
|
|
469
|
-
openai_message_dict={
|
|
470
|
-
"role": "user", # TODO: change to system?
|
|
471
|
-
"content": system.get_token_limit_warning(),
|
|
472
|
-
},
|
|
473
|
-
)
|
|
474
|
-
continue # always chain
|
|
475
|
-
elif function_failed:
|
|
476
|
-
assert letta_agent.agent_state.user_id is not None
|
|
477
|
-
next_input_message = Message.dict_to_message(
|
|
478
|
-
agent_id=letta_agent.agent_state.id,
|
|
479
|
-
user_id=letta_agent.agent_state.user_id,
|
|
480
|
-
model=letta_agent.model,
|
|
481
|
-
openai_message_dict={
|
|
482
|
-
"role": "user", # TODO: change to system?
|
|
483
|
-
"content": system.get_heartbeat(constants.FUNC_FAILED_HEARTBEAT_MESSAGE),
|
|
484
|
-
},
|
|
485
|
-
)
|
|
486
|
-
continue # always chain
|
|
487
|
-
elif heartbeat_request:
|
|
488
|
-
assert letta_agent.agent_state.user_id is not None
|
|
489
|
-
next_input_message = Message.dict_to_message(
|
|
490
|
-
agent_id=letta_agent.agent_state.id,
|
|
491
|
-
user_id=letta_agent.agent_state.user_id,
|
|
492
|
-
model=letta_agent.model,
|
|
493
|
-
openai_message_dict={
|
|
494
|
-
"role": "user", # TODO: change to system?
|
|
495
|
-
"content": system.get_heartbeat(constants.REQ_HEARTBEAT_MESSAGE),
|
|
496
|
-
},
|
|
497
|
-
)
|
|
498
|
-
continue # always chain
|
|
499
|
-
# Letta no-op / yield
|
|
500
|
-
else:
|
|
501
|
-
break
|
|
431
|
+
usage_stats = letta_agent.step(
|
|
432
|
+
messages=input_messages,
|
|
433
|
+
chaining=self.chaining,
|
|
434
|
+
max_chaining_steps=self.max_chaining_steps,
|
|
435
|
+
stream=token_streaming,
|
|
436
|
+
ms=self.ms,
|
|
437
|
+
skip_verify=True,
|
|
438
|
+
)
|
|
502
439
|
|
|
503
440
|
except Exception as e:
|
|
504
441
|
logger.error(f"Error in server._step: {e}")
|
|
@@ -506,9 +443,10 @@ class SyncServer(Server):
|
|
|
506
443
|
raise
|
|
507
444
|
finally:
|
|
508
445
|
logger.debug("Calling step_yield()")
|
|
509
|
-
letta_agent
|
|
446
|
+
if letta_agent:
|
|
447
|
+
letta_agent.interface.step_yield()
|
|
510
448
|
|
|
511
|
-
return
|
|
449
|
+
return usage_stats
|
|
512
450
|
|
|
513
451
|
def _command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
|
|
514
452
|
"""Process a CLI command"""
|
|
@@ -794,7 +732,7 @@ class SyncServer(Server):
|
|
|
794
732
|
message_objects.append(message)
|
|
795
733
|
|
|
796
734
|
else:
|
|
797
|
-
raise ValueError(f"All messages must be of type Message or MessageCreate, got {type(messages
|
|
735
|
+
raise ValueError(f"All messages must be of type Message or MessageCreate, got {[type(message) for message in messages]}")
|
|
798
736
|
|
|
799
737
|
# Run the agent state forward
|
|
800
738
|
return self._step(user_id=user_id, agent_id=agent_id, input_messages=message_objects)
|
|
@@ -1028,6 +966,80 @@ class SyncServer(Server):
|
|
|
1028
966
|
# TODO: probably reload the agent somehow?
|
|
1029
967
|
return letta_agent.agent_state
|
|
1030
968
|
|
|
969
|
+
def add_tool_to_agent(
|
|
970
|
+
self,
|
|
971
|
+
agent_id: str,
|
|
972
|
+
tool_id: str,
|
|
973
|
+
user_id: str,
|
|
974
|
+
):
|
|
975
|
+
"""Update the agents core memory block, return the new state"""
|
|
976
|
+
if self.ms.get_user(user_id=user_id) is None:
|
|
977
|
+
raise ValueError(f"User user_id={user_id} does not exist")
|
|
978
|
+
if self.ms.get_agent(agent_id=agent_id) is None:
|
|
979
|
+
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
980
|
+
|
|
981
|
+
# Get the agent object (loaded in memory)
|
|
982
|
+
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
983
|
+
|
|
984
|
+
# Get all the tool objects from the request
|
|
985
|
+
tool_objs = []
|
|
986
|
+
tool_obj = self.ms.get_tool(tool_id=tool_id, user_id=user_id)
|
|
987
|
+
assert tool_obj, f"Tool with id={tool_id} does not exist"
|
|
988
|
+
tool_objs.append(tool_obj)
|
|
989
|
+
|
|
990
|
+
for tool in letta_agent.tools:
|
|
991
|
+
tool_obj = self.ms.get_tool(tool_id=tool.id, user_id=user_id)
|
|
992
|
+
assert tool_obj, f"Tool with id={tool.id} does not exist"
|
|
993
|
+
|
|
994
|
+
# If it's not the already added tool
|
|
995
|
+
if tool_obj.id != tool_id:
|
|
996
|
+
tool_objs.append(tool_obj)
|
|
997
|
+
|
|
998
|
+
# replace the list of tool names ("ids") inside the agent state
|
|
999
|
+
letta_agent.agent_state.tools = [tool.name for tool in tool_objs]
|
|
1000
|
+
|
|
1001
|
+
# then attempt to link the tools modules
|
|
1002
|
+
letta_agent.link_tools(tool_objs)
|
|
1003
|
+
|
|
1004
|
+
# save the agent
|
|
1005
|
+
save_agent(letta_agent, self.ms)
|
|
1006
|
+
return letta_agent.agent_state
|
|
1007
|
+
|
|
1008
|
+
def remove_tool_from_agent(
|
|
1009
|
+
self,
|
|
1010
|
+
agent_id: str,
|
|
1011
|
+
tool_id: str,
|
|
1012
|
+
user_id: str,
|
|
1013
|
+
):
|
|
1014
|
+
"""Update the agents core memory block, return the new state"""
|
|
1015
|
+
if self.ms.get_user(user_id=user_id) is None:
|
|
1016
|
+
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1017
|
+
if self.ms.get_agent(agent_id=agent_id) is None:
|
|
1018
|
+
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
1019
|
+
|
|
1020
|
+
# Get the agent object (loaded in memory)
|
|
1021
|
+
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
1022
|
+
|
|
1023
|
+
# Get all the tool_objs
|
|
1024
|
+
tool_objs = []
|
|
1025
|
+
for tool in letta_agent.tools:
|
|
1026
|
+
tool_obj = self.ms.get_tool(tool_id=tool.id, user_id=user_id)
|
|
1027
|
+
assert tool_obj, f"Tool with id={tool.id} does not exist"
|
|
1028
|
+
|
|
1029
|
+
# If it's not the tool we want to remove
|
|
1030
|
+
if tool_obj.id != tool_id:
|
|
1031
|
+
tool_objs.append(tool_obj)
|
|
1032
|
+
|
|
1033
|
+
# replace the list of tool names ("ids") inside the agent state
|
|
1034
|
+
letta_agent.agent_state.tools = [tool.name for tool in tool_objs]
|
|
1035
|
+
|
|
1036
|
+
# then attempt to link the tools modules
|
|
1037
|
+
letta_agent.link_tools(tool_objs)
|
|
1038
|
+
|
|
1039
|
+
# save the agent
|
|
1040
|
+
save_agent(letta_agent, self.ms)
|
|
1041
|
+
return letta_agent.agent_state
|
|
1042
|
+
|
|
1031
1043
|
def _agent_state_to_config(self, agent_state: AgentState) -> dict:
|
|
1032
1044
|
"""Convert AgentState to a dict for a JSON response"""
|
|
1033
1045
|
assert agent_state is not None
|
|
@@ -1683,6 +1695,9 @@ class SyncServer(Server):
|
|
|
1683
1695
|
|
|
1684
1696
|
return job
|
|
1685
1697
|
|
|
1698
|
+
def delete_file_from_source(self, source_id: str, file_id: str, user_id: Optional[str]) -> Optional[FileMetadata]:
|
|
1699
|
+
return self.ms.delete_file_from_source(source_id=source_id, file_id=file_id, user_id=user_id)
|
|
1700
|
+
|
|
1686
1701
|
def load_data(
|
|
1687
1702
|
self,
|
|
1688
1703
|
user_id: str,
|
|
@@ -1811,6 +1826,15 @@ class SyncServer(Server):
|
|
|
1811
1826
|
"""Get tool by ID."""
|
|
1812
1827
|
return self.ms.get_tool(tool_id=tool_id)
|
|
1813
1828
|
|
|
1829
|
+
def tool_with_name_and_user_id_exists(self, tool: Tool, user_id: Optional[str] = None) -> bool:
|
|
1830
|
+
"""Check if tool exists"""
|
|
1831
|
+
tool = self.ms.get_tool_with_name_and_user_id(tool_name=tool.name, user_id=user_id)
|
|
1832
|
+
|
|
1833
|
+
if tool is None:
|
|
1834
|
+
return False
|
|
1835
|
+
else:
|
|
1836
|
+
return True
|
|
1837
|
+
|
|
1814
1838
|
def get_tool_id(self, name: str, user_id: str) -> Optional[str]:
|
|
1815
1839
|
"""Get tool ID from name and user_id."""
|
|
1816
1840
|
tool = self.ms.get_tool(tool_name=name, user_id=user_id)
|
|
@@ -1818,16 +1842,27 @@ class SyncServer(Server):
|
|
|
1818
1842
|
return None
|
|
1819
1843
|
return tool.id
|
|
1820
1844
|
|
|
1821
|
-
def update_tool(
|
|
1822
|
-
self,
|
|
1823
|
-
request: ToolUpdate,
|
|
1824
|
-
) -> Tool:
|
|
1845
|
+
def update_tool(self, request: ToolUpdate, user_id: Optional[str] = None) -> Tool:
|
|
1825
1846
|
"""Update an existing tool"""
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1847
|
+
if request.name:
|
|
1848
|
+
existing_tool = self.ms.get_tool_with_name_and_user_id(tool_name=request.name, user_id=user_id)
|
|
1849
|
+
if existing_tool is None:
|
|
1850
|
+
raise ValueError(f"Tool with name={request.name}, user_id={user_id} does not exist")
|
|
1851
|
+
else:
|
|
1852
|
+
existing_tool = self.ms.get_tool(tool_id=request.id)
|
|
1853
|
+
if existing_tool is None:
|
|
1854
|
+
raise ValueError(f"Tool with id={request.id} does not exist")
|
|
1855
|
+
|
|
1856
|
+
# Preserve the original tool id
|
|
1857
|
+
# As we can override the tool id as well
|
|
1858
|
+
# This is probably bad design if this is exposed to users...
|
|
1859
|
+
original_id = existing_tool.id
|
|
1829
1860
|
|
|
1830
1861
|
# override updated fields
|
|
1862
|
+
if request.id:
|
|
1863
|
+
existing_tool.id = request.id
|
|
1864
|
+
if request.description:
|
|
1865
|
+
existing_tool.description = request.description
|
|
1831
1866
|
if request.source_code:
|
|
1832
1867
|
existing_tool.source_code = request.source_code
|
|
1833
1868
|
if request.source_type:
|
|
@@ -1836,10 +1871,15 @@ class SyncServer(Server):
|
|
|
1836
1871
|
existing_tool.tags = request.tags
|
|
1837
1872
|
if request.json_schema:
|
|
1838
1873
|
existing_tool.json_schema = request.json_schema
|
|
1874
|
+
|
|
1875
|
+
# If name is explicitly provided here, overide the tool name
|
|
1839
1876
|
if request.name:
|
|
1840
1877
|
existing_tool.name = request.name
|
|
1878
|
+
# Otherwise, if there's no name, and there's source code, we try to derive the name
|
|
1879
|
+
elif request.source_code:
|
|
1880
|
+
existing_tool.name = derive_function_name_regex(request.source_code)
|
|
1841
1881
|
|
|
1842
|
-
self.ms.update_tool(existing_tool)
|
|
1882
|
+
self.ms.update_tool(original_id, existing_tool)
|
|
1843
1883
|
return self.ms.get_tool(tool_id=request.id)
|
|
1844
1884
|
|
|
1845
1885
|
def create_tool(self, request: ToolCreate, user_id: Optional[str] = None, update: bool = True) -> Tool: # TODO: add other fields
|
|
@@ -1866,7 +1906,7 @@ class SyncServer(Server):
|
|
|
1866
1906
|
|
|
1867
1907
|
# TODO: not sure if this always works
|
|
1868
1908
|
func = env[functions[-1]]
|
|
1869
|
-
json_schema = generate_schema(func)
|
|
1909
|
+
json_schema = generate_schema(func, terminal=request.terminal)
|
|
1870
1910
|
else:
|
|
1871
1911
|
# provided by client
|
|
1872
1912
|
json_schema = request.json_schema
|
|
@@ -1877,15 +1917,23 @@ class SyncServer(Server):
|
|
|
1877
1917
|
assert request.name, f"Tool name must be provided in json_schema {json_schema}. This should never happen."
|
|
1878
1918
|
|
|
1879
1919
|
# check if already exists:
|
|
1880
|
-
existing_tool = self.ms.get_tool(tool_name=request.name, user_id=user_id)
|
|
1920
|
+
existing_tool = self.ms.get_tool(tool_id=request.id, tool_name=request.name, user_id=user_id)
|
|
1881
1921
|
if existing_tool:
|
|
1882
1922
|
if update:
|
|
1883
|
-
|
|
1923
|
+
# id is an optional field, so we will fill it with the existing tool id
|
|
1924
|
+
if not request.id:
|
|
1925
|
+
request.id = existing_tool.id
|
|
1926
|
+
updated_tool = self.update_tool(ToolUpdate(**vars(request)), user_id)
|
|
1884
1927
|
assert updated_tool is not None, f"Failed to update tool {request.name}"
|
|
1885
1928
|
return updated_tool
|
|
1886
1929
|
else:
|
|
1887
1930
|
raise ValueError(f"Tool {request.name} already exists and update=False")
|
|
1888
1931
|
|
|
1932
|
+
# check for description
|
|
1933
|
+
description = None
|
|
1934
|
+
if request.description:
|
|
1935
|
+
description = request.description
|
|
1936
|
+
|
|
1889
1937
|
tool = Tool(
|
|
1890
1938
|
name=request.name,
|
|
1891
1939
|
source_code=request.source_code,
|
|
@@ -1893,9 +1941,14 @@ class SyncServer(Server):
|
|
|
1893
1941
|
tags=request.tags,
|
|
1894
1942
|
json_schema=json_schema,
|
|
1895
1943
|
user_id=user_id,
|
|
1944
|
+
description=description,
|
|
1896
1945
|
)
|
|
1946
|
+
|
|
1947
|
+
if request.id:
|
|
1948
|
+
tool.id = request.id
|
|
1949
|
+
|
|
1897
1950
|
self.ms.create_tool(tool)
|
|
1898
|
-
created_tool = self.ms.get_tool(
|
|
1951
|
+
created_tool = self.ms.get_tool(tool_id=tool.id, user_id=user_id)
|
|
1899
1952
|
return created_tool
|
|
1900
1953
|
|
|
1901
1954
|
def delete_tool(self, tool_id: str):
|
|
@@ -2087,3 +2140,13 @@ class SyncServer(Server):
|
|
|
2087
2140
|
|
|
2088
2141
|
def add_embedding_model(self, request: EmbeddingConfig) -> EmbeddingConfig:
|
|
2089
2142
|
"""Add a new embedding model"""
|
|
2143
|
+
|
|
2144
|
+
def get_agent_context_window(
|
|
2145
|
+
self,
|
|
2146
|
+
user_id: str,
|
|
2147
|
+
agent_id: str,
|
|
2148
|
+
) -> ContextWindowOverview:
|
|
2149
|
+
|
|
2150
|
+
# Get the current message
|
|
2151
|
+
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
2152
|
+
return letta_agent.get_context_window()
|