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

Agent-as-a-Service

Pattern reasoning across unstructured data ..

Overview

The test for whether something is an agent is simple: can PDI replicate the behaviour by adding more steps to the canvas?

Here's summary of the different approaches:

Direct LLM Call / Multi-Stage
LangExtract
Agent as a Service

What it does

Sends one prompt per row, returns structured JSON for that row

Extracts named entities with char offsets from a single text; returns one row per entity

Reads current entry plus retrieved history, reasons across all texts, returns one assessment

Input to the service

Single text field from the PDI row

Single text field + prompt + few-shot examples

Current log text + N history entries (retrieved by PDI before calling the agent)

Output shape

One row in, one enriched row out

One row in, N entity rows out (one per extraction); pivoted by Row Denormaliser

One row in, one assessment row out

LLM calls per row

1 (or N fixed stages)

1 per extraction pass (typically 2 passes over chunked text)

1 - reads all texts together in a single context

Requires history from other records?

No - processes each row in isolation

No - processes each document in isolation

Yes - history retrieved by PDI via Database Join is essential to the assessment

Decision grounded in

LLM inference on current text only

LLM extraction from current text only; char offsets trace back to source

LLM reasoning across current text AND verified historical records from the database

Can PDI replicate it?

Yes - multi-stage MJV + REST Client pattern

Partially - regex/rules cover known entity formats; LangExtract handles novel/variable ones

No - cross-text pattern reasoning is not encodable as PDI steps

Use when

Classifying, enriching, or summarising individual records

Extracting typed fields from free-form text where regex rules are too brittle

Assessment depends on what previous records say, not just the current one

maintenance assessment agent

Start the agent

Windows (PowerShell)

macOS / Linux

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

Verify the setup

Run the checks that match your platform.

Windows (PowerShell)

macOS / Linux

Success means:

  • data/asset_history.db exists

  • Ollama responds on localhost:11434

  • The agent starts without import errors

  • FastAPI responds on port 8000

Boundary Argument

PDI handles the complexity of structured data with remarkable flexibility. There are steps that mimic the actions of an AI agent - RegEx, Fuzzy matching, Rule Executor and so on. Together these provide a powerful framework that provides similarity but not semantic equivalence - inference - on unstructured data.

Task
PDI Step(s)
Capability
Verdict

Extract structured asset ID (e.g. ASSET-0042)

Regex Evaluation

Extracts fixed-format patterns reliably

PDI handles this

Look up asset history rows by asset_id

Database Lookup Database Join

Exact key match, returns all history rows

PDI handles this

Classify fault type from known keywords ("bearing", "temperature")

Rule Executor (Drools) Regex Evaluation + MJV

IF/THEN rules on keyword presence

PDI handles known, enumerated faults

Fuzzy-match current description against a known fault library

Fuzzy Match step

Levenshtein / Jaro-Winkler on string pairs

PDI handles surface similarity; not semantic equivalence

Assign final priority once fault type is known

Filter Rows Switch/Case MJV

Pure deterministic routing on field values

PDI handles this completely

Why Regex fails

"bearing noise getting worse" and "intermittent vibration on startup since maintenance" both indicate bearing degradation. They share no common n-gram, no common keyword sequence, and no pattern a regex can match. The semantic equivalence exists at the meaning level, not the surface level. Regex operates only on surface form.

Why Rule Executor (Drools) fails

A rule can fire on the keyword "bearing" in the current entry. But the rule cannot say: "this entry, combined with two previous entries that mention vibration and one bearing replacement 18 months ago, indicates recurrence of the same root cause rather than a new fault." That conclusion requires reading and integrating four separate natural-language texts. A Drools rule operates on field values in the current row - it does not read and synthesise multiple text fields from a joined history set.

