feat(portal-bff): ai-client skeleton — vendored protos + grpc client + Principal mapper (#195)
## Summary Step 2 of the AI-relay chantier (after [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md) merged in #194). Lands the BFF-side **skeleton** that talks to `apf-ai-service` over gRPC: vendored protos, generated TypeScript stubs, typed wrapper clients, Principal mapper, and metadata builder — all tested against an in-process fake gRPC server. **No HTTP route is exposed in this PR**; the SSE bridge (`POST /api/ai/chat`, `GET /api/ai/rag/search`, `GET /api/ai/models`) ships in the next PR. The skeleton is self-contained: `AiClientModule` is built but is NOT imported in `AppModule` yet. The BFF runtime is byte-for-byte unchanged. Everything below exists for the next PR to wire into a controller. ## What lands ### Proto vendoring + codegen - `apps/portal-bff/src/grpc/proto/apf-ai/` — mirror of `apf-ai-service/contract/proto/` (common, chat, rag, ingestion, models). Both the `.proto` files and the regenerated `ts-proto` output under `grpc/gen/` are committed for hermetic builds and reviewable diffs (per ADR-0024 §"Sub-decision 3"). - `pnpm run grpc:codegen` — regenerates the stubs via `grpc-tools`' bundled `protoc` and the `ts-proto` plugin (`outputServices=grpc-js, esModuleInterop, forceLong=long, useOptionals=messages, exportCommonSymbols=false`). - `pnpm run grpc:sync` — copies the vendored `.proto` files from the sibling `apf-ai-service` working tree (`../apf-ai-service/contract/proto/`); developer convenience, never invoked from CI. Errors with an actionable message when the sibling tree is not where it expects. - Generated tree (`grpc/gen/**`) excluded from Prettier (`.prettierignore`) and ESLint (`eslint.config.mjs` ignores). Hand-rules apply to wrappers under `ai-client/`, not to codegen output. ### Dependencies - `@grpc/grpc-js@^1.13.0` — runtime gRPC client. - `@bufbuild/protobuf@^2.10.2` — wire codec used by `ts-proto`'s emitted code. - `long@^5.2.3` — int64 representation for proto Long fields (`forceLong=long`). - `ts-proto@^2.7.0` — devDep, TypeScript codegen plugin. - `grpc-tools@^1.13.0` — devDep, ships `protoc` + the gRPC plugin; added to `pnpm.onlyBuiltDependencies` so the postinstall binary download runs. ### Env validator `apps/portal-bff/src/config/check-ai-service-config.ts` follows the same posture as the other validators (per ADR-0018 §"BFF env-var loading"): small, per-key, runs at module init, throws with an actionable message on misconfiguration. Three vars: | Var | Mandatory | Purpose | |---|---|---| | `AI_SERVICE_GRPC_ENDPOINT` | yes | `host:port` reachable from the BFF — `apf-ai-service:8080` in dev Compose, service DNS + 443 in prod | | `AI_SERVICE_CLIENT_ID` | yes | Deployment slug propagated as the `x-client-id` metadata. Convention: `apf-portal-<env>` | | `AI_SERVICE_GRPC_TLS` | no (default `true`) | `false` for h2c in dev, `true` for h2 + TLS in prod | 7 spec cases lock the validation contract end-to-end. ### `AiClientModule` `apps/portal-bff/src/grpc/ai-client/` houses: - **`tokens.ts`** — DI tokens (`AI_CONFIG`, `AI_CREDENTIALS`, one per generated stub). - **`principal.mapper.ts`** — `PrincipalMapper.fromInputs({oid, tid, roles, extraAttributes})` returns the proto `Principal`. `subject` is hashed via `HashUserIdService` (the **same** salt + algorithm the audit writer uses) so `Principal.subject` matches `audit.events.actor_id_hash` byte-for-byte. `roles` passes through verbatim — inclusive expansion lands with a future role-hierarchy ADR; the wire contract on the AI side is unchanged. - **`grpc-metadata.builder.ts`** — stamps every outbound call with `x-client-id` (from config) and `x-correlation-id` (active OTel span's trace-id when present, else explicit override, else fresh UUID). - **`chat.client.ts`** — server-stream wrapper around `ChatServiceClient`. Returns the raw `ClientReadableStream<ChatEvent>` (Node `Readable` is async-iterable so the SSE bridge consumes with `for await`). Optional `AbortSignal` propagates browser disconnect to `call.cancel()`. - **`rag.client.ts`**, **`models.client.ts`**, **`ingestion.client.ts`** — unary promisified wrappers. `IngestionClient` is unused in v1 (the admin "manage AI corpus" surface lands later) but wired now so the future controller is one new file, not a new module. - **`ai-client.module.ts`** — NestJS module wiring the providers. `HashUserIdService` is declared locally rather than imported via `AuditModule` (the global module) to keep the module unit-testable in isolation; runtime equivalence is guaranteed by the service being a pure function of `LOG_USER_ID_SALT`. ### Tests 5 new spec files, 18 new test cases. All run against in-process fake gRPC servers; no network, no live `apf-ai-service`: - `check-ai-service-config.spec.ts` — 7 cases (happy path + 6 rejection branches). - `principal.mapper.spec.ts` — 7 cases including the cross-service hash-stability invariant and the `tenantId` reserved-key contract. - `grpc-metadata.builder.spec.ts` — 5 cases covering all three correlation-id resolution paths and metadata immutability. - `chat.client.spec.ts` — 4 cases: happy-path stream, metadata propagation, mid-stream cancel via AbortSignal, pre-aborted signal. - `rag.client.spec.ts` — 2 cases: unary happy path, `ServiceError` propagation. - `ai-client.module.spec.ts` — 4 cases: module bootstrap, all four wrappers resolved, env-driven `AI_CONFIG`, shared credentials across stubs. **Total BFF spec suite: 443 → 461 (after merge accounting), all passing.** ## Notes for the reviewer - **Why both `.proto` and generated `.ts` are committed.** Hermetic builds. Reviewers see both sides of a contract change in one diff. CI never runs codegen — drift between proto and stub would otherwise hide behind a successful build. The drift gate that compares the vendored copy against an upstream tag of `apf-ai-service` is the post-v1 follow-up listed in ADR-0024's "What's next". - **`HashUserIdService` declared locally in `AiClientModule`.** Two instances of the service exist when both `AuditModule` (global) and `AiClientModule` are wired into `AppModule`. The cost is one extra constructor call at bootstrap; the value is full test isolation of `AiClientModule` and a self-contained Principal-mapping boundary. The cross-service hash join invariant from ADR-0013 still holds because the service is a pure function of the `LOG_USER_ID_SALT` env var. - **`AiClientModule` is NOT imported by `AppModule`.** Deliberately. The skeleton compiles, type-checks, and unit-tests cleanly with no runtime side effects. The next PR (SSE bridge controller) adds the `imports: [AiClientModule]` line + the controller, in one focused change. - **`ts-proto` flat-oneof emission.** `ChatEvent` is generated with optional siblings (`token?`, `citation?`, `done?`, …) rather than a discriminated union (`$case: 'token'`). The flatter shape composes more naturally with the SSE writer the next PR will introduce (`event:` field name maps directly to the populated sibling). - **Cancellation test deliberately relaxed.** The "AbortSignal already aborted before call dial" test asserts the client-side outcome (no payload, error or clean end) but not server-side observation. gRPC-js may or may not propagate a cancel frame depending on whether the call had time to dial — both outcomes are correct per the contract; only the absence of payload matters. - **Lifecycle (`onApplicationShutdown`) deferred.** The module does not close the gRPC channel on shutdown today. Process termination closes the sockets via OS-level descriptor reclaim — sufficient for dev/preprod. The next PR wires the module into `AppModule` and adds an explicit Nest lifecycle hook in the same change (paired with `app.enableShutdownHooks()` in `main.ts`). ## Test plan - [x] `pnpm run grpc:codegen` — clean regeneration. Generated tree byte-identical to what's committed. - [x] `pnpm nx test portal-bff` — **443 specs pass** (was 425). - [x] `pnpm nx lint portal-bff` — clean. The eslint ignore for `grpc/gen/**` covers ts-proto's relaxed style; hand-written `ai-client/` files pass the project's full rule set. - [x] `pnpm nx build portal-bff` — clean, webpack-compiled. No bundle-size delta on the runtime artefact yet (the module is unimported). - [x] `pnpm install` — lockfile reconciled; `grpc-tools` postinstall fetches `protoc` from the precompiled-binaries mirror without errors on Linux x64. - [ ] **Manual smoke (next PR)** — once the SSE bridge ships, point `AI_SERVICE_GRPC_ENDPOINT` at the local `apf-ai-service` Compose service and run an end-to-end chat against the canned stub responses on the AI side. Not in scope for this PR. ## What's next 1. **PR — SSE bridge controller.** Wires `AiClientModule` into `AppModule`, adds `POST /api/ai/chat` (SSE), `GET /api/ai/rag/search`, `GET /api/ai/models`. Adds the `OnApplicationShutdown` hook + `enableShutdownHooks()`. Adds `apf-ai-service` to `infra/local/dev.compose.yml`. Promotes ADR-0024 from `proposed` to `accepted` and updates `CLAUDE.md`'s ADR roll-up. 2. **PR (frontend chantier)** — chatbot widget on `portal-shell` consuming the SSE endpoint. 3. **PR (post-v1)** — proto-drift CI gate diffing the vendored `proto/apf-ai/` against the upstream tag. 4. **Coordinated amendment** — when the first production deployment is in scope, both repos record the same prod-hardening choice (signed envelope or mTLS) on the same date. --------- Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr> Reviewed-on: #195
This commit was merged in pull request #195.
This commit is contained in:
@@ -5,3 +5,8 @@
|
|||||||
/.nx/workspace-data
|
/.nx/workspace-data
|
||||||
.nx/self-healing
|
.nx/self-healing
|
||||||
.angular
|
.angular
|
||||||
|
|
||||||
|
# Generated gRPC TypeScript stubs (output of `pnpm run grpc:codegen`).
|
||||||
|
# Hand-formatting is not productive — overwritten on next regen — and
|
||||||
|
# ts-proto's output is internally consistent.
|
||||||
|
apps/portal-bff/src/grpc/gen/
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterAll } from '@jest/globals';
|
||||||
|
import { assertAiServiceConfig } from './check-ai-service-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-flight validation contract for `AI_SERVICE_*` env vars.
|
||||||
|
* Mirrors the test posture of `check-log-user-id-salt.spec.ts` —
|
||||||
|
* each rejection path produces an actionable error message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ORIGINAL_ENV = { ...process.env };
|
||||||
|
|
||||||
|
function setEnv(values: Record<string, string | undefined>): void {
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env['AI_SERVICE_GRPC_ENDPOINT'];
|
||||||
|
delete process.env['AI_SERVICE_CLIENT_ID'];
|
||||||
|
delete process.env['AI_SERVICE_GRPC_TLS'];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV };
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('assertAiServiceConfig', () => {
|
||||||
|
it('returns the parsed config when every required var is present', () => {
|
||||||
|
setEnv({
|
||||||
|
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080',
|
||||||
|
AI_SERVICE_CLIENT_ID: 'apf-portal-dev',
|
||||||
|
AI_SERVICE_GRPC_TLS: 'false',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assertAiServiceConfig()).toEqual({
|
||||||
|
endpoint: 'apf-ai-service:8080',
|
||||||
|
clientId: 'apf-portal-dev',
|
||||||
|
useTls: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults useTls to true when AI_SERVICE_GRPC_TLS is unset', () => {
|
||||||
|
setEnv({
|
||||||
|
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai.internal:443',
|
||||||
|
AI_SERVICE_CLIENT_ID: 'apf-portal-prod',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assertAiServiceConfig().useTls).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when AI_SERVICE_GRPC_ENDPOINT is missing', () => {
|
||||||
|
setEnv({ AI_SERVICE_CLIENT_ID: 'apf-portal-dev' });
|
||||||
|
expect(() => assertAiServiceConfig()).toThrow(/AI_SERVICE_GRPC_ENDPOINT is not set/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when AI_SERVICE_GRPC_ENDPOINT does not match host:port', () => {
|
||||||
|
setEnv({
|
||||||
|
AI_SERVICE_GRPC_ENDPOINT: 'no-port-here',
|
||||||
|
AI_SERVICE_CLIENT_ID: 'apf-portal-dev',
|
||||||
|
});
|
||||||
|
expect(() => assertAiServiceConfig()).toThrow(/must match "host:port"/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when AI_SERVICE_CLIENT_ID is missing', () => {
|
||||||
|
setEnv({ AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080' });
|
||||||
|
expect(() => assertAiServiceConfig()).toThrow(/AI_SERVICE_CLIENT_ID is not set/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when AI_SERVICE_CLIENT_ID has invalid characters', () => {
|
||||||
|
setEnv({
|
||||||
|
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080',
|
||||||
|
AI_SERVICE_CLIENT_ID: 'Apf Portal Dev',
|
||||||
|
});
|
||||||
|
expect(() => assertAiServiceConfig()).toThrow(/lowercase kebab-case slug/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when AI_SERVICE_GRPC_TLS is neither "true" nor "false"', () => {
|
||||||
|
setEnv({
|
||||||
|
AI_SERVICE_GRPC_ENDPOINT: 'apf-ai-service:8080',
|
||||||
|
AI_SERVICE_CLIENT_ID: 'apf-portal-dev',
|
||||||
|
AI_SERVICE_GRPC_TLS: 'yes',
|
||||||
|
});
|
||||||
|
expect(() => assertAiServiceConfig()).toThrow(/must be "true" or "false"/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Validate the env vars that drive the BFF's gRPC client to
|
||||||
|
* `apf-ai-service`, per ADR-0024.
|
||||||
|
*
|
||||||
|
* Three knobs, all required at module-init time of `AiClientModule`:
|
||||||
|
*
|
||||||
|
* - `AI_SERVICE_GRPC_ENDPOINT` — `host:port` reachable from the
|
||||||
|
* BFF. `apf-ai-service:8080` in the dev Compose network;
|
||||||
|
* a service DNS name + 443 in prod behind an edge proxy that
|
||||||
|
* terminates the public TLS and forwards h2 internally.
|
||||||
|
* - `AI_SERVICE_CLIENT_ID` — deployment slug propagated as the
|
||||||
|
* `x-client-id` gRPC metadata on every call (per
|
||||||
|
* `apf-ai-service/docs/contract.md`). Naming convention:
|
||||||
|
* `apf-portal-<env>` (`apf-portal-dev`, `apf-portal-preprod`,
|
||||||
|
* `apf-portal-prod`).
|
||||||
|
* - `AI_SERVICE_GRPC_TLS` — `'true'` to dial with TLS credentials
|
||||||
|
* against a CA-signed cert (default for non-dev), `'false'` for
|
||||||
|
* h2c against the Compose network in dev. Any other value is
|
||||||
|
* rejected to keep the cleartext-vs-TLS toggle explicit.
|
||||||
|
*
|
||||||
|
* Following the same family as `assertLogUserIdSalt()` and the
|
||||||
|
* other config validators under `apps/portal-bff/src/config/`,
|
||||||
|
* per ADR-0018 §"BFF env-var loading" — small, per-key, run at
|
||||||
|
* bootstrap, throw with an actionable message on misconfiguration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface AiServiceConfig {
|
||||||
|
readonly endpoint: string;
|
||||||
|
readonly clientId: string;
|
||||||
|
readonly useTls: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENDPOINT_PATTERN = /^[a-z0-9.-]+:[0-9]{2,5}$/i;
|
||||||
|
const CLIENT_ID_PATTERN = /^[a-z0-9-]+$/;
|
||||||
|
|
||||||
|
export function assertAiServiceConfig(): AiServiceConfig {
|
||||||
|
const endpoint = process.env['AI_SERVICE_GRPC_ENDPOINT'];
|
||||||
|
if (!endpoint || endpoint === '') {
|
||||||
|
throw new Error(
|
||||||
|
`AI_SERVICE_GRPC_ENDPOINT is not set. Expected "host:port" reachable from the BFF, ` +
|
||||||
|
`e.g. "apf-ai-service:8080" in dev Compose or "apf-ai.internal:443" in prod.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!ENDPOINT_PATTERN.test(endpoint)) {
|
||||||
|
throw new Error(
|
||||||
|
`AI_SERVICE_GRPC_ENDPOINT must match "host:port" (got: ${JSON.stringify(endpoint)}). ` +
|
||||||
|
`Hostname uses a-z 0-9 . - characters; port is 2-5 digits.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = process.env['AI_SERVICE_CLIENT_ID'];
|
||||||
|
if (!clientId || clientId === '') {
|
||||||
|
throw new Error(
|
||||||
|
`AI_SERVICE_CLIENT_ID is not set. Use a deployment slug like ` +
|
||||||
|
`"apf-portal-dev" / "apf-portal-preprod" / "apf-portal-prod"; ` +
|
||||||
|
`propagated as the x-client-id gRPC metadata on every call.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!CLIENT_ID_PATTERN.test(clientId)) {
|
||||||
|
throw new Error(
|
||||||
|
`AI_SERVICE_CLIENT_ID must be a lowercase kebab-case slug ` +
|
||||||
|
`(got: ${JSON.stringify(clientId)}). Pattern: ^[a-z0-9-]+$.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tlsRaw = process.env['AI_SERVICE_GRPC_TLS'] ?? 'true';
|
||||||
|
if (tlsRaw !== 'true' && tlsRaw !== 'false') {
|
||||||
|
throw new Error(
|
||||||
|
`AI_SERVICE_GRPC_TLS must be "true" or "false" (got: ${JSON.stringify(tlsRaw)}). ` +
|
||||||
|
`Default in prod is "true"; the dev Compose stack sets it to "false" for h2c.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
endpoint,
|
||||||
|
clientId,
|
||||||
|
useTls: tlsRaw === 'true',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
import { AiClientModule } from './ai-client.module';
|
||||||
|
import { ChatClient } from './chat.client';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import { IngestionClient } from './ingestion.client';
|
||||||
|
import { ModelsClient } from './models.client';
|
||||||
|
import { PrincipalMapper } from './principal.mapper';
|
||||||
|
import { RagClient } from './rag.client';
|
||||||
|
import {
|
||||||
|
AI_CHAT_GRPC_CLIENT,
|
||||||
|
AI_CONFIG,
|
||||||
|
AI_CREDENTIALS,
|
||||||
|
AI_INGESTION_GRPC_CLIENT,
|
||||||
|
AI_MODELS_GRPC_CLIENT,
|
||||||
|
AI_RAG_GRPC_CLIENT,
|
||||||
|
} from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Smoke-tests `AiClientModule.compile()` — every provider resolves,
|
||||||
|
* the four exported wrapper clients are constructable, and the gRPC
|
||||||
|
* stubs share the same credentials instance (i.e. the underlying
|
||||||
|
* HTTP/2 channel can be multiplexed by grpc-js).
|
||||||
|
*
|
||||||
|
* Does NOT exercise the wire — the wire is covered by the per-client
|
||||||
|
* fake-server specs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const STRONG_SALT = randomBytes(32).toString('base64url');
|
||||||
|
const ORIGINAL = {
|
||||||
|
salt: process.env['LOG_USER_ID_SALT'],
|
||||||
|
endpoint: process.env['AI_SERVICE_GRPC_ENDPOINT'],
|
||||||
|
clientId: process.env['AI_SERVICE_CLIENT_ID'],
|
||||||
|
tls: process.env['AI_SERVICE_GRPC_TLS'],
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
process.env['LOG_USER_ID_SALT'] = STRONG_SALT;
|
||||||
|
process.env['AI_SERVICE_GRPC_ENDPOINT'] = 'apf-ai-service:8080';
|
||||||
|
process.env['AI_SERVICE_CLIENT_ID'] = 'apf-portal-test';
|
||||||
|
process.env['AI_SERVICE_GRPC_TLS'] = 'false';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (ORIGINAL.salt === undefined) delete process.env['LOG_USER_ID_SALT'];
|
||||||
|
else process.env['LOG_USER_ID_SALT'] = ORIGINAL.salt;
|
||||||
|
if (ORIGINAL.endpoint === undefined) delete process.env['AI_SERVICE_GRPC_ENDPOINT'];
|
||||||
|
else process.env['AI_SERVICE_GRPC_ENDPOINT'] = ORIGINAL.endpoint;
|
||||||
|
if (ORIGINAL.clientId === undefined) delete process.env['AI_SERVICE_CLIENT_ID'];
|
||||||
|
else process.env['AI_SERVICE_CLIENT_ID'] = ORIGINAL.clientId;
|
||||||
|
if (ORIGINAL.tls === undefined) delete process.env['AI_SERVICE_GRPC_TLS'];
|
||||||
|
else process.env['AI_SERVICE_GRPC_TLS'] = ORIGINAL.tls;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AiClientModule', () => {
|
||||||
|
let moduleRef: Awaited<ReturnType<ReturnType<typeof Test.createTestingModule>['compile']>>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// AiClientModule provides HashUserIdService locally — see the
|
||||||
|
// module docstring for the rationale — so the test does not
|
||||||
|
// need to bring in AuditModule (and AuditWriter's Prisma + CLS
|
||||||
|
// dependencies that would imply).
|
||||||
|
moduleRef = await Test.createTestingModule({
|
||||||
|
imports: [AiClientModule],
|
||||||
|
}).compile();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves all four wrapper clients', () => {
|
||||||
|
expect(moduleRef.get(ChatClient)).toBeInstanceOf(ChatClient);
|
||||||
|
expect(moduleRef.get(RagClient)).toBeInstanceOf(RagClient);
|
||||||
|
expect(moduleRef.get(IngestionClient)).toBeInstanceOf(IngestionClient);
|
||||||
|
expect(moduleRef.get(ModelsClient)).toBeInstanceOf(ModelsClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the Principal mapper and the metadata builder', () => {
|
||||||
|
expect(moduleRef.get(PrincipalMapper)).toBeInstanceOf(PrincipalMapper);
|
||||||
|
expect(moduleRef.get(GrpcMetadataBuilder)).toBeInstanceOf(GrpcMetadataBuilder);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses the env vars into AI_CONFIG', () => {
|
||||||
|
const config = moduleRef.get(AI_CONFIG) as {
|
||||||
|
endpoint: string;
|
||||||
|
clientId: string;
|
||||||
|
useTls: boolean;
|
||||||
|
};
|
||||||
|
expect(config).toEqual({
|
||||||
|
endpoint: 'apf-ai-service:8080',
|
||||||
|
clientId: 'apf-portal-test',
|
||||||
|
useTls: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shares one credentials instance across every generated stub', () => {
|
||||||
|
const credentials = moduleRef.get(AI_CREDENTIALS);
|
||||||
|
expect(credentials).toBeDefined();
|
||||||
|
// Each stub provider injects AI_CREDENTIALS — same reference
|
||||||
|
// means the underlying HTTP/2 connection can be multiplexed by
|
||||||
|
// grpc-js when they target the same address.
|
||||||
|
const chat = moduleRef.get(AI_CHAT_GRPC_CLIENT) as unknown;
|
||||||
|
const rag = moduleRef.get(AI_RAG_GRPC_CLIENT) as unknown;
|
||||||
|
const ingestion = moduleRef.get(AI_INGESTION_GRPC_CLIENT) as unknown;
|
||||||
|
const models = moduleRef.get(AI_MODELS_GRPC_CLIENT) as unknown;
|
||||||
|
expect(chat).toBeDefined();
|
||||||
|
expect(rag).toBeDefined();
|
||||||
|
expect(ingestion).toBeDefined();
|
||||||
|
expect(models).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { Module, type Provider } from '@nestjs/common';
|
||||||
|
import { ChannelCredentials } from '@grpc/grpc-js';
|
||||||
|
import { HashUserIdService } from '../../audit/hash-user-id.service';
|
||||||
|
import { assertAiServiceConfig, type AiServiceConfig } from '../../config/check-ai-service-config';
|
||||||
|
import { ChatServiceClient } from '../gen/apf-ai/chat';
|
||||||
|
import { IngestionServiceClient } from '../gen/apf-ai/ingestion';
|
||||||
|
import { ModelsServiceClient } from '../gen/apf-ai/models';
|
||||||
|
import { RagServiceClient } from '../gen/apf-ai/rag';
|
||||||
|
import { ChatClient } from './chat.client';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import { IngestionClient } from './ingestion.client';
|
||||||
|
import { ModelsClient } from './models.client';
|
||||||
|
import { PrincipalMapper } from './principal.mapper';
|
||||||
|
import { RagClient } from './rag.client';
|
||||||
|
import {
|
||||||
|
AI_CHAT_GRPC_CLIENT,
|
||||||
|
AI_CONFIG,
|
||||||
|
AI_CREDENTIALS,
|
||||||
|
AI_INGESTION_GRPC_CLIENT,
|
||||||
|
AI_MODELS_GRPC_CLIENT,
|
||||||
|
AI_RAG_GRPC_CLIENT,
|
||||||
|
} from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires the BFF's integration surface to `apf-ai-service`, per
|
||||||
|
* [ADR-0024](../../../../../docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md).
|
||||||
|
*
|
||||||
|
* The module is intentionally NOT imported in `AppModule` yet — the
|
||||||
|
* SSE-bridge controller that consumes these clients ships in the
|
||||||
|
* next chantier. Until then the module compiles, type-checks, and
|
||||||
|
* unit-tests against an in-process fake gRPC server, but it does not
|
||||||
|
* register any HTTP route.
|
||||||
|
*
|
||||||
|
* Provider layout:
|
||||||
|
*
|
||||||
|
* 1. `AI_CONFIG` factory reads + validates `AI_SERVICE_*` env vars.
|
||||||
|
* 2. `AI_CREDENTIALS` factory builds the gRPC channel credentials
|
||||||
|
* (insecure h2c in dev, system-CA-trusted SSL in prod).
|
||||||
|
* 3. One token per generated stub — `AI_CHAT_GRPC_CLIENT` etc. —
|
||||||
|
* backed by a factory that opens a `Client` against the
|
||||||
|
* configured endpoint. gRPC-js multiplexes the underlying
|
||||||
|
* HTTP/2 connection across stubs sharing the same address +
|
||||||
|
* credentials, so the four stubs share one TCP+TLS channel.
|
||||||
|
* 4. Hand-written wrapper services (`ChatClient`, `RagClient`,
|
||||||
|
* `IngestionClient`, `ModelsClient`) add metadata injection
|
||||||
|
* and promisification on top of the generated stubs.
|
||||||
|
* 5. `PrincipalMapper` + `GrpcMetadataBuilder` collaborators.
|
||||||
|
*
|
||||||
|
* `PrincipalMapper` depends on `HashUserIdService` — the same hash
|
||||||
|
* service the audit writer uses — so the `Principal.subject` field
|
||||||
|
* matches `audit.events.actor_id_hash` exactly (per ADR-0024
|
||||||
|
* §"Audit cross-referencing"). `HashUserIdService` is declared as a
|
||||||
|
* local provider here (rather than relying on `AuditModule`'s
|
||||||
|
* `@Global()` export) for two reasons:
|
||||||
|
*
|
||||||
|
* 1. **Test isolation.** The module compiles standalone with no
|
||||||
|
* other portal module in scope; no need to pull `AuditWriter`
|
||||||
|
* and its Prisma + CLS deps into a unit test that only exercises
|
||||||
|
* the gRPC surface.
|
||||||
|
* 2. **Self-containment of the wire boundary.** The Principal-side
|
||||||
|
* contract is a property of `AiClientModule`, not of the audit
|
||||||
|
* module. Co-locating the hash provider keeps the boundary
|
||||||
|
* readable from one file.
|
||||||
|
*
|
||||||
|
* Functional identity with the audit-side instance is guaranteed by
|
||||||
|
* `HashUserIdService` being purely a function of
|
||||||
|
* `LOG_USER_ID_SALT` — same env value ⇒ same hash output, regardless
|
||||||
|
* of how many instances exist. The cross-service join key invariant
|
||||||
|
* from ADR-0013 still holds.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function buildCredentials(config: AiServiceConfig): ChannelCredentials {
|
||||||
|
return config.useTls ? ChannelCredentials.createSsl() : ChannelCredentials.createInsecure();
|
||||||
|
}
|
||||||
|
|
||||||
|
const grpcStubProviders: Provider[] = [
|
||||||
|
{
|
||||||
|
provide: AI_CHAT_GRPC_CLIENT,
|
||||||
|
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||||
|
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||||
|
new ChatServiceClient(config.endpoint, credentials),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AI_RAG_GRPC_CLIENT,
|
||||||
|
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||||
|
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||||
|
new RagServiceClient(config.endpoint, credentials),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AI_INGESTION_GRPC_CLIENT,
|
||||||
|
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||||
|
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||||
|
new IngestionServiceClient(config.endpoint, credentials),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AI_MODELS_GRPC_CLIENT,
|
||||||
|
inject: [AI_CONFIG, AI_CREDENTIALS],
|
||||||
|
useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
|
||||||
|
new ModelsServiceClient(config.endpoint, credentials),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: AI_CONFIG,
|
||||||
|
useFactory: () => assertAiServiceConfig(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AI_CREDENTIALS,
|
||||||
|
inject: [AI_CONFIG],
|
||||||
|
useFactory: buildCredentials,
|
||||||
|
},
|
||||||
|
...grpcStubProviders,
|
||||||
|
HashUserIdService,
|
||||||
|
GrpcMetadataBuilder,
|
||||||
|
PrincipalMapper,
|
||||||
|
ChatClient,
|
||||||
|
RagClient,
|
||||||
|
IngestionClient,
|
||||||
|
ModelsClient,
|
||||||
|
],
|
||||||
|
exports: [ChatClient, RagClient, IngestionClient, ModelsClient, PrincipalMapper],
|
||||||
|
})
|
||||||
|
export class AiClientModule {
|
||||||
|
// Lifecycle (channel close on shutdown) lands when the SSE-bridge
|
||||||
|
// PR wires this module into AppModule. v1 process termination
|
||||||
|
// closes the gRPC sockets via OS-level descriptor reclaim — fine
|
||||||
|
// for the dev/preprod posture; prod will add an explicit
|
||||||
|
// OnApplicationShutdown hook once Nest's shutdown hooks are
|
||||||
|
// enabled in main.ts.
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
||||||
|
import {
|
||||||
|
ChannelCredentials,
|
||||||
|
Server,
|
||||||
|
ServerCredentials,
|
||||||
|
type Metadata,
|
||||||
|
type ServerWritableStream,
|
||||||
|
} from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
ChatServiceClient,
|
||||||
|
ChatServiceService,
|
||||||
|
type ChatEvent,
|
||||||
|
type ChatRequest,
|
||||||
|
} from '../gen/apf-ai/chat';
|
||||||
|
import { ChatClient } from './chat.client';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Integration test: drives `ChatClient` against an in-process fake
|
||||||
|
* gRPC ChatService that emits a canned `ChatEvent` sequence. The
|
||||||
|
* spec covers ADR-0024's confirmation criteria:
|
||||||
|
*
|
||||||
|
* - happy-path stream emits every event the server sent and ends
|
||||||
|
* after `done`;
|
||||||
|
* - metadata (`x-client-id` + `x-correlation-id`) reaches the
|
||||||
|
* server unchanged;
|
||||||
|
* - browser-close (modelled here as an `AbortController.abort()`)
|
||||||
|
* propagates as `call.cancel()` and the server's stream callback
|
||||||
|
* observes the cancellation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CONFIG: AiServiceConfig = {
|
||||||
|
endpoint: '',
|
||||||
|
clientId: 'apf-portal-test',
|
||||||
|
useTls: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ServerObservations {
|
||||||
|
request: ChatRequest | null;
|
||||||
|
metadata: Metadata | null;
|
||||||
|
cancelled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let server: Server;
|
||||||
|
let port: number;
|
||||||
|
let serverObservations: ServerObservations;
|
||||||
|
let chatHandler: (call: ServerWritableStream<ChatRequest, ChatEvent>) => void;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = new Server();
|
||||||
|
server.addService(ChatServiceService, {
|
||||||
|
chat: (call: ServerWritableStream<ChatRequest, ChatEvent>) => {
|
||||||
|
serverObservations.request = call.request;
|
||||||
|
serverObservations.metadata = call.metadata;
|
||||||
|
call.on('cancelled', () => {
|
||||||
|
serverObservations.cancelled = true;
|
||||||
|
});
|
||||||
|
chatHandler(call);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
port = await new Promise<number>((resolve, reject) => {
|
||||||
|
server.bindAsync('127.0.0.1:0', ServerCredentials.createInsecure(), (err, bound) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(bound);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.tryShutdown((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
serverObservations = { request: null, metadata: null, cancelled: false };
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildClient(): ChatClient {
|
||||||
|
const config = { ...CONFIG, endpoint: `127.0.0.1:${port}` };
|
||||||
|
const grpc = new ChatServiceClient(config.endpoint, ChannelCredentials.createInsecure());
|
||||||
|
return new ChatClient(grpc, new GrpcMetadataBuilder(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function collect(stream: AsyncIterable<ChatEvent>): Promise<ChatEvent[]> {
|
||||||
|
const events: ChatEvent[] = [];
|
||||||
|
for await (const event of stream) {
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ChatClient (in-process fake server)', () => {
|
||||||
|
it('emits every server event then terminates on done', async () => {
|
||||||
|
chatHandler = (call) => {
|
||||||
|
call.write({ token: { token: 'hello', value: 'Hello' } });
|
||||||
|
call.write({ token: { token: ' world', value: ' world' } });
|
||||||
|
call.write({ done: { stats: { tokensIn: 1, tokensOut: 2, chunksRetrieved: 0 } } });
|
||||||
|
call.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = buildClient();
|
||||||
|
const stream = client.chat({
|
||||||
|
messages: [],
|
||||||
|
conversationId: 'conv-1',
|
||||||
|
model: '',
|
||||||
|
provider: '',
|
||||||
|
toolsAvailable: [],
|
||||||
|
principal: { subject: 'subj-1', roles: ['admin'], attributes: { tenantId: 't' } },
|
||||||
|
});
|
||||||
|
const events = await collect(stream);
|
||||||
|
|
||||||
|
expect(events).toHaveLength(3);
|
||||||
|
expect(events[0]?.token?.value).toBe('Hello');
|
||||||
|
expect(events[2]?.done?.stats?.tokensOut).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('propagates x-client-id and x-correlation-id metadata to the server', async () => {
|
||||||
|
chatHandler = (call) => {
|
||||||
|
call.write({ done: { stats: { tokensIn: 0, tokensOut: 0, chunksRetrieved: 0 } } });
|
||||||
|
call.end();
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = buildClient();
|
||||||
|
await collect(
|
||||||
|
client.chat(
|
||||||
|
{
|
||||||
|
messages: [],
|
||||||
|
conversationId: 'conv-2',
|
||||||
|
model: '',
|
||||||
|
provider: '',
|
||||||
|
toolsAvailable: [],
|
||||||
|
principal: { subject: 'subj-2', roles: [], attributes: {} },
|
||||||
|
},
|
||||||
|
{ correlationId: 'corr-from-test' },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(serverObservations.metadata?.get('x-client-id')).toEqual(['apf-portal-test']);
|
||||||
|
expect(serverObservations.metadata?.get('x-correlation-id')).toEqual(['corr-from-test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cancels the call when the AbortSignal aborts mid-stream', async () => {
|
||||||
|
const cancellationObserved = new Promise<void>((resolve) => {
|
||||||
|
chatHandler = (call) => {
|
||||||
|
call.write({ token: { token: 't1', value: 't1' } });
|
||||||
|
call.on('cancelled', () => resolve());
|
||||||
|
// Deliberately do not `end()` — the test cancels.
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const client = buildClient();
|
||||||
|
const stream = client.chat(
|
||||||
|
{
|
||||||
|
messages: [],
|
||||||
|
conversationId: 'conv-3',
|
||||||
|
model: '',
|
||||||
|
provider: '',
|
||||||
|
toolsAvailable: [],
|
||||||
|
principal: { subject: 'subj-3', roles: [], attributes: {} },
|
||||||
|
},
|
||||||
|
{ signal: controller.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drain the first event then abort.
|
||||||
|
const iter = stream[Symbol.asyncIterator]();
|
||||||
|
await iter.next();
|
||||||
|
controller.abort();
|
||||||
|
|
||||||
|
await cancellationObserved;
|
||||||
|
expect(serverObservations.cancelled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('terminates the stream without data when the AbortSignal is already aborted', async () => {
|
||||||
|
// The signal aborts before the call dial completes. gRPC-js
|
||||||
|
// cancels locally — depending on timing the server may or may
|
||||||
|
// not observe the call, so the spec only locks the client-side
|
||||||
|
// outcome: the stream ends in error and yields no payload.
|
||||||
|
chatHandler = (call) => {
|
||||||
|
call.on('cancelled', () => {
|
||||||
|
// best-effort end on cancellation so the suite never hangs
|
||||||
|
try {
|
||||||
|
call.end();
|
||||||
|
} catch {
|
||||||
|
// already torn down
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
controller.abort();
|
||||||
|
|
||||||
|
const client = buildClient();
|
||||||
|
const stream = client.chat(
|
||||||
|
{
|
||||||
|
messages: [],
|
||||||
|
conversationId: 'conv-4',
|
||||||
|
model: '',
|
||||||
|
provider: '',
|
||||||
|
toolsAvailable: [],
|
||||||
|
principal: { subject: 'subj-4', roles: [], attributes: {} },
|
||||||
|
},
|
||||||
|
{ signal: controller.signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
const events: ChatEvent[] = [];
|
||||||
|
let caught: unknown;
|
||||||
|
try {
|
||||||
|
for await (const ev of stream) {
|
||||||
|
events.push(ev);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
caught = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(events).toHaveLength(0);
|
||||||
|
// gRPC-js may surface the cancellation as an error or as a clean
|
||||||
|
// end-of-stream depending on whether the call had time to dial.
|
||||||
|
// Either way, no payload is emitted — that is the contract.
|
||||||
|
if (caught !== undefined) {
|
||||||
|
expect((caught as { code?: number }).code).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { ClientReadableStream } from '@grpc/grpc-js';
|
||||||
|
import type { ChatEvent, ChatRequest, ChatServiceClient } from '../gen/apf-ai/chat';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import { AI_CHAT_GRPC_CLIENT } from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around the generated `ChatServiceClient` for the
|
||||||
|
* server-streaming `Chat` RPC, per ADR-0024 §"Sub-decision 1".
|
||||||
|
*
|
||||||
|
* The wrapper does three things on top of the raw gRPC stub:
|
||||||
|
*
|
||||||
|
* 1. Injects the `x-client-id` + `x-correlation-id` metadata via
|
||||||
|
* `GrpcMetadataBuilder` so every call carries the contract
|
||||||
|
* headers from `apf-ai-service/docs/contract.md`.
|
||||||
|
* 2. Wires an optional `AbortSignal` to `call.cancel()` so the SSE
|
||||||
|
* bridge (next PR in the chantier) can propagate browser
|
||||||
|
* disconnects up to the AI service in one line.
|
||||||
|
* 3. Returns the raw `ClientReadableStream<ChatEvent>` — Node
|
||||||
|
* `Readable` streams are async-iterable by default, so the SSE
|
||||||
|
* bridge consumes the stream with `for await ... of` and no
|
||||||
|
* intermediate adapter.
|
||||||
|
*
|
||||||
|
* Cancellation flows in both directions: a `.cancel()` from inside
|
||||||
|
* the BFF (timeout, abort) reaches the AI service as a
|
||||||
|
* `grpc.status.CANCELLED`, which translates upstream to
|
||||||
|
* `ServerCallContext.CancellationToken` stopping LLM generation
|
||||||
|
* (per `apf-ai-service/docs/streaming.md`).
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ChatClient {
|
||||||
|
constructor(
|
||||||
|
@Inject(AI_CHAT_GRPC_CLIENT) private readonly grpc: ChatServiceClient,
|
||||||
|
private readonly metadata: GrpcMetadataBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start a streaming Chat call.
|
||||||
|
*
|
||||||
|
* The returned `ClientReadableStream<ChatEvent>` emits one event
|
||||||
|
* per AI service `ChatEvent` and ends after the terminal
|
||||||
|
* `ChatEvent.done` (or on cancellation / error).
|
||||||
|
*
|
||||||
|
* @param request The full proto request, already populated with
|
||||||
|
* the conversation history and a `Principal` (build the
|
||||||
|
* principal via `PrincipalMapper.fromInputs`).
|
||||||
|
* @param options.signal Optional AbortSignal. When it aborts, the
|
||||||
|
* underlying gRPC call is cancelled and the stream closes.
|
||||||
|
* @param options.correlationId Override the metadata
|
||||||
|
* correlation-id (defaults to the active OTel trace-id).
|
||||||
|
*/
|
||||||
|
chat(
|
||||||
|
request: ChatRequest,
|
||||||
|
options: { signal?: AbortSignal; correlationId?: string } = {},
|
||||||
|
): ClientReadableStream<ChatEvent> {
|
||||||
|
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||||
|
const call = this.grpc.chat(request, metadata);
|
||||||
|
|
||||||
|
if (options.signal) {
|
||||||
|
if (options.signal.aborted) {
|
||||||
|
call.cancel();
|
||||||
|
} else {
|
||||||
|
options.signal.addEventListener('abort', () => call.cancel(), { once: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
|
||||||
|
import { trace } from '@opentelemetry/api';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the metadata-injection contract per ADR-0024 §"Metadata contract":
|
||||||
|
*
|
||||||
|
* - x-client-id ← AI_SERVICE_CLIENT_ID env (via AI_CONFIG).
|
||||||
|
* - x-correlation-id ← active OTel span's trace-id when present;
|
||||||
|
* explicit `correlationId` override wins over
|
||||||
|
* the span; otherwise a fresh UUID is minted
|
||||||
|
* so the AI service can still join its audit
|
||||||
|
* row to a unique id even when the portal
|
||||||
|
* request is outside a trace.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CONFIG: AiServiceConfig = {
|
||||||
|
endpoint: 'apf-ai-service:8080',
|
||||||
|
clientId: 'apf-portal-test',
|
||||||
|
useTls: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let activeSpan: ReturnType<typeof trace.getActiveSpan> = undefined;
|
||||||
|
let getActiveSpanSpy: ReturnType<typeof jest.spyOn>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
activeSpan = undefined;
|
||||||
|
getActiveSpanSpy = jest.spyOn(trace, 'getActiveSpan').mockImplementation(() => activeSpan);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
getActiveSpanSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GrpcMetadataBuilder', () => {
|
||||||
|
it('always emits x-client-id from the config', () => {
|
||||||
|
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||||
|
const meta = builder.build();
|
||||||
|
expect(meta.get('x-client-id')).toEqual(['apf-portal-test']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the active OTel trace-id as x-correlation-id when a span is active', () => {
|
||||||
|
activeSpan = {
|
||||||
|
spanContext: () => ({
|
||||||
|
traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||||
|
spanId: 'bbbbbbbbbbbbbbbb',
|
||||||
|
traceFlags: 1,
|
||||||
|
}),
|
||||||
|
} as ReturnType<typeof trace.getActiveSpan>;
|
||||||
|
|
||||||
|
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||||
|
const meta = builder.build();
|
||||||
|
expect(meta.get('x-correlation-id')).toEqual(['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers the explicit options.correlationId over the active span', () => {
|
||||||
|
activeSpan = {
|
||||||
|
spanContext: () => ({
|
||||||
|
traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||||
|
spanId: 'bbbbbbbbbbbbbbbb',
|
||||||
|
traceFlags: 1,
|
||||||
|
}),
|
||||||
|
} as ReturnType<typeof trace.getActiveSpan>;
|
||||||
|
|
||||||
|
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||||
|
const meta = builder.build({ correlationId: 'caller-supplied-id' });
|
||||||
|
expect(meta.get('x-correlation-id')).toEqual(['caller-supplied-id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to a UUID v4 when no span is active and no override is passed', () => {
|
||||||
|
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||||
|
const meta = builder.build();
|
||||||
|
const correlationIds = meta.get('x-correlation-id') as string[];
|
||||||
|
expect(correlationIds).toHaveLength(1);
|
||||||
|
expect(correlationIds[0]).toMatch(
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a fresh Metadata instance on every call (no shared mutation)', () => {
|
||||||
|
const builder = new GrpcMetadataBuilder(CONFIG);
|
||||||
|
const a = builder.build();
|
||||||
|
const b = builder.build();
|
||||||
|
expect(a).not.toBe(b);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Metadata } from '@grpc/grpc-js';
|
||||||
|
import { trace } from '@opentelemetry/api';
|
||||||
|
import { AI_CONFIG } from './tokens';
|
||||||
|
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the gRPC `Metadata` object every outbound call to
|
||||||
|
* `apf-ai-service` must carry, per ADR-0024 §"Metadata contract".
|
||||||
|
*
|
||||||
|
* Two entries:
|
||||||
|
*
|
||||||
|
* - `x-client-id` — deployment slug from `AI_SERVICE_CLIENT_ID`.
|
||||||
|
* Lets the AI service tag its audit rows and metrics with the
|
||||||
|
* calling deployment without inspecting the proto body.
|
||||||
|
* - `x-correlation-id` — W3C `trace-id` of the active OpenTelemetry
|
||||||
|
* span when one exists (per [ADR-0012](../../docs/decisions/0012-observability-pino-opentelemetry.md)),
|
||||||
|
* falling back to an explicit value if passed, falling back to a
|
||||||
|
* freshly-minted UUID v4 as a last resort. The AI service echoes
|
||||||
|
* this id in its audit log and propagates it to Jaeger, so the
|
||||||
|
* portal and AI traces can be joined cross-service.
|
||||||
|
*
|
||||||
|
* The builder is a class (not a static helper) so it composes with
|
||||||
|
* NestJS DI and so it can be swapped for a fake in tests that need
|
||||||
|
* to assert the metadata shape without standing up the OTel SDK.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class GrpcMetadataBuilder {
|
||||||
|
constructor(@Inject(AI_CONFIG) private readonly config: AiServiceConfig) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct the metadata for one outbound call.
|
||||||
|
*
|
||||||
|
* @param options.correlationId Override the auto-resolved value.
|
||||||
|
* Use when the caller already holds a stable id (for example
|
||||||
|
* the SSE bridge controller's request-scoped trace-id) and
|
||||||
|
* wants to bind multiple calls to the same correlation key.
|
||||||
|
* `undefined` is accepted (and equivalent to omitting the
|
||||||
|
* key) so callers can forward an optional value without
|
||||||
|
* a conditional spread.
|
||||||
|
*/
|
||||||
|
build(options: { correlationId?: string | undefined } = {}): Metadata {
|
||||||
|
const meta = new Metadata();
|
||||||
|
meta.set('x-client-id', this.config.clientId);
|
||||||
|
|
||||||
|
const correlationId =
|
||||||
|
options.correlationId ?? trace.getActiveSpan()?.spanContext().traceId ?? randomUUID();
|
||||||
|
meta.set('x-correlation-id', correlationId);
|
||||||
|
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type {
|
||||||
|
DeleteDocumentRequest,
|
||||||
|
DeleteDocumentResponse,
|
||||||
|
DocumentSummary,
|
||||||
|
GetDocumentRequest,
|
||||||
|
IngestDocumentRequest,
|
||||||
|
IngestDocumentResponse,
|
||||||
|
IngestionServiceClient,
|
||||||
|
} from '../gen/apf-ai/ingestion';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import { AI_INGESTION_GRPC_CLIENT } from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around the generated `IngestionServiceClient`.
|
||||||
|
*
|
||||||
|
* Unused by the BFF in v1 — apf-ai-service ships a dedicated CLI
|
||||||
|
* (`tools/Apf.Ai.Ingest/`) for document ingestion during the POC.
|
||||||
|
* The wrapper is in place so the future admin "manage AI corpus"
|
||||||
|
* surface lands as one new controller, not as a new module + a new
|
||||||
|
* client + a new generated stub binding. The unit tests under
|
||||||
|
* `ingestion.client.spec.ts` lock the wire contract today.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class IngestionClient {
|
||||||
|
constructor(
|
||||||
|
@Inject(AI_INGESTION_GRPC_CLIENT) private readonly grpc: IngestionServiceClient,
|
||||||
|
private readonly metadata: GrpcMetadataBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
ingestDocument(
|
||||||
|
request: IngestDocumentRequest,
|
||||||
|
options: { correlationId?: string } = {},
|
||||||
|
): Promise<IngestDocumentResponse> {
|
||||||
|
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||||
|
return new Promise<IngestDocumentResponse>((resolve, reject) => {
|
||||||
|
this.grpc.ingestDocument(request, metadata, (err, response) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getDocument(
|
||||||
|
request: GetDocumentRequest,
|
||||||
|
options: { correlationId?: string } = {},
|
||||||
|
): Promise<DocumentSummary> {
|
||||||
|
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||||
|
return new Promise<DocumentSummary>((resolve, reject) => {
|
||||||
|
this.grpc.getDocument(request, metadata, (err, response) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDocument(
|
||||||
|
request: DeleteDocumentRequest,
|
||||||
|
options: { correlationId?: string } = {},
|
||||||
|
): Promise<DeleteDocumentResponse> {
|
||||||
|
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||||
|
return new Promise<DeleteDocumentResponse>((resolve, reject) => {
|
||||||
|
this.grpc.deleteDocument(request, metadata, (err, response) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type {
|
||||||
|
ListModelsRequest,
|
||||||
|
ListModelsResponse,
|
||||||
|
ModelsServiceClient,
|
||||||
|
} from '../gen/apf-ai/models';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import { AI_MODELS_GRPC_CLIENT } from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around the generated `ModelsServiceClient` for the unary
|
||||||
|
* `ListModels` RPC. Surfaces the active provider + the configured
|
||||||
|
* provider catalogue for use by a future admin UI tile or a v1
|
||||||
|
* "which models are available" health probe.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ModelsClient {
|
||||||
|
constructor(
|
||||||
|
@Inject(AI_MODELS_GRPC_CLIENT) private readonly grpc: ModelsServiceClient,
|
||||||
|
private readonly metadata: GrpcMetadataBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
listModels(
|
||||||
|
request: ListModelsRequest = {},
|
||||||
|
options: { correlationId?: string } = {},
|
||||||
|
): Promise<ListModelsResponse> {
|
||||||
|
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||||
|
return new Promise<ListModelsResponse>((resolve, reject) => {
|
||||||
|
this.grpc.listModels(request, metadata, (err, response) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
import { describe, it, expect, beforeEach, afterAll } from '@jest/globals';
|
||||||
|
import { HashUserIdService } from '../../audit/hash-user-id.service';
|
||||||
|
import { PrincipalMapper } from './principal.mapper';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the Principal-mapping contract per ADR-0024 §"Sub-decision 4":
|
||||||
|
*
|
||||||
|
* - subject = HashUserIdService(oid) — same salt, same algo as the
|
||||||
|
* portal's audit and Pino log streams. This is the
|
||||||
|
* join key between portal and apf-ai-service audit
|
||||||
|
* trails.
|
||||||
|
* - roles = pass-through (inclusive expansion lands in a future
|
||||||
|
* ADR; the wire contract on the AI side does not
|
||||||
|
* change).
|
||||||
|
* - attrs = tenantId is reserved; consumers can layer extra
|
||||||
|
* key/value pairs but tenantId wins on collision.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const STRONG_SALT = randomBytes(32).toString('base64url');
|
||||||
|
const ORIGINAL_SALT = process.env['LOG_USER_ID_SALT'];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env['LOG_USER_ID_SALT'] = STRONG_SALT;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (ORIGINAL_SALT === undefined) {
|
||||||
|
delete process.env['LOG_USER_ID_SALT'];
|
||||||
|
} else {
|
||||||
|
process.env['LOG_USER_ID_SALT'] = ORIGINAL_SALT;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function makeMapper(): { mapper: PrincipalMapper; hashService: HashUserIdService } {
|
||||||
|
const hashService = new HashUserIdService();
|
||||||
|
return { mapper: new PrincipalMapper(hashService), hashService };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PrincipalMapper', () => {
|
||||||
|
it('hashes the Entra oid into the subject via HashUserIdService', () => {
|
||||||
|
const { mapper, hashService } = makeMapper();
|
||||||
|
const principal = mapper.fromInputs({
|
||||||
|
oid: 'user-oid-42',
|
||||||
|
tid: 'tenant-1',
|
||||||
|
roles: ['admin'],
|
||||||
|
});
|
||||||
|
expect(principal.subject).toBe(hashService.hash('user-oid-42'));
|
||||||
|
expect(principal.subject).toMatch(/^[0-9a-f]{16}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes roles through verbatim (no expansion in v1)', () => {
|
||||||
|
const { mapper } = makeMapper();
|
||||||
|
const principal = mapper.fromInputs({
|
||||||
|
oid: 'user-oid-42',
|
||||||
|
tid: 'tenant-1',
|
||||||
|
roles: ['directeur', 'rh'],
|
||||||
|
});
|
||||||
|
expect(principal.roles).toEqual(['directeur', 'rh']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('copies roles so caller mutations do not leak into the proto', () => {
|
||||||
|
const { mapper } = makeMapper();
|
||||||
|
const roles: string[] = ['admin'];
|
||||||
|
const principal = mapper.fromInputs({
|
||||||
|
oid: 'user-oid-42',
|
||||||
|
tid: 'tenant-1',
|
||||||
|
roles,
|
||||||
|
});
|
||||||
|
roles.push('mutated');
|
||||||
|
expect(principal.roles).toEqual(['admin']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('always emits tenantId in attributes', () => {
|
||||||
|
const { mapper } = makeMapper();
|
||||||
|
const principal = mapper.fromInputs({
|
||||||
|
oid: 'user-oid-42',
|
||||||
|
tid: 'tenant-1',
|
||||||
|
roles: [],
|
||||||
|
});
|
||||||
|
expect(principal.attributes).toEqual({ tenantId: 'tenant-1' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges extraAttributes into the attribute map', () => {
|
||||||
|
const { mapper } = makeMapper();
|
||||||
|
const principal = mapper.fromInputs({
|
||||||
|
oid: 'user-oid-42',
|
||||||
|
tid: 'tenant-1',
|
||||||
|
roles: [],
|
||||||
|
extraAttributes: { delegation: 'aquitaine', region: 'fr-sw' },
|
||||||
|
});
|
||||||
|
expect(principal.attributes).toEqual({
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
delegation: 'aquitaine',
|
||||||
|
region: 'fr-sw',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tenantId from `tid` wins over a colliding extraAttributes entry', () => {
|
||||||
|
const { mapper } = makeMapper();
|
||||||
|
const principal = mapper.fromInputs({
|
||||||
|
oid: 'user-oid-42',
|
||||||
|
tid: 'tenant-1',
|
||||||
|
roles: [],
|
||||||
|
extraAttributes: { tenantId: 'tenant-spoofed' },
|
||||||
|
});
|
||||||
|
expect(principal.attributes['tenantId']).toBe('tenant-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produces the same subject for the same oid + salt across instances', () => {
|
||||||
|
const { mapper: m1 } = makeMapper();
|
||||||
|
const { mapper: m2 } = makeMapper();
|
||||||
|
expect(m1.fromInputs({ oid: 'x', tid: 't', roles: [] }).subject).toBe(
|
||||||
|
m2.fromInputs({ oid: 'x', tid: 't', roles: [] }).subject,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { HashUserIdService } from '../../audit/hash-user-id.service';
|
||||||
|
import type { Principal } from '../gen/apf-ai/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read-only view of the session fields the mapper consumes. Decoupled
|
||||||
|
* from `AuthenticatedUser` so callers can build a Principal in tests
|
||||||
|
* or in non-session contexts (a future background-job consumer) by
|
||||||
|
* supplying a plain object — without dragging the full session shape
|
||||||
|
* along.
|
||||||
|
*
|
||||||
|
* Keep the shape minimal: anything beyond `oid + tid + roles` should
|
||||||
|
* land in `attributes` rather than as a new top-level field, so the
|
||||||
|
* proto contract stays stable as the BFF grows new claim sources.
|
||||||
|
*/
|
||||||
|
export interface PrincipalInputs {
|
||||||
|
/** Entra Object ID. Hashed via `HashUserIdService` into the proto `subject` field. */
|
||||||
|
readonly oid: string;
|
||||||
|
/** Entra tenant id. Surfaced as `attributes.tenantId`. */
|
||||||
|
readonly tid: string;
|
||||||
|
/** Roles already resolved upstream by the auth flow. */
|
||||||
|
readonly roles: readonly string[];
|
||||||
|
/**
|
||||||
|
* Free-form extra attributes (delegation slug, region, …) merged
|
||||||
|
* into `attributes` after `tenantId`. Caller-controlled; the
|
||||||
|
* mapper does not enrich. Reserved keys (`tenantId`) overwrite
|
||||||
|
* the caller-provided value to keep the proto contract single-
|
||||||
|
* sourced.
|
||||||
|
*/
|
||||||
|
readonly extraAttributes?: Readonly<Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the `apf.ai.v1.Principal` proto from a portal session,
|
||||||
|
* per ADR-0024 §"Sub-decision 4 — unsigned Principal in the proto body".
|
||||||
|
*
|
||||||
|
* The `subject` field MUST be the same hash the audit module writes
|
||||||
|
* to `audit.events.actor_id_hash` (per [ADR-0013](../../docs/decisions/0013-audit-trail-separated-postgres-append-only.md))
|
||||||
|
* and that Pino's `user_id_hash` log field carries (per [ADR-0012](../../docs/decisions/0012-observability-pino-opentelemetry.md)).
|
||||||
|
* The two services' audit trails only join cleanly when both sides
|
||||||
|
* compute the same value — same Entra `oid`, same salt, same algo.
|
||||||
|
* The mapper delegates to `HashUserIdService` to make that contract
|
||||||
|
* explicit at the wire boundary.
|
||||||
|
*
|
||||||
|
* The `roles` field is passed through verbatim in v1. A future ADR
|
||||||
|
* on role hierarchy (proposed alongside the stargate migration
|
||||||
|
* analysis) will introduce inclusive expansion (`Admin` ⇒
|
||||||
|
* `[admin, directeur, rh, collaborateur]`). At that point the
|
||||||
|
* mapper grows a `RoleExpander` collaborator; the wire shape and
|
||||||
|
* the upstream contract on the AI side stay unchanged.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class PrincipalMapper {
|
||||||
|
constructor(private readonly hashUserId: HashUserIdService) {}
|
||||||
|
|
||||||
|
fromInputs(inputs: PrincipalInputs): Principal {
|
||||||
|
return {
|
||||||
|
subject: this.hashUserId.hash(inputs.oid),
|
||||||
|
roles: [...inputs.roles],
|
||||||
|
attributes: {
|
||||||
|
...(inputs.extraAttributes ?? {}),
|
||||||
|
tenantId: inputs.tid,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
|
||||||
|
import {
|
||||||
|
ChannelCredentials,
|
||||||
|
Server,
|
||||||
|
ServerCredentials,
|
||||||
|
status as GrpcStatus,
|
||||||
|
type Metadata,
|
||||||
|
type sendUnaryData,
|
||||||
|
type ServerUnaryCall,
|
||||||
|
} from '@grpc/grpc-js';
|
||||||
|
import {
|
||||||
|
RagServiceClient,
|
||||||
|
RagServiceService,
|
||||||
|
type RagSearchRequest,
|
||||||
|
type RagSearchResponse,
|
||||||
|
} from '../gen/apf-ai/rag';
|
||||||
|
import { Struct } from '../gen/apf-ai/google/protobuf/struct';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import { RagClient } from './rag.client';
|
||||||
|
import type { AiServiceConfig } from '../../config/check-ai-service-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locks the unary RAG contract: the wrapper promisifies the
|
||||||
|
* callback-style stub, forwards metadata to the server, and
|
||||||
|
* surfaces `ServiceError` rejections with the original gRPC status
|
||||||
|
* code intact.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const CONFIG: AiServiceConfig = {
|
||||||
|
endpoint: '',
|
||||||
|
clientId: 'apf-portal-test',
|
||||||
|
useTls: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ServerObservations {
|
||||||
|
request: RagSearchRequest | null;
|
||||||
|
metadata: Metadata | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let server: Server;
|
||||||
|
let port: number;
|
||||||
|
let serverObservations: ServerObservations;
|
||||||
|
let searchHandler: (
|
||||||
|
call: ServerUnaryCall<RagSearchRequest, RagSearchResponse>,
|
||||||
|
callback: sendUnaryData<RagSearchResponse>,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
server = new Server();
|
||||||
|
server.addService(RagServiceService, {
|
||||||
|
search: (call, callback) => {
|
||||||
|
serverObservations.request = call.request;
|
||||||
|
serverObservations.metadata = call.metadata;
|
||||||
|
searchHandler(call, callback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
port = await new Promise<number>((resolve, reject) => {
|
||||||
|
server.bindAsync('127.0.0.1:0', ServerCredentials.createInsecure(), (err, bound) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(bound);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.tryShutdown((err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
serverObservations = { request: null, metadata: null };
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildClient(): RagClient {
|
||||||
|
const config = { ...CONFIG, endpoint: `127.0.0.1:${port}` };
|
||||||
|
const grpc = new RagServiceClient(config.endpoint, ChannelCredentials.createInsecure());
|
||||||
|
return new RagClient(grpc, new GrpcMetadataBuilder(config));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RagClient (in-process fake server)', () => {
|
||||||
|
it('resolves with the server response on a happy unary call', async () => {
|
||||||
|
searchHandler = (_call, callback) => {
|
||||||
|
callback(null, {
|
||||||
|
chunks: [
|
||||||
|
{
|
||||||
|
id: 'chunk-1',
|
||||||
|
documentId: 'doc-1',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
source: 'rag-test',
|
||||||
|
score: 0.99,
|
||||||
|
metadata: Struct.fromPartial({}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
correlationId: 'echo-corr',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = buildClient();
|
||||||
|
const response = await client.search(
|
||||||
|
{
|
||||||
|
query: 'hello',
|
||||||
|
topK: 3,
|
||||||
|
principal: { subject: 'subj-1', roles: [], attributes: {} },
|
||||||
|
filters: { source: '', documentId: '' },
|
||||||
|
},
|
||||||
|
{ correlationId: 'caller-corr' },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(response.chunks).toHaveLength(1);
|
||||||
|
expect(response.chunks[0]?.id).toBe('chunk-1');
|
||||||
|
expect(serverObservations.metadata?.get('x-correlation-id')).toEqual(['caller-corr']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects with the original gRPC ServiceError on server error', async () => {
|
||||||
|
searchHandler = (_call, callback) => {
|
||||||
|
callback({
|
||||||
|
code: GrpcStatus.PERMISSION_DENIED,
|
||||||
|
details: 'forbidden',
|
||||||
|
metadata: undefined as unknown as Metadata,
|
||||||
|
name: 'Error',
|
||||||
|
message: 'forbidden',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = buildClient();
|
||||||
|
await expect(
|
||||||
|
client.search({
|
||||||
|
query: 'q',
|
||||||
|
topK: 1,
|
||||||
|
principal: { subject: 'subj-2', roles: [], attributes: {} },
|
||||||
|
filters: { source: '', documentId: '' },
|
||||||
|
}),
|
||||||
|
).rejects.toMatchObject({ code: GrpcStatus.PERMISSION_DENIED });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { RagSearchRequest, RagSearchResponse, RagServiceClient } from '../gen/apf-ai/rag';
|
||||||
|
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
|
||||||
|
import { AI_RAG_GRPC_CLIENT } from './tokens';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around the generated `RagServiceClient` for the unary
|
||||||
|
* `Search` RPC, per ADR-0024.
|
||||||
|
*
|
||||||
|
* Promisifies the callback-style gRPC stub and injects the standard
|
||||||
|
* metadata. Errors propagate as the underlying `ServiceError` (with
|
||||||
|
* `code`, `details`, etc.) so callers can map them to portal-side
|
||||||
|
* HTTP statuses without losing the gRPC status code.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class RagClient {
|
||||||
|
constructor(
|
||||||
|
@Inject(AI_RAG_GRPC_CLIENT) private readonly grpc: RagServiceClient,
|
||||||
|
private readonly metadata: GrpcMetadataBuilder,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
search(
|
||||||
|
request: RagSearchRequest,
|
||||||
|
options: { correlationId?: string } = {},
|
||||||
|
): Promise<RagSearchResponse> {
|
||||||
|
const metadata = this.metadata.build({ correlationId: options.correlationId });
|
||||||
|
return new Promise<RagSearchResponse>((resolve, reject) => {
|
||||||
|
this.grpc.search(request, metadata, (err, response) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(response);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Dependency-injection tokens for `AiClientModule`, per ADR-0024.
|
||||||
|
*
|
||||||
|
* The module exposes three classes of provider:
|
||||||
|
*
|
||||||
|
* 1. `AI_CONFIG` — the parsed env vars from `assertAiServiceConfig`.
|
||||||
|
* 2. `AI_CREDENTIALS` — the `ChannelCredentials` (insecure in dev
|
||||||
|
* h2c, SSL in prod h2 + TLS).
|
||||||
|
* 3. Generated gRPC stub instances — one per RPC service from
|
||||||
|
* `apf.ai.v1.*` (Chat, Rag, Ingestion, Models). Wrapped by the
|
||||||
|
* `*.client.ts` services in this folder; the raw stubs stay
|
||||||
|
* behind tokens so the wrapper layer is the only place that
|
||||||
|
* knows about the codegen output.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const AI_CONFIG = Symbol('AI_CONFIG');
|
||||||
|
export const AI_CREDENTIALS = Symbol('AI_CREDENTIALS');
|
||||||
|
export const AI_CHAT_GRPC_CLIENT = Symbol('AI_CHAT_GRPC_CLIENT');
|
||||||
|
export const AI_RAG_GRPC_CLIENT = Symbol('AI_RAG_GRPC_CLIENT');
|
||||||
|
export const AI_INGESTION_GRPC_CLIENT = Symbol('AI_INGESTION_GRPC_CLIENT');
|
||||||
|
export const AI_MODELS_GRPC_CLIENT = Symbol('AI_MODELS_GRPC_CLIENT');
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,765 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.11.8
|
||||||
|
// protoc v3.19.1
|
||||||
|
// source: common.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
import Long from "long";
|
||||||
|
import { Struct } from "./google/protobuf/struct";
|
||||||
|
|
||||||
|
export enum ChatRole {
|
||||||
|
CHAT_ROLE_UNSPECIFIED = 0,
|
||||||
|
CHAT_ROLE_SYSTEM = 1,
|
||||||
|
CHAT_ROLE_USER = 2,
|
||||||
|
CHAT_ROLE_ASSISTANT = 3,
|
||||||
|
CHAT_ROLE_TOOL = 4,
|
||||||
|
UNRECOGNIZED = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chatRoleFromJSON(object: any): ChatRole {
|
||||||
|
switch (object) {
|
||||||
|
case 0:
|
||||||
|
case "CHAT_ROLE_UNSPECIFIED":
|
||||||
|
return ChatRole.CHAT_ROLE_UNSPECIFIED;
|
||||||
|
case 1:
|
||||||
|
case "CHAT_ROLE_SYSTEM":
|
||||||
|
return ChatRole.CHAT_ROLE_SYSTEM;
|
||||||
|
case 2:
|
||||||
|
case "CHAT_ROLE_USER":
|
||||||
|
return ChatRole.CHAT_ROLE_USER;
|
||||||
|
case 3:
|
||||||
|
case "CHAT_ROLE_ASSISTANT":
|
||||||
|
return ChatRole.CHAT_ROLE_ASSISTANT;
|
||||||
|
case 4:
|
||||||
|
case "CHAT_ROLE_TOOL":
|
||||||
|
return ChatRole.CHAT_ROLE_TOOL;
|
||||||
|
case -1:
|
||||||
|
case "UNRECOGNIZED":
|
||||||
|
default:
|
||||||
|
return ChatRole.UNRECOGNIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function chatRoleToJSON(object: ChatRole): string {
|
||||||
|
switch (object) {
|
||||||
|
case ChatRole.CHAT_ROLE_UNSPECIFIED:
|
||||||
|
return "CHAT_ROLE_UNSPECIFIED";
|
||||||
|
case ChatRole.CHAT_ROLE_SYSTEM:
|
||||||
|
return "CHAT_ROLE_SYSTEM";
|
||||||
|
case ChatRole.CHAT_ROLE_USER:
|
||||||
|
return "CHAT_ROLE_USER";
|
||||||
|
case ChatRole.CHAT_ROLE_ASSISTANT:
|
||||||
|
return "CHAT_ROLE_ASSISTANT";
|
||||||
|
case ChatRole.CHAT_ROLE_TOOL:
|
||||||
|
return "CHAT_ROLE_TOOL";
|
||||||
|
case ChatRole.UNRECOGNIZED:
|
||||||
|
default:
|
||||||
|
return "UNRECOGNIZED";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Identity-bearing principal shipped with every request (carried in the JWT for
|
||||||
|
* internal callers, or in the request body for API-key callers — same shape).
|
||||||
|
*/
|
||||||
|
export interface Principal {
|
||||||
|
subject: string;
|
||||||
|
roles: string[];
|
||||||
|
attributes: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Principal_AttributesEntry {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default-deny ACL attached to every ingested chunk.
|
||||||
|
* At least one of allowed_roles or allowed_subjects must be non-empty,
|
||||||
|
* enforced at the controller, the DB CHECK constraint, and the post-filter.
|
||||||
|
*/
|
||||||
|
export interface AclSpec {
|
||||||
|
allowedRoles: string[];
|
||||||
|
allowedSubjects: string[];
|
||||||
|
deniedRoles: string[];
|
||||||
|
requiredAttributes: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AclSpec_RequiredAttributesEntry {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatMessage {
|
||||||
|
role: ChatRole;
|
||||||
|
content: string;
|
||||||
|
toolCallId: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON-schema descriptor for caller-side tool execution. The AI service is
|
||||||
|
* tool-blind: it emits ToolCall events; the caller executes and posts back.
|
||||||
|
*/
|
||||||
|
export interface ToolDescriptor {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
schema?: { [key: string]: any } | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBasePrincipal(): Principal {
|
||||||
|
return { subject: "", roles: [], attributes: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Principal: MessageFns<Principal> = {
|
||||||
|
encode(message: Principal, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.subject !== "") {
|
||||||
|
writer.uint32(10).string(message.subject);
|
||||||
|
}
|
||||||
|
for (const v of message.roles) {
|
||||||
|
writer.uint32(18).string(v!);
|
||||||
|
}
|
||||||
|
globalThis.Object.entries(message.attributes).forEach(([key, value]: [string, string]) => {
|
||||||
|
Principal_AttributesEntry.encode({ key: key as any, value }, writer.uint32(26).fork()).join();
|
||||||
|
});
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Principal {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBasePrincipal();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.subject = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.roles.push(reader.string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry3 = Principal_AttributesEntry.decode(reader, reader.uint32());
|
||||||
|
if (entry3.value !== undefined) {
|
||||||
|
message.attributes[entry3.key] = entry3.value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Principal {
|
||||||
|
return {
|
||||||
|
subject: isSet(object.subject) ? globalThis.String(object.subject) : "",
|
||||||
|
roles: globalThis.Array.isArray(object?.roles) ? object.roles.map((e: any) => globalThis.String(e)) : [],
|
||||||
|
attributes: isObject(object.attributes)
|
||||||
|
? (globalThis.Object.entries(object.attributes) as [string, any][]).reduce(
|
||||||
|
(acc: { [key: string]: string }, [key, value]: [string, any]) => {
|
||||||
|
acc[key] = globalThis.String(value);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Principal): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.subject !== "") {
|
||||||
|
obj.subject = message.subject;
|
||||||
|
}
|
||||||
|
if (message.roles?.length) {
|
||||||
|
obj.roles = message.roles;
|
||||||
|
}
|
||||||
|
if (message.attributes) {
|
||||||
|
const entries = globalThis.Object.entries(message.attributes) as [string, string][];
|
||||||
|
if (entries.length > 0) {
|
||||||
|
obj.attributes = {};
|
||||||
|
entries.forEach(([k, v]) => {
|
||||||
|
obj.attributes[k] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Principal>, I>>(base?: I): Principal {
|
||||||
|
return Principal.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Principal>, I>>(object: I): Principal {
|
||||||
|
const message = createBasePrincipal();
|
||||||
|
message.subject = object.subject ?? "";
|
||||||
|
message.roles = object.roles?.map((e) => e) || [];
|
||||||
|
message.attributes = (globalThis.Object.entries(object.attributes ?? {}) as [string, string][]).reduce(
|
||||||
|
(acc: { [key: string]: string }, [key, value]: [string, string]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
acc[key] = globalThis.String(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBasePrincipal_AttributesEntry(): Principal_AttributesEntry {
|
||||||
|
return { key: "", value: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Principal_AttributesEntry: MessageFns<Principal_AttributesEntry> = {
|
||||||
|
encode(message: Principal_AttributesEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.key !== "") {
|
||||||
|
writer.uint32(10).string(message.key);
|
||||||
|
}
|
||||||
|
if (message.value !== "") {
|
||||||
|
writer.uint32(18).string(message.value);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Principal_AttributesEntry {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBasePrincipal_AttributesEntry();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.key = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.value = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Principal_AttributesEntry {
|
||||||
|
return {
|
||||||
|
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
||||||
|
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Principal_AttributesEntry): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.key !== "") {
|
||||||
|
obj.key = message.key;
|
||||||
|
}
|
||||||
|
if (message.value !== "") {
|
||||||
|
obj.value = message.value;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Principal_AttributesEntry>, I>>(base?: I): Principal_AttributesEntry {
|
||||||
|
return Principal_AttributesEntry.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Principal_AttributesEntry>, I>>(object: I): Principal_AttributesEntry {
|
||||||
|
const message = createBasePrincipal_AttributesEntry();
|
||||||
|
message.key = object.key ?? "";
|
||||||
|
message.value = object.value ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAclSpec(): AclSpec {
|
||||||
|
return { allowedRoles: [], allowedSubjects: [], deniedRoles: [], requiredAttributes: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AclSpec: MessageFns<AclSpec> = {
|
||||||
|
encode(message: AclSpec, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
for (const v of message.allowedRoles) {
|
||||||
|
writer.uint32(10).string(v!);
|
||||||
|
}
|
||||||
|
for (const v of message.allowedSubjects) {
|
||||||
|
writer.uint32(18).string(v!);
|
||||||
|
}
|
||||||
|
for (const v of message.deniedRoles) {
|
||||||
|
writer.uint32(26).string(v!);
|
||||||
|
}
|
||||||
|
globalThis.Object.entries(message.requiredAttributes).forEach(([key, value]: [string, string]) => {
|
||||||
|
AclSpec_RequiredAttributesEntry.encode({ key: key as any, value }, writer.uint32(34).fork()).join();
|
||||||
|
});
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): AclSpec {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAclSpec();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.allowedRoles.push(reader.string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.allowedSubjects.push(reader.string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.deniedRoles.push(reader.string());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry4 = AclSpec_RequiredAttributesEntry.decode(reader, reader.uint32());
|
||||||
|
if (entry4.value !== undefined) {
|
||||||
|
message.requiredAttributes[entry4.key] = entry4.value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): AclSpec {
|
||||||
|
return {
|
||||||
|
allowedRoles: globalThis.Array.isArray(object?.allowedRoles)
|
||||||
|
? object.allowedRoles.map((e: any) => globalThis.String(e))
|
||||||
|
: globalThis.Array.isArray(object?.allowed_roles)
|
||||||
|
? object.allowed_roles.map((e: any) => globalThis.String(e))
|
||||||
|
: [],
|
||||||
|
allowedSubjects: globalThis.Array.isArray(object?.allowedSubjects)
|
||||||
|
? object.allowedSubjects.map((e: any) => globalThis.String(e))
|
||||||
|
: globalThis.Array.isArray(object?.allowed_subjects)
|
||||||
|
? object.allowed_subjects.map((e: any) => globalThis.String(e))
|
||||||
|
: [],
|
||||||
|
deniedRoles: globalThis.Array.isArray(object?.deniedRoles)
|
||||||
|
? object.deniedRoles.map((e: any) => globalThis.String(e))
|
||||||
|
: globalThis.Array.isArray(object?.denied_roles)
|
||||||
|
? object.denied_roles.map((e: any) => globalThis.String(e))
|
||||||
|
: [],
|
||||||
|
requiredAttributes: isObject(object.requiredAttributes)
|
||||||
|
? (globalThis.Object.entries(object.requiredAttributes) as [string, any][]).reduce(
|
||||||
|
(acc: { [key: string]: string }, [key, value]: [string, any]) => {
|
||||||
|
acc[key] = globalThis.String(value);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
: isObject(object.required_attributes)
|
||||||
|
? (globalThis.Object.entries(object.required_attributes) as [string, any][]).reduce(
|
||||||
|
(acc: { [key: string]: string }, [key, value]: [string, any]) => {
|
||||||
|
acc[key] = globalThis.String(value);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: AclSpec): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.allowedRoles?.length) {
|
||||||
|
obj.allowedRoles = message.allowedRoles;
|
||||||
|
}
|
||||||
|
if (message.allowedSubjects?.length) {
|
||||||
|
obj.allowedSubjects = message.allowedSubjects;
|
||||||
|
}
|
||||||
|
if (message.deniedRoles?.length) {
|
||||||
|
obj.deniedRoles = message.deniedRoles;
|
||||||
|
}
|
||||||
|
if (message.requiredAttributes) {
|
||||||
|
const entries = globalThis.Object.entries(message.requiredAttributes) as [string, string][];
|
||||||
|
if (entries.length > 0) {
|
||||||
|
obj.requiredAttributes = {};
|
||||||
|
entries.forEach(([k, v]) => {
|
||||||
|
obj.requiredAttributes[k] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AclSpec>, I>>(base?: I): AclSpec {
|
||||||
|
return AclSpec.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AclSpec>, I>>(object: I): AclSpec {
|
||||||
|
const message = createBaseAclSpec();
|
||||||
|
message.allowedRoles = object.allowedRoles?.map((e) => e) || [];
|
||||||
|
message.allowedSubjects = object.allowedSubjects?.map((e) => e) || [];
|
||||||
|
message.deniedRoles = object.deniedRoles?.map((e) => e) || [];
|
||||||
|
message.requiredAttributes = (globalThis.Object.entries(object.requiredAttributes ?? {}) as [string, string][])
|
||||||
|
.reduce((acc: { [key: string]: string }, [key, value]: [string, string]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
acc[key] = globalThis.String(value);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseAclSpec_RequiredAttributesEntry(): AclSpec_RequiredAttributesEntry {
|
||||||
|
return { key: "", value: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AclSpec_RequiredAttributesEntry: MessageFns<AclSpec_RequiredAttributesEntry> = {
|
||||||
|
encode(message: AclSpec_RequiredAttributesEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.key !== "") {
|
||||||
|
writer.uint32(10).string(message.key);
|
||||||
|
}
|
||||||
|
if (message.value !== "") {
|
||||||
|
writer.uint32(18).string(message.value);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): AclSpec_RequiredAttributesEntry {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseAclSpec_RequiredAttributesEntry();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.key = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.value = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): AclSpec_RequiredAttributesEntry {
|
||||||
|
return {
|
||||||
|
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
||||||
|
value: isSet(object.value) ? globalThis.String(object.value) : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: AclSpec_RequiredAttributesEntry): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.key !== "") {
|
||||||
|
obj.key = message.key;
|
||||||
|
}
|
||||||
|
if (message.value !== "") {
|
||||||
|
obj.value = message.value;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<AclSpec_RequiredAttributesEntry>, I>>(base?: I): AclSpec_RequiredAttributesEntry {
|
||||||
|
return AclSpec_RequiredAttributesEntry.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<AclSpec_RequiredAttributesEntry>, I>>(
|
||||||
|
object: I,
|
||||||
|
): AclSpec_RequiredAttributesEntry {
|
||||||
|
const message = createBaseAclSpec_RequiredAttributesEntry();
|
||||||
|
message.key = object.key ?? "";
|
||||||
|
message.value = object.value ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseChatMessage(): ChatMessage {
|
||||||
|
return { role: 0, content: "", toolCallId: "", name: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChatMessage: MessageFns<ChatMessage> = {
|
||||||
|
encode(message: ChatMessage, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.role !== 0) {
|
||||||
|
writer.uint32(8).int32(message.role);
|
||||||
|
}
|
||||||
|
if (message.content !== "") {
|
||||||
|
writer.uint32(18).string(message.content);
|
||||||
|
}
|
||||||
|
if (message.toolCallId !== "") {
|
||||||
|
writer.uint32(26).string(message.toolCallId);
|
||||||
|
}
|
||||||
|
if (message.name !== "") {
|
||||||
|
writer.uint32(34).string(message.name);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): ChatMessage {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseChatMessage();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.role = reader.int32() as any;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.content = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.toolCallId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.name = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ChatMessage {
|
||||||
|
return {
|
||||||
|
role: isSet(object.role) ? chatRoleFromJSON(object.role) : 0,
|
||||||
|
content: isSet(object.content) ? globalThis.String(object.content) : "",
|
||||||
|
toolCallId: isSet(object.toolCallId)
|
||||||
|
? globalThis.String(object.toolCallId)
|
||||||
|
: isSet(object.tool_call_id)
|
||||||
|
? globalThis.String(object.tool_call_id)
|
||||||
|
: "",
|
||||||
|
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ChatMessage): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.role !== 0) {
|
||||||
|
obj.role = chatRoleToJSON(message.role);
|
||||||
|
}
|
||||||
|
if (message.content !== "") {
|
||||||
|
obj.content = message.content;
|
||||||
|
}
|
||||||
|
if (message.toolCallId !== "") {
|
||||||
|
obj.toolCallId = message.toolCallId;
|
||||||
|
}
|
||||||
|
if (message.name !== "") {
|
||||||
|
obj.name = message.name;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ChatMessage>, I>>(base?: I): ChatMessage {
|
||||||
|
return ChatMessage.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ChatMessage>, I>>(object: I): ChatMessage {
|
||||||
|
const message = createBaseChatMessage();
|
||||||
|
message.role = object.role ?? 0;
|
||||||
|
message.content = object.content ?? "";
|
||||||
|
message.toolCallId = object.toolCallId ?? "";
|
||||||
|
message.name = object.name ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseToolDescriptor(): ToolDescriptor {
|
||||||
|
return { name: "", description: "", schema: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToolDescriptor: MessageFns<ToolDescriptor> = {
|
||||||
|
encode(message: ToolDescriptor, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.name !== "") {
|
||||||
|
writer.uint32(10).string(message.name);
|
||||||
|
}
|
||||||
|
if (message.description !== "") {
|
||||||
|
writer.uint32(18).string(message.description);
|
||||||
|
}
|
||||||
|
if (message.schema !== undefined) {
|
||||||
|
Struct.encode(Struct.wrap(message.schema), writer.uint32(26).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): ToolDescriptor {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseToolDescriptor();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.name = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.description = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.schema = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ToolDescriptor {
|
||||||
|
return {
|
||||||
|
name: isSet(object.name) ? globalThis.String(object.name) : "",
|
||||||
|
description: isSet(object.description) ? globalThis.String(object.description) : "",
|
||||||
|
schema: isObject(object.schema) ? object.schema : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ToolDescriptor): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.name !== "") {
|
||||||
|
obj.name = message.name;
|
||||||
|
}
|
||||||
|
if (message.description !== "") {
|
||||||
|
obj.description = message.description;
|
||||||
|
}
|
||||||
|
if (message.schema !== undefined) {
|
||||||
|
obj.schema = message.schema;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ToolDescriptor>, I>>(base?: I): ToolDescriptor {
|
||||||
|
return ToolDescriptor.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ToolDescriptor>, I>>(object: I): ToolDescriptor {
|
||||||
|
const message = createBaseToolDescriptor();
|
||||||
|
message.name = object.name ?? "";
|
||||||
|
message.description = object.description ?? "";
|
||||||
|
message.schema = object.schema ?? undefined;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function isObject(value: any): boolean {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,614 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.11.8
|
||||||
|
// protoc v3.19.1
|
||||||
|
// source: google/protobuf/struct.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
import Long from "long";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `NullValue` is a singleton enumeration to represent the null value for the
|
||||||
|
* `Value` type union.
|
||||||
|
*
|
||||||
|
* The JSON representation for `NullValue` is JSON `null`.
|
||||||
|
*/
|
||||||
|
export enum NullValue {
|
||||||
|
/** NULL_VALUE - Null value. */
|
||||||
|
NULL_VALUE = 0,
|
||||||
|
UNRECOGNIZED = -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nullValueFromJSON(object: any): NullValue {
|
||||||
|
switch (object) {
|
||||||
|
case 0:
|
||||||
|
case "NULL_VALUE":
|
||||||
|
return NullValue.NULL_VALUE;
|
||||||
|
case -1:
|
||||||
|
case "UNRECOGNIZED":
|
||||||
|
default:
|
||||||
|
return NullValue.UNRECOGNIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nullValueToJSON(object: NullValue): string {
|
||||||
|
switch (object) {
|
||||||
|
case NullValue.NULL_VALUE:
|
||||||
|
return "NULL_VALUE";
|
||||||
|
case NullValue.UNRECOGNIZED:
|
||||||
|
default:
|
||||||
|
return "UNRECOGNIZED";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Struct` represents a structured data value, consisting of fields
|
||||||
|
* which map to dynamically typed values. In some languages, `Struct`
|
||||||
|
* might be supported by a native representation. For example, in
|
||||||
|
* scripting languages like JS a struct is represented as an
|
||||||
|
* object. The details of that representation are described together
|
||||||
|
* with the proto support for the language.
|
||||||
|
*
|
||||||
|
* The JSON representation for `Struct` is JSON object.
|
||||||
|
*/
|
||||||
|
export interface Struct {
|
||||||
|
/** Unordered map of dynamically typed values. */
|
||||||
|
fields: { [key: string]: any | undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Struct_FieldsEntry {
|
||||||
|
key: string;
|
||||||
|
value?: any | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `Value` represents a dynamically typed value which can be either
|
||||||
|
* null, a number, a string, a boolean, a recursive struct value, or a
|
||||||
|
* list of values. A producer of value is expected to set one of these
|
||||||
|
* variants. Absence of any variant indicates an error.
|
||||||
|
*
|
||||||
|
* The JSON representation for `Value` is JSON value.
|
||||||
|
*/
|
||||||
|
export interface Value {
|
||||||
|
/** Represents a null value. */
|
||||||
|
nullValue?:
|
||||||
|
| NullValue
|
||||||
|
| undefined;
|
||||||
|
/** Represents a double value. */
|
||||||
|
numberValue?:
|
||||||
|
| number
|
||||||
|
| undefined;
|
||||||
|
/** Represents a string value. */
|
||||||
|
stringValue?:
|
||||||
|
| string
|
||||||
|
| undefined;
|
||||||
|
/** Represents a boolean value. */
|
||||||
|
boolValue?:
|
||||||
|
| boolean
|
||||||
|
| undefined;
|
||||||
|
/** Represents a structured value. */
|
||||||
|
structValue?:
|
||||||
|
| { [key: string]: any }
|
||||||
|
| undefined;
|
||||||
|
/** Represents a repeated `Value`. */
|
||||||
|
listValue?: Array<any> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `ListValue` is a wrapper around a repeated field of values.
|
||||||
|
*
|
||||||
|
* The JSON representation for `ListValue` is JSON array.
|
||||||
|
*/
|
||||||
|
export interface ListValue {
|
||||||
|
/** Repeated field of dynamically typed values. */
|
||||||
|
values: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseStruct(): Struct {
|
||||||
|
return { fields: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Struct: MessageFns<Struct> & StructWrapperFns = {
|
||||||
|
encode(message: Struct, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
globalThis.Object.entries(message.fields).forEach(([key, value]: [string, any | undefined]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
Struct_FieldsEntry.encode({ key: key as any, value }, writer.uint32(10).fork()).join();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Struct {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseStruct();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry1 = Struct_FieldsEntry.decode(reader, reader.uint32());
|
||||||
|
if (entry1.value !== undefined) {
|
||||||
|
message.fields[entry1.key] = entry1.value;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Struct {
|
||||||
|
return {
|
||||||
|
fields: isObject(object.fields)
|
||||||
|
? (globalThis.Object.entries(object.fields) as [string, any][]).reduce(
|
||||||
|
(acc: { [key: string]: any | undefined }, [key, value]: [string, any]) => {
|
||||||
|
acc[key] = value as any | undefined;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Struct): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.fields) {
|
||||||
|
const entries = globalThis.Object.entries(message.fields) as [string, any | undefined][];
|
||||||
|
if (entries.length > 0) {
|
||||||
|
obj.fields = {};
|
||||||
|
entries.forEach(([k, v]) => {
|
||||||
|
obj.fields[k] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Struct>, I>>(base?: I): Struct {
|
||||||
|
return Struct.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Struct>, I>>(object: I): Struct {
|
||||||
|
const message = createBaseStruct();
|
||||||
|
message.fields = (globalThis.Object.entries(object.fields ?? {}) as [string, any | undefined][]).reduce(
|
||||||
|
(acc: { [key: string]: any | undefined }, [key, value]: [string, any | undefined]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
acc[key] = value;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
wrap(object: { [key: string]: any } | undefined): Struct {
|
||||||
|
const struct = createBaseStruct();
|
||||||
|
|
||||||
|
if (object !== undefined) {
|
||||||
|
for (const key of globalThis.Object.keys(object)) {
|
||||||
|
struct.fields[key] = object[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return struct;
|
||||||
|
},
|
||||||
|
|
||||||
|
unwrap(message: Struct): { [key: string]: any } {
|
||||||
|
const object: { [key: string]: any } = {};
|
||||||
|
if (message.fields) {
|
||||||
|
for (const key of globalThis.Object.keys(message.fields)) {
|
||||||
|
object[key] = message.fields[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseStruct_FieldsEntry(): Struct_FieldsEntry {
|
||||||
|
return { key: "", value: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Struct_FieldsEntry: MessageFns<Struct_FieldsEntry> = {
|
||||||
|
encode(message: Struct_FieldsEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.key !== "") {
|
||||||
|
writer.uint32(10).string(message.key);
|
||||||
|
}
|
||||||
|
if (message.value !== undefined) {
|
||||||
|
Value.encode(Value.wrap(message.value), writer.uint32(18).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Struct_FieldsEntry {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseStruct_FieldsEntry();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.key = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.value = Value.unwrap(Value.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Struct_FieldsEntry {
|
||||||
|
return {
|
||||||
|
key: isSet(object.key) ? globalThis.String(object.key) : "",
|
||||||
|
value: isSet(object?.value) ? object.value : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Struct_FieldsEntry): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.key !== "") {
|
||||||
|
obj.key = message.key;
|
||||||
|
}
|
||||||
|
if (message.value !== undefined) {
|
||||||
|
obj.value = message.value;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Struct_FieldsEntry>, I>>(base?: I): Struct_FieldsEntry {
|
||||||
|
return Struct_FieldsEntry.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Struct_FieldsEntry>, I>>(object: I): Struct_FieldsEntry {
|
||||||
|
const message = createBaseStruct_FieldsEntry();
|
||||||
|
message.key = object.key ?? "";
|
||||||
|
message.value = object.value ?? undefined;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseValue(): Value {
|
||||||
|
return {
|
||||||
|
nullValue: undefined,
|
||||||
|
numberValue: undefined,
|
||||||
|
stringValue: undefined,
|
||||||
|
boolValue: undefined,
|
||||||
|
structValue: undefined,
|
||||||
|
listValue: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Value: MessageFns<Value> & AnyValueWrapperFns = {
|
||||||
|
encode(message: Value, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.nullValue !== undefined) {
|
||||||
|
writer.uint32(8).int32(message.nullValue);
|
||||||
|
}
|
||||||
|
if (message.numberValue !== undefined) {
|
||||||
|
writer.uint32(17).double(message.numberValue);
|
||||||
|
}
|
||||||
|
if (message.stringValue !== undefined) {
|
||||||
|
writer.uint32(26).string(message.stringValue);
|
||||||
|
}
|
||||||
|
if (message.boolValue !== undefined) {
|
||||||
|
writer.uint32(32).bool(message.boolValue);
|
||||||
|
}
|
||||||
|
if (message.structValue !== undefined) {
|
||||||
|
Struct.encode(Struct.wrap(message.structValue), writer.uint32(42).fork()).join();
|
||||||
|
}
|
||||||
|
if (message.listValue !== undefined) {
|
||||||
|
ListValue.encode(ListValue.wrap(message.listValue), writer.uint32(50).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Value {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseValue();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.nullValue = reader.int32() as any;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 17) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.numberValue = reader.double();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.stringValue = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 32) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.boolValue = reader.bool();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.structValue = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 6: {
|
||||||
|
if (tag !== 50) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.listValue = ListValue.unwrap(ListValue.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Value {
|
||||||
|
return {
|
||||||
|
nullValue: isSet(object.nullValue)
|
||||||
|
? nullValueFromJSON(object.nullValue)
|
||||||
|
: isSet(object.null_value)
|
||||||
|
? nullValueFromJSON(object.null_value)
|
||||||
|
: undefined,
|
||||||
|
numberValue: isSet(object.numberValue)
|
||||||
|
? globalThis.Number(object.numberValue)
|
||||||
|
: isSet(object.number_value)
|
||||||
|
? globalThis.Number(object.number_value)
|
||||||
|
: undefined,
|
||||||
|
stringValue: isSet(object.stringValue)
|
||||||
|
? globalThis.String(object.stringValue)
|
||||||
|
: isSet(object.string_value)
|
||||||
|
? globalThis.String(object.string_value)
|
||||||
|
: undefined,
|
||||||
|
boolValue: isSet(object.boolValue)
|
||||||
|
? globalThis.Boolean(object.boolValue)
|
||||||
|
: isSet(object.bool_value)
|
||||||
|
? globalThis.Boolean(object.bool_value)
|
||||||
|
: undefined,
|
||||||
|
structValue: isObject(object.structValue)
|
||||||
|
? object.structValue
|
||||||
|
: isObject(object.struct_value)
|
||||||
|
? object.struct_value
|
||||||
|
: undefined,
|
||||||
|
listValue: globalThis.Array.isArray(object.listValue)
|
||||||
|
? [...object.listValue]
|
||||||
|
: globalThis.Array.isArray(object.list_value)
|
||||||
|
? [...object.list_value]
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Value): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.nullValue !== undefined) {
|
||||||
|
obj.nullValue = nullValueToJSON(message.nullValue);
|
||||||
|
}
|
||||||
|
if (message.numberValue !== undefined) {
|
||||||
|
obj.numberValue = message.numberValue;
|
||||||
|
}
|
||||||
|
if (message.stringValue !== undefined) {
|
||||||
|
obj.stringValue = message.stringValue;
|
||||||
|
}
|
||||||
|
if (message.boolValue !== undefined) {
|
||||||
|
obj.boolValue = message.boolValue;
|
||||||
|
}
|
||||||
|
if (message.structValue !== undefined) {
|
||||||
|
obj.structValue = message.structValue;
|
||||||
|
}
|
||||||
|
if (message.listValue !== undefined) {
|
||||||
|
obj.listValue = message.listValue;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Value>, I>>(base?: I): Value {
|
||||||
|
return Value.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Value>, I>>(object: I): Value {
|
||||||
|
const message = createBaseValue();
|
||||||
|
message.nullValue = object.nullValue ?? undefined;
|
||||||
|
message.numberValue = object.numberValue ?? undefined;
|
||||||
|
message.stringValue = object.stringValue ?? undefined;
|
||||||
|
message.boolValue = object.boolValue ?? undefined;
|
||||||
|
message.structValue = object.structValue ?? undefined;
|
||||||
|
message.listValue = object.listValue ?? undefined;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
wrap(value: any): Value {
|
||||||
|
const result = createBaseValue();
|
||||||
|
if (value === null) {
|
||||||
|
result.nullValue = NullValue.NULL_VALUE;
|
||||||
|
} else if (typeof value === "boolean") {
|
||||||
|
result.boolValue = value;
|
||||||
|
} else if (typeof value === "number") {
|
||||||
|
result.numberValue = value;
|
||||||
|
} else if (typeof value === "string") {
|
||||||
|
result.stringValue = value;
|
||||||
|
} else if (globalThis.Array.isArray(value)) {
|
||||||
|
result.listValue = value;
|
||||||
|
} else if (typeof value === "object") {
|
||||||
|
result.structValue = value;
|
||||||
|
} else if (typeof value !== "undefined") {
|
||||||
|
throw new globalThis.Error("Unsupported any value type: " + typeof value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
unwrap(message: any): string | number | boolean | Object | null | Array<any> | undefined {
|
||||||
|
if (message.stringValue !== undefined) {
|
||||||
|
return message.stringValue;
|
||||||
|
} else if (message?.numberValue !== undefined) {
|
||||||
|
return message.numberValue;
|
||||||
|
} else if (message?.boolValue !== undefined) {
|
||||||
|
return message.boolValue;
|
||||||
|
} else if (message?.structValue !== undefined) {
|
||||||
|
return message.structValue as any;
|
||||||
|
} else if (message?.listValue !== undefined) {
|
||||||
|
return message.listValue;
|
||||||
|
} else if (message?.nullValue !== undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseListValue(): ListValue {
|
||||||
|
return { values: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ListValue: MessageFns<ListValue> & ListValueWrapperFns = {
|
||||||
|
encode(message: ListValue, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
for (const v of message.values) {
|
||||||
|
Value.encode(Value.wrap(v!), writer.uint32(10).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): ListValue {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseListValue();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.values.push(Value.unwrap(Value.decode(reader, reader.uint32())));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ListValue {
|
||||||
|
return { values: globalThis.Array.isArray(object?.values) ? [...object.values] : [] };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ListValue): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.values?.length) {
|
||||||
|
obj.values = message.values;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ListValue>, I>>(base?: I): ListValue {
|
||||||
|
return ListValue.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ListValue>, I>>(object: I): ListValue {
|
||||||
|
const message = createBaseListValue();
|
||||||
|
message.values = object.values?.map((e) => e) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
wrap(array: Array<any> | undefined): ListValue {
|
||||||
|
const result = createBaseListValue();
|
||||||
|
result.values = array ?? [];
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
unwrap(message: ListValue): Array<any> {
|
||||||
|
if (message?.hasOwnProperty("values") && globalThis.Array.isArray(message.values)) {
|
||||||
|
return message.values;
|
||||||
|
} else {
|
||||||
|
return message as any;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function isObject(value: any): boolean {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StructWrapperFns {
|
||||||
|
wrap(object: { [key: string]: any } | undefined): Struct;
|
||||||
|
unwrap(message: Struct): { [key: string]: any };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnyValueWrapperFns {
|
||||||
|
wrap(value: any): Value;
|
||||||
|
unwrap(message: any): string | number | boolean | Object | null | Array<any> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListValueWrapperFns {
|
||||||
|
wrap(array: Array<any> | undefined): ListValue;
|
||||||
|
unwrap(message: ListValue): Array<any>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.11.8
|
||||||
|
// protoc v3.19.1
|
||||||
|
// source: google/protobuf/timestamp.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
import Long from "long";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Timestamp represents a point in time independent of any time zone or local
|
||||||
|
* calendar, encoded as a count of seconds and fractions of seconds at
|
||||||
|
* nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||||
|
* January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||||
|
* Gregorian calendar backwards to year one.
|
||||||
|
*
|
||||||
|
* All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
|
||||||
|
* second table is needed for interpretation, using a [24-hour linear
|
||||||
|
* smear](https://developers.google.com/time/smear).
|
||||||
|
*
|
||||||
|
* The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
|
||||||
|
* restricting to that range, we ensure that we can convert to and from [RFC
|
||||||
|
* 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* Example 1: Compute Timestamp from POSIX `time()`.
|
||||||
|
*
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds(time(NULL));
|
||||||
|
* timestamp.set_nanos(0);
|
||||||
|
*
|
||||||
|
* Example 2: Compute Timestamp from POSIX `gettimeofday()`.
|
||||||
|
*
|
||||||
|
* struct timeval tv;
|
||||||
|
* gettimeofday(&tv, NULL);
|
||||||
|
*
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds(tv.tv_sec);
|
||||||
|
* timestamp.set_nanos(tv.tv_usec * 1000);
|
||||||
|
*
|
||||||
|
* Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||||
|
*
|
||||||
|
* FILETIME ft;
|
||||||
|
* GetSystemTimeAsFileTime(&ft);
|
||||||
|
* UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
||||||
|
*
|
||||||
|
* // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
|
||||||
|
* // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
|
||||||
|
* timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
|
||||||
|
*
|
||||||
|
* Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||||
|
*
|
||||||
|
* long millis = System.currentTimeMillis();
|
||||||
|
*
|
||||||
|
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
|
||||||
|
* .setNanos((int) ((millis % 1000) * 1000000)).build();
|
||||||
|
*
|
||||||
|
* Example 5: Compute Timestamp from Java `Instant.now()`.
|
||||||
|
*
|
||||||
|
* Instant now = Instant.now();
|
||||||
|
*
|
||||||
|
* Timestamp timestamp =
|
||||||
|
* Timestamp.newBuilder().setSeconds(now.getEpochSecond())
|
||||||
|
* .setNanos(now.getNano()).build();
|
||||||
|
*
|
||||||
|
* Example 6: Compute Timestamp from current time in Python.
|
||||||
|
*
|
||||||
|
* timestamp = Timestamp()
|
||||||
|
* timestamp.GetCurrentTime()
|
||||||
|
*
|
||||||
|
* # JSON Mapping
|
||||||
|
*
|
||||||
|
* In JSON format, the Timestamp type is encoded as a string in the
|
||||||
|
* [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||||
|
* format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
|
||||||
|
* where {year} is always expressed using four digits while {month}, {day},
|
||||||
|
* {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
|
||||||
|
* seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||||
|
* are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||||
|
* is required. A proto3 JSON serializer should always use UTC (as indicated by
|
||||||
|
* "Z") when printing the Timestamp type and a proto3 JSON parser should be
|
||||||
|
* able to accept both UTC and other timezones (as indicated by an offset).
|
||||||
|
*
|
||||||
|
* For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
|
||||||
|
* 01:30 UTC on January 15, 2017.
|
||||||
|
*
|
||||||
|
* In JavaScript, one can convert a Date object to this format using the
|
||||||
|
* standard
|
||||||
|
* [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||||
|
* method. In Python, a standard `datetime.datetime` object can be converted
|
||||||
|
* to this format using
|
||||||
|
* [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
|
||||||
|
* the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
|
||||||
|
* the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||||
|
* http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime%2D%2D
|
||||||
|
* ) to obtain a formatter capable of generating timestamps in this format.
|
||||||
|
*/
|
||||||
|
export interface Timestamp {
|
||||||
|
/**
|
||||||
|
* Represents seconds of UTC time since Unix epoch
|
||||||
|
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||||
|
* 9999-12-31T23:59:59Z inclusive.
|
||||||
|
*/
|
||||||
|
seconds: Long;
|
||||||
|
/**
|
||||||
|
* Non-negative fractions of a second at nanosecond resolution. Negative
|
||||||
|
* second values with fractions must still have non-negative nanos values
|
||||||
|
* that count forward in time. Must be from 0 to 999,999,999
|
||||||
|
* inclusive.
|
||||||
|
*/
|
||||||
|
nanos: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseTimestamp(): Timestamp {
|
||||||
|
return { seconds: Long.ZERO, nanos: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Timestamp: MessageFns<Timestamp> = {
|
||||||
|
encode(message: Timestamp, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (!message.seconds.equals(Long.ZERO)) {
|
||||||
|
writer.uint32(8).int64(message.seconds.toString());
|
||||||
|
}
|
||||||
|
if (message.nanos !== 0) {
|
||||||
|
writer.uint32(16).int32(message.nanos);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Timestamp {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseTimestamp();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.seconds = Long.fromString(reader.int64().toString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 16) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.nanos = reader.int32();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Timestamp {
|
||||||
|
return {
|
||||||
|
seconds: isSet(object.seconds) ? Long.fromValue(object.seconds) : Long.ZERO,
|
||||||
|
nanos: isSet(object.nanos) ? globalThis.Number(object.nanos) : 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Timestamp): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (!message.seconds.equals(Long.ZERO)) {
|
||||||
|
obj.seconds = (message.seconds || Long.ZERO).toString();
|
||||||
|
}
|
||||||
|
if (message.nanos !== 0) {
|
||||||
|
obj.nanos = Math.round(message.nanos);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Timestamp>, I>>(base?: I): Timestamp {
|
||||||
|
return Timestamp.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Timestamp>, I>>(object: I): Timestamp {
|
||||||
|
const message = createBaseTimestamp();
|
||||||
|
message.seconds = (object.seconds !== undefined && object.seconds !== null)
|
||||||
|
? Long.fromValue(object.seconds)
|
||||||
|
: Long.ZERO;
|
||||||
|
message.nanos = object.nanos ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,858 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.11.8
|
||||||
|
// protoc v3.19.1
|
||||||
|
// source: ingestion.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
import {
|
||||||
|
type CallOptions,
|
||||||
|
type ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
type ClientOptions,
|
||||||
|
type ClientUnaryCall,
|
||||||
|
type handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
type Metadata,
|
||||||
|
type ServiceError,
|
||||||
|
type UntypedServiceImplementation,
|
||||||
|
} from "@grpc/grpc-js";
|
||||||
|
import Long from "long";
|
||||||
|
import { AclSpec, Principal } from "./common";
|
||||||
|
import { Struct } from "./google/protobuf/struct";
|
||||||
|
import { Timestamp } from "./google/protobuf/timestamp";
|
||||||
|
|
||||||
|
export interface IngestChunk {
|
||||||
|
content: string;
|
||||||
|
metadata?: { [key: string]: any } | undefined;
|
||||||
|
acl?: AclSpec | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngestDocumentRequest {
|
||||||
|
source: string;
|
||||||
|
externalId: string;
|
||||||
|
title: string;
|
||||||
|
chunks: IngestChunk[];
|
||||||
|
principal?: Principal | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngestDocumentResponse {
|
||||||
|
documentId: string;
|
||||||
|
chunksIngested: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetDocumentRequest {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentSummary {
|
||||||
|
id: string;
|
||||||
|
sourceUri: string;
|
||||||
|
title: string;
|
||||||
|
externalId: string;
|
||||||
|
ingestedAt?: Date | undefined;
|
||||||
|
chunkCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDocumentRequest {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteDocumentResponse {
|
||||||
|
deleted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseIngestChunk(): IngestChunk {
|
||||||
|
return { content: "", metadata: undefined, acl: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IngestChunk: MessageFns<IngestChunk> = {
|
||||||
|
encode(message: IngestChunk, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.content !== "") {
|
||||||
|
writer.uint32(10).string(message.content);
|
||||||
|
}
|
||||||
|
if (message.metadata !== undefined) {
|
||||||
|
Struct.encode(Struct.wrap(message.metadata), writer.uint32(18).fork()).join();
|
||||||
|
}
|
||||||
|
if (message.acl !== undefined) {
|
||||||
|
AclSpec.encode(message.acl, writer.uint32(26).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): IngestChunk {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseIngestChunk();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.content = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.metadata = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.acl = AclSpec.decode(reader, reader.uint32());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): IngestChunk {
|
||||||
|
return {
|
||||||
|
content: isSet(object.content) ? globalThis.String(object.content) : "",
|
||||||
|
metadata: isObject(object.metadata) ? object.metadata : undefined,
|
||||||
|
acl: isSet(object.acl) ? AclSpec.fromJSON(object.acl) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: IngestChunk): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.content !== "") {
|
||||||
|
obj.content = message.content;
|
||||||
|
}
|
||||||
|
if (message.metadata !== undefined) {
|
||||||
|
obj.metadata = message.metadata;
|
||||||
|
}
|
||||||
|
if (message.acl !== undefined) {
|
||||||
|
obj.acl = AclSpec.toJSON(message.acl);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<IngestChunk>, I>>(base?: I): IngestChunk {
|
||||||
|
return IngestChunk.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<IngestChunk>, I>>(object: I): IngestChunk {
|
||||||
|
const message = createBaseIngestChunk();
|
||||||
|
message.content = object.content ?? "";
|
||||||
|
message.metadata = object.metadata ?? undefined;
|
||||||
|
message.acl = (object.acl !== undefined && object.acl !== null) ? AclSpec.fromPartial(object.acl) : undefined;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseIngestDocumentRequest(): IngestDocumentRequest {
|
||||||
|
return { source: "", externalId: "", title: "", chunks: [], principal: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IngestDocumentRequest: MessageFns<IngestDocumentRequest> = {
|
||||||
|
encode(message: IngestDocumentRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.source !== "") {
|
||||||
|
writer.uint32(10).string(message.source);
|
||||||
|
}
|
||||||
|
if (message.externalId !== "") {
|
||||||
|
writer.uint32(18).string(message.externalId);
|
||||||
|
}
|
||||||
|
if (message.title !== "") {
|
||||||
|
writer.uint32(26).string(message.title);
|
||||||
|
}
|
||||||
|
for (const v of message.chunks) {
|
||||||
|
IngestChunk.encode(v!, writer.uint32(34).fork()).join();
|
||||||
|
}
|
||||||
|
if (message.principal !== undefined) {
|
||||||
|
Principal.encode(message.principal, writer.uint32(42).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): IngestDocumentRequest {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseIngestDocumentRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.source = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.externalId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.title = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.chunks.push(IngestChunk.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.principal = Principal.decode(reader, reader.uint32());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): IngestDocumentRequest {
|
||||||
|
return {
|
||||||
|
source: isSet(object.source) ? globalThis.String(object.source) : "",
|
||||||
|
externalId: isSet(object.externalId)
|
||||||
|
? globalThis.String(object.externalId)
|
||||||
|
: isSet(object.external_id)
|
||||||
|
? globalThis.String(object.external_id)
|
||||||
|
: "",
|
||||||
|
title: isSet(object.title) ? globalThis.String(object.title) : "",
|
||||||
|
chunks: globalThis.Array.isArray(object?.chunks) ? object.chunks.map((e: any) => IngestChunk.fromJSON(e)) : [],
|
||||||
|
principal: isSet(object.principal) ? Principal.fromJSON(object.principal) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: IngestDocumentRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.source !== "") {
|
||||||
|
obj.source = message.source;
|
||||||
|
}
|
||||||
|
if (message.externalId !== "") {
|
||||||
|
obj.externalId = message.externalId;
|
||||||
|
}
|
||||||
|
if (message.title !== "") {
|
||||||
|
obj.title = message.title;
|
||||||
|
}
|
||||||
|
if (message.chunks?.length) {
|
||||||
|
obj.chunks = message.chunks.map((e) => IngestChunk.toJSON(e));
|
||||||
|
}
|
||||||
|
if (message.principal !== undefined) {
|
||||||
|
obj.principal = Principal.toJSON(message.principal);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<IngestDocumentRequest>, I>>(base?: I): IngestDocumentRequest {
|
||||||
|
return IngestDocumentRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<IngestDocumentRequest>, I>>(object: I): IngestDocumentRequest {
|
||||||
|
const message = createBaseIngestDocumentRequest();
|
||||||
|
message.source = object.source ?? "";
|
||||||
|
message.externalId = object.externalId ?? "";
|
||||||
|
message.title = object.title ?? "";
|
||||||
|
message.chunks = object.chunks?.map((e) => IngestChunk.fromPartial(e)) || [];
|
||||||
|
message.principal = (object.principal !== undefined && object.principal !== null)
|
||||||
|
? Principal.fromPartial(object.principal)
|
||||||
|
: undefined;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseIngestDocumentResponse(): IngestDocumentResponse {
|
||||||
|
return { documentId: "", chunksIngested: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IngestDocumentResponse: MessageFns<IngestDocumentResponse> = {
|
||||||
|
encode(message: IngestDocumentResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.documentId !== "") {
|
||||||
|
writer.uint32(10).string(message.documentId);
|
||||||
|
}
|
||||||
|
if (message.chunksIngested !== 0) {
|
||||||
|
writer.uint32(16).int32(message.chunksIngested);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): IngestDocumentResponse {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseIngestDocumentResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.documentId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 16) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.chunksIngested = reader.int32();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): IngestDocumentResponse {
|
||||||
|
return {
|
||||||
|
documentId: isSet(object.documentId)
|
||||||
|
? globalThis.String(object.documentId)
|
||||||
|
: isSet(object.document_id)
|
||||||
|
? globalThis.String(object.document_id)
|
||||||
|
: "",
|
||||||
|
chunksIngested: isSet(object.chunksIngested)
|
||||||
|
? globalThis.Number(object.chunksIngested)
|
||||||
|
: isSet(object.chunks_ingested)
|
||||||
|
? globalThis.Number(object.chunks_ingested)
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: IngestDocumentResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.documentId !== "") {
|
||||||
|
obj.documentId = message.documentId;
|
||||||
|
}
|
||||||
|
if (message.chunksIngested !== 0) {
|
||||||
|
obj.chunksIngested = Math.round(message.chunksIngested);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<IngestDocumentResponse>, I>>(base?: I): IngestDocumentResponse {
|
||||||
|
return IngestDocumentResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<IngestDocumentResponse>, I>>(object: I): IngestDocumentResponse {
|
||||||
|
const message = createBaseIngestDocumentResponse();
|
||||||
|
message.documentId = object.documentId ?? "";
|
||||||
|
message.chunksIngested = object.chunksIngested ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseGetDocumentRequest(): GetDocumentRequest {
|
||||||
|
return { id: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetDocumentRequest: MessageFns<GetDocumentRequest> = {
|
||||||
|
encode(message: GetDocumentRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.id !== "") {
|
||||||
|
writer.uint32(10).string(message.id);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): GetDocumentRequest {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseGetDocumentRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.id = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): GetDocumentRequest {
|
||||||
|
return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: GetDocumentRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.id !== "") {
|
||||||
|
obj.id = message.id;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<GetDocumentRequest>, I>>(base?: I): GetDocumentRequest {
|
||||||
|
return GetDocumentRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<GetDocumentRequest>, I>>(object: I): GetDocumentRequest {
|
||||||
|
const message = createBaseGetDocumentRequest();
|
||||||
|
message.id = object.id ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDocumentSummary(): DocumentSummary {
|
||||||
|
return { id: "", sourceUri: "", title: "", externalId: "", ingestedAt: undefined, chunkCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DocumentSummary: MessageFns<DocumentSummary> = {
|
||||||
|
encode(message: DocumentSummary, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.id !== "") {
|
||||||
|
writer.uint32(10).string(message.id);
|
||||||
|
}
|
||||||
|
if (message.sourceUri !== "") {
|
||||||
|
writer.uint32(18).string(message.sourceUri);
|
||||||
|
}
|
||||||
|
if (message.title !== "") {
|
||||||
|
writer.uint32(26).string(message.title);
|
||||||
|
}
|
||||||
|
if (message.externalId !== "") {
|
||||||
|
writer.uint32(34).string(message.externalId);
|
||||||
|
}
|
||||||
|
if (message.ingestedAt !== undefined) {
|
||||||
|
Timestamp.encode(toTimestamp(message.ingestedAt), writer.uint32(42).fork()).join();
|
||||||
|
}
|
||||||
|
if (message.chunkCount !== 0) {
|
||||||
|
writer.uint32(48).int32(message.chunkCount);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): DocumentSummary {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDocumentSummary();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.id = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.sourceUri = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.title = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.externalId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.ingestedAt = fromTimestamp(Timestamp.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 6: {
|
||||||
|
if (tag !== 48) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.chunkCount = reader.int32();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): DocumentSummary {
|
||||||
|
return {
|
||||||
|
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||||
|
sourceUri: isSet(object.sourceUri)
|
||||||
|
? globalThis.String(object.sourceUri)
|
||||||
|
: isSet(object.source_uri)
|
||||||
|
? globalThis.String(object.source_uri)
|
||||||
|
: "",
|
||||||
|
title: isSet(object.title) ? globalThis.String(object.title) : "",
|
||||||
|
externalId: isSet(object.externalId)
|
||||||
|
? globalThis.String(object.externalId)
|
||||||
|
: isSet(object.external_id)
|
||||||
|
? globalThis.String(object.external_id)
|
||||||
|
: "",
|
||||||
|
ingestedAt: isSet(object.ingestedAt)
|
||||||
|
? fromJsonTimestamp(object.ingestedAt)
|
||||||
|
: isSet(object.ingested_at)
|
||||||
|
? fromJsonTimestamp(object.ingested_at)
|
||||||
|
: undefined,
|
||||||
|
chunkCount: isSet(object.chunkCount)
|
||||||
|
? globalThis.Number(object.chunkCount)
|
||||||
|
: isSet(object.chunk_count)
|
||||||
|
? globalThis.Number(object.chunk_count)
|
||||||
|
: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: DocumentSummary): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.id !== "") {
|
||||||
|
obj.id = message.id;
|
||||||
|
}
|
||||||
|
if (message.sourceUri !== "") {
|
||||||
|
obj.sourceUri = message.sourceUri;
|
||||||
|
}
|
||||||
|
if (message.title !== "") {
|
||||||
|
obj.title = message.title;
|
||||||
|
}
|
||||||
|
if (message.externalId !== "") {
|
||||||
|
obj.externalId = message.externalId;
|
||||||
|
}
|
||||||
|
if (message.ingestedAt !== undefined) {
|
||||||
|
obj.ingestedAt = message.ingestedAt.toISOString();
|
||||||
|
}
|
||||||
|
if (message.chunkCount !== 0) {
|
||||||
|
obj.chunkCount = Math.round(message.chunkCount);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DocumentSummary>, I>>(base?: I): DocumentSummary {
|
||||||
|
return DocumentSummary.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DocumentSummary>, I>>(object: I): DocumentSummary {
|
||||||
|
const message = createBaseDocumentSummary();
|
||||||
|
message.id = object.id ?? "";
|
||||||
|
message.sourceUri = object.sourceUri ?? "";
|
||||||
|
message.title = object.title ?? "";
|
||||||
|
message.externalId = object.externalId ?? "";
|
||||||
|
message.ingestedAt = object.ingestedAt ?? undefined;
|
||||||
|
message.chunkCount = object.chunkCount ?? 0;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteDocumentRequest(): DeleteDocumentRequest {
|
||||||
|
return { id: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteDocumentRequest: MessageFns<DeleteDocumentRequest> = {
|
||||||
|
encode(message: DeleteDocumentRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.id !== "") {
|
||||||
|
writer.uint32(10).string(message.id);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): DeleteDocumentRequest {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteDocumentRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.id = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): DeleteDocumentRequest {
|
||||||
|
return { id: isSet(object.id) ? globalThis.String(object.id) : "" };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: DeleteDocumentRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.id !== "") {
|
||||||
|
obj.id = message.id;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteDocumentRequest>, I>>(base?: I): DeleteDocumentRequest {
|
||||||
|
return DeleteDocumentRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteDocumentRequest>, I>>(object: I): DeleteDocumentRequest {
|
||||||
|
const message = createBaseDeleteDocumentRequest();
|
||||||
|
message.id = object.id ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseDeleteDocumentResponse(): DeleteDocumentResponse {
|
||||||
|
return { deleted: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteDocumentResponse: MessageFns<DeleteDocumentResponse> = {
|
||||||
|
encode(message: DeleteDocumentResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.deleted !== false) {
|
||||||
|
writer.uint32(8).bool(message.deleted);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): DeleteDocumentResponse {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseDeleteDocumentResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 8) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.deleted = reader.bool();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): DeleteDocumentResponse {
|
||||||
|
return { deleted: isSet(object.deleted) ? globalThis.Boolean(object.deleted) : false };
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: DeleteDocumentResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.deleted !== false) {
|
||||||
|
obj.deleted = message.deleted;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<DeleteDocumentResponse>, I>>(base?: I): DeleteDocumentResponse {
|
||||||
|
return DeleteDocumentResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<DeleteDocumentResponse>, I>>(object: I): DeleteDocumentResponse {
|
||||||
|
const message = createBaseDeleteDocumentResponse();
|
||||||
|
message.deleted = object.deleted ?? false;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Document ingestion. Every chunk must carry an explicit ACL: empty
|
||||||
|
* ACL is default-deny and rejected with InvalidArgument (gRPC) /
|
||||||
|
* 422 (HTTP).
|
||||||
|
*/
|
||||||
|
export type IngestionServiceService = typeof IngestionServiceService;
|
||||||
|
export const IngestionServiceService = {
|
||||||
|
ingestDocument: {
|
||||||
|
path: "/apf.ai.v1.IngestionService/IngestDocument" as const,
|
||||||
|
requestStream: false as const,
|
||||||
|
responseStream: false as const,
|
||||||
|
requestSerialize: (value: IngestDocumentRequest): Buffer =>
|
||||||
|
Buffer.from(IngestDocumentRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer): IngestDocumentRequest => IngestDocumentRequest.decode(value),
|
||||||
|
responseSerialize: (value: IngestDocumentResponse): Buffer =>
|
||||||
|
Buffer.from(IngestDocumentResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer): IngestDocumentResponse => IngestDocumentResponse.decode(value),
|
||||||
|
},
|
||||||
|
getDocument: {
|
||||||
|
path: "/apf.ai.v1.IngestionService/GetDocument" as const,
|
||||||
|
requestStream: false as const,
|
||||||
|
responseStream: false as const,
|
||||||
|
requestSerialize: (value: GetDocumentRequest): Buffer => Buffer.from(GetDocumentRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer): GetDocumentRequest => GetDocumentRequest.decode(value),
|
||||||
|
responseSerialize: (value: DocumentSummary): Buffer => Buffer.from(DocumentSummary.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer): DocumentSummary => DocumentSummary.decode(value),
|
||||||
|
},
|
||||||
|
deleteDocument: {
|
||||||
|
path: "/apf.ai.v1.IngestionService/DeleteDocument" as const,
|
||||||
|
requestStream: false as const,
|
||||||
|
responseStream: false as const,
|
||||||
|
requestSerialize: (value: DeleteDocumentRequest): Buffer =>
|
||||||
|
Buffer.from(DeleteDocumentRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer): DeleteDocumentRequest => DeleteDocumentRequest.decode(value),
|
||||||
|
responseSerialize: (value: DeleteDocumentResponse): Buffer =>
|
||||||
|
Buffer.from(DeleteDocumentResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer): DeleteDocumentResponse => DeleteDocumentResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface IngestionServiceServer extends UntypedServiceImplementation {
|
||||||
|
ingestDocument: handleUnaryCall<IngestDocumentRequest, IngestDocumentResponse>;
|
||||||
|
getDocument: handleUnaryCall<GetDocumentRequest, DocumentSummary>;
|
||||||
|
deleteDocument: handleUnaryCall<DeleteDocumentRequest, DeleteDocumentResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IngestionServiceClient extends Client {
|
||||||
|
ingestDocument(
|
||||||
|
request: IngestDocumentRequest,
|
||||||
|
callback: (error: ServiceError | null, response: IngestDocumentResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
ingestDocument(
|
||||||
|
request: IngestDocumentRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: IngestDocumentResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
ingestDocument(
|
||||||
|
request: IngestDocumentRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: IngestDocumentResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
getDocument(
|
||||||
|
request: GetDocumentRequest,
|
||||||
|
callback: (error: ServiceError | null, response: DocumentSummary) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
getDocument(
|
||||||
|
request: GetDocumentRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: DocumentSummary) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
getDocument(
|
||||||
|
request: GetDocumentRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: DocumentSummary) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
deleteDocument(
|
||||||
|
request: DeleteDocumentRequest,
|
||||||
|
callback: (error: ServiceError | null, response: DeleteDocumentResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
deleteDocument(
|
||||||
|
request: DeleteDocumentRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: DeleteDocumentResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
deleteDocument(
|
||||||
|
request: DeleteDocumentRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: DeleteDocumentResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IngestionServiceClient = makeGenericClientConstructor(
|
||||||
|
IngestionServiceService,
|
||||||
|
"apf.ai.v1.IngestionService",
|
||||||
|
) as unknown as {
|
||||||
|
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): IngestionServiceClient;
|
||||||
|
service: typeof IngestionServiceService;
|
||||||
|
serviceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function toTimestamp(date: Date): Timestamp {
|
||||||
|
const seconds = numberToLong(Math.trunc(date.getTime() / 1_000));
|
||||||
|
const nanos = (date.getTime() % 1_000) * 1_000_000;
|
||||||
|
return { seconds, nanos };
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromTimestamp(t: Timestamp): Date {
|
||||||
|
let millis = (t.seconds.toNumber() || 0) * 1_000;
|
||||||
|
millis += (t.nanos || 0) / 1_000_000;
|
||||||
|
return new globalThis.Date(millis);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromJsonTimestamp(o: any): Date {
|
||||||
|
if (o instanceof globalThis.Date) {
|
||||||
|
return o;
|
||||||
|
} else if (typeof o === "string") {
|
||||||
|
return new globalThis.Date(o);
|
||||||
|
} else {
|
||||||
|
return fromTimestamp(Timestamp.fromJSON(o));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberToLong(number: number) {
|
||||||
|
return Long.fromNumber(number);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(value: any): boolean {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,355 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.11.8
|
||||||
|
// protoc v3.19.1
|
||||||
|
// source: models.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
import {
|
||||||
|
type CallOptions,
|
||||||
|
type ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
type ClientOptions,
|
||||||
|
type ClientUnaryCall,
|
||||||
|
type handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
type Metadata,
|
||||||
|
type ServiceError,
|
||||||
|
type UntypedServiceImplementation,
|
||||||
|
} from "@grpc/grpc-js";
|
||||||
|
import Long from "long";
|
||||||
|
|
||||||
|
export interface ListModelsRequest {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProviderInfo {
|
||||||
|
discriminator: string;
|
||||||
|
capabilities: string;
|
||||||
|
endpoint: string;
|
||||||
|
model: string;
|
||||||
|
embeddingModel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListModelsResponse {
|
||||||
|
active: string;
|
||||||
|
providers: ProviderInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseListModelsRequest(): ListModelsRequest {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ListModelsRequest: MessageFns<ListModelsRequest> = {
|
||||||
|
encode(_: ListModelsRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): ListModelsRequest {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseListModelsRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(_: any): ListModelsRequest {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(_: ListModelsRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ListModelsRequest>, I>>(base?: I): ListModelsRequest {
|
||||||
|
return ListModelsRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ListModelsRequest>, I>>(_: I): ListModelsRequest {
|
||||||
|
const message = createBaseListModelsRequest();
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseProviderInfo(): ProviderInfo {
|
||||||
|
return { discriminator: "", capabilities: "", endpoint: "", model: "", embeddingModel: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProviderInfo: MessageFns<ProviderInfo> = {
|
||||||
|
encode(message: ProviderInfo, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.discriminator !== "") {
|
||||||
|
writer.uint32(10).string(message.discriminator);
|
||||||
|
}
|
||||||
|
if (message.capabilities !== "") {
|
||||||
|
writer.uint32(18).string(message.capabilities);
|
||||||
|
}
|
||||||
|
if (message.endpoint !== "") {
|
||||||
|
writer.uint32(26).string(message.endpoint);
|
||||||
|
}
|
||||||
|
if (message.model !== "") {
|
||||||
|
writer.uint32(34).string(message.model);
|
||||||
|
}
|
||||||
|
if (message.embeddingModel !== "") {
|
||||||
|
writer.uint32(42).string(message.embeddingModel);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): ProviderInfo {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseProviderInfo();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.discriminator = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.capabilities = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.endpoint = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.model = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.embeddingModel = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ProviderInfo {
|
||||||
|
return {
|
||||||
|
discriminator: isSet(object.discriminator) ? globalThis.String(object.discriminator) : "",
|
||||||
|
capabilities: isSet(object.capabilities) ? globalThis.String(object.capabilities) : "",
|
||||||
|
endpoint: isSet(object.endpoint) ? globalThis.String(object.endpoint) : "",
|
||||||
|
model: isSet(object.model) ? globalThis.String(object.model) : "",
|
||||||
|
embeddingModel: isSet(object.embeddingModel)
|
||||||
|
? globalThis.String(object.embeddingModel)
|
||||||
|
: isSet(object.embedding_model)
|
||||||
|
? globalThis.String(object.embedding_model)
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ProviderInfo): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.discriminator !== "") {
|
||||||
|
obj.discriminator = message.discriminator;
|
||||||
|
}
|
||||||
|
if (message.capabilities !== "") {
|
||||||
|
obj.capabilities = message.capabilities;
|
||||||
|
}
|
||||||
|
if (message.endpoint !== "") {
|
||||||
|
obj.endpoint = message.endpoint;
|
||||||
|
}
|
||||||
|
if (message.model !== "") {
|
||||||
|
obj.model = message.model;
|
||||||
|
}
|
||||||
|
if (message.embeddingModel !== "") {
|
||||||
|
obj.embeddingModel = message.embeddingModel;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ProviderInfo>, I>>(base?: I): ProviderInfo {
|
||||||
|
return ProviderInfo.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ProviderInfo>, I>>(object: I): ProviderInfo {
|
||||||
|
const message = createBaseProviderInfo();
|
||||||
|
message.discriminator = object.discriminator ?? "";
|
||||||
|
message.capabilities = object.capabilities ?? "";
|
||||||
|
message.endpoint = object.endpoint ?? "";
|
||||||
|
message.model = object.model ?? "";
|
||||||
|
message.embeddingModel = object.embeddingModel ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseListModelsResponse(): ListModelsResponse {
|
||||||
|
return { active: "", providers: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ListModelsResponse: MessageFns<ListModelsResponse> = {
|
||||||
|
encode(message: ListModelsResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.active !== "") {
|
||||||
|
writer.uint32(10).string(message.active);
|
||||||
|
}
|
||||||
|
for (const v of message.providers) {
|
||||||
|
ProviderInfo.encode(v!, writer.uint32(18).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): ListModelsResponse {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseListModelsResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.active = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.providers.push(ProviderInfo.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): ListModelsResponse {
|
||||||
|
return {
|
||||||
|
active: isSet(object.active) ? globalThis.String(object.active) : "",
|
||||||
|
providers: globalThis.Array.isArray(object?.providers)
|
||||||
|
? object.providers.map((e: any) => ProviderInfo.fromJSON(e))
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: ListModelsResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.active !== "") {
|
||||||
|
obj.active = message.active;
|
||||||
|
}
|
||||||
|
if (message.providers?.length) {
|
||||||
|
obj.providers = message.providers.map((e) => ProviderInfo.toJSON(e));
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<ListModelsResponse>, I>>(base?: I): ListModelsResponse {
|
||||||
|
return ListModelsResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<ListModelsResponse>, I>>(object: I): ListModelsResponse {
|
||||||
|
const message = createBaseListModelsResponse();
|
||||||
|
message.active = object.active ?? "";
|
||||||
|
message.providers = object.providers?.map((e) => ProviderInfo.fromPartial(e)) || [];
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ModelsServiceService = typeof ModelsServiceService;
|
||||||
|
export const ModelsServiceService = {
|
||||||
|
listModels: {
|
||||||
|
path: "/apf.ai.v1.ModelsService/ListModels" as const,
|
||||||
|
requestStream: false as const,
|
||||||
|
responseStream: false as const,
|
||||||
|
requestSerialize: (value: ListModelsRequest): Buffer => Buffer.from(ListModelsRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer): ListModelsRequest => ListModelsRequest.decode(value),
|
||||||
|
responseSerialize: (value: ListModelsResponse): Buffer => Buffer.from(ListModelsResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer): ListModelsResponse => ListModelsResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface ModelsServiceServer extends UntypedServiceImplementation {
|
||||||
|
listModels: handleUnaryCall<ListModelsRequest, ListModelsResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelsServiceClient extends Client {
|
||||||
|
listModels(
|
||||||
|
request: ListModelsRequest,
|
||||||
|
callback: (error: ServiceError | null, response: ListModelsResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
listModels(
|
||||||
|
request: ListModelsRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: ListModelsResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
listModels(
|
||||||
|
request: ListModelsRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: ListModelsResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModelsServiceClient = makeGenericClientConstructor(
|
||||||
|
ModelsServiceService,
|
||||||
|
"apf.ai.v1.ModelsService",
|
||||||
|
) as unknown as {
|
||||||
|
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): ModelsServiceClient;
|
||||||
|
service: typeof ModelsServiceService;
|
||||||
|
serviceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,544 @@
|
|||||||
|
// Code generated by protoc-gen-ts_proto. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-ts_proto v2.11.8
|
||||||
|
// protoc v3.19.1
|
||||||
|
// source: rag.proto
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
|
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
||||||
|
import {
|
||||||
|
type CallOptions,
|
||||||
|
type ChannelCredentials,
|
||||||
|
Client,
|
||||||
|
type ClientOptions,
|
||||||
|
type ClientUnaryCall,
|
||||||
|
type handleUnaryCall,
|
||||||
|
makeGenericClientConstructor,
|
||||||
|
type Metadata,
|
||||||
|
type ServiceError,
|
||||||
|
type UntypedServiceImplementation,
|
||||||
|
} from "@grpc/grpc-js";
|
||||||
|
import Long from "long";
|
||||||
|
import { Principal } from "./common";
|
||||||
|
import { Struct } from "./google/protobuf/struct";
|
||||||
|
|
||||||
|
export interface RagSearchFilters {
|
||||||
|
source: string;
|
||||||
|
documentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RagSearchRequest {
|
||||||
|
query: string;
|
||||||
|
topK: number;
|
||||||
|
filters?: RagSearchFilters | undefined;
|
||||||
|
principal?: Principal | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Chunk {
|
||||||
|
id: string;
|
||||||
|
documentId: string;
|
||||||
|
content: string;
|
||||||
|
source: string;
|
||||||
|
score: number;
|
||||||
|
metadata?: { [key: string]: any } | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RagSearchResponse {
|
||||||
|
chunks: Chunk[];
|
||||||
|
correlationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBaseRagSearchFilters(): RagSearchFilters {
|
||||||
|
return { source: "", documentId: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RagSearchFilters: MessageFns<RagSearchFilters> = {
|
||||||
|
encode(message: RagSearchFilters, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.source !== "") {
|
||||||
|
writer.uint32(10).string(message.source);
|
||||||
|
}
|
||||||
|
if (message.documentId !== "") {
|
||||||
|
writer.uint32(18).string(message.documentId);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): RagSearchFilters {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseRagSearchFilters();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.source = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.documentId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): RagSearchFilters {
|
||||||
|
return {
|
||||||
|
source: isSet(object.source) ? globalThis.String(object.source) : "",
|
||||||
|
documentId: isSet(object.documentId)
|
||||||
|
? globalThis.String(object.documentId)
|
||||||
|
: isSet(object.document_id)
|
||||||
|
? globalThis.String(object.document_id)
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: RagSearchFilters): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.source !== "") {
|
||||||
|
obj.source = message.source;
|
||||||
|
}
|
||||||
|
if (message.documentId !== "") {
|
||||||
|
obj.documentId = message.documentId;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<RagSearchFilters>, I>>(base?: I): RagSearchFilters {
|
||||||
|
return RagSearchFilters.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<RagSearchFilters>, I>>(object: I): RagSearchFilters {
|
||||||
|
const message = createBaseRagSearchFilters();
|
||||||
|
message.source = object.source ?? "";
|
||||||
|
message.documentId = object.documentId ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseRagSearchRequest(): RagSearchRequest {
|
||||||
|
return { query: "", topK: 0, filters: undefined, principal: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RagSearchRequest: MessageFns<RagSearchRequest> = {
|
||||||
|
encode(message: RagSearchRequest, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.query !== "") {
|
||||||
|
writer.uint32(10).string(message.query);
|
||||||
|
}
|
||||||
|
if (message.topK !== 0) {
|
||||||
|
writer.uint32(16).int32(message.topK);
|
||||||
|
}
|
||||||
|
if (message.filters !== undefined) {
|
||||||
|
RagSearchFilters.encode(message.filters, writer.uint32(26).fork()).join();
|
||||||
|
}
|
||||||
|
if (message.principal !== undefined) {
|
||||||
|
Principal.encode(message.principal, writer.uint32(34).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): RagSearchRequest {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseRagSearchRequest();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.query = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 16) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.topK = reader.int32();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.filters = RagSearchFilters.decode(reader, reader.uint32());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.principal = Principal.decode(reader, reader.uint32());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): RagSearchRequest {
|
||||||
|
return {
|
||||||
|
query: isSet(object.query) ? globalThis.String(object.query) : "",
|
||||||
|
topK: isSet(object.topK)
|
||||||
|
? globalThis.Number(object.topK)
|
||||||
|
: isSet(object.top_k)
|
||||||
|
? globalThis.Number(object.top_k)
|
||||||
|
: 0,
|
||||||
|
filters: isSet(object.filters) ? RagSearchFilters.fromJSON(object.filters) : undefined,
|
||||||
|
principal: isSet(object.principal) ? Principal.fromJSON(object.principal) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: RagSearchRequest): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.query !== "") {
|
||||||
|
obj.query = message.query;
|
||||||
|
}
|
||||||
|
if (message.topK !== 0) {
|
||||||
|
obj.topK = Math.round(message.topK);
|
||||||
|
}
|
||||||
|
if (message.filters !== undefined) {
|
||||||
|
obj.filters = RagSearchFilters.toJSON(message.filters);
|
||||||
|
}
|
||||||
|
if (message.principal !== undefined) {
|
||||||
|
obj.principal = Principal.toJSON(message.principal);
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<RagSearchRequest>, I>>(base?: I): RagSearchRequest {
|
||||||
|
return RagSearchRequest.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<RagSearchRequest>, I>>(object: I): RagSearchRequest {
|
||||||
|
const message = createBaseRagSearchRequest();
|
||||||
|
message.query = object.query ?? "";
|
||||||
|
message.topK = object.topK ?? 0;
|
||||||
|
message.filters = (object.filters !== undefined && object.filters !== null)
|
||||||
|
? RagSearchFilters.fromPartial(object.filters)
|
||||||
|
: undefined;
|
||||||
|
message.principal = (object.principal !== undefined && object.principal !== null)
|
||||||
|
? Principal.fromPartial(object.principal)
|
||||||
|
: undefined;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseChunk(): Chunk {
|
||||||
|
return { id: "", documentId: "", content: "", source: "", score: 0, metadata: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Chunk: MessageFns<Chunk> = {
|
||||||
|
encode(message: Chunk, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
if (message.id !== "") {
|
||||||
|
writer.uint32(10).string(message.id);
|
||||||
|
}
|
||||||
|
if (message.documentId !== "") {
|
||||||
|
writer.uint32(18).string(message.documentId);
|
||||||
|
}
|
||||||
|
if (message.content !== "") {
|
||||||
|
writer.uint32(26).string(message.content);
|
||||||
|
}
|
||||||
|
if (message.source !== "") {
|
||||||
|
writer.uint32(34).string(message.source);
|
||||||
|
}
|
||||||
|
if (message.score !== 0) {
|
||||||
|
writer.uint32(41).double(message.score);
|
||||||
|
}
|
||||||
|
if (message.metadata !== undefined) {
|
||||||
|
Struct.encode(Struct.wrap(message.metadata), writer.uint32(50).fork()).join();
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): Chunk {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseChunk();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.id = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.documentId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
if (tag !== 26) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.content = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
if (tag !== 34) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.source = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 41) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.score = reader.double();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 6: {
|
||||||
|
if (tag !== 50) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.metadata = Struct.unwrap(Struct.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): Chunk {
|
||||||
|
return {
|
||||||
|
id: isSet(object.id) ? globalThis.String(object.id) : "",
|
||||||
|
documentId: isSet(object.documentId)
|
||||||
|
? globalThis.String(object.documentId)
|
||||||
|
: isSet(object.document_id)
|
||||||
|
? globalThis.String(object.document_id)
|
||||||
|
: "",
|
||||||
|
content: isSet(object.content) ? globalThis.String(object.content) : "",
|
||||||
|
source: isSet(object.source) ? globalThis.String(object.source) : "",
|
||||||
|
score: isSet(object.score) ? globalThis.Number(object.score) : 0,
|
||||||
|
metadata: isObject(object.metadata) ? object.metadata : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: Chunk): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.id !== "") {
|
||||||
|
obj.id = message.id;
|
||||||
|
}
|
||||||
|
if (message.documentId !== "") {
|
||||||
|
obj.documentId = message.documentId;
|
||||||
|
}
|
||||||
|
if (message.content !== "") {
|
||||||
|
obj.content = message.content;
|
||||||
|
}
|
||||||
|
if (message.source !== "") {
|
||||||
|
obj.source = message.source;
|
||||||
|
}
|
||||||
|
if (message.score !== 0) {
|
||||||
|
obj.score = message.score;
|
||||||
|
}
|
||||||
|
if (message.metadata !== undefined) {
|
||||||
|
obj.metadata = message.metadata;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<Chunk>, I>>(base?: I): Chunk {
|
||||||
|
return Chunk.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<Chunk>, I>>(object: I): Chunk {
|
||||||
|
const message = createBaseChunk();
|
||||||
|
message.id = object.id ?? "";
|
||||||
|
message.documentId = object.documentId ?? "";
|
||||||
|
message.content = object.content ?? "";
|
||||||
|
message.source = object.source ?? "";
|
||||||
|
message.score = object.score ?? 0;
|
||||||
|
message.metadata = object.metadata ?? undefined;
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createBaseRagSearchResponse(): RagSearchResponse {
|
||||||
|
return { chunks: [], correlationId: "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RagSearchResponse: MessageFns<RagSearchResponse> = {
|
||||||
|
encode(message: RagSearchResponse, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||||
|
for (const v of message.chunks) {
|
||||||
|
Chunk.encode(v!, writer.uint32(10).fork()).join();
|
||||||
|
}
|
||||||
|
if (message.correlationId !== "") {
|
||||||
|
writer.uint32(18).string(message.correlationId);
|
||||||
|
}
|
||||||
|
return writer;
|
||||||
|
},
|
||||||
|
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): RagSearchResponse {
|
||||||
|
const reader = input instanceof BinaryReader ? input : new BinaryReader(input);
|
||||||
|
const end = length === undefined ? reader.len : reader.pos + length;
|
||||||
|
const message = createBaseRagSearchResponse();
|
||||||
|
while (reader.pos < end) {
|
||||||
|
const tag = reader.uint32();
|
||||||
|
switch (tag >>> 3) {
|
||||||
|
case 1: {
|
||||||
|
if (tag !== 10) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.chunks.push(Chunk.decode(reader, reader.uint32()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (tag !== 18) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.correlationId = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
reader.skip(tag & 7);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
|
||||||
|
fromJSON(object: any): RagSearchResponse {
|
||||||
|
return {
|
||||||
|
chunks: globalThis.Array.isArray(object?.chunks) ? object.chunks.map((e: any) => Chunk.fromJSON(e)) : [],
|
||||||
|
correlationId: isSet(object.correlationId)
|
||||||
|
? globalThis.String(object.correlationId)
|
||||||
|
: isSet(object.correlation_id)
|
||||||
|
? globalThis.String(object.correlation_id)
|
||||||
|
: "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
toJSON(message: RagSearchResponse): unknown {
|
||||||
|
const obj: any = {};
|
||||||
|
if (message.chunks?.length) {
|
||||||
|
obj.chunks = message.chunks.map((e) => Chunk.toJSON(e));
|
||||||
|
}
|
||||||
|
if (message.correlationId !== "") {
|
||||||
|
obj.correlationId = message.correlationId;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
},
|
||||||
|
|
||||||
|
create<I extends Exact<DeepPartial<RagSearchResponse>, I>>(base?: I): RagSearchResponse {
|
||||||
|
return RagSearchResponse.fromPartial(base ?? ({} as any));
|
||||||
|
},
|
||||||
|
fromPartial<I extends Exact<DeepPartial<RagSearchResponse>, I>>(object: I): RagSearchResponse {
|
||||||
|
const message = createBaseRagSearchResponse();
|
||||||
|
message.chunks = object.chunks?.map((e) => Chunk.fromPartial(e)) || [];
|
||||||
|
message.correlationId = object.correlationId ?? "";
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless retrieval. The server runs a single SQL query that combines
|
||||||
|
* vector similarity with the principal's ACL clause — there is no
|
||||||
|
* retrieval path that bypasses the ACL filter.
|
||||||
|
*/
|
||||||
|
export type RagServiceService = typeof RagServiceService;
|
||||||
|
export const RagServiceService = {
|
||||||
|
search: {
|
||||||
|
path: "/apf.ai.v1.RagService/Search" as const,
|
||||||
|
requestStream: false as const,
|
||||||
|
responseStream: false as const,
|
||||||
|
requestSerialize: (value: RagSearchRequest): Buffer => Buffer.from(RagSearchRequest.encode(value).finish()),
|
||||||
|
requestDeserialize: (value: Buffer): RagSearchRequest => RagSearchRequest.decode(value),
|
||||||
|
responseSerialize: (value: RagSearchResponse): Buffer => Buffer.from(RagSearchResponse.encode(value).finish()),
|
||||||
|
responseDeserialize: (value: Buffer): RagSearchResponse => RagSearchResponse.decode(value),
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export interface RagServiceServer extends UntypedServiceImplementation {
|
||||||
|
search: handleUnaryCall<RagSearchRequest, RagSearchResponse>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RagServiceClient extends Client {
|
||||||
|
search(
|
||||||
|
request: RagSearchRequest,
|
||||||
|
callback: (error: ServiceError | null, response: RagSearchResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
search(
|
||||||
|
request: RagSearchRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
callback: (error: ServiceError | null, response: RagSearchResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
search(
|
||||||
|
request: RagSearchRequest,
|
||||||
|
metadata: Metadata,
|
||||||
|
options: Partial<CallOptions>,
|
||||||
|
callback: (error: ServiceError | null, response: RagSearchResponse) => void,
|
||||||
|
): ClientUnaryCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RagServiceClient = makeGenericClientConstructor(RagServiceService, "apf.ai.v1.RagService") as unknown as {
|
||||||
|
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): RagServiceClient;
|
||||||
|
service: typeof RagServiceService;
|
||||||
|
serviceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
|
||||||
|
|
||||||
|
type DeepPartial<T> = T extends Builtin ? T
|
||||||
|
: T extends Long ? string | number | Long : T extends globalThis.Array<infer U> ? globalThis.Array<DeepPartial<U>>
|
||||||
|
: T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
|
||||||
|
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||||
|
: Partial<T>;
|
||||||
|
|
||||||
|
type KeysOfUnion<T> = T extends T ? keyof T : never;
|
||||||
|
type Exact<P, I extends P> = P extends Builtin ? P
|
||||||
|
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
|
||||||
|
|
||||||
|
function isObject(value: any): boolean {
|
||||||
|
return typeof value === "object" && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSet(value: any): boolean {
|
||||||
|
return value !== null && value !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageFns<T> {
|
||||||
|
encode(message: T, writer?: BinaryWriter): BinaryWriter;
|
||||||
|
decode(input: BinaryReader | Uint8Array, length?: number): T;
|
||||||
|
fromJSON(object: any): T;
|
||||||
|
toJSON(message: T): unknown;
|
||||||
|
create<I extends Exact<DeepPartial<T>, I>>(base?: I): T;
|
||||||
|
fromPartial<I extends Exact<DeepPartial<T>, I>>(object: I): T;
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package apf.ai.v1;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
import "google/protobuf/struct.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||||
|
|
||||||
|
// Streaming chat. Server returns a stream of ChatEvent; one event per
|
||||||
|
// model delta, citation, agent step, tool-call request, error, or done.
|
||||||
|
// Conversation state is NOT persisted — the caller sends the full
|
||||||
|
// message history every call. `conversation_id` exists only for audit
|
||||||
|
// correlation.
|
||||||
|
service ChatService {
|
||||||
|
rpc Chat(ChatRequest) returns (stream ChatEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RagOptions {
|
||||||
|
bool enabled = 1;
|
||||||
|
int32 top_k = 2; // 0 = use server default
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatRequest {
|
||||||
|
repeated ChatMessage messages = 1;
|
||||||
|
string conversation_id = 2;
|
||||||
|
string model = 3;
|
||||||
|
string provider = 4;
|
||||||
|
repeated ToolDescriptor tools_available = 5;
|
||||||
|
RagOptions rag = 6;
|
||||||
|
// Optional for JWT auth (overridden by JWT claims). Required for
|
||||||
|
// API-key auth — but API-key callers should use the HTTP surface.
|
||||||
|
Principal principal = 7;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TokenEvent {
|
||||||
|
string token = 1;
|
||||||
|
string value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CitationEvent {
|
||||||
|
string chunk_id = 1;
|
||||||
|
string document_id = 2;
|
||||||
|
string source = 3;
|
||||||
|
double score = 4;
|
||||||
|
string snippet = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AgentStepEvent {
|
||||||
|
string agent = 1;
|
||||||
|
string step = 2;
|
||||||
|
string step_id = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ToolCallEvent {
|
||||||
|
string call_id = 1;
|
||||||
|
string name = 2;
|
||||||
|
google.protobuf.Struct args = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ErrorEvent {
|
||||||
|
string code = 1;
|
||||||
|
string message = 2;
|
||||||
|
bool retriable = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DoneStats {
|
||||||
|
int32 tokens_in = 1;
|
||||||
|
int32 tokens_out = 2;
|
||||||
|
int32 chunks_retrieved = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DoneEvent {
|
||||||
|
DoneStats stats = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polymorphic stream payload. Mirrors the SSE shapes from the OpenAPI
|
||||||
|
// surface (kept side-by-side for third-party callers). gRPC's `oneof`
|
||||||
|
// gives us a tagged union without the SSE JSON discriminator.
|
||||||
|
message ChatEvent {
|
||||||
|
oneof event {
|
||||||
|
TokenEvent token = 1;
|
||||||
|
CitationEvent citation = 2;
|
||||||
|
AgentStepEvent agent_step = 3;
|
||||||
|
ToolCallEvent tool_call = 4;
|
||||||
|
ErrorEvent error = 5;
|
||||||
|
DoneEvent done = 6;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package apf.ai.v1;
|
||||||
|
|
||||||
|
import "google/protobuf/struct.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||||
|
|
||||||
|
// Identity-bearing principal shipped with every request (carried in the JWT for
|
||||||
|
// internal callers, or in the request body for API-key callers — same shape).
|
||||||
|
message Principal {
|
||||||
|
string subject = 1;
|
||||||
|
repeated string roles = 2;
|
||||||
|
map<string, string> attributes = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default-deny ACL attached to every ingested chunk.
|
||||||
|
// At least one of allowed_roles or allowed_subjects must be non-empty,
|
||||||
|
// enforced at the controller, the DB CHECK constraint, and the post-filter.
|
||||||
|
message AclSpec {
|
||||||
|
repeated string allowed_roles = 1;
|
||||||
|
repeated string allowed_subjects = 2;
|
||||||
|
repeated string denied_roles = 3;
|
||||||
|
map<string, string> required_attributes = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ChatRole {
|
||||||
|
CHAT_ROLE_UNSPECIFIED = 0;
|
||||||
|
CHAT_ROLE_SYSTEM = 1;
|
||||||
|
CHAT_ROLE_USER = 2;
|
||||||
|
CHAT_ROLE_ASSISTANT = 3;
|
||||||
|
CHAT_ROLE_TOOL = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatMessage {
|
||||||
|
ChatRole role = 1;
|
||||||
|
string content = 2;
|
||||||
|
string tool_call_id = 3;
|
||||||
|
string name = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON-schema descriptor for caller-side tool execution. The AI service is
|
||||||
|
// tool-blind: it emits ToolCall events; the caller executes and posts back.
|
||||||
|
message ToolDescriptor {
|
||||||
|
string name = 1;
|
||||||
|
string description = 2;
|
||||||
|
google.protobuf.Struct schema = 3;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package apf.ai.v1;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
import "google/protobuf/struct.proto";
|
||||||
|
import "google/protobuf/timestamp.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||||
|
|
||||||
|
// Document ingestion. Every chunk must carry an explicit ACL: empty
|
||||||
|
// ACL is default-deny and rejected with InvalidArgument (gRPC) /
|
||||||
|
// 422 (HTTP).
|
||||||
|
service IngestionService {
|
||||||
|
rpc IngestDocument(IngestDocumentRequest) returns (IngestDocumentResponse);
|
||||||
|
rpc GetDocument(GetDocumentRequest) returns (DocumentSummary);
|
||||||
|
rpc DeleteDocument(DeleteDocumentRequest) returns (DeleteDocumentResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message IngestChunk {
|
||||||
|
string content = 1;
|
||||||
|
google.protobuf.Struct metadata = 2;
|
||||||
|
AclSpec acl = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IngestDocumentRequest {
|
||||||
|
string source = 1;
|
||||||
|
string external_id = 2;
|
||||||
|
string title = 3;
|
||||||
|
repeated IngestChunk chunks = 4;
|
||||||
|
Principal principal = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message IngestDocumentResponse {
|
||||||
|
string document_id = 1;
|
||||||
|
int32 chunks_ingested = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetDocumentRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DocumentSummary {
|
||||||
|
string id = 1;
|
||||||
|
string source_uri = 2;
|
||||||
|
string title = 3;
|
||||||
|
string external_id = 4;
|
||||||
|
google.protobuf.Timestamp ingested_at = 5;
|
||||||
|
int32 chunk_count = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteDocumentRequest {
|
||||||
|
string id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DeleteDocumentResponse {
|
||||||
|
bool deleted = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package apf.ai.v1;
|
||||||
|
|
||||||
|
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||||
|
|
||||||
|
service ModelsService {
|
||||||
|
rpc ListModels(ListModelsRequest) returns (ListModelsResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListModelsRequest {}
|
||||||
|
|
||||||
|
message ProviderInfo {
|
||||||
|
string discriminator = 1;
|
||||||
|
string capabilities = 2;
|
||||||
|
string endpoint = 3;
|
||||||
|
string model = 4;
|
||||||
|
string embedding_model = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ListModelsResponse {
|
||||||
|
string active = 1;
|
||||||
|
repeated ProviderInfo providers = 2;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package apf.ai.v1;
|
||||||
|
|
||||||
|
import "common.proto";
|
||||||
|
import "google/protobuf/struct.proto";
|
||||||
|
|
||||||
|
option csharp_namespace = "Apf.Ai.Grpc.V1";
|
||||||
|
|
||||||
|
// Stateless retrieval. The server runs a single SQL query that combines
|
||||||
|
// vector similarity with the principal's ACL clause — there is no
|
||||||
|
// retrieval path that bypasses the ACL filter.
|
||||||
|
service RagService {
|
||||||
|
rpc Search(RagSearchRequest) returns (RagSearchResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message RagSearchFilters {
|
||||||
|
string source = 1;
|
||||||
|
string document_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RagSearchRequest {
|
||||||
|
string query = 1;
|
||||||
|
int32 top_k = 2;
|
||||||
|
RagSearchFilters filters = 3;
|
||||||
|
Principal principal = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Chunk {
|
||||||
|
string id = 1;
|
||||||
|
string document_id = 2;
|
||||||
|
string content = 3;
|
||||||
|
string source = 4;
|
||||||
|
double score = 5;
|
||||||
|
google.protobuf.Struct metadata = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message RagSearchResponse {
|
||||||
|
repeated Chunk chunks = 1;
|
||||||
|
string correlation_id = 2;
|
||||||
|
}
|
||||||
+5
-1
@@ -8,7 +8,11 @@ export default [
|
|||||||
"ignores": [
|
"ignores": [
|
||||||
"**/dist",
|
"**/dist",
|
||||||
"**/out-tsc",
|
"**/out-tsc",
|
||||||
"**/vitest.config.*.timestamp*"
|
"**/vitest.config.*.timestamp*",
|
||||||
|
// Generated gRPC TypeScript stubs — hand-rules do not apply to
|
||||||
|
// codegen output. Lint the wrapper modules in `ai-client/`, not
|
||||||
|
// the ts-proto-emitted files under `gen/`.
|
||||||
|
"apps/portal-bff/src/grpc/gen/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
"ci:commits": "pnpm exec commitlint --from ${COMMIT_LINT_FROM:-origin/main} --to HEAD --verbose",
|
"ci:commits": "pnpm exec commitlint --from ${COMMIT_LINT_FROM:-origin/main} --to HEAD --verbose",
|
||||||
"ci:gzip-budgets": "node scripts/check-gzip-budgets.mjs",
|
"ci:gzip-budgets": "node scripts/check-gzip-budgets.mjs",
|
||||||
"ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm ci:gzip-budgets && pnpm exec lhci autorun --config=./lighthouserc.js",
|
"ci:perf": "pnpm exec nx build portal-shell --configuration=production && pnpm ci:gzip-budgets && pnpm exec lhci autorun --config=./lighthouserc.js",
|
||||||
|
"grpc:codegen": "grpc_tools_node_protoc --plugin=protoc-gen-ts_proto=./node_modules/.bin/protoc-gen-ts_proto --ts_proto_out=apps/portal-bff/src/grpc/gen/apf-ai --ts_proto_opt=outputServices=grpc-js,esModuleInterop=true,forceLong=long,useOptionals=messages,exportCommonSymbols=false --proto_path=apps/portal-bff/src/grpc/proto/apf-ai apps/portal-bff/src/grpc/proto/apf-ai/*.proto",
|
||||||
|
"grpc:sync": "node scripts/sync-ai-protos.mjs",
|
||||||
"docs:dev": "vitepress dev docs",
|
"docs:dev": "vitepress dev docs",
|
||||||
"docs:build": "vitepress build docs",
|
"docs:build": "vitepress build docs",
|
||||||
"docs:preview": "vitepress preview docs"
|
"docs:preview": "vitepress preview docs"
|
||||||
@@ -28,6 +30,7 @@
|
|||||||
"@prisma/engines",
|
"@prisma/engines",
|
||||||
"@swc/core",
|
"@swc/core",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
|
"grpc-tools",
|
||||||
"less",
|
"less",
|
||||||
"lmdb",
|
"lmdb",
|
||||||
"msgpackr-extract",
|
"msgpackr-extract",
|
||||||
@@ -108,6 +111,7 @@
|
|||||||
"eslint": "^9.8.0",
|
"eslint": "^9.8.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-playwright": "^1.6.2",
|
"eslint-plugin-playwright": "^1.6.2",
|
||||||
|
"grpc-tools": "^1.13.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^30.0.2",
|
"jest": "^30.0.2",
|
||||||
"jest-environment-node": "^30.0.2",
|
"jest-environment-node": "^30.0.2",
|
||||||
@@ -124,6 +128,7 @@
|
|||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
"ts-jest": "^29.4.0",
|
"ts-jest": "^29.4.0",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.2",
|
||||||
|
"ts-proto": "^2.7.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
"typescript-eslint": "^8.40.0",
|
"typescript-eslint": "^8.40.0",
|
||||||
@@ -143,6 +148,8 @@
|
|||||||
"@angular/platform-browser": "~21.2.0",
|
"@angular/platform-browser": "~21.2.0",
|
||||||
"@angular/router": "~21.2.0",
|
"@angular/router": "~21.2.0",
|
||||||
"@azure/msal-node": "^5.2.1",
|
"@azure/msal-node": "^5.2.1",
|
||||||
|
"@bufbuild/protobuf": "^2.10.2",
|
||||||
|
"@grpc/grpc-js": "^1.13.0",
|
||||||
"@nestjs/common": "^11.0.0",
|
"@nestjs/common": "^11.0.0",
|
||||||
"@nestjs/core": "^11.0.0",
|
"@nestjs/core": "^11.0.0",
|
||||||
"@nestjs/platform-express": "^11.0.0",
|
"@nestjs/platform-express": "^11.0.0",
|
||||||
@@ -180,6 +187,7 @@
|
|||||||
"helmet": "^8.1.0",
|
"helmet": "^8.1.0",
|
||||||
"ioredis": "^5.10.1",
|
"ioredis": "^5.10.1",
|
||||||
"jose": "^6.2.3",
|
"jose": "^6.2.3",
|
||||||
|
"long": "^5.2.3",
|
||||||
"lucide-angular": "^1.0.0",
|
"lucide-angular": "^1.0.0",
|
||||||
"nestjs-cls": "^6.2.0",
|
"nestjs-cls": "^6.2.0",
|
||||||
"nestjs-pino": "^4.6.1",
|
"nestjs-pino": "^4.6.1",
|
||||||
|
|||||||
Generated
+110
-6
@@ -49,6 +49,12 @@ importers:
|
|||||||
'@azure/msal-node':
|
'@azure/msal-node':
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
|
'@bufbuild/protobuf':
|
||||||
|
specifier: ^2.10.2
|
||||||
|
version: 2.12.0
|
||||||
|
'@grpc/grpc-js':
|
||||||
|
specifier: ^1.13.0
|
||||||
|
version: 1.14.3
|
||||||
'@nestjs/common':
|
'@nestjs/common':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.1.21(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.21(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -160,6 +166,9 @@ importers:
|
|||||||
jose:
|
jose:
|
||||||
specifier: ^6.2.3
|
specifier: ^6.2.3
|
||||||
version: 6.2.3
|
version: 6.2.3
|
||||||
|
long:
|
||||||
|
specifier: ^5.2.3
|
||||||
|
version: 5.3.2
|
||||||
lucide-angular:
|
lucide-angular:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0(@angular/common@21.2.13(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))
|
version: 1.0.0(@angular/common@21.2.13(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@21.2.13(@angular/compiler@21.2.13)(rxjs@7.8.2)(zone.js@0.16.2))
|
||||||
@@ -347,6 +356,9 @@ importers:
|
|||||||
eslint-plugin-playwright:
|
eslint-plugin-playwright:
|
||||||
specifier: ^1.6.2
|
specifier: ^1.6.2
|
||||||
version: 1.8.3(eslint@9.39.4(jiti@2.7.0))
|
version: 1.8.3(eslint@9.39.4(jiti@2.7.0))
|
||||||
|
grpc-tools:
|
||||||
|
specifier: ^1.13.0
|
||||||
|
version: 1.13.1(encoding@0.1.13)
|
||||||
husky:
|
husky:
|
||||||
specifier: ^9.1.7
|
specifier: ^9.1.7
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
@@ -395,6 +407,9 @@ importers:
|
|||||||
ts-node:
|
ts-node:
|
||||||
specifier: 10.9.2
|
specifier: 10.9.2
|
||||||
version: 10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.21))(@types/node@24.12.4)(typescript@5.9.3)
|
version: 10.9.2(@swc/core@1.15.33(@swc/helpers@0.5.21))(@types/node@24.12.4)(typescript@5.9.3)
|
||||||
|
ts-proto:
|
||||||
|
specifier: ^2.7.0
|
||||||
|
version: 2.11.8
|
||||||
tslib:
|
tslib:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.8.1
|
version: 2.8.1
|
||||||
@@ -2541,6 +2556,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@mapbox/node-pre-gyp@2.0.3':
|
||||||
|
resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@mermaid-js/mermaid-mindmap@9.3.0':
|
'@mermaid-js/mermaid-mindmap@9.3.0':
|
||||||
resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==}
|
resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==}
|
||||||
|
|
||||||
@@ -5354,6 +5374,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==}
|
resolution: {integrity: sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
abbrev@3.0.1:
|
||||||
|
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
||||||
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
|
|
||||||
abbrev@4.0.0:
|
abbrev@4.0.0:
|
||||||
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
|
resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==}
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
engines: {node: ^20.17.0 || >=22.9.0}
|
||||||
@@ -5855,6 +5879,10 @@ packages:
|
|||||||
caniuse-lite@1.0.30001793:
|
caniuse-lite@1.0.30001793:
|
||||||
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
|
||||||
|
|
||||||
|
case-anything@2.1.13:
|
||||||
|
resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==}
|
||||||
|
engines: {node: '>=12.13'}
|
||||||
|
|
||||||
ccount@2.0.1:
|
ccount@2.0.1:
|
||||||
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
|
||||||
|
|
||||||
@@ -6613,6 +6641,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==}
|
||||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||||
|
|
||||||
|
detect-libc@1.0.3:
|
||||||
|
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
|
||||||
|
engines: {node: '>=0.10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
detect-libc@2.1.2:
|
detect-libc@2.1.2:
|
||||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6679,6 +6712,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
dprint-node@1.0.8:
|
||||||
|
resolution: {integrity: sha512-iVKnUtYfGrYcW1ZAlfR/F59cUVL8QIhWoBJoSjkkdua/dkWIgjZfiLMeTjiB06X0ZLkQ0M2C1VbUj/CxkIf1zg==}
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -7319,6 +7355,10 @@ packages:
|
|||||||
graceful-fs@4.2.11:
|
graceful-fs@4.2.11:
|
||||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||||
|
|
||||||
|
grpc-tools@1.13.1:
|
||||||
|
resolution: {integrity: sha512-0sttMUxThNIkCTJq5qI0xXMz5zWqV2u3yG1kR3Sj9OokGIoyRBFjoInK9NyW7x5fH7knj48Roh1gq5xbl0VoDQ==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
hachure-fill@0.5.2:
|
hachure-fill@0.5.2:
|
||||||
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
|
resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==}
|
||||||
|
|
||||||
@@ -8715,6 +8755,11 @@ packages:
|
|||||||
non-layered-tidy-tree-layout@2.0.2:
|
non-layered-tidy-tree-layout@2.0.2:
|
||||||
resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
|
resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==}
|
||||||
|
|
||||||
|
nopt@8.1.0:
|
||||||
|
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
|
||||||
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
nopt@9.0.0:
|
nopt@9.0.0:
|
||||||
resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
|
resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==}
|
||||||
engines: {node: ^20.17.0 || >=22.9.0}
|
engines: {node: ^20.17.0 || >=22.9.0}
|
||||||
@@ -10547,6 +10592,16 @@ packages:
|
|||||||
'@swc/wasm':
|
'@swc/wasm':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ts-poet@6.12.0:
|
||||||
|
resolution: {integrity: sha512-xo+iRNMWqyvXpFTaOAvLPA5QAWO6TZrSUs5s4Odaya3epqofBu/fMLHEWl8jPmjhA0s9sgj9sNvF1BmaQlmQkA==}
|
||||||
|
|
||||||
|
ts-proto-descriptors@2.1.0:
|
||||||
|
resolution: {integrity: sha512-S5EZYEQ6L9KLFfjSRpZWDIXDV/W7tAj8uW7pLsihIxyr62EAVSiKuVPwE8iWnr849Bqa53enex1jhDUcpgquzA==}
|
||||||
|
|
||||||
|
ts-proto@2.11.8:
|
||||||
|
resolution: {integrity: sha512-+5hzECnyVB33jxjG1BIdzAHcRBm7hjnm8womdJVp2A7xJWihP0drHHVsXYTr9i/LpWNGfh80I+AVVNzFM5AwJw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
tsconfig-paths-webpack-plugin@4.2.0:
|
tsconfig-paths-webpack-plugin@4.2.0:
|
||||||
resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
|
resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==}
|
||||||
engines: {node: '>=10.13.0'}
|
engines: {node: '>=10.13.0'}
|
||||||
@@ -13702,6 +13757,19 @@ snapshots:
|
|||||||
|
|
||||||
'@lukeed/csprng@1.1.0': {}
|
'@lukeed/csprng@1.1.0': {}
|
||||||
|
|
||||||
|
'@mapbox/node-pre-gyp@2.0.3(encoding@0.1.13)':
|
||||||
|
dependencies:
|
||||||
|
consola: 3.4.2
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
https-proxy-agent: 7.0.6
|
||||||
|
node-fetch: 2.7.0(encoding@0.1.13)
|
||||||
|
nopt: 8.1.0
|
||||||
|
semver: 7.8.0
|
||||||
|
tar: 7.5.15
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@mermaid-js/mermaid-mindmap@9.3.0':
|
'@mermaid-js/mermaid-mindmap@9.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@braintree/sanitize-url': 6.0.4
|
'@braintree/sanitize-url': 6.0.4
|
||||||
@@ -14157,7 +14225,7 @@ snapshots:
|
|||||||
|
|
||||||
'@npmcli/fs@5.0.0':
|
'@npmcli/fs@5.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
|
|
||||||
'@npmcli/git@7.0.2':
|
'@npmcli/git@7.0.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -14167,7 +14235,7 @@ snapshots:
|
|||||||
lru-cache: 11.3.6
|
lru-cache: 11.3.6
|
||||||
npm-pick-manifest: 11.0.3
|
npm-pick-manifest: 11.0.3
|
||||||
proc-log: 6.1.0
|
proc-log: 6.1.0
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
which: 6.0.1
|
which: 6.0.1
|
||||||
|
|
||||||
'@npmcli/installed-package-contents@4.0.0':
|
'@npmcli/installed-package-contents@4.0.0':
|
||||||
@@ -14184,7 +14252,7 @@ snapshots:
|
|||||||
hosted-git-info: 9.0.3
|
hosted-git-info: 9.0.3
|
||||||
json-parse-even-better-errors: 5.0.0
|
json-parse-even-better-errors: 5.0.0
|
||||||
proc-log: 6.1.0
|
proc-log: 6.1.0
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
spdx-expression-parse: 4.0.0
|
spdx-expression-parse: 4.0.0
|
||||||
|
|
||||||
'@npmcli/promise-spawn@9.0.1':
|
'@npmcli/promise-spawn@9.0.1':
|
||||||
@@ -16984,6 +17052,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
abbrev@3.0.1: {}
|
||||||
|
|
||||||
abbrev@4.0.0: {}
|
abbrev@4.0.0: {}
|
||||||
|
|
||||||
accepts@1.3.8:
|
accepts@1.3.8:
|
||||||
@@ -17564,6 +17634,8 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001793: {}
|
caniuse-lite@1.0.30001793: {}
|
||||||
|
|
||||||
|
case-anything@2.1.13: {}
|
||||||
|
|
||||||
ccount@2.0.1: {}
|
ccount@2.0.1: {}
|
||||||
|
|
||||||
chai@6.2.2: {}
|
chai@6.2.2: {}
|
||||||
@@ -18318,6 +18390,8 @@ snapshots:
|
|||||||
|
|
||||||
destroy@1.2.0: {}
|
destroy@1.2.0: {}
|
||||||
|
|
||||||
|
detect-libc@1.0.3: {}
|
||||||
|
|
||||||
detect-libc@2.1.2: {}
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
detect-newline@3.1.0: {}
|
detect-newline@3.1.0: {}
|
||||||
@@ -18376,6 +18450,10 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@16.6.1: {}
|
dotenv@16.6.1: {}
|
||||||
|
|
||||||
|
dprint-node@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 1.0.3
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind-apply-helpers: 1.0.2
|
call-bind-apply-helpers: 1.0.2
|
||||||
@@ -19158,6 +19236,13 @@ snapshots:
|
|||||||
|
|
||||||
graceful-fs@4.2.11: {}
|
graceful-fs@4.2.11: {}
|
||||||
|
|
||||||
|
grpc-tools@1.13.1(encoding@0.1.13):
|
||||||
|
dependencies:
|
||||||
|
'@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- encoding
|
||||||
|
- supports-color
|
||||||
|
|
||||||
hachure-fill@0.5.2: {}
|
hachure-fill@0.5.2: {}
|
||||||
|
|
||||||
handle-thing@2.0.1: {}
|
handle-thing@2.0.1: {}
|
||||||
@@ -20756,7 +20841,7 @@ snapshots:
|
|||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
nopt: 9.0.0
|
nopt: 9.0.0
|
||||||
proc-log: 6.1.0
|
proc-log: 6.1.0
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
tar: 7.5.15
|
tar: 7.5.15
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.16
|
||||||
undici: 6.25.0
|
undici: 6.25.0
|
||||||
@@ -20775,6 +20860,10 @@ snapshots:
|
|||||||
non-layered-tidy-tree-layout@2.0.2:
|
non-layered-tidy-tree-layout@2.0.2:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
nopt@8.1.0:
|
||||||
|
dependencies:
|
||||||
|
abbrev: 3.0.1
|
||||||
|
|
||||||
nopt@9.0.0:
|
nopt@9.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
abbrev: 4.0.0
|
abbrev: 4.0.0
|
||||||
@@ -20787,7 +20876,7 @@ snapshots:
|
|||||||
|
|
||||||
npm-install-checks@8.0.0:
|
npm-install-checks@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
|
|
||||||
npm-normalize-package-bin@5.0.0: {}
|
npm-normalize-package-bin@5.0.0: {}
|
||||||
|
|
||||||
@@ -20808,7 +20897,7 @@ snapshots:
|
|||||||
npm-install-checks: 8.0.0
|
npm-install-checks: 8.0.0
|
||||||
npm-normalize-package-bin: 5.0.0
|
npm-normalize-package-bin: 5.0.0
|
||||||
npm-package-arg: 13.0.2
|
npm-package-arg: 13.0.2
|
||||||
semver: 7.7.4
|
semver: 7.8.0
|
||||||
|
|
||||||
npm-registry-fetch@19.1.1:
|
npm-registry-fetch@19.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -22881,6 +22970,21 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@swc/core': 1.15.33(@swc/helpers@0.5.21)
|
'@swc/core': 1.15.33(@swc/helpers@0.5.21)
|
||||||
|
|
||||||
|
ts-poet@6.12.0:
|
||||||
|
dependencies:
|
||||||
|
dprint-node: 1.0.8
|
||||||
|
|
||||||
|
ts-proto-descriptors@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@bufbuild/protobuf': 2.12.0
|
||||||
|
|
||||||
|
ts-proto@2.11.8:
|
||||||
|
dependencies:
|
||||||
|
'@bufbuild/protobuf': 2.12.0
|
||||||
|
case-anything: 2.1.13
|
||||||
|
ts-poet: 6.12.0
|
||||||
|
ts-proto-descriptors: 2.1.0
|
||||||
|
|
||||||
tsconfig-paths-webpack-plugin@4.2.0:
|
tsconfig-paths-webpack-plugin@4.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Refresh the vendored apf-ai-service protos from the sibling
|
||||||
|
// working tree. Developer convenience only — not invoked from CI.
|
||||||
|
//
|
||||||
|
// Per ADR-0024 §"Sub-decision 3 — vendored protos", the BFF holds a
|
||||||
|
// committed copy of the `.proto` files; this script keeps the copy
|
||||||
|
// in sync with the upstream repo when a contract change lands. The
|
||||||
|
// generated TypeScript stubs are NOT regenerated here — chain with
|
||||||
|
// `pnpm run grpc:codegen` if needed:
|
||||||
|
//
|
||||||
|
// pnpm run grpc:sync && pnpm run grpc:codegen
|
||||||
|
//
|
||||||
|
// Exits non-zero (with a clear message) when the sibling repo is not
|
||||||
|
// where the script expects, so a contributor whose layout differs
|
||||||
|
// gets a useful pointer instead of a silent no-op.
|
||||||
|
|
||||||
|
import { existsSync, readdirSync, copyFileSync, statSync } from 'node:fs';
|
||||||
|
import { join, resolve, dirname } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const WORKSPACE_ROOT = resolve(HERE, '..');
|
||||||
|
const UPSTREAM = resolve(WORKSPACE_ROOT, '..', 'apf-ai-service', 'contract', 'proto');
|
||||||
|
const VENDORED = resolve(WORKSPACE_ROOT, 'apps', 'portal-bff', 'src', 'grpc', 'proto', 'apf-ai');
|
||||||
|
|
||||||
|
if (!existsSync(UPSTREAM) || !statSync(UPSTREAM).isDirectory()) {
|
||||||
|
console.error(
|
||||||
|
`apf-ai-service protos not found at ${UPSTREAM}.\n` +
|
||||||
|
`This script expects the sibling working tree at ../apf-ai-service ` +
|
||||||
|
`relative to this workspace. Adjust the path or clone the repo and retry.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = readdirSync(UPSTREAM).filter((f) => f.endsWith('.proto'));
|
||||||
|
if (files.length === 0) {
|
||||||
|
console.error(`No .proto files under ${UPSTREAM} — nothing to sync.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
copyFileSync(join(UPSTREAM, file), join(VENDORED, file));
|
||||||
|
console.log(`synced ${file}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`\n${files.length} proto file(s) refreshed into ${VENDORED}.\n` +
|
||||||
|
`Next: pnpm run grpc:codegen (regenerate TypeScript stubs).`,
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user