@percepta/create 3.1.0 → 3.1.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/README.md +15 -10
- package/dist/{chunk-7NPWSTCY.js → chunk-CO3YWUD6.js} +31 -2
- package/dist/{chunk-WMJT7CB5.js → chunk-V5EJIUBJ.js} +5 -2
- package/dist/index.js +93 -73
- package/dist/{init-NP6GRXLL.js → init-EQZ2TCSJ.js} +2 -2
- package/dist/{status-BTHGN6QH.js → status-QW5TQDYY.js} +1 -1
- package/dist/{sync-3Q27L7XZ.js → sync-RLBZDOFB.js} +1 -1
- package/dist/{upstream-C5KFAHVR.js → upstream-TQFVPMEG.js} +1 -1
- package/package.json +3 -2
- package/templates/monorepo/.dockerignore +18 -0
- package/templates/monorepo/gitignore.template +1 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +6 -2
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform-ryvn-release.yaml +98 -0
- package/templates/webapp/AGENTS.md +17 -5
- package/templates/webapp/Dockerfile +16 -7
- package/templates/webapp/README.md +64 -2
- package/templates/webapp/agent-skills/deploy.md +50 -51
- package/templates/webapp/agent-skills/inngest.md +4 -4
- package/templates/webapp/agent-skills/langfuse.md +15 -14
- package/templates/webapp/agent-skills/llm.md +59 -0
- package/templates/webapp/agent-skills/oneshot.md +14 -1
- package/templates/webapp/agent-skills/ryvn.md +1 -1
- package/templates/webapp/deploy/README.md +41 -16
- package/templates/webapp/deploy/ryvn/__APP_NAME__-terraform.service.yaml +10 -0
- package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +2 -2
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__-terraform.env.percepta-test.serviceinstallation.yaml +11 -0
- package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +60 -11
- package/templates/webapp/env.example.template +20 -2
- package/templates/webapp/eslint.config.mjs +7 -0
- package/templates/webapp/gitignore.template +1 -0
- package/templates/webapp/next.config.ts +9 -0
- package/templates/webapp/package.json.template +6 -2
- package/templates/webapp/scripts/deploy-percepta-test.ts +837 -0
- package/templates/webapp/scripts/migrate.ts +3 -0
- package/templates/webapp/scripts/open-ryvn-deploy-pr.ts +152 -32
- package/templates/webapp/scripts/seed.ts +1 -1
- package/templates/webapp/scripts/setup-database.ts +2 -1
- package/templates/webapp/scripts/start.sh +3 -2
- package/templates/webapp/scripts/with-local-env.ts +75 -0
- package/templates/webapp/src/app/(app)/layout.tsx +1 -5
- package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +11 -1
- package/templates/webapp/src/app/(auth)/auth/signup/CredentialsSignUpForm.tsx +113 -0
- package/templates/webapp/src/app/(auth)/auth/signup/page.tsx +30 -0
- package/templates/webapp/src/app/global-error.tsx +1 -1
- package/templates/webapp/src/components/FaroProvider.tsx +2 -4
- package/templates/webapp/src/components/form/FormItem.tsx +2 -2
- package/templates/webapp/src/config/getEnvConfig.ts +14 -0
- package/templates/webapp/src/drizzle/db.ts +2 -1
- package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +3 -3
- package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +7 -19
- package/templates/webapp/src/drizzle/ssl.ts +5 -0
- package/templates/webapp/src/instrumentation.ts +102 -10
- package/templates/webapp/src/lib/auth/index.ts +1 -1
- package/templates/webapp/src/lib/auth-client.ts +1 -1
- package/templates/webapp/src/services/llm/LLMService.ts +88 -0
- package/templates/webapp/src/services/llm/LlmProviderService.ts +85 -0
- package/templates/webapp/src/services/observability/initFaro.ts +1 -1
- package/templates/webapp/terraform/schema/main.tf +4 -0
- package/templates/webapp/terraform/schema/outputs.tf +9 -0
- package/templates/webapp/terraform/schema/variables.tf +19 -0
- package/templates/webapp/terraform/schema/versions.tf +38 -0
- package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +0 -28
|
@@ -34,7 +34,7 @@ export const FormItem: React.FC<FormItemProps> = ({
|
|
|
34
34
|
return (
|
|
35
35
|
<p
|
|
36
36
|
id={messageId}
|
|
37
|
-
className="text-destructive-foreground
|
|
37
|
+
className="text-sm text-destructive-foreground"
|
|
38
38
|
data-slot="form-message"
|
|
39
39
|
>
|
|
40
40
|
{body}
|
|
@@ -70,7 +70,7 @@ export const FormItem: React.FC<FormItemProps> = ({
|
|
|
70
70
|
{description != null && (
|
|
71
71
|
<p
|
|
72
72
|
id={descriptionId}
|
|
73
|
-
className="text-muted-foreground
|
|
73
|
+
className="text-sm text-muted-foreground"
|
|
74
74
|
data-slot="form-description"
|
|
75
75
|
>
|
|
76
76
|
{description}
|
|
@@ -44,12 +44,26 @@ export const { getEnvConfig, schema: ENV_CONFIG_SCHEMA } = createEnvConfig(
|
|
|
44
44
|
LANGFUSE_PUBLIC_KEY: z.string().optional(),
|
|
45
45
|
LANGFUSE_SECRET_KEY: z.string().optional(),
|
|
46
46
|
|
|
47
|
+
// OpenTelemetry:
|
|
48
|
+
OTEL_SERVICE_NAME: z.string().optional(),
|
|
49
|
+
OTEL_RESOURCE_ATTRIBUTES: z.string().optional(),
|
|
50
|
+
OTEL_TRACES_EXPORTER: z.string().optional(),
|
|
51
|
+
OTEL_METRICS_EXPORTER: z.string().optional(),
|
|
52
|
+
OTEL_EXPORTER_OTLP_PROTOCOL: z.string().optional(),
|
|
53
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: z.string().optional(),
|
|
54
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: z.string().optional(),
|
|
55
|
+
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: z.string().optional(),
|
|
56
|
+
OTEL_METRIC_EXPORT_INTERVAL: z.string().optional(),
|
|
57
|
+
|
|
47
58
|
// Security:
|
|
48
59
|
ENCRYPTION_SECRET_KEY: z.string().optional(),
|
|
49
60
|
TRIGGER_ENDPOINT_API_KEY: z.string().optional(),
|
|
50
61
|
|
|
51
62
|
// LLM/AI providers:
|
|
63
|
+
ANTHROPIC_API_KEY: z.string().optional(),
|
|
52
64
|
OPENAI_API_KEY: z.string().optional(),
|
|
65
|
+
LLM_PROVIDER: z.enum(["anthropic", "openai"]).optional(),
|
|
66
|
+
LLM_MODEL: z.string().optional(),
|
|
53
67
|
|
|
54
68
|
// Readonly database (scripts):
|
|
55
69
|
READONLY_SECRET_NAME: z.string().optional(),
|
|
@@ -3,6 +3,7 @@ import { Pool } from "pg";
|
|
|
3
3
|
import { getEnvConfig } from "../config/getEnvConfig";
|
|
4
4
|
import * as schema from "./schema";
|
|
5
5
|
import { getPgSearchPathOption } from "./searchPath";
|
|
6
|
+
import { getPgSslConfig } from "./ssl";
|
|
6
7
|
|
|
7
8
|
export const { client, db } = createDb();
|
|
8
9
|
|
|
@@ -23,7 +24,7 @@ function createDb(): { client: Pool; db: NodePgDatabase<typeof schema> } {
|
|
|
23
24
|
user,
|
|
24
25
|
password,
|
|
25
26
|
database,
|
|
26
|
-
ssl: useSSL,
|
|
27
|
+
ssl: getPgSslConfig(useSSL),
|
|
27
28
|
options: getPgSearchPathOption(databaseSchema),
|
|
28
29
|
});
|
|
29
30
|
|
|
@@ -52,6 +52,6 @@ CREATE TABLE "verification" (
|
|
|
52
52
|
"updated_at" timestamp
|
|
53
53
|
);
|
|
54
54
|
--> statement-breakpoint
|
|
55
|
-
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "
|
|
56
|
-
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "
|
|
57
|
-
CREATE UNIQUE INDEX "lower_email_index" ON "users" USING btree (lower("email"));
|
|
55
|
+
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
56
|
+
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
57
|
+
CREATE UNIQUE INDEX "lower_email_index" ON "users" USING btree (lower("email"));
|
|
@@ -99,12 +99,8 @@
|
|
|
99
99
|
"name": "account_user_id_users_id_fk",
|
|
100
100
|
"tableFrom": "account",
|
|
101
101
|
"tableTo": "users",
|
|
102
|
-
"columnsFrom": [
|
|
103
|
-
|
|
104
|
-
],
|
|
105
|
-
"columnsTo": [
|
|
106
|
-
"id"
|
|
107
|
-
],
|
|
102
|
+
"columnsFrom": ["user_id"],
|
|
103
|
+
"columnsTo": ["id"],
|
|
108
104
|
"onDelete": "cascade",
|
|
109
105
|
"onUpdate": "no action"
|
|
110
106
|
}
|
|
@@ -180,12 +176,8 @@
|
|
|
180
176
|
"name": "session_user_id_users_id_fk",
|
|
181
177
|
"tableFrom": "session",
|
|
182
178
|
"tableTo": "users",
|
|
183
|
-
"columnsFrom": [
|
|
184
|
-
|
|
185
|
-
],
|
|
186
|
-
"columnsTo": [
|
|
187
|
-
"id"
|
|
188
|
-
],
|
|
179
|
+
"columnsFrom": ["user_id"],
|
|
180
|
+
"columnsTo": ["id"],
|
|
189
181
|
"onDelete": "cascade",
|
|
190
182
|
"onUpdate": "no action"
|
|
191
183
|
}
|
|
@@ -195,9 +187,7 @@
|
|
|
195
187
|
"session_token_unique": {
|
|
196
188
|
"name": "session_token_unique",
|
|
197
189
|
"nullsNotDistinct": false,
|
|
198
|
-
"columns": [
|
|
199
|
-
"token"
|
|
200
|
-
]
|
|
190
|
+
"columns": ["token"]
|
|
201
191
|
}
|
|
202
192
|
},
|
|
203
193
|
"policies": {},
|
|
@@ -303,9 +293,7 @@
|
|
|
303
293
|
"users_email_unique": {
|
|
304
294
|
"name": "users_email_unique",
|
|
305
295
|
"nullsNotDistinct": false,
|
|
306
|
-
"columns": [
|
|
307
|
-
"email"
|
|
308
|
-
]
|
|
296
|
+
"columns": ["email"]
|
|
309
297
|
}
|
|
310
298
|
},
|
|
311
299
|
"policies": {},
|
|
@@ -373,4 +361,4 @@
|
|
|
373
361
|
"schemas": {},
|
|
374
362
|
"tables": {}
|
|
375
363
|
}
|
|
376
|
-
}
|
|
364
|
+
}
|
|
@@ -1,34 +1,126 @@
|
|
|
1
1
|
import { LangfuseSpanProcessor } from "@langfuse/otel";
|
|
2
2
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
3
|
-
import {
|
|
3
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
4
|
+
import { NodeSDK, tracing } from "@opentelemetry/sdk-node";
|
|
4
5
|
import { compact } from "lodash-es";
|
|
5
6
|
import { getEnvConfig } from "./config/getEnvConfig";
|
|
6
7
|
import { getLogger } from "./services/logger/AppLogger";
|
|
7
8
|
|
|
8
|
-
|
|
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
|
+
|
|
51
|
+
function getOtlpTracesEndpoint(): string | undefined {
|
|
52
|
+
const {
|
|
53
|
+
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: tracesEndpoint,
|
|
54
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: baseEndpoint,
|
|
55
|
+
} = getEnvConfig();
|
|
56
|
+
|
|
57
|
+
if (tracesEndpoint) return tracesEndpoint;
|
|
58
|
+
|
|
59
|
+
if (!baseEndpoint) return undefined;
|
|
60
|
+
|
|
61
|
+
return `${baseEndpoint.replace(/\/$/, "")}/v1/traces`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getOtlpSpanProcessor(): tracing.BatchSpanProcessor | undefined {
|
|
65
|
+
const { OTEL_TRACES_EXPORTER: tracesExporter } = getEnvConfig();
|
|
66
|
+
if (tracesExporter === "none") {
|
|
67
|
+
getLogger().debug(
|
|
68
|
+
undefined,
|
|
69
|
+
"OTEL_TRACES_EXPORTER=none. Skipping OTLP trace export.",
|
|
70
|
+
);
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const tracesEndpoint = getOtlpTracesEndpoint();
|
|
75
|
+
if (!tracesEndpoint) {
|
|
76
|
+
getLogger().debug(
|
|
77
|
+
undefined,
|
|
78
|
+
"No OTLP trace endpoint found. Skipping OTLP trace export.",
|
|
79
|
+
);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getLogger().debug(
|
|
84
|
+
{ safe: { tracesEndpoint } },
|
|
85
|
+
"Registering OTLP trace exporter.",
|
|
86
|
+
);
|
|
87
|
+
return new tracing.BatchSpanProcessor(
|
|
88
|
+
new OTLPTraceExporter({ url: tracesEndpoint }),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getLangfuseSpanProcessor(): SpanProcessor | undefined {
|
|
9
93
|
const {
|
|
10
94
|
LANGFUSE_BASE_URL: baseUrl,
|
|
11
95
|
LANGFUSE_PUBLIC_KEY: publicKey,
|
|
12
96
|
LANGFUSE_SECRET_KEY: secretKey,
|
|
13
97
|
} = getEnvConfig();
|
|
14
|
-
if (baseUrl
|
|
98
|
+
if (!baseUrl || !publicKey || !secretKey) {
|
|
15
99
|
getLogger().debug(
|
|
16
100
|
undefined,
|
|
17
|
-
"
|
|
101
|
+
"Langfuse environment is incomplete. Skipping Langfuse OpenTelemetry.",
|
|
18
102
|
);
|
|
19
103
|
return undefined;
|
|
20
104
|
}
|
|
21
105
|
|
|
22
106
|
getLogger().debug(undefined, "Registering Langfuse OpenTelemetry.");
|
|
23
|
-
return new
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
107
|
+
return new FilteringSpanProcessor(
|
|
108
|
+
new LangfuseSpanProcessor({
|
|
109
|
+
baseUrl,
|
|
110
|
+
publicKey,
|
|
111
|
+
secretKey,
|
|
112
|
+
}),
|
|
113
|
+
isAiSdkSpan,
|
|
114
|
+
);
|
|
28
115
|
}
|
|
29
116
|
|
|
117
|
+
const spanProcessors: tracing.SpanProcessor[] = compact([
|
|
118
|
+
getOtlpSpanProcessor(),
|
|
119
|
+
getLangfuseSpanProcessor(),
|
|
120
|
+
]);
|
|
121
|
+
|
|
30
122
|
const sdk = new NodeSDK({
|
|
31
|
-
spanProcessors
|
|
123
|
+
spanProcessors,
|
|
32
124
|
instrumentations: [getNodeAutoInstrumentations()],
|
|
33
125
|
});
|
|
34
126
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { betterAuth } from "better-auth";
|
|
2
2
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
3
|
import { admin } from "better-auth/plugins";
|
|
4
|
+
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
4
5
|
import { db } from "../../drizzle/db";
|
|
5
6
|
import { accounts } from "../../drizzle/schema/auth/accounts";
|
|
6
7
|
import { sessions } from "../../drizzle/schema/auth/sessions";
|
|
7
8
|
import { users } from "../../drizzle/schema/auth/users";
|
|
8
9
|
import { verifications } from "../../drizzle/schema/auth/verifications";
|
|
9
|
-
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
10
10
|
import { getLogger } from "../../services/logger/AppLogger";
|
|
11
11
|
|
|
12
12
|
// eslint-disable-next-line n/no-process-env -- detecting Next.js build phase
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { generateText, streamText } from "ai";
|
|
2
|
+
import { type LlmProviderName, LlmProviderService } from "./LlmProviderService";
|
|
3
|
+
|
|
4
|
+
type GenerateTextOptions = Omit<Parameters<typeof generateText>[0], "model"> & {
|
|
5
|
+
modelId?: string;
|
|
6
|
+
provider?: LlmProviderName;
|
|
7
|
+
telemetryFunctionId?: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type StreamTextOptions = Omit<Parameters<typeof streamText>[0], "model"> & {
|
|
11
|
+
modelId?: string;
|
|
12
|
+
provider?: LlmProviderName;
|
|
13
|
+
telemetryFunctionId?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class LLMService {
|
|
17
|
+
private static SINGLETON: LLMService | undefined;
|
|
18
|
+
|
|
19
|
+
public static create(): LLMService {
|
|
20
|
+
if (LLMService.SINGLETON == null) {
|
|
21
|
+
LLMService.SINGLETON = new LLMService(LlmProviderService.create());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return LLMService.SINGLETON;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private constructor(private llmProviderService: LlmProviderService) {}
|
|
28
|
+
|
|
29
|
+
public generateText(
|
|
30
|
+
options: GenerateTextOptions,
|
|
31
|
+
): ReturnType<typeof generateText> {
|
|
32
|
+
const { modelId, provider, telemetryFunctionId, ...generateOptions } =
|
|
33
|
+
options;
|
|
34
|
+
const selection = this.llmProviderService.getLanguageModel({
|
|
35
|
+
modelId,
|
|
36
|
+
provider,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const aiOptions = {
|
|
40
|
+
...generateOptions,
|
|
41
|
+
model: selection.model,
|
|
42
|
+
experimental_telemetry: {
|
|
43
|
+
...generateOptions.experimental_telemetry,
|
|
44
|
+
isEnabled: generateOptions.experimental_telemetry?.isEnabled ?? true,
|
|
45
|
+
functionId:
|
|
46
|
+
telemetryFunctionId ??
|
|
47
|
+
generateOptions.experimental_telemetry?.functionId ??
|
|
48
|
+
"llm.generateText",
|
|
49
|
+
metadata: {
|
|
50
|
+
...generateOptions.experimental_telemetry?.metadata,
|
|
51
|
+
llmProvider: selection.provider,
|
|
52
|
+
llmModel: selection.modelId,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
} as Parameters<typeof generateText>[0];
|
|
56
|
+
|
|
57
|
+
return generateText(aiOptions);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public streamText(options: StreamTextOptions): ReturnType<typeof streamText> {
|
|
61
|
+
const { modelId, provider, telemetryFunctionId, ...streamOptions } =
|
|
62
|
+
options;
|
|
63
|
+
const selection = this.llmProviderService.getLanguageModel({
|
|
64
|
+
modelId,
|
|
65
|
+
provider,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const aiOptions = {
|
|
69
|
+
...streamOptions,
|
|
70
|
+
model: selection.model,
|
|
71
|
+
experimental_telemetry: {
|
|
72
|
+
...streamOptions.experimental_telemetry,
|
|
73
|
+
isEnabled: streamOptions.experimental_telemetry?.isEnabled ?? true,
|
|
74
|
+
functionId:
|
|
75
|
+
telemetryFunctionId ??
|
|
76
|
+
streamOptions.experimental_telemetry?.functionId ??
|
|
77
|
+
"llm.streamText",
|
|
78
|
+
metadata: {
|
|
79
|
+
...streamOptions.experimental_telemetry?.metadata,
|
|
80
|
+
llmProvider: selection.provider,
|
|
81
|
+
llmModel: selection.modelId,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
} as Parameters<typeof streamText>[0];
|
|
85
|
+
|
|
86
|
+
return streamText(aiOptions);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
3
|
+
import { type LanguageModel } from "ai";
|
|
4
|
+
import { getEnvConfig } from "../../config/getEnvConfig";
|
|
5
|
+
|
|
6
|
+
export type LlmProviderName = "anthropic" | "openai";
|
|
7
|
+
|
|
8
|
+
export interface LanguageModelSelection {
|
|
9
|
+
model: LanguageModel;
|
|
10
|
+
modelId: string;
|
|
11
|
+
provider: LlmProviderName;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface GetLanguageModelOptions {
|
|
15
|
+
modelId?: string;
|
|
16
|
+
provider?: LlmProviderName;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5-20250929";
|
|
20
|
+
const DEFAULT_OPENAI_MODEL = "gpt-4.1";
|
|
21
|
+
|
|
22
|
+
export class LlmProviderService {
|
|
23
|
+
private static SINGLETON: LlmProviderService | undefined;
|
|
24
|
+
|
|
25
|
+
public static create(): LlmProviderService {
|
|
26
|
+
if (LlmProviderService.SINGLETON == null) {
|
|
27
|
+
LlmProviderService.SINGLETON = new LlmProviderService();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return LlmProviderService.SINGLETON;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private constructor() {}
|
|
34
|
+
|
|
35
|
+
public getLanguageModel(
|
|
36
|
+
options: GetLanguageModelOptions = {},
|
|
37
|
+
): LanguageModelSelection {
|
|
38
|
+
const config = getEnvConfig();
|
|
39
|
+
const provider =
|
|
40
|
+
options.provider ??
|
|
41
|
+
config.LLM_PROVIDER ??
|
|
42
|
+
(config.ANTHROPIC_API_KEY ? "anthropic" : undefined) ??
|
|
43
|
+
(config.OPENAI_API_KEY ? "openai" : undefined);
|
|
44
|
+
|
|
45
|
+
if (provider === "anthropic") {
|
|
46
|
+
if (!config.ANTHROPIC_API_KEY) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"LLM_PROVIDER=anthropic but ANTHROPIC_API_KEY is not configured.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const modelId =
|
|
53
|
+
options.modelId ?? config.LLM_MODEL ?? DEFAULT_ANTHROPIC_MODEL;
|
|
54
|
+
return {
|
|
55
|
+
provider,
|
|
56
|
+
modelId,
|
|
57
|
+
model: createAnthropic({
|
|
58
|
+
apiKey: config.ANTHROPIC_API_KEY,
|
|
59
|
+
}).languageModel(modelId),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (provider === "openai") {
|
|
64
|
+
if (!config.OPENAI_API_KEY) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"LLM_PROVIDER=openai but OPENAI_API_KEY is not configured.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const modelId =
|
|
71
|
+
options.modelId ?? config.LLM_MODEL ?? DEFAULT_OPENAI_MODEL;
|
|
72
|
+
return {
|
|
73
|
+
provider,
|
|
74
|
+
modelId,
|
|
75
|
+
model: createOpenAI({
|
|
76
|
+
apiKey: config.OPENAI_API_KEY,
|
|
77
|
+
}).languageModel(modelId),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
throw new Error(
|
|
82
|
+
"No LLM provider is configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY locally, or deploy to an environment with a shared LLM variable group.",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { createFaroInstance } from "@percepta/next-utils/faro";
|
|
4
3
|
import { TracingInstrumentation } from "@grafana/faro-web-tracing";
|
|
4
|
+
import { createFaroInstance } from "@percepta/next-utils/faro";
|
|
5
5
|
import { getClientEnvConfig } from "../../config/clientEnvConfig";
|
|
6
6
|
|
|
7
7
|
const {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
variable "aws_region" {
|
|
2
|
+
description = "AWS region containing the shared Percepta internal database secret."
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "database_secret_name" {
|
|
7
|
+
description = "AWS Secrets Manager secret name containing shared Postgres credentials."
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "database_name" {
|
|
12
|
+
description = "Database where the demo app schema should be created."
|
|
13
|
+
type = string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
variable "schema_name" {
|
|
17
|
+
description = "Postgres schema name for this demo app."
|
|
18
|
+
type = string
|
|
19
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_version = ">= 1.5.0"
|
|
3
|
+
|
|
4
|
+
required_providers {
|
|
5
|
+
aws = {
|
|
6
|
+
source = "hashicorp/aws"
|
|
7
|
+
version = "~> 5.0"
|
|
8
|
+
}
|
|
9
|
+
postgresql = {
|
|
10
|
+
source = "cyrilgdn/postgresql"
|
|
11
|
+
version = "~> 1.22"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
backend "kubernetes" {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
provider "aws" {
|
|
19
|
+
region = var.aws_region
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
data "aws_secretsmanager_secret_version" "database" {
|
|
23
|
+
secret_id = var.database_secret_name
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
locals {
|
|
27
|
+
database_credentials = jsondecode(data.aws_secretsmanager_secret_version.database.secret_string)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
provider "postgresql" {
|
|
31
|
+
host = local.database_credentials.host
|
|
32
|
+
port = tonumber(local.database_credentials.port)
|
|
33
|
+
username = local.database_credentials.username
|
|
34
|
+
password = local.database_credentials.password
|
|
35
|
+
sslmode = "require"
|
|
36
|
+
connect_timeout = 15
|
|
37
|
+
superuser = false
|
|
38
|
+
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
name: Terraform Validate (__APP_NAME__)
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
paths:
|
|
6
|
-
- "packages/__APP_NAME__/terraform/**"
|
|
7
|
-
- ".github/workflows/__APP_NAME__-terraform.yml"
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
terraform-validate:
|
|
11
|
-
name: Validate Terraform
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout repository
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Setup Terraform
|
|
19
|
-
uses: hashicorp/setup-terraform@v3
|
|
20
|
-
|
|
21
|
-
- name: Terraform Init
|
|
22
|
-
run: terraform -chdir=packages/__APP_NAME__/terraform init -backend=false
|
|
23
|
-
|
|
24
|
-
- name: Terraform Format Check
|
|
25
|
-
run: terraform fmt -check -recursive packages/__APP_NAME__/terraform/
|
|
26
|
-
|
|
27
|
-
- name: Terraform Validate
|
|
28
|
-
run: terraform -chdir=packages/__APP_NAME__/terraform validate
|