agent-framework-declarative 1.0.0__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.
@@ -0,0 +1,868 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Callable, Mapping
7
+ from pathlib import Path
8
+ from typing import Any, cast
9
+
10
+ import yaml
11
+ from agent_framework import (
12
+ Agent,
13
+ SupportsChatGetResponse,
14
+ )
15
+ from agent_framework import (
16
+ FunctionTool as AFFunctionTool,
17
+ )
18
+ from agent_framework._feature_stage import (
19
+ ExperimentalFeature,
20
+ experimental,
21
+ )
22
+ from agent_framework.exceptions import AgentException
23
+ from dotenv import load_dotenv
24
+
25
+ from ._models import (
26
+ AnonymousConnection,
27
+ ApiKeyConnection,
28
+ CodeInterpreterTool,
29
+ FileSearchTool,
30
+ FunctionTool,
31
+ McpServerToolSpecifyApprovalMode,
32
+ McpTool,
33
+ Model,
34
+ ModelOptions,
35
+ PromptAgent,
36
+ ReferenceConnection,
37
+ RemoteConnection,
38
+ Tool,
39
+ WebSearchTool,
40
+ _safe_mode_context, # type: ignore[reportPrivateUsage]
41
+ agent_schema_dispatch,
42
+ )
43
+
44
+ if sys.version_info >= (3, 11):
45
+ from typing import TypedDict # pragma: no cover
46
+ else:
47
+ from typing_extensions import TypedDict # pragma: no cover
48
+
49
+
50
+ @experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS)
51
+ class ProviderTypeMapping(TypedDict, total=True):
52
+ package: str
53
+ name: str
54
+ model_field: str
55
+ endpoint_field: str | None
56
+ api_key_field: str | None
57
+
58
+
59
+ PROVIDER_TYPE_OBJECT_MAPPING: dict[str, ProviderTypeMapping] = {
60
+ "AzureOpenAI": {
61
+ "package": "agent_framework.openai",
62
+ "name": "OpenAIChatClient",
63
+ "model_field": "model",
64
+ "endpoint_field": "azure_endpoint",
65
+ "api_key_field": "api_key",
66
+ },
67
+ "AzureOpenAI.Chat": {
68
+ "package": "agent_framework.openai",
69
+ "name": "OpenAIChatCompletionClient",
70
+ "model_field": "model",
71
+ "endpoint_field": "azure_endpoint",
72
+ "api_key_field": "api_key",
73
+ },
74
+ "AzureOpenAI.Responses": {
75
+ "package": "agent_framework.openai",
76
+ "name": "OpenAIChatClient",
77
+ "model_field": "model",
78
+ "endpoint_field": "azure_endpoint",
79
+ "api_key_field": "api_key",
80
+ },
81
+ "Foundry": {
82
+ "package": "agent_framework.foundry",
83
+ "name": "FoundryChatClient",
84
+ "model_field": "model",
85
+ "endpoint_field": "project_endpoint",
86
+ "api_key_field": None,
87
+ },
88
+ "OpenAI.Chat": {
89
+ "package": "agent_framework.openai",
90
+ "name": "OpenAIChatCompletionClient",
91
+ "model_field": "model",
92
+ "endpoint_field": "base_url",
93
+ "api_key_field": "api_key",
94
+ },
95
+ "OpenAI.Responses": {
96
+ "package": "agent_framework.openai",
97
+ "name": "OpenAIChatClient",
98
+ "model_field": "model",
99
+ "endpoint_field": "base_url",
100
+ "api_key_field": "api_key",
101
+ },
102
+ "OpenAI": {
103
+ "package": "agent_framework.openai",
104
+ "name": "OpenAIChatClient",
105
+ "model_field": "model",
106
+ "endpoint_field": "base_url",
107
+ "api_key_field": "api_key",
108
+ },
109
+ "Foundry.Chat": {
110
+ "package": "agent_framework.foundry",
111
+ "name": "FoundryChatClient",
112
+ "model_field": "model",
113
+ "endpoint_field": "project_endpoint",
114
+ "api_key_field": None,
115
+ },
116
+ "Anthropic.Chat": {
117
+ "package": "agent_framework.anthropic",
118
+ "name": "AnthropicChatClient",
119
+ "model_field": "model",
120
+ "endpoint_field": None,
121
+ "api_key_field": "api_key",
122
+ },
123
+ }
124
+
125
+
126
+ @experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS)
127
+ class DeclarativeLoaderError(AgentException):
128
+ """Exception raised for errors in the declarative loader."""
129
+
130
+ pass
131
+
132
+
133
+ @experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS)
134
+ class ProviderLookupError(DeclarativeLoaderError):
135
+ """Exception raised for errors in provider type lookup."""
136
+
137
+ pass
138
+
139
+
140
+ @experimental(feature_id=ExperimentalFeature.DECLARATIVE_AGENTS)
141
+ class AgentFactory:
142
+ """Factory for creating Agent instances from declarative YAML definitions.
143
+
144
+ AgentFactory parses YAML agent definitions (PromptAgent kind) and creates
145
+ configured Agent instances with the appropriate chat client, tools,
146
+ and response format.
147
+
148
+ Examples:
149
+ .. code-block:: python
150
+
151
+ from agent_framework_declarative import AgentFactory
152
+
153
+ # Create agent from YAML file
154
+ factory = AgentFactory()
155
+ agent = factory.create_agent_from_yaml_path("agent.yaml")
156
+
157
+ # Run the agent
158
+ async for event in agent.run("Hello!", stream=True):
159
+ print(event)
160
+
161
+ .. code-block:: python
162
+
163
+ from agent_framework.openai import OpenAIChatClient
164
+ from agent_framework_declarative import AgentFactory
165
+
166
+ # With pre-configured chat client
167
+ client = OpenAIChatClient()
168
+ factory = AgentFactory(client=client)
169
+ agent = factory.create_agent_from_yaml_path("agent.yaml")
170
+
171
+ .. code-block:: python
172
+
173
+ from agent_framework_declarative import AgentFactory
174
+
175
+ # From inline YAML string
176
+ yaml_content = '''
177
+ kind: Prompt
178
+ name: GreetingAgent
179
+ instructions: You are a friendly assistant.
180
+ model:
181
+ id: gpt-4o
182
+ provider: AzureOpenAI
183
+ '''
184
+
185
+ factory = AgentFactory()
186
+ agent = factory.create_agent_from_yaml(yaml_content)
187
+ """
188
+
189
+ def __init__(
190
+ self,
191
+ *,
192
+ client: SupportsChatGetResponse | None = None,
193
+ bindings: Mapping[str, Any] | None = None,
194
+ connections: Mapping[str, Any] | None = None,
195
+ client_kwargs: Mapping[str, Any] | None = None,
196
+ additional_mappings: Mapping[str, ProviderTypeMapping] | None = None,
197
+ default_provider: str = "Foundry",
198
+ safe_mode: bool = True,
199
+ env_file_path: str | None = None,
200
+ env_file_encoding: str | None = None,
201
+ ) -> None:
202
+ """Create the agent factory.
203
+
204
+ Args:
205
+ client: An optional SupportsChatGetResponse instance to use as a dependency.
206
+ This will be passed to the Agent that gets created.
207
+ If you need to create multiple agents with different chat clients,
208
+ do not pass this and instead provide the chat client in the YAML definition.
209
+ bindings: An optional dictionary of bindings to use when creating agents.
210
+ connections: An optional dictionary of connections to resolve ReferenceConnections.
211
+ client_kwargs: An optional dictionary of keyword arguments to pass to chat client constructor.
212
+ additional_mappings: An optional dictionary to extend the provider type to object mapping.
213
+ Should have the structure:
214
+
215
+ ..code-block:: python
216
+
217
+ additional_mappings = {
218
+ "Provider.ApiType": {
219
+ "package": "package.name",
220
+ "name": "ClassName",
221
+ "model_field": "field_name_in_constructor",
222
+ "endpoint_field": "endpoint_kwarg_name_or_null",
223
+ "api_key_field": "api_key_kwarg_name_or_null",
224
+ },
225
+ ...
226
+ }
227
+
228
+ Here, "Provider.ApiType" is the lookup key used when both provider and apiType are specified in the
229
+ model, "Provider" is also allowed.
230
+ Package refers to which model needs to be imported, Name is the class name of the
231
+ SupportsChatGetResponse implementation, and model_field is the name of the field in the
232
+ constructor that accepts the model.id value.
233
+ default_provider: The default provider used when model.provider is not specified,
234
+ default is "Foundry", which uses the FoundryChatClient.
235
+ safe_mode: Whether to run in safe mode, default is True.
236
+ When safe_mode is True, environment variables are not accessible in the powerfx expressions.
237
+ You can still use environment variables, but through the constructors of the classes.
238
+ Which means you must make sure you are using the standard env variable names of the classes
239
+ you are using and not custom ones and remove the powerfx statements that start with `=Env.`.
240
+ Only when you trust the source of your yaml files, you can set safe_mode to False
241
+ via the AgentFactory constructor.
242
+ env_file_path: The path to the .env file to load environment variables from.
243
+ env_file_encoding: The encoding of the .env file, defaults to 'utf-8'.
244
+
245
+ Examples:
246
+ .. code-block:: python
247
+
248
+ from agent_framework_declarative import AgentFactory
249
+
250
+ # Minimal initialization
251
+ factory = AgentFactory()
252
+
253
+ .. code-block:: python
254
+
255
+ from agent_framework.openai import OpenAIChatClient
256
+ from agent_framework_declarative import AgentFactory
257
+
258
+ # With shared chat client
259
+ client = OpenAIChatClient()
260
+ factory = AgentFactory(
261
+ client=client,
262
+ env_file_path=".env",
263
+ )
264
+
265
+ .. code-block:: python
266
+
267
+ from agent_framework_declarative import AgentFactory
268
+
269
+ # With custom provider mappings
270
+ factory = AgentFactory(
271
+ additional_mappings={
272
+ "CustomProvider.Chat": {
273
+ "package": "my_package.clients",
274
+ "name": "CustomChatClient",
275
+ "model_field": "model",
276
+ },
277
+ },
278
+ )
279
+ """
280
+ self.client = client
281
+ self.bindings = bindings
282
+ self.connections = connections
283
+ self.client_kwargs = client_kwargs or {}
284
+ self.additional_mappings = additional_mappings or {}
285
+ self.default_provider: str = default_provider
286
+ self.safe_mode = safe_mode
287
+ load_dotenv(dotenv_path=env_file_path, encoding=env_file_encoding)
288
+
289
+ def create_agent_from_yaml_path(self, yaml_path: str | Path) -> Agent:
290
+ """Create a Agent from a YAML file path.
291
+
292
+ This method does the following things:
293
+
294
+ 1. Loads the YAML file into an AgentSchema object.
295
+ 2. Validates that the loaded object is a PromptAgent.
296
+ 3. Creates the appropriate ChatClient based on the model provider and apiType.
297
+ 4. Parses the tools, options, and response format from the PromptAgent.
298
+ 5. Creates and returns a Agent instance with the configured properties.
299
+
300
+ Args:
301
+ yaml_path: Path to the YAML file representation of a PromptAgent.
302
+
303
+ Returns:
304
+ The ``Agent`` instance created from the YAML file.
305
+
306
+ Raises:
307
+ DeclarativeLoaderError: If the YAML does not represent a PromptAgent.
308
+ ProviderLookupError: If the provider type is unknown or unsupported.
309
+ ValueError: If a ReferenceConnection cannot be resolved.
310
+ ModuleNotFoundError: If the required module for the provider type cannot be imported.
311
+ AttributeError: If the required class for the provider type cannot be found in the module.
312
+
313
+ Examples:
314
+ .. code-block:: python
315
+
316
+ from agent_framework_declarative import AgentFactory
317
+
318
+ factory = AgentFactory()
319
+ agent = factory.create_agent_from_yaml_path("agents/support_agent.yaml")
320
+
321
+ # Execute the agent
322
+ async for event in agent.run("Help me with my order", stream=True):
323
+ print(event)
324
+
325
+ .. code-block:: python
326
+
327
+ from pathlib import Path
328
+ from agent_framework_declarative import AgentFactory
329
+
330
+ # Using Path object for cross-platform compatibility
331
+ agent_path = Path(__file__).parent / "agents" / "writer.yaml"
332
+ factory = AgentFactory()
333
+ agent = factory.create_agent_from_yaml_path(agent_path)
334
+ """
335
+ if not isinstance(yaml_path, Path):
336
+ yaml_path = Path(yaml_path)
337
+ if not yaml_path.exists():
338
+ raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}")
339
+ with open(yaml_path) as f:
340
+ yaml_str = f.read()
341
+ return self.create_agent_from_yaml(yaml_str)
342
+
343
+ def create_agent_from_yaml(self, yaml_str: str) -> Agent:
344
+ """Create a Agent from a YAML string.
345
+
346
+ This method does the following things:
347
+
348
+ 1. Loads the YAML string into an AgentSchema object.
349
+ 2. Validates that the loaded object is a PromptAgent.
350
+ 3. Creates the appropriate ChatClient based on the model provider and apiType.
351
+ 4. Parses the tools, options, and response format from the PromptAgent.
352
+ 5. Creates and returns a Agent instance with the configured properties.
353
+
354
+ Args:
355
+ yaml_str: YAML string representation of a PromptAgent.
356
+
357
+ Returns:
358
+ The ``Agent`` instance created from the YAML string.
359
+
360
+ Raises:
361
+ DeclarativeLoaderError: If the YAML does not represent a PromptAgent.
362
+ ProviderLookupError: If the provider type is unknown or unsupported.
363
+ ValueError: If a ReferenceConnection cannot be resolved.
364
+ ModuleNotFoundError: If the required module for the provider type cannot be imported.
365
+ AttributeError: If the required class for the provider type cannot be found in the module.
366
+
367
+ Examples:
368
+ .. code-block:: python
369
+
370
+ from agent_framework_declarative import AgentFactory
371
+
372
+ yaml_content = '''
373
+ kind: Prompt
374
+ name: TranslationAgent
375
+ description: Translates text between languages
376
+ instructions: |
377
+ You are a translation assistant.
378
+ Translate user input to the requested language.
379
+ model:
380
+ id: gpt-4o
381
+ provider: AzureOpenAI
382
+ options:
383
+ temperature: 0.3
384
+ '''
385
+
386
+ factory = AgentFactory()
387
+ agent = factory.create_agent_from_yaml(yaml_content)
388
+
389
+ .. code-block:: python
390
+
391
+ from agent_framework_declarative import AgentFactory
392
+ from pydantic import BaseModel
393
+
394
+ # Agent with structured output
395
+ yaml_content = '''
396
+ kind: Prompt
397
+ name: SentimentAnalyzer
398
+ instructions: Analyze the sentiment of the input text.
399
+ model:
400
+ id: gpt-4o
401
+ outputSchema:
402
+ type: object
403
+ properties:
404
+ sentiment:
405
+ type: string
406
+ enum: [positive, negative, neutral]
407
+ confidence:
408
+ type: number
409
+ '''
410
+
411
+ factory = AgentFactory()
412
+ agent = factory.create_agent_from_yaml(yaml_content)
413
+ """
414
+ return self.create_agent_from_dict(yaml.safe_load(yaml_str))
415
+
416
+ def create_agent_from_dict(self, agent_def: dict[str, Any]) -> Agent:
417
+ """Create a Agent from a dictionary definition.
418
+
419
+ This method does the following things:
420
+
421
+ 1. Converts the dictionary into an AgentSchema object.
422
+ 2. Validates that the loaded object is a PromptAgent.
423
+ 3. Creates the appropriate ChatClient based on the model provider and apiType.
424
+ 4. Parses the tools, options, and response format from the PromptAgent.
425
+ 5. Creates and returns a Agent instance with the configured properties.
426
+
427
+ Args:
428
+ agent_def: Dictionary representation of a PromptAgent.
429
+
430
+ Returns:
431
+ The `Agent` instance created from the dictionary.
432
+
433
+ Raises:
434
+ DeclarativeLoaderError: If the dictionary does not represent a PromptAgent.
435
+ ProviderLookupError: If the provider type is unknown or unsupported.
436
+ ValueError: If a ReferenceConnection cannot be resolved.
437
+ ModuleNotFoundError: If the required module for the provider type cannot be imported.
438
+ AttributeError: If the required class for the provider type cannot be found in the module.
439
+
440
+ Examples:
441
+ .. code-block:: python
442
+
443
+ from agent_framework_declarative import AgentFactory
444
+
445
+ agent_def = {
446
+ "kind": "Prompt",
447
+ "name": "TranslationAgent",
448
+ "description": "Translates text between languages",
449
+ "instructions": "You are a translation assistant.",
450
+ "model": {
451
+ "id": "gpt-4o",
452
+ "provider": "AzureOpenAI",
453
+ },
454
+ }
455
+
456
+ factory = AgentFactory()
457
+ agent = factory.create_agent_from_dict(agent_def)
458
+ """
459
+ # Set safe_mode context before parsing YAML to control PowerFx environment variable access
460
+ _safe_mode_context.set(self.safe_mode)
461
+ prompt_agent = agent_schema_dispatch(agent_def)
462
+ if not isinstance(prompt_agent, PromptAgent):
463
+ raise DeclarativeLoaderError("Only definitions for a PromptAgent are supported for agent creation.")
464
+
465
+ # Step 1: Create the ChatClient
466
+ client = self._get_client(prompt_agent)
467
+ # Step 2: Get the chat options
468
+ chat_options = self._parse_chat_options(prompt_agent.model)
469
+ if tools := self._parse_tools(prompt_agent.tools):
470
+ chat_options["tools"] = tools
471
+ if output_schema := prompt_agent.outputSchema:
472
+ chat_options["response_format"] = output_schema.to_json_schema()
473
+ # Step 3: Create the agent instance
474
+ return Agent(
475
+ client=client,
476
+ name=prompt_agent.name,
477
+ description=prompt_agent.description,
478
+ instructions=prompt_agent.instructions,
479
+ default_options=chat_options, # type: ignore[arg-type]
480
+ )
481
+
482
+ async def create_agent_from_yaml_path_async(self, yaml_path: str | Path) -> Agent:
483
+ """Async version: Create a Agent from a YAML file path.
484
+
485
+ This is the async counterpart to ``create_agent_from_dict`` and is useful when
486
+ the rest of your setup is already async.
487
+
488
+ Args:
489
+ yaml_path: Path to the YAML file representation of a PromptAgent.
490
+
491
+ Returns:
492
+ The ``Agent`` instance created from the YAML file.
493
+
494
+ Examples:
495
+ .. code-block:: python
496
+
497
+ from agent_framework_declarative import AgentFactory
498
+
499
+ factory = AgentFactory(
500
+ client_kwargs={"credential": credential},
501
+ default_provider="Foundry",
502
+ )
503
+ agent = await factory.create_agent_from_yaml_path_async("agent.yaml")
504
+ """
505
+ if not isinstance(yaml_path, Path):
506
+ yaml_path = Path(yaml_path)
507
+ if not yaml_path.exists():
508
+ raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}")
509
+ yaml_str = yaml_path.read_text()
510
+ return await self.create_agent_from_yaml_async(yaml_str)
511
+
512
+ async def create_agent_from_yaml_async(self, yaml_str: str) -> Agent:
513
+ """Async version: Create a Agent from a YAML string.
514
+
515
+ Use this method when the surrounding call site is already async and you
516
+ want to build an agent directly from YAML text.
517
+
518
+ Args:
519
+ yaml_str: YAML string representation of a PromptAgent.
520
+
521
+ Returns:
522
+ The ``Agent`` instance created from the YAML string.
523
+
524
+ Examples:
525
+ .. code-block:: python
526
+
527
+ from agent_framework_declarative import AgentFactory
528
+
529
+ yaml_content = '''
530
+ kind: Prompt
531
+ name: MyAgent
532
+ instructions: You are a helpful assistant.
533
+ model:
534
+ id: gpt-4o
535
+ provider: Foundry
536
+ '''
537
+
538
+ factory = AgentFactory(client_kwargs={"credential": credential})
539
+ agent = await factory.create_agent_from_yaml_async(yaml_content)
540
+ """
541
+ return await self.create_agent_from_dict_async(yaml.safe_load(yaml_str))
542
+
543
+ async def create_agent_from_dict_async(self, agent_def: dict[str, Any]) -> Agent:
544
+ """Async version: Create a Agent from a dictionary definition.
545
+
546
+ This is the async counterpart to ``create_agent_from_dict`` and is useful when
547
+ the rest of your setup is already async.
548
+
549
+ Args:
550
+ agent_def: Dictionary representation of a PromptAgent.
551
+
552
+ Returns:
553
+ The ``Agent`` instance created from the dictionary.
554
+
555
+ Examples:
556
+ .. code-block:: python
557
+
558
+ from agent_framework_declarative import AgentFactory
559
+
560
+ agent_def = {
561
+ "kind": "Prompt",
562
+ "name": "MyAgent",
563
+ "instructions": "You are a helpful assistant.",
564
+ "model": {
565
+ "id": "gpt-4o",
566
+ "provider": "Foundry",
567
+ },
568
+ }
569
+
570
+ factory = AgentFactory(client_kwargs={"credential": credential})
571
+ agent = await factory.create_agent_from_dict_async(agent_def)
572
+ """
573
+ # Set safe_mode context before parsing YAML to control PowerFx environment variable access
574
+ _safe_mode_context.set(self.safe_mode)
575
+ prompt_agent = agent_schema_dispatch(agent_def)
576
+ if not isinstance(prompt_agent, PromptAgent):
577
+ raise DeclarativeLoaderError("Only definitions for a PromptAgent are supported for agent creation.")
578
+
579
+ client = self._get_client(prompt_agent)
580
+ chat_options = self._parse_chat_options(prompt_agent.model)
581
+ if tools := self._parse_tools(prompt_agent.tools):
582
+ chat_options["tools"] = tools
583
+ if output_schema := prompt_agent.outputSchema:
584
+ chat_options["response_format"] = output_schema.to_json_schema()
585
+ return Agent(
586
+ client=client,
587
+ name=prompt_agent.name,
588
+ description=prompt_agent.description,
589
+ instructions=prompt_agent.instructions,
590
+ default_options=chat_options, # type: ignore[arg-type]
591
+ )
592
+
593
+ async def _create_agent_with_provider(self, prompt_agent: PromptAgent, mapping: ProviderTypeMapping) -> Agent:
594
+ """Create an Agent through a provider object that exposes ``create_agent``.
595
+
596
+ This remains available as an internal escape hatch for provider-style custom mappings
597
+ that return a fully constructed ``Agent`` rather than a chat client.
598
+ """
599
+ module_name = mapping["package"]
600
+ class_name = mapping["name"]
601
+ module = __import__(module_name, fromlist=[class_name])
602
+ provider_class = getattr(module, class_name)
603
+
604
+ provider_kwargs: dict[str, Any] = {}
605
+ provider_kwargs.update(self.client_kwargs)
606
+
607
+ endpoint_field = mapping.get("endpoint_field")
608
+ api_key_field = mapping.get("api_key_field", "api_key")
609
+
610
+ if prompt_agent.model and prompt_agent.model.connection:
611
+ match prompt_agent.model.connection:
612
+ case ApiKeyConnection():
613
+ if api_key_field:
614
+ provider_kwargs[api_key_field] = prompt_agent.model.connection.apiKey
615
+ if prompt_agent.model.connection.endpoint and endpoint_field:
616
+ provider_kwargs[endpoint_field] = prompt_agent.model.connection.endpoint
617
+ case RemoteConnection() | AnonymousConnection():
618
+ if prompt_agent.model.connection.endpoint and endpoint_field:
619
+ provider_kwargs[endpoint_field] = prompt_agent.model.connection.endpoint
620
+ case ReferenceConnection():
621
+ pass
622
+
623
+ provider = provider_class(**provider_kwargs)
624
+ tools = self._parse_tools(prompt_agent.tools) if prompt_agent.tools else None
625
+
626
+ default_options: dict[str, Any] | None = None
627
+ if prompt_agent.outputSchema:
628
+ default_options = {"response_format": prompt_agent.outputSchema.to_json_schema()}
629
+
630
+ return cast(
631
+ Agent,
632
+ await provider.create_agent(
633
+ name=prompt_agent.name,
634
+ model=prompt_agent.model.id if prompt_agent.model else None,
635
+ instructions=prompt_agent.instructions,
636
+ description=prompt_agent.description,
637
+ tools=tools,
638
+ default_options=default_options,
639
+ ),
640
+ )
641
+
642
+ def _get_client(self, prompt_agent: PromptAgent) -> SupportsChatGetResponse:
643
+ """Create the SupportsChatGetResponse instance based on the PromptAgent model."""
644
+ if not prompt_agent.model:
645
+ # if no model is defined, use the supplied client
646
+ if self.client:
647
+ return self.client
648
+ raise DeclarativeLoaderError(
649
+ "ChatClient must be provided to create agent from PromptAgent, "
650
+ "alternatively define a model in the PromptAgent."
651
+ )
652
+
653
+ mapping = self._retrieve_provider_configuration(prompt_agent.model)
654
+ setup_dict: dict[str, Any] = {}
655
+ setup_dict.update(self.client_kwargs)
656
+ endpoint_field = mapping.get("endpoint_field")
657
+ api_key_field = mapping.get("api_key_field", "api_key")
658
+
659
+ # parse connections
660
+ if prompt_agent.model.connection:
661
+ match prompt_agent.model.connection:
662
+ case ApiKeyConnection():
663
+ if api_key_field:
664
+ setup_dict[api_key_field] = prompt_agent.model.connection.apiKey
665
+ elif prompt_agent.model.connection.apiKey:
666
+ raise DeclarativeLoaderError(
667
+ f"{mapping['name']} does not support API key-based model connections."
668
+ )
669
+ if prompt_agent.model.connection.endpoint:
670
+ if not endpoint_field:
671
+ raise DeclarativeLoaderError(
672
+ f"{mapping['name']} does not support endpoint-based model connections."
673
+ )
674
+ setup_dict[endpoint_field] = prompt_agent.model.connection.endpoint
675
+ case RemoteConnection() | AnonymousConnection():
676
+ if prompt_agent.model.connection.endpoint:
677
+ if not endpoint_field:
678
+ raise DeclarativeLoaderError(
679
+ f"{mapping['name']} does not support endpoint-based model connections."
680
+ )
681
+ setup_dict[endpoint_field] = prompt_agent.model.connection.endpoint
682
+ case ReferenceConnection():
683
+ if not self.connections:
684
+ raise ValueError("Connections must be provided to resolve ReferenceConnection")
685
+ # find the referenced connection
686
+ if prompt_agent.model.connection.name and (
687
+ value := self.connections.get(prompt_agent.model.connection.name)
688
+ ):
689
+ setup_dict[prompt_agent.model.connection.name] = value
690
+ else:
691
+ raise ValueError(
692
+ f"ReferenceConnection with name {prompt_agent.model.connection.name} not found in provided "
693
+ "connections."
694
+ )
695
+
696
+ # Any client we create, needs a model.id
697
+ if not prompt_agent.model.id:
698
+ # if prompt_agent.model is defined, but no id, use the supplied client
699
+ if self.client:
700
+ return self.client
701
+ # or raise, since we cannot create a client without a model
702
+ raise DeclarativeLoaderError(
703
+ "ChatClient must be provided to create agent from PromptAgent, or define model.id in the PromptAgent."
704
+ )
705
+ # if provider is defined, use that, if possible with apiType, fallback to default_provider
706
+ module_name = mapping["package"]
707
+ class_name = mapping["name"]
708
+ module = __import__(module_name, fromlist=[class_name])
709
+ agent_class = getattr(module, class_name)
710
+ setup_dict[mapping["model_field"]] = prompt_agent.model.id
711
+ return agent_class(**setup_dict)
712
+
713
+ def _parse_chat_options(self, model: Model | None) -> dict[str, Any]:
714
+ """Parse ModelOptions into chat options dictionary."""
715
+ chat_options: dict[str, Any] = {}
716
+ if not model or not model.options or not isinstance(model.options, ModelOptions):
717
+ return chat_options
718
+ options = model.options
719
+ if options.frequencyPenalty is not None:
720
+ chat_options["frequency_penalty"] = options.frequencyPenalty
721
+ if options.presencePenalty is not None:
722
+ chat_options["presence_penalty"] = options.presencePenalty
723
+ if options.maxOutputTokens is not None:
724
+ chat_options["max_tokens"] = options.maxOutputTokens
725
+ if options.temperature is not None:
726
+ chat_options["temperature"] = options.temperature
727
+ if options.topP is not None:
728
+ chat_options["top_p"] = options.topP
729
+ if options.seed is not None:
730
+ chat_options["seed"] = options.seed
731
+ if options.stopSequences:
732
+ chat_options["stop"] = options.stopSequences
733
+ if options.allowMultipleToolCalls is not None:
734
+ chat_options["allow_multiple_tool_calls"] = options.allowMultipleToolCalls
735
+ if (chat_tool_mode := options.additionalProperties.pop("chatToolMode", None)) is not None:
736
+ chat_options["tool_choice"] = chat_tool_mode
737
+ if options.additionalProperties:
738
+ chat_options["additional_chat_options"] = options.additionalProperties
739
+ return chat_options
740
+
741
+ def _parse_tools(self, tools: list[Tool] | None) -> list[AFFunctionTool | dict[str, Any]] | None:
742
+ """Parse tool resources into AFFunctionTool instances or dict-based tools."""
743
+ if not tools:
744
+ return None
745
+ return [self._parse_tool(tool_resource) for tool_resource in tools]
746
+
747
+ def _parse_tool(self, tool_resource: Tool) -> AFFunctionTool | dict[str, Any]:
748
+ """Parse a single tool resource into an AFFunctionTool instance."""
749
+ match tool_resource:
750
+ case FunctionTool():
751
+ func: Callable[..., Any] | None = None
752
+ if self.bindings and tool_resource.bindings:
753
+ for binding in tool_resource.bindings:
754
+ if binding.name and (func := self.bindings.get(binding.name)):
755
+ break
756
+ return AFFunctionTool(
757
+ name=tool_resource.name, # type: ignore
758
+ description=tool_resource.description, # type: ignore
759
+ input_model=tool_resource.parameters.to_json_schema() if tool_resource.parameters else None,
760
+ func=func,
761
+ )
762
+ case WebSearchTool():
763
+ result: dict[str, Any] = {"type": "web_search_preview"}
764
+ if tool_resource.description:
765
+ result["description"] = tool_resource.description
766
+ if tool_resource.options:
767
+ result.update(tool_resource.options)
768
+ return result
769
+ case FileSearchTool():
770
+ result = {
771
+ "type": "file_search",
772
+ "vector_store_ids": tool_resource.vectorStoreIds or [],
773
+ }
774
+ if tool_resource.maximumResultCount is not None:
775
+ result["max_num_results"] = tool_resource.maximumResultCount
776
+ if tool_resource.description:
777
+ result["description"] = tool_resource.description
778
+ if tool_resource.ranker is not None:
779
+ result["ranker"] = tool_resource.ranker
780
+ if tool_resource.scoreThreshold is not None:
781
+ result["score_threshold"] = tool_resource.scoreThreshold
782
+ if tool_resource.filters:
783
+ result["filters"] = tool_resource.filters
784
+ return result
785
+ case CodeInterpreterTool():
786
+ result = {"type": "code_interpreter"}
787
+ if tool_resource.fileIds:
788
+ result["file_ids"] = tool_resource.fileIds
789
+ if tool_resource.description:
790
+ result["description"] = tool_resource.description
791
+ return result
792
+ case McpTool():
793
+ result = {
794
+ "type": "mcp",
795
+ "server_label": tool_resource.name.replace(" ", "_") if tool_resource.name else "",
796
+ "server_url": str(tool_resource.url) if tool_resource.url else "",
797
+ }
798
+ if tool_resource.description:
799
+ result["server_description"] = tool_resource.description
800
+ if tool_resource.allowedTools:
801
+ result["allowed_tools"] = list(tool_resource.allowedTools)
802
+
803
+ # Handle approval mode
804
+ if tool_resource.approvalMode is not None:
805
+ if tool_resource.approvalMode.kind == "always":
806
+ result["require_approval"] = "always"
807
+ elif tool_resource.approvalMode.kind == "never":
808
+ result["require_approval"] = "never"
809
+ elif isinstance(tool_resource.approvalMode, McpServerToolSpecifyApprovalMode):
810
+ approval_config: dict[str, Any] = {}
811
+ if tool_resource.approvalMode.alwaysRequireApprovalTools:
812
+ approval_config["always"] = {
813
+ "tool_names": list(tool_resource.approvalMode.alwaysRequireApprovalTools)
814
+ }
815
+ if tool_resource.approvalMode.neverRequireApprovalTools:
816
+ approval_config["never"] = {
817
+ "tool_names": list(tool_resource.approvalMode.neverRequireApprovalTools)
818
+ }
819
+ if approval_config:
820
+ result["require_approval"] = approval_config
821
+
822
+ # Handle connection settings
823
+ if tool_resource.connection is not None:
824
+ match tool_resource.connection:
825
+ case ApiKeyConnection():
826
+ if tool_resource.connection.apiKey:
827
+ result["headers"] = {"Authorization": f"Bearer {tool_resource.connection.apiKey}"}
828
+ case RemoteConnection():
829
+ result["project_connection_id"] = tool_resource.connection.name
830
+ case ReferenceConnection():
831
+ result["project_connection_id"] = tool_resource.connection.name
832
+ case AnonymousConnection():
833
+ pass
834
+ case _:
835
+ raise ValueError(f"Unsupported connection kind: {tool_resource.connection.kind}")
836
+
837
+ return result
838
+ case _:
839
+ raise ValueError(f"Unsupported tool kind: {tool_resource.kind}")
840
+
841
+ def _retrieve_provider_configuration(self, model: Model) -> ProviderTypeMapping:
842
+ """Retrieve the provider configuration based on the model's provider and apiType.
843
+
844
+ If only provider is specified, it will be used.
845
+ If both provider and apiType are specified, both will be used.
846
+ If neither is specified, the default_provider will be used.
847
+
848
+ Args:
849
+ model: The Model instance containing provider and apiType information.
850
+
851
+ Returns:
852
+ A dictionary containing the package, name, and model_field for the provider.
853
+
854
+ Raises:
855
+ ProviderLookupError: If the provider type is not supported or can't be found.
856
+ """
857
+ class_lookup = (
858
+ f"{model.provider}.{model.apiType}"
859
+ if model.apiType
860
+ else f"{model.provider}"
861
+ if model.provider
862
+ else self.default_provider
863
+ )
864
+ if class_lookup in self.additional_mappings:
865
+ return self.additional_mappings[class_lookup]
866
+ if class_lookup not in PROVIDER_TYPE_OBJECT_MAPPING:
867
+ raise ProviderLookupError(f"Unsupported provider type: {class_lookup}")
868
+ return PROVIDER_TYPE_OBJECT_MAPPING[class_lookup]