ag2 0.9.1.post0__py3-none-any.whl → 0.9.3__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.

Potentially problematic release.


This version of ag2 might be problematic. Click here for more details.

Files changed (37) hide show
  1. {ag2-0.9.1.post0.dist-info → ag2-0.9.3.dist-info}/METADATA +22 -12
  2. {ag2-0.9.1.post0.dist-info → ag2-0.9.3.dist-info}/RECORD +37 -23
  3. autogen/agentchat/contrib/capabilities/transforms.py +22 -9
  4. autogen/agentchat/conversable_agent.py +37 -34
  5. autogen/agentchat/group/group_utils.py +65 -20
  6. autogen/agentchat/group/handoffs.py +81 -5
  7. autogen/agentchat/group/on_context_condition.py +2 -2
  8. autogen/agentchat/group/patterns/pattern.py +7 -1
  9. autogen/agentchat/groupchat.py +2 -2
  10. autogen/agentchat/realtime/experimental/realtime_swarm.py +12 -4
  11. autogen/agents/experimental/document_agent/document_agent.py +232 -40
  12. autogen/events/agent_events.py +7 -4
  13. autogen/interop/litellm/litellm_config_factory.py +68 -2
  14. autogen/llm_config.py +4 -1
  15. autogen/mcp/__main__.py +78 -0
  16. autogen/mcp/mcp_proxy/__init__.py +19 -0
  17. autogen/mcp/mcp_proxy/fastapi_code_generator_helpers.py +63 -0
  18. autogen/mcp/mcp_proxy/mcp_proxy.py +581 -0
  19. autogen/mcp/mcp_proxy/operation_grouping.py +158 -0
  20. autogen/mcp/mcp_proxy/operation_renaming.py +114 -0
  21. autogen/mcp/mcp_proxy/patch_fastapi_code_generator.py +98 -0
  22. autogen/mcp/mcp_proxy/security.py +400 -0
  23. autogen/mcp/mcp_proxy/security_schema_visitor.py +37 -0
  24. autogen/oai/client.py +11 -2
  25. autogen/oai/gemini.py +20 -3
  26. autogen/oai/gemini_types.py +27 -0
  27. autogen/oai/oai_models/chat_completion.py +1 -1
  28. autogen/tools/experimental/__init__.py +5 -0
  29. autogen/tools/experimental/reliable/__init__.py +10 -0
  30. autogen/tools/experimental/reliable/reliable.py +1316 -0
  31. autogen/version.py +1 -1
  32. templates/client_template/main.jinja2 +69 -0
  33. templates/config_template/config.jinja2 +7 -0
  34. templates/main.jinja2 +61 -0
  35. {ag2-0.9.1.post0.dist-info → ag2-0.9.3.dist-info}/WHEEL +0 -0
  36. {ag2-0.9.1.post0.dist-info → ag2-0.9.3.dist-info}/licenses/LICENSE +0 -0
  37. {ag2-0.9.1.post0.dist-info → ag2-0.9.3.dist-info}/licenses/NOTICE.md +0 -0
