For the complete documentation index, see llms.txt. This page is also available as Markdown.

Agent-as-a-Service

Set up a local maintenance assessment agent with FastAPI and Ollama.

This page includes commands for Windows and macOS/Linux.

On macOS/Linux, these examples use /opt/agent-maintenance.

Architecture

PDI → HTTP Client → FastAPI agent → Ollama → JSON response

Prerequisites

Component
Minimum version
Notes

Python

3.10+

Python 3.11 or 3.12 is recommended.

pip

23+

Bundled with modern Python releases.

Ollama

0.1.30+

Must be available before starting the agent.

curl

Any

Used for quick verification.

Pentaho Data Integration

Optional

Needed only when you call the agent from PDI.

SQLite JDBC driver

3.51

Copy to /lib directory

1

Create the project directory

Windows (PowerShell)

# Open PowerShell as a standard user (no elevation required)
cd $env:USERPROFILE
mkdir LLM-PDI-Integration\agent-maintenance
cd LLM-PDI-Integration\agent-maintenance
mkdir agent
mkdir data
mkdir scripts

macOS / Linux

sudo mkdir -p /opt/agent-maintenance
sudo chown $USER /opt/agent-maintenance
cd /opt/agent-maintenance
mkdir -p agent data scripts
2

Create and activate the virtual environment

Use a virtual environment to isolate the agent dependencies from system Python.

Windows (PowerShell)

cd $env:USERPROFILE\LLM-PDI-Integration\agent-maintenance

python -m venv agent-venv
.\agent-venv\Scripts\Activate.ps1

# If you see a script execution error, run this once:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

# Install dependencies
pip install fastapi uvicorn httpx pydantic

# Verify
python -c "import fastapi, uvicorn, httpx, pydantic; print('OK')"

To reactivate later:

cd $env:USERPROFILE\LLM-PDI-Integration\agent-maintenance
.\agent-venv\Scripts\Activate.ps1

macOS / Linux

cd /opt/agent-maintenance

python3 -m venv agent-venv
source agent-venv/bin/activate

pip install fastapi uvicorn httpx pydantic

# Verify
python -c "import fastapi, uvicorn, httpx, pydantic; print('OK')"

To reactivate later:

cd /opt/agent-maintenance
source agent-venv/bin/activate

The virtual environment must be activated in every new terminal session before running the agent. You will see (agent-venv) in your prompt when it is active.

3

Create the asset history database

The agent reads prior maintenance history from a local SQLite database. This step creates the database and loads sample records for PUMP-017, COMP-004, and VALVE-022.

Windows (PowerShell)

Save this file as scripts\create_db.py:

import os
import sqlite3

os.makedirs("data", exist_ok=True)
con = sqlite3.connect("data/asset_history.db")
con.execute("""CREATE TABLE IF NOT EXISTS asset_history (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    asset_id    TEXT NOT NULL,
    logged_at   TEXT NOT NULL,
    log_text    TEXT NOT NULL
)""")

history = [
    ("PUMP-017", "2025-09-12", "slight rumble on startup, clears after 2 minutes"),
    ("PUMP-017", "2025-11-03", "intermittent vibration under load, bearing checked ok"),
    ("PUMP-017", "2026-01-18", "bearing replaced, work order WO-4412"),
    ("COMP-004", "2025-10-05", "temperature running slightly high 82C, within tolerance"),
    ("COMP-004", "2025-12-14", "cooling fan filter cleaned, temperature normal"),
    ("COMP-004", "2026-02-28", "temperature normal, no issues"),
    ("VALVE-022", "2025-08-20", "valve serviced, seals replaced"),
    ("VALVE-022", "2026-01-09", "operating normally, no issues")
]

con.executemany(
    "INSERT INTO asset_history (asset_id, logged_at, log_text) VALUES (?, ?, ?)",
    history
)
con.commit()
con.close()
print("History database created.")

Run it:

cd $env:USERPROFILE\LLM-PDI-Integration\agent-maintenance
python scripts\create_db.py

macOS / Linux

python3 - << 'EOF'
import os
import sqlite3

os.makedirs("data", exist_ok=True)
con = sqlite3.connect("data/asset_history.db")
con.execute("""CREATE TABLE IF NOT EXISTS asset_history (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    asset_id    TEXT NOT NULL,
    logged_at   TEXT NOT NULL,
    log_text    TEXT NOT NULL
)""")

history = [
    ("PUMP-017", "2025-09-12", "slight rumble on startup, clears after 2 minutes"),
    ("PUMP-017", "2025-11-03", "intermittent vibration under load, bearing checked ok"),
    ("PUMP-017", "2026-01-18", "bearing replaced, work order WO-4412"),
    ("COMP-004", "2025-10-05", "temperature running slightly high 82C, within tolerance"),
    ("COMP-004", "2025-12-14", "cooling fan filter cleaned, temperature normal"),
    ("COMP-004", "2026-02-28", "temperature normal, no issues"),
    ("VALVE-022", "2025-08-20", "valve serviced, seals replaced"),
    ("VALVE-022", "2026-01-09", "operating normally, no issues"),
]

