Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,17 @@ FORMSG_SDK_MODE=
# POSTMAN_INTERNAL_CAMPAIGN_API_KEY=
# POSTMAN_BASE_URL=https://test.postman.gov.sg/api/v2

## Azure OpenAI
# AZURE_OPENAI_API_KEY=
# AZURE_OPENAI_ENDPOINT=
# AZURE_OPENAI_DEPLOYMENT_NAME=
# AZURE_OPENAI_API_VERSION=

## Kill email mode configs, provide a valid storage form id
# KILL_EMAIL_MODE_FEEDBACK_FORMID=

# For WOG AD Login
# For WOG AD Login
# WOGAD_AUTHORITY=
# WOGAD_CLIENT_ID=
# WOGAD_CLIENT_SECRET=
# WOGAD_REDIRECT_URI=
# WOGAD_REDIRECT_URI=

## AI SDK
# AI_SDK_PROVIDER_NAME=
# AI_SDK_BASE_URL=
# AI_SDK_API_KEY=
# AI_SDK_MODEL_NAME=
44 changes: 44 additions & 0 deletions .scratch/migrate-pair-foundry/01-ai-sdk-wrapper-migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Migrate `ai-model` wrapper + both MFB flows to Pair Foundry via Vercel AI SDK

**Type:** AFK
**Triage:** ready-for-agent

## Parent

PRD: `.scratch/migrate-pair-foundry/PRD.md`
ADR: `docs/adr/0001-pair-foundry-llm-provider.md`

## What to build

