@percepta/create 3.4.1 → 3.4.3
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/package.json +1 -1
- package/templates/webapp/AGENTS.md +1 -1
- package/templates/webapp/agent-skills/deploy.md +1 -1
- package/templates/webapp/agent-skills/inngest.md +5 -5
- package/templates/webapp/agent-skills/langfuse.md +4 -4
- package/templates/webapp/agent-skills/llm.md +1 -1
- package/templates/webapp/deploy/README.md +1 -1
- package/templates/webapp/package.json.template +8 -14
- package/templates/webapp/src/app/api/inngest/route.ts +12 -22
- package/templates/webapp/src/instrumentation.ts +2 -63
- package/templates/webapp/src/server/trpc.ts +6 -18
- package/templates/webapp/src/services/AuthContextService.ts +7 -59
- package/templates/webapp/src/services/inngest/InngestService.ts +14 -62
- package/templates/webapp/src/services/langfuse/LangfuseService.ts +9 -77
- package/templates/webapp/src/services/llm/LLMService.ts +10 -88
- package/templates/webapp/src/services/logger/AppLogger.ts +3 -48
- package/templates/webapp/src/utils/syncInngestApp.ts +4 -56
- package/templates/webapp/scripts/deploy-percepta-test.ts +0 -1112
- package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +0 -497
- package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +0 -5
- package/templates/webapp/src/services/llm/LlmProviderService.ts +0 -85
package/package.json
CHANGED
|
@@ -158,7 +158,7 @@ logger.info({ safe: { requestId }, unsafe: { email } }, "User action completed")
|
|
|
158
158
|
logger.error({ safe: { documentId } }, "Processing failed", error);
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
The app's logger is initialized in `src/services/logger/AppLogger.ts` using `
|
|
161
|
+
The app's logger is initialized in `src/services/logger/AppLogger.ts` using `createLoggerRuntime()` from this package. It uses `AsyncLocalStorage` for automatic request context propagation.
|
|
162
162
|
|
|
163
163
|
### @percepta/utils — Shared Utilities
|
|
164
164
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This guide deploys __APP_TITLE__ to `https://__APP_NAME__.percepta-test.aitco.dev` using Ryvn. Tell Claude "deploy this app to percepta-test" and it should run the direct deploy helper below.
|
|
4
4
|
|
|
5
|
-
This is the existing-environment deploy motion: `percepta-test` already owns the shared platform services, and this app is wired into them. Fresh-environment platform bootstrap is separate and should use a Ryvn blueprint or environment-specific platform rollout before app deploys run.
|
|
5
|
+
This is the existing-environment deploy motion: `percepta-test` already owns the shared platform services, and this app is wired into them. Fresh-environment platform bootstrap is separate and should use a Ryvn blueprint or environment-specific platform rollout before app deploys run. The `pnpm deploy:percepta-test` script delegates to the versioned `@percepta/deploy` CLI; this app owns only its Ryvn YAML and generated secrets env file.
|
|
6
6
|
|
|
7
7
|
## What's Already Scaffolded
|
|
8
8
|
|
|
@@ -43,14 +43,14 @@ schemas validate `event.data`, so do not wrap payload schemas in another
|
|
|
43
43
|
|
|
44
44
|
### 1. Create a function collection
|
|
45
45
|
|
|
46
|
-
Group related functions into a collection class that implements `
|
|
46
|
+
Group related functions into a collection class that implements `PerceptaInngestFunctionCollection`:
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
|
+
import { type PerceptaInngestFunctionCollection } from "@percepta/inngest";
|
|
49
50
|
import { type InngestFunction } from "inngest";
|
|
50
|
-
import { type InngestFunctionCollection } from "../InngestFunctionCollection";
|
|
51
51
|
import { InngestService } from "../InngestService";
|
|
52
52
|
|
|
53
|
-
export class DocumentFunctions implements
|
|
53
|
+
export class DocumentFunctions implements PerceptaInngestFunctionCollection {
|
|
54
54
|
private inngestService = InngestService.create();
|
|
55
55
|
|
|
56
56
|
public get functions(): InngestFunction.Like[] {
|
|
@@ -85,9 +85,9 @@ export class DocumentFunctions implements InngestFunctionCollection {
|
|
|
85
85
|
Add your collection to the `functionCollections` array in the Inngest serve endpoint:
|
|
86
86
|
|
|
87
87
|
```typescript
|
|
88
|
-
const functionCollections:
|
|
88
|
+
const functionCollections: PerceptaInngestFunctionCollection[] = [
|
|
89
89
|
new DocumentFunctions(),
|
|
90
|
-
]
|
|
90
|
+
];
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
## Sending Events
|
|
@@ -21,14 +21,14 @@ Langfuse is an open-source LLM observability platform. It captures traces, spans
|
|
|
21
21
|
The template uses Next.js's instrumentation hook (called on server startup) to bootstrap OTEL with both the environment collector and optional Langfuse:
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
25
|
-
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
26
24
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
25
|
+
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
26
|
+
import { createLangfuseSpanProcessor } from "@percepta/ai";
|
|
27
27
|
|
|
28
28
|
const sdk = new NodeSDK({
|
|
29
29
|
spanProcessors: [
|
|
30
30
|
new tracing.BatchSpanProcessor(otlpTraceExporter),
|
|
31
|
-
|
|
31
|
+
createLangfuseSpanProcessor(env, logger),
|
|
32
32
|
],
|
|
33
33
|
instrumentations: [getNodeAutoInstrumentations()],
|
|
34
34
|
});
|
|
@@ -37,7 +37,7 @@ sdk.start();
|
|
|
37
37
|
|
|
38
38
|
- `getNodeAutoInstrumentations()` automatically instruments HTTP calls, database queries, and other standard Node.js operations.
|
|
39
39
|
- The OTLP span processor forwards general server traces to the environment collector.
|
|
40
|
-
-
|
|
40
|
+
- The shared Langfuse span processor forwards only AI SDK spans to Langfuse when all `LANGFUSE_*` variables are set. General HTTP, DB, and server spans stay in the OTEL/LGTM pipeline and are not sent to Langfuse by default.
|
|
41
41
|
|
|
42
42
|
**You do not need to manually instrument LLM calls.** Use `LLMService` from `src/services/llm/LLMService.ts`; it calls the Vercel AI SDK with telemetry enabled and provider/model metadata attached.
|
|
43
43
|
|
|
@@ -30,7 +30,7 @@ const stream = LLMService.create().streamText({
|
|
|
30
30
|
|
|
31
31
|
## Providers
|
|
32
32
|
|
|
33
|
-
`
|
|
33
|
+
The shared `@percepta/ai` provider helper chooses a provider at call time:
|
|
34
34
|
|
|
35
35
|
1. `LLM_PROVIDER`, when explicitly set.
|
|
36
36
|
2. Anthropic when `ANTHROPIC_API_KEY` is available.
|
|
@@ -18,7 +18,7 @@ These files deploy to `https://__APP_NAME__.percepta-test.aitco.dev`.
|
|
|
18
18
|
|
|
19
19
|
The default deploy helper performs the existing-environment deploy motion: it assumes the target Ryvn environment already has the shared platform services installed, then wires this app into them. For `percepta-test`, that means shared Postgres, Inngest, the OTEL collector, the LGTM stack, and Langfuse must already exist before app deploy starts. Fresh-environment platform bootstrap is a separate motion and should be handled by a Ryvn blueprint or environment-specific platform rollout.
|
|
20
20
|
|
|
21
|
-
The helper talks directly to Ryvn: it preflights the existing platform services, creates/updates the services, runs the GitHub Actions release workflows, creates the schema installation, approves the schema Terraform plan, creates or updates app-scoped Ryvn secrets, creates the web installation, waits for health, and verifies the health and app routes.
|
|
21
|
+
The `pnpm deploy:percepta-test` script delegates to the versioned `@percepta/deploy` CLI. The app owns only the Ryvn service/installation YAML and secrets env file. The helper talks directly to Ryvn: it preflights the existing platform services, creates/updates the services, runs the GitHub Actions release workflows, creates the schema installation, approves the schema Terraform plan, creates or updates app-scoped Ryvn secrets, creates the web installation, waits for health, and verifies the health and app routes.
|
|
22
22
|
|
|
23
23
|
## Deploying
|
|
24
24
|
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"db:setup-readonly": "tsx ./scripts/setup-readonly-user.ts",
|
|
25
25
|
"db:studio": "pnpm db:setup-and-migrate && drizzle-kit studio",
|
|
26
26
|
"db:seed": "tsx ./scripts/seed.ts",
|
|
27
|
-
"deploy:percepta-test": "
|
|
28
|
-
"deploy:percepta-test:pr": "
|
|
27
|
+
"deploy:percepta-test": "percepta-deploy percepta-test --app __APP_NAME__ --repo __REPO_NAME__",
|
|
28
|
+
"deploy:percepta-test:pr": "percepta-deploy percepta-test pr --app __APP_NAME__ --database-schema __APP_NAME_SNAKE__",
|
|
29
29
|
"test": "vitest run",
|
|
30
30
|
"test:e2e": "pnpm run setup && playwright test",
|
|
31
31
|
"test:e2e:install": "playwright install chromium",
|
|
@@ -33,8 +33,6 @@
|
|
|
33
33
|
"test:watch": "vitest"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ai-sdk/anthropic": "^2.0.23",
|
|
37
|
-
"@ai-sdk/openai": "^2.0.23",
|
|
38
36
|
"@aws-sdk/client-s3": "^3.888.0",
|
|
39
37
|
"@aws-sdk/client-secrets-manager": "^3.914.0",
|
|
40
38
|
"@aws-sdk/client-sts": "^3.913.0",
|
|
@@ -48,9 +46,6 @@
|
|
|
48
46
|
"@grafana/faro-web-sdk": "^1.14.0",
|
|
49
47
|
"@grafana/faro-web-tracing": "^1.14.0",
|
|
50
48
|
"@hookform/resolvers": "^5.2.2",
|
|
51
|
-
"@inngest/middleware-validation": "^0.0.5",
|
|
52
|
-
"@langfuse/otel": "^4.0.0",
|
|
53
|
-
"@langfuse/tracing": "^4.0.0",
|
|
54
49
|
"@mantine/hooks": "^8.3.1",
|
|
55
50
|
"@next/env": "^15.3.5",
|
|
56
51
|
"@opentelemetry/api": "^1.9.0",
|
|
@@ -58,10 +53,12 @@
|
|
|
58
53
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.203.0",
|
|
59
54
|
"@opentelemetry/sdk-node": "^0.203.0",
|
|
60
55
|
"@__REPO_NAME__/auth": "workspace:*",
|
|
61
|
-
"@percepta/access-control": "0.
|
|
56
|
+
"@percepta/access-control": "0.7.0",
|
|
57
|
+
"@percepta/ai": "0.1.0",
|
|
62
58
|
"@percepta/design": "0.3.2",
|
|
63
|
-
"@percepta/
|
|
64
|
-
"@percepta/
|
|
59
|
+
"@percepta/inngest": "0.1.0",
|
|
60
|
+
"@percepta/logger": "0.1.0",
|
|
61
|
+
"@percepta/next-utils": "0.2.0",
|
|
65
62
|
"@percepta/utils": "0.1.10",
|
|
66
63
|
"@radix-ui/react-slot": "^1.2.3",
|
|
67
64
|
"@tanstack/react-query": "^5.81.5",
|
|
@@ -69,7 +66,6 @@
|
|
|
69
66
|
"@trpc/client": "^11.4.3",
|
|
70
67
|
"@trpc/server": "^11.4.3",
|
|
71
68
|
"@trpc/tanstack-react-query": "^11.4.3",
|
|
72
|
-
"ai": "^5.0.24",
|
|
73
69
|
"better-auth": "^1.6.4",
|
|
74
70
|
"clsx": "^2.1.1",
|
|
75
71
|
"date-fns": "^4.1.0",
|
|
@@ -80,15 +76,12 @@
|
|
|
80
76
|
"import-in-the-middle": "^1.14.2",
|
|
81
77
|
"inngest": "^3.44.3",
|
|
82
78
|
"keyed-type-union": "^0.1.0",
|
|
83
|
-
"langfuse": "^3.38.6",
|
|
84
79
|
"lodash-es": "^4.17.21",
|
|
85
80
|
"lucide-react": "^0.542.0",
|
|
86
81
|
"mime-types": "^3.0.1",
|
|
87
82
|
"next": "^15.3.5",
|
|
88
83
|
"numeral": "^2.0.6",
|
|
89
84
|
"pg": "^8.16.3",
|
|
90
|
-
"pino": "^10.1.0",
|
|
91
|
-
"pino-pretty": "^13.1.3",
|
|
92
85
|
"pluralize": "^8.0.0",
|
|
93
86
|
"react": "^19.0.0",
|
|
94
87
|
"react-dom": "^19.0.0",
|
|
@@ -109,6 +102,7 @@
|
|
|
109
102
|
"@next/eslint-plugin-next": "^15.3.5",
|
|
110
103
|
"@playwright/test": "^1.58.2",
|
|
111
104
|
"@percepta/build": "0.4.0",
|
|
105
|
+
"@percepta/deploy": "0.1.0",
|
|
112
106
|
"@tailwindcss/postcss": "^4.1.11",
|
|
113
107
|
"@types/formidable": "^3.4.5",
|
|
114
108
|
"@types/he": "^1.2.3",
|
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
createPerceptaInngestNextRouteHandlers,
|
|
3
|
+
type PerceptaInngestFunctionCollection,
|
|
4
|
+
} from "@percepta/inngest";
|
|
4
5
|
import { InngestService } from "../../../services/inngest/InngestService";
|
|
5
6
|
|
|
6
7
|
export const dynamic = "force-dynamic";
|
|
7
8
|
|
|
8
|
-
const functionCollections:
|
|
9
|
+
const functionCollections: PerceptaInngestFunctionCollection[] = [];
|
|
9
10
|
|
|
10
11
|
// InngestService.create() reads env vars and throws if INNGEST_BASE_URL is
|
|
11
12
|
// unset. Defer initialization until request time so `next build` can compile
|
|
12
13
|
// this route without those vars present.
|
|
13
|
-
|
|
14
|
+
const inngestHandlers = createPerceptaInngestNextRouteHandlers({
|
|
15
|
+
getService: () => InngestService.create(),
|
|
16
|
+
getFunctions: () => functionCollections.flatMap(({ functions }) => functions),
|
|
17
|
+
});
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (cachedHandlers == null) {
|
|
19
|
-
const inngestService = InngestService.create();
|
|
20
|
-
cachedHandlers = serve({
|
|
21
|
-
client: inngestService.client,
|
|
22
|
-
functions: functionCollections.flatMap(({ functions }) => functions),
|
|
23
|
-
signingKey: inngestService.signingKey,
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
return cachedHandlers;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const GET: Handlers["GET"] = (...args) => getHandlers().GET(...args);
|
|
30
|
-
export const POST: Handlers["POST"] = (...args) => getHandlers().POST(...args);
|
|
31
|
-
export const PUT: Handlers["PUT"] = (...args) => getHandlers().PUT(...args);
|
|
19
|
+
export const GET = inngestHandlers.GET;
|
|
20
|
+
export const POST = inngestHandlers.POST;
|
|
21
|
+
export const PUT = inngestHandlers.PUT;
|
|
@@ -1,52 +1,12 @@
|
|
|
1
|
-
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
2
1
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
3
2
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
4
3
|
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
4
|
+
import { createLangfuseSpanProcessor } from "@percepta/ai";
|
|
5
5
|
import { compact } from "lodash-es";
|
|
6
6
|
import { getEnvConfig } from "./config/getEnvConfig";
|
|
7
7
|
import { getLogger } from "./services/logger/AppLogger";
|
|
8
8
|
|
|
9
9
|
type SpanProcessor = tracing.SpanProcessor;
|
|
10
|
-
type ReadableSpan = Parameters<SpanProcessor["onEnd"]>[0];
|
|
11
|
-
type OnStartArgs = Parameters<SpanProcessor["onStart"]>;
|
|
12
|
-
|
|
13
|
-
class FilteringSpanProcessor implements SpanProcessor {
|
|
14
|
-
public constructor(
|
|
15
|
-
private delegate: SpanProcessor,
|
|
16
|
-
private shouldExport: (span: ReadableSpan) => boolean,
|
|
17
|
-
) {}
|
|
18
|
-
|
|
19
|
-
public forceFlush(): Promise<void> {
|
|
20
|
-
return this.delegate.forceFlush();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
public onStart(...args: OnStartArgs): void {
|
|
24
|
-
this.delegate.onStart(...args);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
public onEnd(span: ReadableSpan): void {
|
|
28
|
-
if (this.shouldExport(span)) {
|
|
29
|
-
this.delegate.onEnd(span);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public shutdown(): Promise<void> {
|
|
34
|
-
return this.delegate.shutdown();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function isAiSdkSpan(span: ReadableSpan): boolean {
|
|
39
|
-
const operationId = span.attributes["ai.operationId"];
|
|
40
|
-
if (typeof operationId === "string" && operationId.startsWith("ai.")) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (span.name.startsWith("ai.")) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return Object.keys(span.attributes).some((key) => key.startsWith("gen_ai."));
|
|
49
|
-
}
|
|
50
10
|
|
|
51
11
|
function getOtlpTracesEndpoint(): string | undefined {
|
|
52
12
|
const {
|
|
@@ -90,28 +50,7 @@ function getOtlpSpanProcessor(): tracing.BatchSpanProcessor | undefined {
|
|
|
90
50
|
}
|
|
91
51
|
|
|
92
52
|
function getLangfuseSpanProcessor(): SpanProcessor | undefined {
|
|
93
|
-
|
|
94
|
-
LANGFUSE_BASE_URL: baseUrl,
|
|
95
|
-
LANGFUSE_PUBLIC_KEY: publicKey,
|
|
96
|
-
LANGFUSE_SECRET_KEY: secretKey,
|
|
97
|
-
} = getEnvConfig();
|
|
98
|
-
if (!baseUrl || !publicKey || !secretKey) {
|
|
99
|
-
getLogger().debug(
|
|
100
|
-
undefined,
|
|
101
|
-
"Langfuse environment is incomplete. Skipping Langfuse OpenTelemetry.",
|
|
102
|
-
);
|
|
103
|
-
return undefined;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
getLogger().debug(undefined, "Registering Langfuse OpenTelemetry.");
|
|
107
|
-
return new FilteringSpanProcessor(
|
|
108
|
-
new LangfuseSpanProcessor({
|
|
109
|
-
baseUrl,
|
|
110
|
-
publicKey,
|
|
111
|
-
secretKey,
|
|
112
|
-
}),
|
|
113
|
-
isAiSdkSpan,
|
|
114
|
-
);
|
|
53
|
+
return createLangfuseSpanProcessor(getEnvConfig(), getLogger());
|
|
115
54
|
}
|
|
116
55
|
|
|
117
56
|
const spanProcessors: tracing.SpanProcessor[] = compact([
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import {
|
|
3
|
-
createRequireApplicationAccess,
|
|
4
|
-
createRequirePermission,
|
|
5
|
-
} from "@percepta/access-control/trpc";
|
|
1
|
+
import type { SubjectRef } from "@percepta/access-control";
|
|
2
|
+
import { createAppAccessControlMiddlewares } from "@percepta/access-control/trpc";
|
|
6
3
|
import { TRPCError, initTRPC } from "@trpc/server";
|
|
7
4
|
import superjson from "superjson";
|
|
8
5
|
import { accessManifest } from "../access/access.manifest";
|
|
@@ -25,12 +22,6 @@ export interface ProtectedContext extends Context {
|
|
|
25
22
|
session: BetterAuthSession;
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
const lazyPermissionClient = {
|
|
29
|
-
can(check) {
|
|
30
|
-
return getAccessControl().permissions.can(check);
|
|
31
|
-
},
|
|
32
|
-
} satisfies Pick<PermissionClient, "can">;
|
|
33
|
-
|
|
34
25
|
function getCurrentSubject(ctx: Context): SubjectRef | null {
|
|
35
26
|
const userId = ctx.session?.user.id;
|
|
36
27
|
return userId == null ? null : toUserSubject(userId);
|
|
@@ -76,17 +67,14 @@ const requireAuthenticatedUser = procedure.use(
|
|
|
76
67
|
},
|
|
77
68
|
);
|
|
78
69
|
|
|
79
|
-
const requireApplicationAccess =
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
const { requireApplicationAccess, requirePermission } =
|
|
71
|
+
createAppAccessControlMiddlewares<ProtectedContext>({
|
|
72
|
+
getAccessControl,
|
|
82
73
|
getSubject: getCurrentSubject,
|
|
83
74
|
manifest: accessManifest,
|
|
84
75
|
});
|
|
85
76
|
|
|
86
|
-
export
|
|
87
|
-
accessControl: lazyPermissionClient,
|
|
88
|
-
getSubject: getCurrentSubject,
|
|
89
|
-
});
|
|
77
|
+
export { requirePermission };
|
|
90
78
|
|
|
91
79
|
export const protectedProcedure = requireAuthenticatedUser;
|
|
92
80
|
|
|
@@ -1,63 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createAuthContextServiceFactory,
|
|
3
|
+
type AuthContextService as PerceptaAuthContextService,
|
|
4
|
+
} from "@percepta/next-utils";
|
|
3
5
|
import type { BetterAuthSession } from "../lib/auth";
|
|
4
6
|
|
|
5
7
|
export type AuthSession = BetterAuthSession;
|
|
6
8
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (AuthContextService.SINGLETON == null) {
|
|
11
|
-
AuthContextService.SINGLETON = new AuthContextService();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return AuthContextService.SINGLETON;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
private authAsyncLocalStorage = new AsyncLocalStorage<LocalStorage>();
|
|
18
|
-
|
|
19
|
-
private constructor() {}
|
|
20
|
-
|
|
21
|
-
public withSession<TReturn>(
|
|
22
|
-
session: AuthSession,
|
|
23
|
-
callback: () => TReturn,
|
|
24
|
-
): TReturn {
|
|
25
|
-
// Set user email on the active OpenTelemetry span
|
|
26
|
-
const activeSpan = trace.getActiveSpan();
|
|
27
|
-
if (
|
|
28
|
-
activeSpan != null &&
|
|
29
|
-
session.user.email != null &&
|
|
30
|
-
session.user.id != null
|
|
31
|
-
) {
|
|
32
|
-
activeSpan.setAttribute("user.email", session.user.email);
|
|
33
|
-
activeSpan.setAttribute("user.id", session.user.id);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return this.authAsyncLocalStorage.run<TReturn>({ session }, () =>
|
|
37
|
-
callback(),
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public getSession(): AuthSession | undefined {
|
|
42
|
-
const context = this.authAsyncLocalStorage.getStore();
|
|
43
|
-
if (context == null) {
|
|
44
|
-
return undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const { session } = context;
|
|
48
|
-
return session;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
public getSessionOrThrow(): AuthSession {
|
|
52
|
-
const session = this.getSession();
|
|
53
|
-
if (session == null) {
|
|
54
|
-
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return session;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
interface LocalStorage {
|
|
62
|
-
session: AuthSession;
|
|
63
|
-
}
|
|
9
|
+
export const AuthContextService =
|
|
10
|
+
createAuthContextServiceFactory<AuthSession>();
|
|
11
|
+
export type AuthContextService = PerceptaAuthContextService<AuthSession>;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createPerceptaInngestServiceFactory,
|
|
3
|
+
type PerceptaInngest,
|
|
4
|
+
type PerceptaInngestService,
|
|
5
|
+
} from "@percepta/inngest";
|
|
6
|
+
import { EventSchemas } from "inngest";
|
|
3
7
|
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
4
8
|
import { AppEvents } from "./events/AppEvents";
|
|
5
9
|
|
|
@@ -8,64 +12,12 @@ export interface PollOpts {
|
|
|
8
12
|
delayMs: number;
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
export interface AppInngest extends AppInngestInternal {
|
|
12
|
-
// Externally, we should never call createFunction on the client interface.
|
|
13
|
-
createFunction: never;
|
|
14
|
-
}
|
|
15
|
-
type AppInngestInternal = Inngest<{
|
|
16
|
-
id: string;
|
|
17
|
-
schemas: typeof EVENT_SCHEMAS;
|
|
18
|
-
}>;
|
|
19
|
-
|
|
20
|
-
export class InngestService {
|
|
21
|
-
private static SINGLETON: InngestService | undefined;
|
|
22
|
-
public static create(): InngestService {
|
|
23
|
-
if (InngestService.SINGLETON == null) {
|
|
24
|
-
InngestService.SINGLETON = new InngestService();
|
|
25
|
-
}
|
|
26
|
-
return InngestService.SINGLETON;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private _signingKey: string | undefined;
|
|
30
|
-
private inngest: AppInngestInternal;
|
|
31
|
-
|
|
32
|
-
private constructor() {
|
|
33
|
-
const {
|
|
34
|
-
INNGEST_BASE_URL: baseUrl,
|
|
35
|
-
INNGEST_SIGNING_KEY: signingKey,
|
|
36
|
-
INNGEST_EVENT_KEY: eventKey,
|
|
37
|
-
} = getEnvConfig();
|
|
38
|
-
|
|
39
|
-
if (baseUrl == null) {
|
|
40
|
-
throw new Error("INNGEST_BASE_URL is not set.");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
this._signingKey = signingKey;
|
|
44
|
-
this.inngest = new Inngest({
|
|
45
|
-
id: "__APP_NAME__",
|
|
46
|
-
eventKey,
|
|
47
|
-
baseUrl,
|
|
48
|
-
schemas: EVENT_SCHEMAS,
|
|
49
|
-
middleware: [validationMiddleware()],
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
public get client(): AppInngest {
|
|
54
|
-
return this.inngest as never;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
public get signingKey(): string | undefined {
|
|
58
|
-
return this._signingKey;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
public createFunction: Inngest.CreateFunction<AppInngest> = (
|
|
62
|
-
options,
|
|
63
|
-
trigger,
|
|
64
|
-
handler,
|
|
65
|
-
) => {
|
|
66
|
-
// TODO: Add context plumbing here.
|
|
67
|
-
return this.inngest.createFunction(options, trigger, handler) as never;
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
15
|
const EVENT_SCHEMAS = new EventSchemas().fromSchema(AppEvents);
|
|
16
|
+
|
|
17
|
+
export type AppInngest = PerceptaInngest<typeof EVENT_SCHEMAS>;
|
|
18
|
+
export const InngestService = createPerceptaInngestServiceFactory({
|
|
19
|
+
appId: "__APP_NAME__",
|
|
20
|
+
getEnv: getEnvConfig,
|
|
21
|
+
schemas: EVENT_SCHEMAS,
|
|
22
|
+
});
|
|
23
|
+
export type InngestService = PerceptaInngestService<typeof EVENT_SCHEMAS>;
|
|
@@ -1,80 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createPerceptaLangfuseServiceFactory,
|
|
3
|
+
type PerceptaLangfuseService,
|
|
4
|
+
} from "@percepta/ai";
|
|
2
5
|
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
3
6
|
import { getLogger } from "../logger/AppLogger";
|
|
4
7
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
LangfuseService.SINGLETON = new LangfuseService();
|
|
11
|
-
}
|
|
12
|
-
return LangfuseService.SINGLETON;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
private readonly client: Langfuse | undefined;
|
|
16
|
-
|
|
17
|
-
private constructor() {
|
|
18
|
-
const config = getEnvConfig();
|
|
19
|
-
const baseUrl = config.LANGFUSE_BASE_URL;
|
|
20
|
-
const publicKey = config.LANGFUSE_PUBLIC_KEY;
|
|
21
|
-
const secretKey = config.LANGFUSE_SECRET_KEY;
|
|
22
|
-
|
|
23
|
-
if (baseUrl && publicKey && secretKey) {
|
|
24
|
-
this.client = new Langfuse({
|
|
25
|
-
baseUrl,
|
|
26
|
-
publicKey,
|
|
27
|
-
secretKey,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Check if Langfuse is configured.
|
|
34
|
-
*/
|
|
35
|
-
public isConfigured(): boolean {
|
|
36
|
-
return this.client != null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Update a Langfuse trace with metadata.
|
|
41
|
-
* Uses trace.update() which merges with existing metadata.
|
|
42
|
-
*
|
|
43
|
-
* @param traceId - The trace ID (same as documentPipelineRuns.id)
|
|
44
|
-
* @param metadata - The metadata to save (must be JSON-serializable)
|
|
45
|
-
*/
|
|
46
|
-
public async updateTraceMetadata<T extends object>(
|
|
47
|
-
traceId: string,
|
|
48
|
-
metadata: T,
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
if (!this.isConfigured()) {
|
|
51
|
-
getLogger().warn(
|
|
52
|
-
{ safe: { traceId } },
|
|
53
|
-
"[LangfuseService] Langfuse not configured, skipping trace update",
|
|
54
|
-
);
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Capture client after isConfigured() guard for type narrowing
|
|
59
|
-
const client = this.client!;
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
// Attach to existing trace by ID and update metadata
|
|
63
|
-
const trace = client.trace({ id: traceId });
|
|
64
|
-
trace.update({ metadata });
|
|
65
|
-
|
|
66
|
-
// Flush to ensure the update is sent
|
|
67
|
-
await client.flushAsync();
|
|
68
|
-
|
|
69
|
-
getLogger().info(
|
|
70
|
-
{ safe: { traceId } },
|
|
71
|
-
"[LangfuseService] Updated trace with metadata",
|
|
72
|
-
);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
getLogger().error(
|
|
75
|
-
{ safe: { traceId }, unsafe: { error: String(error) } },
|
|
76
|
-
"[LangfuseService] Failed to update trace metadata",
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
8
|
+
export const LangfuseService = createPerceptaLangfuseServiceFactory({
|
|
9
|
+
getConfig: getEnvConfig,
|
|
10
|
+
getLogger,
|
|
11
|
+
});
|
|
12
|
+
export type LangfuseService = PerceptaLangfuseService;
|