@@ -0,0 +1,581 @@
1
+ # Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import builtins
5
+ import importlib
6
+ import inspect
7
+ import json
8
+ import re
9
+ import sys
10
+ import tempfile
11
+ from collections.abc import Iterable, Iterator, Mapping
12
+ from contextlib import contextmanager
13
+ from functools import wraps
14
+ from logging import getLogger
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+ from typing import (
18
+ TYPE_CHECKING,
19
+ Any,
20
+ Callable,
21
+ Literal,
22
+ Optional,
23
+ Union,
24
+ )
25
+
26
+ import requests
27
+ from pydantic import PydanticInvalidForJsonSchema
28
+ from pydantic_core import PydanticUndefined
29
+
30
+ from autogen.import_utils import optional_import_block, require_optional_import
31
+
32
+ from .security import BaseSecurity, BaseSecurityParameters
33
+
34
+ with optional_import_block() as result:
35
+ import fastapi
36
+ import yaml
37
+ from datamodel_code_generator import DataModelType
38
+ from fastapi_code_generator.__main__ import generate_code
39
+ from jinja2 import Environment, FileSystemLoader
40
+ from mcp.server.fastmcp import FastMCP
41
+
42
+
43
+ if TYPE_CHECKING:
44
+ from autogen.agentchat import ConversableAgent
45
+
46
+ __all__ = ["MCPProxy"]
47
+
48
+ logger = getLogger(__name__)
49
+
50
+
51
+ @contextmanager
52
+ def optional_temp_path(path: Optional[str] = None) -> Iterator[Path]:
53
+ if path is None:
54
+ with tempfile.TemporaryDirectory() as temp_dir:
55
+ yield Path(temp_dir)
56
+ else:
57
+ yield Path(path)
58
+
59
+
60
+ @contextmanager
61
+ def add_to_builtins(new_globals: dict[str, Any]) -> Iterator[None]:
62
+ old_globals = {key: getattr(builtins, key, None) for key in new_globals}
63
+
64
+ try:
65
+ for key, value in new_globals.items():
66
+ setattr(builtins, key, value) # Inject new global
67
+ yield
68
+ finally:
69
+ for key, value in old_globals.items():
70
+ if value is None:
71
+ delattr(builtins, key) # Remove added globals
72
+ else:
73
+ setattr(builtins, key, value) # Restore original value
74
+
75
+
76
+ class MCPProxy:
77
+ def __init__(self, servers: list[dict[str, Any]], title: Optional[str] = None, **kwargs: Any) -> None:
78
+ """Proxy class to generate client from OpenAPI schema."""
79
+ self._servers = servers
80
+ self._title = title or "MCP Proxy"
81
+ self._kwargs = kwargs
82
+ self._registered_funcs: list[Callable[..., Any]] = []
83
+ self._globals: dict[str, Any] = {}
84
+
85
+ self._security: dict[str, list[BaseSecurity]] = {}
86
+ self._security_params: dict[Optional[str], BaseSecurityParameters] = {}
87
+ self._tags: set[str] = set()
88
+
89
+ self._function_group: dict[str, list[str]] = {}
90
+
91
+ @staticmethod
92
+ def _convert_camel_case_within_braces_to_snake(text: str) -> str:
93
+ # Function to convert camel case to snake case
94
+ def camel_to_snake(match: re.Match[str]) -> str:
95
+ return re.sub(r"(?<!^)(?=[A-Z])", "_", match.group(1)).lower()
96
+
97
+ # Find all occurrences inside curly braces and apply camel_to_snake
98
+ result = re.sub(r"\{([a-zA-Z0-9]+)\}", lambda m: "{" + camel_to_snake(m) + "}", text)
99
+
100
+ return result
101
+
102
+ @staticmethod
103
+ def _get_params(path: str, func: Callable[..., Any]) -> tuple[set[str], set[str], Optional[str], bool]:
104
+ sig = inspect.signature(func)
105
+
106
+ params_names = set(sig.parameters.keys())
107
+
108
+ path_params = set(re.findall(r"\{(.+?)\}", path))
109
+ if not path_params.issubset(params_names):
110
+ raise ValueError(f"Path params {path_params} not in {params_names}")
111
+
112
+ body = "body" if "body" in params_names else None
113
+
114
+ security = "security" in params_names
115
+
116
+ q_params = set(params_names) - path_params - {body} - {"security"}
117
+
118
+ return q_params, path_params, body, security
119
+
120
+ @property
121
+ def mcp(self) -> "FastMCP":
122
+ mcp = FastMCP(title=self._title)
123
+
124
+ for func in self._registered_funcs:
125
+ try:
126
+ mcp.tool()(func) # type: ignore [no-untyped-call]
127
+ except PydanticInvalidForJsonSchema as e:
128
+ logger.warning("Could not register function %s: %s", func.__name__, e)
129
+
130
+ return mcp
131
+
132
+ def _process_params(
133
+ self, path: str, func: Callable[[Any], Any], **kwargs: Any
134
+ ) -> tuple[str, dict[str, Any], dict[str, Any]]:
135
+ path = MCPProxy._convert_camel_case_within_braces_to_snake(path)
136
+ q_params, path_params, body, security = MCPProxy._get_params(path, func)
137
+
138
+ expanded_path = path.format(**{p: kwargs[p] for p in path_params})
139
+
140
+ url = self._servers[0]["url"] + expanded_path
141
+
142
+ body_dict = {}
143
+ if body and body in kwargs:
144
+ body_value = kwargs[body]
145
+ if isinstance(body_value, dict):
146
+ body_dict = {"json": body_value}
147
+ elif hasattr(body_value, "model_dump"):
148
+ body_dict = {"json": body_value.model_dump()}
149
+ else:
150
+ body_dict = {"json": body_value.dict()}
151
+
152
+ body_dict["headers"] = {"Content-Type": "application/json"}
153
+ if security:
154
+ q_params, body_dict = kwargs["security"].add_security(q_params, body_dict)
155
+ # body_dict["headers"][security] = kwargs["security"]
156
+
157
+ params = {k: v for k, v in kwargs.items() if k in q_params}
158
+
159
+ return url, params, body_dict
160
+
161
+ def set_security_params(self, security_params: BaseSecurityParameters, name: Optional[str] = None) -> None:
162
+ if name is not None:
163
+ security = self._security.get(name)
164
+ if security is None:
165
+ raise ValueError(f"Security is not set for '{name}'")
166
+
167
+ for match_security in security:
168
+ if match_security.accept(security_params):
169
+ break
170
+ else:
171
+ raise ValueError(f"Security parameters {security_params} do not match security {security}")
172
+
173
+ self._security_params[name] = security_params
174
+
175
+ def _get_matching_security(
176
+ self, security: list[BaseSecurity], security_params: BaseSecurityParameters
177
+ ) -> BaseSecurity:
178
+ # check if security matches security parameters
179
+ for match_security in security:
180
+ if match_security.accept(security_params):
181
+ return match_security
182
+ raise ValueError(f"Security parameters {security_params} does not match any given security {security}")
183
+
184
+ def _get_security_params(self, name: str) -> tuple[Optional[BaseSecurityParameters], Optional[BaseSecurity]]:
185
+ # check if security is set for the method
186
+ security = self._security.get(name)
187
+ if not security:
188
+ return None, None
189
+
190
+ security_params = self._security_params.get(name)
191
+ if security_params is None:
192
+ # check if default security parameters are set
193
+ security_params = self._security_params.get(None)
194
+ if security_params is None:
195
+ raise ValueError(
196
+ f"Security parameters are not set for {name} and there are no default security parameters"
197
+ )
198
+
199
+ match_security = self._get_matching_security(security, security_params)
200
+
201
+ return security_params, match_security
202
+
203
+ def _request(
204
+ self,
205
+ method: Literal["put", "get", "post", "head", "delete", "patch"],
206
+ path: str,
207
+ description: Optional[str] = None,
208
+ security: Optional[list[BaseSecurity]] = None,
209
+ **kwargs: Any,
210
+ ) -> Callable[..., dict[str, Any]]:
211
+ def decorator(func: Callable[..., Any]) -> Callable[..., dict[str, Any]]:
212
+ name = func.__name__
213
+
214
+ for tag in kwargs.get("tags", []):
215
+ if tag not in self._function_group:
216
+ self._function_group[tag] = []
217
+ self._function_group[tag].append(name)
218
+
219
+ if security is not None:
220
+ self._security[name] = security
221
+
222
+ @wraps(func)
223
+ def wrapper(*args: Any, **kwargs: Any) -> dict[str, Any]:
224
+ url, params, body_dict = self._process_params(path, func, **kwargs)
225
+
226
+ security = self._security.get(name)
227
+ if security is not None:
228
+ security_params, matched_security = self._get_security_params(name)
229
+ if security_params is None:
230
+ raise ValueError(f"Security parameters are not set for '{name}'")
231
+ else:
232
+ security_params.apply(params, body_dict, matched_security) # type: ignore [arg-type]
233
+
234
+ response = getattr(requests, method)(url, params=params, **body_dict)
235
+ return response.json() # type: ignore [no-any-return]
236
+
237
+ wrapper._description = ( # type: ignore [attr-defined]
238
+ description or func.__doc__.strip() if func.__doc__ is not None else None
239
+ )
240
+
241
+ self._registered_funcs.append(wrapper)
242
+
243
+ return wrapper
244
+
245
+ return decorator # type: ignore [return-value]
246
+
247
+ def put(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]:
248
+ return self._request("put", path, **kwargs)
249
+
250
+ def get(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]:
251
+ return self._request("get", path, **kwargs)
252
+
253
+ def post(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]:
254
+ return self._request("post", path, **kwargs)
255
+
256
+ def delete(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]:
257
+ return self._request("delete", path, **kwargs)
258
+
259
+ def head(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]:
260
+ return self._request("head", path, **kwargs)
261
+
262
+ def patch(self, path: str, **kwargs: Any) -> Callable[..., dict[str, Any]]:
263
+ return self._request("patch", path, **kwargs)
264
+
265
+ @classmethod
266
+ def _get_template_dir(cls) -> Path:
267
+ path = Path(__file__).parents[3] / "templates"
268
+ if not path.exists():
269
+ raise RuntimeError(f"Template directory {path.resolve()} not found.")
270
+ return path
271
+
272
+ @classmethod
273
+ @require_optional_import(["datamodel_code_generator", "fastapi_code_generator"], "mcp-proxy-gen")
274
+ def generate_code(
275
+ cls,
276
+ input_text: str,
277
+ output_dir: Path,
278
+ disable_timestamp: bool = False,
279
+ custom_visitors: Optional[list[Path]] = None,
280
+ ) -> str:
281
+ if custom_visitors is None:
282
+ custom_visitors = []
283
+ custom_visitors.append(Path(__file__).parent / "security_schema_visitor.py")
284
+
285
+ # with patch_get_parameter_type():
286
+ generate_code(
287
+ input_name="openapi.yaml",
288
+ input_text=input_text,
289
+ encoding="utf-8",
290
+ output_dir=output_dir,
291
+ template_dir=cls._get_template_dir() / "client_template",
292
+ disable_timestamp=disable_timestamp,
293
+ custom_visitors=custom_visitors,
294
+ output_model_type=DataModelType.PydanticV2BaseModel,
295
+ )
296
+
297
+ main_path = output_dir / "main.py"
298
+
299
+ with main_path.open("r") as f:
300
+ main_py_code = f.read()
301
+ # main_py_code = main_py_code.replace("from .models import", "from models import")
302
+ main_py_code = main_py_code.replace("from .models", "from models")
303
+ # Removing "from __future__ import annotations" to avoid ForwardRef issues, should be fixed in fastapi_code_generator
304
+ main_py_code = main_py_code.replace("from __future__ import annotations", "")
305
+
306
+ with main_path.open("w") as f:
307
+ f.write(main_py_code)
308
+
309
+ return main_path.stem
310
+
311
+ def set_globals(self, main: ModuleType, suffix: str) -> None:
312
+ xs = {k: v for k, v in main.__dict__.items() if not k.startswith("__")}
313
+ self._globals = {
314
+ k: v for k, v in xs.items() if hasattr(v, "__module__") and v.__module__ in [f"models_{suffix}", "typing"]
315
+ }
316
+
317
+ @classmethod
318
+ @require_optional_import(["yaml"], "mcp-proxy-gen")
319
+ def create(
320
+ cls,
321
+ *,
322
+ openapi_specification: Optional[str] = None,
323
+ openapi_url: Optional[str] = None,
324
+ client_source_path: Optional[str] = None,
325
+ servers: Optional[list[dict[str, Any]]] = None,
326
+ rename_functions: bool = False,
327
+ group_functions: bool = False,
328
+ configuration_type: Literal["json", "yaml"] = "json",
329
+ ) -> "MCPProxy":
330
+ if (openapi_specification is None) == (openapi_url is None):
331
+ raise ValueError("Either openapi_specification or openapi_url should be provided")
332
+
333
+ if openapi_specification is None and openapi_url is not None:
334
+ with requests.get(openapi_url, timeout=10) as response:
335
+ response.raise_for_status()
336
+ openapi_specification = response.text
337
+
338
+ openapi_parsed = (
339
+ json.loads(openapi_specification) if configuration_type == "json" else yaml.safe_load(openapi_specification)
340
+ ) # type: ignore [arg-type]
341
+
342
+ if servers:
343
+ openapi_parsed["servers"] = servers
344
+
345
+ yaml_friendly = yaml.safe_dump(openapi_parsed)
346
+
347
+ with optional_temp_path(client_source_path) as td:
348
+ suffix = td.name # noqa F841
349
+
350
+ custom_visitors = []
351
+
352
+ if rename_functions:
353
+ custom_visitors.append(Path(__file__).parent / "operation_renaming.py")
354
+
355
+ if group_functions:
356
+ custom_visitors.append(Path(__file__).parent / "operation_grouping.py")
357
+
358
+ main_name = cls.generate_code( # noqa F841
359
+ input_text=yaml_friendly, # type: ignore [arg-type]
360
+ output_dir=td,
361
+ custom_visitors=custom_visitors,
362
+ )
363
+ # add td to sys.path
364
+ try:
365
+ sys.path.append(str(td))
366
+ main = importlib.import_module(main_name, package=td.name) # nosemgrep
367
+ finally:
368
+ sys.path.remove(str(td))
369
+
370
+ client: MCPProxy = main.app # type: ignore [attr-defined]
371
+ client.set_globals(main, suffix=suffix)
372
+
373
+ client.dump_configurations(output_dir=td)
374
+
375
+ return client
376
+
377
+ def _get_authentications(self) -> list[dict[str, Any]]:
378
+ seen = set()
379
+ authentications = []
380
+
381
+ for security_list in self._security.values():
382
+ for security in security_list:
383
+ params = security.Parameters().dump()
384
+
385
+ if params.get("type") == "unsupported":
386
+ continue
387
+
388
+ dumped = json.dumps(params) # hashable
389
+ if dumped not in seen:
390
+ seen.add(dumped)
391
+ authentications.append(security.Parameters().dump())
392
+ return authentications
393
+
394
+ def dump_configurations(self, output_dir: Path) -> None:
395
+ for tag in self._function_group:
396
+ output_file = output_dir / "mcp_config_{}.json".format(tag)
397
+
398
+ functions = [
399
+ registered_function
400
+ for registered_function in self._registered_funcs
401
+ if registered_function.__name__ in self._function_group[tag]
402
+ ]
403
+
404
+ self.dump_configuration(output_file, functions)
405
+
406
+ self.dump_configuration(output_dir / "mcp_config.json", self._registered_funcs)
407
+
408
+ def dump_configuration(self, output_file: Path, functions: list[Callable[..., Any]] = None) -> None:
409
+ # Define paths
410
+ template_dir = MCPProxy._get_template_dir() / "config_template"
411
+ template_file = "config.jinja2"
412
+
413
+ # Load Jinja environment
414
+ env = Environment(loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True)
415
+
416
+ # Load the template
417
+ template = env.get_template(template_file)
418
+ # Prepare context for rendering
419
+ context = {
420
+ "server_url": self._servers[0]["url"], # single or list depending on your structure
421
+ "authentications": self._get_authentications(), # list of auth blocks, we will also need to check _security_params
422
+ "operations": [
423
+ {
424
+ "name": op.__name__,
425
+ "description": op._description.replace("\n", " ").replace("\r", "").strip()
426
+ if op._description is not None
427
+ else "",
428
+ }
429
+ for op in functions
430
+ ],
431
+ }
432
+
433
+ # Render the template
434
+ rendered_config = template.render(context)
435
+
436
+ # Save the output to a file
437
+ with open(output_file, "w") as f:
438
+ f.write(rendered_config)
439
+
440
+ def load_configuration(self, config_file: str) -> None:
441
+ with Path(config_file).open("r") as f:
442
+ config_data_str = f.read()
443
+
444
+ self.load_configuration_from_string(config_data_str)
445
+
446
+ def load_configuration_from_string(self, config_data_str: str) -> None:
447
+ config_data = json.loads(config_data_str)
448
+ # Load server URL
449
+ self._servers = [{"url": config_data["server"]["url"]}]
450
+
451
+ # Load authentication
452
+ for auth in config_data.get("authentication", []):
453
+ security = BaseSecurity.parse_security_parameters(auth)
454
+ self.set_security_params(security)
455
+
456
+ operation_names = [op["name"] for op in config_data.get("operations", [])]
457
+
458
+ self._registered_funcs = [func for func in self._registered_funcs if func.__name__ in operation_names]
459
+
460
+ def _get_functions_to_register(
461
+ self,
462
+ functions: Optional[Iterable[Union[str, Mapping[str, Mapping[str, str]]]]] = None,
463
+ ) -> dict[Callable[..., Any], dict[str, Union[str, None]]]:
464
+ if functions is None:
465
+ return {
466
+ f: {
467
+ "name": None,
468
+ "description": f._description if hasattr(f, "_description") else None,
469
+ }
470
+ for f in self._registered_funcs
471
+ }
472
+
473
+ functions_with_name_desc: dict[str, dict[str, Union[str, None]]] = {}
474
+
475
+ for f in functions:
476
+ if isinstance(f, str):
477
+ functions_with_name_desc[f] = {"name": None, "description": None}
478
+ elif isinstance(f, dict):
479
+ functions_with_name_desc.update({
480
+ k: {
481
+ "name": v.get("name", None),
482
+ "description": v.get("description", None),
483
+ }
484
+ for k, v in f.items()
485
+ })
486
+ else:
487
+ raise ValueError(f"Invalid type {type(f)} for function {f}")
488
+
489
+ funcs_to_register: dict[Callable[..., Any], dict[str, Union[str, None]]] = {
490
+ f: functions_with_name_desc[f.__name__]
491
+ for f in self._registered_funcs
492
+ if f.__name__ in functions_with_name_desc
493
+ }
494
+ missing_functions = set(functions_with_name_desc.keys()) - {f.__name__ for f in funcs_to_register}
495
+ if missing_functions:
496
+ raise ValueError(f"Following functions {missing_functions} are not valid functions")
497
+
498
+ return funcs_to_register
499
+
500
+ @staticmethod
501
+ def _remove_pydantic_undefined_from_tools(
502
+ tools: list[dict[str, Any]],
503
+ ) -> list[dict[str, Any]]:
504
+ for tool in tools:
505
+ if "function" not in tool:
506
+ continue
507
+
508
+ function = tool["function"]
509
+ if "parameters" not in function or "properties" not in function["parameters"]:
510
+ continue
511
+
512
+ required = function["parameters"].get("required", [])
513
+ for param_name, param_value in function["parameters"]["properties"].items():
514
+ if "default" not in param_value:
515
+ continue
516
+
517
+ default = param_value.get("default")
518
+ if (
519
+ isinstance(default, (fastapi.params.Path, fastapi.params.Query))
520
+ and param_value["default"].default is PydanticUndefined
521
+ ):
522
+ param_value.pop("default")
523
+ # We removed the default value, so we need to add the parameter to the required list
524
+ if param_name not in required:
525
+ required.append(param_name)
526
+
527
+ return tools
528
+
529
+ def _register_for_llm(
530
+ self,
531
+ agent: "ConversableAgent",
532
+ functions: Optional[Iterable[Union[str, Mapping[str, Mapping[str, str]]]]] = None,
533
+ ) -> None:
534
+ funcs_to_register = self._get_functions_to_register(functions)
535
+
536
+ with add_to_builtins(
537
+ new_globals=self._globals,
538
+ ):
539
+ for f, v in funcs_to_register.items():
540
+ agent.register_for_llm(name=v["name"], description=v["description"])(f)
541
+
542
+ agent.llm_config["tools"] = MCPProxy._remove_pydantic_undefined_from_tools(agent.llm_config["tools"])
543
+
544
+ def _register_for_execution(
545
+ self,
546
+ agent: "ConversableAgent",
547
+ functions: Optional[Iterable[Union[str, Mapping[str, Mapping[str, str]]]]] = None,
548
+ ) -> None:
549
+ funcs_to_register = self._get_functions_to_register(functions)
550
+
551
+ for f, v in funcs_to_register.items():
552
+ agent.register_for_execution(name=v["name"])(f)
553
+
554
+ def get_functions(self) -> list[str]:
555
+ raise DeprecationWarning("Use function_names property instead of get_functions method")
556
+
557
+ @property
558
+ def function_names(self) -> list[str]:
559
+ return [f.__name__ for f in self._registered_funcs]
560
+
561
+ def get_function(self, name: str) -> Callable[..., dict[str, Any]]:
562
+ for f in self._registered_funcs:
563
+ if f.__name__ == name:
564
+ return f
565
+ raise ValueError(f"Function {name} not found")
566
+
567
+ def set_function(self, name: str, func: Callable[..., dict[str, Any]]) -> None:
568
+ for i, f in enumerate(self._registered_funcs):
569
+ if f.__name__ == name:
570
+ self._registered_funcs[i] = func
571
+ return
572
+
573
+ raise ValueError(f"Function {name} not found")
574
+
575
+ def inject_parameters(self, name: str, **kwargs: Any) -> None:
576
+ raise NotImplementedError("Injecting parameters is not implemented yet")
577
+ # for f in self._registered_funcs:
578
+ # if f.__name__ == name:
579
+ # return
580
+
581
+ # raise ValueError(f"Function {name} not found")