Replace the Azure-OpenAI-backed Magic Form Builder (MFB) LLM client with a Vercel AI SDK client pointed at Pair Foundry's PX Engine (`engine.pair.gov.sg`). The single application-facing seam — `sendPromptToModel({ messages, options, formId })` — keeps its function signature, but its internals are fully rewritten on top of ai-sdk's `generateText` (using `@ai-sdk/openai`'s `createOpenAI` provider). The `Message` and options types exposed to callers become ai-sdk native (`CoreMessage` / `ModelMessage`).

Both MFB flows (text-prompt and vision-prompt) are switched over together because changing the exported `Message` type forces both callers to update in the same change. The vision flow rewrites its content parts from the OpenAI shape (`{ type: 'image_url', image_url: { url } }`) to ai-sdk's (`{ type: 'image', image: <dataURL> }`). JSON-mode is preserved per-call via `providerOptions.openai.responseFormat`, so downstream `JSON.parse` + zod validation in the assistance service is unchanged.

Connection settings (`providerName`, `apiKey`, `baseUrl`, `modelName`) are read from the existing `aisdkConfig` (env-var-driven, SSM-supplied in prod). The default model is `claude-x` — verified to support both image content parts and JSON mode on PX Engine — and serves both text and vision flows.

Errors from ai-sdk are mapped onto the existing `ModelGetClientFailureError` (provider/client construction failure) and `ModelResponseFailureError` (request failure or empty response) so that error-handling code paths in MFB controllers and the assistance service remain untouched. A null return for missing message content stays the contract for "model responded but produced nothing usable."

New unit tests for the wrapper cover message/options forwarding, JSON-mode plumbing, error mapping, and null-response handling. The wrapper mocks ai-sdk at the `generateText` boundary, not deeper. Existing `admin-form.assistance.service` tests continue to mock `sendPromptToModel` directly and remain valid through the swap.

The PR is structured as conventional commits (one logical step per commit: dependency add, wrapper rewrite, prompt rewrite, wrapper tests) so reviewers can step through it commit-by-commit, per the ADR's implementation considerations.

## Acceptance criteria

- [ ] `ai` package added to `apps/backend/package.json` and pnpm lockfile; `@ai-sdk/openai` already present.
- [ ] `ai-model.ts` no longer imports anything from the `openai` npm package; uses `createOpenAI` from `@ai-sdk/openai` plus ai-sdk's `generateText`.
- [ ] `ai-model.ts` reads `providerName`, `apiKey`, `baseUrl`, `modelName` from `aisdkConfig`; passes `providerName` as the provider `name`, `baseUrl` as `baseURL`, `apiKey` as `apiKey`, and `.chat(modelName)` selects the model.
- [ ] `sendPromptToModel({ messages, options, formId })` retains its three named parameters and its `ResultAsync<string | null, ModelGetClientFailureError | ModelResponseFailureError>` return type.
- [ ] `Message` re-exported by `ai-model.ts` is ai-sdk's `CoreMessage` (or `ModelMessage`); the `Role` enum is removed; callers in `admin-form.assistance.service.ts` are updated accordingly.
- [ ] JSON-mode (`response_format: { type: 'json_object' }`) is plumbed via `providerOptions: { openai: { responseFormat: { type: 'json_object' } } }` on each call from the text and vision prompt flows.
- [ ] Vision-flow content parts use ai-sdk's `{ type: 'image', image: <dataURL> }` shape; the `image_url` shape no longer appears in the prompt builders.
- [ ] ai-sdk client-construction failures surface as `ModelGetClientFailureError`; ai-sdk request failures surface as `ModelResponseFailureError`; empty/missing model responses return `null`.
- [ ] `temperature` is not pinned inside the wrapper; callers can still pass it via `options`.
- [ ] New unit tests added for the `ai-model` wrapper covering: happy-path forwarding of messages and `providerOptions.openai.responseFormat`; null/empty response → `null`; provider construction failure → `ModelGetClientFailureError`; ai-sdk request failure → `ModelResponseFailureError` with `formId` in the log meta; options pass-through (caller options layered correctly with the wrapper's own).
- [ ] Existing `admin-form.assistance.service.spec.ts` continues to pass without modification to its mocks.
- [ ] Backend type-checks, lints, and tests pass.
- [ ] Manual smoke: text-prompt MFB generates expected JSON form fields end-to-end against PX Engine; vision-prompt MFB does the same with an uploaded image.
- [ ] Commits follow conventional commits and are reviewable in sequence.

## Blocked by

None — can start immediately.
32 changes: 32 additions & 0 deletions .scratch/migrate-pair-foundry/02-azure-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Remove orphaned Azure AI Foundry artifacts

**Type:** AFK
**Triage:** ready-for-agent

## Parent

PRD: `.scratch/migrate-pair-foundry/PRD.md`
ADR: `docs/adr/0001-pair-foundry-llm-provider.md`

## What to build

Once the Magic Form Builder LLM wrapper is fully migrated to Pair Foundry (slice 1), the Azure AI Foundry remnants are dead weight: the Azure-specific convict config, the `openai` npm package, and `AZURE_OPENAI_*` env-var references in the repo. This slice deletes them so SSM / IaC stops carrying ghost parameters and the lockfile stops carrying an unused dependency.

The scope is in-repo only. If FormSG's deployment-side IaC lives in a separate repository, those `AZURE_OPENAI_*` parameter definitions are out of scope for this slice and handled in a follow-up PR against that repo.

After this slice the only LLM config surface in the codebase is `aisdk.config.ts` and the `AI_SDK_*` env vars.

## Acceptance criteria

- [ ] `apps/backend/src/app/config/features/azureopenai.config.ts` is deleted.
- [ ] `openai` is removed from `apps/backend/package.json` dependencies; pnpm lockfile is regenerated and no longer contains the `openai` package.
- [ ] Repo-wide grep confirms no remaining imports from `'openai'`, `'openai/error'`, or `'openai/resources/...'` in `apps/backend/src`.
- [ ] Repo-wide grep confirms no remaining references to `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_DEPLOYMENT_NAME`, `AZURE_OPENAI_API_VERSION`, or `AZURE_OPENAI_MODEL` in `.env` examples, docs, or in-repo IaC.
- [ ] Repo-wide grep confirms no remaining references to `azureOpenAIConfig` or `azureopenai.config`.
- [ ] Backend type-checks, lints, and tests pass.
- [ ] Manual smoke: text-prompt MFB and vision-prompt MFB continue to work end-to-end against PX Engine (regression check after dep removal).
- [ ] Commits follow conventional commits.

## Blocked by

- Slice 1: `.scratch/migrate-pair-foundry/01-ai-sdk-wrapper-migration.md` — the Azure config and `openai` package can only be safely removed once nothing imports them.
Loading
Loading