fix(classic): always use native tool calling, fix N/A command loop

- Remove openai_functions config option - native tool calling is now always enabled
- Remove use_functions_api from BaseAgentConfiguration and prompt strategy
- Add use_prefill config to disable prefill for Anthropic (prefill + tools incompatible)
- Update anthropic dependency to ^0.45.0 for tools API support
- Simplify prompt strategy to always expect tool_calls from LLM response

This fixes the N/A command loop bug where models would output "N/A" as a
command name when function calling was disabled. With native tool calling
always enabled, models are forced to pick from valid tools only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-01-18 19:54:40 -06:00
parent 6fbd208fe3
commit 0a65df5102
12 changed files with 6138 additions and 4970 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -102,7 +102,6 @@ smart_llm: ModelName = "gpt-4"
big_brain: bool = True # Use smart_llm
cycle_budget: Optional[int] = 1 # Steps before approval needed
send_token_limit: Optional[int] # Prompt token budget
use_functions_api: bool = False
```
### Component System (`agent/components.py`)

View File

@@ -18,7 +18,7 @@ from typing import (
)
from colorama import Fore
from pydantic import BaseModel, Field, ValidationInfo, field_validator
from pydantic import BaseModel, Field
from pydantic_core import from_json, to_json
from forge.agent import protocols
@@ -53,7 +53,6 @@ class BaseAgentConfiguration(SystemConfiguration):
fast_llm: ModelName = UserConfigurable(default=OpenAIModelName.GPT3_16k)
smart_llm: ModelName = UserConfigurable(default=OpenAIModelName.GPT4)
use_functions_api: bool = UserConfigurable(default=False)
default_cycle_instruction: str = DEFAULT_TRIGGERING_PROMPT
"""The default instruction passed to the AI for a thinking cycle."""
@@ -85,22 +84,6 @@ class BaseAgentConfiguration(SystemConfiguration):
defaults to 75% of `llm.max_tokens`.
"""
@field_validator("use_functions_api")
def validate_openai_functions(cls, value: bool, info: ValidationInfo):
if value:
smart_llm = info.data["smart_llm"]
fast_llm = info.data["fast_llm"]
assert all(
[
not any(s in name for s in {"-0301", "-0314"})
for name in {smart_llm, fast_llm}
]
), (
f"Model {smart_llm} does not support OpenAI Functions. "
"Please disable OPENAI_FUNCTIONS or choose a suitable model."
)
return value
class BaseAgentSettings(SystemSettings):
agent_id: str = ""

8950
classic/forge/poetry.lock generated

File diff suppressed because one or more lines are too long

View File

@@ -27,7 +27,7 @@ python = "^3.10"
agbenchmark = { path = "../benchmark", optional = true }
# agbenchmark = {git = "https://github.com/Significant-Gravitas/AutoGPT.git", subdirectory = "benchmark", optional = true}
aiohttp = "^3.8.5"
anthropic = ">=0.40,<1.0"
anthropic = "^0.45.0"
beautifulsoup4 = "^4.12.2"
boto3 = "^1.33.6"
charset-normalizer = "^3.1.0"

View File

@@ -109,7 +109,6 @@ def create_agent_state(
fast_llm=app_config.fast_llm,
smart_llm=app_config.smart_llm,
allow_fs_access=not app_config.restrict_to_workspace,
use_functions_api=app_config.openai_functions,
),
history=Agent.default_settings.history.model_copy(deep=True),
)

View File

@@ -5,8 +5,6 @@ import logging
from typing import TYPE_CHECKING, Any, ClassVar, Optional
import sentry_sdk
from pydantic import Field
from forge.agent.base import BaseAgent, BaseAgentConfiguration, BaseAgentSettings
from forge.agent.protocols import (
AfterExecute,
@@ -64,6 +62,7 @@ from forge.utils.exceptions import (
CommandExecutionError,
UnknownCommandError,
)
from pydantic import Field
from .prompt_strategies.one_shot import (
OneShotAgentActionProposal,
@@ -113,11 +112,8 @@ class Agent(BaseAgent[OneShotAgentActionProposal], Configurable[AgentSettings]):
prompt_config = OneShotAgentPromptStrategy.default_configuration.model_copy(
deep=True
)
prompt_config.use_functions_api = (
settings.config.use_functions_api
# Anthropic currently doesn't support tools + prefilling :(
and self.llm.provider_name != "anthropic"
)
# Anthropic doesn't support tools + prefilling, so disable prefill for Anthropic
prompt_config.use_prefill = self.llm.provider_name != "anthropic"
self.prompt_strategy = OneShotAgentPromptStrategy(prompt_config, logger)
self.commands: list[Command] = []

View File

@@ -73,7 +73,7 @@ class OneShotAgentPromptConfiguration(SystemConfiguration):
choose_action_instruction: str = UserConfigurable(
default=DEFAULT_CHOOSE_ACTION_INSTRUCTION
)
use_functions_api: bool = UserConfigurable(default=False)
use_prefill: bool = True
#########
# State #
@@ -134,8 +134,8 @@ class OneShotAgentPromptStrategy(PromptStrategy):
*messages,
final_instruction_msg,
],
prefill_response=response_prefill,
functions=commands if self.config.use_functions_api else [],
prefill_response=response_prefill if self.config.use_prefill else "",
functions=commands,
)
def build_system_prompt(
@@ -152,9 +152,7 @@ class OneShotAgentPromptStrategy(PromptStrategy):
str: The system prompt body
str: The desired start for the LLM's response; used to steer the output
"""
response_fmt_instruction, response_prefill = self.response_format_instruction(
self.config.use_functions_api
)
response_fmt_instruction, response_prefill = self.response_format_instruction()
system_prompt_parts = (
self._generate_intro_prompt(ai_profile)
+ (self._generate_os_info() if include_os_info else [])
@@ -181,10 +179,11 @@ class OneShotAgentPromptStrategy(PromptStrategy):
response_prefill,
)
def response_format_instruction(self, use_functions_api: bool) -> tuple[str, str]:
def response_format_instruction(self) -> tuple[str, str]:
response_schema = self.response_schema.model_copy(deep=True)
assert response_schema.properties
if use_functions_api and "use_tool" in response_schema.properties:
# Always use tool calling - remove use_tool from schema since it comes from tool_calls
if "use_tool" in response_schema.properties:
del response_schema.properties["use_tool"]
# Unindent for performance
@@ -199,7 +198,7 @@ class OneShotAgentPromptStrategy(PromptStrategy):
(
f"YOU MUST ALWAYS RESPOND WITH A JSON OBJECT OF THE FOLLOWING TYPE:\n"
f"{response_format}"
+ ("\n\nYOU MUST ALSO INVOKE A TOOL!" if use_functions_api else "")
"\n\nYOU MUST ALSO INVOKE A TOOL!"
),
response_prefill,
)
@@ -269,13 +268,13 @@ class OneShotAgentPromptStrategy(PromptStrategy):
"Parsing object extracted from LLM response:\n"
f"{json.dumps(assistant_reply_dict, indent=4)}"
)
if self.config.use_functions_api:
if not response.tool_calls:
raise InvalidAgentResponseError("Assistant did not use a tool")
assistant_reply_dict["use_tool"] = response.tool_calls[0].function
# Always expect tool calls - native tool calling is always enabled
if not response.tool_calls:
raise InvalidAgentResponseError("Assistant did not use a tool")
assistant_reply_dict["use_tool"] = response.tool_calls[0].function
parsed_response = OneShotAgentActionProposal.model_validate(
assistant_reply_dict
)
parsed_response.raw_message = response.copy()
parsed_response.raw_message = response.model_copy()
return parsed_response

View File

@@ -9,18 +9,20 @@ from pathlib import Path
from typing import Optional, Union
from forge.config.base import BaseConfig
from forge.llm.providers import CHAT_MODELS, ModelName
from forge.llm.providers import ModelName
from forge.llm.providers.openai import OpenAICredentials, OpenAIModelName
from forge.logging.config import LoggingConfig
from forge.models.config import Configurable, UserConfigurable
from pydantic import SecretStr, ValidationInfo, field_validator
from pydantic import SecretStr
logger = logging.getLogger(__name__)
AZURE_CONFIG_FILE = Path("azure.yaml")
GPT_4_MODEL = OpenAIModelName.GPT4_O
GPT_3_MODEL = OpenAIModelName.GPT4_O_MINI # Fallback model for when configured model is unavailable
GPT_3_MODEL = (
OpenAIModelName.GPT4_O_MINI
) # Fallback model for when configured model is unavailable
class AppConfig(BaseConfig):
@@ -55,9 +57,6 @@ class AppConfig(BaseConfig):
from_env="SMART_LLM",
)
temperature: float = UserConfigurable(default=0, from_env="TEMPERATURE")
openai_functions: bool = UserConfigurable(
default=False, from_env=lambda: os.getenv("OPENAI_FUNCTIONS", "False") == "True"
)
embedding_model: str = UserConfigurable(
default="text-embedding-3-small", from_env="EMBEDDING_MODEL"
)
@@ -90,16 +89,6 @@ class AppConfig(BaseConfig):
default=AZURE_CONFIG_FILE, from_env="AZURE_CONFIG_FILE"
)
@field_validator("openai_functions")
def validate_openai_functions(cls, value: bool, info: ValidationInfo):
if value:
smart_llm = info.data["smart_llm"]
assert CHAT_MODELS[smart_llm].has_function_call_api, (
f"Model {smart_llm} does not support tool calling. "
"Please disable OPENAI_FUNCTIONS or choose a suitable model."
)
return value
class ConfigBuilder(Configurable[AppConfig]):
default_settings = AppConfig()

File diff suppressed because it is too large Load Diff

View File

@@ -93,7 +93,6 @@ def agent(
fast_llm=config.fast_llm,
smart_llm=config.smart_llm,
allow_fs_access=not config.restrict_to_workspace,
use_functions_api=config.openai_functions,
),
history=Agent.default_settings.history.model_copy(deep=True),
)

View File

@@ -26,7 +26,6 @@ def dummy_agent(config: AppConfig, llm_provider: MultiProvider):
config=AgentConfiguration(
fast_llm=config.fast_llm,
smart_llm=config.smart_llm,
use_functions_api=config.openai_functions,
),
history=Agent.default_settings.history.model_copy(deep=True),
)