letta-nightly 0.6.34.dev20250302104001__py3-none-any.whl → 0.6.34.dev20250303230404__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (55) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +40 -15
  3. letta/agents/__init__.py +0 -0
  4. letta/agents/base_agent.py +51 -0
  5. letta/agents/ephemeral_agent.py +72 -0
  6. letta/agents/low_latency_agent.py +315 -0
  7. letta/constants.py +3 -1
  8. letta/functions/ast_parsers.py +50 -1
  9. letta/functions/helpers.py +79 -2
  10. letta/functions/schema_generator.py +3 -0
  11. letta/helpers/converters.py +3 -3
  12. letta/interfaces/__init__.py +0 -0
  13. letta/interfaces/openai_chat_completions_streaming_interface.py +109 -0
  14. letta/interfaces/utils.py +11 -0
  15. letta/llm_api/anthropic.py +9 -1
  16. letta/llm_api/azure_openai.py +3 -0
  17. letta/llm_api/google_ai.py +3 -0
  18. letta/llm_api/google_vertex.py +4 -0
  19. letta/llm_api/llm_api_tools.py +1 -1
  20. letta/llm_api/openai.py +6 -0
  21. letta/local_llm/chat_completion_proxy.py +6 -1
  22. letta/log.py +2 -2
  23. letta/orm/step.py +1 -0
  24. letta/orm/tool.py +1 -1
  25. letta/prompts/system/memgpt_convo_only.txt +3 -5
  26. letta/prompts/system/memgpt_memory_only.txt +29 -0
  27. letta/schemas/agent.py +0 -1
  28. letta/schemas/step.py +1 -1
  29. letta/schemas/tool.py +16 -2
  30. letta/server/rest_api/app.py +5 -1
  31. letta/server/rest_api/routers/v1/agents.py +32 -21
  32. letta/server/rest_api/routers/v1/identities.py +9 -1
  33. letta/server/rest_api/routers/v1/runs.py +49 -0
  34. letta/server/rest_api/routers/v1/tools.py +1 -0
  35. letta/server/rest_api/routers/v1/voice.py +19 -255
  36. letta/server/rest_api/utils.py +3 -2
  37. letta/server/server.py +15 -7
  38. letta/services/agent_manager.py +10 -6
  39. letta/services/helpers/agent_manager_helper.py +0 -2
  40. letta/services/helpers/tool_execution_helper.py +18 -0
  41. letta/services/job_manager.py +98 -0
  42. letta/services/step_manager.py +2 -0
  43. letta/services/summarizer/__init__.py +0 -0
  44. letta/services/summarizer/enums.py +9 -0
  45. letta/services/summarizer/summarizer.py +102 -0
  46. letta/services/tool_execution_sandbox.py +20 -3
  47. letta/services/tool_manager.py +1 -1
  48. letta/settings.py +2 -0
  49. letta/tracing.py +176 -156
  50. {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/METADATA +6 -5
  51. {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/RECORD +54 -44
  52. letta/chat_only_agent.py +0 -101
  53. {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/LICENSE +0 -0
  54. {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/WHEEL +0 -0
  55. {letta_nightly-0.6.34.dev20250302104001.dist-info → letta_nightly-0.6.34.dev20250303230404.dist-info}/entry_points.txt +0 -0
@@ -50,6 +50,7 @@ from letta.services.message_manager import MessageManager
50
50
  from letta.services.source_manager import SourceManager
51
51
  from letta.services.tool_manager import ToolManager
52
52
  from letta.settings import settings
53
+ from letta.tracing import trace_method
53
54
  from letta.utils import enforce_types, united_diff
54
55
 
55
56
  logger = get_logger(__name__)
@@ -72,6 +73,7 @@ class AgentManager:
72
73
  # ======================================================================================================================
73
74
  # Basic CRUD operations
74
75
  # ======================================================================================================================
76
+ @trace_method
75
77
  @enforce_types
76
78
  def create_agent(
77
79
  self,
@@ -368,6 +370,7 @@ class AgentManager:
368
370
  agent = AgentModel.read(db_session=session, name=agent_name, actor=actor)
369
371
  return agent.to_pydantic()
370
372
 
373
+ @trace_method
371
374
  @enforce_types
372
375
  def delete_agent(self, agent_id: str, actor: PydanticUser) -> None:
373
376
  """
@@ -529,42 +532,43 @@ class AgentManager:
529
532
  model=agent_state.llm_config.model,
530
533
  openai_message_dict={"role": "system", "content": new_system_message_str},
531
534
  )
535
+ # TODO: This seems kind of silly, why not just update the message?
532
536
  message = self.message_manager.create_message(message, actor=actor)
533
537
  message_ids = [message.id] + agent_state.message_ids[1:] # swap index 0 (system)
534
- return self._set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
538
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
535
539
  else:
536
540
  return agent_state
537
541
 
538
542
  @enforce_types
539
- def _set_in_context_messages(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
543
+ def set_in_context_messages(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
540
544
  return self.update_agent(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
541
545
 
542
546
  @enforce_types
543
547
  def trim_older_in_context_messages(self, num: int, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
544
548
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
545
549
  new_messages = [message_ids[0]] + message_ids[num:] # 0 is system message
546
- return self._set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
550
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
547
551
 
548
552
  @enforce_types
549
553
  def trim_all_in_context_messages_except_system(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
550
554
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
551
555
  # TODO: How do we know this?
552
556
  new_messages = [message_ids[0]] # 0 is system message
553
- return self._set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
557
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=new_messages, actor=actor)
554
558
 
555
559
  @enforce_types
556
560
  def prepend_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
557
561
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
558
562
  new_messages = self.message_manager.create_many_messages(messages, actor=actor)
559
563
  message_ids = [message_ids[0]] + [m.id for m in new_messages] + message_ids[1:]
560
- return self._set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
564
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
561
565
 
562
566
  @enforce_types
563
567
  def append_to_in_context_messages(self, messages: List[PydanticMessage], agent_id: str, actor: PydanticUser) -> PydanticAgentState:
564
568
  messages = self.message_manager.create_many_messages(messages, actor=actor)
565
569
  message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids or []
566
570
  message_ids += [m.id for m in messages]
567
- return self._set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
571
+ return self.set_in_context_messages(agent_id=agent_id, message_ids=message_ids, actor=actor)
568
572
 
569
573
  @enforce_types
570
574
  def reset_messages(self, agent_id: str, actor: PydanticUser, add_default_initial_messages: bool = False) -> PydanticAgentState:
@@ -91,8 +91,6 @@ def derive_system_message(agent_type: AgentType, system: Optional[str] = None):
91
91
  system = gpt_system.get_system_text("memgpt_chat")
92
92
  elif agent_type == AgentType.offline_memory_agent:
93
93
  system = gpt_system.get_system_text("memgpt_offline_memory")
94
- elif agent_type == AgentType.chat_only_agent:
95
- system = gpt_system.get_system_text("memgpt_convo_only")
96
94
  else:
97
95
  raise ValueError(f"Invalid agent type: {agent_type}")
98
96
 
@@ -4,6 +4,10 @@ import subprocess
4
4
  import venv
5
5
  from typing import Dict, Optional
6
6
 
7
+ from datamodel_code_generator import DataModelType, PythonVersion
8
+ from datamodel_code_generator.model import get_data_model_types
9
+ from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
10
+
7
11
  from letta.log import get_logger
8
12
  from letta.schemas.sandbox_config import LocalSandboxConfig
9
13
 
@@ -153,3 +157,17 @@ def create_venv_for_local_sandbox(sandbox_dir_path: str, venv_path: str, env: Di
153
157
  except subprocess.CalledProcessError as e:
154
158
  logger.error(f"Error while setting up the virtual environment: {e}")
155
159
  raise RuntimeError(f"Failed to set up the virtual environment: {e}")
160
+
161
+
162
+ def add_imports_and_pydantic_schemas_for_args(args_json_schema: dict) -> str:
163
+ data_model_types = get_data_model_types(DataModelType.PydanticV2BaseModel, target_python_version=PythonVersion.PY_311)
164
+ parser = JsonSchemaParser(
165
+ str(args_json_schema),
166
+ data_model_type=data_model_types.data_model,
167
+ data_model_root_type=data_model_types.root_model,
168
+ data_model_field_type=data_model_types.field_model,
169
+ data_type_manager_type=data_model_types.data_type_manager,
170
+ dump_resolve_reference_action=data_model_types.dump_resolve_reference_action,
171
+ )
172
+ result = parser.parse()
173
+ return result
@@ -13,12 +13,14 @@ from letta.orm.job_messages import JobMessage
13
13
  from letta.orm.message import Message as MessageModel
14
14
  from letta.orm.sqlalchemy_base import AccessType
15
15
  from letta.orm.step import Step
16
+ from letta.orm.step import Step as StepModel
16
17
  from letta.schemas.enums import JobStatus, MessageRole
17
18
  from letta.schemas.job import Job as PydanticJob
18
19
  from letta.schemas.job import JobUpdate, LettaRequestConfig
19
20
  from letta.schemas.letta_message import LettaMessage
20
21
  from letta.schemas.message import Message as PydanticMessage
21
22
  from letta.schemas.run import Run as PydanticRun
23
+ from letta.schemas.step import Step as PydanticStep
22
24
  from letta.schemas.usage import LettaUsageStatistics
23
25
  from letta.schemas.user import User as PydanticUser
24
26
  from letta.utils import enforce_types
@@ -161,6 +163,51 @@ class JobManager:
161
163
 
162
164
  return [message.to_pydantic() for message in messages]
163
165
 
166
+ @enforce_types
167
+ def get_job_steps(
168
+ self,
169
+ job_id: str,
170
+ actor: PydanticUser,
171
+ before: Optional[str] = None,
172
+ after: Optional[str] = None,
173
+ limit: Optional[int] = 100,
174
+ ascending: bool = True,
175
+ ) -> List[PydanticStep]:
176
+ """
177
+ Get all steps associated with a job.
178
+
179
+ Args:
180
+ job_id: The ID of the job to get steps for
181
+ actor: The user making the request
182
+ before: Cursor for pagination
183
+ after: Cursor for pagination
184
+ limit: Maximum number of steps to return
185
+ ascending: Optional flag to sort in ascending order
186
+
187
+ Returns:
188
+ List of steps associated with the job
189
+
190
+ Raises:
191
+ NoResultFound: If the job does not exist or user does not have access
192
+ """
193
+ with self.session_maker() as session:
194
+ # Build filters
195
+ filters = {}
196
+ filters["job_id"] = job_id
197
+
198
+ # Get steps
199
+ steps = StepModel.list(
200
+ db_session=session,
201
+ before=before,
202
+ after=after,
203
+ ascending=ascending,
204
+ limit=limit,
205
+ actor=actor,
206
+ **filters,
207
+ )
208
+
209
+ return [step.to_pydantic() for step in steps]
210
+
164
211
  @enforce_types
165
212
  def add_message_to_job(self, job_id: str, message_id: str, actor: PydanticUser) -> None:
166
213
  """
@@ -312,6 +359,57 @@ class JobManager:
312
359
 
313
360
  return messages
314
361
 
362
+ @enforce_types
363
+ def get_step_messages(
364
+ self,
365
+ run_id: str,
366
+ actor: PydanticUser,
367
+ before: Optional[str] = None,
368
+ after: Optional[str] = None,
369
+ limit: Optional[int] = 100,
370
+ role: Optional[MessageRole] = None,
371
+ ascending: bool = True,
372
+ ) -> List[LettaMessage]:
373
+ """
374
+ Get steps associated with a job using cursor-based pagination.
375
+ This is a wrapper around get_job_messages that provides cursor-based pagination.
376
+
377
+ Args:
378
+ run_id: The ID of the run to get steps for
379
+ actor: The user making the request
380
+ before: Message ID to get messages after
381
+ after: Message ID to get messages before
382
+ limit: Maximum number of messages to return
383
+ ascending: Whether to return messages in ascending order
384
+ role: Optional role filter
385
+
386
+ Returns:
387
+ List of Steps associated with the job
388
+
389
+ Raises:
390
+ NoResultFound: If the job does not exist or user does not have access
391
+ """
392
+ messages = self.get_job_messages(
393
+ job_id=run_id,
394
+ actor=actor,
395
+ before=before,
396
+ after=after,
397
+ limit=limit,
398
+ role=role,
399
+ ascending=ascending,
400
+ )
401
+
402
+ request_config = self._get_run_request_config(run_id)
403
+
404
+ messages = PydanticMessage.to_letta_messages_from_list(
405
+ messages=messages,
406
+ use_assistant_message=request_config["use_assistant_message"],
407
+ assistant_message_tool_name=request_config["assistant_message_tool_name"],
408
+ assistant_message_tool_kwarg=request_config["assistant_message_tool_kwarg"],
409
+ )
410
+
411
+ return messages
412
+
315
413
  def _verify_job_access(
316
414
  self,
317
415
  session: Session,
@@ -11,6 +11,7 @@ from letta.orm.step import Step as StepModel
11
11
  from letta.schemas.openai.chat_completion_response import UsageStatistics
12
12
  from letta.schemas.step import Step as PydanticStep
13
13
  from letta.schemas.user import User as PydanticUser
14
+ from letta.tracing import get_trace_id
14
15
  from letta.utils import enforce_types
15
16
 
16
17
 
@@ -75,6 +76,7 @@ class StepManager:
75
76
  "job_id": job_id,
76
77
  "tags": [],
77
78
  "tid": None,
79
+ "trace_id": get_trace_id(), # Get the current trace ID
78
80
  }
79
81
  with self.session_maker() as session:
80
82
  if job_id:
File without changes
@@ -0,0 +1,9 @@
1
+ from enum import Enum
2
+
3
+
4
+ class SummarizationMode(str, Enum):
5
+ """
6
+ Represents possible modes of summarization for conversation trimming.
7
+ """
8
+
9
+ STATIC_MESSAGE_BUFFER = "static_message_buffer_mode"
@@ -0,0 +1,102 @@
1
+ import json
2
+ from json import JSONDecodeError
3
+ from typing import List, Tuple
4
+
5
+ from letta.agents.base_agent import BaseAgent
6
+ from letta.schemas.enums import MessageRole
7
+ from letta.schemas.message import Message
8
+ from letta.schemas.openai.chat_completion_request import UserMessage
9
+ from letta.services.summarizer.enums import SummarizationMode
10
+
11
+
12
+ class Summarizer:
13
+ """
14
+ Handles summarization or trimming of conversation messages based on
15
+ the specified SummarizationMode. For now, we demonstrate a simple
16
+ static buffer approach but leave room for more advanced strategies.
17
+ """
18
+
19
+ def __init__(self, mode: SummarizationMode, summarizer_agent: BaseAgent, message_buffer_limit: int = 10, message_buffer_min: int = 3):
20
+ self.mode = mode
21
+
22
+ # Need to do validation on this
23
+ self.message_buffer_limit = message_buffer_limit
24
+ self.message_buffer_min = message_buffer_min
25
+ self.summarizer_agent = summarizer_agent
26
+ # TODO: Move this to config
27
+ self.summary_prefix = "Out of context message summarization:\n"
28
+
29
+ async def summarize(
30
+ self, in_context_messages: List[Message], new_letta_messages: List[Message], previous_summary: str
31
+ ) -> Tuple[List[Message], str, bool]:
32
+ """
33
+ Summarizes or trims in_context_messages according to the chosen mode,
34
+ and returns the updated messages plus any optional "summary message".
35
+
36
+ Args:
37
+ in_context_messages: The existing messages in the conversation's context.
38
+ new_letta_messages: The newly added Letta messages (just appended).
39
+ previous_summary: The previous summary string.
40
+
41
+ Returns:
42
+ (updated_messages, summary_message)
43
+ updated_messages: The new context after trimming/summary
44
+ summary_message: Optional summarization message that was created
45
+ (could be appended to the conversation if desired)
46
+ """
47
+ if self.mode == SummarizationMode.STATIC_MESSAGE_BUFFER:
48
+ return await self._static_buffer_summarization(in_context_messages, new_letta_messages, previous_summary)
49
+ else:
50
+ # Fallback or future logic
51
+ return in_context_messages, "", False
52
+
53
+ async def _static_buffer_summarization(
54
+ self, in_context_messages: List[Message], new_letta_messages: List[Message], previous_summary: str
55
+ ) -> Tuple[List[Message], str, bool]:
56
+ previous_summary = previous_summary[: len(self.summary_prefix)]
57
+ all_in_context_messages = in_context_messages + new_letta_messages
58
+
59
+ # Only summarize if we exceed `message_buffer_limit`
60
+ if len(all_in_context_messages) <= self.message_buffer_limit:
61
+ return all_in_context_messages, previous_summary, False
62
+
63
+ # Aim to trim down to `message_buffer_min`
64
+ target_trim_index = len(all_in_context_messages) - self.message_buffer_min + 1
65
+
66
+ # Move the trim index forward until it's at a `MessageRole.user`
67
+ while target_trim_index < len(all_in_context_messages) and all_in_context_messages[target_trim_index].role != MessageRole.user:
68
+ target_trim_index += 1
69
+
70
+ # TODO: Assuming system message is always at index 0
71
+ updated_in_context_messages = [all_in_context_messages[0]] + all_in_context_messages[target_trim_index:]
72
+ out_of_context_messages = all_in_context_messages[:target_trim_index]
73
+
74
+ formatted_messages = []
75
+ for m in out_of_context_messages:
76
+ if m.content:
77
+ try:
78
+ message = json.loads(m.content[0].text).get("message")
79
+ except JSONDecodeError:
80
+ continue
81
+ if message:
82
+ formatted_messages.append(f"{m.role.value}: {message}")
83
+
84
+ # If we didn't trim any messages, return as-is
85
+ if not formatted_messages:
86
+ return all_in_context_messages, previous_summary, False
87
+
88
+ # Generate summarization request
89
+ summary_request_text = (
90
+ "These are messages that are soon to be removed from the context window:\n"
91
+ f"{formatted_messages}\n\n"
92
+ "This is the current memory:\n"
93
+ f"{previous_summary}\n\n"
94
+ "Your task is to integrate any relevant updates from the messages into the memory."
95
+ "It should be in note-taking format in natural English. You are to return the new, updated memory only."
96
+ )
97
+
98
+ messages = await self.summarizer_agent.step(UserMessage(content=summary_request_text))
99
+ current_summary = "\n".join([m.text for m in messages])
100
+ current_summary = f"{self.summary_prefix}{current_summary}"
101
+
102
+ return updated_in_context_messages, current_summary, True
@@ -11,12 +11,14 @@ import traceback
11
11
  import uuid
12
12
  from typing import Any, Dict, Optional
13
13
 
14
+ from letta.functions.helpers import generate_model_from_args_json_schema
14
15
  from letta.log import get_logger
15
16
  from letta.schemas.agent import AgentState
16
17
  from letta.schemas.sandbox_config import SandboxConfig, SandboxRunResult, SandboxType
17
18
  from letta.schemas.tool import Tool
18
19
  from letta.schemas.user import User
19
20
  from letta.services.helpers.tool_execution_helper import (
21
+ add_imports_and_pydantic_schemas_for_args,
20
22
  create_venv_for_local_sandbox,
21
23
  find_python_executable,
22
24
  install_pip_requirements_for_sandbox,
@@ -408,20 +410,35 @@ class ToolExecutionSandbox:
408
410
  code += "import sys\n"
409
411
  code += "import base64\n"
410
412
 
411
- # Load the agent state data into the program
413
+ # imports to support agent state
412
414
  if agent_state:
413
415
  code += "import letta\n"
414
416
  code += "from letta import * \n"
415
417
  import pickle
416
418
 
419
+ if self.tool.args_json_schema:
420
+ schema_code = add_imports_and_pydantic_schemas_for_args(self.tool.args_json_schema)
421
+ if "from __future__ import annotations" in schema_code:
422
+ schema_code = schema_code.replace("from __future__ import annotations", "").lstrip()
423
+ code = "from __future__ import annotations\n\n" + code
424
+ code += schema_code + "\n"
425
+
426
+ # load the agent state
427
+ if agent_state:
417
428
  agent_state_pickle = pickle.dumps(agent_state)
418
429
  code += f"agent_state = pickle.loads({agent_state_pickle})\n"
419
430
  else:
420
431
  # agent state is None
421
432
  code += "agent_state = None\n"
422
433
 
423
- for param in self.args:
424
- code += self.initialize_param(param, self.args[param])
434
+ if self.tool.args_json_schema:
435
+ args_schema = generate_model_from_args_json_schema(self.tool.args_json_schema)
436
+ code += f"args_object = {args_schema.__name__}(**{self.args})\n"
437
+ for param in self.args:
438
+ code += f"{param} = args_object.{param}\n"
439
+ else:
440
+ for param in self.args:
441
+ code += self.initialize_param(param, self.args[param])
425
442
 
426
443
  if "agent_state" in self.parse_function_arguments(self.tool.source_code, self.tool.name):
427
444
  inject_agent_state = True
@@ -42,7 +42,7 @@ class ToolManager:
42
42
  tool = self.get_tool_by_name(tool_name=pydantic_tool.name, actor=actor)
43
43
  if tool:
44
44
  # Put to dict and remove fields that should not be reset
45
- update_data = pydantic_tool.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
45
+ update_data = pydantic_tool.model_dump(exclude_unset=True, exclude_none=True)
46
46
 
47
47
  # If there's anything to update
48
48
  if update_data:
letta/settings.py CHANGED
@@ -50,6 +50,8 @@ class ModelSettings(BaseSettings):
50
50
 
51
51
  model_config = SettingsConfigDict(env_file=".env", extra="ignore")
52
52
 
53
+ global_max_context_window_limit: int = 32000
54
+
53
55
  # env_prefix='my_prefix_'
54
56
 
55
57
  # when we use /completions APIs (instead of /chat/completions), we need to specify a model wrapper