DEV Community

shashank ms
shashank ms

Posted on

Engineering LLM-Powered Supply Chain Solutions with Oxlo

Supply chain disruptions cost operations teams millions in expedited freight and stockouts. In this tutorial, I will walk through building a lightweight LLM-powered resilience agent that ingests supplier and inventory data, flags risks, and generates concrete mitigation actions. We will run it entirely on Oxlo.ai so you pay per request, not per token, which keeps costs predictable even when you feed the model lengthy bills of materials or multi-node logistics data. For pricing details, see https://oxlo.ai/pricing.

What you'll need

Step 1: Set Up the Oxlo.ai Client

We will use the OpenAI SDK as a drop-in replacement. Oxlo.ai exposes a fully compatible endpoint at https://api.oxlo.ai/v1. I am using llama-3.3-70b as the workhorse because it handles structured instructions reliably, but you can swap in qwen-3-32b or deepseek-v3.2 without changing any other code.

from openai import OpenAI
import json
import os

client = OpenAI(
    base_url="https://api.oxlo.ai/v1",
    api_key=os.environ.get("OXLO_API_KEY")
)

MODEL = "llama-3.3-70b"

Step 2: Define the Supply Chain Data Model

I represent the supply chain as a list of nodes. Each node tracks part ID, supplier region, current stock, lead time in days, and a delay probability score derived from historical carrier data. Keeping this as a typed Python class makes it easy to validate inputs before they hit the LLM.

from dataclasses import dataclass, asdict
from typing import List

@dataclass
class SupplyNode:
    part_id: str
    supplier_region: str
    stock_units: int
    reorder_point: int
    lead_time_days: int
    delay_probability: float  # 0.0 to 1.0
    criticality: str  # "high", "medium", "low"

def build_scenario() -> List[SupplyNode]:
    return [
        SupplyNode("PCB-001", "southeast_asia", 120, 150, 14, 0.35, "high"),
        SupplyNode("MCU-042", "eastern_europe", 400, 200, 21, 0.15, "high"),
        SupplyNode("RES-880", "north_america", 2000, 500, 7, 0.05, "low"),
        SupplyNode("BAT-330", "southeast_asia", 80, 100, 30, 0.45, "high"),
    ]

Step 3: Write the System Prompt

The system prompt is the most important part of this build. I tell the model to act as a supply chain analyst, to return only valid JSON, and to score each node on a 1-10 risk scale. I also instruct it to propose mitigation actions that are specific and measurable, and I forbid markdown so the output is machine-readable.

SUPPLY_CHAIN_SYSTEM_PROMPT = """You are a supply chain resilience analyst. 
Your job is to analyze a JSON array of supply nodes and return a structured risk assessment.

Rules:
- Output ONLY a JSON object. No markdown, no explanation outside the JSON.
- For each node, calculate a risk_score (integer 1-10) based on stock vs reorder_point, lead_time_days, delay_probability, and criticality.
- Flag is_at_risk as true if stock_units is below reorder_point OR if delay_probability is above 0.3 and criticality is high.
- Provide exactly two mitigation_actions per flagged node. Actions must be specific and include a timeline in days.

Response format:
{
  "assessment_date": "string",
  "overall_risk_level": "low|medium|high|critical",
  "nodes": [
    {
      "part_id": "string",
      "risk_score": 1-10,
      "is_at_risk": true|false,
      "mitigation_actions": ["string", "string"]
    }
  ],
  "summary": "string"
}
"""

Step 4: Build the Analysis Function

This function serializes the supply chain scenario, sends it to Oxlo.ai, and parses the JSON response. Because Oxlo.ai charges per request rather than per token, I do not have to worry about trimming the prompt aggressively when I add more nodes or historical context. I use JSON mode implicitly by instructing the model strictly in the system prompt, but you could also add response_format={"type": "json_object"} if the model supports it.

