Skip to content

Commit 9df782b

Browse files
feat(agentkit): update MLLM and LLM vendor wrappers for v2.7
Adds xAI Grok Realtime and Vertex AI MLLM wrappers, and aligns MLLM config serialization with the generated core types. LLM vendors now accept typed greeting_configs and serialize them through the generated model shape, including interruptable.
1 parent 6e30398 commit 9df782b

2 files changed

Lines changed: 113 additions & 36 deletions

File tree

src/agora_agent/agentkit/vendors/llm.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
from typing import Any, Dict, List, Optional
1+
from typing import Any, Dict, List, Optional, Union
22

33
from pydantic import BaseModel, ConfigDict, Field
44

5+
from ...agents.types.start_agents_request_properties_llm_greeting_configs import (
6+
StartAgentsRequestPropertiesLlmGreetingConfigs,
7+
)
58
from .base import BaseLLM
69

10+
LlmGreetingConfigs = Union[StartAgentsRequestPropertiesLlmGreetingConfigs, Dict[str, Any]]
11+
712

813
def _ensure_mcp_transport(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
914
"""Ensure each MCP server has transport set (API requires it). Default to streamable_http."""
@@ -15,6 +20,14 @@ def _ensure_mcp_transport(servers: List[Dict[str, Any]]) -> List[Dict[str, Any]]
1520
result.append(item)
1621
return result
1722

23+
24+
def _dump_optional_model(value: Any) -> Any:
25+
if hasattr(value, "model_dump"):
26+
return value.model_dump(exclude_none=True)
27+
if hasattr(value, "dict"):
28+
return value.dict(exclude_none=True)
29+
return value
30+
1831
class OpenAIOptions(BaseModel):
1932
model_config = ConfigDict(extra="forbid")
2033

@@ -31,7 +44,7 @@ class OpenAIOptions(BaseModel):
3144
params: Optional[Dict[str, Any]] = Field(default=None)
3245
headers: Optional[Dict[str, str]] = Field(default=None)
3346
output_modalities: Optional[List[str]] = Field(default=None)
34-
greeting_configs: Optional[Dict[str, Any]] = Field(default=None)
47+
greeting_configs: Optional[LlmGreetingConfigs] = Field(default=None)
3548
template_variables: Optional[Dict[str, str]] = Field(default=None)
3649
vendor: Optional[str] = Field(default=None)
3750
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
@@ -74,7 +87,7 @@ def to_config(self) -> Dict[str, Any]:
7487
if self.options.output_modalities is not None:
7588
config["output_modalities"] = self.options.output_modalities
7689
if self.options.greeting_configs is not None:
77-
config["greeting_configs"] = self.options.greeting_configs
90+
config["greeting_configs"] = _dump_optional_model(self.options.greeting_configs)
7891
if self.options.template_variables is not None:
7992
config["template_variables"] = self.options.template_variables
8093
if self.options.vendor is not None:
@@ -104,7 +117,7 @@ class AzureOpenAIOptions(BaseModel):
104117
params: Optional[Dict[str, Any]] = Field(default=None)
105118
headers: Optional[Dict[str, str]] = Field(default=None)
106119
output_modalities: Optional[List[str]] = Field(default=None)
107-
greeting_configs: Optional[Dict[str, Any]] = Field(default=None)
120+
greeting_configs: Optional[LlmGreetingConfigs] = Field(default=None)
108121
template_variables: Optional[Dict[str, str]] = Field(default=None)
109122
vendor: Optional[str] = Field(default=None)
110123
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
@@ -150,7 +163,7 @@ def to_config(self) -> Dict[str, Any]:
150163
if self.options.output_modalities is not None:
151164
config["output_modalities"] = self.options.output_modalities
152165
if self.options.greeting_configs is not None:
153-
config["greeting_configs"] = self.options.greeting_configs
166+
config["greeting_configs"] = _dump_optional_model(self.options.greeting_configs)
154167
if self.options.template_variables is not None:
155168
config["template_variables"] = self.options.template_variables
156169
if self.options.mcp_servers is not None:
@@ -177,7 +190,7 @@ class AnthropicOptions(BaseModel):
177190
params: Optional[Dict[str, Any]] = Field(default=None)
178191
headers: Optional[Dict[str, str]] = Field(default=None)
179192
output_modalities: Optional[List[str]] = Field(default=None)
180-
greeting_configs: Optional[Dict[str, Any]] = Field(default=None)
193+
greeting_configs: Optional[LlmGreetingConfigs] = Field(default=None)
181194
template_variables: Optional[Dict[str, str]] = Field(default=None)
182195
vendor: Optional[str] = Field(default=None)
183196
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
@@ -216,7 +229,7 @@ def to_config(self) -> Dict[str, Any]:
216229
if self.options.output_modalities is not None:
217230
config["output_modalities"] = self.options.output_modalities
218231
if self.options.greeting_configs is not None:
219-
config["greeting_configs"] = self.options.greeting_configs
232+
config["greeting_configs"] = _dump_optional_model(self.options.greeting_configs)
220233
if self.options.template_variables is not None:
221234
config["template_variables"] = self.options.template_variables
222235
if self.options.vendor is not None:
@@ -246,7 +259,7 @@ class GeminiOptions(BaseModel):
246259
params: Optional[Dict[str, Any]] = Field(default=None)
247260
headers: Optional[Dict[str, str]] = Field(default=None)
248261
output_modalities: Optional[List[str]] = Field(default=None)
249-
greeting_configs: Optional[Dict[str, Any]] = Field(default=None)
262+
greeting_configs: Optional[LlmGreetingConfigs] = Field(default=None)
250263
template_variables: Optional[Dict[str, str]] = Field(default=None)
251264
vendor: Optional[str] = Field(default=None)
252265
mcp_servers: Optional[List[Dict[str, Any]]] = Field(default=None)
@@ -287,7 +300,7 @@ def to_config(self) -> Dict[str, Any]:
287300
if self.options.output_modalities is not None:
288301
config["output_modalities"] = self.options.output_modalities
289302
if self.options.greeting_configs is not None:
290-
config["greeting_configs"] = self.options.greeting_configs
303+
config["greeting_configs"] = _dump_optional_model(self.options.greeting_configs)
291304
if self.options.template_variables is not None:
292305
config["template_variables"] = self.options.template_variables
293306
if self.options.vendor is not None:

src/agora_agent/agentkit/vendors/mllm.py

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from typing import Any, Dict, List, Optional
23

34
from pydantic import BaseModel, ConfigDict, Field
@@ -22,9 +23,7 @@ class OpenAIRealtimeOptions(BaseModel):
2223
messages: Optional[List[Dict[str, Any]]] = Field(default=None, description="Conversation messages")
2324
params: Optional[Dict[str, Any]] = Field(default=None, description="Additional parameters")
2425
turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None, description="MLLM turn detection configuration")
25-
predefined_tools: Optional[List[str]] = Field(default=None, description="Predefined tools")
2626
failure_message: Optional[str] = Field(default=None, description="Message played on failure")
27-
max_history: Optional[int] = Field(default=None, description="Maximum conversation history length")
2827