You could try to flatten the history into a single concatenated field and write a rule against that. But the rule would still operate on keyword presence, not meaning. "No unusual noise but slight vibration" contains the word "vibration" - a keyword rule would flag it identically to "severe vibration on every startup". The negation and qualifier ("slight", "no unusual") change the meaning completely. Rules cannot parse that.

Why Fuzzy Match fails

The Fuzzy Match step finds strings with high character-level similarity. "Bearing noise" and "audible roughness in drive shaft" have low character similarity but identical diagnostic meaning. Conversely, "bearing temperature normal" and "bearing temperature elevated" have very high character similarity but opposite meanings. Fuzzy matching on maintenance log text produces both false positives and false negatives at an unacceptable rate for safety-relevant prioritisation.

Why MJV (JavaScript) fails

MJV can implement any algorithm expressible in JavaScript. A skilled developer could write a JavaScript function that: (1) concatenates history entries, (2) counts keyword occurrences, (3) applies weighted scoring. This is a rules system with extra steps. It fails on the same cases rules fail on - novel descriptions, combined symptoms, qualified negations, and cross-entry pattern inference. It does not understand language; it manipulates strings.


Given these four history entries for asset PUMP-017:

The correct assessment is: RECURRENCE - bearing degradation has restarted 10 weeks post-replacement, which is abnormally fast and indicates either incorrect installation or an underlying cause not addressed in the January maintenance. Priority: HIGH.

No PDI step produces this assessment.

The Database Lookup returns the four history rows as data. MJV can concatenate them. No step can read them together and conclude that the current entry matches the pre-January pattern and that pattern recurrence within 10 weeks of a replacement is abnormal.

Pipeline Overview

The scenario uses one LLM call - and that is intentional. Here, PDI already knows the asset_id and retrieves the history as structured rows before calling the agent.

The agent receives everything it needs in one payload. One call is cleaner, faster, and easier to reason about. The single call is also the hardest part - reading multiple texts and synthesising a cross-entry assessment is more genuinely difficult than two sequential single-task calls.

PDI Transformation
Maintenance Assessment Agent

Owns

All data flow, retrieval, routing, and persistence

Language understanding and cross-text reasoning

Reads

Structured rows from maintenance_log and asset_history tables

Unstructured text: current log entry + N history entries as text

Produces

Structured rows with priority, fault_type, assessment fields

A single structured JSON assessment per request

Uses

Regex, Database Join, MJV, REST Client, Switch/Case

One LLM call with history context; no tool calls needed

Can be replaced by rules?

Yes - all PDI logic is deterministic

No - language reasoning is not encodable as rules


Let's run some tests:

Windows (Powershell)

Linux / macOS

Response:

The agent is working correctly:

  • priority: HIGH - the model recognises this needs same-day attention, which is correct given the history

  • fault_type: intermittent_vibration_under_load - picked up the dominant symptom from the history

  • pattern: RECURRENCE - correctly identified that this has happened before (pre-January bearing issues)

  • assessment - the model has read the history and connected the dots: bearing was replaced in January and symptoms are returning

  • confidence: 80 - reasonably high, consistent with a clear pattern in the history

Maintenance Agent

The maintenance_assessment.ktr has all the steps in a single pipeline to illustrate the workflow. The MJV approach works for this workshop because the dataset is small and runs single-copy. In production with thousands of rows and STEP_COPIES > 1, the accumulator pattern would break and you would need the sub-transformation approach instead.

So, a cleaner alternative that avoids the MJV accumulator entirely is to use a sub-transformation via Transformation Executor. For each log entry, a child transformation queries the history database and builds the JSON array, returning one row with the complete payload. This is stateless, parallelism-safe, and easier to reason about.

Sample Maintenance Logs

Asset History


Transformation design

Step Name
Type
What PDI does here

Read Log Entries

Table Input

SELECT unprocessed rows from maintenance_log

Retrieve Asset History

Database Join

LEFT JOIN asset_history ON asset_id, concat history rows into JSON array field

Sort rows

Sort rows

