letta-nightly 0.6.9.dev20250119103943__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.
- letta/__init__.py +1 -1
- letta/agent.py +40 -23
- letta/client/client.py +10 -2
- letta/errors.py +14 -0
- letta/functions/ast_parsers.py +105 -0
- letta/llm_api/anthropic.py +130 -82
- letta/llm_api/aws_bedrock.py +134 -0
- letta/llm_api/llm_api_tools.py +30 -7
- letta/orm/__init__.py +1 -0
- letta/orm/job.py +2 -4
- letta/orm/message.py +5 -1
- letta/orm/step.py +54 -0
- letta/schemas/embedding_config.py +1 -0
- letta/schemas/letta_message.py +24 -0
- letta/schemas/letta_response.py +1 -9
- letta/schemas/llm_config.py +1 -0
- letta/schemas/message.py +1 -0
- letta/schemas/providers.py +60 -3
- letta/schemas/step.py +31 -0
- letta/server/rest_api/app.py +21 -6
- letta/server/rest_api/routers/v1/agents.py +15 -2
- letta/server/rest_api/routers/v1/llms.py +2 -2
- letta/server/rest_api/routers/v1/runs.py +12 -2
- letta/server/server.py +9 -3
- letta/services/agent_manager.py +4 -3
- letta/services/job_manager.py +11 -13
- letta/services/provider_manager.py +19 -7
- letta/services/step_manager.py +87 -0
- letta/settings.py +21 -1
- {letta_nightly-0.6.9.dev20250119103943.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/METADATA +8 -6
- {letta_nightly-0.6.9.dev20250119103943.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/RECORD +34 -30
- letta/credentials.py +0 -149
- {letta_nightly-0.6.9.dev20250119103943.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.9.dev20250119103943.dist-info → letta_nightly-0.6.10.dev20250120193553.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.9.dev20250119103943.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
|
+
"""
|
letta/llm_api/llm_api_tools.py
CHANGED
|
@@ -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
|
-
|
|
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")
|
letta/schemas/letta_message.py
CHANGED
|
@@ -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
|
+
}
|
letta/schemas/letta_response.py
CHANGED
|
@@ -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
|
-
"
|
|
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
|
)
|
letta/schemas/llm_config.py
CHANGED
|
@@ -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
|
|
letta/schemas/providers.py
CHANGED
|
@@ -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(
|
|
602
|
-
|
|
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.")
|
letta/server/rest_api/app.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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),
|