RUNLOCALAIv38
->Will it run?Best GPUCompareTroubleshootStartLearnPulseModelsHardwareToolsBench
Run check
RUNLOCALAI

Independently operated catalog for local-AI hardware and software. Hand-written verdicts. Source-cited claims. Reproducible commands when we have them.

OP·Eruo Fredoline
DIR
  • Models
  • Hardware
  • Tools
  • Benchmarks
TOOLS
  • Will it run?
  • Compare hardware
  • Cost vs cloud
  • Choose my GPU
  • Prompting kits
  • Quick answers
REF
  • All buyer guides
  • Learn local AI
  • Methodology
  • Glossary
  • Errors KB
  • Trust
EDITOR
  • About
  • Author
  • How we make money
  • Editorial policy
  • Contact
LEGAL
  • Privacy
  • Terms
  • Sitemap
MAIL · MONTHLY DIGEST
Get monthly local AI changes
Monthly recap. No spam.
DISCLOSURE

Some links on this site are affiliate links (Amazon Associates and other first-class retailers). When you buy through them, we earn a small commission at no extra cost to you. Affiliate links do not influence our verdicts — there are cards we rate highly that we don't have affiliate relationships with, and cards that sell well that we refuse to recommend. Read more →

© 2026 runlocalai.coIndependently operated
RUNLOCALAI · v38
  1. >
  2. Home
  3. /Learn
  4. /Courses
  5. /Custom Agent Frameworks
  6. /Ch. 5
Custom Agent Frameworks

05. Function Calling Integration

Chapter 5 of 24 · 20 min
KEY INSIGHT

Function calling integration is a protocol. Treat it like one: validate inputs, handle errors explicitly, and don't assume the model always produces perfectly-formed output.

Function calling is how the LLM tells your agent to act. Integrating it correctly means handling the protocol precisely—request format, response parsing, error recovery.

Request format (OpenAI-compatible):

def build_function_call_request(
    messages: list[dict],
    tools: list[dict],
    temperature: float = 0.0
) -> dict[str, Any]:
    return {
        "model": "gpt-4-turbo",
        "messages": messages,
        "tools": tools,
        "tool_choice": "auto",  # Let model decide
        "temperature": temperature
    }

Response parsing:

from dataclasses import dataclass
from typing import Optional

@dataclass
class ToolCall:
    id: str
    name: str
    arguments: dict[str, Any]

@dataclass
class LLMResponse:
    content: Optional[str]
    finish_reason: str
    tool_calls: list[ToolCall]

def parse_response(raw: dict) -> LLMResponse:
    raw_message = raw.get("choices", [{}])[0].get("message", {})
    
    tool_calls = []
    for tc in raw_message.get("tool_calls", []):
        tool_calls.append(ToolCall(
            id=tc["id"],
            name=tc["function"]["name"],
            arguments=json.loads(tc["function"]["arguments"])
        ))
    
    return LLMResponse(
        content=raw_message.get("content"),
        finish_reason=raw.get("choices", [{}])[0].get("finish_reason"),
        tool_calls=tool_calls
    )

Critical detail: tool_choice: "auto" lets the model decide whether to use a tool or respond directly. Using "required" forces tool use, which is sometimes what you want but often causes issues when the model should just answer.

Failure mode: JSON parsing errors. Model-generated JSON arguments sometimes contain trailing commas, missing quotes, or invalid escape sequences. Resilient parsing handles these:

def safe_parse_arguments(arg_str: str) -> dict[str, Any]:
    # Attempt JSON parse
    try:
        return json.loads(arg_str)
    except json.JSONDecodeError:
        # Try fixing common issues
        cleaned = arg_str.replace(",\n}", "\n}").replace(",\n]", "]")
        try:
            return json.loads(cleaned)
        except json.JSONDecodeError as e:
            raise ToolArgumentError(f"Failed to parse arguments: {e}\nRaw: {arg_str}")

Failure mode: argument type coercion. The model might pass "42" (string) for an integer parameter. Implement coercion at the registry layer:

def coerce_arguments(tool: Tool, raw_args: dict[str, Any]) -> dict[str, Any]:
    coerced = {}
    for param_name, param_schema in tool.parameters.get("properties", {}).items():
        if param_name in raw_args:
            raw_value = raw_args[param_name]
            target_type = param_schema.get("type")
            
            if target_type == "integer" and isinstance(raw_value, str):
                coerced[param_name] = int(raw_value)
            elif target_type == "number" and isinstance(raw_value, str):
                coerced[param_name] = float(raw_value)
            else:
                coerced[param_name] = raw_value
    return coerced
EXERCISE

Build a mock LLM client that returns function calls with one malformed argument. Write the parsing and coercion logic to handle it gracefully without crashing.

← Chapter 4
Tool Registry
Chapter 6 →
Working Memory