2928
class OpenAIRealtime(BaseMLLM):
3029
def __init__(self, **kwargs: Any):
@@ -53,18 +52,97 @@ def to_config(self) -> Dict[str, Any]:
5352
config["output_modalities"] = self.options.output_modalities
5453
if self.options.messages is not None:
5554
config["messages"] = self.options.messages
56-
if self.options.predefined_tools is not None:
57-
config["predefined_tools"] = self.options.predefined_tools
5855
if self.options.failure_message is not None:
5956
config["failure_message"] = self.options.failure_message
60-
if self.options.max_history is not None:
61-
config["max_history"] = self.options.max_history
6257
if self.options.turn_detection is not None:
6358
config["turn_detection"] = self.options.turn_detection
6459

6560
return config
6661

6762

63+
# xAI MLLM: use XaiGrok (product name, mllm.vendor "xai"). Do not use XaiRealtime—that name
64+
# is deprecated and reserved naming for future XaiSTT / XaiTTS cascading vendors.
65+
66+
67+
class XaiGrokOptions(BaseModel):
68+
model_config = ConfigDict(extra="forbid")
69+
70+
api_key: str = Field(..., description="xAI API key")
71+
url: str = Field(default="wss://api.x.ai/v1/realtime", description="xAI Realtime WebSocket URL")
72+
voice: Optional[str] = Field(default=None, description="Voice identifier (e.g., eve or rex)")
73+
language: Optional[str] = Field(default=None, description="Language code (e.g., en)")
74+
sample_rate: Optional[int] = Field(default=None, description="Audio sample rate in Hz")
75+
greeting_message: Optional[str] = Field(default=None, description="Agent greeting message")
76+
input_modalities: Optional[List[str]] = Field(default=None, description="Input modalities")
77+
output_modalities: Optional[List[str]] = Field(default=None, description="Output modalities")
78+
messages: Optional[List[Dict[str, Any]]] = Field(default=None, description="Conversation messages")
79+
params: Optional[Dict[str, Any]] = Field(default=None, description="Additional xAI parameters")
80+
turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None, description="MLLM turn detection configuration")
81+
failure_message: Optional[str] = Field(default=None, description="Message played on failure")
82+
83+
84+
class XaiGrok(BaseMLLM):
85+
"""xAI Grok MLLM vendor (`mllm.vendor`: ``xai``)."""
86+
87+
def __init__(self, **kwargs: Any):
88+
self.options = XaiGrokOptions(**kwargs)
89+
90+
def to_config(self) -> Dict[str, Any]:
91+
params: Dict[str, Any] = dict(self.options.params or {})
92+
if self.options.voice is not None:
93+
params["voice"] = self.options.voice
94+
if self.options.language is not None:
95+
params["language"] = self.options.language
96+
if self.options.sample_rate is not None:
97+
params["sample_rate"] = self.options.sample_rate
98+
99+
config: Dict[str, Any] = {
100+
"vendor": "xai",
101+
"api_key": self.options.api_key,
102+
"url": self.options.url,
103+
"params": params,
104+
}
105+
106+
if self.options.greeting_message is not None:
107+
config["greeting_message"] = self.options.greeting_message
108+
if self.options.input_modalities is not None:
109+
config["input_modalities"] = self.options.input_modalities
110+
if self.options.output_modalities is not None:
111+
config["output_modalities"] = self.options.output_modalities
112+
if self.options.messages is not None:
113+
config["messages"] = self.options.messages
114+
if self.options.failure_message is not None:
115+
config["failure_message"] = self.options.failure_message
116+
if self.options.turn_detection is not None:
117+
config["turn_detection"] = self.options.turn_detection
118+
119+
return config
120+
121+
122+
class XaiRealtimeOptions(XaiGrokOptions):
123+
"""Deprecated: use :class:`XaiGrokOptions` instead."""
124+
125+
def __init__(self, **data: Any):
126+
warnings.warn(
127+
"XaiRealtimeOptions is deprecated; use XaiGrokOptions instead.",
128+
DeprecationWarning,
129+
stacklevel=2,
130+
)
131+
super().__init__(**data)
132+
133+
134+
class XaiRealtime(XaiGrok):
135+
"""Deprecated: use :class:`XaiGrok` instead."""
136+
137+
def __init__(self, **kwargs: Any):
138+
warnings.warn(
139+
"XaiRealtime is deprecated; use XaiGrok instead.",
140+
DeprecationWarning,
141+
stacklevel=2,
142+
)
143+
super().__init__(**kwargs)
144+
145+
68146
class VertexAIOptions(BaseModel):
69147
model_config = ConfigDict(extra="forbid")
70148

