Skip to content

Run the NL Search Service

The NL Search Service runs as one container on a Linux server close to PeopleSoft, behind nginx. Like the Assistant service, it runs rootless under a dedicated user, such as aips, as a systemd service that Podman’s Quadlet generates. It needs no access to PeopleSoft or OpenSearch: only PeopleSoft’s integration gateway calls it, and it calls only your LLM provider.

Your administrator provides the image, either in your organization’s container registry or as a file. Load it, and tag it aipeoplesoft-nlsearch:1.0:

Terminal window
podman pull <your-registry>/aipeoplesoft-nlsearch:1.0
podman tag <your-registry>/aipeoplesoft-nlsearch:1.0 \
aipeoplesoft-nlsearch:1.0

The service needs the API key for your LLM provider, which it also uses for the embedding model. As the aips user, store it as a Podman secret. If the Assistant service already runs as aips on this server, with the same provider, the secret already exists.

Terminal window
printf 'LLM API key: '; read -rs AIPS_LLM_API_KEY; echo
export AIPS_LLM_API_KEY
podman secret create --env=true \
aips_llm_api_key AIPS_LLM_API_KEY
unset AIPS_LLM_API_KEY

Create the directory ~/nlsearch for the configuration, and ~/nlsearch-audit for the audit log. Inside ~/nlsearch, the Intent Store, the Prompt Store, and the test set each get a directory:

~/nlsearch
nlsearch.yaml the service's configuration
intents/
store.yaml the Intent Store's version
find_vouchers.yaml the first intent
prompts/
plan-writer.v1.txt the prompt that writes plans
tests/
find_vouchers.yaml the first intent's test questions
  • intents/store.yaml holds one line, version: 2026.09.1. Change it whenever you release a new set of intents.
  • intents/find_vouchers.yaml is the intent from The Intent Store.
  • prompts/plan-writer.v1.txt comes with the release. Later versions are yours, as you tune it.
  • tests/find_vouchers.yaml is the test set from Test before you release.

Save the configuration as ~/nlsearch/nlsearch.yaml:

nlsearch.yaml
server:
port: 8080 # inside the container
intents:
path: intents
version: 2026.09.1 # must match intents/store.yaml
candidates: 5 # intents offered to the LLM
prompts:
path: prompts
active:
plan-writer: 1
llm:
endpoint: https://llm.example.com/v1
model: your-model-id
api_key: ${AIPS_LLM_API_KEY}
retries: 1 # after a plan fails validation
embeddings:
endpoint: https://llm.example.com/v1
model: your-embedding-model
api_key: ${AIPS_LLM_API_KEY}
audit:
destination: /var/log/aipeoplesoft/nlsearch-audit.jsonl
  • intents.version: the service won’t start if store.yaml has a different version, so it never runs a set of intents you haven’t released.
  • prompts.active: the version of each prompt in use. To roll back a prompt, set its previous version, and restart the service.
  • llm: your LLM provider’s API endpoint and model, or your own model server’s. The model should support structured output; the service validates every plan either way.
  • embeddings: the model that embeds the intents’ examples when the service starts, and each question as it arrives.

As the aips user, save this unit file as ~/.config/containers/systemd/aipeoplesoft-nlsearch.container:

aipeoplesoft-nlsearch.container
[Unit]
Description=aiPeopleSoft NL Search Service
[Container]
Image=localhost/aipeoplesoft-nlsearch:1.0
Exec=serve
PublishPort=127.0.0.1:8097:8080
Volume=%h/nlsearch:/etc/aipeoplesoft:ro,Z
Volume=%h/nlsearch-audit:/var/log/aipeoplesoft:Z
Secret=aips_llm_api_key,type=env,target=AIPS_LLM_API_KEY
[Service]
Restart=always
[Install]
WantedBy=default.target

Then start it, and keep the user’s services running after you log out:

Terminal window
systemctl --user daemon-reload
systemctl --user start aipeoplesoft-nlsearch
sudo loginctl enable-linger aips

nginx terminates TLS, and lets a request through only if it presents a client certificate signed by the certificate authority that issued the integration gateway’s certificate:

nginx server block
server {
listen 443 ssl;
server_name nlsearch.example.com;
ssl_certificate /etc/ssl/nlsearch.example.com.pem;
ssl_certificate_key /etc/ssl/nlsearch.example.com.key;
# Only certificates signed by this authority get in
ssl_client_certificate /etc/ssl/peoplesoft-gateway-ca.pem;
ssl_verify_client on;
location / {
proxy_pass http://127.0.0.1:8097;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

nginx accepts any certificate that the authority signed, so use an authority that issues certificates only for this, such as a small internal one.

The image’s check command tests the configuration without starting the service:

Terminal window
podman run --rm \
-v ~/nlsearch:/etc/aipeoplesoft:ro,Z \
--secret aips_llm_api_key,type=env,target=AIPS_LLM_API_KEY \
aipeoplesoft-nlsearch:1.0 check
Output of check
aipeoplesoft-nlsearch 1.0.0
Intent Store 2026.09.1, 1 intent, 5 examples embedded
Prompts plan-writer v1
Embeddings your-embedding-model, answering
LLM your-model-id, answering with structured output
Result ready

Then run the test set, with the same command and test in place of check:

Output of test
Prompt plan-writer v1, Intent Store 2026.09.1
2 of 2 questions produced the expected plan

PeopleSoft calls one operation, POST /plan. The request holds only the question, the user’s date, and the user’s language:

Request
{
"question": "Vouchers over $10,000 from Acme entered last month",
"today": "2026-09-25",
"language": "ENG"
}

The response holds the search that the validated plan stands for, ready for the Search Framework, and the summary to show above the results:

Response
{
"requestId": "nls-4f2a91",
"intent": "find_vouchers",
"search": {
"category": "ZZ_VOUCHERS",
"text": "",
"filters": [
{ "attribute": "VENDOR_NAME", "type": "text",
"op": "contains", "value": "Acme" },
{ "attribute": "GROSS_AMT", "type": "number",
"op": "greater_than", "value": 10000 },
{ "attribute": "ENTERED_DT", "type": "date",
"op": "between", "value": ["2026-08-01", "2026-08-31"] }
]
},
"summary": {
"label": "Vouchers",
"filters": ["Supplier: Acme", "Amount: over 10,000",
"Entered: 1–31 August 2026"]
},
"versions": { "intents": "2026.09.1", "prompt": "plan-writer.v1" }
}
  • search is what PeopleSoft runs: the intent’s category, the words to search for, and each filter with its attribute and type. Coded values are already translated, and a user or employee filter has the value me, which PeopleSoft replaces.
  • summary.filters match search.filters one for one, so the page can remove a filter and its part of the summary together.
  • When no intent fits, or a plan fails validation twice, intent is null and there’s no search, and PeopleSoft runs a keyword search with the question as typed. The service still answers with 200 OK, because PeopleSoft’s ConnectorRequest doesn’t pass HTTP status codes back to PeopleCode.

From a machine with a test client certificate signed by the same authority, ask for a plan:

Terminal window
curl -s https://nlsearch.example.com/plan \
--cert test-client.pem --key test-client.key \
-H "Content-Type: application/json" \
-d '{"question": "Unpaid vouchers for Globex",
"today": "2026-09-25", "language": "ENG"}' | jq .

You should get a plan for find_vouchers, with the filters VENDOR_NAME contains Globex and VOUCHER_STATUS not_equals P. Without the certificate, nginx refuses the request with 400 No required SSL certificate was sent.