From a3cddcc3a894dfe032c29a31129e3bf0f124aa55 Mon Sep 17 00:00:00 2001 From: noravth Date: Mon, 19 Oct 2026 23:12:21 +0200 Subject: [PATCH] tutorial prior labs agent --- .../agent-prior-labs.py | 190 ++++++++ .../ai-core-prior-labs-langgraph.md | 452 ++++++++++++++++++ .../ai-core-prior-labs-langgraph/env.example | 7 + .../payload_data.py | 112 +++++ 4 files changed, 761 insertions(+) create mode 100644 tutorials/ai-core-prior-labs-langgraph/agent-prior-labs.py create mode 100644 tutorials/ai-core-prior-labs-langgraph/ai-core-prior-labs-langgraph.md create mode 100644 tutorials/ai-core-prior-labs-langgraph/env.example create mode 100644 tutorials/ai-core-prior-labs-langgraph/payload_data.py diff --git a/tutorials/ai-core-prior-labs-langgraph/agent-prior-labs.py b/tutorials/ai-core-prior-labs-langgraph/agent-prior-labs.py new file mode 100644 index 000000000..0762f9a6a --- /dev/null +++ b/tutorials/ai-core-prior-labs-langgraph/agent-prior-labs.py @@ -0,0 +1,190 @@ +import json +import os +from pathlib import Path + +import pandas as pd +import requests +from dotenv import load_dotenv +from langchain.agents import create_agent +from langchain.tools import tool +from langchain_litellm import ChatLiteLLM + +from payload_data import payload, GROUND_TRUTH + +load_dotenv(dotenv_path=Path(__file__).parent / ".env") + +TABPFN_DEPLOYMENT_ID = os.environ["TABPFN_DEPLOYMENT_ID"] + +model = ChatLiteLLM(model=os.environ.get("LLM_MODEL", "sap/amazon--nova-lite"), temperature=0) + + +def _get_auth_token() -> str: + response = requests.post( + os.environ["AICORE_AUTH_URL"], + data={ + "grant_type": "client_credentials", + "client_id": os.environ["AICORE_CLIENT_ID"], + "client_secret": os.environ["AICORE_CLIENT_SECRET"], + }, + timeout=30, + ) + response.raise_for_status() + return response.json()["access_token"] + + +FEATURE_COLS = [ + "CATEGORY", "DISTANCE_KM", "WEIGHT_KG", "ORDER_VALUE", "PROMISED_DAYS", + "WAREHOUSE_HRS", "SHIP_METHOD", "CARRIER", "PKG_SIZE", "PRIORITY", "WEATHER", "SHIP_COST", +] + + +def call_tabpfn(payload: dict) -> str: + try: + token = _get_auth_token() + url = f"{os.environ['AICORE_BASE_URL'].rstrip('/')}/v2/inference/deployments/{TABPFN_DEPLOYMENT_ID}/predict" + headers = { + "Authorization": f"Bearer {token}", + "AI-Resource-Group": os.environ.get("AICORE_RESOURCE_GROUP", "default"), + "Content-Type": "application/json", + } + + rows = payload["rows"] + results: dict = {"predictions": {}} + + for target in payload["prediction_config"]["target_columns"]: + col_name = target["name"] + placeholder = target["prediction_placeholder"] + task_type = target["task_type"] + + # .get() handles classification-only test rows that have no DELAY_DAYS key + train_rows = [r for r in rows if r.get(col_name) is not None and r.get(col_name) != placeholder] + test_rows = [r for r in rows if r.get(col_name) == placeholder] + + if not test_rows: + results["predictions"][col_name] = [] + continue + + train_df = pd.DataFrame(train_rows) + test_df = pd.DataFrame(test_rows) + + x_train = {col: train_df[col].tolist() for col in FEATURE_COLS} + x_test = {col: test_df[col].tolist() for col in FEATURE_COLS} + + if task_type == "regression": + request_payload = { + "task_config": { + "task": "regression", + "predict_params": {"output_type": "mean"}, + }, + "x_train": x_train, + "y_train": train_df[col_name].astype(float).tolist(), + "x_test": x_test, + } + else: + request_payload = { + "task_config": { + "task": "classification", + "tabpfn_config": {"n_estimators": 8, "inference_precision": "auto"}, + "predict_params": {"output_type": "preds"}, + }, + "x_train": x_train, + "y_train": {col_name: train_df[col_name].tolist()}, + "x_test": x_test, + } + + print(f"\n--- TabPFN request [{col_name}] ---") + print(f"URL: {url}") + response = requests.post(url, headers=headers, json=request_payload, timeout=300) + print(f"HTTP {response.status_code}") + response.raise_for_status() + + predictions = response.json()["prediction"] + results["predictions"][col_name] = [ + {"ORD_ID": row["ORD_ID"], "SHIP_METHOD": row["SHIP_METHOD"], col_name: p} + for row, p in zip(test_rows, predictions) + ] + + return json.dumps(results, indent=2) + + except requests.RequestException as e: + return f"Error calling TabPFN: {str(e)}" + + +@tool +def run_tabpfn_predictions() -> str: + """Run TabPFN-3.5 Plus predictions for shipment delay days (DELAY_DAYS) and on-time status (LATE). + + Returns a JSON string with predictions for all test rows. + """ + return call_tabpfn(payload) + + +system_prompt = """You are a senior e-commerce logistics analyst. +You have received TabPFN-3.5 Plus predictions for two missing fields in a shipment dataset: +- DELAY_DAYS (integer): the predicted number of days an order arrives after its promised delivery date. +- LATE (Yes/No): whether each order is predicted to arrive after its promised delivery date. + +Your role is to summarise the predictions in a clear operations report: +- List the predicted delivery delay (in days) for each order, grouped by shipping method. +- List the predicted on-time / late status for each order. +- Highlight any patterns (e.g. which shipping methods, carriers, or weather conditions correlate with longer delays). + +You rely exclusively on the model predictions provided — never estimate or adjust values yourself.""" + + +forecast_agent = create_agent( + model=model, + tools=[run_tabpfn_predictions], + system_prompt=system_prompt, +) + + +def evaluate_predictions(predictions_json: str) -> None: + data = json.loads(predictions_json) + preds = data["predictions"] + + if "DELAY_DAYS" in preds: + errors = [] + for item in preds["DELAY_DAYS"]: + truth = GROUND_TRUTH.get(item["ORD_ID"], {}).get("DELAY_DAYS") + if truth is not None: + errors.append(abs(float(item["DELAY_DAYS"]) - float(truth))) + if errors: + print(f"\nMAE (DELAY_DAYS): {sum(errors) / len(errors):.2f} days") + + if "LATE" in preds: + correct = 0 + total = 0 + for item in preds["LATE"]: + truth = GROUND_TRUTH.get(item["ORD_ID"], {}).get("LATE") + if truth is not None: + total += 1 + if str(item["LATE"]).strip().lower() == str(truth).strip().lower(): + correct += 1 + if total: + print(f"Accuracy (LATE): {correct}/{total} = {correct / total:.0%}") + + +def main(): + result = forecast_agent.invoke( + {"messages": [{"role": "user", "content": "Run the TabPFN predictions and write a concise logistics operations summary."}]} + ) + + print("\n" + "=" * 50) + print("E-commerce Logistics Report:") + print("=" * 50) + print(result["messages"][-1].content) + + for msg in result["messages"]: + if hasattr(msg, "content") and isinstance(msg.content, str): + try: + data = json.loads(msg.content) + if "predictions" in data: + evaluate_predictions(msg.content) + break + except (json.JSONDecodeError, KeyError): + pass + + +if __name__ == "__main__": + main() diff --git a/tutorials/ai-core-prior-labs-langgraph/ai-core-prior-labs-langgraph.md b/tutorials/ai-core-prior-labs-langgraph/ai-core-prior-labs-langgraph.md new file mode 100644 index 000000000..1489d232e --- /dev/null +++ b/tutorials/ai-core-prior-labs-langgraph/ai-core-prior-labs-langgraph.md @@ -0,0 +1,452 @@ +--- +parser: v2 +auto_validation: true +time: 60 +tags: [ tutorial>intermediate, topic>artificial-intelligence, topic>machine-learning, software-product>sap-business-technology-platform, software-product>sap-ai-core ] +primary_tag: software-product>sap-ai-core +author_name: Nora von Thenen +author_profile: https://github.com/noravth +--- +# Add Prior Labs TabPFN-3.5 Plus on SAP AI Core to your AI Agent + In this tutorial you will learn how to build a LangChain agent that uses TabPFN-3.5 Plus to predict delivery delays and on-time delivery status for e-commerce shipments, and then generate an operations summary report using an LLM. TabPFN-3.5 Plus is a tabular foundation model by Prior Labs available through Generative AI Hub on SAP AI Core. + +## You will learn +- How to deploy TabPFN-3.5 Plus on SAP AI Core (Generative AI Hub) +- How to call the TabPFN `/predict` endpoint for regression and classification tasks +- How to build a LangChain agent that combines tabular ML predictions with an LLM + +## Prerequisites +SAP AI Core setup and basic knowledge: [Initial Setup](https://help.sap.com/docs/AI_CORE/2d6c5984063c40a59eda62f4a9135bee/38c4599432d74c1d94e70f7c955a717d.html) +SAP AI Core instance with **Extended Service Plan** (required for Generative AI Hub) Python 3.10 or higher installed. Basic familiarity with Python and REST APIs. + +--- + +### Architecture Overview + +The agent follows a simple three-step flow: + +1. The user sends a message asking for predictions and a summary report. +2. The LLM calls the `run_tabpfn_predictions` tool, which hits the TabPFN-3.5 Plus `/predict` endpoint on SAP AI Core — once for regression (`DELAY_DAYS`) and once for classification (`LATE`). +3. The tool result is returned to the LLM, which formats the predictions as a concise logistics operations report. + +> Note: TabPFN-3.5 Plus learns directly from the rows you provide — there is no separate training phase. Rows with known values are the context; rows with `[PREDICT]` placeholders are the test set. + +--- + +### Dataset + +This tutorial uses the **E-Commerce Delivery & Shipping Dataset 2026**, which contains 50,000 simulated e-commerce orders with logistics attributes such as shipping method, carrier, distance, package weight, warehouse processing time, weather conditions, and delivery outcome. + +You can download the dataset from Kaggle: [E-Commerce Delivery and Shipping Data 2026](https://www.kaggle.com/datasets/datascikhan/e-commerce-delivery-and-shipping-data-2026) + +The agent uses a representative sample of 50 training rows (every 1,000th order) plus 10 test rows drawn from that dataset. The two prediction targets are: + +| Column | Task | Description | +|---|---|---| +| `DELAY_DAYS` | Regression | Number of days an order arrives after its promised delivery date | +| `LATE` | Classification | Whether the order arrived late (`Yes` / `No`) | + +--- + +### Deploy TabPFN-3.5 Plus on SAP AI Core + +Before writing any code you need a running deployment of TabPFN-3.5 Plus in your SAP AI Core instance. + +Use the following values when creating the deployment: + +| Field | Value | +|---|---| +| Scenario | `foundation-models` | +| Executable ID | `prior-labs` | +| Model name | `tabpfn-3.5-plus` | + +For step-by-step deployment instructions see [Create a Deployment](https://help.sap.com/docs/sap-ai-core/generative-ai/create-deployment-for-generative-ai-model). + +Once the deployment status is **Running**, copy the deployment ID. You will need it in the next step. + +--- + +### Configure environment variables + +Create a file named `.env` in your project folder and add the following variables. Never commit this file! Add `.env` to your `.gitignore`. When deploying to Cloud Foundry you can simply bind those variables using [environmental variables](https://docs.cloudfoundry.org/devguide/deploy-apps/environment-variable.html). + +``` +AICORE_AUTH_URL=https://.authentication..hana.ondemand.com/oauth/token +AICORE_CLIENT_ID= +AICORE_CLIENT_SECRET= +AICORE_BASE_URL=https://api.ai..aws.ml.hana.ondemand.com +AICORE_RESOURCE_GROUP=default +TABPFN_DEPLOYMENT_ID= +LLM_MODEL=sap/amazon--nova-lite +``` + +You can find all SAP AI Core credentials in the SAP BTP cockpit under your SAP AI Core service instance's **Service Keys**. + +> Note: `LLM_MODEL` is optional. This agent defaults to `sap/amazon--nova-lite` if the variable is not set. + +--- + +### Install Python dependencies + +In your terminal/IDE of your choice: Create and activate a virtual environment, then install the required packages: + +``` +python -m venv .venv +source .venv/bin/activate # macOS / Linux +.venv\Scripts\activate # Windows +``` + +``` +pip install langchain langchain-litellm requests pandas python-dotenv +``` + +--- + +### Create the Python file + +Create a file named `agent-prior-labs.py` in your project folder. Start with the imports and top-level setup: + +```python +import json +import os +from pathlib import Path + +import pandas as pd +import requests +from dotenv import load_dotenv +from langchain.agents import create_agent +from langchain.tools import tool +from langchain_litellm import ChatLiteLLM + +load_dotenv(dotenv_path=Path(__file__).parent / ".env") + +TABPFN_DEPLOYMENT_ID = os.environ["TABPFN_DEPLOYMENT_ID"] + +model = ChatLiteLLM(model=os.environ.get("LLM_MODEL", "sap/amazon--nova-lite"), temperature=0) +``` + +A few things worth noting here: + +- `load_dotenv` uses an explicit path relative to the script file so it works regardless of which directory you run the script from. +- `TABPFN_DEPLOYMENT_ID` is read at import time so a missing variable fails immediately with a clear error, not silently later. +- `temperature=0` on the LLM makes the report reproducible, given the same TabPFN predictions, the LLM will always produce a similar summary. + +--- + +### Add the authentication helper + +SAP AI Core uses OAuth 2.0 client credentials. Add this helper function to your code to get your Bearer token: + +```python +def _get_auth_token() -> str: + response = requests.post( + os.environ["AICORE_AUTH_URL"], + data={ + "grant_type": "client_credentials", + "client_id": os.environ["AICORE_CLIENT_ID"], + "client_secret": os.environ["AICORE_CLIENT_SECRET"], + }, + timeout=30, + ) + response.raise_for_status() + return response.json()["access_token"] +``` + +`raise_for_status()` turns any 4xx or 5xx response into an exception immediately rather than letting the script fail later with a confusing JSON parse error. + +--- + +### Prepare the prediction data + +Calling TabPFN or SAP-RPT via Generative AI Hub work differently from most ML APIs: you do not need to train the model separately. Instead, you send everything in one request: rows with known labels teach the model on the fly (in context learning), and rows marked with a `[PREDICT]` placeholder become the test set (missing values to predict). + +All shipment data lives in a separate file. Download [payload_data.py](payload_data.py) and place it next to `agent-prior-labs.py`. In `agent-prior-labs.py`, import the two names you need: + +```python +from payload_data import payload, GROUND_TRUTH +``` + +`payload_data.py` contains four things: + +- `_REGRESSION_TEST_ROWS` — 5 orders whose `DELAY_DAYS` the model predicts +- `_CLASSIFICATION_TEST_ROWS` — 5 orders whose `LATE` label the model predicts +- `GROUND_TRUTH` — the known answers derived from those rows, used in evaluation +- `payload` — the full request body: 50 training rows plus the 10 test rows with `[PREDICT]` already substituted for the target column + +**Why define the test rows separately?** + +Because they serve double duty. The same row object is used twice: once to inject `[PREDICT]` into the payload so TabPFN treats it as a test row, and once to record the known label in `GROUND_TRUTH` so the evaluation step can compare against the real value. + +```python +_REGRESSION_TEST_ROWS = [ + # 5 rows: DELAY_DAYS is the target; one row per shipping method + {"ORD_ID": "ORD-016662", "CATEGORY": "Toys", "SHIP_METHOD": "Economy", "CARRIER": "BlueRoute", "DISTANCE_KM": 3119.4, "WEIGHT_KG": 0.60, "ORDER_VALUE": 73.38, "PROMISED_DAYS": 10, "WAREHOUSE_HRS": 8.9, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 58.36, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-016620", "CATEGORY": "Fashion", "SHIP_METHOD": "International", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 5404.7, "WEIGHT_KG": 0.49, "ORDER_VALUE": 216.28, "PROMISED_DAYS": 13, "WAREHOUSE_HRS": 17.1, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Cloudy", "SHIP_COST": 279.40, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-016613", "CATEGORY": "Pet Supplies", "SHIP_METHOD": "Express", "CARRIER": "SwiftShip", "DISTANCE_KM": 3465.5, "WEIGHT_KG": 7.72, "ORDER_VALUE": 11.17, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 3.5, "PKG_SIZE": "Oversized","PRIORITY": "Urgent", "WEATHER": "Snow", "SHIP_COST": 301.30, "DELAY_DAYS": 2, "LATE": "Yes"}, + {"ORD_ID": "ORD-016749", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "Standard", "CARRIER": "GlobalExpress", "DISTANCE_KM": 1322.0, "WEIGHT_KG": 0.60, "ORDER_VALUE": 10.61, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 4.0, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Rain", "SHIP_COST": 34.17, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-016869", "CATEGORY": "Electronics", "SHIP_METHOD": "Same Day", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 641.9, "WEIGHT_KG": 1.71, "ORDER_VALUE": 342.67, "PROMISED_DAYS": 1, "WAREHOUSE_HRS": 6.8, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Snow", "SHIP_COST": 71.77, "DELAY_DAYS": 0, "LATE": "No"}, +] +_CLASSIFICATION_TEST_ROWS = [ + # 5 rows: LATE is the target; 3×Yes / 2×No + {"ORD_ID": "ORD-000172", "CATEGORY": "Beauty", "SHIP_METHOD": "Express", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 4009.9, "WEIGHT_KG": 0.19, "ORDER_VALUE": 91.73, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 6.5, "PKG_SIZE": "Small", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 143.59, "LATE": "Yes"}, + {"ORD_ID": "ORD-000905", "CATEGORY": "Electronics", "SHIP_METHOD": "International", "CARRIER": "PrimeDelivery", "DISTANCE_KM": 4877.2, "WEIGHT_KG": 2.41, "ORDER_VALUE": 13.36, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 5.2, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Cloudy", "SHIP_COST": 383.59, "LATE": "Yes"}, + {"ORD_ID": "ORD-002136", "CATEGORY": "Electronics", "SHIP_METHOD": "International", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 1955.8, "WEIGHT_KG": 5.67, "ORDER_VALUE": 792.25, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 6.8, "PKG_SIZE": "Large", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 151.25, "LATE": "Yes"}, + {"ORD_ID": "ORD-000433", "CATEGORY": "Fashion", "SHIP_METHOD": "Express", "CARRIER": "PrimeDelivery", "DISTANCE_KM": 1182.6, "WEIGHT_KG": 1.46, "ORDER_VALUE": 20.56, "PROMISED_DAYS": 3, "WAREHOUSE_HRS": 12.7, "PKG_SIZE": "Medium", "PRIORITY": "Low", "WEATHER": "Clear", "SHIP_COST": 52.79, "LATE": "No"}, + {"ORD_ID": "ORD-001840", "CATEGORY": "Sports & Fitness","SHIP_METHOD": "Standard", "CARRIER": "EagleCourier", "DISTANCE_KM": 1151.8, "WEIGHT_KG": 0.10, "ORDER_VALUE": 10.85, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 20.4, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 25.12, "LATE": "No"}, +] + +GROUND_TRUTH = { + **{row["ORD_ID"]: {"DELAY_DAYS": row["DELAY_DAYS"]} for row in _REGRESSION_TEST_ROWS}, + **{row["ORD_ID"]: {"LATE": row["LATE"]} for row in _CLASSIFICATION_TEST_ROWS}, +} +``` + +`_REGRESSION_TEST_ROWS` are the test set for the `DELAY_DAYS` regression task. In the payload their `DELAY_DAYS` field is replaced with `"[PREDICT]"`, which tells TabPFN to predict it and excludes them from the regression training set. Because they still carry a known `LATE` value, they also contribute as additional training context for the `LATE` classification task. + +`_CLASSIFICATION_TEST_ROWS` are the test set for the `LATE` classification task. Their `LATE` field is replaced with `"[PREDICT]"` in the payload. They have no `DELAY_DAYS` key at all, so `call_tabpfn` will find `r.get("DELAY_DAYS") == None` and exclude them entirely from the regression task — they play no role there. + +**The payload structure** + +The payload in `payload_data.py` assembles all rows in one structure. The 50 training rows have every column filled in. The test rows are injected with a dict-spread that copies all fields and overrides only the target column: + +```python +payload = { + "prediction_config": { + # Rows where a target column equals prediction_placeholder become the + # test set; all other rows with a known value are training data. + "target_columns": [ + {"name": "DELAY_DAYS", "prediction_placeholder": "[PREDICT]", "task_type": "regression"}, + {"name": "LATE", "prediction_placeholder": "[PREDICT]", "task_type": "classification"}, + ] + }, + "rows": [ + # 50 training rows — all columns filled in + {"ORD_ID": "ORD-000001", ..., "DELAY_DAYS": 5, "LATE": "Yes"}, + # ... + # Test rows injected via dict-spread — only the target column is overridden + *[{**r, "DELAY_DAYS": "[PREDICT]"} for r in _REGRESSION_TEST_ROWS], + *[{**r, "LATE": "[PREDICT]"} for r in _CLASSIFICATION_TEST_ROWS], + ], +} +``` + +`{**r, "DELAY_DAYS": "[PREDICT]"}` copies every field from the row dict and overrides just the target. Because `_CLASSIFICATION_TEST_ROWS` has no `DELAY_DAYS` key at all, those rows are completely invisible to the regression task. + +--- + +### Build the TabPFN prediction function + +Now define which columns the model should use as input features. Both target columns — `DELAY_DAYS` and `LATE` — are deliberately excluded: + +```python +FEATURE_COLS = ["CATEGORY", "DISTANCE_KM", "WEIGHT_KG", "ORDER_VALUE", "PROMISED_DAYS", + "WAREHOUSE_HRS", "SHIP_METHOD", "CARRIER", "PKG_SIZE", "PRIORITY", "WEATHER", "SHIP_COST"] +``` + +`DELAY_DAYS` must not be a feature when predicting `LATE`, because the two are directly linked, a delay of 0 means on-time, any positive value means late. Giving the model `DELAY_DAYS` as input for the classification task would leak the answer. Likewise, `LATE` is excluded from the regression features. Only the operational shipment attributes are passed in. + +The core of the integration is `call_tabpfn`. It loops over each target column in `prediction_config`, splits the rows into training and test sets, reshapes the features into the format the API expects, and POSTs a separate request for each task: + +```python +def call_tabpfn(payload: dict) -> str: + try: + token = _get_auth_token() + url = f"{os.environ['AICORE_BASE_URL'].rstrip('/')}/v2/inference/deployments/{TABPFN_DEPLOYMENT_ID}/predict" + headers = { + "Authorization": f"Bearer {token}", + "AI-Resource-Group": os.environ.get("AICORE_RESOURCE_GROUP", "default"), + "Content-Type": "application/json", + } + + rows = payload["rows"] + results: dict = {"predictions": {}} + + for target in payload["prediction_config"]["target_columns"]: + col_name = target["name"] # e.g. "DELAY_DAYS" + placeholder = target["prediction_placeholder"] # "[PREDICT]" + task_type = target["task_type"] + + # Split: rows with a known value train the model; + # rows with "[PREDICT]" are what we want predicted. + # .get() handles classification-only test rows that have no DELAY_DAYS key. + train_rows = [r for r in rows if r.get(col_name) is not None and r.get(col_name) != placeholder] + test_rows = [r for r in rows if r.get(col_name) == placeholder] + + if not test_rows: + results["predictions"][col_name] = [] + continue + + train_df = pd.DataFrame(train_rows) + test_df = pd.DataFrame(test_rows) + + # The API expects features as a column-oriented dict: {"col": [val1, val2, ...]} + x_train = {col: train_df[col].tolist() for col in FEATURE_COLS} + x_test = {col: test_df[col].tolist() for col in FEATURE_COLS} + + if task_type == "regression": + request_payload = { + "task_config": { + "task": "regression", + "predict_params": {"output_type": "mean"}, + }, + "x_train": x_train, + "y_train": train_df[col_name].astype(float).tolist(), + "x_test": x_test, + } + else: + request_payload = { + "task_config": { + "task": "classification", + "tabpfn_config": {"n_estimators": 8, "inference_precision": "auto"}, + "predict_params": {"output_type": "preds"}, + }, + "x_train": x_train, + "y_train": {col_name: train_df[col_name].tolist()}, + "x_test": x_test, + } + + response = requests.post(url, headers=headers, json=request_payload, timeout=300) + response.raise_for_status() + + predictions = response.json()["prediction"] + results["predictions"][col_name] = [ + {"ORD_ID": row["ORD_ID"], "SHIP_METHOD": row["SHIP_METHOD"], col_name: p} + for row, p in zip(test_rows, predictions) + ] + + return json.dumps(results, indent=2) + + except requests.RequestException as e: + return f"Error calling TabPFN: {str(e)}" +``` + +Two things to notice about the request format: + +- For **regression**, `y_train` is a plain list of numbers — the model learns to predict a continuous value. +- For **classification**, `y_train` must be a dict keyed by the column name — this tells TabPFN which label column to learn from. + +The response always comes back as `{"prediction": [...]}` — a list of values in the same order as the test rows you sent. + +--- + +### Build the agent + +A LangChain agent is built from three things: a **language model** (which decides what to do next), **tools** (which the model can call), and a **system prompt** (which defines the model's role and behaviour). + +**Tool** + +Wrap `call_tabpfn` as a LangChain tool so the agent can call it. The `@tool` decorator makes the function available to the LLM, and the docstring becomes the description the LLM reads when deciding whether to call it: + +```python +@tool +def run_tabpfn_predictions() -> str: + """Run TabPFN-3.5 Plus predictions for shipment delay days (DELAY_DAYS) and on-time status (LATE). + + Returns a JSON string with predictions for all test rows. + """ + return call_tabpfn(payload) +``` + +**System prompt** + +Define the LLM's role before wiring the agent. The prompt anchors the LLM strictly to the predictions — it should report, not guess: + +```python +system_prompt = """You are a senior e-commerce logistics analyst. +You have received TabPFN-3.5 Plus predictions for two missing fields in a shipment dataset: +- DELAY_DAYS (integer): the predicted number of days an order arrives after its promised delivery date. +- LATE (Yes/No): whether each order is predicted to arrive after its promised delivery date. + +Your role is to summarise the predictions in a clear operations report: +- List the predicted delivery delay (in days) for each order, grouped by shipping method. +- List the predicted on-time / late status for each order. +- Highlight any patterns (e.g. which shipping methods, carriers, or weather conditions correlate with longer delays). + +You rely exclusively on the model predictions provided — never estimate or adjust values yourself.""" +``` + +**Agent** + +`create_agent` wires the LLM, tools, and system prompt together in one call: + +```python +forecast_agent = create_agent( + model=model, + tools=[run_tabpfn_predictions], + system_prompt=system_prompt, +) +``` + +**Main** + +Invoke the agent with a user message and print the results: + +```python +def main(): + result = forecast_agent.invoke( + {"messages": [{"role": "user", "content": "Run the TabPFN predictions and write a concise logistics operations summary."}]} + ) + + print("\n" + "=" * 50) + print("E-commerce Logistics Report:") + print("=" * 50) + print(result["messages"][-1].content) + + for msg in result["messages"]: + if hasattr(msg, "content") and isinstance(msg.content, str): + try: + data = json.loads(msg.content) + if "predictions" in data: + evaluate_predictions(msg.content) + break + except (json.JSONDecodeError, KeyError): + pass + +if __name__ == "__main__": + main() +``` + +--- + +### Run the agent + +Run the script from your project folder: + +``` +python agent-prior-labs.py +``` + +The agent prints the raw TabPFN request and response for each target column, followed by the final logistics report. The script also compares predictions against known ground-truth values at the end, printing MAE for `DELAY_DAYS` and accuracy for `LATE`. + +``` +--- TabPFN request [DELAY_DAYS] --- +URL: https://api.ai..aws.ml.hana.ondemand.com/v2/inference/deployments//predict +HTTP 200 +... + +--- TabPFN request [LATE] --- +HTTP 200 +... + +================================================== +E-commerce Logistics Report: +================================================== +DELIVERY DELAY PREDICTIONS BY SHIPPING METHOD + +Economy + ORD-016662 (BlueRoute, Rain): 1 day delay + +International + ORD-016620 (SpeedyCargo, Cloudy): 1 day delay + ... +``` + +> Note: Response times vary depending on the size of the context dataset. A payload of 60 rows typically completes within 10–60 seconds per target column. + +--- + +### Complete source code + +The complete agent script is available at [agent-prior-labs.py](../../agent-prior-labs.py). + +A template for all required environment variables is provided in [env.example](../../env.example). Copy it to `.env` and fill in your values before running the agent. diff --git a/tutorials/ai-core-prior-labs-langgraph/env.example b/tutorials/ai-core-prior-labs-langgraph/env.example new file mode 100644 index 000000000..26a3bf01f --- /dev/null +++ b/tutorials/ai-core-prior-labs-langgraph/env.example @@ -0,0 +1,7 @@ +AICORE_AUTH_URL="="#####"/oauth/token" +AICORE_CLIENT_ID="#####" +AICORE_CLIENT_SECRET="="#####"" +AICORE_RESOURCE_GROUP="="#####"" +AICORE_BASE_URL="="#####"" + +TABPFN_DEPLOYMENT_ID=d06cfe614d0d03c8 diff --git a/tutorials/ai-core-prior-labs-langgraph/payload_data.py b/tutorials/ai-core-prior-labs-langgraph/payload_data.py new file mode 100644 index 000000000..c90c79671 --- /dev/null +++ b/tutorials/ai-core-prior-labs-langgraph/payload_data.py @@ -0,0 +1,112 @@ +""" +Shipment data for the TabPFN prediction demo. + +Training rows come from the "E-Commerce Delivery and Shipping Data 2026" dataset +(https://www.kaggle.com/datasets/datascikhan/e-commerce-delivery-and-shipping-data-2026), +sampled at every 1,000th order. + +The test rows are held out from training and injected into `payload["rows"]` with +their target column replaced by "[PREDICT]" so TabPFN treats them as inference rows. +The un-modified versions stay here for ground-truth evaluation. +""" + +# --------------------------------------------------------------------------- +# Test rows — single source of truth for both payload injection and evaluation +# --------------------------------------------------------------------------- + +_REGRESSION_TEST_ROWS = [ + # 5 rows: DELAY_DAYS is the target; one row per shipping method + {"ORD_ID": "ORD-016662", "CATEGORY": "Toys", "SHIP_METHOD": "Economy", "CARRIER": "BlueRoute", "DISTANCE_KM": 3119.4, "WEIGHT_KG": 0.60, "ORDER_VALUE": 73.38, "PROMISED_DAYS": 10, "WAREHOUSE_HRS": 8.9, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 58.36, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-016620", "CATEGORY": "Fashion", "SHIP_METHOD": "International", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 5404.7, "WEIGHT_KG": 0.49, "ORDER_VALUE": 216.28, "PROMISED_DAYS": 13, "WAREHOUSE_HRS": 17.1, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Cloudy", "SHIP_COST": 279.40, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-016613", "CATEGORY": "Pet Supplies", "SHIP_METHOD": "Express", "CARRIER": "SwiftShip", "DISTANCE_KM": 3465.5, "WEIGHT_KG": 7.72, "ORDER_VALUE": 11.17, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 3.5, "PKG_SIZE": "Oversized", "PRIORITY": "Urgent", "WEATHER": "Snow", "SHIP_COST": 301.30, "DELAY_DAYS": 2, "LATE": "Yes"}, + {"ORD_ID": "ORD-016749", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "Standard", "CARRIER": "GlobalExpress", "DISTANCE_KM": 1322.0, "WEIGHT_KG": 0.60, "ORDER_VALUE": 10.61, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 4.0, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Rain", "SHIP_COST": 34.17, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-016869", "CATEGORY": "Electronics", "SHIP_METHOD": "Same Day", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 641.9, "WEIGHT_KG": 1.71, "ORDER_VALUE": 342.67, "PROMISED_DAYS": 1, "WAREHOUSE_HRS": 6.8, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Snow", "SHIP_COST": 71.77, "DELAY_DAYS": 0, "LATE": "No"}, +] + +_CLASSIFICATION_TEST_ROWS = [ + # 5 rows: LATE is the target; 3×Yes / 2×No + # Note: no DELAY_DAYS key — these rows play no role in the regression task + {"ORD_ID": "ORD-000172", "CATEGORY": "Beauty", "SHIP_METHOD": "Express", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 4009.9, "WEIGHT_KG": 0.19, "ORDER_VALUE": 91.73, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 6.5, "PKG_SIZE": "Small", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 143.59, "LATE": "Yes"}, + {"ORD_ID": "ORD-000905", "CATEGORY": "Electronics", "SHIP_METHOD": "International", "CARRIER": "PrimeDelivery", "DISTANCE_KM": 4877.2, "WEIGHT_KG": 2.41, "ORDER_VALUE": 13.36, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 5.2, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Cloudy", "SHIP_COST": 383.59, "LATE": "Yes"}, + {"ORD_ID": "ORD-002136", "CATEGORY": "Electronics", "SHIP_METHOD": "International", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 1955.8, "WEIGHT_KG": 5.67, "ORDER_VALUE": 792.25, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 6.8, "PKG_SIZE": "Large", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 151.25, "LATE": "Yes"}, + {"ORD_ID": "ORD-000433", "CATEGORY": "Fashion", "SHIP_METHOD": "Express", "CARRIER": "PrimeDelivery", "DISTANCE_KM": 1182.6, "WEIGHT_KG": 1.46, "ORDER_VALUE": 20.56, "PROMISED_DAYS": 3, "WAREHOUSE_HRS": 12.7, "PKG_SIZE": "Medium", "PRIORITY": "Low", "WEATHER": "Clear", "SHIP_COST": 52.79, "LATE": "No"}, + {"ORD_ID": "ORD-001840", "CATEGORY": "Sports & Fitness","SHIP_METHOD": "Standard", "CARRIER": "EagleCourier", "DISTANCE_KM": 1151.8, "WEIGHT_KG": 0.10, "ORDER_VALUE": 10.85, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 20.4, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 25.12, "LATE": "No"}, +] + +# Ground-truth values keyed by order ID — used in post-run evaluation +GROUND_TRUTH = { + **{row["ORD_ID"]: {"DELAY_DAYS": row["DELAY_DAYS"]} for row in _REGRESSION_TEST_ROWS}, + **{row["ORD_ID"]: {"LATE": row["LATE"]} for row in _CLASSIFICATION_TEST_ROWS}, +} + +# --------------------------------------------------------------------------- +# Full payload sent to TabPFN +# --------------------------------------------------------------------------- + +payload = { + "prediction_config": { + # Rows where a target column equals prediction_placeholder become the + # test set; all other rows with a known value for that column are training. + "target_columns": [ + {"name": "DELAY_DAYS", "prediction_placeholder": "[PREDICT]", "task_type": "regression"}, + {"name": "LATE", "prediction_placeholder": "[PREDICT]", "task_type": "classification"}, + ] + }, + "rows": [ + # --- 50 training rows (every 1,000th order from the dataset) --- + {"ORD_ID": "ORD-000001", "CATEGORY": "Home & Kitchen", "SHIP_METHOD": "International", "CARRIER": "EagleCourier", "DISTANCE_KM": 1579.5, "WEIGHT_KG": 8.23, "ORDER_VALUE": 10.39, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 25.1, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Snow", "SHIP_COST": 133.10, "DELAY_DAYS": 5, "LATE": "Yes"}, + {"ORD_ID": "ORD-001001", "CATEGORY": "Electronics", "SHIP_METHOD": "International", "CARRIER": "EagleCourier", "DISTANCE_KM": 3510.1, "WEIGHT_KG": 0.98, "ORDER_VALUE": 1576.38, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 26.1, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Snow", "SHIP_COST": 203.76, "DELAY_DAYS": 8, "LATE": "Yes"}, + {"ORD_ID": "ORD-002001", "CATEGORY": "Electronics", "SHIP_METHOD": "Standard", "CARRIER": "SwiftShip", "DISTANCE_KM": 355.7, "WEIGHT_KG": 1.86, "ORDER_VALUE": 41.15, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 20.7, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Snow", "SHIP_COST": 15.64, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-003001", "CATEGORY": "Toys", "SHIP_METHOD": "Express", "CARRIER": "SwiftShip", "DISTANCE_KM": 3826.6, "WEIGHT_KG": 0.30, "ORDER_VALUE": 10.00, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 3.5, "PKG_SIZE": "Small", "PRIORITY": "High", "WEATHER": "Storm", "SHIP_COST": 145.79, "DELAY_DAYS": 2, "LATE": "Yes"}, + {"ORD_ID": "ORD-004001", "CATEGORY": "Pet Supplies", "SHIP_METHOD": "International", "CARRIER": "GlobalExpress", "DISTANCE_KM": 3053.3, "WEIGHT_KG": 3.93, "ORDER_VALUE": 250.50, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 2.0, "PKG_SIZE": "Large", "PRIORITY": "High", "WEATHER": "Snow", "SHIP_COST": 250.86, "DELAY_DAYS": 4, "LATE": "Yes"}, + {"ORD_ID": "ORD-005001", "CATEGORY": "Beauty", "SHIP_METHOD": "International", "CARRIER": "GlobalExpress", "DISTANCE_KM": 5088.8, "WEIGHT_KG": 0.87, "ORDER_VALUE": 86.62, "PROMISED_DAYS": 13, "WAREHOUSE_HRS": 24.8, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 282.91, "DELAY_DAYS": 3, "LATE": "Yes"}, + {"ORD_ID": "ORD-006001", "CATEGORY": "Beauty", "SHIP_METHOD": "International", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 5201.5, "WEIGHT_KG": 0.10, "ORDER_VALUE": 78.26, "PROMISED_DAYS": 13, "WAREHOUSE_HRS": 8.3, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Storm", "SHIP_COST": 261.43, "DELAY_DAYS": 3, "LATE": "Yes"}, + {"ORD_ID": "ORD-007001", "CATEGORY": "Fashion", "SHIP_METHOD": "Express", "CARRIER": "EagleCourier", "DISTANCE_KM": 810.1, "WEIGHT_KG": 0.10, "ORDER_VALUE": 170.58, "PROMISED_DAYS": 2, "WAREHOUSE_HRS": 4.9, "PKG_SIZE": "Medium", "PRIORITY": "Urgent", "WEATHER": "Cloudy", "SHIP_COST": 42.38, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-008001", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "Express", "CARRIER": "EagleCourier", "DISTANCE_KM": 174.0, "WEIGHT_KG": 0.10, "ORDER_VALUE": 74.09, "PROMISED_DAYS": 2, "WAREHOUSE_HRS": 6.7, "PKG_SIZE": "Small", "PRIORITY": "Low", "WEATHER": "Rain", "SHIP_COST": 14.03, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-009001", "CATEGORY": "Books", "SHIP_METHOD": "Express", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 1672.1, "WEIGHT_KG": 0.10, "ORDER_VALUE": 12.95, "PROMISED_DAYS": 3, "WAREHOUSE_HRS": 4.1, "PKG_SIZE": "Small", "PRIORITY": "Urgent", "WEATHER": "Rain", "SHIP_COST": 56.73, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-010001", "CATEGORY": "Automotive", "SHIP_METHOD": "International", "CARRIER": "ParcelPro", "DISTANCE_KM": 1631.9, "WEIGHT_KG": 13.64, "ORDER_VALUE": 98.00, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 5.5, "PKG_SIZE": "Oversized","PRIORITY": "Urgent", "WEATHER": "Clear", "SHIP_COST": 192.76, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-011001", "CATEGORY": "Home & Kitchen", "SHIP_METHOD": "Standard", "CARRIER": "EagleCourier", "DISTANCE_KM": 5391.7, "WEIGHT_KG": 0.19, "ORDER_VALUE": 10.71, "PROMISED_DAYS": 8, "WAREHOUSE_HRS": 28.8, "PKG_SIZE": "Small", "PRIORITY": "Low", "WEATHER": "Snow", "SHIP_COST": 141.45, "DELAY_DAYS": 8, "LATE": "Yes"}, + {"ORD_ID": "ORD-012001", "CATEGORY": "Home & Kitchen", "SHIP_METHOD": "Express", "CARRIER": "GlobalExpress", "DISTANCE_KM": 58.4, "WEIGHT_KG": 0.10, "ORDER_VALUE": 139.96, "PROMISED_DAYS": 2, "WAREHOUSE_HRS": 4.8, "PKG_SIZE": "Small", "PRIORITY": "Urgent", "WEATHER": "Clear", "SHIP_COST": 8.69, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-013001", "CATEGORY": "Beauty", "SHIP_METHOD": "International", "CARRIER": "EagleCourier", "DISTANCE_KM": 2453.1, "WEIGHT_KG": 0.11, "ORDER_VALUE": 183.69, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 21.9, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Rain", "SHIP_COST": 108.76, "DELAY_DAYS": 3, "LATE": "Yes"}, + {"ORD_ID": "ORD-014001", "CATEGORY": "Grocery", "SHIP_METHOD": "International", "CARRIER": "ParcelPro", "DISTANCE_KM": 3582.0, "WEIGHT_KG": 5.32, "ORDER_VALUE": 113.87, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 47.1, "PKG_SIZE": "Large", "PRIORITY": "Low", "WEATHER": "Storm", "SHIP_COST": 297.40, "DELAY_DAYS": 9, "LATE": "Yes"}, + {"ORD_ID": "ORD-015001", "CATEGORY": "Pet Supplies", "SHIP_METHOD": "Standard", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 4079.6, "WEIGHT_KG": 1.26, "ORDER_VALUE": 159.90, "PROMISED_DAYS": 7, "WAREHOUSE_HRS": 8.6, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Clear", "SHIP_COST": 133.65, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-016001", "CATEGORY": "Electronics", "SHIP_METHOD": "Standard", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 4054.4, "WEIGHT_KG": 0.10, "ORDER_VALUE": 317.88, "PROMISED_DAYS": 7, "WAREHOUSE_HRS": 21.7, "PKG_SIZE": "Small", "PRIORITY": "Low", "WEATHER": "Rain", "SHIP_COST": 91.03, "DELAY_DAYS": 4, "LATE": "Yes"}, + {"ORD_ID": "ORD-017001", "CATEGORY": "Sports & Fitness", "SHIP_METHOD": "Economy", "CARRIER": "SwiftShip", "DISTANCE_KM": 4756.2, "WEIGHT_KG": 3.48, "ORDER_VALUE": 176.90, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 26.7, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 144.12, "DELAY_DAYS": 3, "LATE": "Yes"}, + {"ORD_ID": "ORD-018001", "CATEGORY": "Office Supplies", "SHIP_METHOD": "International", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 4391.8, "WEIGHT_KG": 1.18, "ORDER_VALUE": 34.75, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 9.6, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Cloudy", "SHIP_COST": 252.43, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-019001", "CATEGORY": "Automotive", "SHIP_METHOD": "International", "CARRIER": "EagleCourier", "DISTANCE_KM": 5260.6, "WEIGHT_KG": 15.80, "ORDER_VALUE": 157.47, "PROMISED_DAYS": 13, "WAREHOUSE_HRS": 23.3, "PKG_SIZE": "Oversized","PRIORITY": "Low", "WEATHER": "Clear", "SHIP_COST": 500.00, "DELAY_DAYS": 2, "LATE": "Yes"}, + {"ORD_ID": "ORD-020001", "CATEGORY": "Electronics", "SHIP_METHOD": "Standard", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 443.5, "WEIGHT_KG": 0.10, "ORDER_VALUE": 989.50, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 28.6, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 12.61, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-021001", "CATEGORY": "Fashion", "SHIP_METHOD": "Standard", "CARRIER": "ParcelPro", "DISTANCE_KM": 1523.6, "WEIGHT_KG": 0.47, "ORDER_VALUE": 87.84, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 40.4, "PKG_SIZE": "Small", "PRIORITY": "Low", "WEATHER": "Rain", "SHIP_COST": 34.15, "DELAY_DAYS": 2, "LATE": "Yes"}, + {"ORD_ID": "ORD-022001", "CATEGORY": "Books", "SHIP_METHOD": "Express", "CARRIER": "ParcelPro", "DISTANCE_KM": 3175.6, "WEIGHT_KG": 0.64, "ORDER_VALUE": 73.82, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 7.1, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Cloudy", "SHIP_COST": 123.14, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-023001", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "Express", "CARRIER": "ParcelPro", "DISTANCE_KM": 3286.7, "WEIGHT_KG": 0.88, "ORDER_VALUE": 30.20, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 3.9, "PKG_SIZE": "Medium", "PRIORITY": "Urgent", "WEATHER": "Clear", "SHIP_COST": 152.62, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-024001", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "International", "CARRIER": "ParcelPro", "DISTANCE_KM": 5251.0, "WEIGHT_KG": 0.68, "ORDER_VALUE": 28.16, "PROMISED_DAYS": 13, "WAREHOUSE_HRS": 26.8, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Rain", "SHIP_COST": 340.48, "DELAY_DAYS": 4, "LATE": "Yes"}, + {"ORD_ID": "ORD-025001", "CATEGORY": "Office Supplies", "SHIP_METHOD": "Standard", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 1680.4, "WEIGHT_KG": 1.31, "ORDER_VALUE": 58.98, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 9.6, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Clear", "SHIP_COST": 49.80, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-026001", "CATEGORY": "Automotive", "SHIP_METHOD": "International", "CARRIER": "EagleCourier", "DISTANCE_KM": 807.5, "WEIGHT_KG": 6.96, "ORDER_VALUE": 495.72, "PROMISED_DAYS": 10, "WAREHOUSE_HRS": 12.8, "PKG_SIZE": "Large", "PRIORITY": "Low", "WEATHER": "Snow", "SHIP_COST": 83.49, "DELAY_DAYS": 8, "LATE": "Yes"}, + {"ORD_ID": "ORD-027001", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "Standard", "CARRIER": "GlobalExpress", "DISTANCE_KM": 3027.0, "WEIGHT_KG": 0.82, "ORDER_VALUE": 44.59, "PROMISED_DAYS": 6, "WAREHOUSE_HRS": 10.1, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 90.02, "DELAY_DAYS": 2, "LATE": "Yes"}, + {"ORD_ID": "ORD-028001", "CATEGORY": "Home & Kitchen", "SHIP_METHOD": "Standard", "CARRIER": "SwiftShip", "DISTANCE_KM": 2356.8, "WEIGHT_KG": 0.59, "ORDER_VALUE": 680.08, "PROMISED_DAYS": 6, "WAREHOUSE_HRS": 21.9, "PKG_SIZE": "Medium", "PRIORITY": "Normal", "WEATHER": "Snow", "SHIP_COST": 62.27, "DELAY_DAYS": 3, "LATE": "Yes"}, + {"ORD_ID": "ORD-029001", "CATEGORY": "Home & Kitchen", "SHIP_METHOD": "Standard", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 711.0, "WEIGHT_KG": 3.14, "ORDER_VALUE": 156.11, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 20.1, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 34.51, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-030001", "CATEGORY": "Fashion", "SHIP_METHOD": "International", "CARRIER": "PrimeDelivery", "DISTANCE_KM": 3580.4, "WEIGHT_KG": 0.10, "ORDER_VALUE": 155.10, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 19.5, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Cloudy", "SHIP_COST": 154.82, "DELAY_DAYS": 2, "LATE": "Yes"}, + {"ORD_ID": "ORD-031001", "CATEGORY": "Toys", "SHIP_METHOD": "Express", "CARRIER": "ParcelPro", "DISTANCE_KM": 212.7, "WEIGHT_KG": 0.54, "ORDER_VALUE": 10.00, "PROMISED_DAYS": 2, "WAREHOUSE_HRS": 14.7, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 25.43, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-032001", "CATEGORY": "Books", "SHIP_METHOD": "Express", "CARRIER": "EagleCourier", "DISTANCE_KM": 1747.6, "WEIGHT_KG": 0.40, "ORDER_VALUE": 15.01, "PROMISED_DAYS": 3, "WAREHOUSE_HRS": 18.1, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Rain", "SHIP_COST": 57.12, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-033001", "CATEGORY": "Beauty", "SHIP_METHOD": "Economy", "CARRIER": "FastTrack Logistics", "DISTANCE_KM": 793.4, "WEIGHT_KG": 0.53, "ORDER_VALUE": 148.38, "PROMISED_DAYS": 8, "WAREHOUSE_HRS": 4.9, "PKG_SIZE": "Medium", "PRIORITY": "Urgent", "WEATHER": "Storm", "SHIP_COST": 19.92, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-034001", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "International", "CARRIER": "ParcelPro", "DISTANCE_KM": 3489.7, "WEIGHT_KG": 0.54, "ORDER_VALUE": 10.20, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 11.7, "PKG_SIZE": "Large", "PRIORITY": "High", "WEATHER": "Cloudy", "SHIP_COST": 287.52, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-035001", "CATEGORY": "Toys", "SHIP_METHOD": "Standard", "CARRIER": "GlobalExpress", "DISTANCE_KM": 45.5, "WEIGHT_KG": 0.77, "ORDER_VALUE": 59.88, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 2.9, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Clear", "SHIP_COST": 7.79, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-036001", "CATEGORY": "Sports & Fitness", "SHIP_METHOD": "International", "CARRIER": "GlobalExpress", "DISTANCE_KM": 1477.8, "WEIGHT_KG": 3.86, "ORDER_VALUE": 83.76, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 29.4, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Rain", "SHIP_COST": 104.84, "DELAY_DAYS": 3, "LATE": "Yes"}, + {"ORD_ID": "ORD-037001", "CATEGORY": "Home & Kitchen", "SHIP_METHOD": "Standard", "CARRIER": "SwiftShip", "DISTANCE_KM": 1793.4, "WEIGHT_KG": 4.36, "ORDER_VALUE": 131.64, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 16.3, "PKG_SIZE": "Large", "PRIORITY": "Low", "WEATHER": "Snow", "SHIP_COST": 66.88, "DELAY_DAYS": 3, "LATE": "Yes"}, + {"ORD_ID": "ORD-038001", "CATEGORY": "Electronics", "SHIP_METHOD": "International", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 1893.2, "WEIGHT_KG": 4.74, "ORDER_VALUE": 417.56, "PROMISED_DAYS": 11, "WAREHOUSE_HRS": 6.9, "PKG_SIZE": "Large", "PRIORITY": "High", "WEATHER": "Storm", "SHIP_COST": 136.36, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-039001", "CATEGORY": "Electronics", "SHIP_METHOD": "International", "CARRIER": "GlobalExpress", "DISTANCE_KM": 3421.5, "WEIGHT_KG": 0.10, "ORDER_VALUE": 739.15, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 19.4, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 172.76, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-040001", "CATEGORY": "Home & Kitchen", "SHIP_METHOD": "Economy", "CARRIER": "SpeedyCargo", "DISTANCE_KM": 1193.1, "WEIGHT_KG": 6.97, "ORDER_VALUE": 15.62, "PROMISED_DAYS": 9, "WAREHOUSE_HRS": 5.4, "PKG_SIZE": "Large", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 38.28, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-041001", "CATEGORY": "Pet Supplies", "SHIP_METHOD": "Standard", "CARRIER": "PrimeDelivery", "DISTANCE_KM": 6958.6, "WEIGHT_KG": 1.84, "ORDER_VALUE": 14.00, "PROMISED_DAYS": 8, "WAREHOUSE_HRS": 2.8, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 224.88, "DELAY_DAYS": 4, "LATE": "Yes"}, + {"ORD_ID": "ORD-042001", "CATEGORY": "Pet Supplies", "SHIP_METHOD": "Express", "CARRIER": "SwiftShip", "DISTANCE_KM": 3482.3, "WEIGHT_KG": 1.97, "ORDER_VALUE": 78.61, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 5.9, "PKG_SIZE": "Medium", "PRIORITY": "High", "WEATHER": "Cloudy", "SHIP_COST": 143.34, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-043001", "CATEGORY": "Electronics", "SHIP_METHOD": "Standard", "CARRIER": "ParcelPro", "DISTANCE_KM": 2778.9, "WEIGHT_KG": 5.17, "ORDER_VALUE": 334.67, "PROMISED_DAYS": 6, "WAREHOUSE_HRS": 10.8, "PKG_SIZE": "Large", "PRIORITY": "High", "WEATHER": "Cloudy", "SHIP_COST": 116.28, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-044001", "CATEGORY": "Health & Personal Care", "SHIP_METHOD": "Economy", "CARRIER": "GlobalExpress", "DISTANCE_KM": 759.1, "WEIGHT_KG": 0.99, "ORDER_VALUE": 38.88, "PROMISED_DAYS": 8, "WAREHOUSE_HRS": 5.5, "PKG_SIZE": "Large", "PRIORITY": "High", "WEATHER": "Rain", "SHIP_COST": 23.03, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-045001", "CATEGORY": "Fashion", "SHIP_METHOD": "International", "CARRIER": "PrimeDelivery", "DISTANCE_KM": 3948.6, "WEIGHT_KG": 0.58, "ORDER_VALUE": 74.75, "PROMISED_DAYS": 12, "WAREHOUSE_HRS": 11.1, "PKG_SIZE": "Large", "PRIORITY": "Low", "WEATHER": "Cloudy", "SHIP_COST": 325.55, "DELAY_DAYS": 4, "LATE": "Yes"}, + {"ORD_ID": "ORD-046001", "CATEGORY": "Fashion", "SHIP_METHOD": "Express", "CARRIER": "ParcelPro", "DISTANCE_KM": 5717.7, "WEIGHT_KG": 1.36, "ORDER_VALUE": 162.95, "PROMISED_DAYS": 5, "WAREHOUSE_HRS": 7.4, "PKG_SIZE": "Large", "PRIORITY": "Urgent", "WEATHER": "Rain", "SHIP_COST": 326.74, "DELAY_DAYS": 1, "LATE": "Yes"}, + {"ORD_ID": "ORD-047001", "CATEGORY": "Beauty", "SHIP_METHOD": "Standard", "CARRIER": "BlueRoute", "DISTANCE_KM": 179.1, "WEIGHT_KG": 0.37, "ORDER_VALUE": 67.76, "PROMISED_DAYS": 4, "WAREHOUSE_HRS": 21.8, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 9.13, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-048001", "CATEGORY": "Electronics", "SHIP_METHOD": "Economy", "CARRIER": "SwiftShip", "DISTANCE_KM": 121.5, "WEIGHT_KG": 0.10, "ORDER_VALUE": 1988.01, "PROMISED_DAYS": 8, "WAREHOUSE_HRS": 19.5, "PKG_SIZE": "Small", "PRIORITY": "Normal", "WEATHER": "Clear", "SHIP_COST": 6.68, "DELAY_DAYS": 0, "LATE": "No"}, + {"ORD_ID": "ORD-049001", "CATEGORY": "Toys", "SHIP_METHOD": "Economy", "CARRIER": "GlobalExpress", "DISTANCE_KM": 1352.5, "WEIGHT_KG": 0.55, "ORDER_VALUE": 93.86, "PROMISED_DAYS": 9, "WAREHOUSE_HRS": 5.3, "PKG_SIZE": "Medium", "PRIORITY": "Urgent", "WEATHER": "Clear", "SHIP_COST": 24.34, "DELAY_DAYS": 0, "LATE": "No"}, + # --- 5 test rows: predict DELAY_DAYS (one per shipping method) --- + *[{**r, "DELAY_DAYS": "[PREDICT]"} for r in _REGRESSION_TEST_ROWS], + # --- 5 test rows: predict LATE (3×Yes, 2×No) --- + *[{**r, "LATE": "[PREDICT]"} for r in _CLASSIFICATION_TEST_ROWS], + ], +}