@@ -81,28 +159,24 @@ class VertexAIOptions(BaseModel):
81159
messages: Optional[List[Dict[str, Any]]] = Field(default=None, description="Conversation messages")
82160
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional parameters")
83161
turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None, description="MLLM turn detection configuration")
84-
predefined_tools: Optional[List[str]] = Field(default=None, description="Predefined tools")
85162
failure_message: Optional[str] = Field(default=None, description="Message played on failure")
86-
max_history: Optional[int] = Field(default=None, description="Maximum conversation history length")
87163

88164
class VertexAI(BaseMLLM):
89165
def __init__(self, **kwargs: Any):
90166
self.options = VertexAIOptions(**kwargs)
91167

92168
def to_config(self) -> Dict[str, Any]:
93-
params: Dict[str, Any] = {
94-
"model": self.options.model,
95-
"project_id": self.options.project_id,
96-
"location": self.options.location,
97-
"adc_credentials_string": self.options.adc_credentials_string,
98-
}
99-
169+
# additional_params spread first so that explicit fields always win,
170+
# matching the TypeScript SDK.
171+
params: Dict[str, Any] = dict(self.options.additional_params or {})
172+
params["model"] = self.options.model
173+
params["project_id"] = self.options.project_id
174+
params["location"] = self.options.location
175+
params["adc_credentials_string"] = self.options.adc_credentials_string
100176
if self.options.instructions is not None:
101177
params["instructions"] = self.options.instructions
102178
if self.options.voice is not None:
103179
params["voice"] = self.options.voice
104-
if self.options.additional_params is not None:
105-
params.update(self.options.additional_params)
106180