Keeps the rows in chronological order

Remove duplicates

Unique rows

Just in case ..

Aggregate History

Modified JavaScript Value

PDI stream is row based, need to aggregate all the records for asset.

Group by

Build Agent Payload

Modified JavaScript Value

JSON.stringify the full request including history array

Call Assessment Agent

REST Client

POST ${AGENT_URL}/assess — socket timeout 180000ms

Check HTTP Status

Filter Rows

Route non-200 to error log

Parse Agent Response

Modified JavaScript Value

Extract priority, fault_type, pattern, assessment, confidence

Route by Priority

Switch / Case

Fan out on priority field

Write CRITICAL

Table Output

assessed_log with alert flag set

Write HIGH

Table Output

assessed_log

Write MEDIUM

Table Output

assessed_log

Write LOW

Table Output

assessed_log

Write Errors

Text File Output

HTTP failures for investigation

This section walks through building maintenance_assessment.ktr step by step in Spoon. Every dialog tab, field name, and script is given in full. Build the steps in order — each one depends on output fields from the previous.

Step 1: Set transformation properties

Double-click anywhere on the empty canvas to open Transformation Properties. Click the Parameters tab.

Parameter
Default Value
Description

AGENT_URL

http://localhost:8000

Base URL of the assessment agent

DB_CONNECTION

path to / maintenance_log

Set path to maintenance_log.db

STEP_COPIES

2

Parallel REST Client copies

HISTORY_LIMIT

10

Max history rows per asset

Add each parameter using the + button. Enter the parameter name in the first column and the default value in the third column (Default value).

Click OK to save. Parameters are accessible as ${PARAM_NAME} in step configuration fields and via getVariable("PARAM_NAME","default") in MJV scripts.

Step 2: Create a database connection

The Table Input and Database Join steps both need a named connection to the SQLite database. Create it once and both steps will share it.

1. View > Connections > New

2. Connection name: maintenance_log_db

3. Connection type: SQLite

4. Database name (file path): ../data/maintenance_log.db

5. Click Test — should return "Connection to database [MAINTENANCE_DB] is OK"

6. Click OK

SQLite stores the entire database in a single file. The Database Join step will query asset_history from the same file if you ATTACH it, or you can create a second connection named HISTORY_DB pointing to data/asset_history.db.

For this workshop, use separate connection objects for clarity.

Step 3: Table Input: Read Log Entries

Add the step

  1. In the Design pane, expand the Input category

  2. Drag Table Input onto the canvas

  3. Double-click the step to open configuration

Configuration

Field
Value

Step name

Read Log Entries

Connection

maintenanace_log_db

SQL

See query below

Enable lazy conversion

No (unchecked)

Replace variables

Yes (checked) - required for ${HISTORY_LIMIT}

SQL query

The processed = 0 filter ensures each log entry is only assessed once.

After a successful run, update processed = 1 in a downstream Table Output

step or via a post-processing Execute SQL Script step in a wrapping Job.

Step 4: Database Join: Retrieve Asset History

The Database Join step executes a parameterised SQL query once per input row, using the asset_id field from the stream as the ? parameter.

It appends all matching history rows to the stream - one output row per history entry. The subsequent MJV step then aggregates those rows back into a single JSON array field.

Configuration

Field
Value

Step name

Get Asset History

Connection

asset_history_db

SQL

See query below

Outer join

Yes (checked) - ensures assets with no history still produce a row

Number of rows to return

${HISTORY_LIMIT} (resolves to 10)

Use variable substitution

Yes (checked)

SQL query

Parameters tab

Click the Parameters tab. Add one row to bind the ? placeholder to the stream field:

Field (from stream)
Type

asset_id

String

The Database Join step produces one output row per history entry matched.

For PUMP-017 (3 history rows), it produces 3 output rows — all carrying the original log entry fields (log_id, asset_id, log_text, etc.).

