148148- `pre_processor` is a processor called once on `input` and on all results
149149obtained by tool invocation. This could for instance be an image tokenizer to
150150preprocess images and avoid re-tokenizing them on every model invocation.
151- - `model` is a model processor that generates content. For the model to know
152- which tools are available, the samefunction/tool set should be given both to the
153- FunctionCalling and the model constructor. This model can be turn-based or
154- bidi but this should be specificed in the FunctionCalling ctor.
151+ - `model` is a model processor that generates content. Tool declarations are
152+ automatically registered on the model via ``add_tools()`` — you only need to
153+ pass functions to ``FunctionCalling(fns=...)``.
155154- `function_call` executes the function calls returned by the model. This is
156155a private processor in this file. The function response is then fed back to the
157156model for another iteration. If there is no function call to execute and the
162161They are all in the same substream as identified by `substream_name` in the
163162FunctionCalling ctor.
164163
165- When used with GenAI models (i.e. Gemini API), the model should be defined as
166- follows:
164+ When used with GenAI models (i.e. Gemini API), the setup is simply:
167165
168166```python
169167genai_processor = genai_model.GenaiModel(
170168 api_key=API_KEY,
171169 model_name='gemini-2.5-flash',
172- generate_content_config=genai_types.GenerateContentConfig(
173- tools=[fns],
174- automatic_function_calling=genai_types.AutomaticFunctionCallingConfig(
175- disable=True
176- ),
177- ),
178170)
171+
172+ agent = FunctionCalling(genai_processor, fns=[get_weather, set_alarm])
179173```
180174
181- where `fns` are the python functions to be called. Note that we disable the
182- automatic function calling feature here to avoid duplicate function calls with
183- the GenAI automatic function calling feature.
175+ Tool declarations and disabling of automatic function calling are handled
176+ automatically via the ``add_tools()`` delegation.
184177
185- If you want to allow cancelling ongoing async functions add `cancel_fc` and
186- `list_fc` tool defined in this file:
178+ If you want to allow cancelling ongoing async functions, add `` cancel_fc` ` and
179+ `` list_fc`` defined in this file:
187180
188181```python
189- generate_content_config=genai_types.GenerateContentConfig(
190- tools=[fns, function_calling.cancel_fc, function_calling.list_fc],
191- automatic_function_calling=genai_types.AutomaticFunctionCallingConfig(
192- disable=True
193- ),
182+ agent = FunctionCalling(
183+ genai_processor,
184+ fns=[get_weather, function_calling.cancel_fc, function_calling.list_fc],
194185)
195186```
196187
@@ -441,17 +432,15 @@ def __init__(
441432 processor.
442433 pre_processor: An optional pre-processor to pass the model input (prompt,
443434 function responses, model output from previous iterations) through.
444- fns: The functions to register for function calling. Those functions must
445- be known to `model`, and will be called only if `model` returns a
446- function call with the matching name. For Gemini API, this means the
447- same functions should be passed in the `GenerationConfig(tools=[...])`
448- to the `model` constructor. If the function name is not found in the
449- `fns` list, the function calling processor will return the unknown
450- function call part and will raise a `ValueError`. If the execution of
451- the function fails, the function calling processor will return a
452- function response with the error message. MCP tools are automatically
453- converted to callables when passed in as functions to this processor,
454- this happens the first time the model is called.
435+ fns: The functions to register for function calling. Tool declarations are
436+ automatically registered on the model via ``add_tools()`` — you do not
437+ need to pass them separately to the model's config. If the function name
438+ is not found in the `fns` list, the function calling processor will
439+ return the unknown function call part and will raise a `ValueError`. If
440+ the execution of the function fails, the function calling processor will
441+ return a function response with the error message. MCP tools are
442+ automatically converted to callables when passed in as functions to this
443+ processor, this happens the first time the model is called.
455444 max_function_calls: maximum number of function calls to make (default set
456445 to 5 of turn-based models and infinity for bidi aka realtime models).
457446 When this limit is reached, the function calling loop will wait for the
@@ -461,8 +450,12 @@ def __init__(
461450 self ._model = model
462451 self ._substream_name = substream_name
463452 self ._is_bidi_model = is_bidi_model
464- self ._fns = fns
453+ self ._fns = list (fns ) if fns else []
454+ if is_bidi_model :
455+ # Automatically add list_fc and cancel_fc for bidi models.
456+ self ._fns .extend ([cancel_fc , list_fc ])
465457 self ._fns_initialized = False
458+ self ._called = False
466459 self ._pre_processor = (
467460 pre_processor .to_processor ()
468461 if pre_processor
@@ -471,6 +464,51 @@ def __init__(
471464 default_max_function_calls = 5 if not is_bidi_model else math .inf
472465 self ._max_function_calls = max_function_calls or default_max_function_calls
473466
467+ def add_fns (self , fns : list [Callable [..., Any ]]) -> None :
468+ """Adds functions dynamically before call() is invoked.
469+
470+ This is useful for wrapper processors that need to inject additional
471+ tools without interfering with how the FunctionCalling processor was
472+ originally created. Functions added here are treated identically to
473+ those passed in the constructor.
474+
475+ Must be called before `call()` is invoked.
476+
477+ Args:
478+ fns: The functions to add.
479+
480+ Raises:
481+ RuntimeError: if called after `call()` has been invoked.
482+ """
483+ if self ._called :
484+ raise RuntimeError ('Cannot add functions after call() has been invoked.' )
485+ if self ._fns is None :
486+ self ._fns = list (fns )
487+ else :
488+ self ._fns = list (self ._fns ) + list (fns )
489+
490+ def register_tools (
491+ self ,
492+ tools : list [Callable [..., Any ] | genai_types .McpClientSession ],
493+ ) -> None :
494+ """Delegates tool registration to the inner model processor.
495+
496+ When ``FunctionCalling`` is used as the inner model for another
497+ ``FunctionCalling`` (nested pattern), the outer processor calls this
498+ method to propagate tool declarations down to the actual model.
499+
500+ Args:
501+ tools: Functions to register. Callables are converted to proto
502+ FunctionDeclarations. McpClientSessions are expected to have been
503+ converted to callables by FunctionCalling before reaching this method.
504+ """
505+ if hasattr (self ._model , 'register_tools' ):
506+ self ._model .register_tools (tools )
507+
508+ def children (self ) -> list [processor .Processor ]:
509+ """Returns the list of sub-processors (children) for this processor."""
510+ return [self ._model ]
511+
474512 async def _initialize_mcp_tools (self ):
475513 """Initializes the MCP tools."""
476514 if self ._fns_initialized :
@@ -486,10 +524,42 @@ async def _initialize_mcp_tools(self):
486524 self ._fns_initialized = True
487525 self ._fns = fns
488526
527+ def _find_last_tool_supporting_processor (
528+ self , proc : processor .Processor
529+ ) -> processor .Processor | None :
530+ """Recursively finds the last processor supporting register_tools."""
531+ if hasattr (proc , 'register_tools' ):
532+ return proc
533+
534+ for child in reversed (proc .children ()):
535+ found = self ._find_last_tool_supporting_processor (child )
536+ if found :
537+ return found
538+ return None
539+
540+ def _register_tools_on_model (self ) -> None :
541+ """Registers tool declarations on the inner model processor.
542+
543+ Uses a generic tree traversal (via ``children()``) to find the last
544+ processor in the hierarchy that supports ``register_tools()``.
545+ """
546+ if not self ._fns :
547+ return
548+
549+ target = self ._find_last_tool_supporting_processor (self ._model )
550+ if target :
551+ getattr (target , 'register_tools' )(self ._fns )
552+ else :
553+ raise ValueError (
554+ f'No processor in the chain supports tool registration: { self ._model } '
555+ )
556+
489557 async def call (
490558 self , content : processor .ProcessorStream
491559 ) -> AsyncIterable [content_api .ProcessorPartTypes ]:
560+ self ._called = True
492561 await self ._initialize_mcp_tools ()
562+ self ._register_tools_on_model ()
493563 state = _FunctionCallState (fn_call_count_limit = self ._max_function_calls )
494564 # To support both bidi and unary models we reduce both cases to bidi.
495565 model = _to_bidi (self ._model ) if not self ._is_bidi_model else self ._model
0 commit comments