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.
Files changed (70) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +8 -12
  3. letta/agents/exceptions.py +6 -0
  4. letta/agents/helpers.py +1 -1
  5. letta/agents/letta_agent.py +48 -35
  6. letta/agents/letta_agent_batch.py +6 -2
  7. letta/agents/voice_agent.py +41 -59
  8. letta/agents/{ephemeral_memory_agent.py → voice_sleeptime_agent.py} +106 -129
  9. letta/client/client.py +3 -3
  10. letta/constants.py +18 -2
  11. letta/functions/composio_helpers.py +100 -0
  12. letta/functions/function_sets/base.py +0 -10
  13. letta/functions/function_sets/voice.py +92 -0
  14. letta/functions/functions.py +4 -2
  15. letta/functions/helpers.py +19 -101
  16. letta/groups/helpers.py +1 -0
  17. letta/groups/sleeptime_multi_agent.py +5 -1
  18. letta/helpers/message_helper.py +21 -4
  19. letta/helpers/tool_execution_helper.py +1 -1
  20. letta/interfaces/anthropic_streaming_interface.py +165 -158
  21. letta/interfaces/openai_chat_completions_streaming_interface.py +1 -1
  22. letta/llm_api/anthropic.py +15 -10
  23. letta/llm_api/anthropic_client.py +5 -1
  24. letta/llm_api/google_vertex_client.py +1 -1
  25. letta/llm_api/llm_api_tools.py +7 -0
  26. letta/llm_api/llm_client.py +12 -2
  27. letta/llm_api/llm_client_base.py +4 -0
  28. letta/llm_api/openai.py +9 -3
  29. letta/llm_api/openai_client.py +18 -4
  30. letta/memory.py +3 -1
  31. letta/orm/enums.py +1 -0
  32. letta/orm/group.py +2 -0
  33. letta/orm/provider.py +10 -0
  34. letta/personas/examples/voice_memory_persona.txt +5 -0
  35. letta/prompts/system/voice_chat.txt +29 -0
  36. letta/prompts/system/voice_sleeptime.txt +74 -0
  37. letta/schemas/agent.py +14 -2
  38. letta/schemas/enums.py +11 -0
  39. letta/schemas/group.py +37 -2
  40. letta/schemas/llm_config.py +1 -0
  41. letta/schemas/llm_config_overrides.py +2 -2
  42. letta/schemas/message.py +4 -3
  43. letta/schemas/providers.py +75 -213
  44. letta/schemas/tool.py +8 -12
  45. letta/server/rest_api/app.py +12 -0
  46. letta/server/rest_api/chat_completions_interface.py +1 -1
  47. letta/server/rest_api/interface.py +8 -10
  48. letta/server/rest_api/{optimistic_json_parser.py → json_parser.py} +62 -26
  49. letta/server/rest_api/routers/v1/agents.py +1 -1
  50. letta/server/rest_api/routers/v1/embeddings.py +4 -3
  51. letta/server/rest_api/routers/v1/llms.py +4 -3
  52. letta/server/rest_api/routers/v1/providers.py +4 -1
  53. letta/server/rest_api/routers/v1/voice.py +0 -2
  54. letta/server/rest_api/utils.py +22 -33
  55. letta/server/server.py +91 -37
  56. letta/services/agent_manager.py +14 -7
  57. letta/services/group_manager.py +61 -0
  58. letta/services/helpers/agent_manager_helper.py +69 -12
  59. letta/services/message_manager.py +2 -2
  60. letta/services/passage_manager.py +13 -4
  61. letta/services/provider_manager.py +25 -14
  62. letta/services/summarizer/summarizer.py +20 -15
  63. letta/services/tool_executor/tool_execution_manager.py +1 -1
  64. letta/services/tool_executor/tool_executor.py +3 -3
  65. letta/services/tool_manager.py +32 -7
  66. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/METADATA +4 -5
  67. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/RECORD +70 -64
  68. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/LICENSE +0 -0
  69. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/WHEEL +0 -0
  70. {letta_nightly-0.7.6.dev20250430104233.dist-info → letta_nightly-0.7.8.dev20250501064110.dist-info}/entry_points.txt +0 -0
@@ -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
- name: str = "openai"
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
- name: str = "deepseek"
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
- name: str = "lmstudio-openai"
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
- name: str = "xai"
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
- name: str = "anthropic"
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
- name: str = "mistral"
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
- name: str = "ollama"
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
- name: str = "groq"
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
- name: str = "together"
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
- name: str = "google_ai"
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
- name: str = "google_vertex"
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
- name: str = "azure"
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
- name: str = "vllm"
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
- name: str = "vllm"
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
- name: str = "bedrock"
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.name,
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.LETTA_SLEEPTIME_CORE}:
108
- # If it's letta sleeptime core tool, we generate the json_schema on the fly here
109
- self.json_schema = get_json_schema_from_module(module_name=LETTA_CORE_TOOL_MODULE_NAME, function_name=self.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)
@@ -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.optimistic_json_parser import OptimisticJSONParser
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__)