Step (Aggregate History) collapses those back to one row per log entry.

Step 5: Aggregate History into JSON Array

The Database Join expanded each log entry into N rows (one per history entry). This MJV step runs after a Group By step to collapse them back into one row per log entry, with the history encoded as a JSON array string in a new field history_json.

Sort rows

Sort on two fields:

Field name
Ascending
Case sensitive

log_id

Yes

No

hist_logged_at

Yes

No

Modified JavaScript Value

Step name: Aggregate History

Copy & Paste the following script into the script editor:

Add output fields in the Fields tab

In the Fields tab at the bottom of the MJV dialog, click Get variables and then add:

Fieldname
Type
Notes

history_json

String

JSON array of history objects, or "[]" if no history

Group By step to keep only the last row per log_id

Group field:

Group by field
Notes

log_id

Primary key — one output row per log entry

asset_id

Carry-through field

logged_at

Carry-through field

engineer

Carry-through field

log_text

Carry-through field

Aggregation (to retain the last history_json value per group):

Name
Subject
Type

history_json

history_json

Last non-null value

The LAST aggregation type returns the last non-NULL value for history_json within the group. Because the MJV accumulates entries into the buffer as rows arrive, and Sort Rows guarantees chronological order within each log_id, the LAST row for each log_id carries the complete history array.

Step 6: MJV: Build Agent Payload

Step name: Build Agent Payload

Fields tab - output fields

Fieldname
Type
Notes

agent_payload

String

Complete JSON payload for POST /assess

Verify the payload

Before connecting the REST Client, preview the output of this step and verify the agent_payload field contains valid JSON with the correct structure:

Step 7: REST Client: Call the Assessment Agent

Step name: Call Assessment Agent

General tab

Field
Value

HTTP method

POST

URL

${AGENT_URL}/assess

Body field

agent_payload

Application type

TEXT PLAIN

Result field name

agent_response

HTTP status code field

response_code

Response time field

response_time_ms

Headers: (leave empty - REST Client auto-adds Content-Type)

The actual timeout behaviour is controlled at the JVM level via kettle.properties, not per-step.

Step 8: Filter Rows: Check HTTP Status

Step name: Check HTTP Status

Condition

Click inside the condition area and build the following filter:

response_code = 200

The Write HTTP Errors step should capture: log_id, asset_id, response_code,

agent_payload (so you can see what was sent), and response_time_ms.

Common non-200 codes:

422 = malformed payload

502 = Ollama not running

500 = agent parse failure (LLM returned malformed JSON after retries).

Output hops

Right-click the Filter Rows step. You need two output hops:

TRUE path → draw a hop to the next step (Parse Agent Response)

FALSE path → draw a hop to a Text File Output step named Write HTTP Errors

Step 8: MJV: Parse Agent Response

Step name: Parse Agent Response

Fields tab - output fields

Fieldname
Type
Notes

priority

String

CRITICAL | HIGH | MEDIUM | LOW

fault_type

String

Short snake_case label from agent

pattern

String

RECURRENCE | ESCALATION | NEW_FAULT | NORMAL_VARIATION

assessment

String

One-to-two sentence explanation

confidence

Integer

0–100

parse_error

String

Y or N

parse_error_msg

String

Exception message if parse_error = Y

Step 9: Switch / Case: Route by Priority

Add four Table Output steps (Output category), one for each priority path. They all write to the same assessed_log table but can be separate tables if your schema requires it. Connect each from the corresponding Switch / Case output hop.

x

x

RUN Transformation

Step 1: Start the agent

Windows (PowerShell)

macOS / Linux

Step 2: RUN

  1. Select Run options — leave defaults

  2. Click Run

Check logs

The Preview panel opens with the Logging tab active. Watch for:

  • Read Log Entries: should show 5 rows read (one per sample log entry)

  • Get Asset History: row count will be higher than 5 — this is expected (one row per history match)

  • Group By: should collapse back to 5 rows

  • Call Assessment Agent: 5 rows sent; watch response_time_ms

  • Route by Priority: rows distributed across CRITICAL (1), HIGH (1), MEDIUM (2), LOW (1)