con.executemany(
    "INSERT INTO asset_history (asset_id, logged_at, log_text) VALUES (?, ?, ?)",
    history
)
con.commit()
con.close()
print("History database created.")
EOF
4

Create the Maintenance Log Database

The PDI transformation reads unprocessed log entries from maintenance_log.db.

Windows (Powershell)

Save this file as scripts\create_maintenance_db.py:

import sqlite3, os

os.makedirs("data", exist_ok=True)
con = sqlite3.connect("data/maintenance_log.db")

# maintenance_log: one row per engineer observation
con.execute('''CREATE TABLE IF NOT EXISTS maintenance_log (
    log_id      TEXT PRIMARY KEY,
    asset_id    TEXT NOT NULL,
    logged_at   TEXT NOT NULL,
    engineer    TEXT NOT NULL,
    log_text    TEXT NOT NULL,
    processed   INTEGER NOT NULL DEFAULT 0)''')

# assessed_log: populated by the PDI transformation
con.execute('''CREATE TABLE IF NOT EXISTS assessed_log (
    log_id           TEXT PRIMARY KEY,
    asset_id         TEXT NOT NULL,
    logged_at        TEXT,
    log_text         TEXT,
    priority         TEXT,
    fault_type       TEXT,
    pattern          TEXT,
    assessment       TEXT,
    confidence       INTEGER,
    parse_error      TEXT DEFAULT 'N',
    response_time_ms INTEGER,
    assessed_at      TEXT DEFAULT (datetime('now')))''')

# Five sample log entries across four assets
log_entries = [
    ("L-1001","PUMP-017","2026-04-07 08:14:00","J.Walsh",
     "Rougher than usual on startup, louder than before the January maintenance. Vibration settling after ~3 minutes.", 0),
    ("L-1002","COMP-004","2026-04-07 09:02:00","S.Okafor",
     "High temperature alarm triggered at 09:00. Reading: 94C. Limit is 85C. No prior warnings this shift.", 0),
    ("L-1003","FAN-011","2026-04-07 10:31:00","T.Marsh",
     "Fan running normally. Slight hum noted but within normal range. No action.", 0),
    ("L-1004","PUMP-017","2026-04-07 11:45:00","J.Walsh",
     "Vibration increased since this morning. Getting worse through the shift.", 0),
    ("L-1005","VALVE-022","2026-04-07 13:10:00","R.Nkosi",
     "Valve sticking on close. Takes 3-4 attempts. Never seen this before.", 0),
]
con.executemany(
    "INSERT OR IGNORE INTO maintenance_log (log_id,asset_id,logged_at,engineer,log_text,processed) VALUES (?,?,?,?,?,?)",
    log_entries)
con.commit()
con.close()
print("maintenance_log.db created:", len(log_entries), "entries.")

Run it:

cd $env:USERPROFILE\LLM-PDI-Integration\agent-maintenance
python scripts\create_maintenance_db.py

Linux / macOS

python3 - << 'EOF'
import sqlite3, os
os.makedirs("data", exist_ok=True)
con = sqlite3.connect("data/maintenance_log.db")

# maintenance_log: one row per engineer observation
con.execute('''CREATE TABLE IF NOT EXISTS maintenance_log (
    log_id      TEXT PRIMARY KEY,
    asset_id    TEXT NOT NULL,
    logged_at   TEXT NOT NULL,
    engineer    TEXT NOT NULL,
    log_text    TEXT NOT NULL,
    processed   INTEGER NOT NULL DEFAULT 0)''')

# assessed_log: populated by the PDI transformation
con.execute('''CREATE TABLE IF NOT EXISTS assessed_log (
    log_id           TEXT PRIMARY KEY,
    asset_id         TEXT NOT NULL,
    logged_at        TEXT,
    log_text         TEXT,
    priority         TEXT,
    fault_type       TEXT,
    pattern          TEXT,
    assessment       TEXT,
    confidence       INTEGER,
    parse_error      TEXT DEFAULT 'N',
    response_time_ms INTEGER,
    assessed_at      TEXT DEFAULT (datetime('now')))''')

