mirror of
https://github.com/microsoft/autogen.git
synced 2026-01-27 15:07:57 -05:00
* adjust conversable and compressible agents to support tool_calls
* split out tools into their own reply def
* copilot typo
* address review comments
* revert compressible_agent and token_count_utils calls
* cleanup terminate check and remove unnecessary code
* doc search and update
* return function/tool calls as interrupted when user provides a reply to a tool call request
* fix tool name reference
* fix formatting
* fix initiate receiving a dict
* missed changed roled
* ignore incoming role, more similiar to existing code
* consistency
* redundant to_dict
* fix todo comment
* uneeded change
* handle dict reply in groupchat
* Fix generate_tool_call_calls_reply_comment
* change method annotation for register_for_llm from functions to tools
* typo autogen/agentchat/conversable_agent.py
Co-authored-by: Chi Wang <wang.chi@microsoft.com>
* add deprecation comments for function_call
* tweak doc strings
* switch to ToolFunction type
* update the return to
* fix generate_init_message return type
* Revert "fix generate_init_message return type"
This reverts commit 645ba8b76a.
* undo force init to dict
* fix notebooks and groupchat tool handling
* fix type
* use get for key error
* fix teachable to pull content from dict
* change single message tool response
* cleanup unnessary changes
* little better tool response concatenation
* update tools tests
* add skip openai check to tools tests
* fix nits
* move func name normalization to oai_reply and assert configured names
* fix whitespace
* remove extra normalize
* tool name is now normalized in the generate_reply function, so will not be incorrect when sent to receive
* validate function names in init and expand comments for validation methods
* fix dict comprehension
* Dummy llm config for unit tests
* handle tool_calls set to None
* fix tool name reference
* method operates on responses not calls
---------
Co-authored-by: Yiran Wu <32823396+kevin666aa@users.noreply.github.com>
Co-authored-by: Chi Wang <wang.chi@microsoft.com>
Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
340 lines
11 KiB
Python
340 lines
11 KiB
Python
import functools
|
|
import inspect
|
|
import json
|
|
from logging import getLogger
|
|
from typing import Any, Callable, Dict, ForwardRef, List, Optional, Set, Tuple, Type, TypeVar, Union
|
|
|
|
from pydantic import BaseModel, Field
|
|
from typing_extensions import Annotated, Literal, get_args, get_origin
|
|
|
|
from ._pydantic import JsonSchemaValue, evaluate_forwardref, model_dump, model_dump_json, type2schema
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def get_typed_annotation(annotation: Any, globalns: Dict[str, Any]) -> Any:
|
|
"""Get the type annotation of a parameter.
|
|
|
|
Args:
|
|
annotation: The annotation of the parameter
|
|
globalns: The global namespace of the function
|
|
|
|
Returns:
|
|
The type annotation of the parameter
|
|
"""
|
|
if isinstance(annotation, str):
|
|
annotation = ForwardRef(annotation)
|
|
annotation = evaluate_forwardref(annotation, globalns, globalns)
|
|
return annotation
|
|
|
|
|
|
def get_typed_signature(call: Callable[..., Any]) -> inspect.Signature:
|
|
"""Get the signature of a function with type annotations.
|
|
|
|
Args:
|
|
call: The function to get the signature for
|
|
|
|
Returns:
|
|
The signature of the function with type annotations
|
|
"""
|
|
signature = inspect.signature(call)
|
|
globalns = getattr(call, "__globals__", {})
|
|
typed_params = [
|
|
inspect.Parameter(
|
|
name=param.name,
|
|
kind=param.kind,
|
|
default=param.default,
|
|
annotation=get_typed_annotation(param.annotation, globalns),
|
|
)
|
|
for param in signature.parameters.values()
|
|
]
|
|
typed_signature = inspect.Signature(typed_params)
|
|
return typed_signature
|
|
|
|
|
|
def get_typed_return_annotation(call: Callable[..., Any]) -> Any:
|
|
"""Get the return annotation of a function.
|
|
|
|
Args:
|
|
call: The function to get the return annotation for
|
|
|
|
Returns:
|
|
The return annotation of the function
|
|
"""
|
|
signature = inspect.signature(call)
|
|
annotation = signature.return_annotation
|
|
|
|
if annotation is inspect.Signature.empty:
|
|
return None
|
|
|
|
globalns = getattr(call, "__globals__", {})
|
|
return get_typed_annotation(annotation, globalns)
|
|
|
|
|
|
def get_param_annotations(typed_signature: inspect.Signature) -> Dict[int, Union[Annotated[Type, str], Type]]:
|
|
"""Get the type annotations of the parameters of a function
|
|
|
|
Args:
|
|
typed_signature: The signature of the function with type annotations
|
|
|
|
Returns:
|
|
A dictionary of the type annotations of the parameters of the function
|
|
"""
|
|
return {
|
|
k: v.annotation for k, v in typed_signature.parameters.items() if v.annotation is not inspect.Signature.empty
|
|
}
|
|
|
|
|
|
class Parameters(BaseModel):
|
|
"""Parameters of a function as defined by the OpenAI API"""
|
|
|
|
type: Literal["object"] = "object"
|
|
properties: Dict[str, JsonSchemaValue]
|
|
required: List[str]
|
|
|
|
|
|
class Function(BaseModel):
|
|
"""A function as defined by the OpenAI API"""
|
|
|
|
description: Annotated[str, Field(description="Description of the function")]
|
|
name: Annotated[str, Field(description="Name of the function")]
|
|
parameters: Annotated[Parameters, Field(description="Parameters of the function")]
|
|
|
|
|
|
class ToolFunction(BaseModel):
|
|
"""A function under tool as defined by the OpenAI API."""
|
|
|
|
type: Literal["function"] = "function"
|
|
function: Annotated[Function, Field(description="Function under tool")]
|
|
|
|
|
|
def get_parameter_json_schema(
|
|
k: str, v: Union[Annotated[Type, str], Type], default_values: Dict[str, Any]
|
|
) -> JsonSchemaValue:
|
|
"""Get a JSON schema for a parameter as defined by the OpenAI API
|
|
|
|
Args:
|
|
k: The name of the parameter
|
|
v: The type of the parameter
|
|
default_values: The default values of the parameters of the function
|
|
|
|
Returns:
|
|
A Pydanitc model for the parameter
|
|
"""
|
|
|
|
def type2description(k: str, v: Union[Annotated[Type, str], Type]) -> str:
|
|
# handles Annotated
|
|
if hasattr(v, "__metadata__"):
|
|
return v.__metadata__[0]
|
|
else:
|
|
return k
|
|
|
|
schema = type2schema(v)
|
|
if k in default_values:
|
|
dv = default_values[k]
|
|
schema["default"] = dv
|
|
|
|
schema["description"] = type2description(k, v)
|
|
|
|
return schema
|
|
|
|
|
|
def get_required_params(typed_signature: inspect.Signature) -> List[str]:
|
|
"""Get the required parameters of a function
|
|
|
|
Args:
|
|
signature: The signature of the function as returned by inspect.signature
|
|
|
|
Returns:
|
|
A list of the required parameters of the function
|
|
"""
|
|
return [k for k, v in typed_signature.parameters.items() if v.default == inspect.Signature.empty]
|
|
|
|
|
|
def get_default_values(typed_signature: inspect.Signature) -> Dict[str, Any]:
|
|
"""Get default values of parameters of a function
|
|
|
|
Args:
|
|
signature: The signature of the function as returned by inspect.signature
|
|
|
|
Returns:
|
|
A dictionary of the default values of the parameters of the function
|
|
"""
|
|
return {k: v.default for k, v in typed_signature.parameters.items() if v.default != inspect.Signature.empty}
|
|
|
|
|
|
def get_parameters(
|
|
required: List[str], param_annotations: Dict[str, Union[Annotated[Type, str], Type]], default_values: Dict[str, Any]
|
|
) -> Parameters:
|
|
"""Get the parameters of a function as defined by the OpenAI API
|
|
|
|
Args:
|
|
required: The required parameters of the function
|
|
hints: The type hints of the function as returned by typing.get_type_hints
|
|
|
|
Returns:
|
|
A Pydantic model for the parameters of the function
|
|
"""
|
|
return Parameters(
|
|
properties={
|
|
k: get_parameter_json_schema(k, v, default_values)
|
|
for k, v in param_annotations.items()
|
|
if v is not inspect.Signature.empty
|
|
},
|
|
required=required,
|
|
)
|
|
|
|
|
|
def get_missing_annotations(typed_signature: inspect.Signature, required: List[str]) -> Tuple[Set[str], Set[str]]:
|
|
"""Get the missing annotations of a function
|
|
|
|
Ignores the parameters with default values as they are not required to be annotated, but logs a warning.
|
|
Args:
|
|
typed_signature: The signature of the function with type annotations
|
|
required: The required parameters of the function
|
|
|
|
Returns:
|
|
A set of the missing annotations of the function
|
|
"""
|
|
all_missing = {k for k, v in typed_signature.parameters.items() if v.annotation is inspect.Signature.empty}
|
|
missing = all_missing.intersection(set(required))
|
|
unannotated_with_default = all_missing.difference(missing)
|
|
return missing, unannotated_with_default
|
|
|
|
|
|
def get_function_schema(f: Callable[..., Any], *, name: Optional[str] = None, description: str) -> Dict[str, Any]:
|
|
"""Get a JSON schema for a function as defined by the OpenAI API
|
|
|
|
Args:
|
|
f: The function to get the JSON schema for
|
|
name: The name of the function
|
|
description: The description of the function
|
|
|
|
Returns:
|
|
A JSON schema for the function
|
|
|
|
Raises:
|
|
TypeError: If the function is not annotated
|
|
|
|
Examples:
|
|
```
|
|
def f(a: Annotated[str, "Parameter a"], b: int = 2, c: Annotated[float, "Parameter c"] = 0.1) -> None:
|
|
pass
|
|
|
|
get_function_schema(f, description="function f")
|
|
|
|
# {'type': 'function',
|
|
# 'function': {'description': 'function f',
|
|
# 'name': 'f',
|
|
# 'parameters': {'type': 'object',
|
|
# 'properties': {'a': {'type': 'str', 'description': 'Parameter a'},
|
|
# 'b': {'type': 'int', 'description': 'b'},
|
|
# 'c': {'type': 'float', 'description': 'Parameter c'}},
|
|
# 'required': ['a']}}}
|
|
```
|
|
|
|
"""
|
|
typed_signature = get_typed_signature(f)
|
|
required = get_required_params(typed_signature)
|
|
default_values = get_default_values(typed_signature)
|
|
param_annotations = get_param_annotations(typed_signature)
|
|
return_annotation = get_typed_return_annotation(f)
|
|
missing, unannotated_with_default = get_missing_annotations(typed_signature, required)
|
|
|
|
if return_annotation is None:
|
|
logger.warning(
|
|
f"The return type of the function '{f.__name__}' is not annotated. Although annotating it is "
|
|
+ "optional, the function should return either a string, a subclass of 'pydantic.BaseModel'."
|
|
)
|
|
|
|
if unannotated_with_default != set():
|
|
unannotated_with_default_s = [f"'{k}'" for k in sorted(unannotated_with_default)]
|
|
logger.warning(
|
|
f"The following parameters of the function '{f.__name__}' with default values are not annotated: "
|
|
+ f"{', '.join(unannotated_with_default_s)}."
|
|
)
|
|
|
|
if missing != set():
|
|
missing_s = [f"'{k}'" for k in sorted(missing)]
|
|
raise TypeError(
|
|
f"All parameters of the function '{f.__name__}' without default values must be annotated. "
|
|
+ f"The annotations are missing for the following parameters: {', '.join(missing_s)}"
|
|
)
|
|
|
|
fname = name if name else f.__name__
|
|
|
|
parameters = get_parameters(required, param_annotations, default_values=default_values)
|
|
|
|
function = ToolFunction(
|
|
function=Function(
|
|
description=description,
|
|
name=fname,
|
|
parameters=parameters,
|
|
)
|
|
)
|
|
|
|
return model_dump(function)
|
|
|
|
|
|
def get_load_param_if_needed_function(t: Any) -> Optional[Callable[[T, Type], BaseModel]]:
|
|
"""Get a function to load a parameter if it is a Pydantic model
|
|
|
|
Args:
|
|
t: The type annotation of the parameter
|
|
|
|
Returns:
|
|
A function to load the parameter if it is a Pydantic model, otherwise None
|
|
|
|
"""
|
|
if get_origin(t) is Annotated:
|
|
return get_load_param_if_needed_function(get_args(t)[0])
|
|
|
|
def load_base_model(v: Dict[str, Any], t: Type[BaseModel]) -> BaseModel:
|
|
return t(**v)
|
|
|
|
return load_base_model if isinstance(t, type) and issubclass(t, BaseModel) else None
|
|
|
|
|
|
def load_basemodels_if_needed(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
"""A decorator to load the parameters of a function if they are Pydantic models
|
|
|
|
Args:
|
|
func: The function with annotated parameters
|
|
|
|
Returns:
|
|
A function that loads the parameters before calling the original function
|
|
|
|
"""
|
|
# get the type annotations of the parameters
|
|
typed_signature = get_typed_signature(func)
|
|
param_annotations = get_param_annotations(typed_signature)
|
|
|
|
# get functions for loading BaseModels when needed based on the type annotations
|
|
kwargs_mapping = {k: get_load_param_if_needed_function(t) for k, t in param_annotations.items()}
|
|
|
|
# remove the None values
|
|
kwargs_mapping = {k: f for k, f in kwargs_mapping.items() if f is not None}
|
|
|
|
# a function that loads the parameters before calling the original function
|
|
@functools.wraps(func)
|
|
def load_parameters_if_needed(*args, **kwargs):
|
|
# load the BaseModels if needed
|
|
for k, f in kwargs_mapping.items():
|
|
kwargs[k] = f(kwargs[k], param_annotations[k])
|
|
|
|
# call the original function
|
|
return func(*args, **kwargs)
|
|
|
|
return load_parameters_if_needed
|
|
|
|
|
|
def serialize_to_str(x: Any) -> str:
|
|
if isinstance(x, str):
|
|
return x
|
|
elif isinstance(x, BaseModel):
|
|
return model_dump_json(x)
|
|
else:
|
|
return json.dumps(x)
|