@aurabase/aurabase-js 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.changeset/config.json +11 -0
- package/CHANGELOG.md +30 -0
- package/LICENSE +21 -0
- package/README.md +833 -0
- package/package.json +90 -0
- package/packages/ai/LICENSE +21 -0
- package/packages/ai/README.md +106 -0
- package/packages/ai/dist/index.cjs +229 -0
- package/packages/ai/dist/index.d.cts +139 -0
- package/packages/ai/dist/index.d.ts +139 -0
- package/packages/ai/dist/index.js +226 -0
- package/packages/ai/node_modules/.bin/esbuild +19 -0
- package/packages/ai/node_modules/.bin/tsc +22 -0
- package/packages/ai/node_modules/.bin/tsserver +22 -0
- package/packages/ai/node_modules/.bin/tsup +22 -0
- package/packages/ai/node_modules/.bin/tsup-node +22 -0
- package/packages/ai/node_modules/.bin/vite +22 -0
- package/packages/ai/node_modules/.bin/vitest +22 -0
- package/packages/ai/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/ai/package.json +61 -0
- package/packages/ai/src/AiService.ts +226 -0
- package/packages/ai/src/index.ts +18 -0
- package/packages/ai/src/sse.ts +141 -0
- package/packages/ai/src/types.ts +30 -0
- package/packages/ai/tests/chat-stream.test.ts +151 -0
- package/packages/ai/tests/rag-namespaces.test.ts +54 -0
- package/packages/ai/tests/sse.test.ts +69 -0
- package/packages/ai/tsconfig.json +5 -0
- package/packages/ai/tsup.config.ts +11 -0
- package/packages/aurabase/LICENSE +21 -0
- package/packages/aurabase/README.md +64 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.cts +77 -0
- package/packages/aurabase/dist/AuraClient-DAtDbfPB.d.ts +77 -0
- package/packages/aurabase/dist/index.cjs +237 -0
- package/packages/aurabase/dist/index.d.cts +46 -0
- package/packages/aurabase/dist/index.d.ts +46 -0
- package/packages/aurabase/dist/index.js +177 -0
- package/packages/aurabase/dist/ssr.cjs +224 -0
- package/packages/aurabase/dist/ssr.d.cts +65 -0
- package/packages/aurabase/dist/ssr.d.ts +65 -0
- package/packages/aurabase/dist/ssr.js +214 -0
- package/packages/aurabase/node_modules/.bin/esbuild +19 -0
- package/packages/aurabase/node_modules/.bin/tsc +22 -0
- package/packages/aurabase/node_modules/.bin/tsserver +22 -0
- package/packages/aurabase/node_modules/.bin/tsup +22 -0
- package/packages/aurabase/node_modules/.bin/tsup-node +22 -0
- package/packages/aurabase/node_modules/.bin/vite +22 -0
- package/packages/aurabase/node_modules/.bin/vitest +22 -0
- package/packages/aurabase/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/aurabase/package.json +80 -0
- package/packages/aurabase/src/AuraClient.ts +237 -0
- package/packages/aurabase/src/index.ts +152 -0
- package/packages/aurabase/src/ssr.ts +156 -0
- package/packages/aurabase/tests/ai-streaming-rag-e2e.test.ts +82 -0
- package/packages/aurabase/tests/ai.test.ts +111 -0
- package/packages/aurabase/tests/auth-admin.test.ts +515 -0
- package/packages/aurabase/tests/auth-e2e.test.ts +135 -0
- package/packages/aurabase/tests/auth-hardening.test.ts +206 -0
- package/packages/aurabase/tests/auth.test.ts +947 -0
- package/packages/aurabase/tests/database.test.ts +230 -0
- package/packages/aurabase/tests/db-types-e2e.test.ts +217 -0
- package/packages/aurabase/tests/dispose.test.ts +23 -0
- package/packages/aurabase/tests/foreign-token.e2e.test.ts +129 -0
- package/packages/aurabase/tests/functions.test.ts +107 -0
- package/packages/aurabase/tests/integration.test.ts +413 -0
- package/packages/aurabase/tests/live-production-validation.js +241 -0
- package/packages/aurabase/tests/live-sdk-production-validation.js +293 -0
- package/packages/aurabase/tests/mocks.ts +139 -0
- package/packages/aurabase/tests/notifications.test.ts +314 -0
- package/packages/aurabase/tests/observability-backup.test.ts +186 -0
- package/packages/aurabase/tests/observability-hook.test.ts +130 -0
- package/packages/aurabase/tests/production-robustness.test.ts +268 -0
- package/packages/aurabase/tests/public-api-surface.test.ts +53 -0
- package/packages/aurabase/tests/query-builder.test.ts +329 -0
- package/packages/aurabase/tests/readme-smoke-e2e.test.ts +231 -0
- package/packages/aurabase/tests/realtime-filter-e2e.test.ts +344 -0
- package/packages/aurabase/tests/refresh-locks.test.ts +210 -0
- package/packages/aurabase/tests/retry-option.test.ts +17 -0
- package/packages/aurabase/tests/rpc-e2e.test.ts +254 -0
- package/packages/aurabase/tests/rs256-local-e2e.test.ts +114 -0
- package/packages/aurabase/tests/rt-alias-public-e2e.test.ts +235 -0
- package/packages/aurabase/tests/rt-http-broadcast-e2e.test.ts +124 -0
- package/packages/aurabase/tests/service-role-guard.test.ts +99 -0
- package/packages/aurabase/tests/ssr.test.ts +91 -0
- package/packages/aurabase/tests/storage-backend-e2e.test.ts +481 -0
- package/packages/aurabase/tests/storage-sdk-parity-e2e.test.ts +104 -0
- package/packages/aurabase/tests/storage-transform-e2e.test.ts +684 -0
- package/packages/aurabase/tests/storage.test.ts +55 -0
- package/packages/aurabase/tests/types/client-generic.test-d.ts +79 -0
- package/packages/aurabase/tests/types/docs-snippets.test-d.ts +571 -0
- package/packages/aurabase/tests/types/error-code.test-d.ts +53 -0
- package/packages/aurabase/tsconfig.json +5 -0
- package/packages/aurabase/tsconfig.test-d.json +5 -0
- package/packages/aurabase/tsup.config.ts +11 -0
- package/packages/aurabase/vitest.config.ts +16 -0
- package/packages/aurabase/vitest.e2e.config.ts +9 -0
- package/packages/auth/LICENSE +21 -0
- package/packages/auth/README.md +215 -0
- package/packages/auth/dist/index.cjs +1175 -0
- package/packages/auth/dist/index.d.cts +397 -0
- package/packages/auth/dist/index.d.ts +397 -0
- package/packages/auth/dist/index.js +1170 -0
- package/packages/auth/node_modules/.bin/esbuild +19 -0
- package/packages/auth/node_modules/.bin/tsc +22 -0
- package/packages/auth/node_modules/.bin/tsserver +22 -0
- package/packages/auth/node_modules/.bin/tsup +22 -0
- package/packages/auth/node_modules/.bin/tsup-node +22 -0
- package/packages/auth/node_modules/.bin/vite +22 -0
- package/packages/auth/node_modules/.bin/vitest +22 -0
- package/packages/auth/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/auth/package.json +61 -0
- package/packages/auth/src/AuthAdminService.ts +149 -0
- package/packages/auth/src/AuthService.ts +1166 -0
- package/packages/auth/src/SessionManager.ts +92 -0
- package/packages/auth/src/index.ts +17 -0
- package/packages/auth/src/jwks.ts +337 -0
- package/packages/auth/src/pkce.ts +27 -0
- package/packages/auth/src/types.ts +89 -0
- package/packages/auth/tests/auth-lifecycle.test.ts +149 -0
- package/packages/auth/tests/detect-session.test.ts +361 -0
- package/packages/auth/tests/getclaims.test.ts +780 -0
- package/packages/auth/tests/is-mfa-challenge.test.ts +27 -0
- package/packages/auth/tests/password-recovery.test.ts +124 -0
- package/packages/auth/tests/pkce.test.ts +192 -0
- package/packages/auth/tests/session-manager.test.ts +103 -0
- package/packages/auth/tests/set-session.test.ts +139 -0
- package/packages/auth/tests/verify-otp.test.ts +153 -0
- package/packages/auth/tsconfig.json +5 -0
- package/packages/auth/tsup.config.ts +11 -0
- package/packages/core/LICENSE +21 -0
- package/packages/core/README.md +132 -0
- package/packages/core/dist/index.cjs +695 -0
- package/packages/core/dist/index.d.cts +1137 -0
- package/packages/core/dist/index.d.ts +1137 -0
- package/packages/core/dist/index.js +673 -0
- package/packages/core/node_modules/.bin/esbuild +22 -0
- package/packages/core/node_modules/.bin/tsc +22 -0
- package/packages/core/node_modules/.bin/tsserver +22 -0
- package/packages/core/node_modules/.bin/tsup +22 -0
- package/packages/core/node_modules/.bin/tsup-node +22 -0
- package/packages/core/node_modules/.bin/vite +22 -0
- package/packages/core/node_modules/.bin/vitest +22 -0
- package/packages/core/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/core/package.json +57 -0
- package/packages/core/src/AuraError.ts +89 -0
- package/packages/core/src/BaseService.ts +44 -0
- package/packages/core/src/client.ts +34 -0
- package/packages/core/src/cookie-utils.ts +42 -0
- package/packages/core/src/fetch.ts +435 -0
- package/packages/core/src/index.ts +22 -0
- package/packages/core/src/retry.ts +63 -0
- package/packages/core/src/serialize.ts +6 -0
- package/packages/core/src/stores/BaseAuthStore.ts +164 -0
- package/packages/core/src/stores/LocalAuthStore.ts +199 -0
- package/packages/core/src/types.ts +1042 -0
- package/packages/core/tests/auth-store.test.ts +300 -0
- package/packages/core/tests/error-code.test.ts +61 -0
- package/packages/core/tests/error.test.ts +24 -0
- package/packages/core/tests/fetch.test.ts +286 -0
- package/packages/core/tests/mocks.ts +139 -0
- package/packages/core/tests/observability.test.ts +86 -0
- package/packages/core/tests/refresh-hardening.test.ts +164 -0
- package/packages/core/tests/refresh-observability.test.ts +111 -0
- package/packages/core/tests/retry.test.ts +106 -0
- package/packages/core/tests/serialize.test.ts +12 -0
- package/packages/core/tests/timeout.test.ts +79 -0
- package/packages/core/tsconfig.json +5 -0
- package/packages/core/tsup.config.ts +11 -0
- package/packages/db/LICENSE +21 -0
- package/packages/db/README.md +152 -0
- package/packages/db/VENDORED.md +93 -0
- package/packages/db/dist/index.cjs +544 -0
- package/packages/db/dist/index.d.cts +1111 -0
- package/packages/db/dist/index.d.ts +1111 -0
- package/packages/db/dist/index.js +541 -0
- package/packages/db/node_modules/.bin/esbuild +19 -0
- package/packages/db/node_modules/.bin/tsc +22 -0
- package/packages/db/node_modules/.bin/tsserver +22 -0
- package/packages/db/node_modules/.bin/tsup +22 -0
- package/packages/db/node_modules/.bin/tsup-node +22 -0
- package/packages/db/node_modules/.bin/vite +22 -0
- package/packages/db/node_modules/.bin/vitest +22 -0
- package/packages/db/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/db/package.json +62 -0
- package/packages/db/src/DatabaseService.ts +170 -0
- package/packages/db/src/QueryBuilder.ts +830 -0
- package/packages/db/src/index.ts +2 -0
- package/packages/db/src/pgrst-types/common/common.ts +86 -0
- package/packages/db/src/pgrst-types/common/rpc.ts +151 -0
- package/packages/db/src/pgrst-types/feature-flags.ts +20 -0
- package/packages/db/src/pgrst-types/types.ts +134 -0
- package/packages/db/src/select-query-parser/parser.ts +479 -0
- package/packages/db/src/select-query-parser/result.ts +553 -0
- package/packages/db/src/select-query-parser/types.ts +132 -0
- package/packages/db/src/select-query-parser/utils.ts +707 -0
- package/packages/db/tests/filters.test.ts +81 -0
- package/packages/db/tests/mutations-filtered.e2e.test.ts +132 -0
- package/packages/db/tests/mutations-filtered.test.ts +188 -0
- package/packages/db/tests/order.test.ts +45 -0
- package/packages/db/tests/rpc.test.ts +41 -0
- package/packages/db/tests/scale/build-large-schema.ts +94 -0
- package/packages/db/tests/scale/scale-compile.test.ts +167 -0
- package/packages/db/tests/schema.test.ts +77 -0
- package/packages/db/tests/single-accept.e2e.test.ts +123 -0
- package/packages/db/tests/single.test.ts +211 -0
- package/packages/db/tests/types/filters.test-d.ts +52 -0
- package/packages/db/tests/types/fixtures.ts +40 -0
- package/packages/db/tests/types/from-views.test-d.ts +18 -0
- package/packages/db/tests/types/mutations.test-d.ts +23 -0
- package/packages/db/tests/types/rpc.test-d.ts +60 -0
- package/packages/db/tests/types/schema-shape.test-d.ts +8 -0
- package/packages/db/tests/types/select-inference.test-d.ts +38 -0
- package/packages/db/tests/vendored/fixtures.ts +83 -0
- package/packages/db/tests/vendored/get-result.test-d.ts +75 -0
- package/packages/db/tsconfig.json +5 -0
- package/packages/db/tsconfig.test-d.json +5 -0
- package/packages/db/tsup.config.ts +11 -0
- package/packages/db/vitest.config.ts +8 -0
- package/packages/functions/LICENSE +21 -0
- package/packages/functions/README.md +96 -0
- package/packages/functions/dist/index.cjs +173 -0
- package/packages/functions/dist/index.d.cts +81 -0
- package/packages/functions/dist/index.d.ts +81 -0
- package/packages/functions/dist/index.js +168 -0
- package/packages/functions/node_modules/.bin/esbuild +19 -0
- package/packages/functions/node_modules/.bin/tsc +22 -0
- package/packages/functions/node_modules/.bin/tsserver +22 -0
- package/packages/functions/node_modules/.bin/tsup +22 -0
- package/packages/functions/node_modules/.bin/tsup-node +22 -0
- package/packages/functions/node_modules/.bin/vite +22 -0
- package/packages/functions/node_modules/.bin/vitest +22 -0
- package/packages/functions/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/functions/package.json +60 -0
- package/packages/functions/src/FunctionsService.ts +188 -0
- package/packages/functions/src/errors.ts +70 -0
- package/packages/functions/src/index.ts +6 -0
- package/packages/functions/tests/invoke.test.ts +97 -0
- package/packages/functions/tests/response-type-errors.test.ts +135 -0
- package/packages/functions/tsconfig.json +5 -0
- package/packages/functions/tsup.config.ts +11 -0
- package/packages/gen/LICENSE +21 -0
- package/packages/gen/README.md +84 -0
- package/packages/gen/dist/cli.cjs +334 -0
- package/packages/gen/dist/cli.d.cts +1 -0
- package/packages/gen/dist/cli.d.ts +1 -0
- package/packages/gen/dist/cli.js +332 -0
- package/packages/gen/dist/index.cjs +257 -0
- package/packages/gen/dist/index.d.cts +12 -0
- package/packages/gen/dist/index.d.ts +12 -0
- package/packages/gen/dist/index.js +255 -0
- package/packages/gen/node_modules/.bin/esbuild +19 -0
- package/packages/gen/node_modules/.bin/tsc +22 -0
- package/packages/gen/node_modules/.bin/tsserver +22 -0
- package/packages/gen/node_modules/.bin/tsup +22 -0
- package/packages/gen/node_modules/.bin/tsup-node +22 -0
- package/packages/gen/node_modules/.bin/vite +22 -0
- package/packages/gen/node_modules/.bin/vitest +22 -0
- package/packages/gen/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/gen/package.json +66 -0
- package/packages/gen/src/cli.ts +67 -0
- package/packages/gen/src/generate.ts +0 -0
- package/packages/gen/src/index.ts +2 -0
- package/packages/gen/tests/compile.test.ts +213 -0
- package/packages/gen/tests/e2e.test.ts +130 -0
- package/packages/gen/tests/generate.test.ts +396 -0
- package/packages/gen/tsconfig.json +5 -0
- package/packages/gen/tsup.config.ts +11 -0
- package/packages/notifications/LICENSE +21 -0
- package/packages/notifications/README.md +134 -0
- package/packages/notifications/dist/index.cjs +128 -0
- package/packages/notifications/dist/index.d.cts +81 -0
- package/packages/notifications/dist/index.d.ts +81 -0
- package/packages/notifications/dist/index.js +125 -0
- package/packages/notifications/node_modules/.bin/esbuild +19 -0
- package/packages/notifications/node_modules/.bin/tsc +22 -0
- package/packages/notifications/node_modules/.bin/tsserver +22 -0
- package/packages/notifications/node_modules/.bin/tsup +22 -0
- package/packages/notifications/node_modules/.bin/tsup-node +22 -0
- package/packages/notifications/node_modules/.bin/vite +22 -0
- package/packages/notifications/node_modules/.bin/vitest +22 -0
- package/packages/notifications/package.json +60 -0
- package/packages/notifications/src/NotificationsService.ts +230 -0
- package/packages/notifications/src/index.ts +19 -0
- package/packages/notifications/tsconfig.json +5 -0
- package/packages/notifications/tsup.config.ts +11 -0
- package/packages/realtime/LICENSE +21 -0
- package/packages/realtime/README.md +183 -0
- package/packages/realtime/dist/index.cjs +977 -0
- package/packages/realtime/dist/index.d.cts +259 -0
- package/packages/realtime/dist/index.d.ts +259 -0
- package/packages/realtime/dist/index.js +973 -0
- package/packages/realtime/node_modules/.bin/esbuild +19 -0
- package/packages/realtime/node_modules/.bin/tsc +22 -0
- package/packages/realtime/node_modules/.bin/tsserver +22 -0
- package/packages/realtime/node_modules/.bin/tsup +22 -0
- package/packages/realtime/node_modules/.bin/tsup-node +22 -0
- package/packages/realtime/node_modules/.bin/vite +22 -0
- package/packages/realtime/node_modules/.bin/vitest +22 -0
- package/packages/realtime/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/realtime/package.json +60 -0
- package/packages/realtime/src/AuraChannel.ts +485 -0
- package/packages/realtime/src/HeartbeatManager.ts +73 -0
- package/packages/realtime/src/RealtimeService.ts +505 -0
- package/packages/realtime/src/columnFilter.ts +301 -0
- package/packages/realtime/src/index.ts +4 -0
- package/packages/realtime/tests/ack-routing.test.ts +81 -0
- package/packages/realtime/tests/column-filter.test.ts +144 -0
- package/packages/realtime/tests/dispatch.test.ts +191 -0
- package/packages/realtime/tests/heartbeat.test.ts +67 -0
- package/packages/realtime/tests/http-broadcast.test.ts +68 -0
- package/packages/realtime/tests/realtime-token.test.ts +43 -0
- package/packages/realtime/tests/reconnect-race.test.ts +149 -0
- package/packages/realtime/tests/ref-id.test.ts +34 -0
- package/packages/realtime/tests/set-auth.test.ts +70 -0
- package/packages/realtime/tsconfig.json +5 -0
- package/packages/realtime/tsup.config.ts +11 -0
- package/packages/storage/LICENSE +21 -0
- package/packages/storage/README.md +141 -0
- package/packages/storage/dist/index.cjs +333 -0
- package/packages/storage/dist/index.d.cts +185 -0
- package/packages/storage/dist/index.d.ts +185 -0
- package/packages/storage/dist/index.js +329 -0
- package/packages/storage/node_modules/.bin/esbuild +19 -0
- package/packages/storage/node_modules/.bin/tsc +22 -0
- package/packages/storage/node_modules/.bin/tsserver +22 -0
- package/packages/storage/node_modules/.bin/tsup +22 -0
- package/packages/storage/node_modules/.bin/tsup-node +22 -0
- package/packages/storage/node_modules/.bin/vite +22 -0
- package/packages/storage/node_modules/.bin/vitest +22 -0
- package/packages/storage/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/packages/storage/package.json +60 -0
- package/packages/storage/src/DownloadBuilder.ts +60 -0
- package/packages/storage/src/StorageService.ts +323 -0
- package/packages/storage/src/index.ts +2 -0
- package/packages/storage/src/path.ts +35 -0
- package/packages/storage/src/transform.ts +15 -0
- package/packages/storage/tests/download.test.ts +64 -0
- package/packages/storage/tests/parity.test.ts +154 -0
- package/packages/storage/tests/path.test.ts +87 -0
- package/packages/storage/tests/transform.test.ts +79 -0
- package/packages/storage/tsconfig.json +5 -0
- package/packages/storage/tsup.config.ts +11 -0
- package/pnpm-workspace.yaml +24 -0
- package/tsconfig.base.json +15 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { BaseService } from '@aurabase/core'
|
|
2
|
+
import { AuraError, isTokenExpired, refreshOnce, isAbortError } from '@aurabase/core'
|
|
3
|
+
import type {
|
|
4
|
+
AuraResponse,
|
|
5
|
+
ChatMessage,
|
|
6
|
+
ChatResponse,
|
|
7
|
+
EmbedRequest,
|
|
8
|
+
EmbedResponse,
|
|
9
|
+
RagIngestRequest,
|
|
10
|
+
RagIngestResponse,
|
|
11
|
+
RagRequest,
|
|
12
|
+
RagResponse,
|
|
13
|
+
SemanticSearchResult,
|
|
14
|
+
} from '@aurabase/core'
|
|
15
|
+
import type { AuraClientLike } from '@aurabase/core'
|
|
16
|
+
import { parseSSE, type ChatStreamChunk, type ChatStreamFinal } from './sse'
|
|
17
|
+
import type { RagNamespace, RagDeleteResponse, RagReindexResponse } from './types'
|
|
18
|
+
|
|
19
|
+
export class AiService extends BaseService {
|
|
20
|
+
constructor(client: AuraClientLike) {
|
|
21
|
+
super(client)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private get prefix(): string {
|
|
25
|
+
return `/v1/ai/${this.client.projectId}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Send a chat completion request.
|
|
30
|
+
*/
|
|
31
|
+
async chat(
|
|
32
|
+
messages: ChatMessage[],
|
|
33
|
+
options?: { model?: string; temperature?: number; max_tokens?: number },
|
|
34
|
+
): Promise<AuraResponse<ChatResponse>> {
|
|
35
|
+
return this.request('POST', `${this.prefix}/chat`, {
|
|
36
|
+
messages,
|
|
37
|
+
...options,
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Chat completion streamé (SSE). Renvoie un `AsyncIterable` de deltas de tokens,
|
|
43
|
+
* consommable par `for await (const { delta } of ai.chatStream(msgs)) { … }`.
|
|
44
|
+
*
|
|
45
|
+
* Court-circuite volontairement le pipeline JSON (`@aurabase/core/fetch.ts`) et
|
|
46
|
+
* emprunte le chemin transport BRUT `client.fetchFn` (patron `StorageService.download`) :
|
|
47
|
+
* - refresh proactif du token expiré (même règle que `download`, marge 30 s) ;
|
|
48
|
+
* - pose manuelle de `Authorization: Bearer` + `apikey`, `Accept: text/event-stream` ;
|
|
49
|
+
* - lecture de `res.body` via `parseSSE`, mapping des events serveur :
|
|
50
|
+
* `delta` (`{ content }`) → `yield { delta }`, `error` (`{ error }`) → lève une
|
|
51
|
+
* `AuraError`, `done` → termine l'itération.
|
|
52
|
+
*
|
|
53
|
+
* `options.signal` interrompt proprement le flux (annulation du `fetch` + fin
|
|
54
|
+
* d'itération sans throw). Sur `!res.ok`, lève `AuraError(res.status)`.
|
|
55
|
+
*
|
|
56
|
+
* Backend route: POST /v1/ai/{project_id}/chat/stream
|
|
57
|
+
*/
|
|
58
|
+
async *chatStream(
|
|
59
|
+
messages: ChatMessage[],
|
|
60
|
+
options?: { model?: string; temperature?: number; max_tokens?: number; signal?: AbortSignal },
|
|
61
|
+
): AsyncGenerator<ChatStreamChunk> {
|
|
62
|
+
const client = this.client
|
|
63
|
+
const store = client.authStore
|
|
64
|
+
// Refresh proactif AVANT d'ouvrir le flux (aligné sur download()).
|
|
65
|
+
if (store.token && store.refreshToken && isTokenExpired(store.token, 30)) {
|
|
66
|
+
await refreshOnce(client)
|
|
67
|
+
}
|
|
68
|
+
const headers: Record<string, string> = {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
Accept: 'text/event-stream',
|
|
71
|
+
}
|
|
72
|
+
if (store.token) headers['Authorization'] = `Bearer ${store.token}`
|
|
73
|
+
if (client.apiKey) headers['apikey'] = client.apiKey
|
|
74
|
+
|
|
75
|
+
const body = JSON.stringify({
|
|
76
|
+
messages,
|
|
77
|
+
model: options?.model,
|
|
78
|
+
temperature: options?.temperature,
|
|
79
|
+
max_tokens: options?.max_tokens,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const res = await client.fetchFn(this.url(`${this.prefix}/chat/stream`), {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers,
|
|
86
|
+
body,
|
|
87
|
+
signal: options?.signal,
|
|
88
|
+
})
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
let parsed: { error?: { code?: string; message?: string }; message?: string } | null = null
|
|
91
|
+
try {
|
|
92
|
+
parsed = (await res.json()) as typeof parsed
|
|
93
|
+
} catch {
|
|
94
|
+
// corps non-JSON / vide → AuraError générique sur le status
|
|
95
|
+
}
|
|
96
|
+
throw new AuraError(res.status, parsed)
|
|
97
|
+
}
|
|
98
|
+
if (!res.body) {
|
|
99
|
+
throw new AuraError(0, {
|
|
100
|
+
error: { code: 'invalid_response', message: 'chatStream: la réponse SSE n\'a pas de corps lisible' },
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
for await (const msg of parseSSE(res.body as ReadableStream<Uint8Array>)) {
|
|
104
|
+
if (msg.event === 'delta') {
|
|
105
|
+
let content = ''
|
|
106
|
+
try {
|
|
107
|
+
content = (JSON.parse(msg.data) as { content?: string }).content ?? ''
|
|
108
|
+
} catch {
|
|
109
|
+
content = ''
|
|
110
|
+
}
|
|
111
|
+
if (content) yield { delta: content }
|
|
112
|
+
} else if (msg.event === 'error') {
|
|
113
|
+
let message = 'AI stream error'
|
|
114
|
+
try {
|
|
115
|
+
message = (JSON.parse(msg.data) as { error?: string }).error ?? message
|
|
116
|
+
} catch {
|
|
117
|
+
// conserve le message par défaut
|
|
118
|
+
}
|
|
119
|
+
throw new AuraError(0, { error: { code: 'ai_stream_error', message } })
|
|
120
|
+
} else if (msg.event === 'done') {
|
|
121
|
+
// Le serveur joint au `done` le provider effectif, la bascule éventuelle,
|
|
122
|
+
// le motif d'arrêt et l'usage réel. Les jeter privait l'application de
|
|
123
|
+
// toute visibilité sur une réponse tronquée et sur sa consommation.
|
|
124
|
+
let final: ChatStreamFinal | undefined
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(msg.data) as ChatStreamFinal & { status?: string }
|
|
127
|
+
const { status: _status, ...rest } = parsed
|
|
128
|
+
// Un chunk final n'est émis QUE s'il apporte quelque chose : face à un
|
|
129
|
+
// serveur plus ancien (`done` réduit à `[DONE]`), l'itération se termine
|
|
130
|
+
// exactement comme avant — aucun chunk supplémentaire à filtrer.
|
|
131
|
+
if (Object.keys(rest).length > 0) final = rest
|
|
132
|
+
} catch {
|
|
133
|
+
final = undefined
|
|
134
|
+
}
|
|
135
|
+
if (final) yield { delta: '', final }
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// Annulation demandée → fin d'itération PROPRE (pas de throw), parité avec
|
|
141
|
+
// une consommation interrompue via AbortController.
|
|
142
|
+
if (options?.signal?.aborted || isAbortError(err)) return
|
|
143
|
+
throw err
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generate embeddings for the given input(s).
|
|
149
|
+
*/
|
|
150
|
+
async embed(req: EmbedRequest): Promise<AuraResponse<EmbedResponse>> {
|
|
151
|
+
return this.request('POST', `${this.prefix}/embed`, req)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Retrieval-Augmented Generation: query a collection and get an answer.
|
|
156
|
+
*/
|
|
157
|
+
async rag(req: RagRequest): Promise<AuraResponse<RagResponse>> {
|
|
158
|
+
return this.request('POST', `${this.prefix}/rag/query`, req)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Ingest a document into a RAG namespace.
|
|
163
|
+
*/
|
|
164
|
+
async ragIngest(req: RagIngestRequest): Promise<AuraResponse<RagIngestResponse>> {
|
|
165
|
+
return this.request('POST', `${this.prefix}/rag/ingest`, req)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Liste les namespaces RAG du projet.
|
|
170
|
+
* Backend route: GET /v1/ai/{project_id}/rag/namespaces (→ `Vec<String>`).
|
|
171
|
+
*/
|
|
172
|
+
async listNamespaces(): Promise<AuraResponse<RagNamespace[]>> {
|
|
173
|
+
return this.request('GET', `${this.prefix}/rag/namespaces`)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Supprime un namespace RAG (et son corpus). Le segment `namespace` est
|
|
178
|
+
* URL-encodé (anti-injection de chemin).
|
|
179
|
+
* Backend route: DELETE /v1/ai/{project_id}/rag/{namespace} (→ `{ deleted }`).
|
|
180
|
+
*/
|
|
181
|
+
async deleteNamespace(namespace: string): Promise<AuraResponse<RagDeleteResponse>> {
|
|
182
|
+
return this.request('DELETE', `${this.prefix}/rag/${encodeURIComponent(namespace)}`)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Ré-indexe un namespace RAG avec le modèle d'embedding courant (remédiation
|
|
187
|
+
* après un changement de modèle/dimension). Le segment `namespace` est URL-encodé.
|
|
188
|
+
* Backend route: POST /v1/ai/{project_id}/rag/{namespace}/reindex (→ `ReindexResponse`).
|
|
189
|
+
*/
|
|
190
|
+
async reindexNamespace(namespace: string): Promise<AuraResponse<RagReindexResponse>> {
|
|
191
|
+
return this.request('POST', `${this.prefix}/rag/${encodeURIComponent(namespace)}/reindex`)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Semantic search over an embedding namespace.
|
|
196
|
+
*/
|
|
197
|
+
async semanticSearch(
|
|
198
|
+
query: string,
|
|
199
|
+
namespace: string,
|
|
200
|
+
options?: { top_k?: number; threshold?: number },
|
|
201
|
+
): Promise<AuraResponse<SemanticSearchResult[]>> {
|
|
202
|
+
const params = new URLSearchParams({ query, namespace })
|
|
203
|
+
if (options?.top_k !== undefined) params.set('top_k', String(options.top_k))
|
|
204
|
+
if (options?.threshold !== undefined) params.set('threshold', String(options.threshold))
|
|
205
|
+
return this.request('GET', `${this.prefix}/search?${params.toString()}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Natural-language to SQL: convert a question into a SQL query.
|
|
210
|
+
*/
|
|
211
|
+
async nl2sql(
|
|
212
|
+
question: string,
|
|
213
|
+
schema: string,
|
|
214
|
+
options?: {
|
|
215
|
+
dialect?: string
|
|
216
|
+
examples?: Array<{ question: string; sql: string }>
|
|
217
|
+
},
|
|
218
|
+
): Promise<AuraResponse<{ sql: string; explanation: string }>> {
|
|
219
|
+
return this.request('POST', `${this.prefix}/nl2sql`, {
|
|
220
|
+
question,
|
|
221
|
+
schema,
|
|
222
|
+
dialect: options?.dialect,
|
|
223
|
+
examples: options?.examples,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { AiService } from './AiService'
|
|
2
|
+
export { parseSSE } from './sse'
|
|
3
|
+
export type { SSEMessage, ChatStreamChunk } from './sse'
|
|
4
|
+
export type { RagNamespace, RagDeleteResponse, RagReindexResponse } from './types'
|
|
5
|
+
|
|
6
|
+
// Types publics (utilisables en consommant `@aurabase/ai` seul, sans importer core).
|
|
7
|
+
export type {
|
|
8
|
+
AuraResponse,
|
|
9
|
+
ChatMessage,
|
|
10
|
+
ChatResponse,
|
|
11
|
+
EmbedRequest,
|
|
12
|
+
EmbedResponse,
|
|
13
|
+
RagRequest,
|
|
14
|
+
RagResponse,
|
|
15
|
+
RagIngestRequest,
|
|
16
|
+
RagIngestResponse,
|
|
17
|
+
SemanticSearchResult,
|
|
18
|
+
} from '@aurabase/core'
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Parseur SSE (Server-Sent Events) — utilitaire de streaming côté SDK.
|
|
3
|
+
//
|
|
4
|
+
// Extrait pour être testable en isolation (trames partielles, multi-events par
|
|
5
|
+
// chunk, lignes keep-alive `:`) et réutilisable (chat streaming, futur SSE
|
|
6
|
+
// realtime/functions). NE DÉPEND PAS de `@aurabase/core/fetch.ts` : il opère sur
|
|
7
|
+
// un `ReadableStream<Uint8Array>` brut obtenu via `client.fetchFn` (patron Storage).
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/** Un message SSE dispatché : nom d'event (`message` par défaut) + données jointes. */
|
|
11
|
+
export interface SSEMessage {
|
|
12
|
+
event: string
|
|
13
|
+
data: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Usage de tokens rapporté par le provider à la fin d'un flux. */
|
|
17
|
+
export interface ChatStreamUsage {
|
|
18
|
+
input_tokens?: number | null
|
|
19
|
+
/** Tokens de sortie facturés, raisonnement inclus. */
|
|
20
|
+
output_tokens?: number | null
|
|
21
|
+
/** Part de `output_tokens` consommée par le raisonnement du modèle. */
|
|
22
|
+
reasoning_tokens?: number | null
|
|
23
|
+
/** `true` si les compteurs sont estimés faute d'usage rapporté par le provider. */
|
|
24
|
+
estimated?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Dernier chunk d'un flux de chat, émis avant la fin de l'itération.
|
|
29
|
+
*
|
|
30
|
+
* Le serveur envoie ces informations dans l'event SSE `done` ; elles étaient
|
|
31
|
+
* jusqu'ici ignorées par le SDK, si bien qu'une réponse tronquée (`length`) ou la
|
|
32
|
+
* consommation réelle de tokens restaient invisibles côté application.
|
|
33
|
+
*/
|
|
34
|
+
export interface ChatStreamFinal {
|
|
35
|
+
/** Provider ayant effectivement répondu. */
|
|
36
|
+
provider?: string
|
|
37
|
+
/** `true` si une bascule de fallback a eu lieu. */
|
|
38
|
+
fallback_used?: boolean
|
|
39
|
+
/** Motif d'arrêt normalisé : `stop` | `length` | `content_filter` | `tool_use` | `other`. */
|
|
40
|
+
finish_reason?: string | null
|
|
41
|
+
/** Raccourci : la réponse a été coupée par la limite de tokens. */
|
|
42
|
+
truncated?: boolean
|
|
43
|
+
usage?: ChatStreamUsage
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Chunk de streaming de chat.
|
|
48
|
+
*
|
|
49
|
+
* `delta` porte le texte incrémental ; le **dernier** chunk du flux porte `final`
|
|
50
|
+
* (sans `delta`) avec l'usage et le motif d'arrêt. Les consommateurs existants
|
|
51
|
+
* qui n'utilisent que `delta` continuent de fonctionner : ils reçoivent une
|
|
52
|
+
* chaîne vide sur ce dernier chunk.
|
|
53
|
+
*/
|
|
54
|
+
export interface ChatStreamChunk {
|
|
55
|
+
delta: string
|
|
56
|
+
final?: ChatStreamFinal
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse un flux SSE en `AsyncGenerator<SSEMessage>`.
|
|
61
|
+
*
|
|
62
|
+
* Conforme au protocole EventSource :
|
|
63
|
+
* - événements séparés par une ligne vide (`\n\n`) ;
|
|
64
|
+
* - champs `event:` / `data:` (espace optionnel après le `:`), autres champs ignorés ;
|
|
65
|
+
* - plusieurs `data:` d'un même event sont jointes par `\n` ;
|
|
66
|
+
* - lignes commençant par `:` = commentaires/keep-alive → ignorées ;
|
|
67
|
+
* - CRLF toléré (le `\r` de fin de ligne est retiré).
|
|
68
|
+
*
|
|
69
|
+
* Gère les trames partielles : un event scindé sur plusieurs chunks est
|
|
70
|
+
* bufferisé jusqu'à réception de la ligne vide. Un éventuel event final non
|
|
71
|
+
* terminé par une ligne vide est tout de même livré à la fermeture du flux.
|
|
72
|
+
*/
|
|
73
|
+
export async function* parseSSE(
|
|
74
|
+
stream: ReadableStream<Uint8Array>,
|
|
75
|
+
): AsyncGenerator<SSEMessage> {
|
|
76
|
+
const reader = stream.getReader()
|
|
77
|
+
const decoder = new TextDecoder()
|
|
78
|
+
let buffer = ''
|
|
79
|
+
let eventName = 'message'
|
|
80
|
+
let dataLines: string[] = []
|
|
81
|
+
|
|
82
|
+
// Consomme une ligne (déjà débarrassée de son `\n` et de son éventuel `\r`).
|
|
83
|
+
// Renvoie un SSEMessage à dispatcher sur ligne vide, sinon null.
|
|
84
|
+
const consumeLine = (raw: string): SSEMessage | null => {
|
|
85
|
+
const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw
|
|
86
|
+
if (line === '') {
|
|
87
|
+
// Fin d'event : dispatch si des données ont été accumulées.
|
|
88
|
+
if (dataLines.length === 0) {
|
|
89
|
+
eventName = 'message'
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
const msg: SSEMessage = { event: eventName, data: dataLines.join('\n') }
|
|
93
|
+
eventName = 'message'
|
|
94
|
+
dataLines = []
|
|
95
|
+
return msg
|
|
96
|
+
}
|
|
97
|
+
if (line.startsWith(':')) return null // commentaire / keep-alive
|
|
98
|
+
const colon = line.indexOf(':')
|
|
99
|
+
let field: string
|
|
100
|
+
let value: string
|
|
101
|
+
if (colon === -1) {
|
|
102
|
+
field = line
|
|
103
|
+
value = ''
|
|
104
|
+
} else {
|
|
105
|
+
field = line.slice(0, colon)
|
|
106
|
+
value = line.slice(colon + 1)
|
|
107
|
+
if (value.startsWith(' ')) value = value.slice(1)
|
|
108
|
+
}
|
|
109
|
+
if (field === 'event') eventName = value
|
|
110
|
+
else if (field === 'data') dataLines.push(value)
|
|
111
|
+
// id / retry : ignorés (non pertinents pour le SDK)
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
for (;;) {
|
|
117
|
+
const { done, value } = await reader.read()
|
|
118
|
+
if (done) break
|
|
119
|
+
buffer += decoder.decode(value, { stream: true })
|
|
120
|
+
let idx: number
|
|
121
|
+
while ((idx = buffer.indexOf('\n')) !== -1) {
|
|
122
|
+
const raw = buffer.slice(0, idx)
|
|
123
|
+
buffer = buffer.slice(idx + 1)
|
|
124
|
+
const msg = consumeLine(raw)
|
|
125
|
+
if (msg) yield msg
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Vidange du décodeur + traitement d'une éventuelle dernière ligne sans `\n`.
|
|
129
|
+
buffer += decoder.decode()
|
|
130
|
+
if (buffer.length > 0) {
|
|
131
|
+
const msg = consumeLine(buffer)
|
|
132
|
+
if (msg) yield msg
|
|
133
|
+
}
|
|
134
|
+
// Event final non terminé par une ligne vide (data accumulée sans dispatch).
|
|
135
|
+
if (dataLines.length > 0) {
|
|
136
|
+
yield { event: eventName, data: dataLines.join('\n') }
|
|
137
|
+
}
|
|
138
|
+
} finally {
|
|
139
|
+
reader.releaseLock()
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Types locaux du package @aurabase/ai (non mutualisés dans @aurabase/core).
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Un namespace RAG. Le backend (`GET /rag/namespaces` → `Vec<String>`) renvoie
|
|
7
|
+
* de simples chaînes ; le type reste un alias pour documenter l'intention et
|
|
8
|
+
* autoriser une évolution ultérieure vers un objet sans casser les appelants.
|
|
9
|
+
*/
|
|
10
|
+
export type RagNamespace = string
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Réponse de `DELETE /rag/{namespace}` — forme réelle du handler
|
|
14
|
+
* (`rag_delete_namespace` → `{ "deleted": <count> }`).
|
|
15
|
+
*/
|
|
16
|
+
export interface RagDeleteResponse {
|
|
17
|
+
/** Nombre d'entrées supprimées du namespace. */
|
|
18
|
+
deleted: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Réponse de `POST /rag/{namespace}/reindex` — forme réelle du handler
|
|
23
|
+
* (`ReindexResponse` dans `services/aura-ai/src/rag/mod.rs`).
|
|
24
|
+
*/
|
|
25
|
+
export interface RagReindexResponse {
|
|
26
|
+
/** Nombre de chunks ré-embeddés sous le modèle courant. */
|
|
27
|
+
reindexed: number
|
|
28
|
+
/** Modèle d'embedding désormais appliqué au namespace. */
|
|
29
|
+
embedding_model: string
|
|
30
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { AiService } from '../src/AiService'
|
|
3
|
+
import { BaseAuthStore, isAuraError } from '@aurabase/core'
|
|
4
|
+
import type { AuraClientLike } from '@aurabase/core'
|
|
5
|
+
|
|
6
|
+
function b64url(o: unknown) { return btoa(JSON.stringify(o)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') }
|
|
7
|
+
function expiredJwt() { return `${b64url({ alg: 'HS256' })}.${b64url({ exp: Math.floor(Date.now() / 1000) - 60 })}.s` }
|
|
8
|
+
function validJwt() { return `${b64url({ alg: 'HS256' })}.${b64url({ exp: Math.floor(Date.now() / 1000) + 3600 })}.s` }
|
|
9
|
+
function fakeUser() { return { id: 'u', email: null, role: 'authenticated', email_verified: true, is_banned: false, is_anonymous: false, mfa_enabled: false, user_metadata: {}, app_metadata: {}, created_at: '2025-01-01T00:00:00Z' } }
|
|
10
|
+
|
|
11
|
+
function streamResponse(chunks: string[], status = 200): Response {
|
|
12
|
+
const enc = new TextEncoder()
|
|
13
|
+
let i = 0
|
|
14
|
+
const body = new ReadableStream<Uint8Array>({
|
|
15
|
+
pull(controller) {
|
|
16
|
+
if (i < chunks.length) controller.enqueue(enc.encode(chunks[i++]))
|
|
17
|
+
else controller.close()
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
return new Response(body, { status, headers: { 'Content-Type': 'text/event-stream' } })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function make(
|
|
24
|
+
responder: (url: string, init: RequestInit) => Response | Promise<Response>,
|
|
25
|
+
store = new BaseAuthStore(),
|
|
26
|
+
) {
|
|
27
|
+
const calls: Array<{ url: string; init: RequestInit }> = []
|
|
28
|
+
const client: AuraClientLike = {
|
|
29
|
+
baseUrl: 'http://x', projectId: 'p', apiKey: 'k', authStore: store,
|
|
30
|
+
fetchFn: (async (url: string, init: RequestInit) => { calls.push({ url: String(url), init }); return responder(String(url), init) }) as unknown as typeof globalThis.fetch,
|
|
31
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
33
|
+
}
|
|
34
|
+
return { ai: new AiService(client), calls }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const delta = (c: string) => `event: delta\ndata: ${JSON.stringify({ content: c })}\n\n`
|
|
38
|
+
const done = () => `event: done\ndata: ${JSON.stringify({ status: '[DONE]' })}\n\n`
|
|
39
|
+
|
|
40
|
+
const doneRiche = () =>
|
|
41
|
+
`event: done\ndata: ${JSON.stringify({
|
|
42
|
+
status: '[DONE]',
|
|
43
|
+
provider: 'google',
|
|
44
|
+
fallback_used: false,
|
|
45
|
+
finish_reason: 'length',
|
|
46
|
+
truncated: true,
|
|
47
|
+
usage: { input_tokens: 11, output_tokens: 76, reasoning_tokens: 74, estimated: false },
|
|
48
|
+
})}\n\n`
|
|
49
|
+
|
|
50
|
+
describe('ai.chatStream — SSE via fetchFn brut', () => {
|
|
51
|
+
it('livre un chunk final avec usage et motif d’arrêt quand le serveur les fournit', async () => {
|
|
52
|
+
const { ai } = make(() => streamResponse([delta('Ah, les'), doneRiche()]))
|
|
53
|
+
const chunks: { delta: string; final?: Record<string, unknown> }[] = []
|
|
54
|
+
for await (const c of ai.chatStream([{ role: 'user', content: 'hi' }])) chunks.push(c)
|
|
55
|
+
|
|
56
|
+
// Le texte reste livré à l'identique…
|
|
57
|
+
expect(chunks.map((c) => c.delta).join('')).toBe('Ah, les')
|
|
58
|
+
// …et le dernier chunk porte de quoi détecter une réponse TRONQUÉE, qui était
|
|
59
|
+
// jusqu'ici indiscernable d'une réponse complète côté application.
|
|
60
|
+
const final = chunks[chunks.length - 1].final as Record<string, unknown>
|
|
61
|
+
expect(final).toBeDefined()
|
|
62
|
+
expect(final.finish_reason).toBe('length')
|
|
63
|
+
expect(final.truncated).toBe(true)
|
|
64
|
+
expect(final.provider).toBe('google')
|
|
65
|
+
// Usage réel, raisonnement inclus (74 des 76 tokens de sortie).
|
|
66
|
+
expect((final.usage as Record<string, number>).output_tokens).toBe(76)
|
|
67
|
+
expect((final.usage as Record<string, number>).reasoning_tokens).toBe(74)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('serveur ancien (done sans détail) → aucun chunk final ajouté', async () => {
|
|
71
|
+
const { ai } = make(() => streamResponse([delta('a'), delta('b'), done()]))
|
|
72
|
+
const out: string[] = []
|
|
73
|
+
for await (const c of ai.chatStream([{ role: 'user', content: 'hi' }])) out.push(c.delta)
|
|
74
|
+
expect(out).toEqual(['a', 'b'])
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('livre des deltas incrémentaux puis termine sur done', async () => {
|
|
78
|
+
const { ai } = make(() => streamResponse([delta('Hel'), delta('lo'), delta('!'), done()]))
|
|
79
|
+
const out: string[] = []
|
|
80
|
+
for await (const chunk of ai.chatStream([{ role: 'user', content: 'hi' }])) {
|
|
81
|
+
out.push(chunk.delta)
|
|
82
|
+
}
|
|
83
|
+
expect(out).toEqual(['Hel', 'lo', '!'])
|
|
84
|
+
expect(out.join('')).toBe('Hello!')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('POST vers /chat/stream avec Accept text/event-stream + apikey + Authorization', async () => {
|
|
88
|
+
const store = new BaseAuthStore()
|
|
89
|
+
store.save(validJwt(), 'rt', fakeUser())
|
|
90
|
+
const { ai, calls } = make(() => streamResponse([done()]), store)
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
92
|
+
for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
|
|
93
|
+
expect(calls[0]!.url).toBe('http://x/v1/ai/p/chat/stream')
|
|
94
|
+
expect(calls[0]!.init.method).toBe('POST')
|
|
95
|
+
const h = calls[0]!.init.headers as Record<string, string>
|
|
96
|
+
expect(h['Accept']).toBe('text/event-stream')
|
|
97
|
+
expect(h['apikey']).toBe('k')
|
|
98
|
+
expect(h['Authorization']).toBe(`Bearer ${store.token}`)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('event error → lève une AuraError exploitable', async () => {
|
|
102
|
+
const { ai } = make(() => streamResponse([delta('a'), `event: error\ndata: ${JSON.stringify({ error: 'boom' })}\n\n`]))
|
|
103
|
+
await expect(async () => {
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
105
|
+
for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
|
|
106
|
+
}).rejects.toSatisfy((e: unknown) => isAuraError(e) && (e as { message: string }).message.includes('boom'))
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('!res.ok → lève AuraError(status)', async () => {
|
|
110
|
+
const { ai } = make(() => new Response(JSON.stringify({ error: { code: 'nope', message: 'x' } }), { status: 429 }))
|
|
111
|
+
await expect(async () => {
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
113
|
+
for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
|
|
114
|
+
}).rejects.toSatisfy((e: unknown) => isAuraError(e) && (e as { status: number }).status === 429)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('AbortSignal déjà avorté → fin d’itération propre (aucun delta, pas de throw)', async () => {
|
|
118
|
+
const ac = new AbortController()
|
|
119
|
+
ac.abort()
|
|
120
|
+
const { ai } = make((_url, init) =>
|
|
121
|
+
init.signal?.aborted
|
|
122
|
+
? Promise.reject(Object.assign(new Error('aborted'), { name: 'AbortError' }))
|
|
123
|
+
: Promise.resolve(streamResponse([delta('a'), done()])),
|
|
124
|
+
)
|
|
125
|
+
const out: string[] = []
|
|
126
|
+
for await (const chunk of ai.chatStream([{ role: 'user', content: 'hi' }], { signal: ac.signal })) {
|
|
127
|
+
out.push(chunk.delta)
|
|
128
|
+
}
|
|
129
|
+
expect(out).toEqual([])
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('refresh proactif si token expiré (refresh AVANT ouverture du flux)', async () => {
|
|
133
|
+
const store = new BaseAuthStore()
|
|
134
|
+
store.save(expiredJwt(), 'rt', fakeUser())
|
|
135
|
+
const { ai, calls } = make((url) => {
|
|
136
|
+
if (url.includes('/v1/auth/')) return new Response(JSON.stringify({ access_token: validJwt(), refresh_token: 'rt2', token_type: 'bearer', expires_in: 3600, user: fakeUser() }), { status: 200, headers: { 'Content-Type': 'application/json' } })
|
|
137
|
+
return streamResponse([done()])
|
|
138
|
+
}, store)
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
140
|
+
for await (const _ of ai.chatStream([{ role: 'user', content: 'hi' }])) { /* drain */ }
|
|
141
|
+
expect(calls[0]!.url).toContain('/v1/auth/p/refresh')
|
|
142
|
+
expect(calls[1]!.url).toContain('/chat/stream')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('chat() (JSON) reste inchangé', async () => {
|
|
146
|
+
const { ai } = make(() => new Response(JSON.stringify({ data: { model: 'm', content: 'hi' } }), { status: 200, headers: { 'Content-Type': 'application/json' } }))
|
|
147
|
+
const { data, error } = await ai.chat([{ role: 'user', content: 'hi' }])
|
|
148
|
+
expect(error).toBeNull()
|
|
149
|
+
expect(data).toEqual({ model: 'm', content: 'hi' })
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { AiService } from '../src/AiService'
|
|
3
|
+
import { BaseAuthStore } from '@aurabase/core'
|
|
4
|
+
import type { AuraClientLike } from '@aurabase/core'
|
|
5
|
+
|
|
6
|
+
function make(responder: (url: string, init: RequestInit) => Response) {
|
|
7
|
+
const calls: Array<{ url: string; method: string }> = []
|
|
8
|
+
const client: AuraClientLike = {
|
|
9
|
+
baseUrl: 'http://x', projectId: 'p', apiKey: 'k', authStore: new BaseAuthStore(),
|
|
10
|
+
fetchFn: (async (url: string, init: RequestInit) => {
|
|
11
|
+
calls.push({ url: String(url), method: String(init.method) })
|
|
12
|
+
return responder(String(url), init)
|
|
13
|
+
}) as unknown as typeof globalThis.fetch,
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
auth: { refreshSession: async () => ({ data: null, error: null }) as any },
|
|
16
|
+
}
|
|
17
|
+
return { ai: new AiService(client), calls }
|
|
18
|
+
}
|
|
19
|
+
const json = (o: unknown, s = 200) => new Response(JSON.stringify(o), { status: s, headers: { 'Content-Type': 'application/json' } })
|
|
20
|
+
|
|
21
|
+
describe('ai RAG — CRUD namespaces', () => {
|
|
22
|
+
it('listNamespaces → GET /rag/namespaces, dé-wrappe la liste', async () => {
|
|
23
|
+
const { ai, calls } = make(() => json({ data: ['ns_a', 'ns_b'] }))
|
|
24
|
+
const { data, error } = await ai.listNamespaces()
|
|
25
|
+
expect(error).toBeNull()
|
|
26
|
+
expect(data).toEqual(['ns_a', 'ns_b'])
|
|
27
|
+
expect(calls[0]).toEqual({ url: 'http://x/v1/ai/p/rag/namespaces', method: 'GET' })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('deleteNamespace → DELETE /rag/{ns} (encodé), dé-wrappe { deleted }', async () => {
|
|
31
|
+
const { ai, calls } = make(() => json({ data: { deleted: 3 } }))
|
|
32
|
+
const { data, error } = await ai.deleteNamespace('ns test/x')
|
|
33
|
+
expect(error).toBeNull()
|
|
34
|
+
expect(data).toEqual({ deleted: 3 })
|
|
35
|
+
expect(calls[0]!.method).toBe('DELETE')
|
|
36
|
+
expect(calls[0]!.url).toBe('http://x/v1/ai/p/rag/ns%20test%2Fx')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('reindexNamespace → POST /rag/{ns}/reindex (encodé), dé-wrappe ReindexResponse', async () => {
|
|
40
|
+
const { ai, calls } = make(() => json({ data: { reindexed: 42, embedding_model: 'text-embedding-3-small' } }))
|
|
41
|
+
const { data, error } = await ai.reindexNamespace('ns#1')
|
|
42
|
+
expect(error).toBeNull()
|
|
43
|
+
expect(data).toEqual({ reindexed: 42, embedding_model: 'text-embedding-3-small' })
|
|
44
|
+
expect(calls[0]!.method).toBe('POST')
|
|
45
|
+
expect(calls[0]!.url).toBe('http://x/v1/ai/p/rag/ns%231/reindex')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('propage une erreur HTTP (status)', async () => {
|
|
49
|
+
const { ai } = make(() => json({ error: { code: 'not_found', message: 'x' } }, 404))
|
|
50
|
+
const { data, error } = await ai.deleteNamespace('missing')
|
|
51
|
+
expect(data).toBeNull()
|
|
52
|
+
expect(error?.status).toBe(404)
|
|
53
|
+
})
|
|
54
|
+
})
|