# Agent-as-a-Service

{% hint style="success" %}
This setup gives you a local Maintenance Assessment Agent API backed by Ollama.

At the end, you will have:

* A Python virtual environment with the required packages
* A local SQLite history database with sample asset records
* A FastAPI service running on port `8000`
* A local endpoint ready for use from PDI
  {% endhint %}

{% hint style="info" %}
This page includes commands for Windows and macOS/Linux.

On macOS/Linux, these examples use `/opt/agent-maintenance`.
{% endhint %}

**Architecture**

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

**Prerequisites**

<table><thead><tr><th width="215" valign="top">Component</th><th width="161" valign="top">Minimum version</th><th valign="top">Notes</th></tr></thead><tbody><tr><td valign="top">Python</td><td valign="top">3.10+</td><td valign="top">Python 3.11 or 3.12 is recommended.</td></tr><tr><td valign="top">pip</td><td valign="top">23+</td><td valign="top">Bundled with modern Python releases.</td></tr><tr><td valign="top">Ollama</td><td valign="top">0.1.30+</td><td valign="top">Must be available before starting the agent.</td></tr><tr><td valign="top">curl</td><td valign="top">Any</td><td valign="top">Used for quick verification.</td></tr><tr><td valign="top">Pentaho Data Integration</td><td valign="top">Optional</td><td valign="top">Needed only when you call the agent from PDI.</td></tr><tr><td valign="top">SQLite JDBC driver</td><td valign="top">3.51</td><td valign="top">Copy to /lib directory</td></tr></tbody></table>

{% stepper %}
{% step %}
**Create the project directory**

**Windows (PowerShell)**

```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**

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

{% endstep %}

{% step %}
**Create and activate the virtual environment**

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

**Windows (PowerShell)**

```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:

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

**macOS / Linux**

```bash
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:

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

{% hint style="info" %}
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.
{% endhint %}
{% endstep %}

{% step %}
**Create the asset history database**

{% hint style="danger" %}
Remember to copy the SQLite jdbc driver to: `../data-integration/lib`&#x20;
{% endhint %}

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`.

{% hint style="warning" %}
Run these commands from the project directory with the virtual environment active.
{% endhint %}

**Windows (PowerShell)**

Save this file as `scripts\create_db.py`:

```python
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:

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

**macOS / Linux**

```bash
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
```

{% endstep %}

{% step %}
**Create the Maintenance Log Database**

{% hint style="danger" %}
Remember to copy the SQLite jdbc driver to: `../data-integration/lib`&#x20;
{% endhint %}

The PDI transformation reads unprocessed log entries from maintenance\_log.db.

{% hint style="warning" %}
Run these commands from the project directory with the virtual environment active.
{% endhint %}

**Windows (Powershell)**

Save this file as `scripts\create_maintenance_db.py`:

```powershell
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:

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

**Linux / macOS**

```python
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
```

{% hint style="info" %}
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.

&#x20;

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.
{% endhint %}
{% endstep %}

{% step %}
**Pull the Ollama model**

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

```bash
ollama pull llama3.1:8b
```

{% hint style="info" %}
This is a \~4.7 GB download. Ensure Ollama is running before you pull the model.
{% endhint %}

**Windows**

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

**macOS / Linux**

```bash
ollama serve
```

Verify that the model is available:

```bash
ollama list
```

{% endstep %}

{% step %}
**Save the API service**

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

<table data-header-hidden><thead><tr><th width="213" valign="top">Variable</th><th width="287" valign="top">Default</th><th valign="top">Description</th></tr></thead><tbody><tr><td valign="top">AGENT_MODEL_URL</td><td valign="top">http://localhost:11434/api/generate</td><td valign="top">Ollama inference endpoint</td></tr><tr><td valign="top">AGENT_MODEL_NAME</td><td valign="top">llama3.1:8b</td><td valign="top">Model identifier</td></tr><tr><td valign="top">AGENT_TEMPERATURE</td><td valign="top">0.1</td><td valign="top">Low temperature for consistent structured output</td></tr><tr><td valign="top">AGENT_TIMEOUT</td><td valign="top">120</td><td valign="top">Seconds - history context increases prompt length</td></tr></tbody></table>

{% file src="<https://3680356391-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FZpCSy6Skj215f4oWypdc%2Fuploads%2F7BGVYishCFPQd9qdOF4F%2Fagent.py?alt=media&token=89313a80-7bc0-434b-85e4-6790c6c397f7>" %}

Optional session overrides:

**Windows (PowerShell)**

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

**macOS / Linux**

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

{% endstep %}

{% step %}
**Start the agent**

**Windows (PowerShell)**

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

```powershell
# Keep this terminal open
uvicorn agent.agent:app --host 0.0.0.0 --port 8000
```

**macOS / Linux**

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

```bash
# 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)
...
```

{% hint style="info" %}
Leave this terminal open while running the PDI pipeline. The agent process must remain running to serve assessment requests.
{% endhint %}
{% endstep %}

{% step %}
**Verify the setup**

Run the checks that match your platform.

**Windows (PowerShell)**

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

**macOS / Linux**

```bash
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`
  {% endstep %}

{% step %}
**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.
{% endstep %}
{% endstepper %}

**Troubleshooting**

<table><thead><tr><th width="273">Symptom</th><th>Resolution</th></tr></thead><tbody><tr><td><code>502 — LLM backend unavailable</code></td><td>Ollama is not running or is not reachable at <code>localhost:11434</code>. Start Ollama and confirm with <code>curl http://localhost:11434</code>.</td></tr><tr><td><code>500 — Assessment parse failed</code></td><td>The model returned malformed JSON. Retry the request. If the issue persists, try a larger model or lower <code>AGENT_TEMPERATURE</code>.</td></tr><tr><td><code>ModuleNotFoundError</code></td><td>The virtual environment is not activated. Run <code>.\agent-venv\Scripts\Activate.ps1</code> on Windows or <code>source agent-venv/bin/activate</code> on Linux.</td></tr><tr><td><code>PS execution policy error</code></td><td>Run once in PowerShell: <code>Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser</code></td></tr><tr><td><code>Port 8000 already in use</code></td><td>Change the port: <code>uvicorn agent.agent:app --port 8001</code> and update your PDI REST Client step accordingly.</td></tr><tr><td><code>Slow responses (>60s)</code></td><td>Increase <code>AGENT_TIMEOUT</code> or switch to a smaller quantised model such as <code>llama3.1:8b-instruct-q4_K_M</code>.</td></tr></tbody></table>
