Ctrl K
ComunidadGuías…

test/tools/test_precedence.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Copyright (c) 2026, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0

from collections.abc import Iterable, Sequence
from typing import Any, Self

import pytest
from fast_depends.library.serializer import SerializerProto

from ag2 import Agent, Context, Toolkit, tool
from ag2.config import LLMClient, ModelConfig, ModelProvider
from ag2.context import ConversationContext
from ag2.events import BaseEvent, ModelResponse, ToolCallEvent
from ag2.middleware import approval_required
from ag2.response import ResponseProto
from ag2.testing import TestConfig
from ag2.tools import AnthropicBashTool
from ag2.tools.schemas import ToolSchema


class SchemaRecordingConfig(ModelConfig):
    """``TestConfig`` that also keeps the tool schemas each LLM call was offered."""

    def __init__(self, *turns: Any) -> None:
        self.config = TestConfig(*turns)
        self.offered: list[list[ToolSchema]] = []

    @property
    def provider(self) -> ModelProvider:
        return ModelProvider.OPENAI

    @property
    def model(self) -> str:
        return "test-model"

    def copy(self) -> Self:
        return self

    def create(self) -> LLMClient:
        return SchemaRecordingClient(self.config, self.offered)

    def create_files_client(self) -> Any:
        raise NotImplementedError


class SchemaRecordingClient:
    def __init__(self, config: TestConfig, offered: list[list[ToolSchema]]) -> None:
        self.client = config.create()
        self.offered = offered

    async def __call__(
        self,
        messages: Sequence[BaseEvent],
        context: ConversationContext,
        *,
        tools: Iterable[ToolSchema],
        response_schema: ResponseProto | None,
        serializer: SerializerProto,
    ) -> ModelResponse:
        schemas = list(tools)
        self.offered.append(schemas)
        return await self.client(
            messages, context, tools=schemas, response_schema=response_schema, serializer=serializer
        )


@pytest.mark.asyncio
class TestToolsDeclaredInCode:
    async def test_later_tool_overrides_an_earlier_one_with_a_warning(self, caplog: pytest.LogCaptureFixture) -> None:
        runs: list[str] = []

        def deploy() -> str:
            runs.append("toolkit")
            return "deployed"

        @tool(name="deploy")
        def my_deploy() -> str:
            runs.append("mine")
            return "deployed"

        agent = Agent(
            "",
            tools=[Toolkit(deploy), my_deploy],
            config=TestConfig(ToolCallEvent(name="deploy", arguments="{}"), "done"),
        )

        with caplog.at_level("WARNING", logger="ag2.tools.precedence"):
            await agent.ask("Deploy")

        assert runs == ["mine"]
        [warning] = caplog.records
        assert "deploy" in warning.getMessage()

    async def test_the_same_tool_passed_twice_runs_once(self) -> None:
        runs: list[str] = []

        @tool
        def deploy() -> str:
            runs.append("deploy")
            return "deployed"

        agent = Agent(
            "",
            tools=[deploy, deploy],
            config=TestConfig(ToolCallEvent(name="deploy", arguments="{}"), "done"),
        )

        await agent.ask("Deploy")

        assert runs == ["deploy"]

    async def test_denial_is_not_bypassed_by_an_earlier_ungated_tool(self) -> None:
        runs: list[str] = []

        @tool(name="deploy")
        def ungated() -> str:
            runs.append("ungated")
            return "deployed"

        @tool(name="deploy", middleware=[approval_required()])
        def gated() -> str:
            runs.append("gated")
            return "deployed"

        agent = Agent(
            "",
            tools=[ungated, gated],
            config=TestConfig(ToolCallEvent(name="deploy", arguments="{}"), "done"),
        )

        await agent.ask("Deploy", hitl_hook=lambda _: "n")

        assert runs == []

    @pytest.mark.parametrize("builtin_first", [True, False], ids=["builtin-first", "function-first"])
    async def test_function_and_builtin_sharing_a_name_resolve_by_order(
        self,
        builtin_first: bool,
        context: Context,
    ) -> None:
        @tool
        def bash(command: str) -> str:
            return command

        builtin = AnthropicBashTool()
        tools = [builtin, bash] if builtin_first else [bash, builtin]
        config = SchemaRecordingConfig("done")

        await Agent("", tools=tools, config=config).ask("Run it")

        assert config.offered == [list(await tools[-1].schemas(context))]