letta-nightly 0.7.6.dev20250430104233__py3-none-any.whl → 0.7.8.dev20250501064110__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.
- letta/__init__.py +1 -1
- letta/agent.py +8 -12
- letta/agents/exceptions.py +6 -0
- letta/agents/helpers.py +1 -1
- letta/agents/letta_agent.py +48 -35
- letta/agents/letta_agent_batch.py +6 -2
- letta/agents/voice_agent.py +41 -59
- letta/agents/{ephemeral_memory_agent.py → voice_sleeptime_agent.py} +106 -129
- letta/client/client.py +3 -3
- letta/constants.py +18 -2
- letta/functions/composio_helpers.py +100 -0
- letta/functions/function_sets/base.py +0 -10
- letta/functions/function_sets/voice.py +92 -0
- letta/functions/functions.py +4 -2
- letta/functions/helpers.py +19 -101
- letta/groups/helpers.py +1 -0
- letta/groups/sleeptime_multi_agent.py +5 -1
- letta/helpers/message_helper.py +21 -4
- letta/helpers/tool_execution_helper.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +165 -158
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
- letta/llm_api/anthropic.py +15 -10
- letta/llm_api/anthropic_client.py +5 -1
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/llm_api_tools.py +7 -0
- letta/llm_api/llm_client.py +12 -2
- letta/llm_api/llm_client_base.py +4 -0
- letta/llm_api/openai.py +9 -3
- letta/llm_api/openai_client.py +18 -4
- letta/memory.py +3 -1
- letta/orm/enums.py +1 -0
- letta/orm/group.py +2 -0
- letta/orm/provider.py +10 -0
- letta/personas/examples/voice_memory_persona.txt +5 -0
- letta/prompts/system/voice_chat.txt +29 -0
- letta/prompts/system/voice_sleeptime.txt +74 -0
- letta/schemas/agent.py +14 -2
- letta/schemas/enums.py +11 -0
- letta/schemas/group.py +37 -2
- letta/schemas/llm_config.py +1 -0
- letta/schemas/llm_config_overrides.py +2 -2
- letta/schemas/message.py +4 -3
- letta/schemas/providers.py +75 -213
- letta/schemas/tool.py +8 -12
- letta/server/rest_api/app.py +12 -0
- letta/server/rest_api/chat_completions_interface.py +1 -1
- letta/server/rest_api/interface.py +8 -10
- letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
- letta/server/rest_api/routers/v1/agents.py +1 -1
- letta/server/rest_api/routers/v1/embeddings.py +4 -3
- letta/server/rest_api/routers/v1/llms.py +4 -3
- letta/server/rest_api/routers/v1/providers.py +4 -1
- letta/server/rest_api/routers/v1/voice.py +0 -2
- letta/server/rest_api/utils.py +22 -33
- letta/server/server.py +91 -37
- letta/services/agent_manager.py +14 -7
- letta/services/group_manager.py +61 -0
- letta/services/helpers/agent_manager_helper.py +69 -12
- letta/services/message_manager.py +2 -2
- letta/services/passage_manager.py +13 -4
- letta/services/provider_manager.py +25 -14
- letta/services/summarizer/summarizer.py +20 -15
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -3
- letta/services/tool_manager.py +32 -7
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +70 -64
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/entry_points.txt +0 -0
letta/schemas/providers.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import warnings
|
2
2
|
from datetime import datetime
|
3
|
-
from typing import List, Optional
|
3
|
+
from typing import List, Literal, Optional
|
4
4
|
|
5
5
|
from pydantic import Field, model_validator
|
6
6
|
|
@@ -9,9 +9,11 @@ from letta.llm_api.azure_openai import get_azure_chat_completions_endpoint, get_
|
|
9
9
|
from letta.llm_api.azure_openai_constants import AZURE_MODEL_TO_CONTEXT_LENGTH
|
10
10
|
from letta.schemas.embedding_config import EmbeddingConfig
|
11
11
|
from letta.schemas.embedding_config_overrides import EMBEDDING_HANDLE_OVERRIDES
|
12
|
+
from letta.schemas.enums import ProviderType
|
12
13
|
from letta.schemas.letta_base import LettaBase
|
13
14
|
from letta.schemas.llm_config import LLMConfig
|
14
15
|
from letta.schemas.llm_config_overrides import LLM_HANDLE_OVERRIDES
|
16
|
+
from letta.settings import model_settings
|
15
17
|
|
16
18
|
|
17
19
|
class ProviderBase(LettaBase):
|
@@ -21,10 +23,18 @@ class ProviderBase(LettaBase):
|
|
21
23
|
class Provider(ProviderBase):
|
22
24
|
id: Optional[str] = Field(None, description="The id of the provider, lazily created by the database manager.")
|
23
25
|
name: str = Field(..., description="The name of the provider")
|
26
|
+
provider_type: ProviderType = Field(..., description="The type of the provider")
|
24
27
|
api_key: Optional[str] = Field(None, description="API key used for requests to the provider.")
|
28
|
+
base_url: Optional[str] = Field(None, description="Base URL for the provider.")
|
25
29
|
organization_id: Optional[str] = Field(None, description="The organization id of the user")
|
26
30
|
updated_at: Optional[datetime] = Field(None, description="The last update timestamp of the provider.")
|
27
31
|
|
32
|
+
@model_validator(mode="after")
|
33
|
+
def default_base_url(self):
|
34
|
+
if self.provider_type == ProviderType.openai and self.base_url is None:
|
35
|
+
self.base_url = model_settings.openai_api_base
|
36
|
+
return self
|
37
|
+
|
28
38
|
def resolve_identifier(self):
|
29
39
|
if not self.id:
|
30
40
|
self.id = ProviderBase.generate_id(prefix=ProviderBase.__id_prefix__)
|
@@ -59,9 +69,41 @@ class Provider(ProviderBase):
|
|
59
69
|
|
60
70
|
return f"{self.name}/{model_name}"
|
61
71
|
|
72
|
+
def cast_to_subtype(self):
|
73
|
+
match (self.provider_type):
|
74
|
+
case ProviderType.letta:
|
75
|
+
return LettaProvider(**self.model_dump(exclude_none=True))
|
76
|
+
case ProviderType.openai:
|
77
|
+
return OpenAIProvider(**self.model_dump(exclude_none=True))
|
78
|
+
case ProviderType.anthropic:
|
79
|
+
return AnthropicProvider(**self.model_dump(exclude_none=True))
|
80
|
+
case ProviderType.anthropic_bedrock:
|
81
|
+
return AnthropicBedrockProvider(**self.model_dump(exclude_none=True))
|
82
|
+
case ProviderType.ollama:
|
83
|
+
return OllamaProvider(**self.model_dump(exclude_none=True))
|
84
|
+
case ProviderType.google_ai:
|
85
|
+
return GoogleAIProvider(**self.model_dump(exclude_none=True))
|
86
|
+
case ProviderType.google_vertex:
|
87
|
+
return GoogleVertexProvider(**self.model_dump(exclude_none=True))
|
88
|
+
case ProviderType.azure:
|
89
|
+
return AzureProvider(**self.model_dump(exclude_none=True))
|
90
|
+
case ProviderType.groq:
|
91
|
+
return GroqProvider(**self.model_dump(exclude_none=True))
|
92
|
+
case ProviderType.together:
|
93
|
+
return TogetherProvider(**self.model_dump(exclude_none=True))
|
94
|
+
case ProviderType.vllm_chat_completions:
|
95
|
+
return VLLMChatCompletionsProvider(**self.model_dump(exclude_none=True))
|
96
|
+
case ProviderType.vllm_completions:
|
97
|
+
return VLLMCompletionsProvider(**self.model_dump(exclude_none=True))
|
98
|
+
case ProviderType.xai:
|
99
|
+
return XAIProvider(**self.model_dump(exclude_none=True))
|
100
|
+
case _:
|
101
|
+
raise ValueError(f"Unknown provider type: {self.provider_type}")
|
102
|
+
|
62
103
|
|
63
104
|
class ProviderCreate(ProviderBase):
|
64
105
|
name: str = Field(..., description="The name of the provider.")
|
106
|
+
provider_type: ProviderType = Field(..., description="The type of the provider.")
|
65
107
|
api_key: str = Field(..., description="API key used for requests to the provider.")
|
66
108
|
|
67
109
|
|
@@ -70,8 +112,7 @@ class ProviderUpdate(ProviderBase):
|
|
70
112
|
|
71
113
|
|
72
114
|
class LettaProvider(Provider):
|
73
|
-
|
74
|
-
name: str = "letta"
|
115
|
+
provider_type: Literal[ProviderType.letta] = Field(ProviderType.letta, description="The type of the provider.")
|
75
116
|
|
76
117
|
def list_llm_models(self) -> List[LLMConfig]:
|
77
118
|
return [
|
@@ -81,6 +122,7 @@ class LettaProvider(Provider):
|
|
81
122
|
model_endpoint=LETTA_MODEL_ENDPOINT,
|
82
123
|
context_window=8192,
|
83
124
|
handle=self.get_handle("letta-free"),
|
125
|
+
provider_name=self.name,
|
84
126
|
)
|
85
127
|
]
|
86
128
|
|
@@ -98,7 +140,7 @@ class LettaProvider(Provider):
|
|
98
140
|
|
99
141
|
|
100
142
|
class OpenAIProvider(Provider):
|
101
|
-
|
143
|
+
provider_type: Literal[ProviderType.openai] = Field(ProviderType.openai, description="The type of the provider.")
|
102
144
|
api_key: str = Field(..., description="API key for the OpenAI API.")
|
103
145
|
base_url: str = Field(..., description="Base URL for the OpenAI API.")
|
104
146
|
|
@@ -180,6 +222,7 @@ class OpenAIProvider(Provider):
|
|
180
222
|
model_endpoint=self.base_url,
|
181
223
|
context_window=context_window_size,
|
182
224
|
handle=self.get_handle(model_name),
|
225
|
+
provider_name=self.name,
|
183
226
|
)
|
184
227
|
)
|
185
228
|
|
@@ -235,7 +278,7 @@ class DeepSeekProvider(OpenAIProvider):
|
|
235
278
|
* It also does not support native function calling
|
236
279
|
"""
|
237
280
|
|
238
|
-
|
281
|
+
provider_type: Literal[ProviderType.deepseek] = Field(ProviderType.deepseek, description="The type of the provider.")
|
239
282
|
base_url: str = Field("https://api.deepseek.com/v1", description="Base URL for the DeepSeek API.")
|
240
283
|
api_key: str = Field(..., description="API key for the DeepSeek API.")
|
241
284
|
|
@@ -286,6 +329,7 @@ class DeepSeekProvider(OpenAIProvider):
|
|
286
329
|
context_window=context_window_size,
|
287
330
|
handle=self.get_handle(model_name),
|
288
331
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
332
|
+
provider_name=self.name,
|
289
333
|
)
|
290
334
|
)
|
291
335
|
|
@@ -297,7 +341,7 @@ class DeepSeekProvider(OpenAIProvider):
|
|
297
341
|
|
298
342
|
|
299
343
|
class LMStudioOpenAIProvider(OpenAIProvider):
|
300
|
-
|
344
|
+
provider_type: Literal[ProviderType.lmstudio_openai] = Field(ProviderType.lmstudio_openai, description="The type of the provider.")
|
301
345
|
base_url: str = Field(..., description="Base URL for the LMStudio OpenAI API.")
|
302
346
|
api_key: Optional[str] = Field(None, description="API key for the LMStudio API.")
|
303
347
|
|
@@ -423,7 +467,7 @@ class LMStudioOpenAIProvider(OpenAIProvider):
|
|
423
467
|
class XAIProvider(OpenAIProvider):
|
424
468
|
"""https://docs.x.ai/docs/api-reference"""
|
425
469
|
|
426
|
-
|
470
|
+
provider_type: Literal[ProviderType.xai] = Field(ProviderType.xai, description="The type of the provider.")
|
427
471
|
api_key: str = Field(..., description="API key for the xAI/Grok API.")
|
428
472
|
base_url: str = Field("https://api.x.ai/v1", description="Base URL for the xAI/Grok API.")
|
429
473
|
|
@@ -476,6 +520,7 @@ class XAIProvider(OpenAIProvider):
|
|
476
520
|
model_endpoint=self.base_url,
|
477
521
|
context_window=context_window_size,
|
478
522
|
handle=self.get_handle(model_name),
|
523
|
+
provider_name=self.name,
|
479
524
|
)
|
480
525
|
)
|
481
526
|
|
@@ -486,201 +531,8 @@ class XAIProvider(OpenAIProvider):
|
|
486
531
|
return []
|
487
532
|
|
488
533
|
|
489
|
-
class DeepSeekProvider(OpenAIProvider):
|
490
|
-
"""
|
491
|
-
DeepSeek ChatCompletions API is similar to OpenAI's reasoning API,
|
492
|
-
but with slight differences:
|
493
|
-
* For example, DeepSeek's API requires perfect interleaving of user/assistant
|
494
|
-
* It also does not support native function calling
|
495
|
-
"""
|
496
|
-
|
497
|
-
name: str = "deepseek"
|
498
|
-
base_url: str = Field("https://api.deepseek.com/v1", description="Base URL for the DeepSeek API.")
|
499
|
-
api_key: str = Field(..., description="API key for the DeepSeek API.")
|
500
|
-
|
501
|
-
def get_model_context_window_size(self, model_name: str) -> Optional[int]:
|
502
|
-
# DeepSeek doesn't return context window in the model listing,
|
503
|
-
# so these are hardcoded from their website
|
504
|
-
if model_name == "deepseek-reasoner":
|
505
|
-
return 64000
|
506
|
-
elif model_name == "deepseek-chat":
|
507
|
-
return 64000
|
508
|
-
else:
|
509
|
-
return None
|
510
|
-
|
511
|
-
def list_llm_models(self) -> List[LLMConfig]:
|
512
|
-
from letta.llm_api.openai import openai_get_model_list
|
513
|
-
|
514
|
-
response = openai_get_model_list(self.base_url, api_key=self.api_key)
|
515
|
-
|
516
|
-
if "data" in response:
|
517
|
-
data = response["data"]
|
518
|
-
else:
|
519
|
-
data = response
|
520
|
-
|
521
|
-
configs = []
|
522
|
-
for model in data:
|
523
|
-
assert "id" in model, f"DeepSeek model missing 'id' field: {model}"
|
524
|
-
model_name = model["id"]
|
525
|
-
|
526
|
-
# In case DeepSeek starts supporting it in the future:
|
527
|
-
if "context_length" in model:
|
528
|
-
# Context length is returned in OpenRouter as "context_length"
|
529
|
-
context_window_size = model["context_length"]
|
530
|
-
else:
|
531
|
-
context_window_size = self.get_model_context_window_size(model_name)
|
532
|
-
|
533
|
-
if not context_window_size:
|
534
|
-
warnings.warn(f"Couldn't find context window size for model {model_name}")
|
535
|
-
continue
|
536
|
-
|
537
|
-
# Not used for deepseek-reasoner, but otherwise is true
|
538
|
-
put_inner_thoughts_in_kwargs = False if model_name == "deepseek-reasoner" else True
|
539
|
-
|
540
|
-
configs.append(
|
541
|
-
LLMConfig(
|
542
|
-
model=model_name,
|
543
|
-
model_endpoint_type="deepseek",
|
544
|
-
model_endpoint=self.base_url,
|
545
|
-
context_window=context_window_size,
|
546
|
-
handle=self.get_handle(model_name),
|
547
|
-
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
548
|
-
)
|
549
|
-
)
|
550
|
-
|
551
|
-
return configs
|
552
|
-
|
553
|
-
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
554
|
-
# No embeddings supported
|
555
|
-
return []
|
556
|
-
|
557
|
-
|
558
|
-
class LMStudioOpenAIProvider(OpenAIProvider):
|
559
|
-
name: str = "lmstudio-openai"
|
560
|
-
base_url: str = Field(..., description="Base URL for the LMStudio OpenAI API.")
|
561
|
-
api_key: Optional[str] = Field(None, description="API key for the LMStudio API.")
|
562
|
-
|
563
|
-
def list_llm_models(self) -> List[LLMConfig]:
|
564
|
-
from letta.llm_api.openai import openai_get_model_list
|
565
|
-
|
566
|
-
# For LMStudio, we want to hit 'GET /api/v0/models' instead of 'GET /v1/models'
|
567
|
-
MODEL_ENDPOINT_URL = f"{self.base_url.strip('/v1')}/api/v0"
|
568
|
-
response = openai_get_model_list(MODEL_ENDPOINT_URL)
|
569
|
-
|
570
|
-
"""
|
571
|
-
Example response:
|
572
|
-
|
573
|
-
{
|
574
|
-
"object": "list",
|
575
|
-
"data": [
|
576
|
-
{
|
577
|
-
"id": "qwen2-vl-7b-instruct",
|
578
|
-
"object": "model",
|
579
|
-
"type": "vlm",
|
580
|
-
"publisher": "mlx-community",
|
581
|
-
"arch": "qwen2_vl",
|
582
|
-
"compatibility_type": "mlx",
|
583
|
-
"quantization": "4bit",
|
584
|
-
"state": "not-loaded",
|
585
|
-
"max_context_length": 32768
|
586
|
-
},
|
587
|
-
...
|
588
|
-
"""
|
589
|
-
if "data" not in response:
|
590
|
-
warnings.warn(f"LMStudio OpenAI model query response missing 'data' field: {response}")
|
591
|
-
return []
|
592
|
-
|
593
|
-
configs = []
|
594
|
-
for model in response["data"]:
|
595
|
-
assert "id" in model, f"Model missing 'id' field: {model}"
|
596
|
-
model_name = model["id"]
|
597
|
-
|
598
|
-
if "type" not in model:
|
599
|
-
warnings.warn(f"LMStudio OpenAI model missing 'type' field: {model}")
|
600
|
-
continue
|
601
|
-
elif model["type"] not in ["vlm", "llm"]:
|
602
|
-
continue
|
603
|
-
|
604
|
-
if "max_context_length" in model:
|
605
|
-
context_window_size = model["max_context_length"]
|
606
|
-
else:
|
607
|
-
warnings.warn(f"LMStudio OpenAI model missing 'max_context_length' field: {model}")
|
608
|
-
continue
|
609
|
-
|
610
|
-
configs.append(
|
611
|
-
LLMConfig(
|
612
|
-
model=model_name,
|
613
|
-
model_endpoint_type="openai",
|
614
|
-
model_endpoint=self.base_url,
|
615
|
-
context_window=context_window_size,
|
616
|
-
handle=self.get_handle(model_name),
|
617
|
-
)
|
618
|
-
)
|
619
|
-
|
620
|
-
return configs
|
621
|
-
|
622
|
-
def list_embedding_models(self) -> List[EmbeddingConfig]:
|
623
|
-
from letta.llm_api.openai import openai_get_model_list
|
624
|
-
|
625
|
-
# For LMStudio, we want to hit 'GET /api/v0/models' instead of 'GET /v1/models'
|
626
|
-
MODEL_ENDPOINT_URL = f"{self.base_url.strip('/v1')}/api/v0"
|
627
|
-
response = openai_get_model_list(MODEL_ENDPOINT_URL)
|
628
|
-
|
629
|
-
"""
|
630
|
-
Example response:
|
631
|
-
{
|
632
|
-
"object": "list",
|
633
|
-
"data": [
|
634
|
-
{
|
635
|
-
"id": "text-embedding-nomic-embed-text-v1.5",
|
636
|
-
"object": "model",
|
637
|
-
"type": "embeddings",
|
638
|
-
"publisher": "nomic-ai",
|
639
|
-
"arch": "nomic-bert",
|
640
|
-
"compatibility_type": "gguf",
|
641
|
-
"quantization": "Q4_0",
|
642
|
-
"state": "not-loaded",
|
643
|
-
"max_context_length": 2048
|
644
|
-
}
|
645
|
-
...
|
646
|
-
"""
|
647
|
-
if "data" not in response:
|
648
|
-
warnings.warn(f"LMStudio OpenAI model query response missing 'data' field: {response}")
|
649
|
-
return []
|
650
|
-
|
651
|
-
configs = []
|
652
|
-
for model in response["data"]:
|
653
|
-
assert "id" in model, f"Model missing 'id' field: {model}"
|
654
|
-
model_name = model["id"]
|
655
|
-
|
656
|
-
if "type" not in model:
|
657
|
-
warnings.warn(f"LMStudio OpenAI model missing 'type' field: {model}")
|
658
|
-
continue
|
659
|
-
elif model["type"] not in ["embeddings"]:
|
660
|
-
continue
|
661
|
-
|
662
|
-
if "max_context_length" in model:
|
663
|
-
context_window_size = model["max_context_length"]
|
664
|
-
else:
|
665
|
-
warnings.warn(f"LMStudio OpenAI model missing 'max_context_length' field: {model}")
|
666
|
-
continue
|
667
|
-
|
668
|
-
configs.append(
|
669
|
-
EmbeddingConfig(
|
670
|
-
embedding_model=model_name,
|
671
|
-
embedding_endpoint_type="openai",
|
672
|
-
embedding_endpoint=self.base_url,
|
673
|
-
embedding_dim=context_window_size,
|
674
|
-
embedding_chunk_size=300, # NOTE: max is 2048
|
675
|
-
handle=self.get_handle(model_name),
|
676
|
-
),
|
677
|
-
)
|
678
|
-
|
679
|
-
return configs
|
680
|
-
|
681
|
-
|
682
534
|
class AnthropicProvider(Provider):
|
683
|
-
|
535
|
+
provider_type: Literal[ProviderType.anthropic] = Field(ProviderType.anthropic, description="The type of the provider.")
|
684
536
|
api_key: str = Field(..., description="API key for the Anthropic API.")
|
685
537
|
base_url: str = "https://api.anthropic.com/v1"
|
686
538
|
|
@@ -756,6 +608,7 @@ class AnthropicProvider(Provider):
|
|
756
608
|
handle=self.get_handle(model["id"]),
|
757
609
|
put_inner_thoughts_in_kwargs=inner_thoughts_in_kwargs,
|
758
610
|
max_tokens=max_tokens,
|
611
|
+
provider_name=self.name,
|
759
612
|
)
|
760
613
|
)
|
761
614
|
return configs
|
@@ -765,7 +618,7 @@ class AnthropicProvider(Provider):
|
|
765
618
|
|
766
619
|
|
767
620
|
class MistralProvider(Provider):
|
768
|
-
|
621
|
+
provider_type: Literal[ProviderType.mistral] = Field(ProviderType.mistral, description="The type of the provider.")
|
769
622
|
api_key: str = Field(..., description="API key for the Mistral API.")
|
770
623
|
base_url: str = "https://api.mistral.ai/v1"
|
771
624
|
|
@@ -789,6 +642,7 @@ class MistralProvider(Provider):
|
|
789
642
|
model_endpoint=self.base_url,
|
790
643
|
context_window=model["max_context_length"],
|
791
644
|
handle=self.get_handle(model["id"]),
|
645
|
+
provider_name=self.name,
|
792
646
|
)
|
793
647
|
)
|
794
648
|
|
@@ -815,7 +669,7 @@ class OllamaProvider(OpenAIProvider):
|
|
815
669
|
See: https://github.com/ollama/ollama/blob/main/docs/api.md#generate-a-completion
|
816
670
|
"""
|
817
671
|
|
818
|
-
|
672
|
+
provider_type: Literal[ProviderType.ollama] = Field(ProviderType.ollama, description="The type of the provider.")
|
819
673
|
base_url: str = Field(..., description="Base URL for the Ollama API.")
|
820
674
|
api_key: Optional[str] = Field(None, description="API key for the Ollama API (default: `None`).")
|
821
675
|
default_prompt_formatter: str = Field(
|
@@ -845,6 +699,7 @@ class OllamaProvider(OpenAIProvider):
|
|
845
699
|
model_wrapper=self.default_prompt_formatter,
|
846
700
|
context_window=context_window,
|
847
701
|
handle=self.get_handle(model["name"]),
|
702
|
+
provider_name=self.name,
|
848
703
|
)
|
849
704
|
)
|
850
705
|
return configs
|
@@ -927,7 +782,7 @@ class OllamaProvider(OpenAIProvider):
|
|
927
782
|
|
928
783
|
|
929
784
|
class GroqProvider(OpenAIProvider):
|
930
|
-
|
785
|
+
provider_type: Literal[ProviderType.groq] = Field(ProviderType.groq, description="The type of the provider.")
|
931
786
|
base_url: str = "https://api.groq.com/openai/v1"
|
932
787
|
api_key: str = Field(..., description="API key for the Groq API.")
|
933
788
|
|
@@ -946,6 +801,7 @@ class GroqProvider(OpenAIProvider):
|
|
946
801
|
model_endpoint=self.base_url,
|
947
802
|
context_window=model["context_window"],
|
948
803
|
handle=self.get_handle(model["id"]),
|
804
|
+
provider_name=self.name,
|
949
805
|
)
|
950
806
|
)
|
951
807
|
return configs
|
@@ -966,7 +822,7 @@ class TogetherProvider(OpenAIProvider):
|
|
966
822
|
function calling support is limited.
|
967
823
|
"""
|
968
824
|
|
969
|
-
|
825
|
+
provider_type: Literal[ProviderType.together] = Field(ProviderType.together, description="The type of the provider.")
|
970
826
|
base_url: str = "https://api.together.ai/v1"
|
971
827
|
api_key: str = Field(..., description="API key for the TogetherAI API.")
|
972
828
|
default_prompt_formatter: str = Field(..., description="Default prompt formatter (aka model wrapper) to use on vLLM /completions API.")
|
@@ -1014,6 +870,7 @@ class TogetherProvider(OpenAIProvider):
|
|
1014
870
|
model_wrapper=self.default_prompt_formatter,
|
1015
871
|
context_window=context_window_size,
|
1016
872
|
handle=self.get_handle(model_name),
|
873
|
+
provider_name=self.name,
|
1017
874
|
)
|
1018
875
|
)
|
1019
876
|
|
@@ -1067,7 +924,7 @@ class TogetherProvider(OpenAIProvider):
|
|
1067
924
|
|
1068
925
|
class GoogleAIProvider(Provider):
|
1069
926
|
# gemini
|
1070
|
-
|
927
|
+
provider_type: Literal[ProviderType.google_ai] = Field(ProviderType.google_ai, description="The type of the provider.")
|
1071
928
|
api_key: str = Field(..., description="API key for the Google AI API.")
|
1072
929
|
base_url: str = "https://generativelanguage.googleapis.com"
|
1073
930
|
|
@@ -1082,7 +939,6 @@ class GoogleAIProvider(Provider):
|
|
1082
939
|
# filter by model names
|
1083
940
|
model_options = [mo[len("models/") :] if mo.startswith("models/") else mo for mo in model_options]
|
1084
941
|
|
1085
|
-
# TODO remove manual filtering for gemini-pro
|
1086
942
|
# Add support for all gemini models
|
1087
943
|
model_options = [mo for mo in model_options if str(mo).startswith("gemini-")]
|
1088
944
|
|
@@ -1096,6 +952,7 @@ class GoogleAIProvider(Provider):
|
|
1096
952
|
context_window=self.get_model_context_window(model),
|
1097
953
|
handle=self.get_handle(model),
|
1098
954
|
max_tokens=8192,
|
955
|
+
provider_name=self.name,
|
1099
956
|
)
|
1100
957
|
)
|
1101
958
|
return configs
|
@@ -1131,7 +988,7 @@ class GoogleAIProvider(Provider):
|
|
1131
988
|
|
1132
989
|
|
1133
990
|
class GoogleVertexProvider(Provider):
|
1134
|
-
|
991
|
+
provider_type: Literal[ProviderType.google_vertex] = Field(ProviderType.google_vertex, description="The type of the provider.")
|
1135
992
|
google_cloud_project: str = Field(..., description="GCP project ID for the Google Vertex API.")
|
1136
993
|
google_cloud_location: str = Field(..., description="GCP region for the Google Vertex API.")
|
1137
994
|
|
@@ -1148,6 +1005,7 @@ class GoogleVertexProvider(Provider):
|
|
1148
1005
|
context_window=context_length,
|
1149
1006
|
handle=self.get_handle(model),
|
1150
1007
|
max_tokens=8192,
|
1008
|
+
provider_name=self.name,
|
1151
1009
|
)
|
1152
1010
|
)
|
1153
1011
|
return configs
|
@@ -1171,7 +1029,7 @@ class GoogleVertexProvider(Provider):
|
|
1171
1029
|
|
1172
1030
|
|
1173
1031
|
class AzureProvider(Provider):
|
1174
|
-
|
1032
|
+
provider_type: Literal[ProviderType.azure] = Field(ProviderType.azure, description="The type of the provider.")
|
1175
1033
|
latest_api_version: str = "2024-09-01-preview" # https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation
|
1176
1034
|
base_url: str = Field(
|
1177
1035
|
..., description="Base URL for the Azure API endpoint. This should be specific to your org, e.g. `https://letta.openai.azure.com`."
|
@@ -1204,6 +1062,7 @@ class AzureProvider(Provider):
|
|
1204
1062
|
model_endpoint=model_endpoint,
|
1205
1063
|
context_window=context_window_size,
|
1206
1064
|
handle=self.get_handle(model_name),
|
1065
|
+
provider_name=self.name,
|
1207
1066
|
),
|
1208
1067
|
)
|
1209
1068
|
return configs
|
@@ -1244,7 +1103,7 @@ class VLLMChatCompletionsProvider(Provider):
|
|
1244
1103
|
"""vLLM provider that treats vLLM as an OpenAI /chat/completions proxy"""
|
1245
1104
|
|
1246
1105
|
# NOTE: vLLM only serves one model at a time (so could configure that through env variables)
|
1247
|
-
|
1106
|
+
provider_type: Literal[ProviderType.vllm] = Field(ProviderType.vllm, description="The type of the provider.")
|
1248
1107
|
base_url: str = Field(..., description="Base URL for the vLLM API.")
|
1249
1108
|
|
1250
1109
|
def list_llm_models(self) -> List[LLMConfig]:
|
@@ -1263,6 +1122,7 @@ class VLLMChatCompletionsProvider(Provider):
|
|
1263
1122
|
model_endpoint=self.base_url,
|
1264
1123
|
context_window=model["max_model_len"],
|
1265
1124
|
handle=self.get_handle(model["id"]),
|
1125
|
+
provider_name=self.name,
|
1266
1126
|
)
|
1267
1127
|
)
|
1268
1128
|
return configs
|
@@ -1276,7 +1136,7 @@ class VLLMCompletionsProvider(Provider):
|
|
1276
1136
|
"""This uses /completions API as the backend, not /chat/completions, so we need to specify a model wrapper"""
|
1277
1137
|
|
1278
1138
|
# NOTE: vLLM only serves one model at a time (so could configure that through env variables)
|
1279
|
-
|
1139
|
+
provider_type: Literal[ProviderType.vllm] = Field(ProviderType.vllm, description="The type of the provider.")
|
1280
1140
|
base_url: str = Field(..., description="Base URL for the vLLM API.")
|
1281
1141
|
default_prompt_formatter: str = Field(..., description="Default prompt formatter (aka model wrapper) to use on vLLM /completions API.")
|
1282
1142
|
|
@@ -1296,6 +1156,7 @@ class VLLMCompletionsProvider(Provider):
|
|
1296
1156
|
model_wrapper=self.default_prompt_formatter,
|
1297
1157
|
context_window=model["max_model_len"],
|
1298
1158
|
handle=self.get_handle(model["id"]),
|
1159
|
+
provider_name=self.name,
|
1299
1160
|
)
|
1300
1161
|
)
|
1301
1162
|
return configs
|
@@ -1310,7 +1171,7 @@ class CohereProvider(OpenAIProvider):
|
|
1310
1171
|
|
1311
1172
|
|
1312
1173
|
class AnthropicBedrockProvider(Provider):
|
1313
|
-
|
1174
|
+
provider_type: Literal[ProviderType.bedrock] = Field(ProviderType.bedrock, description="The type of the provider.")
|
1314
1175
|
aws_region: str = Field(..., description="AWS region for Bedrock")
|
1315
1176
|
|
1316
1177
|
def list_llm_models(self):
|
@@ -1324,10 +1185,11 @@ class AnthropicBedrockProvider(Provider):
|
|
1324
1185
|
configs.append(
|
1325
1186
|
LLMConfig(
|
1326
1187
|
model=model_arn,
|
1327
|
-
model_endpoint_type=self.
|
1188
|
+
model_endpoint_type=self.provider_type.value,
|
1328
1189
|
model_endpoint=None,
|
1329
1190
|
context_window=self.get_model_context_window(model_arn),
|
1330
1191
|
handle=self.get_handle(model_arn),
|
1192
|
+
provider_name=self.name,
|
1331
1193
|
)
|
1332
1194
|
)
|
1333
1195
|
return configs
|
letta/schemas/tool.py
CHANGED
@@ -7,16 +7,13 @@ from letta.constants import (
|
|
7
7
|
FUNCTION_RETURN_CHAR_LIMIT,
|
8
8
|
LETTA_CORE_TOOL_MODULE_NAME,
|
9
9
|
LETTA_MULTI_AGENT_TOOL_MODULE_NAME,
|
10
|
+
LETTA_VOICE_TOOL_MODULE_NAME,
|
10
11
|
MCP_TOOL_TAG_NAME_PREFIX,
|
11
12
|
)
|
12
13
|
from letta.functions.ast_parsers import get_function_name_and_description
|
14
|
+
from letta.functions.composio_helpers import generate_composio_tool_wrapper
|
13
15
|
from letta.functions.functions import derive_openai_json_schema, get_json_schema_from_module
|
14
|
-
from letta.functions.helpers import
|
15
|
-
generate_composio_tool_wrapper,
|
16
|
-
generate_langchain_tool_wrapper,
|
17
|
-
generate_mcp_tool_wrapper,
|
18
|
-
generate_model_from_args_json_schema,
|
19
|
-
)
|
16
|
+
from letta.functions.helpers import generate_langchain_tool_wrapper, generate_mcp_tool_wrapper, generate_model_from_args_json_schema
|
20
17
|
from letta.functions.mcp_client.types import MCPTool
|
21
18
|
from letta.functions.schema_generator import (
|
22
19
|
generate_schema_from_args_schema_v2,
|
@@ -98,15 +95,15 @@ class Tool(BaseTool):
|
|
98
95
|
except Exception as e:
|
99
96
|
error_msg = f"Failed to derive json schema for tool with id={self.id} name={self.name}. Error: {str(e)}"
|
100
97
|
logger.error(error_msg)
|
101
|
-
elif self.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MEMORY_CORE}:
|
98
|
+
elif self.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MEMORY_CORE, ToolType.LETTA_SLEEPTIME_CORE}:
|
102
99
|
# If it's letta core tool, we generate the json_schema on the fly here
|
103
100
|
self.json_schema = get_json_schema_from_module(module_name=LETTA_CORE_TOOL_MODULE_NAME, function_name=self.name)
|
104
101
|
elif self.tool_type in {ToolType.LETTA_MULTI_AGENT_CORE}:
|
105
102
|
# If it's letta multi-agent tool, we also generate the json_schema on the fly here
|
106
103
|
self.json_schema = get_json_schema_from_module(module_name=LETTA_MULTI_AGENT_TOOL_MODULE_NAME, function_name=self.name)
|
107
|
-
elif self.tool_type in {ToolType.
|
108
|
-
# If it's letta
|
109
|
-
self.json_schema = get_json_schema_from_module(module_name=
|
104
|
+
elif self.tool_type in {ToolType.LETTA_VOICE_SLEEPTIME_CORE}:
|
105
|
+
# If it's letta voice tool, we generate the json_schema on the fly here
|
106
|
+
self.json_schema = get_json_schema_from_module(module_name=LETTA_VOICE_TOOL_MODULE_NAME, function_name=self.name)
|
110
107
|
|
111
108
|
# At this point, we need to validate that at least json_schema is populated
|
112
109
|
if not self.json_schema:
|
@@ -175,8 +172,7 @@ class ToolCreate(LettaBase):
|
|
175
172
|
Returns:
|
176
173
|
Tool: A Letta Tool initialized with attributes derived from the Composio tool.
|
177
174
|
"""
|
178
|
-
from composio import LogLevel
|
179
|
-
from composio_langchain import ComposioToolSet
|
175
|
+
from composio import ComposioToolSet, LogLevel
|
180
176
|
|
181
177
|
composio_toolset = ComposioToolSet(logging_level=LogLevel.ERROR, lock=False)
|
182
178
|
composio_action_schemas = composio_toolset.get_action_schemas(actions=[action_name], check_connected_accounts=False)
|
letta/server/rest_api/app.py
CHANGED
@@ -14,6 +14,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
|
14
14
|
from starlette.middleware.cors import CORSMiddleware
|
15
15
|
|
16
16
|
from letta.__init__ import __version__
|
17
|
+
from letta.agents.exceptions import IncompatibleAgentType
|
17
18
|
from letta.constants import ADMIN_PREFIX, API_PREFIX, OPENAI_API_PREFIX
|
18
19
|
from letta.errors import BedrockPermissionError, LettaAgentNotFoundError, LettaUserNotFoundError
|
19
20
|
from letta.jobs.scheduler import shutdown_cron_scheduler, start_cron_jobs
|
@@ -173,6 +174,17 @@ def create_application() -> "FastAPI":
|
|
173
174
|
def shutdown_scheduler():
|
174
175
|
shutdown_cron_scheduler()
|
175
176
|
|
177
|
+
@app.exception_handler(IncompatibleAgentType)
|
178
|
+
async def handle_incompatible_agent_type(request: Request, exc: IncompatibleAgentType):
|
179
|
+
return JSONResponse(
|
180
|
+
status_code=400,
|
181
|
+
content={
|
182
|
+
"detail": str(exc),
|
183
|
+
"expected_type": exc.expected_type,
|
184
|
+
"actual_type": exc.actual_type,
|
185
|
+
},
|
186
|
+
)
|
187
|
+
|
176
188
|
@app.exception_handler(Exception)
|
177
189
|
async def generic_error_handler(request: Request, exc: Exception):
|
178
190
|
# Log the actual error for debugging
|
@@ -12,7 +12,7 @@ from letta.schemas.enums import MessageStreamStatus
|
|
12
12
|
from letta.schemas.letta_message import LettaMessage
|
13
13
|
from letta.schemas.message import Message
|
14
14
|
from letta.schemas.openai.chat_completion_response import ChatCompletionChunkResponse
|
15
|
-
from letta.server.rest_api.
|
15
|
+
from letta.server.rest_api.json_parser import OptimisticJSONParser
|
16
16
|
from letta.streaming_interface import AgentChunkStreamingInterface
|
17
17
|
|
18
18
|
logger = get_logger(__name__)
|