CRITICAL

Field
Value

log_id

L-1002

asset_id

COMP-004

logged_at

2026-04-07 09:02:00

engineer

S.Okafor

log_text

High temperature alarm triggered at 09:00. Reading: 94C. Limit is 85C. No prior warnings this shift.

history_json

[{"logged_at":"2025-10-05","log_text":"temperature running slightly high 82C, within tolerance"},{"logged_at":"2025-12-14","log_text":"cooling fan filter cleaned, temperature normal"},{"logged_at":"2026-02-28","log_text":"temperature normal, no issues"}]

agent_payload

{"log_id":"L-1002","asset_id":"COMP-004","log_text":"High temperature alarm triggered...","history":[...3 entries...]}

agent_response

{"log_id":"L-1002","asset_id":"COMP-004","priority":"CRITICAL","fault_type":"high_temperature_alarm","pattern":"RECURRENCE","assessment":"High temperature alarm triggered at 09:00 with a reading of 94C, exceeding the limit of 85C.","confidence":100}

response_code

200

response_time_ms

5094

priority

CRITICAL

fault_type

high_temperature_alarm

pattern

RECURRENCE

assessment

High temperature alarm triggered at 09:00 with a reading of 94C, exceeding the limit of 85C.

confidence

100

parse_error

N

parse_error_msg

(blank)

log_id
priority
fault_type
pattern

L-1001

HIGH

bearing_degradation / vibration_recurrence

RECURRENCE

L-1002

CRITICAL

high_temperature_alarm

RECURRENCE

L-1003

LOW

normal_variation

NORMAL_VARIATION

L-1004

CRITICAL

increased_vibration / vibration_escalation

ESCALATION

L-1005

MEDIUM

valve_sticking

NEW_FAULT

KEY POINTS

1. The boundary is precise and provable.

PDI steps handle extraction, lookup, rules, and routing.

No PDI step reads multiple unstructured text entries together and

reasons about whether they collectively indicate a known pattern.

That is the exact and only task the agent owns.

2. PDI does more work than the agent in this pipeline.

Regex extracts the asset ID. Database Join retrieves the history.

MJV builds the payload. Switch/Case routes the result.

The agent does one thing: language reasoning across text.

3. One LLM call, grounded in retrieved data, is the clean pattern.

The history context is retrieved by PDI as structured rows,

formatted by MJV, and passed to the agent in a single payload.

The agent does not need tools because PDI already did the lookup.

4. Confidence scores surface model uncertainty for human review.

Entries with low history or ambiguous text should route to a

review queue regardless of the priority label.

5. The test for any agent pattern is: can PDI replicate it?

If yes with steps — use steps. Faster, simpler, deterministic.

If no — that is where the agent earns its place.

Performance

History context increases prompt length compared to earlier workshops. A typical prompt for this workshop is 300–600 tokens (system instructions + up to 10 history entries + current entry). At llama3.1:8b on the 3080 12GB, expect 4–8 seconds per assessment.

Configuration
Step Copies
5 entries
Notes

llama3.1:8b, RTX 3080 12GB, no history

1

~15-25 sec

~3-5 sec/call, short prompt

llama3.1:8b, RTX 3080 12GB, 5 history entries

1

~25-40 sec

~5-8 sec/call, longer prompt

llama3.1:8b, RTX 3080 12GB, 10 history entries

1

~30-50 sec

~6-10 sec/call

llama3.1:8b, RTX 3080 12GB, 10 history entries

2

~18-30 sec

Recommended starting point

llama3.1:8b, RTX 3080 12GB, 10 history entries

4

~14-24 sec

Try this; 7GB KV headroom supports it

Last updated

Was this helpful?