# 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="/files/Ouq621JslKsS7oNjuyYa" %}

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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://academy.pentaho.com/pentaho-data-integration/setup/use-cases/agent-as-a-service.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
