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
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
|