letta-nightly 0.6.5.dev20241218213641__py3-none-any.whl → 0.6.5.dev20241220104040__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 (30) hide show
  1. letta/agent.py +37 -6
  2. letta/client/client.py +2 -2
  3. letta/client/streaming.py +9 -9
  4. letta/errors.py +60 -25
  5. letta/functions/function_sets/base.py +0 -54
  6. letta/helpers/tool_rule_solver.py +82 -51
  7. letta/llm_api/llm_api_tools.py +2 -2
  8. letta/orm/custom_columns.py +5 -2
  9. letta/orm/message.py +2 -1
  10. letta/orm/passage.py +14 -15
  11. letta/providers.py +2 -1
  12. letta/schemas/enums.py +1 -0
  13. letta/schemas/letta_message.py +76 -40
  14. letta/schemas/letta_response.py +9 -1
  15. letta/schemas/message.py +13 -13
  16. letta/schemas/tool_rule.py +12 -2
  17. letta/server/rest_api/interface.py +48 -48
  18. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +2 -2
  19. letta/server/rest_api/routers/v1/agents.py +3 -0
  20. letta/server/rest_api/routers/v1/tools.py +5 -20
  21. letta/server/rest_api/utils.py +23 -22
  22. letta/server/server.py +12 -18
  23. letta/services/agent_manager.py +32 -46
  24. letta/services/message_manager.py +1 -0
  25. letta/services/tool_manager.py +3 -3
  26. {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/METADATA +1 -1
  27. {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/RECORD +30 -30
  28. {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/LICENSE +0 -0
  29. {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/WHEEL +0 -0
  30. {letta_nightly-0.6.5.dev20241218213641.dist-info → letta_nightly-0.6.5.dev20241220104040.dist-info}/entry_points.txt +0 -0
letta/orm/passage.py CHANGED
@@ -1,26 +1,26 @@
1
1
  from typing import TYPE_CHECKING
2
- from sqlalchemy import Column, JSON, Index
3
- from sqlalchemy.orm import Mapped, mapped_column, relationship, declared_attr
4
2
 
5
- from letta.orm.mixins import FileMixin, OrganizationMixin
3
+ from sqlalchemy import JSON, Column, Index
4
+ from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
5
+
6
+ from letta.config import LettaConfig
7
+ from letta.constants import MAX_EMBEDDING_DIM
6
8
  from letta.orm.custom_columns import CommonVector, EmbeddingConfigColumn
7
- from letta.orm.sqlalchemy_base import SqlalchemyBase
8
9
  from letta.orm.mixins import AgentMixin, FileMixin, OrganizationMixin, SourceMixin
10
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
9
11
  from letta.schemas.passage import Passage as PydanticPassage
10
12
  from letta.settings import settings
11
13
 
12
- from letta.config import LettaConfig
13
- from letta.constants import MAX_EMBEDDING_DIM
14
-
15
14
  config = LettaConfig()
16
15
 
17
16
  if TYPE_CHECKING:
18
- from letta.orm.organization import Organization
19
17
  from letta.orm.agent import Agent
18
+ from letta.orm.organization import Organization
20
19
 
21
20
 
22
21
  class BasePassage(SqlalchemyBase, OrganizationMixin):
23
22
  """Base class for all passage types with common fields"""
23
+
24
24
  __abstract__ = True
25
25
  __pydantic_model__ = PydanticPassage
26
26
 
@@ -45,17 +45,15 @@ class BasePassage(SqlalchemyBase, OrganizationMixin):
45
45
  @declared_attr
46
46
  def __table_args__(cls):
47
47
  if settings.letta_pg_uri_no_default:
48
- return (
49
- Index(f'{cls.__tablename__}_org_idx', 'organization_id'),
50
- {"extend_existing": True}
51
- )
48
+ return (Index(f"{cls.__tablename__}_org_idx", "organization_id"), {"extend_existing": True})
52
49
  return ({"extend_existing": True},)
53
50
 
54
51
 
55
52
  class SourcePassage(BasePassage, FileMixin, SourceMixin):
56
53
  """Passages derived from external files/sources"""
54
+
57
55
  __tablename__ = "source_passages"
58
-
56
+
59
57
  @declared_attr
60
58
  def file(cls) -> Mapped["FileMetadata"]:
61
59
  """Relationship to file"""
@@ -64,7 +62,7 @@ class SourcePassage(BasePassage, FileMixin, SourceMixin):
64
62
  @declared_attr
65
63
  def organization(cls) -> Mapped["Organization"]:
66
64
  return relationship("Organization", back_populates="source_passages", lazy="selectin")
67
-
65
+
68
66
  @declared_attr
69
67
  def source(cls) -> Mapped["Source"]:
70
68
  """Relationship to source"""
@@ -73,8 +71,9 @@ class SourcePassage(BasePassage, FileMixin, SourceMixin):
73
71
 
74
72
  class AgentPassage(BasePassage, AgentMixin):
75
73
  """Passages created by agents as archival memories"""
74
+
76
75
  __tablename__ = "agent_passages"
77
-
76
+
78
77
  @declared_attr
79
78
  def organization(cls) -> Mapped["Organization"]:
80
79
  return relationship("Organization", back_populates="agent_passages", lazy="selectin")
letta/providers.py CHANGED
@@ -482,7 +482,8 @@ class GoogleAIProvider(Provider):
482
482
  model_options = [mo[len("models/") :] if mo.startswith("models/") else mo for mo in model_options]
483
483
 
484
484
  # TODO remove manual filtering for gemini-pro
485
- model_options = [mo for mo in model_options if str(mo).startswith("gemini") and "-pro" in str(mo)]
485
+ # Add support for all gemini models
486
+ model_options = [mo for mo in model_options if str(mo).startswith("gemini-")]
486
487
 
487
488
  configs = []
488
489
  for model in model_options:
letta/schemas/enums.py CHANGED
@@ -45,5 +45,6 @@ class ToolRuleType(str, Enum):
45
45
  run_first = "InitToolRule"
46
46
  exit_loop = "TerminalToolRule" # reasoning loop should exit
47
47
  continue_loop = "continue_loop" # reasoning loop should continue
48
+ conditional = "conditional"
48
49
  constrain_child_tools = "ToolRule"
49
50
  require_parent_tools = "require_parent_tools"
@@ -9,7 +9,7 @@ from pydantic import BaseModel, Field, field_serializer, field_validator
9
9
 
10
10
  class LettaMessage(BaseModel):
11
11
  """
12
- Base class for simplified Letta message response type. This is intended to be used for developers who want the internal monologue, function calls, and function returns in a simplified format that does not include additional information other than the content and timestamp.
12
+ Base class for simplified Letta message response type. This is intended to be used for developers who want the internal monologue, tool calls, and tool returns in a simplified format that does not include additional information other than the content and timestamp.
13
13
 
14
14
  Attributes:
15
15
  id (str): The ID of the message
@@ -60,32 +60,32 @@ class UserMessage(LettaMessage):
60
60
  message: str
61
61
 
62
62
 
63
- class InternalMonologue(LettaMessage):
63
+ class ReasoningMessage(LettaMessage):
64
64
  """
65
- Representation of an agent's internal monologue.
65
+ Representation of an agent's internal reasoning.
66
66
 
67
67
  Attributes:
68
- internal_monologue (str): The internal monologue of the agent
68
+ reasoning (str): The internal reasoning of the agent
69
69
  id (str): The ID of the message
70
70
  date (datetime): The date the message was created in ISO format
71
71
  """
72
72
 
73
- message_type: Literal["internal_monologue"] = "internal_monologue"
74
- internal_monologue: str
73
+ message_type: Literal["reasoning_message"] = "reasoning_message"
74
+ reasoning: str
75
75
 
76
76
 
77
- class FunctionCall(BaseModel):
77
+ class ToolCall(BaseModel):
78
78
 
79
79
  name: str
80
80
  arguments: str
81
- function_call_id: str
81
+ tool_call_id: str
82
82
 
83
83
 
84
- class FunctionCallDelta(BaseModel):
84
+ class ToolCallDelta(BaseModel):
85
85
 
86
86
  name: Optional[str]
87
87
  arguments: Optional[str]
88
- function_call_id: Optional[str]
88
+ tool_call_id: Optional[str]
89
89
 
90
90
  # NOTE: this is a workaround to exclude None values from the JSON dump,
91
91
  # since the OpenAI style of returning chunks doesn't include keys with null values
@@ -97,50 +97,84 @@ class FunctionCallDelta(BaseModel):
97
97
  return json.dumps(self.model_dump(exclude_none=True), *args, **kwargs)
98
98
 
99
99
 
100
- class FunctionCallMessage(LettaMessage):
100
+ class ToolCallMessage(LettaMessage):
101
101
  """
102
- A message representing a request to call a function (generated by the LLM to trigger function execution).
102
+ A message representing a request to call a tool (generated by the LLM to trigger tool execution).
103
103
 
104
104
  Attributes:
105
- function_call (Union[FunctionCall, FunctionCallDelta]): The function call
105
+ tool_call (Union[ToolCall, ToolCallDelta]): The tool call
106
106
  id (str): The ID of the message
107
107
  date (datetime): The date the message was created in ISO format
108
108
  """
109
109
 
110
- message_type: Literal["function_call"] = "function_call"
111
- function_call: Union[FunctionCall, FunctionCallDelta]
110
+ message_type: Literal["tool_call_message"] = "tool_call_message"
111
+ tool_call: Union[ToolCall, ToolCallDelta]
112
112
 
113
- # NOTE: this is required for the FunctionCallDelta exclude_none to work correctly
113
+ # NOTE: this is required for the ToolCallDelta exclude_none to work correctly
114
114
  def model_dump(self, *args, **kwargs):
115
115
  kwargs["exclude_none"] = True
116
116
  data = super().model_dump(*args, **kwargs)
117
- if isinstance(data["function_call"], dict):
118
- data["function_call"] = {k: v for k, v in data["function_call"].items() if v is not None}
117
+ if isinstance(data["tool_call"], dict):
118
+ data["tool_call"] = {k: v for k, v in data["tool_call"].items() if v is not None}
119
119
  return data
120
120
 
121
121
  class Config:
122
122
  json_encoders = {
123
- FunctionCallDelta: lambda v: v.model_dump(exclude_none=True),
124
- FunctionCall: lambda v: v.model_dump(exclude_none=True),
123
+ ToolCallDelta: lambda v: v.model_dump(exclude_none=True),
124
+ ToolCall: lambda v: v.model_dump(exclude_none=True),
125
125
  }
126
126
 
127
- # NOTE: this is required to cast dicts into FunctionCallMessage objects
127
+ # NOTE: this is required to cast dicts into ToolCallMessage objects
128
128
  # Without this extra validator, Pydantic will throw an error if 'name' or 'arguments' are None
129
- # (instead of properly casting to FunctionCallDelta instead of FunctionCall)
130
- @field_validator("function_call", mode="before")
129
+ # (instead of properly casting to ToolCallDelta instead of ToolCall)
130
+ @field_validator("tool_call", mode="before")
131
131
  @classmethod
132
- def validate_function_call(cls, v):
132
+ def validate_tool_call(cls, v):
133
133
  if isinstance(v, dict):
134
- if "name" in v and "arguments" in v and "function_call_id" in v:
135
- return FunctionCall(name=v["name"], arguments=v["arguments"], function_call_id=v["function_call_id"])
136
- elif "name" in v or "arguments" in v or "function_call_id" in v:
137
- return FunctionCallDelta(name=v.get("name"), arguments=v.get("arguments"), function_call_id=v.get("function_call_id"))
134
+ if "name" in v and "arguments" in v and "tool_call_id" in v:
135
+ return ToolCall(name=v["name"], arguments=v["arguments"], tool_call_id=v["tool_call_id"])
136
+ elif "name" in v or "arguments" in v or "tool_call_id" in v:
137
+ return ToolCallDelta(name=v.get("name"), arguments=v.get("arguments"), tool_call_id=v.get("tool_call_id"))
138
138
  else:
139
- raise ValueError("function_call must contain either 'name' or 'arguments'")
139
+ raise ValueError("tool_call must contain either 'name' or 'arguments'")
140
140
  return v
141
141
 
142
142
 
143
- class FunctionReturn(LettaMessage):
143
+ class ToolReturnMessage(LettaMessage):
144
+ """
145
+ A message representing the return value of a tool call (generated by Letta executing the requested tool).
146
+
147
+ Attributes:
148
+ tool_return (str): The return value of the tool
149
+ status (Literal["success", "error"]): The status of the tool call
150
+ id (str): The ID of the message
151
+ date (datetime): The date the message was created in ISO format
152
+ tool_call_id (str): A unique identifier for the tool call that generated this message
153
+ stdout (Optional[List(str)]): Captured stdout (e.g. prints, logs) from the tool invocation
154
+ stderr (Optional[List(str)]): Captured stderr from the tool invocation
155
+ """
156
+
157
+ message_type: Literal["tool_return_message"] = "tool_return_message"
158
+ tool_return: str
159
+ status: Literal["success", "error"]
160
+ tool_call_id: str
161
+ stdout: Optional[List[str]] = None
162
+ stderr: Optional[List[str]] = None
163
+
164
+
165
+ # Legacy Letta API had an additional type "assistant_message" and the "function_call" was a formatted string
166
+
167
+
168
+ class AssistantMessage(LettaMessage):
169
+ message_type: Literal["assistant_message"] = "assistant_message"
170
+ assistant_message: str
171
+
172
+
173
+ class LegacyFunctionCallMessage(LettaMessage):
174
+ function_call: str
175
+
176
+
177
+ class LegacyFunctionReturn(LettaMessage):
144
178
  """
145
179
  A message representing the return value of a function call (generated by Letta executing the requested function).
146
180
 
@@ -162,22 +196,24 @@ class FunctionReturn(LettaMessage):
162
196
  stderr: Optional[List[str]] = None
163
197
 
164
198
 
165
- # Legacy Letta API had an additional type "assistant_message" and the "function_call" was a formatted string
166
-
167
-
168
- class AssistantMessage(LettaMessage):
169
- message_type: Literal["assistant_message"] = "assistant_message"
170
- assistant_message: str
199
+ class LegacyInternalMonologue(LettaMessage):
200
+ """
201
+ Representation of an agent's internal monologue.
171
202
 
203
+ Attributes:
204
+ internal_monologue (str): The internal monologue of the agent
205
+ id (str): The ID of the message
206
+ date (datetime): The date the message was created in ISO format
207
+ """
172
208
 
173
- class LegacyFunctionCallMessage(LettaMessage):
174
- function_call: str
209
+ message_type: Literal["internal_monologue"] = "internal_monologue"
210
+ internal_monologue: str
175
211
 
176
212
 
177
- LegacyLettaMessage = Union[InternalMonologue, AssistantMessage, LegacyFunctionCallMessage, FunctionReturn]
213
+ LegacyLettaMessage = Union[LegacyInternalMonologue, AssistantMessage, LegacyFunctionCallMessage, LegacyFunctionReturn]
178
214
 
179
215
 
180
216
  LettaMessageUnion = Annotated[
181
- Union[SystemMessage, UserMessage, InternalMonologue, FunctionCallMessage, FunctionReturn, AssistantMessage],
217
+ Union[SystemMessage, UserMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage, AssistantMessage],
182
218
  Field(discriminator="message_type"),
183
219
  ]
@@ -40,14 +40,22 @@ class LettaResponse(BaseModel):
40
40
  def get_formatted_content(msg):
41
41
  if msg.message_type == "internal_monologue":
42
42
  return f'<div class="content"><span class="internal-monologue">{html.escape(msg.internal_monologue)}</span></div>'
43
+ if msg.message_type == "reasoning_message":
44
+ return f'<div class="content"><span class="internal-monologue">{html.escape(msg.reasoning)}</span></div>'
43
45
  elif msg.message_type == "function_call":
44
46
  args = format_json(msg.function_call.arguments)
45
47
  return f'<div class="content"><span class="function-name">{html.escape(msg.function_call.name)}</span>({args})</div>'
48
+ elif msg.message_type == "tool_call_message":
49
+ args = format_json(msg.tool_call.arguments)
50
+ return f'<div class="content"><span class="function-name">{html.escape(msg.function_call.name)}</span>({args})</div>'
46
51
  elif msg.message_type == "function_return":
47
-
48
52
  return_value = format_json(msg.function_return)
49
53
  # return f'<div class="status-line">Status: {html.escape(msg.status)}</div><div class="content">{return_value}</div>'
50
54
  return f'<div class="content">{return_value}</div>'
55
+ elif msg.message_type == "tool_return_message":
56
+ return_value = format_json(msg.tool_return)
57
+ # return f'<div class="status-line">Status: {html.escape(msg.status)}</div><div class="content">{return_value}</div>'
58
+ return f'<div class="content">{return_value}</div>'
51
59
  elif msg.message_type == "user_message":
52
60
  if is_json(msg.message):
53
61
  return f'<div class="content">{format_json(msg.message)}</div>'
letta/schemas/message.py CHANGED
@@ -16,10 +16,10 @@ from letta.schemas.enums import MessageRole
16
16
  from letta.schemas.letta_base import OrmMetadataBase
17
17
  from letta.schemas.letta_message import (
18
18
  AssistantMessage,
19
- FunctionCall,
20
- FunctionCallMessage,
21
- FunctionReturn,
22
- InternalMonologue,
19
+ ToolCall as LettaToolCall,
20
+ ToolCallMessage,
21
+ ToolReturnMessage,
22
+ ReasoningMessage,
23
23
  LettaMessage,
24
24
  SystemMessage,
25
25
  UserMessage,
@@ -145,10 +145,10 @@ class Message(BaseMessage):
145
145
  if self.text is not None:
146
146
  # This is type InnerThoughts
147
147
  messages.append(
148
- InternalMonologue(
148
+ ReasoningMessage(
149
149
  id=self.id,
150
150
  date=self.created_at,
151
- internal_monologue=self.text,
151
+ reasoning=self.text,
152
152
  )
153
153
  )
154
154
  if self.tool_calls is not None:
@@ -172,18 +172,18 @@ class Message(BaseMessage):
172
172
  )
173
173
  else:
174
174
  messages.append(
175
- FunctionCallMessage(
175
+ ToolCallMessage(
176
176
  id=self.id,
177
177
  date=self.created_at,
178
- function_call=FunctionCall(
178
+ tool_call=LettaToolCall(
179
179
  name=tool_call.function.name,
180
180
  arguments=tool_call.function.arguments,
181
- function_call_id=tool_call.id,
181
+ tool_call_id=tool_call.id,
182
182
  ),
183
183
  )
184
184
  )
185
185
  elif self.role == MessageRole.tool:
186
- # This is type FunctionReturn
186
+ # This is type ToolReturnMessage
187
187
  # Try to interpret the function return, recall that this is how we packaged:
188
188
  # def package_function_response(was_success, response_string, timestamp=None):
189
189
  # formatted_time = get_local_time() if timestamp is None else timestamp
@@ -208,12 +208,12 @@ class Message(BaseMessage):
208
208
  messages.append(
209
209
  # TODO make sure this is what the API returns
210
210
  # function_return may not match exactly...
211
- FunctionReturn(
211
+ ToolReturnMessage(
212
212
  id=self.id,
213
213
  date=self.created_at,
214
- function_return=self.text,
214
+ tool_return=self.text,
215
215
  status=status_enum,
216
- function_call_id=self.tool_call_id,
216
+ tool_call_id=self.tool_call_id,
217
217
  )
218
218
  )
219
219
  elif self.role == MessageRole.user:
@@ -1,4 +1,4 @@
1
- from typing import List, Union
1
+ from typing import Any, Dict, List, Optional, Union
2
2
 
3
3
  from pydantic import Field
4
4
 
@@ -21,6 +21,16 @@ class ChildToolRule(BaseToolRule):
21
21
  children: List[str] = Field(..., description="The children tools that can be invoked.")
22
22
 
23
23
 
24
+ class ConditionalToolRule(BaseToolRule):
25
+ """
26
+ A ToolRule that conditionally maps to different child tools based on the output.
27
+ """
28
+ type: ToolRuleType = ToolRuleType.conditional
29
+ default_child: Optional[str] = Field(None, description="The default child tool to be called. If None, any tool can be called.")
30
+ child_output_mapping: Dict[Any, str] = Field(..., description="The output case to check for mapping")
31
+ require_output_mapping: bool = Field(default=False, description="Whether to throw an error when output doesn't match any case")
32
+
33
+
24
34
  class InitToolRule(BaseToolRule):
25
35
  """
26
36
  Represents the initial tool rule configuration.
@@ -37,4 +47,4 @@ class TerminalToolRule(BaseToolRule):
37
47
  type: ToolRuleType = ToolRuleType.exit_loop
38
48
 
39
49
 
40
- ToolRule = Union[ChildToolRule, InitToolRule, TerminalToolRule]
50
+ ToolRule = Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule]