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
letta/settings.py ADDED
@@ -0,0 +1,165 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+ import os
4
+
5
+ from pydantic import Field
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+ from letta.schemas.embedding_config import EmbeddingConfig
9
+ from letta.schemas.llm_config import LLMConfig
10
+ from letta.utils import printd
11
+
12
+
13
+ class Settings(BaseSettings):
14
+ model_config = SettingsConfigDict(env_prefix="letta_")
15
+
16
+ letta_dir: Optional[Path] = Field(Path.home() / ".letta", env="LETTA_DIR")
17
+ debug: Optional[bool] = False
18
+ cors_origins: Optional[list] = ["http://letta.localhost", "http://localhost:8283", "http://localhost:8083"]
19
+
20
+ # database configuration
21
+ pg_db: Optional[str] = None
22
+ pg_user: Optional[str] = None
23
+ pg_password: Optional[str] = None
24
+ pg_host: Optional[str] = None
25
+ pg_port: Optional[int] = None
26
+ pg_uri: Optional[str] = None # option to specifiy full uri
27
+
28
+ # llm configuration
29
+ llm_endpoint: Optional[str] = None
30
+ llm_endpoint_type: Optional[str] = None
31
+ llm_model: Optional[str] = None
32
+ llm_context_window: Optional[int] = None
33
+
34
+ # embedding configuration
35
+ embedding_endpoint: Optional[str] = None
36
+ embedding_endpoint_type: Optional[str] = None
37
+ embedding_dim: Optional[int] = None
38
+ embedding_model: Optional[str] = None
39
+ embedding_chunk_size: int = 300
40
+
41
+ @property
42
+ def llm_config(self):
43
+
44
+ # try to get LLM config from settings
45
+ if self.llm_endpoint and self.llm_endpoint_type and self.llm_model and self.llm_context_window:
46
+ return LLMConfig(
47
+ model=self.llm_model,
48
+ model_endpoint_type=self.llm_endpoint_type,
49
+ model_endpoint=self.llm_endpoint,
50
+ model_wrapper=None,
51
+ context_window=self.llm_context_window,
52
+ )
53
+ else:
54
+ if not self.llm_endpoint:
55
+ printd(f"No LETTA_LLM_ENDPOINT provided")
56
+ if not self.llm_endpoint_type:
57
+ printd(f"No LETTA_LLM_ENDPOINT_TYPE provided")
58
+ if not self.llm_model:
59
+ printd(f"No LETTA_LLM_MODEL provided")
60
+ if not self.llm_context_window:
61
+ printd(f"No LETTA_LLM_CONTEX_WINDOW provided")
62
+
63
+ # quickstart options
64
+ if self.llm_model:
65
+ try:
66
+ return LLMConfig.default_config(self.llm_model)
67
+ except ValueError as e:
68
+ pass
69
+
70
+ # try to read from config file (last resort)
71
+ from letta.config import LettaConfig
72
+ if LettaConfig.exists():
73
+ config = LettaConfig.load()
74
+ llm_config = LLMConfig(
75
+ model=config.default_llm_config.model,
76
+ model_endpoint_type=config.default_llm_config.model_endpoint_type,
77
+ model_endpoint=config.default_llm_config.model_endpoint,
78
+ model_wrapper=config.default_llm_config.model_wrapper,
79
+ context_window=config.default_llm_config.context_window,
80
+ )
81
+ return llm_config
82
+
83
+ # check OpenAI API key
84
+ if os.getenv("OPENAI_API_KEY"):
85
+ return LLMConfig.default_config(self.llm_model if self.llm_model else "gpt-4")
86
+
87
+ return LLMConfig.default_config("letta")
88
+
89
+ @property
90
+ def embedding_config(self):
91
+
92
+ # try to get LLM config from settings
93
+ if self.embedding_endpoint and self.embedding_endpoint_type and self.embedding_model and self.embedding_dim:
94
+ return EmbeddingConfig(
95
+ embedding_model=self.embedding_model,
96
+ embedding_endpoint_type=self.embedding_endpoint_type,
97
+ embedding_endpoint=self.embedding_endpoint,
98
+ embedding_dim=self.embedding_dim,
99
+ embedding_chunk_size=self.embedding_chunk_size,
100
+ )
101
+ else:
102
+ if not self.embedding_endpoint:
103
+ printd(f"No LETTA_EMBEDDING_ENDPOINT provided")
104
+ if not self.embedding_endpoint_type:
105
+ printd(f"No LETTA_EMBEDDING_ENDPOINT_TYPE provided")
106
+ if not self.embedding_model:
107
+ printd(f"No LETTA_EMBEDDING_MODEL provided")
108
+ if not self.embedding_dim:
109
+ printd(f"No LETTA_EMBEDDING_DIM provided")
110
+
111
+ # TODO
112
+ ## quickstart options
113
+ # if self.embedding_model:
114
+ # try:
115
+ # return EmbeddingConfig.default_config(self.embedding_model)
116
+ # except ValueError as e:
117
+ # pass
118
+
119
+ # try to read from config file (last resort)
120
+ from letta.config import LettaConfig
121
+ if LettaConfig.exists():
122
+ config = LettaConfig.load()
123
+ return EmbeddingConfig(
124
+ embedding_model=config.default_embedding_config.embedding_model,
125
+ embedding_endpoint_type=config.default_embedding_config.embedding_endpoint_type,
126
+ embedding_endpoint=config.default_embedding_config.embedding_endpoint,
127
+ embedding_dim=config.default_embedding_config.embedding_dim,
128
+ embedding_chunk_size=config.default_embedding_config.embedding_chunk_size,
129
+ )
130
+
131
+ if os.getenv("OPENAI_API_KEY"):
132
+ return EmbeddingConfig.default_config(self.embedding_model if self.embedding_model else "text-embedding-ada-002")
133
+
134
+ return EmbeddingConfig.default_config("letta")
135
+
136
+ @property
137
+ def letta_pg_uri(self) -> str:
138
+ if self.pg_uri:
139
+ return self.pg_uri
140
+ elif self.pg_db and self.pg_user and self.pg_password and self.pg_host and self.pg_port:
141
+ return f"postgresql+pg8000://{self.pg_user}:{self.pg_password}@{self.pg_host}:{self.pg_port}/{self.pg_db}"
142
+ else:
143
+ return f"postgresql+pg8000://letta:letta@localhost:5432/letta"
144
+
145
+ # add this property to avoid being returned the default
146
+ # reference: https://github.com/cpacker/Letta/issues/1362
147
+ @property
148
+ def letta_pg_uri_no_default(self) -> str:
149
+ if self.pg_uri:
150
+ return self.pg_uri
151
+ elif self.pg_db and self.pg_user and self.pg_password and self.pg_host and self.pg_port:
152
+ return f"postgresql+pg8000://{self.pg_user}:{self.pg_password}@{self.pg_host}:{self.pg_port}/{self.pg_db}"
153
+ else:
154
+ return None
155
+
156
+
157
+ class TestSettings(Settings):
158
+ model_config = SettingsConfigDict(env_prefix="letta_test_")
159
+
160
+ letta_dir: Optional[Path] = Field(Path.home() / ".letta/test", env="LETTA_TEST_DIR")
161
+
162
+
163
+ # singleton
164
+ settings = Settings(_env_parse_none_str='None')
165
+ test_settings = TestSettings()
@@ -0,0 +1,396 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from datetime import datetime
4
+ from typing import List, Optional
5
+
6
+ # from colorama import Fore, Style, init
7
+ from rich.console import Console
8
+ from rich.live import Live
9
+ from rich.markup import escape
10
+
11
+ from letta.interface import CLIInterface
12
+ from letta.schemas.message import Message
13
+ from letta.schemas.openai.chat_completion_response import (
14
+ ChatCompletionChunkResponse,
15
+ ChatCompletionResponse,
16
+ )
17
+
18
+ # init(autoreset=True)
19
+
20
+ # DEBUG = True # puts full message outputs in the terminal
21
+ DEBUG = False # only dumps important messages in the terminal
22
+
23
+ STRIP_UI = False
24
+
25
+
26
+ class AgentChunkStreamingInterface(ABC):
27
+ """Interfaces handle Letta-related events (observer pattern)
28
+
29
+ The 'msg' args provides the scoped message, and the optional Message arg can provide additional metadata.
30
+ """
31
+
32
+ @abstractmethod
33
+ def user_message(self, msg: str, msg_obj: Optional[Message] = None):
34
+ """Letta receives a user message"""
35
+ raise NotImplementedError
36
+
37
+ @abstractmethod
38
+ def internal_monologue(self, msg: str, msg_obj: Optional[Message] = None):
39
+ """Letta generates some internal monologue"""
40
+ raise NotImplementedError
41
+
42
+ @abstractmethod
43
+ def assistant_message(self, msg: str, msg_obj: Optional[Message] = None):
44
+ """Letta uses send_message"""
45
+ raise NotImplementedError
46
+
47
+ @abstractmethod
48
+ def function_message(self, msg: str, msg_obj: Optional[Message] = None):
49
+ """Letta calls a function"""
50
+ raise NotImplementedError
51
+
52
+ @abstractmethod
53
+ def process_chunk(self, chunk: ChatCompletionChunkResponse, message_id: str, message_date: datetime):
54
+ """Process a streaming chunk from an OpenAI-compatible server"""
55
+ raise NotImplementedError
56
+
57
+ @abstractmethod
58
+ def stream_start(self):
59
+ """Any setup required before streaming begins"""
60
+ raise NotImplementedError
61
+
62
+ @abstractmethod
63
+ def stream_end(self):
64
+ """Any cleanup required after streaming ends"""
65
+ raise NotImplementedError
66
+
67
+
68
+ class StreamingCLIInterface(AgentChunkStreamingInterface):
69
+ """Version of the CLI interface that attaches to a stream generator and prints along the way.
70
+
71
+ When a chunk is received, we write the delta to the buffer. If the buffer type has changed,
72
+ we write out a newline + set the formatting for the new line.
73
+
74
+ The two buffer types are:
75
+ (1) content (inner thoughts)
76
+ (2) tool_calls (function calling)
77
+
78
+ NOTE: this assumes that the deltas received in the chunks are in-order, e.g.
79
+ that once 'content' deltas stop streaming, they won't be received again. See notes
80
+ on alternative version of the StreamingCLIInterface that does not have this same problem below:
81
+
82
+ An alternative implementation could instead maintain the partial message state, and on each
83
+ process chunk (1) update the partial message state, (2) refresh/rewrite the state to the screen.
84
+ """
85
+
86
+ # CLIInterface is static/stateless
87
+ nonstreaming_interface = CLIInterface()
88
+
89
+ def __init__(self):
90
+ """The streaming CLI interface state for determining which buffer is currently being written to"""
91
+
92
+ self.streaming_buffer_type = None
93
+
94
+ def _flush(self):
95
+ pass
96
+
97
+ def process_chunk(self, chunk: ChatCompletionChunkResponse, message_id: str, message_date: datetime):
98
+ assert len(chunk.choices) == 1, chunk
99
+
100
+ message_delta = chunk.choices[0].delta
101
+
102
+ # Starting a new buffer line
103
+ if not self.streaming_buffer_type:
104
+ assert not (
105
+ message_delta.content is not None and message_delta.tool_calls is not None and len(message_delta.tool_calls)
106
+ ), f"Error: got both content and tool_calls in message stream\n{message_delta}"
107
+
108
+ if message_delta.content is not None:
109
+ # Write out the prefix for inner thoughts
110
+ print("Inner thoughts: ", end="", flush=True)
111
+ elif message_delta.tool_calls is not None:
112
+ assert len(message_delta.tool_calls) == 1, f"Error: got more than one tool call in response\n{message_delta}"
113
+ # Write out the prefix for function calling
114
+ print("Calling function: ", end="", flush=True)
115
+
116
+ # Potentially switch/flush a buffer line
117
+ else:
118
+ pass
119
+
120
+ # Write out the delta
121
+ if message_delta.content is not None:
122
+ if self.streaming_buffer_type and self.streaming_buffer_type != "content":
123
+ print()
124
+ self.streaming_buffer_type = "content"
125
+
126
+ # Simple, just write out to the buffer
127
+ print(message_delta.content, end="", flush=True)
128
+
129
+ elif message_delta.tool_calls is not None:
130
+ if self.streaming_buffer_type and self.streaming_buffer_type != "tool_calls":
131
+ print()
132
+ self.streaming_buffer_type = "tool_calls"
133
+
134
+ assert len(message_delta.tool_calls) == 1, f"Error: got more than one tool call in response\n{message_delta}"
135
+ function_call = message_delta.tool_calls[0].function
136
+
137
+ # Slightly more complex - want to write parameters in a certain way (paren-style)
138
+ # function_name(function_args)
139
+ if function_call and function_call.name:
140
+ # NOTE: need to account for closing the brace later
141
+ print(f"{function_call.name}(", end="", flush=True)
142
+ if function_call and function_call.arguments:
143
+ print(function_call.arguments, end="", flush=True)
144
+
145
+ def stream_start(self):
146
+ # should be handled by stream_end(), but just in case
147
+ self.streaming_buffer_type = None
148
+
149
+ def stream_end(self):
150
+ if self.streaming_buffer_type is not None:
151
+ # TODO: should have a separate self.tool_call_open_paren flag
152
+ if self.streaming_buffer_type == "tool_calls":
153
+ print(")", end="", flush=True)
154
+
155
+ print() # newline to move the cursor
156
+ self.streaming_buffer_type = None # reset buffer tracker
157
+
158
+ @staticmethod
159
+ def important_message(msg: str):
160
+ StreamingCLIInterface.nonstreaming_interface(msg)
161
+
162
+ @staticmethod
163
+ def warning_message(msg: str):
164
+ StreamingCLIInterface.nonstreaming_interface(msg)
165
+
166
+ @staticmethod
167
+ def internal_monologue(msg: str, msg_obj: Optional[Message] = None):
168
+ StreamingCLIInterface.nonstreaming_interface(msg, msg_obj)
169
+
170
+ @staticmethod
171
+ def assistant_message(msg: str, msg_obj: Optional[Message] = None):
172
+ StreamingCLIInterface.nonstreaming_interface(msg, msg_obj)
173
+
174
+ @staticmethod
175
+ def memory_message(msg: str, msg_obj: Optional[Message] = None):
176
+ StreamingCLIInterface.nonstreaming_interface(msg, msg_obj)
177
+
178
+ @staticmethod
179
+ def system_message(msg: str, msg_obj: Optional[Message] = None):
180
+ StreamingCLIInterface.nonstreaming_interface(msg, msg_obj)
181
+
182
+ @staticmethod
183
+ def user_message(msg: str, msg_obj: Optional[Message] = None, raw: bool = False, dump: bool = False, debug: bool = DEBUG):
184
+ StreamingCLIInterface.nonstreaming_interface(msg, msg_obj)
185
+
186
+ @staticmethod
187
+ def function_message(msg: str, msg_obj: Optional[Message] = None, debug: bool = DEBUG):
188
+ StreamingCLIInterface.nonstreaming_interface(msg, msg_obj)
189
+
190
+ @staticmethod
191
+ def print_messages(message_sequence: List[Message], dump=False):
192
+ StreamingCLIInterface.nonstreaming_interface(message_sequence, dump)
193
+
194
+ @staticmethod
195
+ def print_messages_simple(message_sequence: List[Message]):
196
+ StreamingCLIInterface.nonstreaming_interface.print_messages_simple(message_sequence)
197
+
198
+ @staticmethod
199
+ def print_messages_raw(message_sequence: List[Message]):
200
+ StreamingCLIInterface.nonstreaming_interface.print_messages_raw(message_sequence)
201
+
202
+ @staticmethod
203
+ def step_yield():
204
+ pass
205
+
206
+
207
+ class AgentRefreshStreamingInterface(ABC):
208
+ """Same as the ChunkStreamingInterface, but
209
+
210
+ The 'msg' args provides the scoped message, and the optional Message arg can provide additional metadata.
211
+ """
212
+
213
+ @abstractmethod
214
+ def user_message(self, msg: str, msg_obj: Optional[Message] = None):
215
+ """Letta receives a user message"""
216
+ raise NotImplementedError
217
+
218
+ @abstractmethod
219
+ def internal_monologue(self, msg: str, msg_obj: Optional[Message] = None):
220
+ """Letta generates some internal monologue"""
221
+ raise NotImplementedError
222
+
223
+ @abstractmethod
224
+ def assistant_message(self, msg: str, msg_obj: Optional[Message] = None):
225
+ """Letta uses send_message"""
226
+ raise NotImplementedError
227
+
228
+ @abstractmethod
229
+ def function_message(self, msg: str, msg_obj: Optional[Message] = None):
230
+ """Letta calls a function"""
231
+ raise NotImplementedError
232
+
233
+ @abstractmethod
234
+ def process_refresh(self, response: ChatCompletionResponse):
235
+ """Process a streaming chunk from an OpenAI-compatible server"""
236
+ raise NotImplementedError
237
+
238
+ @abstractmethod
239
+ def stream_start(self):
240
+ """Any setup required before streaming begins"""
241
+ raise NotImplementedError
242
+
243
+ @abstractmethod
244
+ def stream_end(self):
245
+ """Any cleanup required after streaming ends"""
246
+ raise NotImplementedError
247
+
248
+ @abstractmethod
249
+ def toggle_streaming(self, on: bool):
250
+ """Toggle streaming on/off (off = regular CLI interface)"""
251
+ raise NotImplementedError
252
+
253
+
254
+ class StreamingRefreshCLIInterface(AgentRefreshStreamingInterface):
255
+ """Version of the CLI interface that attaches to a stream generator and refreshes a render of the message at every step.
256
+
257
+ We maintain the partial message state in the interface state, and on each
258
+ process chunk we:
259
+ (1) update the partial message state,
260
+ (2) refresh/rewrite the state to the screen.
261
+ """
262
+
263
+ nonstreaming_interface = CLIInterface
264
+
265
+ def __init__(self, fancy: bool = True, separate_send_message: bool = True, disable_inner_mono_call: bool = True):
266
+ """Initialize the streaming CLI interface state."""
267
+ self.console = Console()
268
+
269
+ # Using `Live` with `refresh_per_second` parameter to limit the refresh rate, avoiding excessive updates
270
+ self.live = Live("", console=self.console, refresh_per_second=10)
271
+ # self.live.start() # Start the Live display context and keep it running
272
+
273
+ # Use italics / emoji?
274
+ self.fancy = fancy
275
+
276
+ self.streaming = True
277
+ self.separate_send_message = separate_send_message
278
+ self.disable_inner_mono_call = disable_inner_mono_call
279
+
280
+ def toggle_streaming(self, on: bool):
281
+ self.streaming = on
282
+ if on:
283
+ self.separate_send_message = True
284
+ self.disable_inner_mono_call = True
285
+ else:
286
+ self.separate_send_message = False
287
+ self.disable_inner_mono_call = False
288
+
289
+ def update_output(self, content: str):
290
+ """Update the displayed output with new content."""
291
+ # We use the `Live` object's update mechanism to refresh content without clearing the console
292
+ if not self.fancy:
293
+ content = escape(content)
294
+ self.live.update(self.console.render_str(content), refresh=True)
295
+
296
+ def process_refresh(self, response: ChatCompletionResponse):
297
+ """Process the response to rewrite the current output buffer."""
298
+ if not response.choices:
299
+ self.update_output("💭 [italic]...[/italic]")
300
+ return # Early exit if there are no choices
301
+
302
+ choice = response.choices[0]
303
+ inner_thoughts = choice.message.content if choice.message.content else ""
304
+ tool_calls = choice.message.tool_calls if choice.message.tool_calls else []
305
+
306
+ if self.fancy:
307
+ message_string = f"💭 [italic]{inner_thoughts}[/italic]" if inner_thoughts else ""
308
+ else:
309
+ message_string = "[inner thoughts] " + inner_thoughts if inner_thoughts else ""
310
+
311
+ if tool_calls:
312
+ function_call = tool_calls[0].function
313
+ function_name = function_call.name # Function name, can be an empty string
314
+ function_args = function_call.arguments # Function arguments, can be an empty string
315
+ if message_string:
316
+ message_string += "\n"
317
+ # special case here for send_message
318
+ if self.separate_send_message and function_name == "send_message":
319
+ try:
320
+ message = json.loads(function_args)["message"]
321
+ except:
322
+ prefix = '{\n "message": "'
323
+ if len(function_args) < len(prefix):
324
+ message = "..."
325
+ elif function_args.startswith(prefix):
326
+ message = function_args[len(prefix) :]
327
+ else:
328
+ message = function_args
329
+ message_string += f"🤖 [bold yellow]{message}[/bold yellow]"
330
+ else:
331
+ message_string += f"{function_name}({function_args})"
332
+
333
+ self.update_output(message_string)
334
+
335
+ def stream_start(self):
336
+ if self.streaming:
337
+ print()
338
+ self.live.start() # Start the Live display context and keep it running
339
+ self.update_output("💭 [italic]...[/italic]")
340
+
341
+ def stream_end(self):
342
+ if self.streaming:
343
+ if self.live.is_started:
344
+ self.live.stop()
345
+ print()
346
+ self.live = Live("", console=self.console, refresh_per_second=10)
347
+
348
+ @staticmethod
349
+ def important_message(msg: str):
350
+ StreamingCLIInterface.nonstreaming_interface.important_message(msg)
351
+
352
+ @staticmethod
353
+ def warning_message(msg: str):
354
+ StreamingCLIInterface.nonstreaming_interface.warning_message(msg)
355
+
356
+ def internal_monologue(self, msg: str, msg_obj: Optional[Message] = None):
357
+ if self.disable_inner_mono_call:
358
+ return
359
+ StreamingCLIInterface.nonstreaming_interface.internal_monologue(msg, msg_obj)
360
+
361
+ def assistant_message(self, msg: str, msg_obj: Optional[Message] = None):
362
+ if self.separate_send_message:
363
+ return
364
+ StreamingCLIInterface.nonstreaming_interface.assistant_message(msg, msg_obj)
365
+
366
+ @staticmethod
367
+ def memory_message(msg: str, msg_obj: Optional[Message] = None):
368
+ StreamingCLIInterface.nonstreaming_interface.memory_message(msg, msg_obj)
369
+
370
+ @staticmethod
371
+ def system_message(msg: str, msg_obj: Optional[Message] = None):
372
+ StreamingCLIInterface.nonstreaming_interface.system_message(msg, msg_obj)
373
+
374
+ @staticmethod
375
+ def user_message(msg: str, msg_obj: Optional[Message] = None, raw: bool = False, dump: bool = False, debug: bool = DEBUG):
376
+ StreamingCLIInterface.nonstreaming_interface.user_message(msg, msg_obj)
377
+
378
+ @staticmethod
379
+ def function_message(msg: str, msg_obj: Optional[Message] = None, debug: bool = DEBUG):
380
+ StreamingCLIInterface.nonstreaming_interface.function_message(msg, msg_obj)
381
+
382
+ @staticmethod
383
+ def print_messages(message_sequence: List[Message], dump=False):
384
+ StreamingCLIInterface.nonstreaming_interface.print_messages(message_sequence, dump)
385
+
386
+ @staticmethod
387
+ def print_messages_simple(message_sequence: List[Message]):
388
+ StreamingCLIInterface.nonstreaming_interface.print_messages_simple(message_sequence)
389
+
390
+ @staticmethod
391
+ def print_messages_raw(message_sequence: List[Message]):
392
+ StreamingCLIInterface.nonstreaming_interface.print_messages_raw(message_sequence)
393
+
394
+ @staticmethod
395
+ def step_yield():
396
+ pass