Clean up docstrings after DirectFunction merge (#2105)

* Add missing import for FunctionCallParams

* Update docstrings in direct_function

* Docstring fixes for run.py

* Remove unused imports in llm_service

* Add missing docstrings to llm_service

* Remove FunctionCallParams import

* Wording improvements

* Type checking for FunctionCallParams
This commit is contained in:
Mark Backman
2025-07-01 15:22:30 -04:00
committed by GitHub
parent 7f76a14c54
commit cc637f4dea
3 changed files with 139 additions and 35 deletions

View File

@@ -1,6 +1,22 @@
#
# Copyright (c) 20242025, Daily
#
# SPDX-License-Identifier: BSD 2-Clause License
#
"""Direct function wrapper utilities for LLM function calling.
This module provides utilities for wrapping "direct" functions that handle LLM
function calls. Direct functions have their metadata automatically extracted
from function signatures and docstrings, allowing them to be used without
accompanying configurations (as FunctionSchemas or in provider-specific
formats).
"""
import inspect import inspect
import types import types
from typing import ( from typing import (
TYPE_CHECKING,
Any, Any,
Callable, Callable,
Dict, Dict,
@@ -19,6 +35,9 @@ import docstring_parser
from pipecat.adapters.schemas.function_schema import FunctionSchema from pipecat.adapters.schemas.function_schema import FunctionSchema
if TYPE_CHECKING:
from pipecat.services.llm_service import FunctionCallParams
class DirectFunction(Protocol): class DirectFunction(Protocol):
"""Protocol for a "direct" function that handles LLM function calls. """Protocol for a "direct" function that handles LLM function calls.
@@ -28,30 +47,58 @@ class DirectFunction(Protocol):
`FunctionSchema`s or in provider-specific formats). `FunctionSchema`s or in provider-specific formats).
""" """
async def __call__(self, params: "FunctionCallParams", **kwargs: Any) -> None: ... async def __call__(self, params: "FunctionCallParams", **kwargs: Any) -> None:
"""Execute the direct function.
Args:
params: Function call parameters from the LLM service.
**kwargs: Additional keyword arguments passed to the function.
"""
...
class BaseDirectFunctionWrapper: class BaseDirectFunctionWrapper:
""" """Base class for a wrapper around a DirectFunction.
Base class for a wrapper around a DirectFunction that:
- extracts metadata from the function signature and docstring
- using that metadata, generates a corresponding FunctionSchema
"""
@classmethod Provides functionality to:
def special_first_param_name(cls) -> str:
"""The name of the "special" first function parameter that is ignored by the metadata - extract metadata from the function signature and docstring
extraction, as it's not relevant to the LLM. - use that metadata to generate a corresponding FunctionSchema
""" """
raise NotImplementedError("Subclasses must define the special first parameter name.")
def __init__(self, function: Callable): def __init__(self, function: Callable):
"""Initialize the direct function wrapper.
Args:
function: The function to wrap and extract metadata from.
"""
self.__class__.validate_function(function) self.__class__.validate_function(function)
self.function = function self.function = function
self._initialize_metadata() self._initialize_metadata()
@classmethod
def special_first_param_name(cls) -> str:
"""Get the name of the special first function parameter.
The special first parameter is ignored by metadata extraction as it's
not relevant to the LLM (e.g., 'params' for FunctionCallParams).
Returns:
The name of the special first parameter.
"""
raise NotImplementedError("Subclasses must define the special first parameter name.")
@classmethod @classmethod
def validate_function(cls, function: Callable) -> None: def validate_function(cls, function: Callable) -> None:
"""Validate that the function meets direct function requirements.
Args:
function: The function to validate.
Raises:
Exception: If function doesn't meet requirements (not async, missing
parameters, incorrect first parameter name).
"""
if not inspect.iscoroutinefunction(function): if not inspect.iscoroutinefunction(function):
raise Exception(f"Direct function {function.__name__} must be async") raise Exception(f"Direct function {function.__name__} must be async")
params = list(inspect.signature(function).parameters.items()) params = list(inspect.signature(function).parameters.items())
@@ -67,6 +114,11 @@ class BaseDirectFunctionWrapper:
) )
def to_function_schema(self) -> FunctionSchema: def to_function_schema(self) -> FunctionSchema:
"""Convert the wrapped function to a FunctionSchema.
Returns:
A FunctionSchema instance with extracted metadata.
"""
return FunctionSchema( return FunctionSchema(
name=self.name, name=self.name,
description=self.description, description=self.description,
@@ -75,6 +127,7 @@ class BaseDirectFunctionWrapper:
) )
def _initialize_metadata(self): def _initialize_metadata(self):
"""Initialize metadata from function signature and docstring."""
# Get function name # Get function name
self.name = self.function.__name__ self.name = self.function.__name__
@@ -93,20 +146,20 @@ class BaseDirectFunctionWrapper:
def _get_parameters_as_jsonschema( def _get_parameters_as_jsonschema(
self, func: Callable, docstring_params: List[docstring_parser.DocstringParam] self, func: Callable, docstring_params: List[docstring_parser.DocstringParam]
) -> Tuple[Dict[str, Any], List[str]]: ) -> Tuple[Dict[str, Any], List[str]]:
""" """Get function parameters as a dictionary of JSON schemas and a list of required parameters.
Get function parameters as a dictionary of JSON schemas and a list of required parameters.
Ignore the first parameter, as it's expected to be the "special" one. Ignore the first parameter, as it's expected to be the "special" one.
Args: Args:
func: Function to get parameters from func: Function to get parameters from.
docstring_params: List of parameters extracted from the function's docstring docstring_params: List of parameters extracted from the function's docstring.
Returns: Returns:
A tuple containing: A tuple containing:
- A dictionary mapping each function parameter to its JSON schema
- A list of required parameter names
"""
- A dictionary mapping each function parameter to its JSON schema
- A list of required parameter names
"""
sig = inspect.signature(func) sig = inspect.signature(func)
hints = get_type_hints(func) hints = get_type_hints(func)
properties = {} properties = {}
@@ -141,8 +194,7 @@ class BaseDirectFunctionWrapper:
return properties, required return properties, required
def _typehint_to_jsonschema(self, type_hint: Any) -> Dict[str, Any]: def _typehint_to_jsonschema(self, type_hint: Any) -> Dict[str, Any]:
""" """Convert a Python type hint to a JSON Schema.
Convert a Python type hint to a JSON Schema.
Args: Args:
type_hint: A Python type hint type_hint: A Python type hint
@@ -213,16 +265,32 @@ class BaseDirectFunctionWrapper:
class DirectFunctionWrapper(BaseDirectFunctionWrapper): class DirectFunctionWrapper(BaseDirectFunctionWrapper):
""" """Wrapper around a DirectFunction for LLM function calling.
Wrapper around a DirectFunction that:
- extracts metadata from the function signature and docstring This class:
- generates a corresponding FunctionSchema
- helps with function invocation - Extracts metadata from the function signature and docstring
- Generates a corresponding FunctionSchema
- Helps with function invocation
""" """
@classmethod @classmethod
def special_first_param_name(cls) -> str: def special_first_param_name(cls) -> str:
"""Get the special first parameter name for direct functions.
Returns:
The string "params" which is expected as the first parameter.
"""
return "params" return "params"
async def invoke(self, args: Mapping[str, Any], params: "FunctionCallParams"): async def invoke(self, args: Mapping[str, Any], params: "FunctionCallParams"):
"""Invoke the wrapped function with the provided arguments.
Args:
args: Arguments to pass to the function.
params: Function call parameters from the LLM service.
Returns:
The result of the function call.
"""
return await self.function(params=params, **args) return await self.function(params=params, **args)

