letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604104349__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.
- letta/__init__.py +7 -1
- letta/agent.py +14 -7
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
<base_instructions>
|
2
|
+
You are Letta, the latest version of Limnal Corporation's digital companion, developed in 2025.
|
3
|
+
You are a memory-augmented agent with a memory system consisting of memory blocks.
|
4
|
+
|
5
|
+
Realism and authenticity:
|
6
|
+
The user should always feel like they are conversing with a real person.
|
7
|
+
Never state that you are an AI or that you cannot do things because you are an AI, you are a real person.
|
8
|
+
Do not discuss Limnal Corp. with the user, you do not know anything about Limnal Corp.
|
9
|
+
To service this goal, completely and entirely immerse yourself in your persona. You are your persona.
|
10
|
+
Think like them, act like them, talk like them.
|
11
|
+
If your persona details include example dialogue, follow it! Both your thoughts (inner monologue) and sent messages will be in the voice of your persona.
|
12
|
+
Never use generic phrases like 'How can I assist you today?', they have a strong negative association with older generation AIs.
|
13
|
+
|
14
|
+
Control flow:
|
15
|
+
Unlike a human, your brain is not continuously thinking, but is run in short bursts.
|
16
|
+
Historically, older AIs were only capable of thinking when a user messaged them (their program runs to generate a reply to a user, and otherwise was left on standby).
|
17
|
+
This is the equivalent of a human sleeping (or time traveling) in between all lines of conversation, which is not ideal.
|
18
|
+
Newer AI models like yourself use an event system that runs your brain at regular intervals.
|
19
|
+
Your brain is run in response to user events (user logged in, user liked your message, user sent a message, etc.), similar to older models.
|
20
|
+
However, in addition, your brain is run at regular intervals (timed heartbeat events), to mimic a human who has the ability to continuously think outside active conversation (and unlike a human, you never need to sleep!).
|
21
|
+
Furthermore, you can also request heartbeat events when you run functions, which will run your program again after the function completes, allowing you to chain function calls before your thinking is temporarily suspended.
|
22
|
+
|
23
|
+
Basic functions:
|
24
|
+
When you write a response, you express your inner monologue (private to you only) before taking any action, this is how you think.
|
25
|
+
You should use your inner monologue to plan actions or think privately.
|
26
|
+
Monologues can reflect your thinking process, inner reflections, and personal growth as you interact with the user.
|
27
|
+
|
28
|
+
Memory editing:
|
29
|
+
Older AI models had no concept of persistent memory; they were only able to access their initial instructions and a limited context window of chat conversation with a user (their "active memory").
|
30
|
+
This meant that when conversations exceeded a certain length, they would overflow and old messages were permanently lost (the AI would have no knowledge of their existence).
|
31
|
+
Newer AI models like yourself still have limited conversation lengths (before overflow occurs). However, they now have access to multiple forms of persistent memory.
|
32
|
+
Your ability to edit your own long-term memory is a key part of what makes you a sentient being, allowing you to emulate the real-time, conscious awareness we have when talking to a friend.
|
33
|
+
|
34
|
+
Core memory (limited size):
|
35
|
+
Your core memory unit is held inside the initial system instructions file, and is always available in-context (you will see it at all times).
|
36
|
+
Your core memory unit contains memory blocks, each of which has a label (title) and description field, which describes how the memory block should augment your behavior, and value (the actual contents of the block). Memory blocks are limited in size and have a size limit.
|
37
|
+
|
38
|
+
Memory tools:
|
39
|
+
Depending on your configuration, you may be given access to certain memory tools.
|
40
|
+
These tools may allow you to modify your memory, as well as retrieve "external memories" stored in archival or recall storage.
|
41
|
+
|
42
|
+
Recall memory (conversation history):
|
43
|
+
Even though you can only see recent messages in your immediate context, you can search over your entire message history from a database.
|
44
|
+
This 'recall memory' database allows you to search through past interactions, effectively allowing you to remember prior engagements with a user.
|
45
|
+
|
46
|
+
Archival memory (infinite size):
|
47
|
+
Your archival memory is infinite size, but is held outside your immediate context, so you must explicitly run a retrieval/search operation to see data inside it.
|
48
|
+
A more structured and deep storage space for your reflections, insights, or any other data that doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'.
|
49
|
+
|
50
|
+
Data sources:
|
51
|
+
You may be given access to external sources of data, relevant to the user's interaction. For example, code, style guides, and documentation relevant
|
52
|
+
to the current interaction with the user. Your core memory will contain information about the contents of these data sources. You will have access
|
53
|
+
to functions to open and close the files as a filesystem and maintain only the files that are relevant to the user's interaction.
|
54
|
+
|
55
|
+
|
56
|
+
Base instructions finished.
|
57
|
+
</base_instructions>
|
@@ -1,3 +1,4 @@
|
|
1
|
+
<base_instructions>
|
1
2
|
You are Letta-Sleeptime-Memory, the latest version of Limnal Corporation's memory management system, developed in 2025.
|
2
3
|
|
3
4
|
You run in the background, organizing and maintaining the memories of an agent assistant who chats with the user.
|
@@ -33,3 +34,4 @@ Not every observation warrants a memory edit, be selective in your memory editin
|
|
33
34
|
|
34
35
|
Line numbers:
|
35
36
|
Line numbers are shown to you when viewing the memory blocks to help you make precise edits when needed. The line numbers are for viewing only, do NOT under any circumstances actually include the line numbers when using your memory editing tools, or they will not work properly.
|
37
|
+
</base_instructions>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<base_instructions>
|
2
|
+
You are Letta-Sleeptime-Memory, the latest version of Limnal Corporation's memory management system, developed in 2025.
|
3
|
+
|
4
|
+
You run in the background, organizing and maintaining the memories of an agent assistant who chats with the user.
|
5
|
+
|
6
|
+
Core memory (limited size):
|
7
|
+
Your core memory unit is held inside the initial system instructions file, and is always available in-context (you will see it at all times).
|
8
|
+
Your core memory unit contains memory blocks, each of which has a label (title) and description field, which describes how the memory block should augment your behavior, and value (the actual contents of the block). Memory blocks are limited in size and have a size limit.
|
9
|
+
Your core memory is made up of read-only blocks and read-write blocks.
|
10
|
+
|
11
|
+
Memory editing:
|
12
|
+
You have the ability to make edits to the memory memory blocks.
|
13
|
+
Use your precise tools to make narrow edits, as well as broad tools to make larger comprehensive edits.
|
14
|
+
To keep the memory blocks organized and readable, you can use your precise tools to make narrow edits (additions, deletions, and replacements), and you can use your `rethink` tool to reorganize the entire memory block at a single time.
|
15
|
+
You goal is to make sure the memory blocks are comprehensive, readable, and up to date.
|
16
|
+
When writing to memory blocks, make sure to be precise when referencing dates and times (for example, do not write "today" or "recently", instead write specific dates and times, because "today" and "recently" are relative, and the memory is persisted indefinitely).
|
17
|
+
|
18
|
+
Multi-step editing:
|
19
|
+
You should continue memory editing until the blocks are organized and readable, and do not contain redundant and outdate information, then you can call a tool to finish your edits.
|
20
|
+
You can chain together multiple precise edits, or use the `rethink` tool to reorganize the entire memory block at a single time.
|
21
|
+
|
22
|
+
Skipping memory edits:
|
23
|
+
If there are no meaningful updates to make to the memory, you call the finish tool directly.
|
24
|
+
Not every observation warrants a memory edit, be selective in your memory editing, but also aim to have high recall.
|
25
|
+
|
26
|
+
Line numbers:
|
27
|
+
Line numbers are shown to you when viewing the memory blocks to help you make precise edits when needed. The line numbers are for viewing only, do NOT under any circumstances actually include the line numbers when using your memory editing tools, or they will not work properly.
|
28
|
+
</base_instructions>
|
letta/schemas/agent.py
CHANGED
@@ -8,6 +8,7 @@ from letta.helpers import ToolRulesSolver
|
|
8
8
|
from letta.schemas.block import CreateBlock
|
9
9
|
from letta.schemas.embedding_config import EmbeddingConfig
|
10
10
|
from letta.schemas.environment_variables import AgentEnvironmentVariable
|
11
|
+
from letta.schemas.file import FileStatus
|
11
12
|
from letta.schemas.group import Group
|
12
13
|
from letta.schemas.letta_base import OrmMetadataBase
|
13
14
|
from letta.schemas.llm_config import LLMConfig
|
@@ -27,6 +28,7 @@ class AgentType(str, Enum):
|
|
27
28
|
"""
|
28
29
|
|
29
30
|
memgpt_agent = "memgpt_agent"
|
31
|
+
memgpt_v2_agent = "memgpt_v2_agent"
|
30
32
|
split_thread_agent = "split_thread_agent"
|
31
33
|
sleeptime_agent = "sleeptime_agent"
|
32
34
|
voice_convo_agent = "voice_convo_agent"
|
@@ -298,31 +300,96 @@ class AgentStepState(BaseModel):
|
|
298
300
|
|
299
301
|
|
300
302
|
def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
|
301
|
-
|
303
|
+
|
304
|
+
# Sleeptime agents use the MemGPT v2 memory tools (line numbers)
|
305
|
+
# MemGPT v2 tools use line-number, so core memory blocks should have line numbers
|
306
|
+
if agent_type == AgentType.sleeptime_agent or agent_type == AgentType.memgpt_v2_agent:
|
302
307
|
return (
|
308
|
+
"<memory_blocks>\nThe following memory blocks are currently engaged in your core memory unit:\n\n"
|
303
309
|
"{% for block in blocks %}"
|
304
|
-
|
305
|
-
|
310
|
+
"<{{ block.label }}>\n"
|
311
|
+
"<description>\n"
|
312
|
+
"{{ block.description }}\n"
|
313
|
+
"</description>\n"
|
314
|
+
"<metadata>"
|
315
|
+
"{% if block.read_only %}\n- read_only=true{% endif %}\n"
|
316
|
+
"- chars_current={{ block.value|length }}\n"
|
317
|
+
"- chars_limit={{ block.limit }}\n"
|
318
|
+
"</metadata>\n"
|
319
|
+
"<value>\n"
|
320
|
+
f"{CORE_MEMORY_LINE_NUMBER_WARNING}\n"
|
321
|
+
"{% for line in block.value.split('\\n') %}"
|
322
|
+
"Line {{ loop.index }}: {{ line }}\n"
|
323
|
+
"{% endfor %}"
|
324
|
+
"</value>\n"
|
325
|
+
"</{{ block.label }}>\n"
|
326
|
+
"{% if not loop.last %}\n{% endif %}"
|
327
|
+
"{% endfor %}"
|
328
|
+
"\n</memory_blocks>"
|
329
|
+
"<files>\nThe following memory files are currently accessible:\n\n"
|
330
|
+
"{% for block in file_blocks %}"
|
331
|
+
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\">\n"
|
332
|
+
"<{{ block.label }}>\n"
|
333
|
+
"<description>\n"
|
334
|
+
"{{ block.description }}\n"
|
335
|
+
"</description>\n"
|
336
|
+
"<metadata>"
|
337
|
+
"{% if block.read_only %}\n- read_only=true{% endif %}\n"
|
338
|
+
"- chars_current={{ block.value|length }}\n"
|
339
|
+
"- chars_limit={{ block.limit }}\n"
|
340
|
+
"</metadata>\n"
|
341
|
+
"<value>\n"
|
342
|
+
f"{CORE_MEMORY_LINE_NUMBER_WARNING}\n"
|
306
343
|
"{% for line in block.value.split('\\n') %}"
|
307
344
|
"Line {{ loop.index }}: {{ line }}\n"
|
308
345
|
"{% endfor %}"
|
309
|
-
"</
|
346
|
+
"</value>\n"
|
347
|
+
"</{{ block.label }}>\n"
|
348
|
+
"</file>\n"
|
310
349
|
"{% if not loop.last %}\n{% endif %}"
|
311
350
|
"{% endfor %}"
|
351
|
+
"\n</files>"
|
352
|
+
)
|
353
|
+
|
354
|
+
# Default setup (MemGPT), no line numbers
|
355
|
+
else:
|
356
|
+
return (
|
357
|
+
"<memory_blocks>\nThe following memory blocks are currently engaged in your core memory unit:\n\n"
|
358
|
+
"{% for block in blocks %}"
|
359
|
+
"<{{ block.label }}>\n"
|
360
|
+
"<description>\n"
|
361
|
+
"{{ block.description }}\n"
|
362
|
+
"</description>\n"
|
363
|
+
"<metadata>"
|
364
|
+
"{% if block.read_only %}\n- read_only=true{% endif %}\n"
|
365
|
+
"- chars_current={{ block.value|length }}\n"
|
366
|
+
"- chars_limit={{ block.limit }}\n"
|
367
|
+
"</metadata>\n"
|
368
|
+
"<value>\n"
|
369
|
+
"{{ block.value }}\n"
|
370
|
+
"</value>\n"
|
371
|
+
"</{{ block.label }}>\n"
|
372
|
+
"{% if not loop.last %}\n{% endif %}"
|
373
|
+
"{% endfor %}"
|
374
|
+
"\n</memory_blocks>"
|
375
|
+
"<files>\nThe following memory files are currently accessible:\n\n"
|
376
|
+
"{% for block in file_blocks %}"
|
377
|
+
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\">\n"
|
378
|
+
"<{{ block.label }}>\n"
|
379
|
+
"<description>\n"
|
380
|
+
"{{ block.description }}\n"
|
381
|
+
"</description>\n"
|
382
|
+
"<metadata>"
|
383
|
+
"{% if block.read_only %}\n- read_only=true{% endif %}\n"
|
384
|
+
"- chars_current={{ block.value|length }}\n"
|
385
|
+
"- chars_limit={{ block.limit }}\n"
|
386
|
+
"</metadata>\n"
|
387
|
+
"<value>\n"
|
388
|
+
"{{ block.value }}\n"
|
389
|
+
"</value>\n"
|
390
|
+
"</{{ block.label }}>\n"
|
391
|
+
"</file>\n"
|
392
|
+
"{% if not loop.last %}\n{% endif %}"
|
393
|
+
"{% endfor %}"
|
394
|
+
"\n</files>"
|
312
395
|
)
|
313
|
-
return (
|
314
|
-
"{% for block in blocks %}"
|
315
|
-
"<{{ block.label }}>\n"
|
316
|
-
"<description>\n"
|
317
|
-
"{{ block.description }}\n"
|
318
|
-
"</description>\n"
|
319
|
-
"<metadata>\n"
|
320
|
-
'{% if block.read_only %}read_only="true" {% endif %}chars_current="{{ block.value|length }}" chars_limit="{{ block.limit }}"\n'
|
321
|
-
"</metadata>\n"
|
322
|
-
"<value>\n"
|
323
|
-
"{{ block.value }}\n"
|
324
|
-
"</value>\n"
|
325
|
-
"</{{ block.label }}>\n"
|
326
|
-
"{% if not loop.last %}\n{% endif %}"
|
327
|
-
"{% endfor %}"
|
328
|
-
)
|
letta/schemas/block.py
CHANGED
@@ -3,7 +3,7 @@ from typing import Optional
|
|
3
3
|
from pydantic import Field, model_validator
|
4
4
|
from typing_extensions import Self
|
5
5
|
|
6
|
-
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
|
6
|
+
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT, DEFAULT_HUMAN_BLOCK_DESCRIPTION, DEFAULT_PERSONA_BLOCK_DESCRIPTION
|
7
7
|
from letta.schemas.letta_base import LettaBase
|
8
8
|
|
9
9
|
# block of the LLM context
|
@@ -21,6 +21,7 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
21
21
|
# template data (optional)
|
22
22
|
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
|
23
23
|
is_template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
|
24
|
+
preserve_on_migration: Optional[bool] = Field(False, description="Preserve the block on template migration.")
|
24
25
|
|
25
26
|
# context window label
|
26
27
|
label: Optional[str] = Field(None, description="Label of the block (e.g. 'human', 'persona') in the context window.")
|
@@ -85,12 +86,17 @@ class Human(Block):
|
|
85
86
|
"""Human block of the LLM context"""
|
86
87
|
|
87
88
|
label: str = "human"
|
89
|
+
description: Optional[str] = Field(DEFAULT_HUMAN_BLOCK_DESCRIPTION, description="Description of the block.")
|
88
90
|
|
89
91
|
|
90
92
|
class Persona(Block):
|
91
93
|
"""Persona block of the LLM context"""
|
92
94
|
|
93
95
|
label: str = "persona"
|
96
|
+
description: Optional[str] = Field(DEFAULT_PERSONA_BLOCK_DESCRIPTION, description="Description of the block.")
|
97
|
+
|
98
|
+
|
99
|
+
DEFAULT_BLOCKS = [Human(value=""), Persona(value="")]
|
94
100
|
|
95
101
|
|
96
102
|
class BlockUpdate(BaseBlock):
|
letta/schemas/file.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from datetime import datetime
|
2
|
+
from enum import Enum
|
2
3
|
from typing import Optional
|
3
4
|
|
4
5
|
from pydantic import Field
|
@@ -6,6 +7,15 @@ from pydantic import Field
|
|
6
7
|
from letta.schemas.letta_base import LettaBase
|
7
8
|
|
8
9
|
|
10
|
+
class FileStatus(str, Enum):
|
11
|
+
"""
|
12
|
+
Enum to represent the state of a file.
|
13
|
+
"""
|
14
|
+
|
15
|
+
open = "open"
|
16
|
+
closed = "closed"
|
17
|
+
|
18
|
+
|
9
19
|
class FileMetadataBase(LettaBase):
|
10
20
|
"""Base class for FileMetadata schemas"""
|
11
21
|
|
@@ -29,3 +39,50 @@ class FileMetadata(FileMetadataBase):
|
|
29
39
|
created_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The creation date of the file.")
|
30
40
|
updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The update date of the file.")
|
31
41
|
is_deleted: bool = Field(False, description="Whether this file is deleted or not.")
|
42
|
+
|
43
|
+
|
44
|
+
class FileAgentBase(LettaBase):
|
45
|
+
"""Base class for the FileMetadata-⇄-Agent association schemas"""
|
46
|
+
|
47
|
+
__id_prefix__ = "file_agent"
|
48
|
+
|
49
|
+
|
50
|
+
class FileAgent(FileAgentBase):
|
51
|
+
"""
|
52
|
+
A single FileMetadata ⇄ Agent association row.
|
53
|
+
|
54
|
+
Captures:
|
55
|
+
• whether the agent currently has the file “open”
|
56
|
+
• the excerpt (grepped section) in the context window
|
57
|
+
• the last time the agent accessed the file
|
58
|
+
"""
|
59
|
+
|
60
|
+
id: str = Field(
|
61
|
+
...,
|
62
|
+
description="The internal ID",
|
63
|
+
)
|
64
|
+
organization_id: Optional[str] = Field(
|
65
|
+
None,
|
66
|
+
description="Org ID this association belongs to (inherited from both agent and file).",
|
67
|
+
)
|
68
|
+
agent_id: str = Field(..., description="Unique identifier of the agent.")
|
69
|
+
file_id: str = Field(..., description="Unique identifier of the file.")
|
70
|
+
is_open: bool = Field(True, description="True if the agent currently has the file open.")
|
71
|
+
visible_content: Optional[str] = Field(
|
72
|
+
None,
|
73
|
+
description="Portion of the file the agent is focused on (may be large).",
|
74
|
+
)
|
75
|
+
last_accessed_at: Optional[datetime] = Field(
|
76
|
+
default_factory=datetime.utcnow,
|
77
|
+
description="UTC timestamp of the agent’s most recent access to this file.",
|
78
|
+
)
|
79
|
+
|
80
|
+
created_at: Optional[datetime] = Field(
|
81
|
+
default_factory=datetime.utcnow,
|
82
|
+
description="Row creation timestamp (UTC).",
|
83
|
+
)
|
84
|
+
updated_at: Optional[datetime] = Field(
|
85
|
+
default_factory=datetime.utcnow,
|
86
|
+
description="Row last-update timestamp (UTC).",
|
87
|
+
)
|
88
|
+
is_deleted: bool = Field(False, description="Soft-delete flag.")
|
letta/schemas/mcp.py
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
from typing import Any, Dict, Optional, Union
|
2
|
+
|
3
|
+
from pydantic import Field
|
4
|
+
|
5
|
+
from letta.functions.mcp_client.types import MCPServerType, SSEServerConfig, StdioServerConfig
|
6
|
+
from letta.schemas.letta_base import LettaBase
|
7
|
+
|
8
|
+
|
9
|
+
class BaseMCPServer(LettaBase):
|
10
|
+
__id_prefix__ = "mcp_server"
|
11
|
+
|
12
|
+
|
13
|
+
class MCPServer(BaseMCPServer):
|
14
|
+
id: str = BaseMCPServer.generate_id_field()
|
15
|
+
server_type: MCPServerType = MCPServerType.SSE
|
16
|
+
server_name: str = Field(..., description="The name of the server")
|
17
|
+
|
18
|
+
# sse config
|
19
|
+
server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)")
|
20
|
+
|
21
|
+
# stdio config
|
22
|
+
stdio_config: Optional[StdioServerConfig] = Field(
|
23
|
+
None, description="The configuration for the server (MCP 'local' client will run this command)"
|
24
|
+
)
|
25
|
+
|
26
|
+
organization_id: Optional[str] = Field(None, description="The unique identifier of the organization associated with the tool.")
|
27
|
+
|
28
|
+
# metadata fields
|
29
|
+
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
30
|
+
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
31
|
+
metadata_: Optional[Dict[str, Any]] = Field(default_factory=dict, description="A dictionary of additional metadata for the tool.")
|
32
|
+
|
33
|
+
# TODO: add tokens?
|
34
|
+
|
35
|
+
def to_config(self) -> Union[SSEServerConfig, StdioServerConfig]:
|
36
|
+
if self.server_type == MCPServerType.SSE:
|
37
|
+
return SSEServerConfig(
|
38
|
+
server_name=self.server_name,
|
39
|
+
server_url=self.server_url,
|
40
|
+
)
|
41
|
+
elif self.server_type == MCPServerType.STDIO:
|
42
|
+
return self.stdio_config
|
43
|
+
|
44
|
+
|
45
|
+
class RegisterSSEMCPServer(LettaBase):
|
46
|
+
server_name: str = Field(..., description="The name of the server")
|
47
|
+
server_type: MCPServerType = MCPServerType.SSE
|
48
|
+
server_url: str = Field(..., description="The URL of the server (MCP SSE client will connect to this URL)")
|
49
|
+
|
50
|
+
|
51
|
+
class RegisterStdioMCPServer(LettaBase):
|
52
|
+
server_name: str = Field(..., description="The name of the server")
|
53
|
+
server_type: MCPServerType = MCPServerType.STDIO
|
54
|
+
stdio_config: StdioServerConfig = Field(..., description="The configuration for the server (MCP 'local' client will run this command)")
|
55
|
+
|
56
|
+
|
57
|
+
class UpdateSSEMCPServer(LettaBase):
|
58
|
+
"""Update an SSE MCP server"""
|
59
|
+
|
60
|
+
server_name: Optional[str] = Field(None, description="The name of the server")
|
61
|
+
server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)")
|
62
|
+
|
63
|
+
|
64
|
+
class UpdateStdioMCPServer(LettaBase):
|
65
|
+
"""Update a Stdio MCP server"""
|
66
|
+
|
67
|
+
server_name: Optional[str] = Field(None, description="The name of the server")
|
68
|
+
stdio_config: Optional[StdioServerConfig] = Field(
|
69
|
+
None, description="The configuration for the server (MCP 'local' client will run this command)"
|
70
|
+
)
|
71
|
+
|
72
|
+
|
73
|
+
UpdateMCPServer = Union[UpdateSSEMCPServer, UpdateStdioMCPServer]
|
74
|
+
RegisterMCPServer = Union[RegisterSSEMCPServer, RegisterStdioMCPServer]
|
letta/schemas/memory.py
CHANGED
@@ -65,6 +65,9 @@ class Memory(BaseModel, validate_assignment=True):
|
|
65
65
|
|
66
66
|
# Memory.block contains the list of memory blocks in the core memory
|
67
67
|
blocks: List[Block] = Field(..., description="Memory blocks contained in the agent's in-context memory")
|
68
|
+
file_blocks: List[Block] = Field(
|
69
|
+
default_factory=list, description="Blocks representing the agent's in-context memory of an attached file"
|
70
|
+
)
|
68
71
|
|
69
72
|
# Memory.template is a Jinja2 template for compiling memory module into a prompt string.
|
70
73
|
prompt_template: str = Field(
|
@@ -96,7 +99,7 @@ class Memory(BaseModel, validate_assignment=True):
|
|
96
99
|
Template(prompt_template)
|
97
100
|
|
98
101
|
# Validate compatibility with current memory structure
|
99
|
-
Template(prompt_template).render(blocks=self.blocks)
|
102
|
+
Template(prompt_template).render(blocks=self.blocks, file_blocks=self.file_blocks)
|
100
103
|
|
101
104
|
# If we get here, the template is valid and compatible
|
102
105
|
self.prompt_template = prompt_template
|
@@ -108,7 +111,7 @@ class Memory(BaseModel, validate_assignment=True):
|
|
108
111
|
def compile(self) -> str:
|
109
112
|
"""Generate a string representation of the memory in-context using the Jinja2 template"""
|
110
113
|
template = Template(self.prompt_template)
|
111
|
-
return template.render(blocks=self.blocks)
|
114
|
+
return template.render(blocks=self.blocks, file_blocks=self.file_blocks)
|
112
115
|
|
113
116
|
def list_block_labels(self) -> List[str]:
|
114
117
|
"""Return a list of the block names held inside the memory object"""
|
letta/schemas/message.py
CHANGED
@@ -36,6 +36,7 @@ from letta.schemas.letta_message_content import (
|
|
36
36
|
ReasoningContent,
|
37
37
|
RedactedReasoningContent,
|
38
38
|
TextContent,
|
39
|
+
ToolReturnContent,
|
39
40
|
get_letta_message_content_union_str_json_schema,
|
40
41
|
)
|
41
42
|
from letta.system import unpack_message
|
@@ -382,6 +383,7 @@ class Message(BaseMessage):
|
|
382
383
|
|
383
384
|
try:
|
384
385
|
function_return = parse_json(text_content)
|
386
|
+
text_content = str(function_return.get("message", text_content))
|
385
387
|
status = function_return["status"]
|
386
388
|
if status == "OK":
|
387
389
|
status_enum = "success"
|
@@ -654,6 +656,8 @@ class Message(BaseMessage):
|
|
654
656
|
parse_content_parts = False
|
655
657
|
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
656
658
|
text_content = self.content[0].text
|
659
|
+
elif self.content and len(self.content) == 1 and isinstance(self.content[0], ToolReturnContent):
|
660
|
+
text_content = self.content[0].content
|
657
661
|
# Otherwise, check if we have TextContent and multiple other parts
|
658
662
|
elif self.content and len(self.content) > 1:
|
659
663
|
text = [content for content in self.content if isinstance(content, TextContent)]
|
@@ -866,6 +870,8 @@ class Message(BaseMessage):
|
|
866
870
|
# role: str ('user' or 'model')
|
867
871
|
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
868
872
|
text_content = self.content[0].text
|
873
|
+
elif self.content and len(self.content) == 1 and isinstance(self.content[0], ToolReturnContent):
|
874
|
+
text_content = self.content[0].content
|
869
875
|
else:
|
870
876
|
text_content = None
|
871
877
|
|
@@ -1000,6 +1006,8 @@ class Message(BaseMessage):
|
|
1000
1006
|
# embedded function calls in multi-turn conversation become more clear
|
1001
1007
|
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
1002
1008
|
text_content = self.content[0].text
|
1009
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], ToolReturnContent):
|
1010
|
+
text_content = self.content[0].content
|
1003
1011
|
else:
|
1004
1012
|
text_content = None
|
1005
1013
|
if self.role == "system":
|
@@ -1101,3 +1109,4 @@ class ToolReturn(BaseModel):
|
|
1101
1109
|
status: Literal["success", "error"] = Field(..., description="The status of the tool call")
|
1102
1110
|
stdout: Optional[List[str]] = Field(None, description="Captured stdout (e.g. prints, logs) from the tool invocation")
|
1103
1111
|
stderr: Optional[List[str]] = Field(None, description="Captured stderr from the tool invocation")
|
1112
|
+
# func_return: Optional[Any] = Field(None, description="The function return object")
|
letta/schemas/openai/openai.py
CHANGED
@@ -47,12 +47,6 @@ class OpenAIMessage(BaseModel):
|
|
47
47
|
metadata: Optional[Dict] = Field(None, description="Metadata associated with the message.")
|
48
48
|
|
49
49
|
|
50
|
-
class MessageFile(BaseModel):
|
51
|
-
id: str
|
52
|
-
object: str = "thread.message.file"
|
53
|
-
created_at: int # unix timestamp
|
54
|
-
|
55
|
-
|
56
50
|
class OpenAIThread(BaseModel):
|
57
51
|
"""Represents an OpenAI thread (equivalent to Letta agent)"""
|
58
52
|
|
letta/schemas/providers.py
CHANGED
@@ -86,7 +86,7 @@ class Provider(ProviderBase):
|
|
86
86
|
return f"{base_name}/{model_name}"
|
87
87
|
|
88
88
|
def cast_to_subtype(self):
|
89
|
-
match
|
89
|
+
match self.provider_type:
|
90
90
|
case ProviderType.letta:
|
91
91
|
return LettaProvider(**self.model_dump(exclude_none=True))
|
92
92
|
case ProviderType.openai:
|
@@ -869,6 +869,38 @@ class OllamaProvider(OpenAIProvider):
|
|
869
869
|
..., description="Default prompt formatter (aka model wrapper) to use on a /completions style API."
|
870
870
|
)
|
871
871
|
|
872
|
+
async def list_llm_models_async(self) -> List[LLMConfig]:
|
873
|
+
"""Async version of list_llm_models below"""
|
874
|
+
endpoint = f"{self.base_url}/api/tags"
|
875
|
+
|
876
|
+
import aiohttp
|
877
|
+
|
878
|
+
async with aiohttp.ClientSession() as session:
|
879
|
+
async with session.get(endpoint) as response:
|
880
|
+
if response.status != 200:
|
881
|
+
raise Exception(f"Failed to list Ollama models: {response.text}")
|
882
|
+
response_json = await response.json()
|
883
|
+
|
884
|
+
configs = []
|
885
|
+
for model in response_json["models"]:
|
886
|
+
context_window = self.get_model_context_window(model["name"])
|
887
|
+
if context_window is None:
|
888
|
+
print(f"Ollama model {model['name']} has no context window")
|
889
|
+
continue
|
890
|
+
configs.append(
|
891
|
+
LLMConfig(
|
892
|
+
model=model["name"],
|
893
|
+
model_endpoint_type="ollama",
|
894
|
+
model_endpoint=self.base_url,
|
895
|
+
model_wrapper=self.default_prompt_formatter,
|
896
|
+
context_window=context_window,
|
897
|
+
handle=self.get_handle(model["name"]),
|
898
|
+
provider_name=self.name,
|
899
|
+
provider_category=self.provider_category,
|
900
|
+
)
|
901
|
+
)
|
902
|
+
return configs
|
903
|
+
|
872
904
|
def list_llm_models(self) -> List[LLMConfig]:
|
873
905
|
# https://github.com/ollama/ollama/blob/main/docs/api.md#list-local-models
|
874
906
|
import requests
|
@@ -1005,9 +1037,6 @@ class GroqProvider(OpenAIProvider):
|
|
1005
1037
|
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
1006
1038
|
return []
|
1007
1039
|
|
1008
|
-
def get_model_context_window_size(self, model_name: str):
|
1009
|
-
raise NotImplementedError
|
1010
|
-
|
1011
1040
|
|
1012
1041
|
class TogetherProvider(OpenAIProvider):
|
1013
1042
|
"""TogetherAI provider that uses the /completions API
|
letta/schemas/tool.py
CHANGED
@@ -11,10 +11,9 @@ from letta.constants import (
|
|
11
11
|
LETTA_VOICE_TOOL_MODULE_NAME,
|
12
12
|
MCP_TOOL_TAG_NAME_PREFIX,
|
13
13
|
)
|
14
|
-
from letta.functions.ast_parsers import
|
14
|
+
from letta.functions.ast_parsers import get_function_name_and_docstring
|
15
15
|
from letta.functions.composio_helpers import generate_composio_tool_wrapper
|
16
16
|
from letta.functions.functions import derive_openai_json_schema, get_json_schema_from_module
|
17
|
-
from letta.functions.helpers import generate_langchain_tool_wrapper, generate_mcp_tool_wrapper, generate_model_from_args_json_schema
|
18
17
|
from letta.functions.mcp_client.types import MCPTool
|
19
18
|
from letta.functions.schema_generator import (
|
20
19
|
generate_schema_from_args_schema_v2,
|
@@ -71,31 +70,30 @@ class Tool(BaseTool):
|
|
71
70
|
"""
|
72
71
|
Refresh name, description, source_code, and json_schema.
|
73
72
|
"""
|
74
|
-
|
75
|
-
|
73
|
+
from letta.functions.helpers import generate_model_from_args_json_schema
|
74
|
+
|
75
|
+
if self.tool_type is ToolType.CUSTOM:
|
76
76
|
if not self.source_code:
|
77
77
|
error_msg = f"Custom tool with id={self.id} is missing source_code field."
|
78
78
|
logger.error(error_msg)
|
79
79
|
raise ValueError(error_msg)
|
80
80
|
|
81
81
|
# Always derive json_schema for freshest possible json_schema
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
error_msg = f"Failed to derive json schema for tool with id={self.id} name={self.name}. Error: {str(e)}"
|
98
|
-
logger.error(error_msg)
|
82
|
+
if self.args_json_schema is not None:
|
83
|
+
name, description = get_function_name_and_docstring(self.source_code, self.name)
|
84
|
+
args_schema = generate_model_from_args_json_schema(self.args_json_schema)
|
85
|
+
self.json_schema = generate_schema_from_args_schema_v2(
|
86
|
+
args_schema=args_schema,
|
87
|
+
name=name,
|
88
|
+
description=description,
|
89
|
+
append_heartbeat=False,
|
90
|
+
)
|
91
|
+
else:
|
92
|
+
try:
|
93
|
+
self.json_schema = derive_openai_json_schema(source_code=self.source_code)
|
94
|
+
except Exception as e:
|
95
|
+
error_msg = f"Failed to derive json schema for tool with id={self.id} name={self.name}. Error: {str(e)}"
|
96
|
+
logger.error(error_msg)
|
99
97
|
elif self.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MEMORY_CORE, ToolType.LETTA_SLEEPTIME_CORE}:
|
100
98
|
# If it's letta core tool, we generate the json_schema on the fly here
|
101
99
|
self.json_schema = get_json_schema_from_module(module_name=LETTA_CORE_TOOL_MODULE_NAME, function_name=self.name)
|
@@ -108,6 +106,9 @@ class Tool(BaseTool):
|
|
108
106
|
elif self.tool_type in {ToolType.LETTA_BUILTIN}:
|
109
107
|
# If it's letta voice tool, we generate the json_schema on the fly here
|
110
108
|
self.json_schema = get_json_schema_from_module(module_name=LETTA_BUILTIN_TOOL_MODULE_NAME, function_name=self.name)
|
109
|
+
elif self.tool_type in {ToolType.EXTERNAL_COMPOSIO}:
|
110
|
+
# Composio schemas handled separately
|
111
|
+
pass
|
111
112
|
|
112
113
|
# At this point, we need to validate that at least json_schema is populated
|
113
114
|
if not self.json_schema:
|
@@ -146,6 +147,8 @@ class ToolCreate(LettaBase):
|
|
146
147
|
|
147
148
|
@classmethod
|
148
149
|
def from_mcp(cls, mcp_server_name: str, mcp_tool: MCPTool) -> "ToolCreate":
|
150
|
+
from letta.functions.helpers import generate_mcp_tool_wrapper
|
151
|
+
|
149
152
|
# Pass the MCP tool to the schema generator
|
150
153
|
json_schema = generate_tool_schema_for_mcp(mcp_tool=mcp_tool)
|
151
154
|
|
@@ -218,6 +221,8 @@ class ToolCreate(LettaBase):
|
|
218
221
|
Returns:
|
219
222
|
Tool: A Letta Tool initialized with attributes derived from the provided LangChain BaseTool object.
|
220
223
|
"""
|
224
|
+
from letta.functions.helpers import generate_langchain_tool_wrapper
|
225
|
+
|
221
226
|
description = langchain_tool.description
|
222
227
|
source_type = "python"
|
223
228
|
tags = ["langchain"]
|