import { env } from "../../config/env.js"; /** * Thin fetch wrapper around `services/tech-step-intent-service`'s HTTP * contract (`POST /v1/process`) — the microservice * {@link TechStepClassifierService} (`tech-step-matcher.ts`) delegates NER + * intent classification to, in place of the `node-nlp` `NlpManager` it used * to own directly. See that service's own README for the full contract and * why it never touches Postgres itself — it also owns its own training * corpus now (`training_data.py`), trained once at its own startup, so * `apps/api` never pushes anything to it; `process()` below is this * client's only method. * * Authenticated with `INTENT_SERVICE_SECRET` — the inverse direction of * `requireInternalWorker`'s `INTERNAL_WORKER_SECRET` (this time `apps/api` * is the caller, not the callee), but the same "one flat shared secret" * shape. */ /** One candidate mention one of the service's two `PhraseMatcher`s found — offsets `[start, end)`, same convention as `String.prototype.slice`. Mirrors `EntityPayload` (Python `schemas.py`). `kind` distinguishes a technique mention (`self._matcher`, the corpus-trained one) from a utensil mention (`self._utensil_matcher`, static — see `utensil_vocabulary.py`) — `tech-step-matcher.ts` resolves each against a different catalog (`TechStep`/`Utensil`). */ export interface IntentServiceEntity { uid: string; start: number; end: number; kind: "technique" | "utensil"; } /** The full result of a `POST /v1/process` call — mirrors `ProcessResponse` (Python `schemas.py`). `intent` is `null` only when `locale` isn't one this service trains for, or `text` is blank; otherwise always a real `uid` (the Python service's `textcat` has no "None" sentinel, unlike node-nlp — see that service's README). */ export interface IntentServiceProcessResult { entities: IntentServiceEntity[]; intent: string | null; score: number; } /** * Client for `services/tech-step-intent-service` — a real class (not a * plain object of functions) per this repo's service-style-logic * convention, even though it holds no state of its own: it's used as the * one shared {@link intentServiceClient} singleton below, same reasoning as * `TechStepClassifierService` itself. */ export class IntentServiceClient { /** * Performs a JSON request against the intent service and returns the * parsed body. * * @throws {Error} if the response status is not in the 2xx range, or the * request itself fails (network error, service down) — left as a plain * `Error` rather than a typed `HttpError`: this is an internal * service-to-service call, not a request `apps/api`'s own HTTP layer * needs to map to a client-facing status code (see * `TechStepClassifierService.warmUp`'s retry in `server.ts` for how a * failure here is actually handled). */ private async _request( path: string, init: RequestInit = {}, ): Promise { try { const response = await fetch(`${env.INTENT_SERVICE_BASE_URL}${path}`, { ...init, headers: { "Content-Type": "application/json", "X-Intent-Service-Secret": env.INTENT_SERVICE_SECRET, ...init.headers, }, }); if (!response.ok) { const body = await response.text().catch(() => ""); throw new Error(`${init.method ?? "GET"} ${path} failed: ${response.status} ${body}`); } return (await response.json()) as TResponseBody; } catch (err) { // Rethrown as-is — every caller (`TechStepClassifierService`) already // wraps its own `await`s per the repo's try/catch convention; this is // just where the `await` itself has to sit inside one. throw err; } } /** * Equivalent to the old `NlpManager.process(locale, text)` — returns every * candidate technique mention (NER) plus the intent classifier's verdict * for `text` as a whole, whether `text` is a full step description or a * single clause `TechStepClassifierService` already cut out of one (this * service doesn't know or care which, exactly like `NlpManager` before * it). */ public async process(locale: string, text: string): Promise { try { return await this._request("/v1/process", { method: "POST", body: JSON.stringify({ locale, text }), }); } catch (err) { throw err; } } } /** Single shared instance — stateless, no reason for more than one (same reasoning as `techStepClassifier`/`prisma`). */ export const intentServiceClient = new IntentServiceClient();