Files
rohitg00--ai-engineering-fr…/phases/11-llm-engineering/09-function-calling/code/function_calling.py
T
2026-07-13 12:09:03 +08:00

428 lines
16 KiB
Python

import json
import math
import time
TOOL_REGISTRY = {}
def register_tool(name, description, parameters, function):
TOOL_REGISTRY[name] = {
"definition": {
"type": "function",
"function": {
"name": name,
"description": description,
"parameters": parameters,
},
},
"function": function,
}
def calculator(expression, precision=2):
allowed = set("0123456789+-*/.() ")
if not all(c in allowed for c in expression):
return {"error": True, "message": f"Invalid characters in expression: {expression}"}
try:
result = eval(expression, {"__builtins__": {}}, {"math": math})
return {"result": round(float(result), precision), "expression": expression}
except Exception as e:
return {"error": True, "message": str(e)}
WEATHER_DB = {
"tokyo": {"temp_c": 18, "condition": "cloudy", "humidity": 72, "wind_kph": 14},
"new york": {"temp_c": 22, "condition": "sunny", "humidity": 45, "wind_kph": 8},
"london": {"temp_c": 12, "condition": "rainy", "humidity": 88, "wind_kph": 22},
"san francisco": {"temp_c": 16, "condition": "foggy", "humidity": 80, "wind_kph": 18},
"sydney": {"temp_c": 25, "condition": "sunny", "humidity": 55, "wind_kph": 10},
}
def get_weather(city, units="celsius"):
key = city.lower().strip()
if key not in WEATHER_DB:
suggestions = [c for c in WEATHER_DB if c.startswith(key[:3])]
return {
"error": True,
"message": f"City '{city}' not found.",
"suggestions": suggestions,
"code": "CITY_NOT_FOUND",
}
data = WEATHER_DB[key].copy()
if units == "fahrenheit":
data["temp_f"] = round(data["temp_c"] * 9 / 5 + 32, 1)
del data["temp_c"]
data["city"] = city
return data
SEARCH_DB = {
"python function calling": [
{"title": "OpenAI Function Calling Guide", "url": "https://platform.openai.com/docs/guides/function-calling", "snippet": "Learn how to connect LLMs to external tools."},
{"title": "Anthropic Tool Use", "url": "https://docs.anthropic.com/en/docs/tool-use", "snippet": "Claude can interact with external tools and APIs."},
],
"MCP protocol": [
{"title": "Model Context Protocol", "url": "https://modelcontextprotocol.io", "snippet": "An open standard for connecting AI models to data sources."},
],
"weather API": [
{"title": "OpenWeatherMap API", "url": "https://openweathermap.org/api", "snippet": "Free weather API with current, forecast, and historical data."},
],
}
def web_search(query, max_results=3):
key = query.lower().strip()
for db_key, results in SEARCH_DB.items():
if db_key in key or key in db_key:
return {"query": query, "results": results[:max_results], "total": len(results)}
return {"query": query, "results": [], "total": 0}
FILE_SYSTEM = {
"data/config.json": '{"model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096}',
"data/users.csv": "name,email,role\nAlice,alice@example.com,admin\nBob,bob@example.com,user",
"README.md": "# My Project\nA tool-use agent built from scratch.",
}
def read_file(path):
if ".." in path or path.startswith("/"):
return {"error": True, "message": "Path traversal not allowed.", "code": "FORBIDDEN"}
if path not in FILE_SYSTEM:
available = list(FILE_SYSTEM.keys())
return {"error": True, "message": f"File '{path}' not found.", "available_files": available, "code": "NOT_FOUND"}
content = FILE_SYSTEM[path]
return {"path": path, "content": content, "size_bytes": len(content), "lines": content.count("\n") + 1}
def run_code(code, language="python"):
if language != "python":
return {"error": True, "message": f"Language '{language}' not supported. Only 'python' is available."}
forbidden = ["import os", "import sys", "import subprocess", "exec(", "eval(", "__import__", "open("]
for pattern in forbidden:
if pattern in code:
return {"error": True, "message": f"Forbidden operation: {pattern}", "code": "SECURITY_VIOLATION"}
try:
local_vars = {}
exec(
code,
{
"__builtins__": {
"print": print, "range": range, "len": len, "str": str,
"int": int, "float": float, "list": list, "dict": dict,
"sum": sum, "min": min, "max": max, "abs": abs, "round": round,
"sorted": sorted, "enumerate": enumerate, "zip": zip,
"map": map, "filter": filter, "math": math,
}
},
local_vars,
)
result = local_vars.get("result", None)
return {
"success": True,
"result": result,
"variables": {k: str(v) for k, v in local_vars.items() if not k.startswith("_")},
}
except Exception as e:
return {"error": True, "message": f"{type(e).__name__}: {e}"}
def register_all_tools():
register_tool(
"calculator",
"Evaluate a mathematical expression. Supports +, -, *, /, parentheses, and decimals. Returns the numeric result.",
{
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Math expression, e.g. '(10 + 5) * 3'"},
"precision": {"type": "integer", "description": "Decimal places in result", "default": 2},
},
"required": ["expression"],
},
calculator,
)
register_tool(
"get_weather",
"Get current weather for a city. Returns temperature, condition, humidity, and wind speed.",
{
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name, e.g. 'Tokyo' or 'San Francisco'"},
"units": {"type": "string", "enum": ["celsius", "fahrenheit"], "description": "Temperature units, defaults to celsius"},
},
"required": ["city"],
},
get_weather,
)
register_tool(
"web_search",
"Search the web for information. Returns a list of results with title, URL, and snippet.",
{
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"max_results": {"type": "integer", "description": "Maximum results to return", "default": 3},
},
"required": ["query"],
},
web_search,
)
register_tool(
"read_file",
"Read the contents of a file. Returns the file content, size, and line count.",
{
"type": "object",
"properties": {
"path": {"type": "string", "description": "Relative file path, e.g. 'data/config.json'"},
},
"required": ["path"],
},
read_file,
)
register_tool(
"run_code",
"Execute Python code in a sandboxed environment. Set a 'result' variable to return output.",
{
"type": "object",
"properties": {
"code": {"type": "string", "description": "Python code to execute"},
"language": {"type": "string", "enum": ["python"], "description": "Programming language"},
},
"required": ["code"],
},
run_code,
)
def simulate_model_decision(user_message, tools, conversation_history):
msg = user_message.lower()
if any(word in msg for word in ["weather", "temperature", "forecast"]):
cities = []
for city in WEATHER_DB:
if city in msg:
cities.append(city)
if not cities:
for word in msg.split():
if word.capitalize() in [c.title() for c in WEATHER_DB]:
cities.append(word)
if not cities:
cities = ["tokyo"]
calls = []
for city in cities:
calls.append({"name": "get_weather", "arguments": {"city": city.title()}})
return calls
if any(word in msg for word in ["calculate", "compute", "math", "what is", "how much"]):
for token in msg.split():
if any(c in token for c in "+-*/"):
return [{"name": "calculator", "arguments": {"expression": token}}]
if "+" in msg or "-" in msg or "*" in msg or "/" in msg:
expr = "".join(c for c in msg if c in "0123456789+-*/.() ")
if expr.strip():
return [{"name": "calculator", "arguments": {"expression": expr.strip()}}]
return [{"name": "calculator", "arguments": {"expression": "0"}}]
if any(word in msg for word in ["search", "find", "look up", "google"]):
query = msg.replace("search for", "").replace("look up", "").replace("find", "").strip()
return [{"name": "web_search", "arguments": {"query": query}}]
if any(word in msg for word in ["read", "file", "open", "cat", "show"]):
for path in FILE_SYSTEM:
if path.split("/")[-1].split(".")[0] in msg:
return [{"name": "read_file", "arguments": {"path": path}}]
return [{"name": "read_file", "arguments": {"path": "README.md"}}]
if any(word in msg for word in ["run", "execute", "code", "python"]):
return [{"name": "run_code", "arguments": {"code": "result = 'Hello from the sandbox!'", "language": "python"}}]
return []
def execute_tool_call(tool_call):
name = tool_call["name"]
args = tool_call["arguments"]
if name not in TOOL_REGISTRY:
return {"tool": name, "result": {"error": True, "message": f"Unknown tool: {name}", "code": "UNKNOWN_TOOL"}, "execution_time_ms": 0}
tool = TOOL_REGISTRY[name]
func = tool["function"]
start = time.time()
try:
result = func(**args)
except TypeError as e:
result = {"error": True, "message": f"Invalid arguments: {e}"}
elapsed_ms = round((time.time() - start) * 1000, 2)
return {"tool": name, "result": result, "execution_time_ms": elapsed_ms}
def validate_tool_arguments(tool_name, arguments):
if tool_name not in TOOL_REGISTRY:
return [f"Unknown tool: {tool_name}"]
schema = TOOL_REGISTRY[tool_name]["definition"]["function"]["parameters"]
errors = []
if not isinstance(arguments, dict):
return [f"Arguments must be an object, got {type(arguments).__name__}"]
for required_field in schema.get("required", []):
if required_field not in arguments:
errors.append(f"Missing required argument: {required_field}")
properties = schema.get("properties", {})
for arg_name, arg_value in arguments.items():
if arg_name not in properties:
errors.append(f"Unknown argument: {arg_name}")
continue
prop_schema = properties[arg_name]
expected_type = prop_schema.get("type")
type_checks = {
"string": str,
"integer": int,
"number": (int, float),
"boolean": bool,
"array": list,
"object": dict,
}
if expected_type in type_checks:
if not isinstance(arg_value, type_checks[expected_type]):
errors.append(f"Argument '{arg_name}': expected {expected_type}, got {type(arg_value).__name__}")
if "enum" in prop_schema and arg_value not in prop_schema["enum"]:
errors.append(f"Argument '{arg_name}': '{arg_value}' not in {prop_schema['enum']}")
return errors
def run_function_calling_loop(user_message, max_iterations=5):
conversation = [{"role": "user", "content": user_message}]
tool_definitions = [t["definition"] for t in TOOL_REGISTRY.values()]
all_tool_results = []
for iteration in range(max_iterations):
tool_calls = simulate_model_decision(user_message, tool_definitions, conversation)
if not tool_calls:
break
results = []
for call in tool_calls:
result = execute_tool_call(call)
results.append(result)
conversation.append({"role": "assistant", "content": None, "tool_calls": tool_calls})
for result in results:
conversation.append({
"role": "tool",
"content": json.dumps(result["result"]),
"tool_name": result["tool"],
})
all_tool_results.extend(results)
break
return {
"conversation": conversation,
"tool_results": all_tool_results,
"iterations": iteration + 1 if tool_calls else 0,
}
def run_demo():
register_all_tools()
print("=" * 60)
print(" Function Calling & Tool Use Demo")
print("=" * 60)
print("\n--- Registered Tools ---")
for name, tool in TOOL_REGISTRY.items():
desc = tool["definition"]["function"]["description"][:60]
params = list(tool["definition"]["function"]["parameters"].get("properties", {}).keys())
print(f" {name}: {desc}...")
print(f" params: {params}")
print(f"\n--- Argument Validation ---")
validation_tests = [
("get_weather", {"city": "Tokyo"}, "Valid call"),
("get_weather", {}, "Missing required arg"),
("get_weather", {"city": "Tokyo", "units": "kelvin"}, "Invalid enum value"),
("calculator", {"expression": 123}, "Wrong type (int for string)"),
("unknown_tool", {"x": 1}, "Unknown tool"),
]
for tool_name, args, label in validation_tests:
errors = validate_tool_arguments(tool_name, args)
status = "VALID" if not errors else f"ERRORS: {errors}"
print(f" {label}: {status}")
print(f"\n--- Tool Execution ---")
direct_tests = [
{"name": "calculator", "arguments": {"expression": "(10 + 5) * 3 / 2"}},
{"name": "get_weather", "arguments": {"city": "Tokyo"}},
{"name": "get_weather", "arguments": {"city": "Mars"}},
{"name": "web_search", "arguments": {"query": "python function calling"}},
{"name": "read_file", "arguments": {"path": "data/config.json"}},
{"name": "read_file", "arguments": {"path": "../etc/passwd"}},
{"name": "run_code", "arguments": {"code": "result = sum(range(1, 101))"}},
{"name": "run_code", "arguments": {"code": "import os; os.system('rm -rf /')"}},
]
for call in direct_tests:
result = execute_tool_call(call)
print(f"\n {call['name']}({json.dumps(call['arguments'])})")
print(f" -> {json.dumps(result['result'], indent=None)[:100]}")
print(f" time: {result['execution_time_ms']}ms")
print(f"\n--- Full Function Calling Loop ---")
test_queries = [
"What's the weather in Tokyo?",
"Calculate (100 + 250) * 0.15",
"Search for MCP protocol",
"Read the config file",
"Run some Python code",
"Tell me a joke",
]
for query in test_queries:
print(f"\n User: {query}")
result = run_function_calling_loop(query)
if result["tool_results"]:
for tr in result["tool_results"]:
print(f" Tool: {tr['tool']} ({tr['execution_time_ms']}ms)")
print(f" Result: {json.dumps(tr['result'], indent=None)[:90]}")
else:
print(f" [No tool called -- direct response]")
print(f" Iterations: {result['iterations']}")
print(f"\n--- Parallel Tool Calls ---")
multi_city_query = "What's the weather in tokyo and london?"
print(f" User: {multi_city_query}")
result = run_function_calling_loop(multi_city_query)
print(f" Tool calls made: {len(result['tool_results'])}")
for tr in result["tool_results"]:
city = tr["result"].get("city", "unknown")
temp = tr["result"].get("temp_c", "N/A")
print(f" {city}: {temp}C, {tr['result'].get('condition', 'N/A')}")
print(f"\n--- Security Checks ---")
security_tests = [
("read_file", {"path": "../../etc/passwd"}),
("run_code", {"code": "import subprocess; subprocess.run(['ls'])"}),
("calculator", {"expression": "__import__('os').system('ls')"}),
]
for tool_name, args in security_tests:
result = execute_tool_call({"name": tool_name, "arguments": args})
blocked = result["result"].get("error", False)
print(f" {tool_name}({list(args.values())[0][:40]}): {'BLOCKED' if blocked else 'ALLOWED'}")
if __name__ == "__main__":
run_demo()