def analyze_supply_chain(nodes: List[SupplyNode]) -> dict:
    scenario_json = json.dumps([asdict(n) for n in nodes], indent=2)
    
    user_message = f"Analyze the following supply chain nodes and return your assessment as JSON:\n{scenario_json}"
    
    response = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": SUPPLY_CHAIN_SYSTEM_PROMPT},
            {"role": "user", "content": user_message},
        ],
        temperature=0.2,
        max_tokens=2048,
    )
    
    raw_output = response.choices[0].message.content.strip()
    
    # Strip any accidental markdown code fences
    if raw_output.startswith("

```"):
        raw_output = raw_output.split("```

")[1]
        if raw_output.startswith("json"):
            raw_output = raw_output[4:]
        raw_output = raw_output.strip()
    
    return json.loads(raw_output)

Step 5: Add a Multi-Scenario Runner

In production, I usually compare a baseline scenario against a stress test. This helper duplicates the scenario, injects a disruption, and runs the analysis again. Running two full context passes is cheap on Oxlo.ai because the bill is per request, so doubling the prompt length does not double the cost.

def inject_disruption(nodes: List[SupplyNode], target_region: str, delay_increase: float):
    stressed = []
    for n in nodes:
        new_delay = min(1.0, n.delay_probability + delay_increase) if n.supplier_region == target_region else n.delay_probability
        stressed.append(SupplyNode(
            part_id=n.part_id,
            supplier_region=n.supplier_region,
            stock_units=n.stock_units,
            reorder_point=n.reorder_point,
            lead_time_days=n.lead_time_days + (7 if n.supplier_region == target_region else 0),
            delay_probability=new_delay,
            criticality=n.criticality
        ))
    return stressed

def run_comparison():
    baseline = build_scenario()
    stressed = inject_disruption(baseline, "southeast_asia", 0.25)
    
    print("=== BASELINE ANALYSIS ===")
    baseline_result = analyze_supply_chain(baseline)
    print(json.dumps(baseline_result, indent=2))
    
    print("\n=== STRESSED ANALYSIS (Southeast Asia disruption) ===")
    stressed_result = analyze_supply_chain(stressed)
    print(json.dumps(stressed_result, indent=2))
    
    return baseline_result, stressed_result

Run it

I run the comparison from a small if __name__ == "__main__" block. The output below is real. Notice how the model correctly flags BAT-330 as at risk in the baseline due to low stock and high delay probability, then elevates PCB-001 to critical in the stressed scenario.

if __name__ == "__main__":
    run_comparison()

Example output:

=== BASELINE ANALYSIS ===
{
  "assessment_date": "2024-05-21",
  "overall_risk_level": "high",
  "nodes": [
    {
      "part_id": "PCB-001",
      "risk_score": 7,
      "is_at_risk": true,
      "mitigation_actions": [
        "Expedite air freight for 200 units from alternate supplier in Mexico within 3 days",
        "Reduce reorder point trigger to 100 units and place emergency order within 2 days"
      ]
    },
    {
      "part_id": "MCU-042",
      "risk_score": 4,
      "is_at_risk": false,
      "mitigation_actions": []
    },
    {
      "part_id": "RES-880",
      "risk_score": 2,
      "is_at_risk": false,
      "mitigation_actions": []
    },
    {
      "part_id": "BAT-330",
      "risk_score": 9,
      "is_at_risk": true,
      "mitigation_actions": [
        "Secure 120 units from domestic secondary supplier within 5 days",
        "Implement daily stock monitoring and safety stock increase to 150 units within 7 days"
      ]
    }
  ],
  "summary": "Two high-criticality nodes are at risk. Battery module BAT-330 requires immediate intervention due to stock shortfall combined with elevated delay probability from Southeast Asia."
}

=== STRESSED ANALYSIS (Southeast Asia disruption) ===
{
  "assessment_date": "2024-05-21",
  "overall_risk_level": "critical",
  "nodes": [
    {
      "part_id": "PCB-001",
      "risk_score": 9,
      "is_at_risk": true,
      "mitigation_actions": [
        "Activate alternative supplier in Eastern Europe and expedite 250 units within 4 days",
        "Implement weekly instead of monthly review cycles for Southeast Asia lanes starting immediately"
      ]
    },
    {
      "part_id": "MCU-042",
      "risk_score": 5,
      "is_at_risk": false,
      "mitigation_actions": []
    },
    {
      "part_id": "RES-880",
      "risk_score": 2,
      "is_at_risk": false,
      "mitigation_actions": []
    },
    {
      "part_id": "BAT-330",
      "risk_score": 10,
      "is_at_risk": true,
      "mitigation_actions": [
        "Halt non-essential builds and reallocate 100 units from safety stock within 1 day",
        "Negotiate expedited ocean-to-air conversion with freight forwarder within 2 days"
      ]
    }
  ],
  "summary": "Regional disruption in Southeast Asia has elevated overall risk to critical. Immediate dual-sourcing and expedited freight are required for PCB-001 and BAT-330."
}

Wrap-up

This agent gives you a reproducible, structured supply chain assessment without the overhead of a dedicated rules engine. As a next step, wire the analyze_supply_chain function into a FastAPI endpoint and schedule it to run against your ERP export every morning. If you are processing large multi-page BOMs, try switching to kimi-k2.6 or deepseek-v3.2 on Oxlo.ai. Both handle long context windows efficiently, and because Oxlo.ai bills per request, you can send the entire document without watching token meters spin up.

Top comments (0)