{"id":450,"date":"2026-02-22T06:24:00","date_gmt":"2026-02-21T22:24:00","guid":{"rendered":"https:\/\/connectword.dpdns.org\/?p=450"},"modified":"2026-02-22T06:24:00","modified_gmt":"2026-02-21T22:24:00","slug":"how-to-design-an-agentic-workflow-for-tool-driven-route-optimization-with-deterministic-computation-and-structured-outputs","status":"publish","type":"post","link":"https:\/\/connectword.dpdns.org\/?p=450","title":{"rendered":"How to Design an Agentic Workflow for Tool-Driven Route Optimization with Deterministic Computation and Structured Outputs"},"content":{"rendered":"<p>In this tutorial, we build a production-style Route Optimizer Agent for a logistics dispatch center using the latest LangChain agent APIs. We design a tool-driven workflow in which the agent reliably computes distances, ETAs, and optimal routes rather than guessing, and we enforce structured outputs to make the results directly usable in downstream systems. We integrate geographic calculations, configurable speed profiles, traffic buffers, and multi-stop route optimization, ensuring the agent behaves deterministically while still reasoning flexibly through tools.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">!pip -q install -U langchain langchain-openai pydantic\n\n\nimport os\nfrom getpass import getpass\n\n\nif not os.environ.get(\"OPENAI_API_KEY\"):\n   os.environ[\"OPENAI_API_KEY\"] = getpass(\"Enter OPENAI_API_KEY (input hidden): \")\n\n\nfrom typing import Dict, List, Optional, Tuple, Any\nfrom math import radians, sin, cos, sqrt, atan2\n\n\nfrom pydantic import BaseModel, Field, ValidationError\n\n\nfrom langchain_openai import ChatOpenAI\nfrom langchain.tools import tool\nfrom langchain.agents import create_agent<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We set up the execution environment and ensure all required libraries are installed and imported correctly. We securely load the OpenAI API key so the agent can interact with the language model without hardcoding credentials. We also prepare the core dependencies that power tools, agents, and structured outputs.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">SITES: Dict[str, Dict[str, Any]] = {\n   \"Rig_A\": {\"lat\": 23.5880, \"lon\": 58.3829, \"type\": \"rig\"},\n   \"Rig_B\": {\"lat\": 23.6100, \"lon\": 58.5400, \"type\": \"rig\"},\n   \"Rig_C\": {\"lat\": 23.4500, \"lon\": 58.3000, \"type\": \"rig\"},\n   \"Yard_Main\": {\"lat\": 23.5700, \"lon\": 58.4100, \"type\": \"yard\"},\n   \"Depot_1\": {\"lat\": 23.5200, \"lon\": 58.4700, \"type\": \"depot\"},\n   \"Depot_2\": {\"lat\": 23.6400, \"lon\": 58.4300, \"type\": \"depot\"},\n}\n\n\nSPEED_PROFILES: Dict[str, float] = {\n   \"highway\": 90.0,\n   \"arterial\": 65.0,\n   \"local\": 45.0,\n}\n\n\nDEFAULT_TRAFFIC_MULTIPLIER = 1.10\n\n\ndef haversine_km(lat1: float, lon1: float, lat2: float, lon2: float) -&gt; float:\n   R = 6371.0\n   dlat = radians(lat2 - lat1)\n   dlon = radians(lon2 - lon1)\n   a = sin(dlat \/ 2) ** 2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon \/ 2) ** 2\n   return R * c<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We define the core domain data representing rigs, yards, and depots along with their geographic coordinates. We establish speed profiles and a default traffic multiplier to reflect realistic driving conditions. We also implement the Haversine distance function, which serves as the mathematical backbone of all routing decisions.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">def _normalize_site_name(name: str) -&gt; str:\n   return name.strip()\n\n\ndef _assert_site_exists(name: str) -&gt; None:\n   if name not in SITES:\n       raise ValueError(f\"Unknown site '{name}'. Use list_sites() or suggest_site().\")\n\n\ndef _distance_between(a: str, b: str) -&gt; float:\n   _assert_site_exists(a)\n   _assert_site_exists(b)\n   sa, sb = SITES[a], SITES[b]\n   return float(haversine_km(sa[\"lat\"], sa[\"lon\"], sb[\"lat\"], sb[\"lon\"]))\n\n\ndef _eta_minutes(distance_km: float, speed_kmph: float, traffic_multiplier: float) -&gt; float:\n   speed = max(float(speed_kmph), 1e-6)\n   base_minutes = (distance_km \/ speed) * 60.0\n   return float(base_minutes * max(float(traffic_multiplier), 0.0))\n\n\ndef compute_route_metrics(path: List[str], speed_kmph: float, traffic_multiplier: float) -&gt; Dict[str, Any]:\n   if len(path) &lt; 2:\n       raise ValueError(\"Route path must include at least origin and destination.\")\n   for s in path:\n       _assert_site_exists(s)\n   legs = []\n   total_km = 0.0\n   total_min = 0.0\n   for i in range(len(path) - 1):\n       a, b = path[i], path[i + 1]\n       d_km = _distance_between(a, b)\n       t_min = _eta_minutes(d_km, speed_kmph, traffic_multiplier)\n       legs.append({\"from\": a, \"to\": b, \"distance_km\": d_km, \"eta_minutes\": t_min})\n       total_km += d_km\n       total_min += t_min\n   return {\"route\": path, \"distance_km\": float(total_km), \"eta_minutes\": float(total_min), \"legs\": legs}<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We build the low-level utility functions that validate site names and compute distances and travel times. We implement logic to calculate per-leg and total route metrics deterministically. This ensures that every ETA and distance returned by the agent is based on explicit computation rather than inference.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">def _all_paths_with_waypoints(origin: str, destination: str, waypoints: List[str], max_stops: int) -&gt; List[List[str]]:\n   from itertools import permutations\n   waypoints = [w for w in waypoints if w not in (origin, destination)]\n   max_stops = int(max(0, max_stops))\n   candidates = []\n   for k in range(0, min(len(waypoints), max_stops) + 1):\n       for perm in permutations(waypoints, k):\n           candidates.append([origin, *perm, destination])\n   if [origin, destination] not in candidates:\n       candidates.insert(0, [origin, destination])\n   return candidates\n\n\ndef find_best_route(origin: str, destination: str, allowed_waypoints: Optional[List[str]], max_stops: int, speed_kmph: float, traffic_multiplier: float, objective: str, top_k: int) -&gt; Dict[str, Any]:\n   origin = _normalize_site_name(origin)\n   destination = _normalize_site_name(destination)\n   _assert_site_exists(origin)\n   _assert_site_exists(destination)\n   allowed_waypoints = allowed_waypoints or []\n   for w in allowed_waypoints:\n       _assert_site_exists(_normalize_site_name(w))\n   objective = (objective or \"eta\").strip().lower()\n   if objective not in {\"eta\", \"distance\"}:\n       raise ValueError(\"objective must be one of: 'eta', 'distance'\")\n   top_k = max(1, int(top_k))\n   candidates = _all_paths_with_waypoints(origin, destination, allowed_waypoints, max_stops=max_stops)\n   scored = []\n   for path in candidates:\n       metrics = compute_route_metrics(path, speed_kmph=speed_kmph, traffic_multiplier=traffic_multiplier)\n       score = metrics[\"eta_minutes\"] if objective == \"eta\" else metrics[\"distance_km\"]\n       scored.append((score, metrics))\n   scored.sort(key=lambda x: x[0])\n   best = scored[0][1]\n   alternatives = [m for _, m in scored[1:top_k]]\n   return {\"best\": best, \"alternatives\": alternatives, \"objective\": objective}<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We introduce multi-stop routing logic by generating candidate paths with optional waypoints. We evaluate each candidate route against a clear optimization objective, such as ETA or distance. We then rank routes and extract the best option along with a set of strong alternatives.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">@tool\ndef list_sites(site_type: Optional[str] = None) -&gt; List[str]:\n   if site_type:\n       st = site_type.strip().lower()\n       return sorted([k for k, v in SITES.items() if str(v.get(\"type\", \"\")).lower() == st])\n   return sorted(SITES.keys())\n\n\n@tool\ndef get_site_details(site: str) -&gt; Dict[str, Any]:\n   s = _normalize_site_name(site)\n   _assert_site_exists(s)\n   return {\"site\": s, **SITES[s]}\n\n\n@tool\ndef suggest_site(query: str, max_suggestions: int = 5) -&gt; List[str]:\n   q = (query or \"\").strip().lower()\n   max_suggestions = max(1, int(max_suggestions))\n   scored = []\n   for name in SITES.keys():\n       n = name.lower()\n       common = len(set(q) &amp; set(n))\n       bonus = 5 if q and q in n else 0\n       scored.append((common + bonus, name))\n   scored.sort(key=lambda x: x[0], reverse=True)\n   return [name for _, name in scored[:max_suggestions]]\n\n\n@tool\ndef compute_direct_route(origin: str, destination: str, road_class: str = \"arterial\", traffic_multiplier: float = DEFAULT_TRAFFIC_MULTIPLIER) -&gt; Dict[str, Any]:\n   origin = _normalize_site_name(origin)\n   destination = _normalize_site_name(destination)\n   rc = (road_class or \"arterial\").strip().lower()\n   if rc not in SPEED_PROFILES:\n       raise ValueError(f\"Unknown road_class '{road_class}'. Use one of: {sorted(SPEED_PROFILES.keys())}\")\n   speed = SPEED_PROFILES[rc]\n   return compute_route_metrics([origin, destination], speed_kmph=speed, traffic_multiplier=float(traffic_multiplier))\n\n\n@tool\ndef optimize_route(origin: str, destination: str, allowed_waypoints: Optional[List[str]] = None, max_stops: int = 2, road_class: str = \"arterial\", traffic_multiplier: float = DEFAULT_TRAFFIC_MULTIPLIER, objective: str = \"eta\", top_k: int = 3) -&gt; Dict[str, Any]:\n   origin = _normalize_site_name(origin)\n   destination = _normalize_site_name(destination)\n   rc = (road_class or \"arterial\").strip().lower()\n   if rc not in SPEED_PROFILES:\n       raise ValueError(f\"Unknown road_class '{road_class}'. Use one of: {sorted(SPEED_PROFILES.keys())}\")\n   speed = SPEED_PROFILES[rc]\n   allowed_waypoints = allowed_waypoints or []\n   allowed_waypoints = [_normalize_site_name(w) for w in allowed_waypoints]\n   return find_best_route(origin, destination, allowed_waypoints, int(max_stops), float(speed), float(traffic_multiplier), str(objective), int(top_k))<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We expose the routing and discovery logic as callable tools for the agent. We allow the agent to list sites, inspect site details, resolve ambiguous names, and compute both direct and optimized routes. This tool layer ensures that the agent always reasons by calling verified functions rather than hallucinating results.<\/p>\n<div class=\"dm-code-snippet dark dm-normal-version default no-background-mobile\">\n<div class=\"control-language\">\n<div class=\"dm-buttons\">\n<div class=\"dm-buttons-left\">\n<div class=\"dm-button-snippet red-button\"><\/div>\n<div class=\"dm-button-snippet orange-button\"><\/div>\n<div class=\"dm-button-snippet green-button\"><\/div>\n<\/div>\n<div class=\"dm-buttons-right\"><a><span class=\"dm-copy-text\">Copy Code<\/span><span class=\"dm-copy-confirmed\">Copied<\/span><span class=\"dm-error-message\">Use a different Browser<\/span><\/a><\/div>\n<\/div>\n<pre class=\" no-line-numbers\"><code class=\" no-wrap language-php\">class RouteLeg(BaseModel):\n   from_site: str\n   to_site: str\n   distance_km: float\n   eta_minutes: float\n\n\nclass RoutePlan(BaseModel):\n   route: List[str]\n   distance_km: float\n   eta_minutes: float\n   legs: List[RouteLeg]\n   objective: str\n\n\nclass RouteDecision(BaseModel):\n   chosen: RoutePlan\n   alternatives: List[RoutePlan] = []\n   assumptions: Dict[str, Any] = {}\n   notes: str = \"\"\n   audit: List[str] = []\n\n\nllm = ChatOpenAI(model=\"gpt-4o-mini\", temperature=0.2)\n\n\nSYSTEM_PROMPT = (\n   \"You are the Route Optimizer Agent for a logistics dispatch center.n\"\n   \"You MUST use tools for any distance\/ETA calculation.n\"\n   \"Return ONLY the structured RouteDecision.\"\n)\n\n\nroute_agent = create_agent(\n   model=llm,\n   tools=[list_sites, get_site_details, suggest_site, compute_direct_route, optimize_route],\n   system_prompt=SYSTEM_PROMPT,\n   response_format=RouteDecision,\n)\n\n\ndef get_route_decision(origin: str, destination: str, road_class: str = \"arterial\", traffic_multiplier: float = DEFAULT_TRAFFIC_MULTIPLIER, allowed_waypoints: Optional[List[str]] = None, max_stops: int = 2, objective: str = \"eta\", top_k: int = 3) -&gt; RouteDecision:\n   user_msg = {\n       \"role\": \"user\",\n       \"content\": (\n           f\"Optimize the route from {origin} to {destination}.n\"\n           f\"road_class={road_class}, traffic_multiplier={traffic_multiplier}n\"\n           f\"objective={objective}, top_k={top_k}n\"\n           f\"allowed_waypoints={allowed_waypoints}, max_stops={max_stops}n\"\n           \"Return the structured RouteDecision only.\"\n       ),\n   }\n   result = route_agent.invoke({\"messages\": [user_msg]})\n   return result[\"structured_response\"]\n\n\ndecision1 = get_route_decision(\"Yard_Main\", \"Rig_B\", road_class=\"arterial\", traffic_multiplier=1.12)\nprint(decision1.model_dump())\n\n\ndecision2 = get_route_decision(\"Rig_C\", \"Rig_B\", road_class=\"highway\", traffic_multiplier=1.08, allowed_waypoints=[\"Depot_1\", \"Depot_2\", \"Yard_Main\"], max_stops=2, objective=\"eta\", top_k=3)\nprint(decision2.model_dump())<\/code><\/pre>\n<\/div>\n<\/div>\n<p>We define strict Pydantic schemas to enforce structured, machine-readable outputs from the agent. We initialize the language model and create the agent with a clear system prompt and response format. We then demonstrate how to invoke the agent and obtain reliable route decisions ready for real logistics workflows.<\/p>\n<p>In conclusion, we have implemented a robust, extensible route optimization agent that selects the best path between sites while clearly explaining its assumptions and alternatives. We demonstrated how combining deterministic routing logic with a tool-calling LLM produces reliable, auditable decisions suitable for real logistics operations. This foundation allows us to easily extend the system with live traffic data, fleet constraints, or cost-based objectives, making the agent a practical component in a larger dispatch or fleet-management platform.<\/p>\n<hr class=\"wp-block-separator has-alpha-channel-opacity\" \/>\n<p>Check out the\u00a0<strong><a href=\"https:\/\/github.com\/Marktechpost\/AI-Tutorial-Codes-Included\/blob\/main\/Agentic%20Workflows\/agentic_workflow_tool_driven_route_optimization_Marktechpost.ipynb\" target=\"_blank\" rel=\"noreferrer noopener\">Full Codes here<\/a>.\u00a0<\/strong>Also,\u00a0feel free to follow us on\u00a0<strong><a href=\"https:\/\/x.com\/intent\/follow?screen_name=marktechpost\" target=\"_blank\" rel=\"noreferrer noopener\"><mark>Twitter<\/mark><\/a><\/strong>\u00a0and don\u2019t forget to join our\u00a0<strong><a href=\"https:\/\/www.reddit.com\/r\/machinelearningnews\/\" target=\"_blank\" rel=\"noreferrer noopener\">100k+ ML SubReddit<\/a><\/strong>\u00a0and Subscribe to\u00a0<strong><a href=\"https:\/\/www.aidevsignals.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">our Newsletter<\/a><\/strong>. Wait! are you on telegram?\u00a0<strong><a href=\"https:\/\/t.me\/machinelearningresearchnews\" target=\"_blank\" rel=\"noreferrer noopener\">now you can join us on telegram as well.<\/a><\/strong><\/p>\n<p>The post <a href=\"https:\/\/www.marktechpost.com\/2026\/02\/21\/how-to-design-an-agentic-workflow-for-tool-driven-route-optimization-with-deterministic-computation-and-structured-outputs\/\">How to Design an Agentic Workflow for Tool-Driven Route Optimization with Deterministic Computation and Structured Outputs<\/a> appeared first on <a href=\"https:\/\/www.marktechpost.com\/\">MarkTechPost<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>In this tutorial, we build a p&hellip;<\/p>\n","protected":false},"author":1,"featured_media":29,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-450","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized"],"_links":{"self":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/450","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=450"}],"version-history":[{"count":0,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/posts\/450\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=\/wp\/v2\/media\/29"}],"wp:attachment":[{"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=450"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=450"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/connectword.dpdns.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=450"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}