chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
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()
|
||||
@@ -0,0 +1,409 @@
|
||||
// Function calling in TypeScript: JSON-schema tool definitions, registry,
|
||||
// validator, sandboxed dispatcher, mock model decision loop, parallel calls.
|
||||
// Mirrors code/function_calling.py and follows the four-step pattern shared
|
||||
// by OpenAI, Anthropic, and Google: define, detect, execute, return.
|
||||
// Sources:
|
||||
// https://platform.openai.com/docs/guides/function-calling
|
||||
// https://docs.anthropic.com/en/docs/build-with-claude/tool-use
|
||||
// https://ai.google.dev/gemini-api/docs/function-calling
|
||||
|
||||
type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
|
||||
|
||||
type ParamType = "string" | "integer" | "number" | "boolean" | "array" | "object";
|
||||
|
||||
type ParamSchema = {
|
||||
type: ParamType;
|
||||
description?: string;
|
||||
enum?: readonly JsonValue[];
|
||||
default?: JsonValue;
|
||||
};
|
||||
|
||||
type ToolParameters = {
|
||||
type: "object";
|
||||
properties: Readonly<Record<string, ParamSchema>>;
|
||||
required?: readonly string[];
|
||||
};
|
||||
|
||||
type ToolDefinition = {
|
||||
type: "function";
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: ToolParameters;
|
||||
};
|
||||
};
|
||||
|
||||
type ToolFunction = (args: Readonly<Record<string, JsonValue>>) => JsonValue;
|
||||
|
||||
type RegisteredTool = {
|
||||
definition: ToolDefinition;
|
||||
fn: ToolFunction;
|
||||
};
|
||||
|
||||
const TOOL_REGISTRY: Map<string, RegisteredTool> = new Map();
|
||||
|
||||
function registerTool(name: string, description: string, parameters: ToolParameters, fn: ToolFunction): void {
|
||||
TOOL_REGISTRY.set(name, {
|
||||
definition: { type: "function", function: { name, description, parameters } },
|
||||
fn,
|
||||
});
|
||||
}
|
||||
|
||||
const ARITH_RE = /^[\d+\-*/().\s]+$/;
|
||||
|
||||
function calculator(args: Readonly<Record<string, JsonValue>>): JsonValue {
|
||||
const expression = String(args.expression ?? "");
|
||||
const precision = typeof args.precision === "number" ? args.precision : 2;
|
||||
if (!ARITH_RE.test(expression)) {
|
||||
return { error: true, message: "Invalid characters in expression: " + expression };
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-new-func
|
||||
const value = new Function("return (" + expression + ")")() as unknown;
|
||||
const num = Number(value);
|
||||
if (!Number.isFinite(num)) return { error: true, message: "non-finite result" };
|
||||
return { result: Number(num.toFixed(precision)), expression };
|
||||
} catch (err) {
|
||||
return { error: true, message: String(err) };
|
||||
}
|
||||
}
|
||||
|
||||
const WEATHER_DB: Readonly<Record<string, { temp_c: number; condition: string; humidity: number; wind_kph: number }>> = {
|
||||
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 },
|
||||
};
|
||||
|
||||
function getWeather(args: Readonly<Record<string, JsonValue>>): JsonValue {
|
||||
const city = String(args.city ?? "");
|
||||
const units = String(args.units ?? "celsius");
|
||||
const key = city.toLowerCase().trim();
|
||||
const row = WEATHER_DB[key];
|
||||
if (!row) {
|
||||
const suggestions = Object.keys(WEATHER_DB).filter((c) => c.startsWith(key.slice(0, 3)));
|
||||
return { error: true, message: "City '" + city + "' not found.", suggestions, code: "CITY_NOT_FOUND" };
|
||||
}
|
||||
if (units === "fahrenheit") {
|
||||
return { city, condition: row.condition, humidity: row.humidity, wind_kph: row.wind_kph, temp_f: Number((row.temp_c * 9 / 5 + 32).toFixed(1)) };
|
||||
}
|
||||
return { city, ...row };
|
||||
}
|
||||
|
||||
const SEARCH_DB: Readonly<Record<string, ReadonlyArray<{ title: string; url: string; snippet: string }>>> = {
|
||||
"python function calling": [
|
||||
{ title: "OpenAI Function Calling Guide", url: "https://platform.openai.com/docs/guides/function-calling", snippet: "Connect LLMs to external tools." },
|
||||
{ title: "Anthropic Tool Use", url: "https://docs.anthropic.com/en/docs/build-with-claude/tool-use", snippet: "Claude can interact with tools and APIs." },
|
||||
],
|
||||
"mcp protocol": [
|
||||
{ title: "Model Context Protocol", url: "https://modelcontextprotocol.io", snippet: "Open standard connecting models to data sources." },
|
||||
],
|
||||
"weather api": [
|
||||
{ title: "OpenWeatherMap API", url: "https://openweathermap.org/api", snippet: "Free weather API." },
|
||||
],
|
||||
};
|
||||
|
||||
function webSearch(args: Readonly<Record<string, JsonValue>>): JsonValue {
|
||||
const query = String(args.query ?? "");
|
||||
const maxResults = typeof args.max_results === "number" ? args.max_results : 3;
|
||||
const key = query.toLowerCase().trim();
|
||||
for (const dbKey of Object.keys(SEARCH_DB)) {
|
||||
if (dbKey.includes(key) || key.includes(dbKey)) {
|
||||
const all = SEARCH_DB[dbKey];
|
||||
return { query, results: all.slice(0, maxResults), total: all.length };
|
||||
}
|
||||
}
|
||||
return { query, results: [], total: 0 };
|
||||
}
|
||||
|
||||
const FILE_SYSTEM: Readonly<Record<string, string>> = {
|
||||
"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.",
|
||||
};
|
||||
|
||||
function readFile(args: Readonly<Record<string, JsonValue>>): JsonValue {
|
||||
const path = String(args.path ?? "");
|
||||
if (path.includes("..") || path.startsWith("/")) {
|
||||
return { error: true, message: "Path traversal not allowed.", code: "FORBIDDEN" };
|
||||
}
|
||||
if (!(path in FILE_SYSTEM)) {
|
||||
return { error: true, message: "File '" + path + "' not found.", available_files: Object.keys(FILE_SYSTEM), code: "NOT_FOUND" };
|
||||
}
|
||||
const content = FILE_SYSTEM[path];
|
||||
return { path, content, size_bytes: content.length, lines: content.split("\n").length };
|
||||
}
|
||||
|
||||
function runCode(args: Readonly<Record<string, JsonValue>>): JsonValue {
|
||||
const code = String(args.code ?? "");
|
||||
const language = String(args.language ?? "javascript");
|
||||
if (language !== "javascript") {
|
||||
return { error: true, message: "Language '" + language + "' not supported." };
|
||||
}
|
||||
const FORBIDDEN = ["require(", "process.", "fs.", "child_process", "import ", "eval(", "Function("];
|
||||
for (const p of FORBIDDEN) {
|
||||
if (code.includes(p)) {
|
||||
return { error: true, message: "Forbidden operation: " + p, code: "SECURITY_VIOLATION" };
|
||||
}
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-new-func
|
||||
const fn = new Function("Math", "let result; " + code + "; return result;");
|
||||
const result = fn(Math) as unknown;
|
||||
return { success: true, result: result as JsonValue };
|
||||
} catch (err) {
|
||||
return { error: true, message: (err as Error).name + ": " + (err as Error).message };
|
||||
}
|
||||
}
|
||||
|
||||
function registerAllTools(): void {
|
||||
registerTool(
|
||||
"calculator",
|
||||
"Evaluate a math expression. Supports +, -, *, /, parentheses, decimals.",
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
expression: { type: "string", description: "Math expression, e.g. '(10 + 5) * 3'" },
|
||||
precision: { type: "integer", description: "Decimal places", default: 2 },
|
||||
},
|
||||
required: ["expression"],
|
||||
},
|
||||
calculator,
|
||||
);
|
||||
registerTool(
|
||||
"get_weather",
|
||||
"Get current weather for a city.",
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
city: { type: "string", description: "City name" },
|
||||
units: { type: "string", description: "celsius or fahrenheit", enum: ["celsius", "fahrenheit"] },
|
||||
},
|
||||
required: ["city"],
|
||||
},
|
||||
getWeather,
|
||||
);
|
||||
registerTool(
|
||||
"web_search",
|
||||
"Search the web.",
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
query: { type: "string", description: "Search query" },
|
||||
max_results: { type: "integer", description: "Max results", default: 3 },
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
webSearch,
|
||||
);
|
||||
registerTool(
|
||||
"read_file",
|
||||
"Read file contents.",
|
||||
{
|
||||
type: "object",
|
||||
properties: { path: { type: "string", description: "Relative path" } },
|
||||
required: ["path"],
|
||||
},
|
||||
readFile,
|
||||
);
|
||||
registerTool(
|
||||
"run_code",
|
||||
"Execute JavaScript in a sandbox. Assign to 'result' to return output.",
|
||||
{
|
||||
type: "object",
|
||||
properties: {
|
||||
code: { type: "string", description: "JavaScript code to run" },
|
||||
language: { type: "string", description: "javascript only", enum: ["javascript"] },
|
||||
},
|
||||
required: ["code"],
|
||||
},
|
||||
runCode,
|
||||
);
|
||||
}
|
||||
|
||||
type ToolCall = { name: string; arguments: Readonly<Record<string, JsonValue>> };
|
||||
|
||||
function simulateModelDecision(userMessage: string): ToolCall[] {
|
||||
const msg = userMessage.toLowerCase();
|
||||
if (/weather|temperature|forecast/.test(msg)) {
|
||||
const cities = Object.keys(WEATHER_DB).filter((c) => msg.includes(c));
|
||||
const targets = cities.length > 0 ? cities : ["tokyo"];
|
||||
return targets.map((city) => ({
|
||||
name: "get_weather",
|
||||
arguments: { city: city.replace(/\b\w/g, (c) => c.toUpperCase()) },
|
||||
}));
|
||||
}
|
||||
if (/calculate|compute|math|what is|how much/.test(msg)) {
|
||||
const m = msg.match(/[\d+\-*/().\s]{3,}/);
|
||||
if (m) return [{ name: "calculator", arguments: { expression: m[0].trim() } }];
|
||||
return [{ name: "calculator", arguments: { expression: "0" } }];
|
||||
}
|
||||
if (/search|find|look up/.test(msg)) {
|
||||
const query = msg.replace(/search for|look up|find|search/g, "").trim();
|
||||
return [{ name: "web_search", arguments: { query } }];
|
||||
}
|
||||
if (/read|file|open|show/.test(msg)) {
|
||||
for (const path of Object.keys(FILE_SYSTEM)) {
|
||||
const stem = path.split("/").pop()?.split(".")[0] ?? "";
|
||||
if (stem.length > 0 && msg.includes(stem)) {
|
||||
return [{ name: "read_file", arguments: { path } }];
|
||||
}
|
||||
}
|
||||
return [{ name: "read_file", arguments: { path: "README.md" } }];
|
||||
}
|
||||
if (/run|execute|code|javascript/.test(msg)) {
|
||||
return [{ name: "run_code", arguments: { code: "result = 'Hello from the sandbox!'", language: "javascript" } }];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
type ToolResult = { tool: string; result: JsonValue; executionTimeMs: number };
|
||||
|
||||
function executeToolCall(call: ToolCall): ToolResult {
|
||||
const tool = TOOL_REGISTRY.get(call.name);
|
||||
if (!tool) {
|
||||
return { tool: call.name, result: { error: true, message: "Unknown tool: " + call.name, code: "UNKNOWN_TOOL" }, executionTimeMs: 0 };
|
||||
}
|
||||
const start = Date.now();
|
||||
let result: JsonValue;
|
||||
try {
|
||||
result = tool.fn(call.arguments);
|
||||
} catch (err) {
|
||||
result = { error: true, message: "Invalid arguments: " + (err as Error).message };
|
||||
}
|
||||
return { tool: call.name, result, executionTimeMs: Date.now() - start };
|
||||
}
|
||||
|
||||
function validateToolArguments(toolName: string, args: unknown): string[] {
|
||||
const tool = TOOL_REGISTRY.get(toolName);
|
||||
if (!tool) return ["Unknown tool: " + toolName];
|
||||
if (args === null || typeof args !== "object" || Array.isArray(args)) {
|
||||
return ["Arguments must be an object, got " + typeof args];
|
||||
}
|
||||
const schema = tool.definition.function.parameters;
|
||||
const errors: string[] = [];
|
||||
for (const required of schema.required ?? []) {
|
||||
if (!(required in (args as Record<string, unknown>))) {
|
||||
errors.push("Missing required argument: " + required);
|
||||
}
|
||||
}
|
||||
const typeChecks: Readonly<Record<ParamType, (v: unknown) => boolean>> = {
|
||||
string: (v) => typeof v === "string",
|
||||
integer: (v) => Number.isInteger(v),
|
||||
number: (v) => typeof v === "number",
|
||||
boolean: (v) => typeof v === "boolean",
|
||||
array: (v) => Array.isArray(v),
|
||||
object: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
||||
};
|
||||
for (const [argName, argValue] of Object.entries(args as Record<string, unknown>)) {
|
||||
const prop = schema.properties[argName];
|
||||
if (!prop) {
|
||||
errors.push("Unknown argument: " + argName);
|
||||
continue;
|
||||
}
|
||||
if (!typeChecks[prop.type](argValue)) {
|
||||
errors.push("Argument '" + argName + "': expected " + prop.type + ", got " + typeof argValue);
|
||||
}
|
||||
if (prop.enum && !prop.enum.includes(argValue as JsonValue)) {
|
||||
errors.push("Argument '" + argName + "': '" + String(argValue) + "' not in " + JSON.stringify(prop.enum));
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function runFunctionCallingLoop(userMessage: string): { toolResults: ToolResult[]; iterations: number } {
|
||||
const calls = simulateModelDecision(userMessage);
|
||||
if (calls.length === 0) return { toolResults: [], iterations: 0 };
|
||||
const results = calls.map((c) => executeToolCall(c));
|
||||
return { toolResults: results, iterations: 1 };
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
registerAllTools();
|
||||
console.log("=".repeat(60));
|
||||
console.log(" Function Calling and Tool Use");
|
||||
console.log("=".repeat(60));
|
||||
|
||||
console.log("\n--- Registered Tools ---");
|
||||
for (const [name, tool] of TOOL_REGISTRY) {
|
||||
const params = Object.keys(tool.definition.function.parameters.properties);
|
||||
console.log(" " + name + ": " + tool.definition.function.description.slice(0, 60) + " | params: " + params.join(","));
|
||||
}
|
||||
|
||||
console.log("\n--- Argument Validation ---");
|
||||
const validationTests: ReadonlyArray<{ tool: string; args: unknown; label: string }> = [
|
||||
{ tool: "get_weather", args: { city: "Tokyo" }, label: "Valid call" },
|
||||
{ tool: "get_weather", args: {}, label: "Missing required arg" },
|
||||
{ tool: "get_weather", args: { city: "Tokyo", units: "kelvin" }, label: "Invalid enum value" },
|
||||
{ tool: "calculator", args: { expression: 123 }, label: "Wrong type (number for string)" },
|
||||
{ tool: "unknown_tool", args: { x: 1 }, label: "Unknown tool" },
|
||||
];
|
||||
for (const { tool, args, label } of validationTests) {
|
||||
const errors = validateToolArguments(tool, args);
|
||||
console.log(" " + label + ": " + (errors.length === 0 ? "VALID" : "ERRORS: " + errors.join(" / ")));
|
||||
}
|
||||
|
||||
console.log("\n--- Direct Tool Execution ---");
|
||||
const directTests: readonly ToolCall[] = [
|
||||
{ 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: "let s=0; for(let i=1;i<=100;i++) s+=i; result=s;" } },
|
||||
{ name: "run_code", arguments: { code: "require('child_process').exec('ls')" } },
|
||||
];
|
||||
for (const call of directTests) {
|
||||
const r = executeToolCall(call);
|
||||
const argsStr = JSON.stringify(call.arguments);
|
||||
const resStr = JSON.stringify(r.result).slice(0, 90);
|
||||
console.log("\n " + call.name + "(" + argsStr.slice(0, 60) + ")");
|
||||
console.log(" -> " + resStr);
|
||||
console.log(" time: " + r.executionTimeMs + "ms");
|
||||
}
|
||||
|
||||
console.log("\n--- Function Calling Loop ---");
|
||||
const queries = [
|
||||
"What's the weather in Tokyo?",
|
||||
"Calculate (100 + 250) * 0.15",
|
||||
"Search for MCP protocol",
|
||||
"Read the config file",
|
||||
"Run some JavaScript code",
|
||||
"Tell me a joke",
|
||||
];
|
||||
for (const q of queries) {
|
||||
const { toolResults, iterations } = runFunctionCallingLoop(q);
|
||||
console.log("\n User: " + q);
|
||||
for (const tr of toolResults) {
|
||||
console.log(" Tool: " + tr.tool + " (" + tr.executionTimeMs + "ms)");
|
||||
}
|
||||
if (toolResults.length === 0) console.log(" [No tool called]");
|
||||
console.log(" Iterations: " + iterations);
|
||||
}
|
||||
|
||||
console.log("\n--- Parallel Tool Calls ---");
|
||||
const { toolResults: multi } = runFunctionCallingLoop("What's the weather in tokyo and london?");
|
||||
console.log(" Tool calls made: " + multi.length);
|
||||
for (const tr of multi) {
|
||||
const r = tr.result as Record<string, JsonValue>;
|
||||
console.log(" " + String(r.city) + ": " + String(r.temp_c ?? r.temp_f) + ", " + String(r.condition));
|
||||
}
|
||||
|
||||
console.log("\n--- Security Checks ---");
|
||||
const securityTests: ReadonlyArray<{ tool: string; args: Record<string, JsonValue> }> = [
|
||||
{ tool: "read_file", args: { path: "../../etc/passwd" } },
|
||||
{ tool: "run_code", args: { code: "process.exit(0)" } },
|
||||
{ tool: "calculator", args: { expression: "Function('return 1')()" } },
|
||||
];
|
||||
for (const { tool, args } of securityTests) {
|
||||
const r = executeToolCall({ name: tool, arguments: args });
|
||||
const blocked = typeof r.result === "object" && r.result !== null && (r.result as Record<string, JsonValue>).error === true;
|
||||
const firstArg = Object.values(args)[0];
|
||||
const argDisplay = String(firstArg).slice(0, 40);
|
||||
console.log(" " + tool + "(" + argDisplay + "): " + (blocked ? "BLOCKED" : "ALLOWED"));
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user