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,1154 @@
1
+ # Copyright (c) Microsoft. All rights reserved.
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+ import os
6
+ from collections.abc import MutableMapping
7
+ from contextvars import ContextVar
8
+ from typing import TYPE_CHECKING, Any, Literal, TypeVar, Union, cast, overload
9
+
10
+ from agent_framework._serialization import SerializationMixin
11
+
12
+ if TYPE_CHECKING:
13
+ from powerfx import Engine
14
+
15
+ _engine_initialized = False
16
+ _engine: Engine | None = None
17
+
18
+
19
+ def _get_engine() -> Engine | None:
20
+ """Lazily initialize the PowerFx engine on first use."""
21
+ global _engine_initialized, _engine
22
+ if not _engine_initialized:
23
+ _engine_initialized = True
24
+ try:
25
+ from powerfx import Engine
26
+
27
+ _engine = Engine()
28
+ except (ImportError, RuntimeError):
29
+ # ImportError: powerfx package not installed
30
+ # RuntimeError: .NET runtime not available or misconfigured
31
+ pass
32
+ return _engine
33
+
34
+
35
+ logger = logging.getLogger("agent_framework.declarative")
36
+
37
+ # Context variable for safe_mode setting.
38
+ # When True (default), environment variables are NOT accessible in PowerFx expressions.
39
+ # When False, environment variables CAN be accessed via Env symbol in PowerFx.
40
+ _safe_mode_context: ContextVar[bool] = ContextVar("safe_mode", default=True)
41
+
42
+
43
+ @overload
44
+ def _try_powerfx_eval(value: None, log_value: bool = True) -> None: ...
45
+
46
+
47
+ @overload
48
+ def _try_powerfx_eval(value: str, log_value: bool = True) -> str: ...
49
+
50
+
51
+ def _try_powerfx_eval(value: str | None, log_value: bool = True) -> str | None:
52
+ """Check if a value refers to a environment variable and parse it if so.
53
+
54
+ Args:
55
+ value: The value to check.
56
+ log_value: Whether to log additional context on error.
57
+ """
58
+ if value is None:
59
+ return value
60
+ if not value.startswith("="):
61
+ return value
62
+ engine = _get_engine()
63
+ if engine is None:
64
+ logger.warning(
65
+ "PowerFx engine not available for evaluating values starting with '='. "
66
+ "Ensure you are on python 3.13 or less and have the powerfx package installed. "
67
+ "Otherwise replace all powerfx statements in your yaml with strings."
68
+ )
69
+ return value
70
+ try:
71
+ safe_mode = _safe_mode_context.get()
72
+ if safe_mode:
73
+ return engine.eval(value[1:])
74
+ return engine.eval(value[1:], symbols={"Env": dict(os.environ)})
75
+ except Exception as exc:
76
+ if log_value:
77
+ logger.debug("PowerFx evaluation failed for a value: %s", exc)
78
+ else:
79
+ logger.debug("PowerFx evaluation failed for a value (details redacted): %s", exc)
80
+ return value
81
+
82
+
83
+ class Binding(SerializationMixin):
84
+ """Object representing a tool argument binding."""
85
+
86
+ def __init__(
87
+ self,
88
+ name: str | None = None,
89
+ input: str | None = None,
90
+ ) -> None:
91
+ self.name = _try_powerfx_eval(name)
92
+ self.input = _try_powerfx_eval(input)
93
+
94
+
95
+ class Property(SerializationMixin):
96
+ """Object representing a property in a schema."""
97
+
98
+ def __init__(
99
+ self,
100
+ name: str | None = None,
101
+ kind: str | None = None,
102
+ description: str | None = None,
103
+ required: bool | None = None,
104
+ default: Any | None = None,
105
+ example: Any | None = None,
106
+ enum: list[Any] | None = None,
107
+ ) -> None:
108
+ self.name = _try_powerfx_eval(name)
109
+ self.kind = _try_powerfx_eval(kind)
110
+ self.description = _try_powerfx_eval(description)
111
+ self.required = required
112
+ self.default = default
113
+ self.example = example
114
+ self.enum = enum or []
115
+
116
+ @classmethod
117
+ def from_dict(
118
+ cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
119
+ ) -> Property:
120
+ """Create a Property instance from a dictionary, dispatching to the appropriate subclass."""
121
+ # Only dispatch if we're being called on the base Property class
122
+ if cls is not Property:
123
+ # We're being called on a subclass, use the normal from_dict
124
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
125
+
126
+ # The YAML spec uses 'type' for the data type, but Property stores it as 'kind'
127
+ if "type" in value:
128
+ if "kind" not in value:
129
+ value["kind"] = value.pop("type")
130
+ else:
131
+ value.pop("type")
132
+ kind = value.get("kind", "")
133
+ if kind == "array":
134
+ return ArrayProperty.from_dict(value, dependencies=dependencies)
135
+ if kind == "object":
136
+ return ObjectProperty.from_dict(value, dependencies=dependencies)
137
+ # Default to Property for kind="property" or empty
138
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
139
+
140
+
141
+ class ArrayProperty(Property):
142
+ """Object representing an array property."""
143
+
144
+ def __init__(
145
+ self,
146
+ name: str | None = None,
147
+ kind: str = "array",
148
+ description: str | None = None,
149
+ required: bool | None = None,
150
+ default: Any | None = None,
151
+ example: Any | None = None,
152
+ enum: list[Any] | None = None,
153
+ items: Property | None = None,
154
+ ) -> None:
155
+ super().__init__(
156
+ name=name,
157
+ kind=kind,
158
+ description=description,
159
+ required=required,
160
+ default=default,
161
+ example=example,
162
+ enum=enum,
163
+ )
164
+ if not isinstance(items, Property) and items is not None:
165
+ items = Property.from_dict(items)
166
+ self.items = items
167
+
168
+
169
+ class ObjectProperty(Property):
170
+ """Object representing an object property."""
171
+
172
+ def __init__(
173
+ self,
174
+ name: str | None = None,
175
+ kind: str = "object",
176
+ description: str | None = None,
177
+ required: bool | None = None,
178
+ default: Any | None = None,
179
+ example: Any | None = None,
180
+ enum: list[Any] | None = None,
181
+ properties: list[Property] | dict[str, dict[str, Any]] | None = None,
182
+ ) -> None:
183
+ super().__init__(
184
+ name=name,
185
+ kind=kind,
186
+ description=description,
187
+ required=required,
188
+ default=default,
189
+ example=example,
190
+ enum=enum,
191
+ )
192
+ converted_properties: list[Property] = []
193
+ if isinstance(properties, list):
194
+ for prop in properties:
195
+ if not isinstance(prop, Property):
196
+ prop = Property.from_dict(prop)
197
+ converted_properties.append(prop)
198
+ elif isinstance(properties, dict):
199
+ for k, v in properties.items():
200
+ temp_prop = {"name": k, **v}
201
+ prop = Property.from_dict(temp_prop)
202
+ converted_properties.append(prop)
203
+ self.properties = converted_properties
204
+
205
+
206
+ def _normalize_nested_schemas(node: dict[str, Any]) -> None:
207
+ """Recursively convert a node's nested schemas to JSON Schema form.
208
+
209
+ Nested schemas (array ``items``, object ``properties``) keep the declarative
210
+ shape after serialization: ``kind`` instead of ``type``, empty ``enum``
211
+ placeholders, and object properties as a list of ``{"name": ..., ...}``
212
+ entries. OpenAI rejects schemas whose nested nodes lack a ``type`` key, so
213
+ apply the same conversion the top-level properties loop performs.
214
+ """
215
+ items = node.get("items")
216
+ if isinstance(items, dict):
217
+ _normalize_schema_node(cast("dict[str, Any]", items))
218
+ props = node.get("properties")
219
+ if not isinstance(props, list):
220
+ return
221
+ # Serialized PropertySchema shape: [{"name": ..., "kind": ..., ...}, ...].
222
+ # Validate every element BEFORE mutating any, so an unexpected shape
223
+ # leaves the node fully untouched rather than half-converted.
224
+ if not all(isinstance(prop, dict) and "name" in prop for prop in cast("list[Any]", props)):
225
+ return
226
+ new_props: dict[str, Any] = {}
227
+ required_fields: list[str] = []
228
+ for prop in cast("list[dict[str, Any]]", props):
229
+ prop_name = prop.pop("name")
230
+ if prop.pop("required", False):
231
+ required_fields.append(prop_name)
232
+ _normalize_schema_node(prop)
233
+ new_props[prop_name] = prop
234
+ node["properties"] = new_props
235
+ if required_fields:
236
+ node["required"] = required_fields
237
+
238
+
239
+ def _normalize_schema_node(node: dict[str, Any]) -> None:
240
+ """Rename ``kind`` -> ``type``, drop empty ``enum``, and recurse into children."""
241
+ if "kind" in node:
242
+ node["type"] = node.pop("kind")
243
+ if not node.get("enum"):
244
+ node.pop("enum", None)
245
+ if node.get("type") == "object":
246
+ # OpenAI strict structured outputs require additionalProperties: false on
247
+ # every object node; chat clients only inject it at the schema root.
248
+ node.setdefault("additionalProperties", False)
249
+ _normalize_nested_schemas(node)
250
+
251
+
252
+ class PropertySchema(SerializationMixin):
253
+ """Object representing a property schema."""
254
+
255
+ def __init__(
256
+ self,
257
+ examples: list[dict[str, Any]] | None = None,
258
+ strict: bool = False,
259
+ properties: list[Property] | dict[str, dict[str, Any]] | None = None,
260
+ ) -> None:
261
+ self.examples = examples or []
262
+ self.strict = strict
263
+ converted_properties: list[Property] = []
264
+ if isinstance(properties, list):
265
+ for prop in properties:
266
+ if not isinstance(prop, Property):
267
+ prop = Property.from_dict(prop)
268
+ converted_properties.append(prop)
269
+ elif isinstance(properties, dict):
270
+ for k, v in properties.items():
271
+ temp_prop = {"name": k, **v}
272
+ prop = Property.from_dict(temp_prop)
273
+ converted_properties.append(prop)
274
+ self.properties = converted_properties
275
+
276
+ @classmethod
277
+ def from_dict(
278
+ cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
279
+ ) -> PropertySchema:
280
+ """Create a PropertySchema instance from a dictionary, filtering out 'kind' field."""
281
+ # Filter out 'kind', 'type', 'name', and 'description' fields that may appear in YAML
282
+ # but aren't PropertySchema params
283
+ kwargs = {k: v for k, v in value.items() if k not in ("type", "kind", "name", "description")}
284
+ return SerializationMixin.from_dict.__func__(cls, kwargs, dependencies=dependencies)
285
+
286
+ def to_json_schema(self) -> dict[str, Any]:
287
+ """Get a schema out of this PropertySchema to create pydantic models."""
288
+ json_schema = self.to_dict(exclude={"type"}, exclude_none=True)
289
+ new_props = {}
290
+ required_fields: list[str] = []
291
+ for prop in json_schema.get("properties", []):
292
+ prop_name = prop.pop("name")
293
+ # Convert property-level 'required' boolean to a top-level 'required' array
294
+ if prop.pop("required", False):
295
+ required_fields.append(prop_name)
296
+ _normalize_schema_node(prop)
297
+ new_props[prop_name] = prop
298
+ json_schema["type"] = "object"
299
+ json_schema["properties"] = new_props
300
+ if required_fields:
301
+ json_schema["required"] = required_fields
302
+ return json_schema
303
+
304
+
305
+ ConnectionT = TypeVar("ConnectionT", bound="Connection")
306
+
307
+
308
+ class Connection(SerializationMixin):
309
+ """Object representing a connection specification."""
310
+
311
+ def __init__(
312
+ self,
313
+ kind: Literal["reference", "remote", "key", "anonymous"],
314
+ authenticationMode: str | None = None,
315
+ usageDescription: str | None = None,
316
+ ) -> None:
317
+ self.kind = kind
318
+ self.authenticationMode = _try_powerfx_eval(authenticationMode)
319
+ self.usageDescription = _try_powerfx_eval(usageDescription)
320
+
321
+ @classmethod
322
+ def from_dict(
323
+ cls: type[ConnectionT],
324
+ value: MutableMapping[str, Any],
325
+ /,
326
+ *,
327
+ dependencies: MutableMapping[str, Any] | None = None,
328
+ ) -> ConnectionT:
329
+ """Create a Connection instance from a dictionary, dispatching to the appropriate subclass."""
330
+ # Only dispatch if we're being called on the base Connection class
331
+ if cls is not Connection:
332
+ # We're being called on a subclass, use the normal from_dict
333
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
334
+
335
+ kind = value.get("kind", "").lower()
336
+ if kind == "reference":
337
+ return SerializationMixin.from_dict.__func__(ReferenceConnection, value, dependencies=dependencies)
338
+ if kind == "remote":
339
+ return SerializationMixin.from_dict.__func__(RemoteConnection, value, dependencies=dependencies)
340
+ if kind in ("key", "apikey"):
341
+ return SerializationMixin.from_dict.__func__(ApiKeyConnection, value, dependencies=dependencies)
342
+ if kind == "anonymous":
343
+ return SerializationMixin.from_dict.__func__(AnonymousConnection, value, dependencies=dependencies)
344
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
345
+
346
+
347
+ class ReferenceConnection(Connection):
348
+ """Object representing a reference connection."""
349
+
350
+ def __init__(
351
+ self,
352
+ kind: Literal["reference"] = "reference",
353
+ authenticationMode: str | None = None,
354
+ usageDescription: str | None = None,
355
+ name: str | None = None,
356
+ target: str | None = None,
357
+ ) -> None:
358
+ super().__init__(
359
+ kind=kind,
360
+ authenticationMode=authenticationMode,
361
+ usageDescription=usageDescription,
362
+ )
363
+ self.name = _try_powerfx_eval(name)
364
+ self.target = _try_powerfx_eval(target)
365
+
366
+
367
+ class RemoteConnection(Connection):
368
+ """Object representing a remote connection."""
369
+
370
+ def __init__(
371
+ self,
372
+ kind: Literal["remote"] = "remote",
373
+ authenticationMode: str | None = None,
374
+ usageDescription: str | None = None,
375
+ name: str | None = None,
376
+ endpoint: str | None = None,
377
+ ) -> None:
378
+ super().__init__(
379
+ kind=kind,
380
+ authenticationMode=authenticationMode,
381
+ usageDescription=usageDescription,
382
+ )
383
+ self.name = _try_powerfx_eval(name)
384
+ self.endpoint = _try_powerfx_eval(endpoint)
385
+
386
+
387
+ class ApiKeyConnection(Connection):
388
+ """Object representing an API key connection."""
389
+
390
+ def __init__(
391
+ self,
392
+ kind: Literal["key"] = "key",
393
+ authenticationMode: str | None = None,
394
+ usageDescription: str | None = None,
395
+ endpoint: str | None = None,
396
+ apiKey: str | None = None,
397
+ key: str | None = None,
398
+ ) -> None:
399
+ super().__init__(
400
+ kind=kind,
401
+ authenticationMode=authenticationMode,
402
+ usageDescription=usageDescription,
403
+ )
404
+ self.endpoint = _try_powerfx_eval(endpoint)
405
+ # Support both 'apiKey' and 'key' fields, with 'key' taking precedence if both are provided
406
+ self.apiKey = _try_powerfx_eval(key if key else apiKey, False)
407
+
408
+
409
+ class AnonymousConnection(Connection):
410
+ """Object representing an anonymous connection."""
411
+
412
+ def __init__(
413
+ self,
414
+ kind: Literal["anonymous"] = "anonymous",
415
+ authenticationMode: str | None = None,
416
+ usageDescription: str | None = None,
417
+ endpoint: str | None = None,
418
+ ) -> None:
419
+ super().__init__(
420
+ kind=kind,
421
+ authenticationMode=authenticationMode,
422
+ usageDescription=usageDescription,
423
+ )
424
+ self.endpoint = _try_powerfx_eval(endpoint)
425
+
426
+
427
+ Connections = Union[
428
+ ReferenceConnection,
429
+ RemoteConnection,
430
+ ApiKeyConnection,
431
+ AnonymousConnection,
432
+ ]
433
+
434
+
435
+ class ModelOptions(SerializationMixin):
436
+ """Object representing model options."""
437
+
438
+ def __init__(
439
+ self,
440
+ frequencyPenalty: float | None = None,
441
+ maxOutputTokens: int | None = None,
442
+ presencePenalty: float | None = None,
443
+ seed: int | None = None,
444
+ temperature: float | None = None,
445
+ topK: int | None = None,
446
+ topP: float | None = None,
447
+ stopSequences: list[str] | None = None,
448
+ allowMultipleToolCalls: bool | None = None,
449
+ additionalProperties: dict[str, Any] | None = None,
450
+ **kwargs: Any,
451
+ ) -> None:
452
+ self.frequencyPenalty = frequencyPenalty
453
+ self.maxOutputTokens = maxOutputTokens
454
+ self.presencePenalty = presencePenalty
455
+ self.seed = seed
456
+ self.temperature = temperature
457
+ self.topK = topK
458
+ self.topP = topP
459
+ self.stopSequences = stopSequences or []
460
+ self.allowMultipleToolCalls = allowMultipleToolCalls
461
+ # Merge any additional properties from kwargs into additionalProperties
462
+ self.additionalProperties = additionalProperties or {}
463
+ self.additionalProperties.update(kwargs)
464
+
465
+
466
+ class Model(SerializationMixin):
467
+ """Object representing a model specification."""
468
+
469
+ def __init__(
470
+ self,
471
+ id: str | None = None,
472
+ provider: str | None = None,
473
+ apiType: str | None = None,
474
+ connection: Connections | None = None,
475
+ options: ModelOptions | None = None,
476
+ ) -> None:
477
+ self.id = _try_powerfx_eval(id)
478
+ self.provider = _try_powerfx_eval(provider)
479
+ self.apiType = _try_powerfx_eval(apiType)
480
+ if not isinstance(connection, Connection) and connection is not None:
481
+ connection = Connection.from_dict(connection)
482
+ self.connection = connection
483
+ if not isinstance(options, ModelOptions) and options is not None:
484
+ options = ModelOptions.from_dict(options)
485
+ self.options = options
486
+
487
+
488
+ class Format(SerializationMixin):
489
+ """Object representing template format."""
490
+
491
+ def __init__(
492
+ self,
493
+ kind: str | None = None,
494
+ strict: bool = False,
495
+ options: dict[str, Any] | None = None,
496
+ ) -> None:
497
+ self.kind = _try_powerfx_eval(kind)
498
+ self.strict = strict
499
+ self.options = options or {}
500
+
501
+
502
+ class Parser(SerializationMixin):
503
+ """Object representing template parser."""
504
+
505
+ def __init__(
506
+ self,
507
+ kind: str | None = None,
508
+ options: dict[str, Any] | None = None,
509
+ ) -> None:
510
+ self.kind = _try_powerfx_eval(kind)
511
+ self.options = options or {}
512
+
513
+
514
+ class Template(SerializationMixin):
515
+ """Object representing a template configuration."""
516
+
517
+ def __init__(
518
+ self,
519
+ format: Format | None = None,
520
+ parser: Parser | None = None,
521
+ ) -> None:
522
+ if not isinstance(format, Format) and format is not None:
523
+ format = Format.from_dict(format)
524
+ self.format = format
525
+ if not isinstance(parser, Parser) and parser is not None:
526
+ parser = Parser.from_dict(parser)
527
+ self.parser = parser
528
+
529
+
530
+ class AgentDefinition(SerializationMixin):
531
+ """Object representing a prompt specification."""
532
+
533
+ def __init__(
534
+ self,
535
+ kind: str | None = None,
536
+ name: str | None = None,
537
+ displayName: str | None = None,
538
+ description: str | None = None,
539
+ metadata: dict[str, Any] | None = None,
540
+ inputSchema: PropertySchema | None = None,
541
+ outputSchema: PropertySchema | None = None,
542
+ ) -> None:
543
+ self.kind = _try_powerfx_eval(kind)
544
+ self.name = _try_powerfx_eval(name)
545
+ self.displayName = _try_powerfx_eval(displayName)
546
+ self.description = _try_powerfx_eval(description)
547
+ self.metadata = metadata
548
+ if not isinstance(inputSchema, PropertySchema) and inputSchema is not None:
549
+ inputSchema = PropertySchema.from_dict(inputSchema)
550
+ self.inputSchema = inputSchema
551
+ if not isinstance(outputSchema, PropertySchema) and outputSchema is not None:
552
+ outputSchema = PropertySchema.from_dict(outputSchema)
553
+ self.outputSchema = outputSchema
554
+
555
+ @classmethod
556
+ def from_dict(
557
+ cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
558
+ ) -> AgentDefinition:
559
+ """Create an AgentDefinition instance from a dictionary, dispatching to the appropriate subclass."""
560
+ # Only dispatch if we're being called on the base AgentDefinition class
561
+ if cls is not AgentDefinition:
562
+ # We're being called on a subclass, use the normal from_dict
563
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
564
+
565
+ kind = value.get("kind", "")
566
+ if kind == "Prompt" or kind == "Agent":
567
+ return PromptAgent.from_dict(value, dependencies=dependencies)
568
+ # Default to AgentDefinition
569
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
570
+
571
+
572
+ ToolT = TypeVar("ToolT", bound="Tool")
573
+
574
+
575
+ class Tool(SerializationMixin):
576
+ """Base class for tools."""
577
+
578
+ def __init__(
579
+ self,
580
+ name: str | None = None,
581
+ kind: str | None = None,
582
+ description: str | None = None,
583
+ bindings: list[Binding] | dict[str, Any] | None = None,
584
+ ) -> None:
585
+ self.name = _try_powerfx_eval(name)
586
+ self.kind = _try_powerfx_eval(kind)
587
+ self.description = _try_powerfx_eval(description)
588
+ converted_bindings: list[Binding] = []
589
+ if isinstance(bindings, list):
590
+ for binding in bindings:
591
+ if not isinstance(binding, Binding):
592
+ binding = Binding.from_dict(binding)
593
+ converted_bindings.append(binding)
594
+ elif isinstance(bindings, dict):
595
+ for k, v in bindings.items():
596
+ temp_binding = {"name": k, "input": v} if isinstance(v, str) else {"name": k, **v}
597
+ binding = Binding.from_dict(temp_binding)
598
+ converted_bindings.append(binding)
599
+ self.bindings = converted_bindings
600
+
601
+ @classmethod
602
+ def from_dict(
603
+ cls: type[ToolT], value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
604
+ ) -> ToolT:
605
+ """Create a Tool instance from a dictionary, dispatching to the appropriate subclass."""
606
+ # Only dispatch if we're being called on the base Tool class
607
+ if cls is not Tool:
608
+ # We're being called on a subclass, use the normal from_dict
609
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
610
+
611
+ kind = value.get("kind", "")
612
+ if kind == "function":
613
+ return SerializationMixin.from_dict.__func__(FunctionTool, value, dependencies=dependencies)
614
+ if kind == "custom":
615
+ return SerializationMixin.from_dict.__func__(CustomTool, value, dependencies=dependencies)
616
+ if kind == "web_search":
617
+ return SerializationMixin.from_dict.__func__(WebSearchTool, value, dependencies=dependencies)
618
+ if kind == "file_search":
619
+ return SerializationMixin.from_dict.__func__(FileSearchTool, value, dependencies=dependencies)
620
+ if kind == "mcp":
621
+ return SerializationMixin.from_dict.__func__(McpTool, value, dependencies=dependencies)
622
+ if kind == "openapi":
623
+ return SerializationMixin.from_dict.__func__(OpenApiTool, value, dependencies=dependencies)
624
+ if kind == "code_interpreter":
625
+ return SerializationMixin.from_dict.__func__(CodeInterpreterTool, value, dependencies=dependencies)
626
+ # Default to base Tool class
627
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
628
+
629
+
630
+ class FunctionTool(Tool):
631
+ """Object representing a function tool."""
632
+
633
+ def __init__(
634
+ self,
635
+ name: str | None = None,
636
+ kind: str = "function",
637
+ description: str | None = None,
638
+ bindings: list[Binding] | None = None,
639
+ parameters: PropertySchema | list[Property] | dict[str, Any] | None = None,
640
+ strict: bool = False,
641
+ ) -> None:
642
+ super().__init__(
643
+ name=name,
644
+ kind=kind,
645
+ description=description,
646
+ bindings=bindings,
647
+ )
648
+ if isinstance(parameters, list):
649
+ # If parameters is a list, wrap it in a PropertySchema
650
+ parameters = PropertySchema(properties=parameters)
651
+ elif not isinstance(parameters, PropertySchema) and parameters is not None:
652
+ parameters = PropertySchema.from_dict(parameters)
653
+ self.parameters = parameters
654
+ self.strict = strict
655
+
656
+
657
+ class CustomTool(Tool):
658
+ """Object representing a custom tool."""
659
+
660
+ def __init__(
661
+ self,
662
+ name: str | None = None,
663
+ kind: str = "custom",
664
+ description: str | None = None,
665
+ bindings: list[Binding] | None = None,
666
+ connection: Connection | None = None,
667
+ options: dict[str, Any] | None = None,
668
+ ) -> None:
669
+ super().__init__(
670
+ name=name,
671
+ kind=kind,
672
+ description=description,
673
+ bindings=bindings,
674
+ )
675
+ if not isinstance(connection, Connection) and connection is not None:
676
+ connection = Connection.from_dict(connection)
677
+ self.connection = connection
678
+ self.options = options or {}
679
+
680
+
681
+ class WebSearchTool(Tool):
682
+ """Object representing a web search tool."""
683
+
684
+ def __init__(
685
+ self,
686
+ name: str | None = None,
687
+ kind: str = "web_search",
688
+ description: str | None = None,
689
+ bindings: list[Binding] | None = None,
690
+ connection: Connection | None = None,
691
+ options: dict[str, Any] | None = None,
692
+ ) -> None:
693
+ super().__init__(
694
+ name=name,
695
+ kind=kind,
696
+ description=description,
697
+ bindings=bindings,
698
+ )
699
+ if not isinstance(connection, Connection) and connection is not None:
700
+ connection = Connection.from_dict(connection)
701
+ self.connection = connection
702
+ self.options = options or {}
703
+
704
+
705
+ class FileSearchTool(Tool):
706
+ """Object representing a file search tool."""
707
+
708
+ def __init__(
709
+ self,
710
+ name: str | None = None,
711
+ kind: str = "file_search",
712
+ description: str | None = None,
713
+ bindings: list[Binding] | None = None,
714
+ connection: Connection | None = None,
715
+ vectorStoreIds: list[str] | None = None,
716
+ maximumResultCount: int | None = None,
717
+ ranker: str | None = None,
718
+ scoreThreshold: float | None = None,
719
+ filters: dict[str, Any] | None = None,
720
+ ) -> None:
721
+ super().__init__(
722
+ name=name,
723
+ kind=kind,
724
+ description=description,
725
+ bindings=bindings,
726
+ )
727
+ if not isinstance(connection, Connection) and connection is not None:
728
+ connection = Connection.from_dict(connection)
729
+ self.connection = connection
730
+ self.vectorStoreIds = vectorStoreIds or []
731
+ self.maximumResultCount = maximumResultCount
732
+ self.ranker = _try_powerfx_eval(ranker)
733
+ self.scoreThreshold = scoreThreshold
734
+ self.filters = filters or {}
735
+
736
+
737
+ class McpServerApprovalMode(SerializationMixin):
738
+ """Base class for MCP server approval modes."""
739
+
740
+ def __init__(
741
+ self,
742
+ kind: str | None = None,
743
+ ) -> None:
744
+ self.kind = _try_powerfx_eval(kind)
745
+
746
+
747
+ class McpServerToolAlwaysRequireApprovalMode(McpServerApprovalMode):
748
+ """MCP server tool always require approval mode."""
749
+
750
+ def __init__(
751
+ self,
752
+ kind: str = "always",
753
+ ) -> None:
754
+ super().__init__(kind=kind)
755
+
756
+
757
+ class McpServerToolNeverRequireApprovalMode(McpServerApprovalMode):
758
+ """MCP server tool never require approval mode."""
759
+
760
+ def __init__(
761
+ self,
762
+ kind: str = "never",
763
+ ) -> None:
764
+ super().__init__(kind=kind)
765
+
766
+
767
+ class McpServerToolSpecifyApprovalMode(McpServerApprovalMode):
768
+ """MCP server tool specify approval mode."""
769
+
770
+ def __init__(
771
+ self,
772
+ kind: str = "specify",
773
+ alwaysRequireApprovalTools: list[str] | None = None,
774
+ neverRequireApprovalTools: list[str] | None = None,
775
+ ) -> None:
776
+ super().__init__(kind=kind)
777
+ self.alwaysRequireApprovalTools = alwaysRequireApprovalTools
778
+ self.neverRequireApprovalTools = neverRequireApprovalTools
779
+
780
+
781
+ class McpTool(Tool):
782
+ """Object representing an MCP tool."""
783
+
784
+ def __init__(
785
+ self,
786
+ name: str | None = None,
787
+ kind: str = "mcp",
788
+ description: str | None = None,
789
+ bindings: list[Binding] | None = None,
790
+ connection: Connection | None = None,
791
+ serverName: str | None = None,
792
+ serverDescription: str | None = None,
793
+ approvalMode: McpServerApprovalMode | None = None,
794
+ allowedTools: list[str] | None = None,
795
+ url: str | None = None,
796
+ ) -> None:
797
+ super().__init__(
798
+ name=name,
799
+ kind=kind,
800
+ description=description,
801
+ bindings=bindings,
802
+ )
803
+ if not isinstance(connection, Connection) and connection is not None:
804
+ connection = Connection.from_dict(connection)
805
+ self.connection = connection
806
+ self.serverName = _try_powerfx_eval(serverName)
807
+ self.serverDescription = _try_powerfx_eval(serverDescription)
808
+ if not isinstance(approvalMode, McpServerApprovalMode) and approvalMode is not None:
809
+ # Handle simplified string format: "always" -> {"kind": "always"}
810
+ if isinstance(approvalMode, str):
811
+ approvalMode = McpServerApprovalMode.from_dict({"kind": approvalMode})
812
+ else:
813
+ approvalMode = McpServerApprovalMode.from_dict(approvalMode)
814
+ self.approvalMode = approvalMode
815
+ self.allowedTools = allowedTools or []
816
+ self.url = _try_powerfx_eval(url)
817
+
818
+
819
+ class OpenApiTool(Tool):
820
+ """Object representing an OpenAPI tool."""
821
+
822
+ def __init__(
823
+ self,
824
+ name: str | None = None,
825
+ kind: str = "openapi",
826
+ description: str | None = None,
827
+ bindings: list[Binding] | None = None,
828
+ connection: Connection | None = None,
829
+ specification: str | None = None,
830
+ ) -> None:
831
+ super().__init__(
832
+ name=name,
833
+ kind=kind,
834
+ description=description,
835
+ bindings=bindings,
836
+ )
837
+ if not isinstance(connection, Connection) and connection is not None:
838
+ connection = Connection.from_dict(connection)
839
+ self.connection = connection
840
+ self.specification = _try_powerfx_eval(specification)
841
+
842
+
843
+ class CodeInterpreterTool(Tool):
844
+ """Object representing a code interpreter tool."""
845
+
846
+ def __init__(
847
+ self,
848
+ name: str | None = None,
849
+ kind: str = "code_interpreter",
850
+ description: str | None = None,
851
+ bindings: list[Binding] | None = None,
852
+ fileIds: list[str] | None = None,
853
+ ) -> None:
854
+ super().__init__(
855
+ name=name,
856
+ kind=kind,
857
+ description=description,
858
+ bindings=bindings,
859
+ )
860
+ self.fileIds = fileIds or []
861
+
862
+
863
+ class PromptAgent(AgentDefinition):
864
+ """Object representing a prompt agent specification."""
865
+
866
+ def __init__(
867
+ self,
868
+ kind: str = "Prompt",
869
+ name: str | None = None,
870
+ displayName: str | None = None,
871
+ description: str | None = None,
872
+ metadata: dict[str, Any] | None = None,
873
+ inputSchema: PropertySchema | None = None,
874
+ outputSchema: PropertySchema | None = None,
875
+ model: Model | dict[str, Any] | None = None,
876
+ tools: list[Tool] | None = None,
877
+ template: Template | dict[str, Any] | None = None,
878
+ instructions: str | None = None,
879
+ additionalInstructions: str | None = None,
880
+ ) -> None:
881
+ super().__init__(
882
+ kind=kind,
883
+ name=name,
884
+ displayName=displayName,
885
+ description=description,
886
+ metadata=metadata,
887
+ inputSchema=inputSchema,
888
+ outputSchema=outputSchema,
889
+ )
890
+ if not isinstance(model, Model) and model is not None:
891
+ model = Model.from_dict(model)
892
+ self.model = model
893
+ converted_tools: list[Tool] = []
894
+ for tool in tools or []:
895
+ if not isinstance(tool, Tool):
896
+ tool = Tool.from_dict(tool)
897
+ converted_tools.append(tool)
898
+ self.tools = converted_tools
899
+ if not isinstance(template, Template) and template is not None:
900
+ template = Template.from_dict(template)
901
+ self.template = template
902
+ self.instructions = _try_powerfx_eval(instructions)
903
+ self.additionalInstructions = _try_powerfx_eval(additionalInstructions)
904
+
905
+
906
+ class Resource(SerializationMixin):
907
+ """Object representing a resource."""
908
+
909
+ def __init__(
910
+ self,
911
+ name: str | None = None,
912
+ kind: str | None = None,
913
+ ) -> None:
914
+ self.name = _try_powerfx_eval(name)
915
+ self.kind = _try_powerfx_eval(kind)
916
+
917
+ @classmethod
918
+ def from_dict(
919
+ cls, value: MutableMapping[str, Any], /, *, dependencies: MutableMapping[str, Any] | None = None
920
+ ) -> Resource:
921
+ """Create a Resource instance from a dictionary, dispatching to the appropriate subclass."""
922
+ # Only dispatch if we're being called on the base Resource class
923
+ if cls is not Resource:
924
+ # We're being called on a subclass, use the normal from_dict
925
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
926
+
927
+ kind = value.get("kind", "")
928
+ if kind == "model":
929
+ return SerializationMixin.from_dict.__func__(ModelResource, value, dependencies=dependencies)
930
+ if kind == "tool":
931
+ return SerializationMixin.from_dict.__func__(ToolResource, value, dependencies=dependencies)
932
+ return SerializationMixin.from_dict.__func__(cls, value, dependencies=dependencies)
933
+
934
+
935
+ class ModelResource(Resource):
936
+ """Object representing a model resource."""
937
+
938
+ def __init__(
939
+ self,
940
+ kind: str = "model",
941
+ name: str | None = None,
942
+ id: str | None = None,
943
+ ) -> None:
944
+ super().__init__(kind=kind, name=name)
945
+ self.id = _try_powerfx_eval(id)
946
+
947
+
948
+ class ToolResource(Resource):
949
+ """Object representing a tool resource."""
950
+
951
+ def __init__(
952
+ self,
953
+ kind: str = "tool",
954
+ name: str | None = None,
955
+ id: str | None = None,
956
+ options: dict[str, Any] | None = None,
957
+ ) -> None:
958
+ super().__init__(kind=kind, name=name)
959
+ self.id = _try_powerfx_eval(id)
960
+ self.options = options or {}
961
+
962
+
963
+ class ProtocolVersionRecord(SerializationMixin):
964
+ """Object representing a protocol version record."""
965
+
966
+ def __init__(
967
+ self,
968
+ protocol: str | None = None,
969
+ version: str | None = None,
970
+ ) -> None:
971
+ self.protocol = _try_powerfx_eval(protocol)
972
+ self.version = _try_powerfx_eval(version)
973
+
974
+
975
+ class EnvironmentVariable(SerializationMixin):
976
+ """Object representing an environment variable."""
977
+
978
+ def __init__(
979
+ self,
980
+ name: str | None = None,
981
+ value: str | None = None,
982
+ ) -> None:
983
+ self.name = _try_powerfx_eval(name)
984
+ self.value = _try_powerfx_eval(value)
985
+
986
+
987
+ class AgentManifest(SerializationMixin):
988
+ """Object representing an agent manifest."""
989
+
990
+ def __init__(
991
+ self,
992
+ name: str | None = None,
993
+ displayName: str | None = None,
994
+ description: str | None = None,
995
+ metadata: dict[str, Any] | None = None,
996
+ template: AgentDefinition | None = None,
997
+ parameters: PropertySchema | None = None,
998
+ resources: list[Resource] | dict[str, Any] | None = None,
999
+ ) -> None:
1000
+ self.name = _try_powerfx_eval(name)
1001
+ self.displayName = _try_powerfx_eval(displayName)
1002
+ self.description = _try_powerfx_eval(description)
1003
+ self.metadata = metadata or {}
1004
+ if not isinstance(template, AgentDefinition) and template is not None:
1005
+ template = AgentDefinition.from_dict(template)
1006
+ self.template = template or AgentDefinition()
1007
+ if not isinstance(parameters, PropertySchema) and parameters is not None:
1008
+ parameters = PropertySchema.from_dict(parameters)
1009
+ self.parameters = parameters or PropertySchema()
1010
+ converted_resources: list[Resource] = []
1011
+ if isinstance(resources, list):
1012
+ for resource in resources:
1013
+ if not isinstance(resource, Resource):
1014
+ resource = Resource.from_dict(resource)
1015
+ converted_resources.append(resource)
1016
+ elif isinstance(resources, dict):
1017
+ for k, v in resources.items():
1018
+ temp_resource = {"name": k, **v}
1019
+ resource = Resource.from_dict(temp_resource)
1020
+ converted_resources.append(resource)
1021
+ self.resources = converted_resources
1022
+
1023
+
1024
+ AgentSchemaSpec = Union[
1025
+ AgentManifest,
1026
+ AgentDefinition,
1027
+ PromptAgent,
1028
+ Tool,
1029
+ FunctionTool,
1030
+ CustomTool,
1031
+ WebSearchTool,
1032
+ FileSearchTool,
1033
+ McpTool,
1034
+ OpenApiTool,
1035
+ CodeInterpreterTool,
1036
+ Resource,
1037
+ ModelResource,
1038
+ ToolResource,
1039
+ Connection,
1040
+ ReferenceConnection,
1041
+ RemoteConnection,
1042
+ ApiKeyConnection,
1043
+ AnonymousConnection,
1044
+ Property,
1045
+ ArrayProperty,
1046
+ ObjectProperty,
1047
+ PropertySchema,
1048
+ McpServerApprovalMode,
1049
+ McpServerToolAlwaysRequireApprovalMode,
1050
+ McpServerToolNeverRequireApprovalMode,
1051
+ McpServerToolSpecifyApprovalMode,
1052
+ Binding,
1053
+ Format,
1054
+ Parser,
1055
+ Template,
1056
+ Model,
1057
+ ModelOptions,
1058
+ ProtocolVersionRecord,
1059
+ EnvironmentVariable,
1060
+ ]
1061
+
1062
+
1063
+ def agent_schema_dispatch(schema: dict[str, Any]) -> AgentSchemaSpec | None:
1064
+ """Create a component instance from a dictionary, dispatching to the appropriate class based on 'kind' field."""
1065
+ kind = schema.get("kind")
1066
+
1067
+ # If no kind field, assume it's an AgentManifest
1068
+ if kind is None:
1069
+ return AgentManifest.from_dict(schema)
1070
+ # Match on the kind field to determine which class to instantiate
1071
+ match kind.lower():
1072
+ # Agent types
1073
+ case "prompt":
1074
+ return PromptAgent.from_dict(schema)
1075
+ case "agent":
1076
+ return AgentDefinition.from_dict(schema)
1077
+
1078
+ # Resource types
1079
+ case "tool":
1080
+ return ToolResource.from_dict(schema)
1081
+ case "model":
1082
+ return ModelResource.from_dict(schema)
1083
+ case "resource":
1084
+ return Resource.from_dict(schema)
1085
+
1086
+ # Tool types
1087
+ case "function":
1088
+ return FunctionTool.from_dict(schema)
1089
+ case "custom":
1090
+ return CustomTool.from_dict(schema)
1091
+ case "web_search":
1092
+ return WebSearchTool.from_dict(schema)
1093
+ case "file_search":
1094
+ return FileSearchTool.from_dict(schema)
1095
+ case "mcp":
1096
+ return McpTool.from_dict(schema)
1097
+ case "openapi":
1098
+ return OpenApiTool.from_dict(schema)
1099
+ case "code_interpreter":
1100
+ return CodeInterpreterTool.from_dict(schema)
1101
+
1102
+ # Connection types
1103
+ case "reference":
1104
+ return ReferenceConnection.from_dict(schema)
1105
+ case "remote":
1106
+ return RemoteConnection.from_dict(schema)
1107
+ case "key":
1108
+ return ApiKeyConnection.from_dict(schema)
1109
+ case "anonymous":
1110
+ return AnonymousConnection.from_dict(schema)
1111
+ case "connection":
1112
+ return Connection.from_dict(schema)
1113
+
1114
+ # Property types
1115
+ case "array":
1116
+ return ArrayProperty.from_dict(schema)
1117
+ case "object":
1118
+ return ObjectProperty.from_dict(schema)
1119
+ case "property":
1120
+ return Property.from_dict(schema)
1121
+
1122
+ # MCP Server Approval Mode types
1123
+ case "always":
1124
+ return McpServerToolAlwaysRequireApprovalMode.from_dict(schema)
1125
+ case "never":
1126
+ return McpServerToolNeverRequireApprovalMode.from_dict(schema)
1127
+ case "specify":
1128
+ return McpServerToolSpecifyApprovalMode.from_dict(schema)
1129
+ case "approval_mode":
1130
+ return McpServerApprovalMode.from_dict(schema)
1131
+
1132
+ # Other component types
1133
+ case "binding":
1134
+ return Binding.from_dict(schema)
1135
+ case "format":
1136
+ return Format.from_dict(schema)
1137
+ case "parser":
1138
+ return Parser.from_dict(schema)
1139
+ case "template":
1140
+ return Template.from_dict(schema)
1141
+ case "model":
1142
+ return Model.from_dict(schema)
1143
+ case "model_options":
1144
+ return ModelOptions.from_dict(schema)
1145
+ case "property_schema":
1146
+ return PropertySchema.from_dict(schema)
1147
+ case "protocol_version":
1148
+ return ProtocolVersionRecord.from_dict(schema)
1149
+ case "environment_variable":
1150
+ return EnvironmentVariable.from_dict(schema)
1151
+
1152
+ # Unknown kind
1153
+ case _:
1154
+ return None