letta-nightly 0.1.7.dev20240924104148__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 +24 -0
- letta/__main__.py +3 -0
- letta/agent.py +1427 -0
- letta/agent_store/chroma.py +295 -0
- letta/agent_store/db.py +546 -0
- letta/agent_store/lancedb.py +177 -0
- letta/agent_store/milvus.py +198 -0
- letta/agent_store/qdrant.py +201 -0
- letta/agent_store/storage.py +188 -0
- letta/benchmark/benchmark.py +96 -0
- letta/benchmark/constants.py +14 -0
- letta/cli/cli.py +689 -0
- letta/cli/cli_config.py +1282 -0
- letta/cli/cli_load.py +166 -0
- letta/client/__init__.py +0 -0
- letta/client/admin.py +171 -0
- letta/client/client.py +2360 -0
- letta/client/streaming.py +90 -0
- letta/client/utils.py +61 -0
- letta/config.py +484 -0
- letta/configs/anthropic.json +13 -0
- letta/configs/letta_hosted.json +11 -0
- letta/configs/openai.json +12 -0
- letta/constants.py +134 -0
- letta/credentials.py +140 -0
- letta/data_sources/connectors.py +247 -0
- letta/embeddings.py +218 -0
- letta/errors.py +26 -0
- letta/functions/__init__.py +0 -0
- letta/functions/function_sets/base.py +174 -0
- letta/functions/function_sets/extras.py +132 -0
- letta/functions/functions.py +105 -0
- letta/functions/schema_generator.py +205 -0
- letta/humans/__init__.py +0 -0
- letta/humans/examples/basic.txt +1 -0
- letta/humans/examples/cs_phd.txt +9 -0
- letta/interface.py +314 -0
- letta/llm_api/__init__.py +0 -0
- letta/llm_api/anthropic.py +383 -0
- letta/llm_api/azure_openai.py +155 -0
- letta/llm_api/cohere.py +396 -0
- letta/llm_api/google_ai.py +468 -0
- letta/llm_api/llm_api_tools.py +485 -0
- letta/llm_api/openai.py +470 -0
- letta/local_llm/README.md +3 -0
- letta/local_llm/__init__.py +0 -0
- letta/local_llm/chat_completion_proxy.py +279 -0
- letta/local_llm/constants.py +31 -0
- letta/local_llm/function_parser.py +68 -0
- letta/local_llm/grammars/__init__.py +0 -0
- letta/local_llm/grammars/gbnf_grammar_generator.py +1324 -0
- letta/local_llm/grammars/json.gbnf +26 -0
- letta/local_llm/grammars/json_func_calls_with_inner_thoughts.gbnf +32 -0
- letta/local_llm/groq/api.py +97 -0
- letta/local_llm/json_parser.py +202 -0
- letta/local_llm/koboldcpp/api.py +62 -0
- letta/local_llm/koboldcpp/settings.py +23 -0
- letta/local_llm/llamacpp/api.py +58 -0
- letta/local_llm/llamacpp/settings.py +22 -0
- letta/local_llm/llm_chat_completion_wrappers/__init__.py +0 -0
- letta/local_llm/llm_chat_completion_wrappers/airoboros.py +452 -0
- letta/local_llm/llm_chat_completion_wrappers/chatml.py +470 -0
- letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +387 -0
- letta/local_llm/llm_chat_completion_wrappers/dolphin.py +246 -0
- letta/local_llm/llm_chat_completion_wrappers/llama3.py +345 -0
- letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +156 -0
- letta/local_llm/llm_chat_completion_wrappers/wrapper_base.py +11 -0
- letta/local_llm/llm_chat_completion_wrappers/zephyr.py +345 -0
- letta/local_llm/lmstudio/api.py +100 -0
- letta/local_llm/lmstudio/settings.py +29 -0
- letta/local_llm/ollama/api.py +88 -0
- letta/local_llm/ollama/settings.py +32 -0
- letta/local_llm/settings/__init__.py +0 -0
- letta/local_llm/settings/deterministic_mirostat.py +45 -0
- letta/local_llm/settings/settings.py +72 -0
- letta/local_llm/settings/simple.py +28 -0
- letta/local_llm/utils.py +265 -0
- letta/local_llm/vllm/api.py +63 -0
- letta/local_llm/webui/api.py +60 -0
- letta/local_llm/webui/legacy_api.py +58 -0
- letta/local_llm/webui/legacy_settings.py +23 -0
- letta/local_llm/webui/settings.py +24 -0
- letta/log.py +76 -0
- letta/main.py +437 -0
- letta/memory.py +440 -0
- letta/metadata.py +884 -0
- letta/openai_backcompat/__init__.py +0 -0
- letta/openai_backcompat/openai_object.py +437 -0
- letta/persistence_manager.py +148 -0
- letta/personas/__init__.py +0 -0
- letta/personas/examples/anna_pa.txt +13 -0
- letta/personas/examples/google_search_persona.txt +15 -0
- letta/personas/examples/memgpt_doc.txt +6 -0
- letta/personas/examples/memgpt_starter.txt +4 -0
- letta/personas/examples/sam.txt +14 -0
- letta/personas/examples/sam_pov.txt +14 -0
- letta/personas/examples/sam_simple_pov_gpt35.txt +13 -0
- letta/personas/examples/sqldb/test.db +0 -0
- letta/prompts/__init__.py +0 -0
- letta/prompts/gpt_summarize.py +14 -0
- letta/prompts/gpt_system.py +26 -0
- letta/prompts/system/memgpt_base.txt +49 -0
- letta/prompts/system/memgpt_chat.txt +58 -0
- letta/prompts/system/memgpt_chat_compressed.txt +13 -0
- letta/prompts/system/memgpt_chat_fstring.txt +51 -0
- letta/prompts/system/memgpt_doc.txt +50 -0
- letta/prompts/system/memgpt_gpt35_extralong.txt +53 -0
- letta/prompts/system/memgpt_intuitive_knowledge.txt +31 -0
- letta/prompts/system/memgpt_modified_chat.txt +23 -0
- letta/pytest.ini +0 -0
- letta/schemas/agent.py +117 -0
- letta/schemas/api_key.py +21 -0
- letta/schemas/block.py +135 -0
- letta/schemas/document.py +21 -0
- letta/schemas/embedding_config.py +54 -0
- letta/schemas/enums.py +35 -0
- letta/schemas/job.py +38 -0
- letta/schemas/letta_base.py +80 -0
- letta/schemas/letta_message.py +175 -0
- letta/schemas/letta_request.py +23 -0
- letta/schemas/letta_response.py +28 -0
- letta/schemas/llm_config.py +54 -0
- letta/schemas/memory.py +224 -0
- letta/schemas/message.py +727 -0
- letta/schemas/openai/chat_completion_request.py +123 -0
- letta/schemas/openai/chat_completion_response.py +136 -0
- letta/schemas/openai/chat_completions.py +123 -0
- letta/schemas/openai/embedding_response.py +11 -0
- letta/schemas/openai/openai.py +157 -0
- letta/schemas/organization.py +20 -0
- letta/schemas/passage.py +80 -0
- letta/schemas/source.py +62 -0
- letta/schemas/tool.py +143 -0
- letta/schemas/usage.py +18 -0
- letta/schemas/user.py +33 -0
- letta/server/__init__.py +0 -0
- letta/server/constants.py +6 -0
- letta/server/rest_api/__init__.py +0 -0
- letta/server/rest_api/admin/__init__.py +0 -0
- letta/server/rest_api/admin/agents.py +21 -0
- letta/server/rest_api/admin/tools.py +83 -0
- letta/server/rest_api/admin/users.py +98 -0
- letta/server/rest_api/app.py +193 -0
- letta/server/rest_api/auth/__init__.py +0 -0
- letta/server/rest_api/auth/index.py +43 -0
- letta/server/rest_api/auth_token.py +22 -0
- letta/server/rest_api/interface.py +726 -0
- letta/server/rest_api/routers/__init__.py +0 -0
- letta/server/rest_api/routers/openai/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
- letta/server/rest_api/routers/openai/assistants/assistants.py +115 -0
- letta/server/rest_api/routers/openai/assistants/schemas.py +121 -0
- letta/server/rest_api/routers/openai/assistants/threads.py +336 -0
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +131 -0
- letta/server/rest_api/routers/v1/__init__.py +15 -0
- letta/server/rest_api/routers/v1/agents.py +543 -0
- letta/server/rest_api/routers/v1/blocks.py +73 -0
- letta/server/rest_api/routers/v1/jobs.py +46 -0
- letta/server/rest_api/routers/v1/llms.py +28 -0
- letta/server/rest_api/routers/v1/organizations.py +61 -0
- letta/server/rest_api/routers/v1/sources.py +199 -0
- letta/server/rest_api/routers/v1/tools.py +103 -0
- letta/server/rest_api/routers/v1/users.py +109 -0
- letta/server/rest_api/static_files.py +74 -0
- letta/server/rest_api/utils.py +69 -0
- letta/server/server.py +1995 -0
- letta/server/startup.sh +8 -0
- letta/server/static_files/assets/index-0cbf7ad5.js +274 -0
- letta/server/static_files/assets/index-156816da.css +1 -0
- letta/server/static_files/assets/index-486e3228.js +274 -0
- letta/server/static_files/favicon.ico +0 -0
- letta/server/static_files/index.html +39 -0
- letta/server/static_files/memgpt_logo_transparent.png +0 -0
- letta/server/utils.py +46 -0
- letta/server/ws_api/__init__.py +0 -0
- letta/server/ws_api/example_client.py +104 -0
- letta/server/ws_api/interface.py +108 -0
- letta/server/ws_api/protocol.py +100 -0
- letta/server/ws_api/server.py +145 -0
- letta/settings.py +165 -0
- letta/streaming_interface.py +396 -0
- letta/system.py +207 -0
- letta/utils.py +1065 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/LICENSE +190 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/METADATA +98 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/RECORD +189 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/WHEEL +4 -0
- letta_nightly-0.1.7.dev20240924104148.dist-info/entry_points.txt +3 -0
|
Binary file
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Letta</title>
|
|
6
|
+
<base href="/" />
|
|
7
|
+
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
9
|
+
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
|
10
|
+
|
|
11
|
+
<script>
|
|
12
|
+
if (localStorage.theme === 'dark') {
|
|
13
|
+
if (document && document.documentElement) {
|
|
14
|
+
document.documentElement.classList.add('dark');
|
|
15
|
+
}
|
|
16
|
+
} else if (localStorage.theme === 'light') {
|
|
17
|
+
if (document && document.documentElement) {
|
|
18
|
+
document.documentElement.classList.remove('dark');
|
|
19
|
+
localStorage.setItem('theme', 'light');
|
|
20
|
+
}
|
|
21
|
+
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
22
|
+
localStorage.setItem('theme', 'system');
|
|
23
|
+
if (document && document.documentElement) {
|
|
24
|
+
document.documentElement.classList.add('dark');
|
|
25
|
+
}
|
|
26
|
+
} else {
|
|
27
|
+
if (document && document.documentElement) {
|
|
28
|
+
document.documentElement.classList.remove('dark');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
<script type="module" crossorigin src="/assets/index-486e3228.js"></script>
|
|
33
|
+
<link rel="stylesheet" href="/assets/index-156816da.css">
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<div class="h-full w-full" id="root"></div>
|
|
37
|
+
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
Binary file
|
letta/server/utils.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
def condition_to_stop_receiving(response):
|
|
2
|
+
"""Determines when to stop listening to the server"""
|
|
3
|
+
if response.get("type") in ["agent_response_end", "agent_response_error", "command_response", "server_error"]:
|
|
4
|
+
return True
|
|
5
|
+
else:
|
|
6
|
+
return False
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def print_server_response(response):
|
|
10
|
+
"""Turn response json into a nice print"""
|
|
11
|
+
if response["type"] == "agent_response_start":
|
|
12
|
+
print("[agent.step start]")
|
|
13
|
+
elif response["type"] == "agent_response_end":
|
|
14
|
+
print("[agent.step end]")
|
|
15
|
+
elif response["type"] == "agent_response":
|
|
16
|
+
msg = response["message"]
|
|
17
|
+
if response["message_type"] == "internal_monologue":
|
|
18
|
+
print(f"[inner thoughts] {msg}")
|
|
19
|
+
elif response["message_type"] == "assistant_message":
|
|
20
|
+
print(f"{msg}")
|
|
21
|
+
elif response["message_type"] == "function_message":
|
|
22
|
+
pass
|
|
23
|
+
else:
|
|
24
|
+
print(response)
|
|
25
|
+
else:
|
|
26
|
+
print(response)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def shorten_key_middle(key_string, chars_each_side=3):
|
|
30
|
+
"""
|
|
31
|
+
Shortens a key string by showing a specified number of characters on each side and adding an ellipsis in the middle.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
key_string (str): The key string to be shortened.
|
|
35
|
+
chars_each_side (int): The number of characters to show on each side of the ellipsis.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
str: The shortened key string with an ellipsis in the middle.
|
|
39
|
+
"""
|
|
40
|
+
if not key_string:
|
|
41
|
+
return key_string
|
|
42
|
+
key_length = len(key_string)
|
|
43
|
+
if key_length <= 2 * chars_each_side:
|
|
44
|
+
return "..." # Return ellipsis if the key is too short
|
|
45
|
+
else:
|
|
46
|
+
return key_string[:chars_each_side] + "..." + key_string[-chars_each_side:]
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import websockets
|
|
4
|
+
|
|
5
|
+
import letta.server.ws_api.protocol as protocol
|
|
6
|
+
from letta.server.constants import WS_CLIENT_TIMEOUT, WS_DEFAULT_PORT
|
|
7
|
+
from letta.server.utils import condition_to_stop_receiving, print_server_response
|
|
8
|
+
|
|
9
|
+
# CLEAN_RESPONSES = False # print the raw server responses (JSON)
|
|
10
|
+
CLEAN_RESPONSES = True # make the server responses cleaner
|
|
11
|
+
|
|
12
|
+
# LOAD_AGENT = None # create a brand new agent
|
|
13
|
+
AGENT_NAME = "agent_26" # load an existing agent
|
|
14
|
+
NEW_AGENT = False
|
|
15
|
+
|
|
16
|
+
RECONNECT_DELAY = 1
|
|
17
|
+
RECONNECT_MAX_TRIES = 5
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def send_message_and_print_replies(websocket, user_message, agent_id):
|
|
21
|
+
"""Send a message over websocket protocol and wait for the reply stream to end"""
|
|
22
|
+
# Send a message to the agent
|
|
23
|
+
await websocket.send(protocol.client_user_message(msg=str(user_message), agent_id=agent_id))
|
|
24
|
+
|
|
25
|
+
# Wait for messages in a loop, since the server may send a few
|
|
26
|
+
while True:
|
|
27
|
+
response = await asyncio.wait_for(websocket.recv(), WS_CLIENT_TIMEOUT)
|
|
28
|
+
response = json_loads(response)
|
|
29
|
+
|
|
30
|
+
if CLEAN_RESPONSES:
|
|
31
|
+
print_server_response(response)
|
|
32
|
+
else:
|
|
33
|
+
print(f"Server response:\n{json_dumps(response, indent=2)}")
|
|
34
|
+
|
|
35
|
+
# Check for a specific condition to break the loop
|
|
36
|
+
if condition_to_stop_receiving(response):
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def basic_cli_client():
|
|
41
|
+
"""Basic example of a Letta CLI client that connects to a Letta server.py process via WebSockets
|
|
42
|
+
|
|
43
|
+
Meant to illustrate how to use the server.py process, so limited in features (only supports sending user messages)
|
|
44
|
+
"""
|
|
45
|
+
uri = f"ws://localhost:{WS_DEFAULT_PORT}"
|
|
46
|
+
|
|
47
|
+
closed_on_message = False
|
|
48
|
+
retry_attempts = 0
|
|
49
|
+
while True: # Outer loop for reconnection attempts
|
|
50
|
+
try:
|
|
51
|
+
async with websockets.connect(uri) as websocket:
|
|
52
|
+
if NEW_AGENT:
|
|
53
|
+
# Initialize new agent
|
|
54
|
+
print("Sending config to server...")
|
|
55
|
+
example_config = {
|
|
56
|
+
"persona": "sam_pov",
|
|
57
|
+
"human": "cs_phd",
|
|
58
|
+
"model": "gpt-4-1106-preview", # gpt-4-turbo
|
|
59
|
+
}
|
|
60
|
+
await websocket.send(protocol.client_command_create(example_config))
|
|
61
|
+
# Wait for the response
|
|
62
|
+
response = await websocket.recv()
|
|
63
|
+
response = json_loads(response)
|
|
64
|
+
print(f"Server response:\n{json_dumps(response, indent=2)}")
|
|
65
|
+
|
|
66
|
+
await asyncio.sleep(1)
|
|
67
|
+
|
|
68
|
+
while True:
|
|
69
|
+
if closed_on_message:
|
|
70
|
+
# If we're on a retry after a disconnect, don't ask for input again
|
|
71
|
+
closed_on_message = False
|
|
72
|
+
else:
|
|
73
|
+
user_input = input("\nEnter your message: ")
|
|
74
|
+
print("\n")
|
|
75
|
+
|
|
76
|
+
# Send a message to the agent
|
|
77
|
+
try:
|
|
78
|
+
await send_message_and_print_replies(websocket=websocket, user_message=user_input, agent_id=AGENT_NAME)
|
|
79
|
+
retry_attempts = 0
|
|
80
|
+
except websockets.exceptions.ConnectionClosedError:
|
|
81
|
+
print("Connection to server was lost. Attempting to reconnect...")
|
|
82
|
+
closed_on_message = True
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
except websockets.exceptions.ConnectionClosedError:
|
|
86
|
+
# Decide whether or not to retry the connection
|
|
87
|
+
if retry_attempts < RECONNECT_MAX_TRIES:
|
|
88
|
+
retry_attempts += 1
|
|
89
|
+
await asyncio.sleep(RECONNECT_DELAY) # Wait for N seconds before reconnecting
|
|
90
|
+
continue
|
|
91
|
+
else:
|
|
92
|
+
print(f"Max attempts exceeded ({retry_attempts} > {RECONNECT_MAX_TRIES})")
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
except asyncio.TimeoutError:
|
|
96
|
+
print("Timeout waiting for the server response.")
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
print(f"An error occurred: {e}")
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
asyncio.run(basic_cli_client())
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import threading
|
|
3
|
+
|
|
4
|
+
import letta.server.ws_api.protocol as protocol
|
|
5
|
+
from letta.interface import AgentInterface
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseWebSocketInterface(AgentInterface):
|
|
9
|
+
"""Interface for interacting with a Letta agent over a WebSocket"""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
self.clients = set()
|
|
13
|
+
|
|
14
|
+
def register_client(self, websocket):
|
|
15
|
+
"""Register a new client connection"""
|
|
16
|
+
self.clients.add(websocket)
|
|
17
|
+
|
|
18
|
+
def unregister_client(self, websocket):
|
|
19
|
+
"""Unregister a client connection"""
|
|
20
|
+
self.clients.remove(websocket)
|
|
21
|
+
|
|
22
|
+
def step_yield(self):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AsyncWebSocketInterface(BaseWebSocketInterface):
|
|
27
|
+
"""WebSocket calls are async"""
|
|
28
|
+
|
|
29
|
+
async def user_message(self, msg):
|
|
30
|
+
"""Handle reception of a user message"""
|
|
31
|
+
# Logic to process the user message and possibly trigger agent's response
|
|
32
|
+
|
|
33
|
+
async def internal_monologue(self, msg):
|
|
34
|
+
"""Handle the agent's internal monologue"""
|
|
35
|
+
print(msg)
|
|
36
|
+
# Send the internal monologue to all clients
|
|
37
|
+
if self.clients: # Check if there are any clients connected
|
|
38
|
+
await asyncio.gather(*[client.send_text(protocol.server_agent_internal_monologue(msg)) for client in self.clients])
|
|
39
|
+
|
|
40
|
+
async def assistant_message(self, msg):
|
|
41
|
+
"""Handle the agent sending a message"""
|
|
42
|
+
print(msg)
|
|
43
|
+
# Send the assistant's message to all clients
|
|
44
|
+
if self.clients:
|
|
45
|
+
await asyncio.gather(*[client.send_text(protocol.server_agent_assistant_message(msg)) for client in self.clients])
|
|
46
|
+
|
|
47
|
+
async def function_message(self, msg):
|
|
48
|
+
"""Handle the agent calling a function"""
|
|
49
|
+
print(msg)
|
|
50
|
+
# Send the function call message to all clients
|
|
51
|
+
if self.clients:
|
|
52
|
+
await asyncio.gather(*[client.send_text(protocol.server_agent_function_message(msg)) for client in self.clients])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class SyncWebSocketInterface(BaseWebSocketInterface):
|
|
56
|
+
def __init__(self):
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.clients = set()
|
|
59
|
+
self.loop = asyncio.new_event_loop() # Create a new event loop
|
|
60
|
+
self.thread = threading.Thread(target=self._run_event_loop, daemon=True)
|
|
61
|
+
self.thread.start()
|
|
62
|
+
|
|
63
|
+
def _run_event_loop(self):
|
|
64
|
+
"""Run the dedicated event loop and handle its closure."""
|
|
65
|
+
asyncio.set_event_loop(self.loop)
|
|
66
|
+
try:
|
|
67
|
+
self.loop.run_forever()
|
|
68
|
+
finally:
|
|
69
|
+
# Run the cleanup tasks in the event loop
|
|
70
|
+
self.loop.run_until_complete(self.loop.shutdown_asyncgens())
|
|
71
|
+
self.loop.close()
|
|
72
|
+
|
|
73
|
+
def _run_async(self, coroutine):
|
|
74
|
+
"""Schedule coroutine to be run in the dedicated event loop."""
|
|
75
|
+
if not self.loop.is_closed():
|
|
76
|
+
asyncio.run_coroutine_threadsafe(coroutine, self.loop)
|
|
77
|
+
|
|
78
|
+
async def _send_to_all_clients(self, clients, msg):
|
|
79
|
+
"""Asynchronously sends a message to all clients."""
|
|
80
|
+
if clients:
|
|
81
|
+
await asyncio.gather(*(client.send_text(msg) for client in clients))
|
|
82
|
+
|
|
83
|
+
def user_message(self, msg):
|
|
84
|
+
"""Handle reception of a user message"""
|
|
85
|
+
# Logic to process the user message and possibly trigger agent's response
|
|
86
|
+
|
|
87
|
+
def internal_monologue(self, msg):
|
|
88
|
+
"""Handle the agent's internal monologue"""
|
|
89
|
+
print(msg)
|
|
90
|
+
if self.clients:
|
|
91
|
+
self._run_async(self._send_to_all_clients(self.clients, protocol.server_agent_internal_monologue(msg)))
|
|
92
|
+
|
|
93
|
+
def assistant_message(self, msg):
|
|
94
|
+
"""Handle the agent sending a message"""
|
|
95
|
+
print(msg)
|
|
96
|
+
if self.clients:
|
|
97
|
+
self._run_async(self._send_to_all_clients(self.clients, protocol.server_agent_assistant_message(msg)))
|
|
98
|
+
|
|
99
|
+
def function_message(self, msg):
|
|
100
|
+
"""Handle the agent calling a function"""
|
|
101
|
+
print(msg)
|
|
102
|
+
if self.clients:
|
|
103
|
+
self._run_async(self._send_to_all_clients(self.clients, protocol.server_agent_function_message(msg)))
|
|
104
|
+
|
|
105
|
+
def close(self):
|
|
106
|
+
"""Shut down the WebSocket interface and its event loop."""
|
|
107
|
+
self.loop.call_soon_threadsafe(self.loop.stop) # Signal the loop to stop
|
|
108
|
+
self.thread.join() # Wait for the thread to finish
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from letta.utils import json_dumps
|
|
2
|
+
|
|
3
|
+
# Server -> client
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def server_error(msg):
|
|
7
|
+
"""General server error"""
|
|
8
|
+
return json_dumps(
|
|
9
|
+
{
|
|
10
|
+
"type": "server_error",
|
|
11
|
+
"message": msg,
|
|
12
|
+
}
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def server_command_response(status):
|
|
17
|
+
return json_dumps(
|
|
18
|
+
{
|
|
19
|
+
"type": "command_response",
|
|
20
|
+
"status": status,
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def server_agent_response_error(msg):
|
|
26
|
+
return json_dumps(
|
|
27
|
+
{
|
|
28
|
+
"type": "agent_response_error",
|
|
29
|
+
"message": msg,
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def server_agent_response_start():
|
|
35
|
+
return json_dumps(
|
|
36
|
+
{
|
|
37
|
+
"type": "agent_response_start",
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def server_agent_response_end():
|
|
43
|
+
return json_dumps(
|
|
44
|
+
{
|
|
45
|
+
"type": "agent_response_end",
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def server_agent_internal_monologue(msg):
|
|
51
|
+
return json_dumps(
|
|
52
|
+
{
|
|
53
|
+
"type": "agent_response",
|
|
54
|
+
"message_type": "internal_monologue",
|
|
55
|
+
"message": msg,
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def server_agent_assistant_message(msg):
|
|
61
|
+
return json_dumps(
|
|
62
|
+
{
|
|
63
|
+
"type": "agent_response",
|
|
64
|
+
"message_type": "assistant_message",
|
|
65
|
+
"message": msg,
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def server_agent_function_message(msg):
|
|
71
|
+
return json_dumps(
|
|
72
|
+
{
|
|
73
|
+
"type": "agent_response",
|
|
74
|
+
"message_type": "function_message",
|
|
75
|
+
"message": msg,
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Client -> server
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def client_user_message(msg, agent_id=None):
|
|
84
|
+
return json_dumps(
|
|
85
|
+
{
|
|
86
|
+
"type": "user_message",
|
|
87
|
+
"message": msg,
|
|
88
|
+
"agent_id": agent_id,
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def client_command_create(config):
|
|
94
|
+
return json_dumps(
|
|
95
|
+
{
|
|
96
|
+
"type": "command",
|
|
97
|
+
"command": "create_agent",
|
|
98
|
+
"config": config,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import signal
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
import websockets
|
|
7
|
+
|
|
8
|
+
import letta.server.ws_api.protocol as protocol
|
|
9
|
+
from letta.server.constants import WS_DEFAULT_PORT
|
|
10
|
+
from letta.server.server import SyncServer
|
|
11
|
+
from letta.server.ws_api.interface import SyncWebSocketInterface
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WebSocketServer:
|
|
15
|
+
def __init__(self, host="localhost", port=WS_DEFAULT_PORT):
|
|
16
|
+
self.host = host
|
|
17
|
+
self.port = port
|
|
18
|
+
self.interface = SyncWebSocketInterface()
|
|
19
|
+
self.server = SyncServer(default_interface=self.interface)
|
|
20
|
+
|
|
21
|
+
def shutdown_server(self):
|
|
22
|
+
try:
|
|
23
|
+
self.server.save_agents()
|
|
24
|
+
print(f"Saved agents")
|
|
25
|
+
except Exception as e:
|
|
26
|
+
print(f"Saving agents failed with: {e}")
|
|
27
|
+
try:
|
|
28
|
+
self.interface.close()
|
|
29
|
+
print(f"Closed the WS interface")
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"Closing the WS interface failed with: {e}")
|
|
32
|
+
|
|
33
|
+
def initialize_server(self):
|
|
34
|
+
print("Server is initializing...")
|
|
35
|
+
print(f"Listening on {self.host}:{self.port}...")
|
|
36
|
+
|
|
37
|
+
async def start_server(self):
|
|
38
|
+
self.initialize_server()
|
|
39
|
+
# Can play with ping_interval and ping_timeout
|
|
40
|
+
# See: https://websockets.readthedocs.io/en/stable/topics/timeouts.html
|
|
41
|
+
# and https://github.com/cpacker/Letta/issues/471
|
|
42
|
+
async with websockets.serve(self.handle_client, self.host, self.port):
|
|
43
|
+
await asyncio.Future() # Run forever
|
|
44
|
+
|
|
45
|
+
def run(self):
|
|
46
|
+
return self.start_server() # Return the coroutine
|
|
47
|
+
|
|
48
|
+
async def handle_client(self, websocket, path):
|
|
49
|
+
self.interface.register_client(websocket)
|
|
50
|
+
try:
|
|
51
|
+
# async for message in websocket:
|
|
52
|
+
while True:
|
|
53
|
+
message = await websocket.recv()
|
|
54
|
+
|
|
55
|
+
# Assuming the message is a JSON string
|
|
56
|
+
try:
|
|
57
|
+
data = json_loads(message)
|
|
58
|
+
except:
|
|
59
|
+
print(f"[server] bad data from client:\n{data}")
|
|
60
|
+
await websocket.send(protocol.server_command_response(f"Error: bad data from client - {str(data)}"))
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
if "type" not in data:
|
|
64
|
+
print(f"[server] bad data from client (JSON but no type):\n{data}")
|
|
65
|
+
await websocket.send(protocol.server_command_response(f"Error: bad data from client - {str(data)}"))
|
|
66
|
+
|
|
67
|
+
elif data["type"] == "command":
|
|
68
|
+
# Create a new agent
|
|
69
|
+
if data["command"] == "create_agent":
|
|
70
|
+
try:
|
|
71
|
+
# self.agent = self.create_new_agent(data["config"])
|
|
72
|
+
self.server.create_agent(user_id="NULL", agent_config=data["config"])
|
|
73
|
+
await websocket.send(protocol.server_command_response("OK: Agent initialized"))
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.agent = None
|
|
76
|
+
print(f"[server] self.create_new_agent failed with:\n{e}")
|
|
77
|
+
print(f"{traceback.format_exc()}")
|
|
78
|
+
await websocket.send(protocol.server_command_response(f"Error: Failed to init agent - {str(e)}"))
|
|
79
|
+
|
|
80
|
+
else:
|
|
81
|
+
print(f"[server] unrecognized client command type: {data}")
|
|
82
|
+
await websocket.send(protocol.server_error(f"unrecognized client command type: {data}"))
|
|
83
|
+
|
|
84
|
+
elif data["type"] == "user_message":
|
|
85
|
+
user_message = data["message"]
|
|
86
|
+
|
|
87
|
+
if "agent_id" not in data or data["agent_id"] is None:
|
|
88
|
+
await websocket.send(protocol.server_agent_response_error("agent_name was not specified in the request"))
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
await websocket.send(protocol.server_agent_response_start())
|
|
92
|
+
try:
|
|
93
|
+
# self.run_step(user_message)
|
|
94
|
+
self.server.user_message(user_id="NULL", agent_id=data["agent_id"], message=user_message)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
print(f"[server] self.server.user_message failed with:\n{e}")
|
|
97
|
+
print(f"{traceback.format_exc()}")
|
|
98
|
+
await websocket.send(protocol.server_agent_response_error(f"server.user_message failed with: {e}"))
|
|
99
|
+
await asyncio.sleep(1) # pause before sending the terminating message, w/o this messages may be missed
|
|
100
|
+
await websocket.send(protocol.server_agent_response_end())
|
|
101
|
+
|
|
102
|
+
# ... handle other message types as needed ...
|
|
103
|
+
else:
|
|
104
|
+
print(f"[server] unrecognized client package data type: {data}")
|
|
105
|
+
await websocket.send(protocol.server_error(f"unrecognized client package data type: {data}"))
|
|
106
|
+
|
|
107
|
+
except websockets.exceptions.ConnectionClosed:
|
|
108
|
+
print(f"[server] connection with client was closed")
|
|
109
|
+
finally:
|
|
110
|
+
self.interface.unregister_client(websocket)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def start_server():
|
|
114
|
+
# Check if a port argument is provided
|
|
115
|
+
port = WS_DEFAULT_PORT
|
|
116
|
+
if len(sys.argv) > 1:
|
|
117
|
+
try:
|
|
118
|
+
port = int(sys.argv[1])
|
|
119
|
+
except ValueError:
|
|
120
|
+
print(f"Invalid port number. Using default port {port}.")
|
|
121
|
+
|
|
122
|
+
server = WebSocketServer(port=port)
|
|
123
|
+
|
|
124
|
+
def handle_sigterm(*args):
|
|
125
|
+
# Perform necessary cleanup
|
|
126
|
+
print("SIGTERM received, shutting down...")
|
|
127
|
+
# Note: This should be quick and not involve asynchronous calls
|
|
128
|
+
print("Shutting down the server...")
|
|
129
|
+
server.shutdown_server()
|
|
130
|
+
print("Server has been shut down.")
|
|
131
|
+
sys.exit(0)
|
|
132
|
+
|
|
133
|
+
signal.signal(signal.SIGTERM, handle_sigterm)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
asyncio.run(server.run())
|
|
137
|
+
except KeyboardInterrupt:
|
|
138
|
+
print("Shutting down the server...")
|
|
139
|
+
finally:
|
|
140
|
+
server.shutdown_server()
|
|
141
|
+
print("Server has been shut down.")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
if __name__ == "__main__":
|
|
145
|
+
start_server()
|