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.
Get the image
Section titled “Get the image”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:
podman pull <your-registry>/aipeoplesoft-nlsearch:1.0podman tag <your-registry>/aipeoplesoft-nlsearch:1.0 \ aipeoplesoft-nlsearch:1.0podman load -i aipeoplesoft-nlsearch-1.0.tarStore the secret
Section titled “Store the secret”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.
printf 'LLM API key: '; read -rs AIPS_LLM_API_KEY; echoexport AIPS_LLM_API_KEYpodman secret create --env=true \ aips_llm_api_key AIPS_LLM_API_KEYunset AIPS_LLM_API_KEYAdd the Intent Store and Prompt Store
Section titled “Add the Intent Store and Prompt Store”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.yaml the service's configurationintents/ store.yaml the Intent Store's version find_vouchers.yaml the first intentprompts/ plan-writer.v1.txt the prompt that writes planstests/ find_vouchers.yaml the first intent's test questionsintents/store.yamlholds one line,version: 2026.09.1. Change it whenever you release a new set of intents.intents/find_vouchers.yamlis the intent from The Intent Store.prompts/plan-writer.v1.txtcomes with the release. Later versions are yours, as you tune it.tests/find_vouchers.yamlis the test set from Test before you release.
Configure the service
Section titled “Configure the service”Save the configuration as ~/nlsearch/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.jsonlintents.version: the service won’t start ifstore.yamlhas 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.
Run it with Quadlet
Section titled “Run it with Quadlet”As the aips user, save this unit file as
~/.config/containers/systemd/aipeoplesoft-nlsearch.container:
[Unit]Description=aiPeopleSoft NL Search Service
[Container]Image=localhost/aipeoplesoft-nlsearch:1.0Exec=servePublishPort=127.0.0.1:8097:8080Volume=%h/nlsearch:/etc/aipeoplesoft:ro,ZVolume=%h/nlsearch-audit:/var/log/aipeoplesoft:ZSecret=aips_llm_api_key,type=env,target=AIPS_LLM_API_KEY
[Service]Restart=always
[Install]WantedBy=default.targetThen start it, and keep the user’s services running after you log out:
systemctl --user daemon-reloadsystemctl --user start aipeoplesoft-nlsearchsudo loginctl enable-linger aipsPut nginx in front
Section titled “Put nginx in front”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:
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.
Check it
Section titled “Check it”The image’s check command tests the configuration without starting the service:
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 checkaipeoplesoft-nlsearch 1.0.0Intent Store 2026.09.1, 1 intent, 5 examples embeddedPrompts plan-writer v1Embeddings your-embedding-model, answeringLLM your-model-id, answering with structured outputResult readyThen run the test set, with the same command and test in place of check:
Prompt plan-writer v1, Intent Store 2026.09.12 of 2 questions produced the expected planThe plan API
Section titled “The plan API”PeopleSoft calls one operation, POST /plan. The request holds only the question, the user’s
date, and the user’s language:
{ "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:
{ "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" }}searchis 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 auseroremployeefilter has the valueme, which PeopleSoft replaces.summary.filtersmatchsearch.filtersone 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,
intentisnulland there’s nosearch, and PeopleSoft runs a keyword search with the question as typed. The service still answers with200 OK, because PeopleSoft’sConnectorRequestdoesn’t pass HTTP status codes back to PeopleCode.
Ask for a first plan
Section titled “Ask for a first plan”From a machine with a test client certificate signed by the same authority, ask for a plan:
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.
