letta-nightly 0.6.9.dev20250120104049__py3-none-any.whl → 0.6.10.dev20250120193553__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 (35) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +40 -23
  3. letta/client/client.py +10 -2
  4. letta/errors.py +14 -0
  5. letta/functions/ast_parsers.py +105 -0
  6. letta/llm_api/anthropic.py +130 -82
  7. letta/llm_api/aws_bedrock.py +134 -0
  8. letta/llm_api/llm_api_tools.py +30 -7
  9. letta/orm/__init__.py +1 -0
  10. letta/orm/job.py +2 -4
  11. letta/orm/message.py +5 -1
  12. letta/orm/step.py +54 -0
  13. letta/schemas/embedding_config.py +1 -0
  14. letta/schemas/letta_message.py +24 -0
  15. letta/schemas/letta_response.py +1 -9
  16. letta/schemas/llm_config.py +1 -0
  17. letta/schemas/message.py +1 -0
  18. letta/schemas/providers.py +60 -3
  19. letta/schemas/step.py +31 -0
  20. letta/server/rest_api/app.py +21 -6
  21. letta/server/rest_api/routers/v1/agents.py +15 -2
  22. letta/server/rest_api/routers/v1/llms.py +2 -2
  23. letta/server/rest_api/routers/v1/runs.py +12 -2
  24. letta/server/server.py +9 -3
  25. letta/services/agent_manager.py +4 -3
  26. letta/services/job_manager.py +11 -13
  27. letta/services/provider_manager.py +19 -7
  28. letta/services/step_manager.py +87 -0
  29. letta/settings.py +21 -1
  30. {letta_nightly-0.6.9.dev20250120104049.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/METADATA +8 -6
  31. {letta_nightly-0.6.9.dev20250120104049.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/RECORD +34 -30
  32. letta/credentials.py +0 -149
  33. {letta_nightly-0.6.9.dev20250120104049.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/LICENSE +0 -0
  34. {letta_nightly-0.6.9.dev20250120104049.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/WHEEL +0 -0
  35. {letta_nightly-0.6.9.dev20250120104049.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,134 @@
1
+ import os
2
+ from typing import Any, Dict, List
3
+
4
+ from anthropic import AnthropicBedrock
5
+
6
+ from letta.settings import model_settings
7
+
8
+
9
+ def has_valid_aws_credentials() -> bool:
10
+ """
11
+ Check if AWS credentials are properly configured.
12
+ """
13
+ valid_aws_credentials = os.getenv("AWS_ACCESS_KEY_ID") and os.getenv("AWS_SECRET_ACCESS_KEY") and os.getenv("AWS_REGION")
14
+ return valid_aws_credentials
15
+
16
+
17
+ def get_bedrock_client():
18
+ """
19
+ Get a Bedrock client
20
+ """
21
+ import boto3
22
+
23
+ sts_client = boto3.client(
24
+ "sts",
25
+ aws_access_key_id=model_settings.aws_access_key,
26
+ aws_secret_access_key=model_settings.aws_secret_access_key,
27
+ region_name=model_settings.aws_region,
28
+ )
29
+ credentials = sts_client.get_session_token()["Credentials"]
30
+
31
+ bedrock = AnthropicBedrock(
32
+ aws_access_key=credentials["AccessKeyId"],
33
+ aws_secret_key=credentials["SecretAccessKey"],
34
+ aws_session_token=credentials["SessionToken"],
35
+ aws_region=model_settings.aws_region,
36
+ )
37
+ return bedrock
38
+
39
+
40
+ def bedrock_get_model_list(region_name: str) -> List[dict]:
41
+ """
42
+ Get list of available models from Bedrock.
43
+
44
+ Args:
45
+ region_name: AWS region name
46
+ model_provider: Optional provider name to filter models. If None, returns all models.
47
+ output_modality: Output modality to filter models. Defaults to "text".
48
+
49
+ Returns:
50
+ List of model summaries
51
+ """
52
+ import boto3
53
+
54
+ try:
55
+ bedrock = boto3.client("bedrock", region_name=region_name)
56
+ response = bedrock.list_inference_profiles()
57
+ return response["inferenceProfileSummaries"]
58
+ except Exception as e:
59
+ print(f"Error getting model list: {str(e)}")
60
+ raise e
61
+
62
+
63
+ def bedrock_get_model_details(region_name: str, model_id: str) -> Dict[str, Any]:
64
+ """
65
+ Get details for a specific model from Bedrock.
66
+ """
67
+ import boto3
68
+ from botocore.exceptions import ClientError
69
+
70
+ try:
71
+ bedrock = boto3.client("bedrock", region_name=region_name)
72
+ response = bedrock.get_foundation_model(modelIdentifier=model_id)
73
+ return response["modelDetails"]
74
+ except ClientError as e:
75
+ print(f"Error getting model details: {str(e)}")
76
+ raise e
77
+
78
+
79
+ def bedrock_get_model_context_window(model_id: str) -> int:
80
+ """
81
+ Get context window size for a specific model.
82
+ """
83
+ # Bedrock doesn't provide this via API, so we maintain a mapping
84
+ context_windows = {
85
+ "anthropic.claude-3-5-sonnet-20241022-v2:0": 200000,
86
+ "anthropic.claude-3-5-sonnet-20240620-v1:0": 200000,
87
+ "anthropic.claude-3-5-haiku-20241022-v1:0": 200000,
88
+ "anthropic.claude-3-haiku-20240307-v1:0": 200000,
89
+ "anthropic.claude-3-opus-20240229-v1:0": 200000,
90
+ "anthropic.claude-3-sonnet-20240229-v1:0": 200000,
91
+ }
92
+ return context_windows.get(model_id, 200000) # default to 100k if unknown
93
+
94
+
95
+ """
96
+ {
97
+ "id": "msg_123",
98
+ "type": "message",
99
+ "role": "assistant",
100
+ "model": "anthropic.claude-3-5-sonnet-20241022-v2:0",
101
+ "content": [
102
+ {
103
+ "type": "text",
104
+ "text": "I see the Firefox icon. Let me click on it and then navigate to a weather website."
105
+ },
106
+ {
107
+ "type": "tool_use",
108
+ "id": "toolu_123",
109
+ "name": "computer",
110
+ "input": {
111
+ "action": "mouse_move",
112
+ "coordinate": [
113
+ 708,
114
+ 736
115
+ ]
116
+ }
117
+ },
118
+ {
119
+ "type": "tool_use",
120
+ "id": "toolu_234",
121
+ "name": "computer",
122
+ "input": {
123
+ "action": "left_click"
124
+ }
125
+ }
126
+ ],
127
+ "stop_reason": "tool_use",
128
+ "stop_sequence": null,
129
+ "usage": {
130
+ "input_tokens": 3391,
131
+ "output_tokens": 132
132
+ }
133
+ }
134
+ """
@@ -6,7 +6,8 @@ import requests
6
6
 
7
7
  from letta.constants import CLI_WARNING_PREFIX
8
8
  from letta.errors import LettaConfigurationError, RateLimitExceededError
9
- from letta.llm_api.anthropic import anthropic_chat_completions_request
9
+ from letta.llm_api.anthropic import anthropic_bedrock_chat_completions_request, anthropic_chat_completions_request
10
+ from letta.llm_api.aws_bedrock import has_valid_aws_credentials
10
11
  from letta.llm_api.azure_openai import azure_openai_chat_completions_request
11
12
  from letta.llm_api.google_ai import convert_tools_to_google_ai_format, google_ai_chat_completions_request
12
13
  from letta.llm_api.helpers import add_inner_thoughts_to_functions, unpack_all_inner_thoughts_from_kwargs
@@ -22,7 +23,6 @@ from letta.schemas.llm_config import LLMConfig
22
23
  from letta.schemas.message import Message
23
24
  from letta.schemas.openai.chat_completion_request import ChatCompletionRequest, Tool, cast_message_to_subtype
24
25
  from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
25
- from letta.services.provider_manager import ProviderManager
26
26
  from letta.settings import ModelSettings
27
27
  from letta.streaming_interface import AgentChunkStreamingInterface, AgentRefreshStreamingInterface
28
28
 
@@ -252,12 +252,7 @@ def create(
252
252
  tool_call = {"type": "function", "function": {"name": force_tool_call}}
253
253
  assert functions is not None
254
254
 
255
- # load anthropic key from db in case a custom key has been stored
256
- anthropic_key_override = ProviderManager().get_anthropic_key_override()
257
-
258
255
  return anthropic_chat_completions_request(
259
- url=llm_config.model_endpoint,
260
- api_key=anthropic_key_override if anthropic_key_override else model_settings.anthropic_api_key,
261
256
  data=ChatCompletionRequest(
262
257
  model=llm_config.model,
263
258
  messages=[cast_message_to_subtype(m.to_openai_dict()) for m in messages],
@@ -374,6 +369,34 @@ def create(
374
369
  auth_key=model_settings.together_api_key,
375
370
  )
376
371
 
372
+ elif llm_config.model_endpoint_type == "bedrock":
373
+ """Anthropic endpoint that goes via /embeddings instead of /chat/completions"""
374
+
375
+ if stream:
376
+ raise NotImplementedError(f"Streaming not yet implemented for Anthropic (via the /embeddings endpoint).")
377
+ if not use_tool_naming:
378
+ raise NotImplementedError("Only tool calling supported on Anthropic API requests")
379
+
380
+ if not has_valid_aws_credentials():
381
+ raise LettaConfigurationError(message="Invalid or missing AWS credentials. Please configure valid AWS credentials.")
382
+
383
+ tool_call = None
384
+ if force_tool_call is not None:
385
+ tool_call = {"type": "function", "function": {"name": force_tool_call}}
386
+ assert functions is not None
387
+
388
+ return anthropic_bedrock_chat_completions_request(
389
+ data=ChatCompletionRequest(
390
+ model=llm_config.model,
391
+ messages=[cast_message_to_subtype(m.to_openai_dict()) for m in messages],
392
+ tools=[{"type": "function", "function": f} for f in functions] if functions else None,
393
+ tool_choice=tool_call,
394
+ # user=str(user_id),
395
+ # NOTE: max_tokens is required for Anthropic API
396
+ max_tokens=1024, # TODO make dynamic
397
+ ),
398
+ )
399
+
377
400
  # local model
378
401
  else:
379
402
  if stream:
letta/orm/__init__.py CHANGED
@@ -13,6 +13,7 @@ from letta.orm.provider import Provider
13
13
  from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
14
14
  from letta.orm.source import Source
15
15
  from letta.orm.sources_agents import SourcesAgents
16
+ from letta.orm.step import Step
16
17
  from letta.orm.tool import Tool
17
18
  from letta.orm.tools_agents import ToolsAgents
18
19
  from letta.orm.user import User
letta/orm/job.py CHANGED
@@ -13,8 +13,8 @@ from letta.schemas.letta_request import LettaRequestConfig
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from letta.orm.job_messages import JobMessage
16
- from letta.orm.job_usage_statistics import JobUsageStatistics
17
16
  from letta.orm.message import Message
17
+ from letta.orm.step import Step
18
18
  from letta.orm.user import User
19
19
 
20
20
 
@@ -41,9 +41,7 @@ class Job(SqlalchemyBase, UserMixin):
41
41
  # relationships
42
42
  user: Mapped["User"] = relationship("User", back_populates="jobs")
43
43
  job_messages: Mapped[List["JobMessage"]] = relationship("JobMessage", back_populates="job", cascade="all, delete-orphan")
44
- usage_statistics: Mapped[list["JobUsageStatistics"]] = relationship(
45
- "JobUsageStatistics", back_populates="job", cascade="all, delete-orphan"
46
- )
44
+ steps: Mapped[List["Step"]] = relationship("Step", back_populates="job", cascade="save-update")
47
45
 
48
46
  @property
49
47
  def messages(self) -> List["Message"]:
letta/orm/message.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import Optional
2
2
 
3
- from sqlalchemy import Index
3
+ from sqlalchemy import ForeignKey, Index
4
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
5
5
 
6
6
  from letta.orm.custom_columns import ToolCallColumn
@@ -24,10 +24,14 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
24
24
  name: Mapped[Optional[str]] = mapped_column(nullable=True, doc="Name for multi-agent scenarios")
25
25
  tool_calls: Mapped[ToolCall] = mapped_column(ToolCallColumn, doc="Tool call information")
26
26
  tool_call_id: Mapped[Optional[str]] = mapped_column(nullable=True, doc="ID of the tool call")
27
+ step_id: Mapped[Optional[str]] = mapped_column(
28
+ ForeignKey("steps.id", ondelete="SET NULL"), nullable=True, doc="ID of the step that this message belongs to"
29
+ )
27
30
 
28
31
  # Relationships
29
32
  agent: Mapped["Agent"] = relationship("Agent", back_populates="messages", lazy="selectin")
30
33
  organization: Mapped["Organization"] = relationship("Organization", back_populates="messages", lazy="selectin")
34
+ step: Mapped["Step"] = relationship("Step", back_populates="messages", lazy="selectin")
31
35
 
32
36
  # Job relationship
33
37
  job_message: Mapped[Optional["JobMessage"]] = relationship(
letta/orm/step.py ADDED
@@ -0,0 +1,54 @@
1
+ import uuid
2
+ from typing import TYPE_CHECKING, Dict, List, Optional
3
+
4
+ from sqlalchemy import JSON, ForeignKey, String
5
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
+
7
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
8
+ from letta.schemas.step import Step as PydanticStep
9
+
10
+ if TYPE_CHECKING:
11
+ from letta.orm.job import Job
12
+ from letta.orm.provider import Provider
13
+
14
+
15
+ class Step(SqlalchemyBase):
16
+ """Tracks all metadata for agent step."""
17
+
18
+ __tablename__ = "steps"
19
+ __pydantic_model__ = PydanticStep
20
+
21
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"step-{uuid.uuid4()}")
22
+ origin: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The surface that this agent step was initiated from.")
23
+ organization_id: Mapped[str] = mapped_column(
24
+ ForeignKey("organizations.id", ondelete="RESTRICT"),
25
+ nullable=True,
26
+ doc="The unique identifier of the organization that this step ran for",
27
+ )
28
+ provider_id: Mapped[Optional[str]] = mapped_column(
29
+ ForeignKey("providers.id", ondelete="RESTRICT"),
30
+ nullable=True,
31
+ doc="The unique identifier of the provider that was configured for this step",
32
+ )
33
+ job_id: Mapped[Optional[str]] = mapped_column(
34
+ ForeignKey("jobs.id", ondelete="SET NULL"), nullable=True, doc="The unique identified of the job run that triggered this step"
35
+ )
36
+ provider_name: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The name of the provider used for this step.")
37
+ model: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The name of the model used for this step.")
38
+ context_window_limit: Mapped[Optional[int]] = mapped_column(
39
+ None, nullable=True, doc="The context window limit configured for this step."
40
+ )
41
+ completion_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens generated by the agent")
42
+ prompt_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens in the prompt")
43
+ total_tokens: Mapped[int] = mapped_column(default=0, doc="Total number of tokens processed by the agent")
44
+ completion_tokens_details: Mapped[Optional[Dict]] = mapped_column(JSON, nullable=True, doc="metadata for the agent.")
45
+ tags: Mapped[Optional[List]] = mapped_column(JSON, doc="Metadata tags.")
46
+ tid: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="Transaction ID that processed the step.")
47
+
48
+ # Relationships (foreign keys)
49
+ organization: Mapped[Optional["Organization"]] = relationship("Organization")
50
+ provider: Mapped[Optional["Provider"]] = relationship("Provider")
51
+ job: Mapped[Optional["Job"]] = relationship("Job", back_populates="steps")
52
+
53
+ # Relationships (backrefs)
54
+ messages: Mapped[List["Message"]] = relationship("Message", back_populates="step", cascade="save-update", lazy="noload")
@@ -23,6 +23,7 @@ class EmbeddingConfig(BaseModel):
23
23
  embedding_endpoint_type: Literal[
24
24
  "openai",
25
25
  "anthropic",
26
+ "bedrock",
26
27
  "cohere",
27
28
  "google_ai",
28
29
  "azure",
@@ -217,3 +217,27 @@ LettaMessageUnion = Annotated[
217
217
  Union[SystemMessage, UserMessage, ReasoningMessage, ToolCallMessage, ToolReturnMessage, AssistantMessage],
218
218
  Field(discriminator="message_type"),
219
219
  ]
220
+
221
+
222
+ def create_letta_message_union_schema():
223
+ return {
224
+ "oneOf": [
225
+ {"$ref": "#/components/schemas/SystemMessage-Output"},
226
+ {"$ref": "#/components/schemas/UserMessage-Output"},
227
+ {"$ref": "#/components/schemas/ReasoningMessage"},
228
+ {"$ref": "#/components/schemas/ToolCallMessage"},
229
+ {"$ref": "#/components/schemas/ToolReturnMessage"},
230
+ {"$ref": "#/components/schemas/AssistantMessage-Output"},
231
+ ],
232
+ "discriminator": {
233
+ "propertyName": "message_type",
234
+ "mapping": {
235
+ "system_message": "#/components/schemas/SystemMessage-Output",
236
+ "user_message": "#/components/schemas/UserMessage-Output",
237
+ "reasoning_message": "#/components/schemas/ReasoningMessage",
238
+ "tool_call_message": "#/components/schemas/ToolCallMessage",
239
+ "tool_return_message": "#/components/schemas/ToolReturnMessage",
240
+ "assistant_message": "#/components/schemas/AssistantMessage-Output",
241
+ },
242
+ },
243
+ }
@@ -28,15 +28,7 @@ class LettaResponse(BaseModel):
28
28
  description="The messages returned by the agent.",
29
29
  json_schema_extra={
30
30
  "items": {
31
- "oneOf": [
32
- {"$ref": "#/components/schemas/SystemMessage-Output"},
33
- {"$ref": "#/components/schemas/UserMessage-Output"},
34
- {"$ref": "#/components/schemas/ReasoningMessage"},
35
- {"$ref": "#/components/schemas/ToolCallMessage"},
36
- {"$ref": "#/components/schemas/ToolReturnMessage"},
37
- {"$ref": "#/components/schemas/AssistantMessage-Output"},
38
- ],
39
- "discriminator": {"propertyName": "message_type"},
31
+ "$ref": "#/components/schemas/LettaMessageUnion",
40
32
  }
41
33
  },
42
34
  )
@@ -36,6 +36,7 @@ class LLMConfig(BaseModel):
36
36
  "hugging-face",
37
37
  "mistral",
38
38
  "together", # completions endpoint
39
+ "bedrock",
39
40
  ] = Field(..., description="The endpoint type for the model.")
40
41
  model_endpoint: Optional[str] = Field(None, description="The endpoint for the model.")
41
42
  model_wrapper: Optional[str] = Field(None, description="The wrapper for the model.")
letta/schemas/message.py CHANGED
@@ -99,6 +99,7 @@ class Message(BaseMessage):
99
99
  name: Optional[str] = Field(None, description="The name of the participant.")
100
100
  tool_calls: Optional[List[ToolCall]] = Field(None, description="The list of tool calls requested.")
101
101
  tool_call_id: Optional[str] = Field(None, description="The id of the tool call.")
102
+ step_id: Optional[str] = Field(None, description="The id of the step that this message was created in.")
102
103
  # This overrides the optional base orm schema, created_at MUST exist on all messages objects
103
104
  created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
104
105
 
@@ -168,7 +168,23 @@ class OpenAIProvider(Provider):
168
168
  embedding_dim=1536,
169
169
  embedding_chunk_size=300,
170
170
  handle=self.get_handle("text-embedding-ada-002"),
171
- )
171
+ ),
172
+ EmbeddingConfig(
173
+ embedding_model="text-embedding-3-small",
174
+ embedding_endpoint_type="openai",
175
+ embedding_endpoint="https://api.openai.com/v1",
176
+ embedding_dim=2000,
177
+ embedding_chunk_size=300,
178
+ handle=self.get_handle("text-embedding-3-small"),
179
+ ),
180
+ EmbeddingConfig(
181
+ embedding_model="text-embedding-3-large",
182
+ embedding_endpoint_type="openai",
183
+ embedding_endpoint="https://api.openai.com/v1",
184
+ embedding_dim=2000,
185
+ embedding_chunk_size=300,
186
+ handle=self.get_handle("text-embedding-3-large"),
187
+ ),
172
188
  ]
173
189
 
174
190
  def get_model_context_window_size(self, model_name: str):
@@ -598,8 +614,13 @@ class AzureProvider(Provider):
598
614
  context_window_size = self.get_model_context_window(model_name)
599
615
  model_endpoint = get_azure_chat_completions_endpoint(self.base_url, model_name, self.api_version)
600
616
  configs.append(
601
- LLMConfig(model=model_name, model_endpoint_type="azure", model_endpoint=model_endpoint, context_window=context_window_size),
602
- handle=self.get_handle(model_name),
617
+ LLMConfig(
618
+ model=model_name,
619
+ model_endpoint_type="azure",
620
+ model_endpoint=model_endpoint,
621
+ context_window=context_window_size,
622
+ handle=self.get_handle(model_name),
623
+ ),
603
624
  )
604
625
  return configs
605
626
 
@@ -699,3 +720,39 @@ class VLLMCompletionsProvider(Provider):
699
720
 
700
721
  class CohereProvider(OpenAIProvider):
701
722
  pass
723
+
724
+
725
+ class AnthropicBedrockProvider(Provider):
726
+ name: str = "bedrock"
727
+ aws_region: str = Field(..., description="AWS region for Bedrock")
728
+
729
+ def list_llm_models(self):
730
+ from letta.llm_api.aws_bedrock import bedrock_get_model_list
731
+
732
+ models = bedrock_get_model_list(self.aws_region)
733
+
734
+ configs = []
735
+ for model_summary in models:
736
+ model_arn = model_summary["inferenceProfileArn"]
737
+ configs.append(
738
+ LLMConfig(
739
+ model=model_arn,
740
+ model_endpoint_type=self.name,
741
+ model_endpoint=None,
742
+ context_window=self.get_model_context_window(model_arn),
743
+ handle=self.get_handle(model_arn),
744
+ )
745
+ )
746
+ return configs
747
+
748
+ def list_embedding_models(self):
749
+ return []
750
+
751
+ def get_model_context_window(self, model_name: str) -> Optional[int]:
752
+ # Context windows for Claude models
753
+ from letta.llm_api.aws_bedrock import bedrock_get_model_context_window
754
+
755
+ return bedrock_get_model_context_window(model_name)
756
+
757
+ def get_handle(self, model_name: str) -> str:
758
+ return f"anthropic/{model_name}"
letta/schemas/step.py ADDED
@@ -0,0 +1,31 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from pydantic import Field
4
+
5
+ from letta.schemas.letta_base import LettaBase
6
+ from letta.schemas.message import Message
7
+
8
+
9
+ class StepBase(LettaBase):
10
+ __id_prefix__ = "step"
11
+
12
+
13
+ class Step(StepBase):
14
+ id: str = Field(..., description="The id of the step. Assigned by the database.")
15
+ origin: Optional[str] = Field(None, description="The surface that this agent step was initiated from.")
16
+ organization_id: Optional[str] = Field(None, description="The unique identifier of the organization associated with the step.")
17
+ provider_id: Optional[str] = Field(None, description="The unique identifier of the provider that was configured for this step")
18
+ job_id: Optional[str] = Field(
19
+ None, description="The unique identifier of the job that this step belongs to. Only included for async calls."
20
+ )
21
+ provider_name: Optional[str] = Field(None, description="The name of the provider used for this step.")
22
+ model: Optional[str] = Field(None, description="The name of the model used for this step.")
23
+ context_window_limit: Optional[int] = Field(None, description="The context window limit configured for this step.")
24
+ completion_tokens: Optional[int] = Field(None, description="The number of tokens generated by the agent during this step.")
25
+ prompt_tokens: Optional[int] = Field(None, description="The number of tokens in the prompt during this step.")
26
+ total_tokens: Optional[int] = Field(None, description="The total number of tokens processed by the agent during this step.")
27
+ completion_tokens_details: Optional[Dict] = Field(None, description="Metadata for the agent.")
28
+
29
+ tags: List[str] = Field([], description="Metadata tags.")
30
+ tid: Optional[str] = Field(None, description="The unique identifier of the transaction that processed this step.")
31
+ messages: List[Message] = Field([], description="The messages generated during this step.")
@@ -13,9 +13,10 @@ from starlette.middleware.cors import CORSMiddleware
13
13
 
14
14
  from letta.__init__ import __version__
15
15
  from letta.constants import ADMIN_PREFIX, API_PREFIX, OPENAI_API_PREFIX
16
- from letta.errors import LettaAgentNotFoundError, LettaUserNotFoundError
16
+ from letta.errors import BedrockPermissionError, LettaAgentNotFoundError, LettaUserNotFoundError
17
17
  from letta.log import get_logger
18
18
  from letta.orm.errors import DatabaseTimeoutError, ForeignKeyConstraintViolationError, NoResultFound, UniqueConstraintViolationError
19
+ from letta.schemas.letta_message import create_letta_message_union_schema
19
20
  from letta.server.constants import REST_DEFAULT_PORT
20
21
 
21
22
  # NOTE(charles): these are extra routes that are not part of v1 but we still need to mount to pass tests
@@ -67,6 +68,7 @@ def generate_openapi_schema(app: FastAPI):
67
68
  openai_docs["info"]["title"] = "OpenAI Assistants API"
68
69
  letta_docs["paths"] = {k: v for k, v in letta_docs["paths"].items() if not k.startswith("/openai")}
69
70
  letta_docs["info"]["title"] = "Letta API"
71
+ letta_docs["components"]["schemas"]["LettaMessageUnion"] = create_letta_message_union_schema()
70
72
 
71
73
  # Split the API docs into Letta API, and OpenAI Assistants compatible API
72
74
  for name, docs in [
@@ -144,7 +146,7 @@ def create_application() -> "FastAPI":
144
146
  log.error(f"Unhandled error: {exc}", exc_info=True)
145
147
 
146
148
  # Print the stack trace
147
- print(f"Stack trace: {exc.__traceback__}")
149
+ print(f"Stack trace: {exc}")
148
150
  if (os.getenv("SENTRY_DSN") is not None) and (os.getenv("SENTRY_DSN") != ""):
149
151
  import sentry_sdk
150
152
 
@@ -206,6 +208,19 @@ def create_application() -> "FastAPI":
206
208
  async def user_not_found_handler(request: Request, exc: LettaUserNotFoundError):
207
209
  return JSONResponse(status_code=404, content={"detail": "User not found"})
208
210
 
211
+ @app.exception_handler(BedrockPermissionError)
212
+ async def bedrock_permission_error_handler(request, exc: BedrockPermissionError):
213
+ return JSONResponse(
214
+ status_code=403,
215
+ content={
216
+ "error": {
217
+ "type": "bedrock_permission_denied",
218
+ "message": "Unable to access the required AI model. Please check your Bedrock permissions or contact support.",
219
+ "details": {"model_arn": exc.model_arn, "reason": str(exc)},
220
+ }
221
+ },
222
+ )
223
+
209
224
  settings.cors_origins.append("https://app.letta.com")
210
225
 
211
226
  if (os.getenv("LETTA_SERVER_SECURE") == "true") or "--secure" in sys.argv:
@@ -275,6 +290,8 @@ def start_server(
275
290
  server_logger.addHandler(stream_handler)
276
291
 
277
292
  if (os.getenv("LOCAL_HTTPS") == "true") or "--localhttps" in sys.argv:
293
+ print(f"▶ Server running at: https://{host or 'localhost'}:{port or REST_DEFAULT_PORT}\n")
294
+ print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard")
278
295
  uvicorn.run(
279
296
  app,
280
297
  host=host or "localhost",
@@ -282,13 +299,11 @@ def start_server(
282
299
  ssl_keyfile="certs/localhost-key.pem",
283
300
  ssl_certfile="certs/localhost.pem",
284
301
  )
285
- print(f"▶ Server running at: https://{host or 'localhost'}:{port or REST_DEFAULT_PORT}\n")
286
302
  else:
303
+ print(f"▶ Server running at: http://{host or 'localhost'}:{port or REST_DEFAULT_PORT}\n")
304
+ print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard")
287
305
  uvicorn.run(
288
306
  app,
289
307
  host=host or "localhost",
290
308
  port=port or REST_DEFAULT_PORT,
291
309
  )
292
- print(f"▶ Server running at: http://{host or 'localhost'}:{port or REST_DEFAULT_PORT}\n")
293
-
294
- print(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard")
@@ -1,5 +1,5 @@
1
1
  from datetime import datetime
2
- from typing import List, Optional, Union
2
+ from typing import Annotated, List, Optional, Union
3
3
 
4
4
  from fastapi import APIRouter, BackgroundTasks, Body, Depends, Header, HTTPException, Query, status
5
5
  from fastapi.responses import JSONResponse
@@ -428,7 +428,20 @@ def delete_agent_archival_memory(
428
428
  return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Memory id={memory_id} successfully deleted"})
429
429
 
430
430
 
431
- @router.get("/{agent_id}/messages", response_model=Union[List[Message], List[LettaMessageUnion]], operation_id="list_agent_messages")
431
+ AgentMessagesResponse = Annotated[
432
+ Union[List[Message], List[LettaMessageUnion]],
433
+ Field(
434
+ json_schema_extra={
435
+ "anyOf": [
436
+ {"type": "array", "items": {"$ref": "#/components/schemas/letta__schemas__message__Message"}},
437
+ {"type": "array", "items": {"$ref": "#/components/schemas/LettaMessageUnion"}},
438
+ ]
439
+ }
440
+ ),
441
+ ]
442
+
443
+
444
+ @router.get("/{agent_id}/messages", response_model=AgentMessagesResponse, operation_id="list_agent_messages")
432
445
  def get_agent_messages(
433
446
  agent_id: str,
434
447
  server: "SyncServer" = Depends(get_letta_server),
@@ -18,7 +18,7 @@ def list_llm_backends(
18
18
  ):
19
19
 
20
20
  models = server.list_llm_models()
21
- print(models)
21
+ # print(models)
22
22
  return models
23
23
 
24
24
 
@@ -28,5 +28,5 @@ def list_embedding_backends(
28
28
  ):
29
29
 
30
30
  models = server.list_embedding_models()
31
- print(models)
31
+ # print(models)
32
32
  return models
@@ -1,6 +1,7 @@
1
- from typing import List, Optional
1
+ from typing import Annotated, List, Optional
2
2
 
3
3
  from fastapi import APIRouter, Depends, Header, HTTPException, Query
4
+ from pydantic import Field
4
5
 
5
6
  from letta.orm.enums import JobType
6
7
  from letta.orm.errors import NoResultFound
@@ -60,7 +61,16 @@ def get_run(
60
61
  raise HTTPException(status_code=404, detail="Run not found")
61
62
 
62
63
 
63
- @router.get("/{run_id}/messages", response_model=List[LettaMessageUnion], operation_id="get_run_messages")
64
+ RunMessagesResponse = Annotated[
65
+ List[LettaMessageUnion], Field(json_schema_extra={"type": "array", "items": {"$ref": "#/components/schemas/LettaMessageUnion"}})
66
+ ]
67
+
68
+
69
+ @router.get(
70
+ "/{run_id}/messages",
71
+ response_model=RunMessagesResponse,
72
+ operation_id="get_run_messages",
73
+ )
64
74
  async def get_run_messages(
65
75
  run_id: str,
66
76
  server: "SyncServer" = Depends(get_letta_server),