من Function لـ Tool أوتوماتيك
هنستخدم اللي اتعلمناه في المرحلة ٢ عشان نبني نظام أدوات نضيف.
المشكلة
لو عندك ٢٠ أداة، كتابة الـ schema لكل واحدة بإيدك مملة ومصدر أخطاء: تغير parameter في الدالة وتنسى تغيره في الـ schema. الأحسن إن الـ schema تتولد من الدالة نفسها: الأنواع من الـ type hints، والوصف من الـ docstring.
هنكتب decorator (فاكرهم من المرحلة ٢؟) بيعمل ده، وبيسجل كل أداة في registry واحد:
import inspect import json TYPE_MAP = {int: "integer", float: "number", str: "string", bool: "boolean"} TOOLS = {} # name -> {"function", "schema"} def tool(func): """Register a function as an agent tool, building its schema automatically.""" props, required = {}, [] for name, param in inspect.signature(func).parameters.items(): props[name] = {"type": TYPE_MAP.get(param.annotation, "string")} if param.default is inspect.Parameter.empty: required.append(name) # no default -> required TOOLS[func.__name__] = { "function": func, "schema": { "name": func.__name__, "description": inspect.getdoc(func), "input_schema": {"type": "object", "properties": props, "required": required}, }, } return func # ---------------- our store's tools ---------------- ORDERS = {"1024": {"status": "out for delivery", "eta": "today before 9 PM", "total": 1850}, "1031": {"status": "delivered", "eta": None, "total": 640}} @tool def get_order_status(order_id: str) -> str: """Get the status and delivery estimate of an order by its 4-digit number. Use when the customer asks about a specific order.""" order = ORDERS.get(order_id) if order is None: raise ValueError(f"No order with number {order_id}. Ask the customer to double-check it.") return json.dumps(order) @tool def shipping_cost(governorate: str, order_total: float) -> str: """Calculate shipping cost in EGP for a governorate and order total. Orders above 1000 EGP ship free.""" if order_total > 1000: return "0" return "50" if governorate.lower() in ("cairo", "giza") else "75" if __name__ == "__main__": for t in TOOLS.values(): print(json.dumps(t["schema"], indent=2)) print("---")
كتبنا الدالة مرة واحدة، والـ schema اتولدت لوحدها. order_total بقت number لأنها float، والاتنين إجباريين لأن مفيش قيم افتراضية.
الـ Dispatcher: ينفذ أي أداة بالاسم
لما الموديل يطلب أداة، بيبعت اسمها ومدخلاتها. محتاجين دالة واحدة تنفذ أي أداة بأمان، وترجع النتيجة أو رسالة خطأ واضحة بدل ما البرنامج يقع:
from tools_registry import TOOLS def run_tool(name: str, tool_input: dict) -> tuple[str, bool]: """Execute a registered tool. Returns (content, is_error).""" if name not in TOOLS: return f"Unknown tool: {name}", True try: result = TOOLS[name]["function"](**tool_input) return str(result), False except TypeError as e: # wrong/missing arguments return f"Invalid arguments: {e}", True except Exception as e: # the tool itself failed return f"Error: {e}", True if __name__ == "__main__": # demo only when run directly print(run_tool("get_order_status", {"order_id": "1024"})) print(run_tool("get_order_status", {"order_id": "9999"})) print(run_tool("shipping_cost", {"governorate": "Aswan", "order_total": 640})) print(run_tool("shipping_cost", {"governorate": "Cairo"})) # missing argument print(run_tool("delete_everything", {}))
كل حالة فشل رجعت رسالة مفهومة مع True. الرسالة دي هتروح للموديل، وهو هيفهم إيه اللي حصل: يسأل العميل عن رقم الأوردر تاني، أو يكمل الـ parameter الناقص. الـ agent الكويس مش اللي مبيغلطش، ده اللي بيعرف يتصرف لما يغلط.