letta-nightly 0.8.15.dev20250720104313__py3-none-any.whl → 0.8.16.dev20250721104533__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.
Files changed (99) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +27 -11
  3. letta/agents/helpers.py +1 -1
  4. letta/agents/letta_agent.py +518 -322
  5. letta/agents/letta_agent_batch.py +1 -2
  6. letta/agents/voice_agent.py +15 -17
  7. letta/client/client.py +3 -3
  8. letta/constants.py +5 -0
  9. letta/embeddings.py +0 -2
  10. letta/errors.py +8 -0
  11. letta/functions/function_sets/base.py +3 -3
  12. letta/functions/helpers.py +2 -3
  13. letta/groups/sleeptime_multi_agent.py +0 -1
  14. letta/helpers/composio_helpers.py +2 -2
  15. letta/helpers/converters.py +1 -1
  16. letta/helpers/pinecone_utils.py +8 -0
  17. letta/helpers/tool_rule_solver.py +13 -18
  18. letta/llm_api/aws_bedrock.py +16 -2
  19. letta/llm_api/cohere.py +1 -1
  20. letta/llm_api/openai_client.py +1 -1
  21. letta/local_llm/grammars/gbnf_grammar_generator.py +1 -1
  22. letta/local_llm/llm_chat_completion_wrappers/zephyr.py +14 -14
  23. letta/local_llm/utils.py +1 -2
  24. letta/orm/agent.py +3 -3
  25. letta/orm/block.py +4 -4
  26. letta/orm/files_agents.py +0 -1
  27. letta/orm/identity.py +2 -0
  28. letta/orm/mcp_server.py +0 -2
  29. letta/orm/message.py +140 -14
  30. letta/orm/organization.py +5 -5
  31. letta/orm/passage.py +4 -4
  32. letta/orm/source.py +1 -1
  33. letta/orm/sqlalchemy_base.py +61 -39
  34. letta/orm/step.py +2 -0
  35. letta/otel/db_pool_monitoring.py +308 -0
  36. letta/otel/metric_registry.py +94 -1
  37. letta/otel/sqlalchemy_instrumentation.py +548 -0
  38. letta/otel/sqlalchemy_instrumentation_integration.py +124 -0
  39. letta/otel/tracing.py +37 -1
  40. letta/schemas/agent.py +0 -3
  41. letta/schemas/agent_file.py +283 -0
  42. letta/schemas/block.py +0 -3
  43. letta/schemas/file.py +28 -26
  44. letta/schemas/letta_message.py +15 -4
  45. letta/schemas/memory.py +1 -1
  46. letta/schemas/message.py +31 -26
  47. letta/schemas/openai/chat_completion_response.py +0 -1
  48. letta/schemas/providers.py +20 -0
  49. letta/schemas/source.py +11 -13
  50. letta/schemas/step.py +12 -0
  51. letta/schemas/tool.py +0 -4
  52. letta/serialize_schemas/marshmallow_agent.py +14 -1
  53. letta/serialize_schemas/marshmallow_block.py +23 -1
  54. letta/serialize_schemas/marshmallow_message.py +1 -3
  55. letta/serialize_schemas/marshmallow_tool.py +23 -1
  56. letta/server/db.py +110 -6
  57. letta/server/rest_api/app.py +85 -73
  58. letta/server/rest_api/routers/v1/agents.py +68 -53
  59. letta/server/rest_api/routers/v1/blocks.py +2 -2
  60. letta/server/rest_api/routers/v1/jobs.py +3 -0
  61. letta/server/rest_api/routers/v1/organizations.py +2 -2
  62. letta/server/rest_api/routers/v1/sources.py +18 -2
  63. letta/server/rest_api/routers/v1/tools.py +11 -12
  64. letta/server/rest_api/routers/v1/users.py +1 -1
  65. letta/server/rest_api/streaming_response.py +13 -5
  66. letta/server/rest_api/utils.py +8 -25
  67. letta/server/server.py +11 -4
  68. letta/server/ws_api/server.py +2 -2
  69. letta/services/agent_file_manager.py +616 -0
  70. letta/services/agent_manager.py +133 -46
  71. letta/services/block_manager.py +38 -17
  72. letta/services/file_manager.py +106 -21
  73. letta/services/file_processor/file_processor.py +93 -0
  74. letta/services/files_agents_manager.py +28 -0
  75. letta/services/group_manager.py +4 -5
  76. letta/services/helpers/agent_manager_helper.py +57 -9
  77. letta/services/identity_manager.py +22 -0
  78. letta/services/job_manager.py +210 -91
  79. letta/services/llm_batch_manager.py +9 -6
  80. letta/services/mcp/stdio_client.py +1 -2
  81. letta/services/mcp_manager.py +0 -1
  82. letta/services/message_manager.py +49 -26
  83. letta/services/passage_manager.py +0 -1
  84. letta/services/provider_manager.py +1 -1
  85. letta/services/source_manager.py +114 -5
  86. letta/services/step_manager.py +36 -4
  87. letta/services/telemetry_manager.py +9 -2
  88. letta/services/tool_executor/builtin_tool_executor.py +5 -1
  89. letta/services/tool_executor/core_tool_executor.py +3 -3
  90. letta/services/tool_manager.py +95 -20
  91. letta/services/user_manager.py +4 -12
  92. letta/settings.py +23 -6
  93. letta/system.py +1 -1
  94. letta/utils.py +26 -2
  95. {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721104533.dist-info}/METADATA +3 -2
  96. {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721104533.dist-info}/RECORD +99 -94
  97. {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721104533.dist-info}/LICENSE +0 -0
  98. {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721104533.dist-info}/WHEEL +0 -0
  99. {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721104533.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.set_in_context_messages_async(
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,
@@ -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.set_in_context_messages_async(
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, self.num_archival_memories = await asyncio.gather(
312
- (
313
- self.message_manager.size_async(actor=self.actor, agent_id=agent_state.id)
314
- if self.num_messages is None
315
- else asyncio.sleep(0, result=self.num_messages)
316
- ),
317
- (
318
- self.passage_manager.agent_passage_size_async(actor=self.actor, agent_id=agent_state.id)
319
- if self.num_archival_memories is None
320
- else asyncio.sleep(0, result=self.num_archival_memories)
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). " "Use *null* when the utterance is already specific."
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. " "Use *null* if no lower bound is needed."
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. " "Use *null* if no upper bound is needed."
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, f"Embedding config must be provided"
572
- assert llm_config or self._default_llm_config, f"LLM config must be provided"
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, f"Must specify embedding_config for source"
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(f"'page' argument must be an integer")
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 = f"No results found."
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(f"'page' argument must be an integer")
106
+ raise ValueError("'page' argument must be an integer")
107
107
  count = RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE
108
108
 
109
109
  try:
@@ -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 = f"return tool._run(**kwargs)"
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 " f"({sender_agent.user.organization_id}).")
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(
@@ -20,7 +20,6 @@ from letta.services.message_manager import MessageManager
20
20
 
21
21
 
22
22
  class SleeptimeMultiAgent(Agent):
23
-
24
23
  def __init__(
25
24
  self,
26
25
  interface: AgentInterface,
@@ -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(f"No API keys found for Composio. Defaulting to the environment variable...")
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(f"No API keys found for Composio. Defaulting to the environment variable...")
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:
@@ -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'Invalid image source type: {item["source"]["type"]}'
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)
@@ -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, Set, Union
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: Set[str], error_on_empty: bool = False, last_function_response: Optional[str] = None
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
- # If no tool has been called yet, return InitToolRules additively
119
- if not self.tool_call_history:
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: Set[str]) -> bool:
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: Set[str]) -> List[str]:
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
@@ -41,22 +41,36 @@ def get_bedrock_client(
41
41
  return bedrock
42
42
 
43
43
 
44
- def bedrock_get_model_list(region_name: str) -> List[dict]:
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("bedrock", region_name=region_name)
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(f"'functions' unexpected in Anthropic API payload")
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:
@@ -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 not "o1-mini" in model or "o1-preview" in model:
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 += f" attributes:\n"
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 += f"<|system|>"
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 += f"\n params:"
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 += f"\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 += f"\nAvailable functions:"
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 += f"\n<|assistant|>"
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 += f"\n<|assistant|>"
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 += f"\n<|assistant|>"
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 += f"\n params:"
217
+ func_str += "\n params:"
218
218
  if add_inner_thoughts:
219
- func_str += f"\n inner_thoughts: Deep inner monologue private to you only."
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 += f"\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 += f"\nAvailable functions:"
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 += f"\n<|assistant|>"
262
+ prompt += "\n<|assistant|>"
263
263
  # need to add the function call if there was one
264
264
  inner_thoughts = message["content"]
265
- if "function_call" in message and message["function_call"]:
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 += f"\n<|assistant|>"
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(f"Warning: model not found. Using cl100k_base encoding.")
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="selectin",
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="selectin")
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="selectin",
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="selectin",
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="selectin",
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")