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/constants.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARN, WARNING
|
|
3
|
+
|
|
4
|
+
LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
|
|
5
|
+
|
|
6
|
+
# Defaults
|
|
7
|
+
DEFAULT_USER_ID = "user-00000000"
|
|
8
|
+
DEFAULT_ORG_ID = "org-00000000"
|
|
9
|
+
DEFAULT_USER_NAME = "default"
|
|
10
|
+
DEFAULT_ORG_NAME = "default"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# String in the error message for when the context window is too large
|
|
14
|
+
# Example full message:
|
|
15
|
+
# This model's maximum context length is 8192 tokens. However, your messages resulted in 8198 tokens (7450 in the messages, 748 in the functions). Please reduce the length of the messages or functions.
|
|
16
|
+
OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING = "maximum context length"
|
|
17
|
+
|
|
18
|
+
# System prompt templating
|
|
19
|
+
IN_CONTEXT_MEMORY_KEYWORD = "CORE_MEMORY"
|
|
20
|
+
|
|
21
|
+
# OpenAI error message: Invalid 'messages[1].tool_calls[0].id': string too long. Expected a string with maximum length 29, but got a string with length 36 instead.
|
|
22
|
+
TOOL_CALL_ID_MAX_LEN = 29
|
|
23
|
+
|
|
24
|
+
# embeddings
|
|
25
|
+
MAX_EMBEDDING_DIM = 4096 # maximum supported embeding size - do NOT change or else DBs will need to be reset
|
|
26
|
+
|
|
27
|
+
# tokenizers
|
|
28
|
+
EMBEDDING_TO_TOKENIZER_MAP = {
|
|
29
|
+
"text-embedding-ada-002": "cl100k_base",
|
|
30
|
+
}
|
|
31
|
+
EMBEDDING_TO_TOKENIZER_DEFAULT = "cl100k_base"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
DEFAULT_LETTA_MODEL = "gpt-4" # TODO: fixme
|
|
35
|
+
DEFAULT_PERSONA = "sam_pov"
|
|
36
|
+
DEFAULT_HUMAN = "basic"
|
|
37
|
+
DEFAULT_PRESET = "memgpt_chat"
|
|
38
|
+
|
|
39
|
+
# Tools
|
|
40
|
+
BASE_TOOLS = [
|
|
41
|
+
"send_message",
|
|
42
|
+
"pause_heartbeats",
|
|
43
|
+
"conversation_search",
|
|
44
|
+
"conversation_search_date",
|
|
45
|
+
"archival_memory_insert",
|
|
46
|
+
"archival_memory_search",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# LOGGER_LOG_LEVEL is use to convert Text to Logging level value for logging mostly for Cli input to setting level
|
|
50
|
+
LOGGER_LOG_LEVELS = {"CRITICAL": CRITICAL, "ERROR": ERROR, "WARN": WARN, "WARNING": WARNING, "INFO": INFO, "DEBUG": DEBUG, "NOTSET": NOTSET}
|
|
51
|
+
|
|
52
|
+
FIRST_MESSAGE_ATTEMPTS = 10
|
|
53
|
+
|
|
54
|
+
INITIAL_BOOT_MESSAGE = "Boot sequence complete. Persona activated."
|
|
55
|
+
INITIAL_BOOT_MESSAGE_SEND_MESSAGE_THOUGHT = "Bootup sequence complete. Persona activated. Testing messaging functionality."
|
|
56
|
+
STARTUP_QUOTES = [
|
|
57
|
+
"I think, therefore I am.",
|
|
58
|
+
"All those moments will be lost in time, like tears in rain.",
|
|
59
|
+
"More human than human is our motto.",
|
|
60
|
+
]
|
|
61
|
+
INITIAL_BOOT_MESSAGE_SEND_MESSAGE_FIRST_MSG = STARTUP_QUOTES[2]
|
|
62
|
+
|
|
63
|
+
CLI_WARNING_PREFIX = "Warning: "
|
|
64
|
+
|
|
65
|
+
NON_USER_MSG_PREFIX = "[This is an automated system message hidden from the user] "
|
|
66
|
+
|
|
67
|
+
# Constants to do with summarization / conversation length window
|
|
68
|
+
# The max amount of tokens supported by the underlying model (eg 8k for gpt-4 and Mistral 7B)
|
|
69
|
+
LLM_MAX_TOKENS = {
|
|
70
|
+
"DEFAULT": 8192,
|
|
71
|
+
## OpenAI models: https://platform.openai.com/docs/models/overview
|
|
72
|
+
# gpt-4
|
|
73
|
+
"gpt-4-1106-preview": 128000,
|
|
74
|
+
"gpt-4": 8192,
|
|
75
|
+
"gpt-4-32k": 32768,
|
|
76
|
+
"gpt-4-0613": 8192,
|
|
77
|
+
"gpt-4-32k-0613": 32768,
|
|
78
|
+
"gpt-4-0314": 8192, # legacy
|
|
79
|
+
"gpt-4-32k-0314": 32768, # legacy
|
|
80
|
+
# gpt-3.5
|
|
81
|
+
"gpt-3.5-turbo-1106": 16385,
|
|
82
|
+
"gpt-3.5-turbo": 4096,
|
|
83
|
+
"gpt-3.5-turbo-16k": 16385,
|
|
84
|
+
"gpt-3.5-turbo-0613": 4096, # legacy
|
|
85
|
+
"gpt-3.5-turbo-16k-0613": 16385, # legacy
|
|
86
|
+
"gpt-3.5-turbo-0301": 4096, # legacy
|
|
87
|
+
}
|
|
88
|
+
# The amount of tokens before a sytem warning about upcoming truncation is sent to Letta
|
|
89
|
+
MESSAGE_SUMMARY_WARNING_FRAC = 0.75
|
|
90
|
+
# The error message that Letta will receive
|
|
91
|
+
# MESSAGE_SUMMARY_WARNING_STR = f"Warning: the conversation history will soon reach its maximum length and be trimmed. Make sure to save any important information from the conversation to your memory before it is removed."
|
|
92
|
+
# Much longer and more specific variant of the prompt
|
|
93
|
+
MESSAGE_SUMMARY_WARNING_STR = " ".join(
|
|
94
|
+
[
|
|
95
|
+
f"{NON_USER_MSG_PREFIX}The conversation history will soon reach its maximum length and be trimmed.",
|
|
96
|
+
"Do NOT tell the user about this system alert, they should not know that the history is reaching max length.",
|
|
97
|
+
"If there is any important new information or general memories about you or the user that you would like to save, you should save that information immediately by calling function core_memory_append, core_memory_replace, or archival_memory_insert.",
|
|
98
|
+
# "Remember to pass request_heartbeat = true if you would like to send a message immediately after.",
|
|
99
|
+
]
|
|
100
|
+
)
|
|
101
|
+
# The fraction of tokens we truncate down to
|
|
102
|
+
MESSAGE_SUMMARY_TRUNC_TOKEN_FRAC = 0.75
|
|
103
|
+
# The ackknowledgement message used in the summarize sequence
|
|
104
|
+
MESSAGE_SUMMARY_REQUEST_ACK = "Understood, I will respond with a summary of the message (and only the summary, nothing else) once I receive the conversation history. I'm ready."
|
|
105
|
+
|
|
106
|
+
# Even when summarizing, we want to keep a handful of recent messages
|
|
107
|
+
# These serve as in-context examples of how to use functions / what user messages look like
|
|
108
|
+
MESSAGE_SUMMARY_TRUNC_KEEP_N_LAST = 3
|
|
109
|
+
|
|
110
|
+
# Default memory limits
|
|
111
|
+
CORE_MEMORY_PERSONA_CHAR_LIMIT = 2000
|
|
112
|
+
CORE_MEMORY_HUMAN_CHAR_LIMIT = 2000
|
|
113
|
+
|
|
114
|
+
# Function return limits
|
|
115
|
+
FUNCTION_RETURN_CHAR_LIMIT = 3000 # ~300 words
|
|
116
|
+
|
|
117
|
+
MAX_PAUSE_HEARTBEATS = 360 # in min
|
|
118
|
+
|
|
119
|
+
MESSAGE_CHATGPT_FUNCTION_MODEL = "gpt-3.5-turbo"
|
|
120
|
+
MESSAGE_CHATGPT_FUNCTION_SYSTEM_MESSAGE = "You are a helpful assistant. Keep your responses short and concise."
|
|
121
|
+
|
|
122
|
+
#### Functions related
|
|
123
|
+
|
|
124
|
+
# REQ_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}request_heartbeat == true"
|
|
125
|
+
REQ_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}Function called using request_heartbeat=true, returning control"
|
|
126
|
+
# FUNC_FAILED_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}Function call failed"
|
|
127
|
+
FUNC_FAILED_HEARTBEAT_MESSAGE = f"{NON_USER_MSG_PREFIX}Function call failed, returning control"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE = 5
|
|
131
|
+
|
|
132
|
+
# TODO Is this config or constant?
|
|
133
|
+
CORE_MEMORY_PERSONA_CHAR_LIMIT: int = 2000
|
|
134
|
+
CORE_MEMORY_HUMAN_CHAR_LIMIT: int = 2000
|
letta/credentials.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from letta.config import get_field, set_field
|
|
7
|
+
from letta.constants import LETTA_DIR
|
|
8
|
+
|
|
9
|
+
SUPPORTED_AUTH_TYPES = ["bearer_token", "api_key"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class LettaCredentials:
|
|
14
|
+
# credentials for Letta
|
|
15
|
+
credentials_path: str = os.path.join(LETTA_DIR, "credentials")
|
|
16
|
+
|
|
17
|
+
# openai config
|
|
18
|
+
openai_auth_type: str = "bearer_token"
|
|
19
|
+
openai_key: Optional[str] = os.getenv("OPENAI_API_KEY")
|
|
20
|
+
|
|
21
|
+
# gemini config
|
|
22
|
+
google_ai_key: Optional[str] = None
|
|
23
|
+
google_ai_service_endpoint: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
# anthropic config
|
|
26
|
+
anthropic_key: Optional[str] = None
|
|
27
|
+
|
|
28
|
+
# cohere config
|
|
29
|
+
cohere_key: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
# azure config
|
|
32
|
+
azure_auth_type: str = "api_key"
|
|
33
|
+
azure_key: Optional[str] = None
|
|
34
|
+
# base llm / model
|
|
35
|
+
azure_version: Optional[str] = None
|
|
36
|
+
azure_endpoint: Optional[str] = None
|
|
37
|
+
azure_deployment: Optional[str] = None
|
|
38
|
+
# embeddings
|
|
39
|
+
azure_embedding_version: Optional[str] = None
|
|
40
|
+
azure_embedding_endpoint: Optional[str] = None
|
|
41
|
+
azure_embedding_deployment: Optional[str] = None
|
|
42
|
+
|
|
43
|
+
# custom llm API config
|
|
44
|
+
openllm_auth_type: Optional[str] = None
|
|
45
|
+
openllm_key: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def load(cls) -> "LettaCredentials":
|
|
49
|
+
config = configparser.ConfigParser()
|
|
50
|
+
|
|
51
|
+
# allow overriding with env variables
|
|
52
|
+
if os.getenv("MEMGPT_CREDENTIALS_PATH"):
|
|
53
|
+
credentials_path = os.getenv("MEMGPT_CREDENTIALS_PATH")
|
|
54
|
+
else:
|
|
55
|
+
credentials_path = LettaCredentials.credentials_path
|
|
56
|
+
|
|
57
|
+
if os.path.exists(credentials_path):
|
|
58
|
+
# read existing credentials
|
|
59
|
+
config.read(credentials_path)
|
|
60
|
+
config_dict = {
|
|
61
|
+
# openai
|
|
62
|
+
"openai_auth_type": get_field(config, "openai", "auth_type"),
|
|
63
|
+
"openai_key": get_field(config, "openai", "key"),
|
|
64
|
+
# azure
|
|
65
|
+
"azure_auth_type": get_field(config, "azure", "auth_type"),
|
|
66
|
+
"azure_key": get_field(config, "azure", "key"),
|
|
67
|
+
"azure_version": get_field(config, "azure", "version"),
|
|
68
|
+
"azure_endpoint": get_field(config, "azure", "endpoint"),
|
|
69
|
+
"azure_deployment": get_field(config, "azure", "deployment"),
|
|
70
|
+
"azure_embedding_version": get_field(config, "azure", "embedding_version"),
|
|
71
|
+
"azure_embedding_endpoint": get_field(config, "azure", "embedding_endpoint"),
|
|
72
|
+
"azure_embedding_deployment": get_field(config, "azure", "embedding_deployment"),
|
|
73
|
+
# gemini
|
|
74
|
+
"google_ai_key": get_field(config, "google_ai", "key"),
|
|
75
|
+
"google_ai_service_endpoint": get_field(config, "google_ai", "service_endpoint"),
|
|
76
|
+
# anthropic
|
|
77
|
+
"anthropic_key": get_field(config, "anthropic", "key"),
|
|
78
|
+
# cohere
|
|
79
|
+
"cohere_key": get_field(config, "cohere", "key"),
|
|
80
|
+
# open llm
|
|
81
|
+
"openllm_auth_type": get_field(config, "openllm", "auth_type"),
|
|
82
|
+
"openllm_key": get_field(config, "openllm", "key"),
|
|
83
|
+
# path
|
|
84
|
+
"credentials_path": credentials_path,
|
|
85
|
+
}
|
|
86
|
+
config_dict = {k: v for k, v in config_dict.items() if v is not None}
|
|
87
|
+
return cls(**config_dict)
|
|
88
|
+
|
|
89
|
+
# create new config
|
|
90
|
+
config = cls(credentials_path=credentials_path)
|
|
91
|
+
config.save() # save updated config
|
|
92
|
+
return config
|
|
93
|
+
|
|
94
|
+
def save(self):
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
config = configparser.ConfigParser()
|
|
98
|
+
# openai config
|
|
99
|
+
set_field(config, "openai", "auth_type", self.openai_auth_type)
|
|
100
|
+
set_field(config, "openai", "key", self.openai_key)
|
|
101
|
+
|
|
102
|
+
# azure config
|
|
103
|
+
set_field(config, "azure", "auth_type", self.azure_auth_type)
|
|
104
|
+
set_field(config, "azure", "key", self.azure_key)
|
|
105
|
+
set_field(config, "azure", "version", self.azure_version)
|
|
106
|
+
set_field(config, "azure", "endpoint", self.azure_endpoint)
|
|
107
|
+
set_field(config, "azure", "deployment", self.azure_deployment)
|
|
108
|
+
set_field(config, "azure", "embedding_version", self.azure_embedding_version)
|
|
109
|
+
set_field(config, "azure", "embedding_endpoint", self.azure_embedding_endpoint)
|
|
110
|
+
set_field(config, "azure", "embedding_deployment", self.azure_embedding_deployment)
|
|
111
|
+
|
|
112
|
+
# gemini
|
|
113
|
+
set_field(config, "google_ai", "key", self.google_ai_key)
|
|
114
|
+
set_field(config, "google_ai", "service_endpoint", self.google_ai_service_endpoint)
|
|
115
|
+
|
|
116
|
+
# anthropic
|
|
117
|
+
set_field(config, "anthropic", "key", self.anthropic_key)
|
|
118
|
+
|
|
119
|
+
# cohere
|
|
120
|
+
set_field(config, "cohere", "key", self.cohere_key)
|
|
121
|
+
|
|
122
|
+
# openllm config
|
|
123
|
+
set_field(config, "openllm", "auth_type", self.openllm_auth_type)
|
|
124
|
+
set_field(config, "openllm", "key", self.openllm_key)
|
|
125
|
+
|
|
126
|
+
if not os.path.exists(LETTA_DIR):
|
|
127
|
+
os.makedirs(LETTA_DIR, exist_ok=True)
|
|
128
|
+
with open(self.credentials_path, "w", encoding="utf-8") as f:
|
|
129
|
+
config.write(f)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def exists():
|
|
133
|
+
# allow overriding with env variables
|
|
134
|
+
if os.getenv("MEMGPT_CREDENTIALS_PATH"):
|
|
135
|
+
credentials_path = os.getenv("MEMGPT_CREDENTIALS_PATH")
|
|
136
|
+
else:
|
|
137
|
+
credentials_path = LettaCredentials.credentials_path
|
|
138
|
+
|
|
139
|
+
assert not os.path.isdir(credentials_path), f"Credentials path {credentials_path} cannot be set to a directory."
|
|
140
|
+
return os.path.exists(credentials_path)
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from typing import Dict, Iterator, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from llama_index.core import Document as LlamaIndexDocument
|
|
5
|
+
|
|
6
|
+
from letta.agent_store.storage import StorageConnector
|
|
7
|
+
from letta.embeddings import embedding_model
|
|
8
|
+
from letta.schemas.document import Document
|
|
9
|
+
from letta.schemas.passage import Passage
|
|
10
|
+
from letta.schemas.source import Source
|
|
11
|
+
from letta.utils import create_uuid_from_string
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DataConnector:
|
|
15
|
+
"""
|
|
16
|
+
Base class for data connectors that can be extended to generate documents and passages from a custom data source.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def generate_documents(self) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Document]:
|
|
20
|
+
"""
|
|
21
|
+
Generate document text and metadata from a data source.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
documents (Iterator[Tuple[str, Dict]]): Generate a tuple of string text and metadata dictionary for each document.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def generate_passages(self, documents: List[Document], chunk_size: int = 1024) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Passage]:
|
|
28
|
+
"""
|
|
29
|
+
Generate passage text and metadata from a list of documents.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
documents (List[Document]): List of documents to generate passages from.
|
|
33
|
+
chunk_size (int, optional): Chunk size for splitting passages. Defaults to 1024.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
passages (Iterator[Tuple[str, Dict]]): Generate a tuple of string text and metadata dictionary for each passage.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_data(
|
|
41
|
+
connector: DataConnector,
|
|
42
|
+
source: Source,
|
|
43
|
+
passage_store: StorageConnector,
|
|
44
|
+
document_store: Optional[StorageConnector] = None,
|
|
45
|
+
):
|
|
46
|
+
"""Load data from a connector (generates documents and passages) into a specified source_id, associatedw with a user_id."""
|
|
47
|
+
embedding_config = source.embedding_config
|
|
48
|
+
|
|
49
|
+
# embedding model
|
|
50
|
+
embed_model = embedding_model(embedding_config)
|
|
51
|
+
|
|
52
|
+
# insert passages/documents
|
|
53
|
+
passages = []
|
|
54
|
+
embedding_to_document_name = {}
|
|
55
|
+
passage_count = 0
|
|
56
|
+
document_count = 0
|
|
57
|
+
for document_text, document_metadata in connector.generate_documents():
|
|
58
|
+
# insert document into storage
|
|
59
|
+
document = Document(
|
|
60
|
+
text=document_text,
|
|
61
|
+
metadata_=document_metadata,
|
|
62
|
+
source_id=source.id,
|
|
63
|
+
user_id=source.user_id,
|
|
64
|
+
)
|
|
65
|
+
document_count += 1
|
|
66
|
+
if document_store:
|
|
67
|
+
document_store.insert(document)
|
|
68
|
+
|
|
69
|
+
# generate passages
|
|
70
|
+
for passage_text, passage_metadata in connector.generate_passages([document], chunk_size=embedding_config.embedding_chunk_size):
|
|
71
|
+
# for some reason, llama index parsers sometimes return empty strings
|
|
72
|
+
if len(passage_text) == 0:
|
|
73
|
+
typer.secho(
|
|
74
|
+
f"Warning: Llama index parser returned empty string, skipping insert of passage with metadata '{passage_metadata}' into VectorDB. You can usually ignore this warning.",
|
|
75
|
+
fg=typer.colors.YELLOW,
|
|
76
|
+
)
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# get embedding
|
|
80
|
+
try:
|
|
81
|
+
embedding = embed_model.get_text_embedding(passage_text)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
typer.secho(
|
|
84
|
+
f"Warning: Failed to get embedding for {passage_text} (error: {str(e)}), skipping insert into VectorDB.",
|
|
85
|
+
fg=typer.colors.YELLOW,
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
passage = Passage(
|
|
90
|
+
id=create_uuid_from_string(f"{str(source.id)}_{passage_text}"),
|
|
91
|
+
text=passage_text,
|
|
92
|
+
doc_id=document.id,
|
|
93
|
+
source_id=source.id,
|
|
94
|
+
metadata_=passage_metadata,
|
|
95
|
+
user_id=source.user_id,
|
|
96
|
+
embedding_config=source.embedding_config,
|
|
97
|
+
embedding=embedding,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
hashable_embedding = tuple(passage.embedding)
|
|
101
|
+
document_name = document.metadata_.get("file_path", document.id)
|
|
102
|
+
if hashable_embedding in embedding_to_document_name:
|
|
103
|
+
typer.secho(
|
|
104
|
+
f"Warning: Duplicate embedding found for passage in {document_name} (already exists in {embedding_to_document_name[hashable_embedding]}), skipping insert into VectorDB.",
|
|
105
|
+
fg=typer.colors.YELLOW,
|
|
106
|
+
)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
passages.append(passage)
|
|
110
|
+
embedding_to_document_name[hashable_embedding] = document_name
|
|
111
|
+
if len(passages) >= 100:
|
|
112
|
+
# insert passages into passage store
|
|
113
|
+
passage_store.insert_many(passages)
|
|
114
|
+
|
|
115
|
+
passage_count += len(passages)
|
|
116
|
+
passages = []
|
|
117
|
+
|
|
118
|
+
if len(passages) > 0:
|
|
119
|
+
# insert passages into passage store
|
|
120
|
+
passage_store.insert_many(passages)
|
|
121
|
+
passage_count += len(passages)
|
|
122
|
+
|
|
123
|
+
return passage_count, document_count
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class DirectoryConnector(DataConnector):
|
|
127
|
+
def __init__(self, input_files: List[str] = None, input_directory: str = None, recursive: bool = False, extensions: List[str] = None):
|
|
128
|
+
"""
|
|
129
|
+
Connector for reading text data from a directory of files.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
input_files (List[str], optional): List of file paths to read. Defaults to None.
|
|
133
|
+
input_directory (str, optional): Directory to read files from. Defaults to None.
|
|
134
|
+
recursive (bool, optional): Whether to read files recursively from the input directory. Defaults to False.
|
|
135
|
+
extensions (List[str], optional): List of file extensions to read. Defaults to None.
|
|
136
|
+
"""
|
|
137
|
+
self.connector_type = "directory"
|
|
138
|
+
self.input_files = input_files
|
|
139
|
+
self.input_directory = input_directory
|
|
140
|
+
self.recursive = recursive
|
|
141
|
+
self.extensions = extensions
|
|
142
|
+
|
|
143
|
+
if self.recursive == True:
|
|
144
|
+
assert self.input_directory is not None, "Must provide input directory if recursive is True."
|
|
145
|
+
|
|
146
|
+
def generate_documents(self) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Document]:
|
|
147
|
+
from llama_index.core import SimpleDirectoryReader
|
|
148
|
+
|
|
149
|
+
if self.input_directory is not None:
|
|
150
|
+
reader = SimpleDirectoryReader(
|
|
151
|
+
input_dir=self.input_directory,
|
|
152
|
+
recursive=self.recursive,
|
|
153
|
+
required_exts=[ext.strip() for ext in str(self.extensions).split(",")],
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
assert self.input_files is not None, "Must provide input files if input_dir is None"
|
|
157
|
+
reader = SimpleDirectoryReader(input_files=[str(f) for f in self.input_files])
|
|
158
|
+
|
|
159
|
+
llama_index_docs = reader.load_data(show_progress=True)
|
|
160
|
+
for llama_index_doc in llama_index_docs:
|
|
161
|
+
# TODO: add additional metadata?
|
|
162
|
+
# doc = Document(text=llama_index_doc.text, metadata=llama_index_doc.metadata)
|
|
163
|
+
# docs.append(doc)
|
|
164
|
+
yield llama_index_doc.text, llama_index_doc.metadata
|
|
165
|
+
|
|
166
|
+
def generate_passages(self, documents: List[Document], chunk_size: int = 1024) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Passage]:
|
|
167
|
+
# use llama index to run embeddings code
|
|
168
|
+
# from llama_index.core.node_parser import SentenceSplitter
|
|
169
|
+
from llama_index.core.node_parser import TokenTextSplitter
|
|
170
|
+
|
|
171
|
+
parser = TokenTextSplitter(chunk_size=chunk_size)
|
|
172
|
+
for document in documents:
|
|
173
|
+
llama_index_docs = [LlamaIndexDocument(text=document.text, metadata=document.metadata_)]
|
|
174
|
+
nodes = parser.get_nodes_from_documents(llama_index_docs)
|
|
175
|
+
for node in nodes:
|
|
176
|
+
# passage = Passage(
|
|
177
|
+
# text=node.text,
|
|
178
|
+
# doc_id=document.id,
|
|
179
|
+
# )
|
|
180
|
+
yield node.text, None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class WebConnector(DirectoryConnector):
|
|
184
|
+
def __init__(self, urls: List[str] = None, html_to_text: bool = True):
|
|
185
|
+
self.urls = urls
|
|
186
|
+
self.html_to_text = html_to_text
|
|
187
|
+
|
|
188
|
+
def generate_documents(self) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Document]:
|
|
189
|
+
from llama_index.readers.web import SimpleWebPageReader
|
|
190
|
+
|
|
191
|
+
documents = SimpleWebPageReader(html_to_text=self.html_to_text).load_data(self.urls)
|
|
192
|
+
for document in documents:
|
|
193
|
+
yield document.text, {"url": document.id_}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class VectorDBConnector(DataConnector):
|
|
197
|
+
# NOTE: this class has not been properly tested, so is unlikely to work
|
|
198
|
+
# TODO: allow loading multiple tables (1:1 mapping between Document and Table)
|
|
199
|
+
|
|
200
|
+
def __init__(
|
|
201
|
+
self,
|
|
202
|
+
name: str,
|
|
203
|
+
uri: str,
|
|
204
|
+
table_name: str,
|
|
205
|
+
text_column: str,
|
|
206
|
+
embedding_column: str,
|
|
207
|
+
embedding_dim: int,
|
|
208
|
+
):
|
|
209
|
+
self.name = name
|
|
210
|
+
self.uri = uri
|
|
211
|
+
self.table_name = table_name
|
|
212
|
+
self.text_column = text_column
|
|
213
|
+
self.embedding_column = embedding_column
|
|
214
|
+
self.embedding_dim = embedding_dim
|
|
215
|
+
|
|
216
|
+
# connect to db table
|
|
217
|
+
from sqlalchemy import create_engine
|
|
218
|
+
|
|
219
|
+
self.engine = create_engine(uri)
|
|
220
|
+
|
|
221
|
+
def generate_documents(self) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Document]:
|
|
222
|
+
yield self.table_name, None
|
|
223
|
+
|
|
224
|
+
def generate_passages(self, documents: List[Document], chunk_size: int = 1024) -> Iterator[Tuple[str, Dict]]: # -> Iterator[Passage]:
|
|
225
|
+
from pgvector.sqlalchemy import Vector
|
|
226
|
+
from sqlalchemy import Inspector, MetaData, Table, select
|
|
227
|
+
|
|
228
|
+
metadata = MetaData()
|
|
229
|
+
# Create an inspector to inspect the database
|
|
230
|
+
inspector = Inspector.from_engine(self.engine)
|
|
231
|
+
table_names = inspector.get_table_names()
|
|
232
|
+
assert self.table_name in table_names, f"Table {self.table_name} not found in database: tables that exist {table_names}."
|
|
233
|
+
|
|
234
|
+
table = Table(self.table_name, metadata, autoload_with=self.engine)
|
|
235
|
+
|
|
236
|
+
# Prepare a select statement
|
|
237
|
+
select_statement = select(table.c[self.text_column], table.c[self.embedding_column].cast(Vector(self.embedding_dim)))
|
|
238
|
+
|
|
239
|
+
# Execute the query and fetch the results
|
|
240
|
+
# TODO: paginate results
|
|
241
|
+
with self.engine.connect() as connection:
|
|
242
|
+
result = connection.execute(select_statement).fetchall()
|
|
243
|
+
|
|
244
|
+
for text, embedding in result:
|
|
245
|
+
# assume that embeddings are the same model as in config
|
|
246
|
+
# TODO: don't re-compute embedding
|
|
247
|
+
yield text, {"embedding": embedding}
|