# Five sample log entries across four assets
log_entries = [
    ("L-1001","PUMP-017","2026-04-07 08:14:00","J.Walsh",
     "Rougher than usual on startup, louder than before the January maintenance. Vibration settling after ~3 minutes.", 0),
    ("L-1002","COMP-004","2026-04-07 09:02:00","S.Okafor",
     "High temperature alarm triggered at 09:00. Reading: 94C. Limit is 85C. No prior warnings this shift.", 0),
    ("L-1003","FAN-011","2026-04-07 10:31:00","T.Marsh",
     "Fan running normally. Slight hum noted but within normal range. No action.", 0),
    ("L-1004","PUMP-017","2026-04-07 11:45:00","J.Walsh",
     "Vibration increased since this morning. Getting worse through the shift.", 0),
    ("L-1005","VALVE-022","2026-04-07 13:10:00","R.Nkosi",
     "Valve sticking on close. Takes 3-4 attempts. Never seen this before.", 0),
]
con.executemany(
    "INSERT OR IGNORE INTO maintenance_log (log_id,asset_id,logged_at,engineer,log_text,processed) VALUES (?,?,?,?,?,?)",
    log_entries)
con.commit(); con.close()
print("maintenance_log.db created:", len(log_entries), "entries.")
EOF

Both maintenance_log and assessed_log live in the same file: data/maintenance_log.db.

The PDI Table Input step reads from maintenance_log WHERE processed = 0.

The four Table Output steps write assessments to assessed_log.

Use a single Spoon connection (MAINTENANCE_DB) pointing to this file.

The asset_history table lives in a separate file: data/asset_history.db.

Use a second Spoon connection (HISTORY_DB) for the Database Join step.

Keeping them separate lets you reset the exercise by deleting

maintenance_log.db and re-running the seeder without touching history.

5

Pull the Ollama model

The agent defaults to llama3.1:8b. Pull the model before starting the service.

ollama pull llama3.1:8b

This is a ~4.7 GB download. Ensure Ollama is running before you pull the model.

Windows

Start the Ollama desktop app if it is not already running.

macOS / Linux

ollama serve

Verify that the model is available:

ollama list
6

Save the API service

Save the attached service file as agent/agent.py. The service exposes POST /assess.

AGENT_MODEL_URL

http://localhost:11434/api/generate

Ollama inference endpoint

AGENT_MODEL_NAME

llama3.1:8b

Model identifier

AGENT_TEMPERATURE

0.1

Low temperature for consistent structured output

AGENT_TIMEOUT

120

Seconds - history context increases prompt length

8KB
Open

Optional session overrides:

Windows (PowerShell)

$env:AGENT_MODEL_NAME = "llama3.1:8b"
$env:AGENT_TEMPERATURE = "0.1"
$env:AGENT_TIMEOUT = "120"

macOS / Linux

export AGENT_MODEL_NAME="llama3.1:8b"
export AGENT_TEMPERATURE="0.1"
export AGENT_TIMEOUT="120"
7

Start the agent

Windows (PowerShell)

cd $env:USERPROFILE\LLM-PDI-Integration\agent-maintenance
.\agent-venv\scripts\Activate.ps1
# Keep this terminal open
uvicorn agent.agent:app --host 0.0.0.0 --port 8000

macOS / Linux

cd /opt/agent-maintenance
source agent-venv/bin/activate
# Keep this terminal open
uvicorn agent.agent:app --host 0.0.0.0 --port 8000
Expected output:
INFO:     Started server process [xxxxx]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
...

Leave this terminal open while running the PDI pipeline. The agent process must remain running to serve assessment requests.

8

Verify the setup

Run the checks that match your platform.

Windows (PowerShell)

Test-Path .\data\asset_history.db
curl.exe http://localhost:11434
curl.exe http://localhost:8000/docs

macOS / Linux

test -f data/asset_history.db && echo "Database OK"
curl http://localhost:11434
curl http://localhost:8000/docs

Success means:

  • data/asset_history.db exists

  • Ollama responds on localhost:11434

  • The agent starts without import errors

  • FastAPI responds on port 8000

9

Use the agent from PDI

Use the HTTP Client step with:

  • URL: http://localhost:8000/assess

  • Method: POST

  • Content-Type: application/json

  • Response field: response_json

Use the FastAPI docs at http://localhost:8000/docs to inspect the request body and test the endpoint interactively.

Then parse response_json with JSON Input using the fields returned by your service.

Troubleshooting

Symptom
Resolution

502 — LLM backend unavailable

Ollama is not running or is not reachable at localhost:11434. Start Ollama and confirm with curl http://localhost:11434.

500 — Assessment parse failed

The model returned malformed JSON. Retry the request. If the issue persists, try a larger model or lower AGENT_TEMPERATURE.

ModuleNotFoundError

The virtual environment is not activated. Run .\agent-venv\Scripts\Activate.ps1 on Windows or source agent-venv/bin/activate on Linux.

PS execution policy error

Run once in PowerShell: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

Port 8000 already in use

Change the port: uvicorn agent.agent:app --port 8001 and update your PDI REST Client step accordingly.

Slow responses (>60s)

Increase AGENT_TIMEOUT or switch to a smaller quantised model such as llama3.1:8b-instruct-q4_K_M.

Last updated

Was this helpful?