agent-framework-declarative 1.0.0b251120__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Microsoft Corporation.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-framework-declarative
3
+ Version: 1.0.0b251120
4
+ Summary: Declarative specification support for Microsoft Agent Framework.
5
+ Author-email: Microsoft <af-support@microsoft.com>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Typing :: Typed
17
+ License-File: LICENSE
18
+ Requires-Dist: agent-framework-core
19
+ Requires-Dist: powerfx>=0.0.31; python_version < '3.14'
20
+ Requires-Dist: pyyaml>=6.0,<7.0
21
+ Project-URL: homepage, https://aka.ms/agent-framework
22
+ Project-URL: issues, https://github.com/microsoft/agent-framework/issues
23
+ Project-URL: release_notes, https://github.com/microsoft/agent-framework/releases?q=tag%3Apython-1&expanded=true
24
+ Project-URL: source, https://github.com/microsoft/agent-framework/tree/main/python
25
+
26
+ # Get Started with Microsoft Agent Framework Declarative
27
+
28
+ Please install this package via pip:
29
+
30
+ ```bash
31
+ pip install agent-framework-declarative --pre
32
+ ```
33
+
34
+ ## Declarative features
35
+
36
+ The declarative packages provides support for building agents based on a declarative yaml specification.
37
+
@@ -0,0 +1,11 @@
1
+ # Get Started with Microsoft Agent Framework Declarative
2
+
3
+ Please install this package via pip:
4
+
5
+ ```bash
6
+ pip install agent-framework-declarative --pre
7
+ ```
8
+
9
+ ## Declarative features
10
+
11
+ The declarative packages provides support for building agents based on a declarative yaml specification.
@@ -0,0 +1,12 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ from importlib import metadata
4
+
5
+ from ._loader import AgentFactory, DeclarativeLoaderError, ProviderLookupError, ProviderTypeMapping
6
+
7
+ try:
8
+ __version__ = metadata.version(__name__)
9
+ except metadata.PackageNotFoundError:
10
+ __version__ = "0.0.0" # Fallback for development mode
11
+
12
+ __all__ = ["AgentFactory", "DeclarativeLoaderError", "ProviderLookupError", "ProviderTypeMapping", "__version__"]
@@ -0,0 +1,422 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from pathlib import Path
5
+ from typing import Any, Literal, TypedDict
6
+
7
+ import yaml
8
+ from agent_framework import (
9
+ AIFunction,
10
+ ChatAgent,
11
+ ChatClientProtocol,
12
+ HostedCodeInterpreterTool,
13
+ HostedFileContent,
14
+ HostedFileSearchTool,
15
+ HostedMCPSpecificApproval,
16
+ HostedMCPTool,
17
+ HostedVectorStoreContent,
18
+ HostedWebSearchTool,
19
+ ToolProtocol,
20
+ )
21
+ from agent_framework._tools import _create_model_from_json_schema # type: ignore
22
+ from agent_framework.exceptions import AgentFrameworkException
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
+ agent_schema_dispatch,
41
+ )
42
+
43
+
44
+ class ProviderTypeMapping(TypedDict, total=True):
45
+ package: str
46
+ name: str
47
+ model_id_field: str
48
+
49
+
50
+ PROVIDER_TYPE_OBJECT_MAPPING: dict[str, ProviderTypeMapping] = {
51
+ "AzureOpenAI.Chat": {
52
+ "package": "agent_framework.azure",
53
+ "name": "AzureOpenAIChatClient",
54
+ "model_id_field": "deployment_name",
55
+ },
56
+ "AzureOpenAI.Assistants": {
57
+ "package": "agent_framework.azure",
58
+ "name": "AzureOpenAIAssistantsClient",
59
+ "model_id_field": "deployment_name",
60
+ },
61
+ "AzureOpenAI.Responses": {
62
+ "package": "agent_framework.azure",
63
+ "name": "AzureOpenAIResponsesClient",
64
+ "model_id_field": "deployment_name",
65
+ },
66
+ "OpenAI.Chat": {
67
+ "package": "agent_framework.openai",
68
+ "name": "OpenAIChatClient",
69
+ "model_id_field": "model_id",
70
+ },
71
+ "OpenAI.Assistants": {
72
+ "package": "agent_framework.openai",
73
+ "name": "OpenAIAssistantsClient",
74
+ "model_id_field": "model_id",
75
+ },
76
+ "OpenAI.Responses": {
77
+ "package": "agent_framework.openai",
78
+ "name": "OpenAIResponsesClient",
79
+ "model_id_field": "model_id",
80
+ },
81
+ "AzureAIAgentClient": {
82
+ "package": "agent_framework.azure",
83
+ "name": "AzureAIAgentClient",
84
+ "model_id_field": "model_deployment_name",
85
+ },
86
+ "AzureAIClient": {
87
+ "package": "agent_framework.azure",
88
+ "name": "AzureAIClient",
89
+ "model_id_field": "model_deployment_name",
90
+ },
91
+ "Anthropic.Chat": {
92
+ "package": "agent_framework.anthropic",
93
+ "name": "AnthropicChatClient",
94
+ "model_id_field": "model_id",
95
+ },
96
+ }
97
+
98
+
99
+ class DeclarativeLoaderError(AgentFrameworkException):
100
+ """Exception raised for errors in the declarative loader."""
101
+
102
+ pass
103
+
104
+
105
+ class ProviderLookupError(DeclarativeLoaderError):
106
+ """Exception raised for errors in provider type lookup."""
107
+
108
+ pass
109
+
110
+
111
+ class AgentFactory:
112
+ def __init__(
113
+ self,
114
+ *,
115
+ chat_client: ChatClientProtocol | None = None,
116
+ bindings: Mapping[str, Any] | None = None,
117
+ connections: Mapping[str, Any] | None = None,
118
+ client_kwargs: Mapping[str, Any] | None = None,
119
+ additional_mappings: Mapping[str, ProviderTypeMapping] | None = None,
120
+ default_provider: str = "AzureAIClient",
121
+ env_file: str | None = None,
122
+ ) -> None:
123
+ """Create the agent factory, with bindings.
124
+
125
+ Args:
126
+ chat_client: An optional ChatClientProtocol instance to use as a dependency,
127
+ this will be passed to the ChatAgent that get's created.
128
+ If you need to create multiple agents with different chat clients,
129
+ do not pass this and instead provide the chat client in the YAML definition.
130
+ bindings: An optional dictionary of bindings to use when creating agents.
131
+ connections: An optional dictionary of connections to resolve ReferenceConnections.
132
+ client_kwargs: An optional dictionary of keyword arguments to pass to chat client constructor.
133
+ additional_mappings: An optional dictionary to extend the provider type to object mapping.
134
+ Should have the structure:
135
+
136
+ ..code-block:: python
137
+
138
+ additional_mappings = {
139
+ "Provider.ApiType": {
140
+ "package": "package.name",
141
+ "name": "ClassName",
142
+ "model_id_field": "field_name_in_constructor",
143
+ },
144
+ ...
145
+ }
146
+
147
+ Here, "Provider.ApiType" is the lookup key used when both provider and apiType are specified in the
148
+ model, "Provider" is also allowed.
149
+ Package refers to which model needs to be imported, Name is the class name of the ChatClientProtocol
150
+ implementation, and model_id_field is the name of the field in the constructor
151
+ that accepts the model.id value.
152
+ default_provider: The default provider used when model.provider is not specified,
153
+ default is "AzureAIClient".
154
+ env_file: An optional path to a .env file to load environment variables from.
155
+ """
156
+ self.chat_client = chat_client
157
+ self.bindings = bindings
158
+ self.connections = connections
159
+ self.client_kwargs = client_kwargs or {}
160
+ self.additional_mappings = additional_mappings or {}
161
+ self.default_provider: str = default_provider
162
+ load_dotenv(dotenv_path=env_file)
163
+
164
+ def create_agent_from_yaml_path(self, yaml_path: str | Path) -> ChatAgent:
165
+ """Create a ChatAgent from a YAML file path.
166
+
167
+ This method does the following things:
168
+ 1. Loads the YAML file into a AgentSchema object using open and agent_schema_dispatch.
169
+ 2. Validates that the loaded object is a PromptAgent.
170
+ 3. Creates the appropriate ChatClient based on the model provider and apiType.
171
+ 4. Parses the tools, options, and response format from the PromptAgent.
172
+ 5. Creates and returns a ChatAgent instance with the configured properties.
173
+
174
+ Args:
175
+ yaml_path: Path to the YAML file representation of a AgentSchema object
176
+
177
+ Returns:
178
+ The ``ChatAgent`` instance created from the YAML file.
179
+
180
+ Raises:
181
+ DeclarativeLoaderError: If the YAML does not represent a PromptAgent.
182
+ ProviderLookupError: If the provider type is unknown or unsupported.
183
+ ValueError: If a ReferenceConnection cannot be resolved.
184
+ ModuleNotFoundError: If the required module for the provider type cannot be imported.
185
+ AttributeError: If the required class for the provider type cannot be found in the module.
186
+ """
187
+ if not isinstance(yaml_path, Path):
188
+ yaml_path = Path(yaml_path)
189
+ if not yaml_path.exists():
190
+ raise DeclarativeLoaderError(f"YAML file not found at path: {yaml_path}")
191
+ with open(yaml_path) as f:
192
+ yaml_str = f.read()
193
+ return self.create_agent_from_yaml(yaml_str)
194
+
195
+ def create_agent_from_yaml(self, yaml_str: str) -> ChatAgent:
196
+ """Create a ChatAgent from a YAML string.
197
+
198
+ This method does the following things:
199
+ 1. Loads the YAML string into a AgentSchema object using agent_schema_dispatch.
200
+ 2. Validates that the loaded object is a PromptAgent.
201
+ 3. Creates the appropriate ChatClient based on the model provider and apiType.
202
+ 4. Parses the tools, options, and response format from the PromptAgent.
203
+ 5. Creates and returns a ChatAgent instance with the configured properties.
204
+
205
+ Args:
206
+ yaml_str: YAML string representation of a AgentSchema object
207
+
208
+ Returns:
209
+ The ``ChatAgent`` instance created from the YAML string.
210
+
211
+ Raises:
212
+ DeclarativeLoaderError: If the YAML does not represent a PromptAgent.
213
+ ProviderLookupError: If the provider type is unknown or unsupported.
214
+ ValueError: If a ReferenceConnection cannot be resolved.
215
+ ModuleNotFoundError: If the required module for the provider type cannot be imported.
216
+ AttributeError: If the required class for the provider type cannot be found in the module.
217
+ """
218
+ prompt_agent = agent_schema_dispatch(yaml.safe_load(yaml_str))
219
+ if not isinstance(prompt_agent, PromptAgent):
220
+ raise DeclarativeLoaderError("Only yaml definitions for a PromptAgent are supported for agent creation.")
221
+
222
+ # Step 1: Create the ChatClient
223
+ client = self._get_client(prompt_agent)
224
+ # Step 2: Get the chat options
225
+ chat_options = self._parse_chat_options(prompt_agent.model)
226
+ if tools := self._parse_tools(prompt_agent.tools):
227
+ chat_options["tools"] = tools
228
+ if output_schema := prompt_agent.outputSchema:
229
+ chat_options["response_format"] = _create_model_from_json_schema("agent", output_schema.to_json_schema())
230
+ # Step 3: Create the agent instance
231
+ return ChatAgent(
232
+ chat_client=client,
233
+ name=prompt_agent.name,
234
+ description=prompt_agent.description,
235
+ instructions=prompt_agent.instructions,
236
+ **chat_options,
237
+ )
238
+
239
+ def _get_client(self, prompt_agent: PromptAgent) -> ChatClientProtocol:
240
+ """Create the ChatClientProtocol instance based on the PromptAgent model."""
241
+ if not prompt_agent.model:
242
+ # if no model is defined, use the supplied chat_client
243
+ if self.chat_client:
244
+ return self.chat_client
245
+ raise DeclarativeLoaderError(
246
+ "ChatClient must be provided to create agent from PromptAgent, "
247
+ "alternatively define a model in the PromptAgent."
248
+ )
249
+
250
+ setup_dict: dict[str, Any] = {}
251
+ setup_dict.update(self.client_kwargs)
252
+
253
+ # parse connections
254
+ if prompt_agent.model.connection:
255
+ match prompt_agent.model.connection:
256
+ case ApiKeyConnection():
257
+ setup_dict["api_key"] = prompt_agent.model.connection.apiKey
258
+ if prompt_agent.model.connection.endpoint:
259
+ setup_dict["endpoint"] = prompt_agent.model.connection.endpoint
260
+ case RemoteConnection() | AnonymousConnection():
261
+ setup_dict["endpoint"] = prompt_agent.model.connection.endpoint
262
+ case ReferenceConnection():
263
+ if not self.connections:
264
+ raise ValueError("Connections must be provided to resolve ReferenceConnection")
265
+ # find the referenced connection
266
+ if prompt_agent.model.connection.name and (
267
+ value := self.connections.get(prompt_agent.model.connection.name)
268
+ ):
269
+ setup_dict[prompt_agent.model.connection.name] = value
270
+ else:
271
+ raise ValueError(
272
+ f"ReferenceConnection with name {prompt_agent.model.connection.name} not found in provided "
273
+ "connections."
274
+ )
275
+
276
+ # Any client we create, needs a model.id
277
+ if not prompt_agent.model.id:
278
+ # if prompt_agent.model is defined, but no id, use the supplied chat_client
279
+ if self.chat_client:
280
+ return self.chat_client
281
+ # or raise, since we cannot create a client without model id
282
+ raise DeclarativeLoaderError(
283
+ "ChatClient must be provided to create agent from PromptAgent, or define model.id in the PromptAgent."
284
+ )
285
+ # if provider is defined, use that, if possible with apiType, fallback to default_provider
286
+ mapping = self._retrieve_provider_configuration(prompt_agent.model)
287
+ module_name = mapping["package"]
288
+ class_name = mapping["name"]
289
+ module = __import__(module_name, fromlist=[class_name])
290
+ agent_class = getattr(module, class_name)
291
+ setup_dict[mapping["model_id_field"]] = prompt_agent.model.id
292
+ return agent_class(**setup_dict) # type: ignore[no-any-return]
293
+
294
+ def _parse_chat_options(self, model: Model | None) -> dict[str, Any]:
295
+ """Parse ModelOptions into chat options dictionary."""
296
+ chat_options: dict[str, Any] = {}
297
+ if not model or not model.options or not isinstance(model.options, ModelOptions):
298
+ return chat_options
299
+ options = model.options
300
+ if options.frequencyPenalty is not None:
301
+ chat_options["frequency_penalty"] = options.frequencyPenalty
302
+ if options.presencePenalty is not None:
303
+ chat_options["presence_penalty"] = options.presencePenalty
304
+ if options.maxOutputTokens is not None:
305
+ chat_options["max_tokens"] = options.maxOutputTokens
306
+ if options.temperature is not None:
307
+ chat_options["temperature"] = options.temperature
308
+ if options.topP is not None:
309
+ chat_options["top_p"] = options.topP
310
+ if options.seed is not None:
311
+ chat_options["seed"] = options.seed
312
+ if options.stopSequences:
313
+ chat_options["stop"] = options.stopSequences
314
+ if options.allowMultipleToolCalls is not None:
315
+ chat_options["allow_multiple_tool_calls"] = options.allowMultipleToolCalls
316
+ if (chat_tool_mode := options.additionalProperties.pop("chatToolMode", None)) is not None:
317
+ chat_options["tool_choice"] = chat_tool_mode
318
+ if options.additionalProperties:
319
+ chat_options["additional_chat_options"] = options.additionalProperties
320
+ return chat_options
321
+
322
+ def _parse_tools(self, tools: list[Tool] | None) -> list[ToolProtocol] | None:
323
+ """Parse tool resources into ToolProtocol instances."""
324
+ if not tools:
325
+ return None
326
+ return [self._parse_tool(tool_resource) for tool_resource in tools]
327
+
328
+ def _parse_tool(self, tool_resource: Tool) -> ToolProtocol:
329
+ """Parse a single tool resource into a ToolProtocol instance."""
330
+ match tool_resource:
331
+ case FunctionTool():
332
+ func: Callable[..., Any] | None = None
333
+ if self.bindings and tool_resource.bindings:
334
+ for binding in tool_resource.bindings:
335
+ if binding.name and (func := self.bindings.get(binding.name)):
336
+ break
337
+ return AIFunction( # type: ignore
338
+ name=tool_resource.name, # type: ignore
339
+ description=tool_resource.description, # type: ignore
340
+ input_model=tool_resource.parameters.to_json_schema() if tool_resource.parameters else None,
341
+ func=func,
342
+ )
343
+ case WebSearchTool():
344
+ return HostedWebSearchTool(
345
+ description=tool_resource.description, additional_properties=tool_resource.options
346
+ )
347
+ case FileSearchTool():
348
+ add_props: dict[str, Any] = {}
349
+ if tool_resource.ranker is not None:
350
+ add_props["ranker"] = tool_resource.ranker
351
+ if tool_resource.scoreThreshold is not None:
352
+ add_props["score_threshold"] = tool_resource.scoreThreshold
353
+ if tool_resource.filters:
354
+ add_props["filters"] = tool_resource.filters
355
+ return HostedFileSearchTool(
356
+ inputs=[HostedVectorStoreContent(id) for id in tool_resource.vectorStoreIds or []],
357
+ description=tool_resource.description,
358
+ max_results=tool_resource.maximumResultCount,
359
+ additional_properties=add_props,
360
+ )
361
+ case CodeInterpreterTool():
362
+ return HostedCodeInterpreterTool(
363
+ inputs=[HostedFileContent(file_id=file) for file in tool_resource.fileIds or []],
364
+ description=tool_resource.description,
365
+ )
366
+ case McpTool():
367
+ approval_mode: HostedMCPSpecificApproval | Literal["always_require", "never_require"] | None = None
368
+ if tool_resource.approvalMode is not None:
369
+ if tool_resource.approvalMode.kind == "always":
370
+ approval_mode = "always_require"
371
+ elif tool_resource.approvalMode.kind == "never":
372
+ approval_mode = "never_require"
373
+ elif isinstance(tool_resource.approvalMode, McpServerToolSpecifyApprovalMode):
374
+ approval_mode = {}
375
+ if tool_resource.approvalMode.alwaysRequireApprovalTools:
376
+ approval_mode["always_require_approval"] = (
377
+ tool_resource.approvalMode.alwaysRequireApprovalTools
378
+ )
379
+ if tool_resource.approvalMode.neverRequireApprovalTools:
380
+ approval_mode["never_require_approval"] = (
381
+ tool_resource.approvalMode.neverRequireApprovalTools
382
+ )
383
+ if not approval_mode:
384
+ approval_mode = None
385
+ return HostedMCPTool(
386
+ name=tool_resource.name, # type: ignore
387
+ description=tool_resource.description,
388
+ url=tool_resource.url, # type: ignore
389
+ allowed_tools=tool_resource.allowedTools,
390
+ approval_mode=approval_mode,
391
+ )
392
+ case _:
393
+ raise ValueError(f"Unsupported tool kind: {tool_resource.kind}")
394
+
395
+ def _retrieve_provider_configuration(self, model: Model) -> ProviderTypeMapping:
396
+ """Retrieve the provider configuration based on the model's provider and apiType.
397
+
398
+ If only provider is specified, it will be used.
399
+ If both provider and apiType are specified, both will be used.
400
+ If neither is specified, the default_provider will be used.
401
+
402
+ Args:
403
+ model: The Model instance containing provider and apiType information.
404
+
405
+ Returns:
406
+ A dictionary containing the package, name, and model_id_field for the provider.
407
+
408
+ Raises:
409
+ ProviderLookupError: If the provider type is not supported or can't be found.
410
+ """
411
+ class_lookup = (
412
+ f"{model.provider}.{model.apiType}"
413
+ if model.apiType
414
+ else f"{model.provider}"
415
+ if model.provider
416
+ else self.default_provider
417
+ )
418
+ if class_lookup in self.additional_mappings:
419
+ return self.additional_mappings[class_lookup]
420
+ if class_lookup not in PROVIDER_TYPE_OBJECT_MAPPING:
421
+ raise ProviderLookupError(f"Unsupported provider type: {class_lookup}")
422
+ return PROVIDER_TYPE_OBJECT_MAPPING[class_lookup]