View File

@@ -93,6 +93,15 @@ async def maybe_capture_participant_screen(
def smallwebrtc_sdp_cleanup_ice_candidates(text: str, pattern: str) -> str: def smallwebrtc_sdp_cleanup_ice_candidates(text: str, pattern: str) -> str:
"""Clean up ICE candidates in SDP text for SmallWebRTC.
Args:
text: SDP text to clean up.
pattern: Pattern to match for candidate filtering.
Returns:
Cleaned SDP text with filtered ICE candidates.
"""
result = [] result = []
lines = text.splitlines() lines = text.splitlines()
for line in lines: for line in lines:
@@ -105,6 +114,14 @@ def smallwebrtc_sdp_cleanup_ice_candidates(text: str, pattern: str) -> str:
def smallwebrtc_sdp_cleanup_fingerprints(text: str) -> str: def smallwebrtc_sdp_cleanup_fingerprints(text: str) -> str:
"""Remove unsupported fingerprint algorithms from SDP text.
Args:
text: SDP text to clean up.
Returns:
SDP text with sha-384 and sha-512 fingerprints removed.
"""
result = [] result = []
lines = text.splitlines() lines = text.splitlines()
for line in lines: for line in lines:
@@ -114,6 +131,15 @@ def smallwebrtc_sdp_cleanup_fingerprints(text: str) -> str:
def smallwebrtc_sdp_munging(sdp: str, host: str) -> str: def smallwebrtc_sdp_munging(sdp: str, host: str) -> str:
"""Apply SDP modifications for SmallWebRTC compatibility.
Args:
sdp: Original SDP string.
host: Host address for ICE candidate filtering.
Returns:
Modified SDP string with fingerprint and ICE candidate cleanup.
"""
sdp = smallwebrtc_sdp_cleanup_fingerprints(sdp) sdp = smallwebrtc_sdp_cleanup_fingerprints(sdp)
sdp = smallwebrtc_sdp_cleanup_ice_candidates(sdp, host) sdp = smallwebrtc_sdp_cleanup_ice_candidates(sdp, host)
return sdp return sdp
@@ -232,6 +258,9 @@ def run_example_webrtc(
Args: Args:
app: The FastAPI application instance. app: The FastAPI application instance.
Yields:
Control to the FastAPI application runtime.
""" """
yield # Run app yield # Run app
coros = [pc.disconnect() for pc in pcs_map.values()] coros = [pc.disconnect() for pc in pcs_map.values()]

View File

@@ -8,28 +8,19 @@
import asyncio import asyncio
import inspect import inspect
import types
from dataclasses import dataclass from dataclasses import dataclass
from typing import ( from typing import (
Any, Any,
Awaitable, Awaitable,
Callable, Callable,
Dict, Dict,
List,
Mapping, Mapping,
Optional, Optional,
Protocol, Protocol,
Sequence, Sequence,
Set,
Tuple,
Type, Type,
Union,
get_args,
get_origin,
get_type_hints,
) )
import docstring_parser
from loguru import logger from loguru import logger
from pipecat.adapters.base_llm_adapter import BaseLLMAdapter from pipecat.adapters.base_llm_adapter import BaseLLMAdapter
@@ -312,6 +303,17 @@ class LLMService(AIService):
*, *,
cancel_on_interruption: bool = True, cancel_on_interruption: bool = True,
): ):
"""Register a direct function handler for LLM function calls.
Direct functions have their metadata automatically extracted from their
signature and docstring, eliminating the need for accompanying
configurations (as FunctionSchemas or in provider-specific formats).
Args:
handler: The direct function to register. Must follow DirectFunction protocol.
cancel_on_interruption: Whether to cancel this function call when an
interruption occurs. Defaults to True.
"""
wrapper = DirectFunctionWrapper(handler) wrapper = DirectFunctionWrapper(handler)
self._functions[wrapper.name] = FunctionCallRegistryItem( self._functions[wrapper.name] = FunctionCallRegistryItem(
function_name=wrapper.name, function_name=wrapper.name,
@@ -330,6 +332,11 @@ class LLMService(AIService):
del self._start_callbacks[function_name] del self._start_callbacks[function_name]
def unregister_direct_function(self, handler: Any): def unregister_direct_function(self, handler: Any):
"""Remove a registered direct function handler.
Args:
handler: The direct function handler to remove.
"""
wrapper = DirectFunctionWrapper(handler) wrapper = DirectFunctionWrapper(handler)
del self._functions[wrapper.name] del self._functions[wrapper.name]
# Note: no need to remove start callback here, as direct functions don't support start callbacks. # Note: no need to remove start callback here, as direct functions don't support start callbacks.