107181
config: Dict[str, Any] = {
108182
"vendor": "vertexai",
@@ -119,12 +193,8 @@ def to_config(self) -> Dict[str, Any]:
119193
config["output_modalities"] = self.options.output_modalities
120194
if self.options.messages is not None:
121195
config["messages"] = self.options.messages
122-
if self.options.predefined_tools is not None:
123-
config["predefined_tools"] = self.options.predefined_tools
124196
if self.options.failure_message is not None:
125197
config["failure_message"] = self.options.failure_message
126-
if self.options.max_history is not None:
127-
config["max_history"] = self.options.max_history
128198
if self.options.turn_detection is not None:
129199
config["turn_detection"] = self.options.turn_detection
130200

@@ -145,9 +215,7 @@ class GeminiLiveOptions(BaseModel):
145215
messages: Optional[List[Dict[str, Any]]] = Field(default=None, description="Conversation messages")
146216
additional_params: Optional[Dict[str, Any]] = Field(default=None, description="Additional parameters")
147217
turn_detection: Optional[MllmTurnDetectionConfig] = Field(default=None, description="MLLM turn detection configuration")
148-
predefined_tools: Optional[List[str]] = Field(default=None, description="Predefined tools")
149218
failure_message: Optional[str] = Field(default=None, description="Message played on failure")
150-
max_history: Optional[int] = Field(default=None, description="Maximum conversation history length")
151219

152220
class GeminiLive(BaseMLLM):
153221
def __init__(self, **kwargs: Any):
@@ -179,12 +247,8 @@ def to_config(self) -> Dict[str, Any]:
179247
config["output_modalities"] = self.options.output_modalities
180248
if self.options.messages is not None:
181249
config["messages"] = self.options.messages
182-
if self.options.predefined_tools is not None:
183-
config["predefined_tools"] = self.options.predefined_tools
184250
if self.options.failure_message is not None:
185251
config["failure_message"] = self.options.failure_message
186-
if self.options.max_history is not None:
187-
config["max_history"] = self.options.max_history
188252
if self.options.turn_detection is not None:
189253
config["turn_detection"] = self.options.turn_detection
190254

0 commit comments

Comments
 (0)