letta-nightly 0.8.15.dev20250719104256__py3-none-any.whl → 0.8.16.dev20250721070720__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 +1 -1
- letta/agent.py +27 -11
- letta/agents/helpers.py +1 -1
- letta/agents/letta_agent.py +518 -322
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +15 -17
- letta/client/client.py +3 -3
- letta/constants.py +5 -0
- letta/embeddings.py +0 -2
- letta/errors.py +8 -0
- letta/functions/function_sets/base.py +3 -3
- letta/functions/helpers.py +2 -3
- letta/groups/sleeptime_multi_agent.py +0 -1
- letta/helpers/composio_helpers.py +2 -2
- letta/helpers/converters.py +1 -1
- letta/helpers/pinecone_utils.py +8 -0
- letta/helpers/tool_rule_solver.py +13 -18
- letta/llm_api/aws_bedrock.py +16 -2
- letta/llm_api/cohere.py +1 -1
- letta/llm_api/openai_client.py +1 -1
- letta/local_llm/grammars/gbnf_grammar_generator.py +1 -1
- letta/local_llm/llm_chat_completion_wrappers/zephyr.py +14 -14
- letta/local_llm/utils.py +1 -2
- letta/orm/agent.py +3 -3
- letta/orm/block.py +4 -4
- letta/orm/files_agents.py +0 -1
- letta/orm/identity.py +2 -0
- letta/orm/mcp_server.py +0 -2
- letta/orm/message.py +140 -14
- letta/orm/organization.py +5 -5
- letta/orm/passage.py +4 -4
- letta/orm/source.py +1 -1
- letta/orm/sqlalchemy_base.py +61 -39
- letta/orm/step.py +2 -0
- letta/otel/db_pool_monitoring.py +308 -0
- letta/otel/metric_registry.py +94 -1
- letta/otel/sqlalchemy_instrumentation.py +548 -0
- letta/otel/sqlalchemy_instrumentation_integration.py +124 -0
- letta/otel/tracing.py +37 -1
- letta/schemas/agent.py +0 -3
- letta/schemas/agent_file.py +283 -0
- letta/schemas/block.py +0 -3
- letta/schemas/file.py +28 -26
- letta/schemas/letta_message.py +15 -4
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +31 -26
- letta/schemas/openai/chat_completion_response.py +0 -1
- letta/schemas/providers.py +20 -0
- letta/schemas/source.py +11 -13
- letta/schemas/step.py +12 -0
- letta/schemas/tool.py +0 -4
- letta/serialize_schemas/marshmallow_agent.py +14 -1
- letta/serialize_schemas/marshmallow_block.py +23 -1
- letta/serialize_schemas/marshmallow_message.py +1 -3
- letta/serialize_schemas/marshmallow_tool.py +23 -1
- letta/server/db.py +110 -6
- letta/server/rest_api/app.py +85 -73
- letta/server/rest_api/routers/v1/agents.py +68 -53
- letta/server/rest_api/routers/v1/blocks.py +2 -2
- letta/server/rest_api/routers/v1/jobs.py +3 -0
- letta/server/rest_api/routers/v1/organizations.py +2 -2
- letta/server/rest_api/routers/v1/sources.py +18 -2
- letta/server/rest_api/routers/v1/tools.py +11 -12
- letta/server/rest_api/routers/v1/users.py +1 -1
- letta/server/rest_api/streaming_response.py +13 -5
- letta/server/rest_api/utils.py +8 -25
- letta/server/server.py +11 -4
- letta/server/ws_api/server.py +2 -2
- letta/services/agent_file_manager.py +616 -0
- letta/services/agent_manager.py +133 -46
- letta/services/block_manager.py +38 -17
- letta/services/file_manager.py +106 -21
- letta/services/file_processor/file_processor.py +93 -0
- letta/services/files_agents_manager.py +28 -0
- letta/services/group_manager.py +4 -5
- letta/services/helpers/agent_manager_helper.py +57 -9
- letta/services/identity_manager.py +22 -0
- letta/services/job_manager.py +210 -91
- letta/services/llm_batch_manager.py +9 -6
- letta/services/mcp/stdio_client.py +1 -2
- letta/services/mcp_manager.py +0 -1
- letta/services/message_manager.py +49 -26
- letta/services/passage_manager.py +0 -1
- letta/services/provider_manager.py +1 -1
- letta/services/source_manager.py +114 -5
- letta/services/step_manager.py +36 -4
- letta/services/telemetry_manager.py +9 -2
- letta/services/tool_executor/builtin_tool_executor.py +5 -1
- letta/services/tool_executor/core_tool_executor.py +3 -3
- letta/services/tool_manager.py +95 -20
- letta/services/user_manager.py +4 -12
- letta/settings.py +23 -6
- letta/system.py +1 -1
- letta/utils.py +26 -2
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/METADATA +3 -2
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/RECORD +99 -94
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/entry_points.txt +0 -0
@@ -100,7 +100,6 @@ async def execute_tool_wrapper(params: ToolExecutionParams) -> tuple[str, ToolEx
|
|
100
100
|
# TODO: Limitations ->
|
101
101
|
# TODO: Only works with anthropic for now
|
102
102
|
class LettaAgentBatch(BaseAgent):
|
103
|
-
|
104
103
|
def __init__(
|
105
104
|
self,
|
106
105
|
message_manager: MessageManager,
|
@@ -516,7 +515,7 @@ class LettaAgentBatch(BaseAgent):
|
|
516
515
|
for agent_id, new_msgs in msg_map.items():
|
517
516
|
ast = ctx.agent_state_map[agent_id]
|
518
517
|
if not ast.message_buffer_autoclear:
|
519
|
-
await self.agent_manager.
|
518
|
+
await self.agent_manager.update_message_ids_async(
|
520
519
|
agent_id=agent_id,
|
521
520
|
message_ids=ast.message_ids + [m.id for m in new_msgs],
|
522
521
|
actor=self.actor,
|
letta/agents/voice_agent.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
import asyncio
|
2
1
|
import json
|
3
2
|
import uuid
|
4
3
|
from datetime import datetime, timedelta, timezone
|
@@ -299,7 +298,7 @@ class VoiceAgent(BaseAgent):
|
|
299
298
|
in_context_messages=in_context_messages, new_letta_messages=new_letta_messages
|
300
299
|
)
|
301
300
|
|
302
|
-
await self.agent_manager.
|
301
|
+
await self.agent_manager.update_message_ids_async(
|
303
302
|
agent_id=self.agent_id, message_ids=[m.id for m in new_in_context_messages], actor=self.actor
|
304
303
|
)
|
305
304
|
|
@@ -308,18 +307,17 @@ class VoiceAgent(BaseAgent):
|
|
308
307
|
in_context_messages: List[Message],
|
309
308
|
agent_state: AgentState,
|
310
309
|
) -> List[Message]:
|
311
|
-
self.num_messages
|
312
|
-
(
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
(
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
)
|
310
|
+
if not self.num_messages:
|
311
|
+
self.num_messages = await self.message_manager.size_async(
|
312
|
+
agent_id=agent_state.id,
|
313
|
+
actor=self.actor,
|
314
|
+
)
|
315
|
+
if not self.num_archival_memories:
|
316
|
+
self.num_archival_memories = await self.passage_manager.agent_passage_size_async(
|
317
|
+
agent_id=agent_state.id,
|
318
|
+
actor=self.actor,
|
319
|
+
)
|
320
|
+
|
323
321
|
return await super()._rebuild_memory_async(
|
324
322
|
in_context_messages, agent_state, num_messages=self.num_messages, num_archival_memories=self.num_archival_memories
|
325
323
|
)
|
@@ -379,19 +377,19 @@ class VoiceAgent(BaseAgent):
|
|
379
377
|
"type": ["array", "null"],
|
380
378
|
"items": {"type": "string"},
|
381
379
|
"description": (
|
382
|
-
"Extra keywords (e.g., order ID, place name).
|
380
|
+
"Extra keywords (e.g., order ID, place name). Use *null* when the utterance is already specific."
|
383
381
|
),
|
384
382
|
},
|
385
383
|
"start_minutes_ago": {
|
386
384
|
"type": ["integer", "null"],
|
387
385
|
"description": (
|
388
|
-
"Newer bound of the time window, in minutes ago.
|
386
|
+
"Newer bound of the time window, in minutes ago. Use *null* if no lower bound is needed."
|
389
387
|
),
|
390
388
|
},
|
391
389
|
"end_minutes_ago": {
|
392
390
|
"type": ["integer", "null"],
|
393
391
|
"description": (
|
394
|
-
"Older bound of the time window, in minutes ago.
|
392
|
+
"Older bound of the time window, in minutes ago. Use *null* if no upper bound is needed."
|
395
393
|
),
|
396
394
|
},
|
397
395
|
},
|
letta/client/client.py
CHANGED
@@ -568,8 +568,8 @@ class RESTClient(AbstractClient):
|
|
568
568
|
tool_names += BASE_MEMORY_TOOLS
|
569
569
|
tool_ids += [self.get_tool_id(tool_name=name) for name in tool_names]
|
570
570
|
|
571
|
-
assert embedding_config or self._default_embedding_config,
|
572
|
-
assert llm_config or self._default_llm_config,
|
571
|
+
assert embedding_config or self._default_embedding_config, "Embedding config must be provided"
|
572
|
+
assert llm_config or self._default_llm_config, "LLM config must be provided"
|
573
573
|
|
574
574
|
# TODO: This should not happen here, we need to have clear separation between create/add blocks
|
575
575
|
# TODO: This is insanely hacky and a result of allowing free-floating blocks
|
@@ -1392,7 +1392,7 @@ class RESTClient(AbstractClient):
|
|
1392
1392
|
Returns:
|
1393
1393
|
source (Source): Created source
|
1394
1394
|
"""
|
1395
|
-
assert embedding_config or self._default_embedding_config,
|
1395
|
+
assert embedding_config or self._default_embedding_config, "Must specify embedding_config for source"
|
1396
1396
|
source_create = SourceCreate(name=name, embedding_config=embedding_config or self._default_embedding_config)
|
1397
1397
|
payload = source_create.model_dump()
|
1398
1398
|
response = requests.post(f"{self.base_url}/{self.api_prefix}/sources", json=payload, headers=self.headers)
|
letta/constants.py
CHANGED
@@ -378,3 +378,8 @@ PINECONE_MAX_RETRY_ATTEMPTS = 3
|
|
378
378
|
PINECONE_RETRY_BASE_DELAY = 1.0 # seconds
|
379
379
|
PINECONE_RETRY_MAX_DELAY = 60.0 # seconds
|
380
380
|
PINECONE_RETRY_BACKOFF_FACTOR = 2.0
|
381
|
+
PINECONE_THROTTLE_DELAY = 0.75 # seconds base delay between batches
|
382
|
+
|
383
|
+
# builtin web search
|
384
|
+
WEB_SEARCH_MODEL_ENV_VAR_NAME = "LETTA_BUILTIN_WEBSEARCH_OPENAI_MODEL_NAME"
|
385
|
+
WEB_SEARCH_MODEL_ENV_VAR_DEFAULT_VALUE = "gpt-4.1-mini-2025-04-14"
|
letta/embeddings.py
CHANGED
@@ -190,7 +190,6 @@ class GoogleEmbeddings:
|
|
190
190
|
|
191
191
|
|
192
192
|
class GoogleVertexEmbeddings:
|
193
|
-
|
194
193
|
def __init__(self, model: str, project_id: str, region: str):
|
195
194
|
from google import genai
|
196
195
|
|
@@ -203,7 +202,6 @@ class GoogleVertexEmbeddings:
|
|
203
202
|
|
204
203
|
|
205
204
|
class OpenAIEmbeddings:
|
206
|
-
|
207
205
|
def __init__(self, api_key: str, model: str, base_url: str):
|
208
206
|
if base_url:
|
209
207
|
self.client = OpenAI(api_key=api_key, base_url=base_url)
|
letta/errors.py
CHANGED
@@ -219,3 +219,11 @@ class HandleNotFoundError(LettaError):
|
|
219
219
|
message=f"Handle {handle} not found, must be one of {available_handles}",
|
220
220
|
code=ErrorCode.NOT_FOUND,
|
221
221
|
)
|
222
|
+
|
223
|
+
|
224
|
+
class AgentFileExportError(Exception):
|
225
|
+
"""Exception raised during agent file export operations"""
|
226
|
+
|
227
|
+
|
228
|
+
class AgentFileImportError(Exception):
|
229
|
+
"""Exception raised during agent file import operations"""
|
@@ -42,7 +42,7 @@ def conversation_search(self: "Agent", query: str, page: Optional[int] = 0) -> O
|
|
42
42
|
try:
|
43
43
|
page = int(page)
|
44
44
|
except:
|
45
|
-
raise ValueError(
|
45
|
+
raise ValueError("'page' argument must be an integer")
|
46
46
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
47
47
|
# TODO: add paging by page number. currently cursor only works with strings.
|
48
48
|
# original: start=page * count
|
@@ -55,7 +55,7 @@ def conversation_search(self: "Agent", query: str, page: Optional[int] = 0) -> O
|
|
55
55
|
total = len(messages)
|
56
56
|
num_pages = math.ceil(total / count) - 1 # 0 index
|
57
57
|
if len(messages) == 0:
|
58
|
-
results_str =
|
58
|
+
results_str = "No results found."
|
59
59
|
else:
|
60
60
|
results_pref = f"Showing {len(messages)} of {total} results (page {page}/{num_pages}):"
|
61
61
|
results_formatted = [message.content[0].text for message in messages]
|
@@ -103,7 +103,7 @@ def archival_memory_search(self: "Agent", query: str, page: Optional[int] = 0, s
|
|
103
103
|
try:
|
104
104
|
page = int(page)
|
105
105
|
except:
|
106
|
-
raise ValueError(
|
106
|
+
raise ValueError("'page' argument must be an integer")
|
107
107
|
count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
|
108
108
|
|
109
109
|
try:
|
letta/functions/helpers.py
CHANGED
@@ -23,7 +23,6 @@ from letta.settings import settings
|
|
23
23
|
|
24
24
|
# TODO needed?
|
25
25
|
def generate_mcp_tool_wrapper(mcp_tool_name: str) -> tuple[str, str]:
|
26
|
-
|
27
26
|
wrapper_function_str = f"""\
|
28
27
|
def {mcp_tool_name}(**kwargs):
|
29
28
|
raise RuntimeError("Something went wrong - we should never be using the persisted source code for MCP. Please reach out to Letta team")
|
@@ -46,7 +45,7 @@ def generate_langchain_tool_wrapper(
|
|
46
45
|
_assert_all_classes_are_imported(tool, additional_imports_module_attr_map)
|
47
46
|
|
48
47
|
tool_instantiation = f"tool = {generate_imported_tool_instantiation_call_str(tool)}"
|
49
|
-
run_call =
|
48
|
+
run_call = "return tool._run(**kwargs)"
|
50
49
|
func_name = humps.decamelize(tool_name)
|
51
50
|
|
52
51
|
# Combine all parts into the wrapper function
|
@@ -240,7 +239,7 @@ async def async_execute_send_message_to_agent(
|
|
240
239
|
try:
|
241
240
|
server.agent_manager.get_agent_by_id(agent_id=other_agent_id, actor=sender_agent.user)
|
242
241
|
except NoResultFound:
|
243
|
-
raise ValueError(f"Target agent {other_agent_id} either does not exist or is not in org
|
242
|
+
raise ValueError(f"Target agent {other_agent_id} either does not exist or is not in org ({sender_agent.user.organization_id}).")
|
244
243
|
|
245
244
|
# 2. Use your async retry logic
|
246
245
|
return await _async_send_message_with_retries(
|
@@ -10,7 +10,7 @@ def get_composio_api_key(actor: User, logger: Optional[Logger] = None) -> Option
|
|
10
10
|
api_keys = SandboxConfigManager().list_sandbox_env_vars_by_key(key="COMPOSIO_API_KEY", actor=actor)
|
11
11
|
if not api_keys:
|
12
12
|
if logger:
|
13
|
-
logger.debug(
|
13
|
+
logger.debug("No API keys found for Composio. Defaulting to the environment variable...")
|
14
14
|
if tool_settings.composio_api_key:
|
15
15
|
return tool_settings.composio_api_key
|
16
16
|
else:
|
@@ -26,7 +26,7 @@ async def get_composio_api_key_async(actor: User, logger: Optional[Logger] = Non
|
|
26
26
|
api_keys = await SandboxConfigManager().list_sandbox_env_vars_by_key_async(key="COMPOSIO_API_KEY", actor=actor)
|
27
27
|
if not api_keys:
|
28
28
|
if logger:
|
29
|
-
logger.debug(
|
29
|
+
logger.debug("No API keys found for Composio. Defaulting to the environment variable...")
|
30
30
|
if tool_settings.composio_api_key:
|
31
31
|
return tool_settings.composio_api_key
|
32
32
|
else:
|
letta/helpers/converters.py
CHANGED
@@ -245,7 +245,7 @@ def deserialize_message_content(data: Optional[List[Dict]]) -> List[MessageConte
|
|
245
245
|
if content_type == MessageContentType.text:
|
246
246
|
content = TextContent(**item)
|
247
247
|
elif content_type == MessageContentType.image:
|
248
|
-
assert item["source"]["type"] == ImageSourceType.letta, f
|
248
|
+
assert item["source"]["type"] == ImageSourceType.letta, f"Invalid image source type: {item['source']['type']}"
|
249
249
|
content = ImageContent(**item)
|
250
250
|
elif content_type == MessageContentType.tool_call:
|
251
251
|
content = ToolCallContent(**item)
|
letta/helpers/pinecone_utils.py
CHANGED
@@ -31,6 +31,7 @@ from letta.constants import (
|
|
31
31
|
PINECONE_RETRY_BASE_DELAY,
|
32
32
|
PINECONE_RETRY_MAX_DELAY,
|
33
33
|
PINECONE_TEXT_FIELD_NAME,
|
34
|
+
PINECONE_THROTTLE_DELAY,
|
34
35
|
)
|
35
36
|
from letta.log import get_logger
|
36
37
|
from letta.schemas.user import User
|
@@ -256,6 +257,13 @@ async def upsert_records_to_pinecone_index(records: List[dict], actor: User):
|
|
256
257
|
logger.debug(f"[Pinecone] Upserting batch {batch_num}/{total_batches} with {len(batch)} records")
|
257
258
|
await dense_index.upsert_records(actor.organization_id, batch)
|
258
259
|
|
260
|
+
# throttle between batches (except the last one)
|
261
|
+
if batch_num < total_batches:
|
262
|
+
jitter = random.uniform(0, PINECONE_THROTTLE_DELAY * 0.2) # ±20% jitter
|
263
|
+
throttle_delay = PINECONE_THROTTLE_DELAY + jitter
|
264
|
+
logger.debug(f"[Pinecone] Throttling for {throttle_delay:.3f}s before next batch")
|
265
|
+
await asyncio.sleep(throttle_delay)
|
266
|
+
|
259
267
|
logger.info(f"[Pinecone] Successfully upserted all {len(records)} records in {total_batches} batches")
|
260
268
|
|
261
269
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import List, Optional,
|
1
|
+
from typing import List, Optional, Union
|
2
2
|
|
3
3
|
from pydantic import BaseModel, Field
|
4
4
|
|
@@ -107,25 +107,20 @@ class ToolRulesSolver(BaseModel):
|
|
107
107
|
self.tool_call_history.clear()
|
108
108
|
|
109
109
|
def get_allowed_tool_names(
|
110
|
-
self, available_tools:
|
110
|
+
self, available_tools: set[str], error_on_empty: bool = True, last_function_response: str | None = None
|
111
111
|
) -> List[str]:
|
112
|
-
"""Get a list of tool names allowed based on the last tool called.
|
112
|
+
"""Get a list of tool names allowed based on the last tool called.
|
113
|
+
|
114
|
+
The logic is as follows:
|
115
|
+
1. if there are no previous tool calls and we have InitToolRules, those are the only options for the first tool call
|
116
|
+
2. else we take the intersection of the Parent/Child/Conditional/MaxSteps as the options
|
117
|
+
3. Continue/Terminal/RequiredBeforeExit rules are applied in the agent loop flow, not to restrict tools
|
118
|
+
"""
|
113
119
|
# TODO: This piece of code here is quite ugly and deserves a refactor
|
114
|
-
# TODO: There's some weird logic encoded here:
|
115
|
-
# TODO: -> This only takes into consideration Init, and a set of Child/Conditional/MaxSteps tool rules
|
116
|
-
# TODO: -> Init tool rules outputs are treated additively, Child/Conditional/MaxSteps are intersection based
|
117
120
|
# TODO: -> Tool rules should probably be refactored to take in a set of tool names?
|
118
|
-
|
119
|
-
|
120
|
-
if self.init_tool_rules:
|
121
|
-
# If there are init tool rules, only return those defined in the init tool rules
|
122
|
-
return [rule.tool_name for rule in self.init_tool_rules]
|
123
|
-
else:
|
124
|
-
# Otherwise, return all tools besides those constrained by parent tool rules
|
125
|
-
available_tools = available_tools - set.union(set(), *(set(rule.children) for rule in self.parent_tool_rules))
|
126
|
-
return list(available_tools)
|
121
|
+
if not self.tool_call_history and self.init_tool_rules:
|
122
|
+
return [rule.tool_name for rule in self.init_tool_rules]
|
127
123
|
else:
|
128
|
-
# Collect valid tools from all child-based rules
|
129
124
|
valid_tool_sets = []
|
130
125
|
for rule in self.child_based_tool_rules + self.parent_tool_rules:
|
131
126
|
tools = rule.get_valid_tools(self.tool_call_history, available_tools, last_function_response)
|
@@ -151,11 +146,11 @@ class ToolRulesSolver(BaseModel):
|
|
151
146
|
"""Check if the tool is defined as a continue tool in the tool rules."""
|
152
147
|
return any(rule.tool_name == tool_name for rule in self.continue_tool_rules)
|
153
148
|
|
154
|
-
def has_required_tools_been_called(self, available_tools:
|
149
|
+
def has_required_tools_been_called(self, available_tools: set[str]) -> bool:
|
155
150
|
"""Check if all required-before-exit tools have been called."""
|
156
151
|
return len(self.get_uncalled_required_tools(available_tools=available_tools)) == 0
|
157
152
|
|
158
|
-
def get_uncalled_required_tools(self, available_tools:
|
153
|
+
def get_uncalled_required_tools(self, available_tools: set[str]) -> List[str]:
|
159
154
|
"""Get the list of required-before-exit tools that have not been called yet."""
|
160
155
|
if not self.required_before_exit_tool_rules:
|
161
156
|
return [] # No required tools means no uncalled tools
|
letta/llm_api/aws_bedrock.py
CHANGED
@@ -41,22 +41,36 @@ def get_bedrock_client(
|
|
41
41
|
return bedrock
|
42
42
|
|
43
43
|
|
44
|
-
def bedrock_get_model_list(
|
44
|
+
def bedrock_get_model_list(
|
45
|
+
region_name: str,
|
46
|
+
access_key_id: Optional[str] = None,
|
47
|
+
secret_access_key: Optional[str] = None,
|
48
|
+
) -> List[dict]:
|
45
49
|
"""
|
46
50
|
Get list of available models from Bedrock.
|
47
51
|
|
48
52
|
Args:
|
49
53
|
region_name: AWS region name
|
54
|
+
access_key_id: Optional AWS access key ID
|
55
|
+
secret_access_key: Optional AWS secret access key
|
56
|
+
|
57
|
+
TODO: Implement model_provider and output_modality filtering
|
50
58
|
model_provider: Optional provider name to filter models. If None, returns all models.
|
51
59
|
output_modality: Output modality to filter models. Defaults to "text".
|
52
60
|
|
53
61
|
Returns:
|
54
62
|
List of model summaries
|
63
|
+
|
55
64
|
"""
|
56
65
|
import boto3
|
57
66
|
|
58
67
|
try:
|
59
|
-
bedrock = boto3.client(
|
68
|
+
bedrock = boto3.client(
|
69
|
+
"bedrock",
|
70
|
+
region_name=region_name,
|
71
|
+
aws_access_key_id=access_key_id,
|
72
|
+
aws_secret_access_key=secret_access_key,
|
73
|
+
)
|
60
74
|
response = bedrock.list_inference_profiles()
|
61
75
|
return response["inferenceProfileSummaries"]
|
62
76
|
except Exception as e:
|
letta/llm_api/cohere.py
CHANGED
@@ -307,7 +307,7 @@ def cohere_chat_completions_request(
|
|
307
307
|
data = chat_completion_request.model_dump(exclude_none=True)
|
308
308
|
|
309
309
|
if "functions" in data:
|
310
|
-
raise ValueError(
|
310
|
+
raise ValueError("'functions' unexpected in Anthropic API payload")
|
311
311
|
|
312
312
|
# If tools == None, strip from the payload
|
313
313
|
if "tools" in data and data["tools"] is None:
|
letta/llm_api/openai_client.py
CHANGED
@@ -54,7 +54,7 @@ def accepts_developer_role(model: str) -> bool:
|
|
54
54
|
|
55
55
|
See: https://community.openai.com/t/developer-role-not-accepted-for-o1-o1-mini-o3-mini/1110750/7
|
56
56
|
"""
|
57
|
-
if is_openai_reasoning_model(model) and
|
57
|
+
if is_openai_reasoning_model(model) and "o1-mini" not in model or "o1-preview" in model:
|
58
58
|
return True
|
59
59
|
else:
|
60
60
|
return False
|
@@ -697,7 +697,7 @@ def generate_markdown_documentation(
|
|
697
697
|
# Indenting the fields section
|
698
698
|
documentation += f" {fields_prefix}:\n"
|
699
699
|
else:
|
700
|
-
documentation +=
|
700
|
+
documentation += " attributes:\n"
|
701
701
|
if isclass(model) and issubclass(model, BaseModel):
|
702
702
|
for name, field_type in model.__annotations__.items():
|
703
703
|
# if name == "markdown_code_block":
|
@@ -43,7 +43,7 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper):
|
|
43
43
|
|
44
44
|
# System instructions go first
|
45
45
|
assert messages[0]["role"] == "system"
|
46
|
-
prompt +=
|
46
|
+
prompt += "<|system|>"
|
47
47
|
prompt += f"\n{messages[0]['content']}"
|
48
48
|
|
49
49
|
# Next is the functions preamble
|
@@ -52,7 +52,7 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper):
|
|
52
52
|
func_str = ""
|
53
53
|
func_str += f"{schema['name']}:"
|
54
54
|
func_str += f"\n description: {schema['description']}"
|
55
|
-
func_str +=
|
55
|
+
func_str += "\n params:"
|
56
56
|
for param_k, param_v in schema["parameters"]["properties"].items():
|
57
57
|
# TODO we're ignoring type
|
58
58
|
func_str += f"\n {param_k}: {param_v['description']}"
|
@@ -60,8 +60,8 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper):
|
|
60
60
|
return func_str
|
61
61
|
|
62
62
|
# prompt += f"\nPlease select the most suitable function and parameters from the list of available functions below, based on the user's input. Provide your response in JSON format."
|
63
|
-
prompt +=
|
64
|
-
prompt +=
|
63
|
+
prompt += "\nPlease select the most suitable function and parameters from the list of available functions below, based on the ongoing conversation. Provide your response in JSON format."
|
64
|
+
prompt += "\nAvailable functions:"
|
65
65
|
if function_documentation is not None:
|
66
66
|
prompt += f"\n{function_documentation}"
|
67
67
|
else:
|
@@ -92,7 +92,7 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper):
|
|
92
92
|
prompt += f"\n<|user|>\n{message['content']}{IM_END_TOKEN}"
|
93
93
|
# prompt += f"\nUSER: {message['content']}"
|
94
94
|
elif message["role"] == "assistant":
|
95
|
-
prompt +=
|
95
|
+
prompt += "\n<|assistant|>"
|
96
96
|
if message["content"] is not None:
|
97
97
|
prompt += f"\n{message['content']}"
|
98
98
|
# prompt += f"\nASSISTANT: {message['content']}"
|
@@ -103,7 +103,7 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper):
|
|
103
103
|
elif message["role"] in ["function", "tool"]:
|
104
104
|
# TODO find a good way to add this
|
105
105
|
# prompt += f"\nASSISTANT: (function return) {message['content']}"
|
106
|
-
prompt +=
|
106
|
+
prompt += "\n<|assistant|>"
|
107
107
|
prompt += f"\nFUNCTION RETURN: {message['content']}"
|
108
108
|
# prompt += f"\nFUNCTION RETURN: {message['content']}"
|
109
109
|
continue
|
@@ -116,7 +116,7 @@ class ZephyrMistralWrapper(LLMChatCompletionWrapper):
|
|
116
116
|
|
117
117
|
if self.include_assistant_prefix:
|
118
118
|
# prompt += f"\nASSISTANT:"
|
119
|
-
prompt +=
|
119
|
+
prompt += "\n<|assistant|>"
|
120
120
|
if self.include_opening_brance_in_prefix:
|
121
121
|
prompt += "\n{"
|
122
122
|
|
@@ -214,9 +214,9 @@ class ZephyrMistralInnerMonologueWrapper(ZephyrMistralWrapper):
|
|
214
214
|
func_str = ""
|
215
215
|
func_str += f"{schema['name']}:"
|
216
216
|
func_str += f"\n description: {schema['description']}"
|
217
|
-
func_str +=
|
217
|
+
func_str += "\n params:"
|
218
218
|
if add_inner_thoughts:
|
219
|
-
func_str +=
|
219
|
+
func_str += "\n inner_thoughts: Deep inner monologue private to you only."
|
220
220
|
for param_k, param_v in schema["parameters"]["properties"].items():
|
221
221
|
# TODO we're ignoring type
|
222
222
|
func_str += f"\n {param_k}: {param_v['description']}"
|
@@ -224,8 +224,8 @@ class ZephyrMistralInnerMonologueWrapper(ZephyrMistralWrapper):
|
|
224
224
|
return func_str
|
225
225
|
|
226
226
|
# prompt += f"\nPlease select the most suitable function and parameters from the list of available functions below, based on the user's input. Provide your response in JSON format."
|
227
|
-
prompt +=
|
228
|
-
prompt +=
|
227
|
+
prompt += "\nPlease select the most suitable function and parameters from the list of available functions below, based on the ongoing conversation. Provide your response in JSON format."
|
228
|
+
prompt += "\nAvailable functions:"
|
229
229
|
if function_documentation is not None:
|
230
230
|
prompt += f"\n{function_documentation}"
|
231
231
|
else:
|
@@ -259,10 +259,10 @@ class ZephyrMistralInnerMonologueWrapper(ZephyrMistralWrapper):
|
|
259
259
|
except:
|
260
260
|
prompt += f"\n<|user|>\n{message['content']}{IM_END_TOKEN}"
|
261
261
|
elif message["role"] == "assistant":
|
262
|
-
prompt +=
|
262
|
+
prompt += "\n<|assistant|>"
|
263
263
|
# need to add the function call if there was one
|
264
264
|
inner_thoughts = message["content"]
|
265
|
-
if
|
265
|
+
if message.get("function_call"):
|
266
266
|
prompt += f"\n{create_function_call(message['function_call'], inner_thoughts=inner_thoughts)}"
|
267
267
|
elif message["role"] in ["function", "tool"]:
|
268
268
|
# TODO find a good way to add this
|
@@ -277,7 +277,7 @@ class ZephyrMistralInnerMonologueWrapper(ZephyrMistralWrapper):
|
|
277
277
|
# prompt += "\n### RESPONSE"
|
278
278
|
|
279
279
|
if self.include_assistant_prefix:
|
280
|
-
prompt +=
|
280
|
+
prompt += "\n<|assistant|>"
|
281
281
|
if self.include_opening_brance_in_prefix:
|
282
282
|
prompt += "\n{"
|
283
283
|
|
letta/local_llm/utils.py
CHANGED
@@ -76,7 +76,7 @@ def num_tokens_from_functions(functions: List[dict], model: str = "gpt-4"):
|
|
76
76
|
except KeyError:
|
77
77
|
from letta.utils import printd
|
78
78
|
|
79
|
-
printd(
|
79
|
+
printd("Warning: model not found. Using cl100k_base encoding.")
|
80
80
|
encoding = tiktoken.get_encoding("cl100k_base")
|
81
81
|
|
82
82
|
num_tokens = 0
|
@@ -238,7 +238,6 @@ def num_tokens_from_messages(messages: List[dict], model: str = "gpt-4") -> int:
|
|
238
238
|
num_tokens += tokens_per_message
|
239
239
|
for key, value in message.items():
|
240
240
|
try:
|
241
|
-
|
242
241
|
if isinstance(value, list) and key == "tool_calls":
|
243
242
|
num_tokens += num_tokens_from_tool_calls(tool_calls=value, model=model)
|
244
243
|
# special case for tool calling (list)
|
letta/orm/agent.py
CHANGED
@@ -93,7 +93,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
93
93
|
timezone: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The timezone of the agent (for the context window).")
|
94
94
|
|
95
95
|
# relationships
|
96
|
-
organization: Mapped["Organization"] = relationship("Organization", back_populates="agents")
|
96
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="agents", lazy="raise")
|
97
97
|
tool_exec_environment_variables: Mapped[List["AgentEnvironmentVariable"]] = relationship(
|
98
98
|
"AgentEnvironmentVariable",
|
99
99
|
back_populates="agent",
|
@@ -128,7 +128,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
128
128
|
groups: Mapped[List["Group"]] = relationship(
|
129
129
|
"Group",
|
130
130
|
secondary="groups_agents",
|
131
|
-
lazy="
|
131
|
+
lazy="raise",
|
132
132
|
back_populates="agents",
|
133
133
|
passive_deletes=True,
|
134
134
|
)
|
@@ -138,7 +138,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
|
|
138
138
|
viewonly=True,
|
139
139
|
back_populates="manager_agent",
|
140
140
|
)
|
141
|
-
batch_items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="agent", lazy="
|
141
|
+
batch_items: Mapped[List["LLMBatchItem"]] = relationship("LLMBatchItem", back_populates="agent", lazy="raise")
|
142
142
|
file_agents: Mapped[List["FileAgent"]] = relationship(
|
143
143
|
"FileAgent",
|
144
144
|
back_populates="agent",
|
letta/orm/block.py
CHANGED
@@ -55,11 +55,11 @@ class Block(OrganizationMixin, SqlalchemyBase):
|
|
55
55
|
__mapper_args__ = {"version_id_col": version}
|
56
56
|
|
57
57
|
# relationships
|
58
|
-
organization: Mapped[Optional["Organization"]] = relationship("Organization")
|
58
|
+
organization: Mapped[Optional["Organization"]] = relationship("Organization", lazy="raise")
|
59
59
|
agents: Mapped[List["Agent"]] = relationship(
|
60
60
|
"Agent",
|
61
61
|
secondary="blocks_agents",
|
62
|
-
lazy="
|
62
|
+
lazy="raise",
|
63
63
|
passive_deletes=True, # Ensures SQLAlchemy doesn't fetch blocks_agents rows before deleting
|
64
64
|
back_populates="core_memory",
|
65
65
|
doc="Agents associated with this block.",
|
@@ -67,14 +67,14 @@ class Block(OrganizationMixin, SqlalchemyBase):
|
|
67
67
|
identities: Mapped[List["Identity"]] = relationship(
|
68
68
|
"Identity",
|
69
69
|
secondary="identities_blocks",
|
70
|
-
lazy="
|
70
|
+
lazy="raise",
|
71
71
|
back_populates="blocks",
|
72
72
|
passive_deletes=True,
|
73
73
|
)
|
74
74
|
groups: Mapped[List["Group"]] = relationship(
|
75
75
|
"Group",
|
76
76
|
secondary="groups_blocks",
|
77
|
-
lazy="
|
77
|
+
lazy="raise",
|
78
78
|
back_populates="shared_blocks",
|
79
79
|
passive_deletes=True,
|
80
80
|
)
|
letta/orm/files_agents.py
CHANGED
@@ -96,7 +96,6 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
|
|
96
96
|
visible_content += truncated_warning
|
97
97
|
|
98
98
|
return PydanticBlock(
|
99
|
-
organization_id=self.organization_id,
|
100
99
|
value=visible_content,
|
101
100
|
label=self.file_name, # use denormalized file_name instead of self.file.file_name
|
102
101
|
read_only=True,
|
letta/orm/identity.py
CHANGED
@@ -23,6 +23,8 @@ class Identity(SqlalchemyBase, OrganizationMixin):
|
|
23
23
|
"organization_id",
|
24
24
|
name="unique_identifier_key_project_id_organization_id",
|
25
25
|
postgresql_nulls_not_distinct=True,
|
26
|
+
# For SQLite compatibility, we'll need to handle the NULL case differently
|
27
|
+
# in the service layer since SQLite doesn't support postgresql_nulls_not_distinct
|
26
28
|
),
|
27
29
|
)
|
28
30
|
|
letta/orm/mcp_server.py
CHANGED
@@ -50,5 +50,3 @@ class MCPServer(SqlalchemyBase, OrganizationMixin):
|
|
50
50
|
metadata_: Mapped[Optional[dict]] = mapped_column(
|
51
51
|
JSON, default=lambda: {}, doc="A dictionary of additional metadata for the MCP server."
|
52
52
|
)
|
53
|
-
# relationships
|
54
|
-
# organization: Mapped["Organization"] = relationship("Organization", back_populates="mcp_server", lazy="selectin")
|