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.

Files changed (189) hide show
  1. letta/__init__.py +24 -0
  2. letta/__main__.py +3 -0
  3. letta/agent.py +1427 -0
  4. letta/agent_store/chroma.py +295 -0
  5. letta/agent_store/db.py +546 -0
  6. letta/agent_store/lancedb.py +177 -0
  7. letta/agent_store/milvus.py +198 -0
  8. letta/agent_store/qdrant.py +201 -0
  9. letta/agent_store/storage.py +188 -0
  10. letta/benchmark/benchmark.py +96 -0
  11. letta/benchmark/constants.py +14 -0
  12. letta/cli/cli.py +689 -0
  13. letta/cli/cli_config.py +1282 -0
  14. letta/cli/cli_load.py +166 -0
  15. letta/client/__init__.py +0 -0
  16. letta/client/admin.py +171 -0
  17. letta/client/client.py +2360 -0
  18. letta/client/streaming.py +90 -0
  19. letta/client/utils.py +61 -0
  20. letta/config.py +484 -0
  21. letta/configs/anthropic.json +13 -0
  22. letta/configs/letta_hosted.json +11 -0
  23. letta/configs/openai.json +12 -0
  24. letta/constants.py +134 -0
  25. letta/credentials.py +140 -0
  26. letta/data_sources/connectors.py +247 -0
  27. letta/embeddings.py +218 -0
  28. letta/errors.py +26 -0
  29. letta/functions/__init__.py +0 -0
  30. letta/functions/function_sets/base.py +174 -0
  31. letta/functions/function_sets/extras.py +132 -0
  32. letta/functions/functions.py +105 -0
  33. letta/functions/schema_generator.py +205 -0
  34. letta/humans/__init__.py +0 -0
  35. letta/humans/examples/basic.txt +1 -0
  36. letta/humans/examples/cs_phd.txt +9 -0
  37. letta/interface.py +314 -0
  38. letta/llm_api/__init__.py +0 -0
  39. letta/llm_api/anthropic.py +383 -0
  40. letta/llm_api/azure_openai.py +155 -0
  41. letta/llm_api/cohere.py +396 -0
  42. letta/llm_api/google_ai.py +468 -0
  43. letta/llm_api/llm_api_tools.py +485 -0
  44. letta/llm_api/openai.py +470 -0
  45. letta/local_llm/README.md +3 -0
  46. letta/local_llm/__init__.py +0 -0
  47. letta/local_llm/chat_completion_proxy.py +279 -0
  48. letta/local_llm/constants.py +31 -0
  49. letta/local_llm/function_parser.py +68 -0
  50. letta/local_llm/grammars/__init__.py +0 -0
  51. letta/local_llm/grammars/gbnf_grammar_generator.py +1324 -0
  52. letta/local_llm/grammars/json.gbnf +26 -0
  53. letta/local_llm/grammars/json_func_calls_with_inner_thoughts.gbnf +32 -0
  54. letta/local_llm/groq/api.py +97 -0
  55. letta/local_llm/json_parser.py +202 -0
  56. letta/local_llm/koboldcpp/api.py +62 -0
  57. letta/local_llm/koboldcpp/settings.py +23 -0
  58. letta/local_llm/llamacpp/api.py +58 -0
  59. letta/local_llm/llamacpp/settings.py +22 -0
  60. letta/local_llm/llm_chat_completion_wrappers/__init__.py +0 -0
  61. letta/local_llm/llm_chat_completion_wrappers/airoboros.py +452 -0
  62. letta/local_llm/llm_chat_completion_wrappers/chatml.py +470 -0
  63. letta/local_llm/llm_chat_completion_wrappers/configurable_wrapper.py +387 -0
  64. letta/local_llm/llm_chat_completion_wrappers/dolphin.py +246 -0
  65. letta/local_llm/llm_chat_completion_wrappers/llama3.py +345 -0
  66. letta/local_llm/llm_chat_completion_wrappers/simple_summary_wrapper.py +156 -0
  67. letta/local_llm/llm_chat_completion_wrappers/wrapper_base.py +11 -0
  68. letta/local_llm/llm_chat_completion_wrappers/zephyr.py +345 -0
  69. letta/local_llm/lmstudio/api.py +100 -0
  70. letta/local_llm/lmstudio/settings.py +29 -0
  71. letta/local_llm/ollama/api.py +88 -0
  72. letta/local_llm/ollama/settings.py +32 -0
  73. letta/local_llm/settings/__init__.py +0 -0
  74. letta/local_llm/settings/deterministic_mirostat.py +45 -0
  75. letta/local_llm/settings/settings.py +72 -0
  76. letta/local_llm/settings/simple.py +28 -0
  77. letta/local_llm/utils.py +265 -0
  78. letta/local_llm/vllm/api.py +63 -0
  79. letta/local_llm/webui/api.py +60 -0
  80. letta/local_llm/webui/legacy_api.py +58 -0
  81. letta/local_llm/webui/legacy_settings.py +23 -0
  82. letta/local_llm/webui/settings.py +24 -0
  83. letta/log.py +76 -0
  84. letta/main.py +437 -0
  85. letta/memory.py +440 -0
  86. letta/metadata.py +884 -0
  87. letta/openai_backcompat/__init__.py +0 -0
  88. letta/openai_backcompat/openai_object.py +437 -0
  89. letta/persistence_manager.py +148 -0
  90. letta/personas/__init__.py +0 -0
  91. letta/personas/examples/anna_pa.txt +13 -0
  92. letta/personas/examples/google_search_persona.txt +15 -0
  93. letta/personas/examples/memgpt_doc.txt +6 -0
  94. letta/personas/examples/memgpt_starter.txt +4 -0
  95. letta/personas/examples/sam.txt +14 -0
  96. letta/personas/examples/sam_pov.txt +14 -0
  97. letta/personas/examples/sam_simple_pov_gpt35.txt +13 -0
  98. letta/personas/examples/sqldb/test.db +0 -0
  99. letta/prompts/__init__.py +0 -0
  100. letta/prompts/gpt_summarize.py +14 -0
  101. letta/prompts/gpt_system.py +26 -0
  102. letta/prompts/system/memgpt_base.txt +49 -0
  103. letta/prompts/system/memgpt_chat.txt +58 -0
  104. letta/prompts/system/memgpt_chat_compressed.txt +13 -0
  105. letta/prompts/system/memgpt_chat_fstring.txt +51 -0
  106. letta/prompts/system/memgpt_doc.txt +50 -0
  107. letta/prompts/system/memgpt_gpt35_extralong.txt +53 -0
  108. letta/prompts/system/memgpt_intuitive_knowledge.txt +31 -0
  109. letta/prompts/system/memgpt_modified_chat.txt +23 -0
  110. letta/pytest.ini +0 -0
  111. letta/schemas/agent.py +117 -0
  112. letta/schemas/api_key.py +21 -0
  113. letta/schemas/block.py +135 -0
  114. letta/schemas/document.py +21 -0
  115. letta/schemas/embedding_config.py +54 -0
  116. letta/schemas/enums.py +35 -0
  117. letta/schemas/job.py +38 -0
  118. letta/schemas/letta_base.py +80 -0
  119. letta/schemas/letta_message.py +175 -0
  120. letta/schemas/letta_request.py +23 -0
  121. letta/schemas/letta_response.py +28 -0
  122. letta/schemas/llm_config.py +54 -0
  123. letta/schemas/memory.py +224 -0
  124. letta/schemas/message.py +727 -0
  125. letta/schemas/openai/chat_completion_request.py +123 -0
  126. letta/schemas/openai/chat_completion_response.py +136 -0
  127. letta/schemas/openai/chat_completions.py +123 -0
  128. letta/schemas/openai/embedding_response.py +11 -0
  129. letta/schemas/openai/openai.py +157 -0
  130. letta/schemas/organization.py +20 -0
  131. letta/schemas/passage.py +80 -0
  132. letta/schemas/source.py +62 -0
  133. letta/schemas/tool.py +143 -0
  134. letta/schemas/usage.py +18 -0
  135. letta/schemas/user.py +33 -0
  136. letta/server/__init__.py +0 -0
  137. letta/server/constants.py +6 -0
  138. letta/server/rest_api/__init__.py +0 -0
  139. letta/server/rest_api/admin/__init__.py +0 -0
  140. letta/server/rest_api/admin/agents.py +21 -0
  141. letta/server/rest_api/admin/tools.py +83 -0
  142. letta/server/rest_api/admin/users.py +98 -0
  143. letta/server/rest_api/app.py +193 -0
  144. letta/server/rest_api/auth/__init__.py +0 -0
  145. letta/server/rest_api/auth/index.py +43 -0
  146. letta/server/rest_api/auth_token.py +22 -0
  147. letta/server/rest_api/interface.py +726 -0
  148. letta/server/rest_api/routers/__init__.py +0 -0
  149. letta/server/rest_api/routers/openai/__init__.py +0 -0
  150. letta/server/rest_api/routers/openai/assistants/__init__.py +0 -0
  151. letta/server/rest_api/routers/openai/assistants/assistants.py +115 -0
  152. letta/server/rest_api/routers/openai/assistants/schemas.py +121 -0
  153. letta/server/rest_api/routers/openai/assistants/threads.py +336 -0
  154. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  155. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +131 -0
  156. letta/server/rest_api/routers/v1/__init__.py +15 -0
  157. letta/server/rest_api/routers/v1/agents.py +543 -0
  158. letta/server/rest_api/routers/v1/blocks.py +73 -0
  159. letta/server/rest_api/routers/v1/jobs.py +46 -0
  160. letta/server/rest_api/routers/v1/llms.py +28 -0
  161. letta/server/rest_api/routers/v1/organizations.py +61 -0
  162. letta/server/rest_api/routers/v1/sources.py +199 -0
  163. letta/server/rest_api/routers/v1/tools.py +103 -0
  164. letta/server/rest_api/routers/v1/users.py +109 -0
  165. letta/server/rest_api/static_files.py +74 -0
  166. letta/server/rest_api/utils.py +69 -0
  167. letta/server/server.py +1995 -0
  168. letta/server/startup.sh +8 -0
  169. letta/server/static_files/assets/index-0cbf7ad5.js +274 -0
  170. letta/server/static_files/assets/index-156816da.css +1 -0
  171. letta/server/static_files/assets/index-486e3228.js +274 -0
  172. letta/server/static_files/favicon.ico +0 -0
  173. letta/server/static_files/index.html +39 -0
  174. letta/server/static_files/memgpt_logo_transparent.png +0 -0
  175. letta/server/utils.py +46 -0
  176. letta/server/ws_api/__init__.py +0 -0
  177. letta/server/ws_api/example_client.py +104 -0
  178. letta/server/ws_api/interface.py +108 -0
  179. letta/server/ws_api/protocol.py +100 -0
  180. letta/server/ws_api/server.py +145 -0
  181. letta/settings.py +165 -0
  182. letta/streaming_interface.py +396 -0
  183. letta/system.py +207 -0
  184. letta/utils.py +1065 -0
  185. letta_nightly-0.1.7.dev20240924104148.dist-info/LICENSE +190 -0
  186. letta_nightly-0.1.7.dev20240924104148.dist-info/METADATA +98 -0
  187. letta_nightly-0.1.7.dev20240924104148.dist-info/RECORD +189 -0
  188. letta_nightly-0.1.7.dev20240924104148.dist-info/WHEEL +4 -0
  189. 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>
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()