Colab User commited on
Commit
eab050b
·
1 Parent(s): 51ffef2

Initial commit: Deploy rasa-engine API server

Browse files
.dockerignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ build
3
+ .env
4
+ .git
5
+ .vscode
6
+ *.log
7
+ *.tgz
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Node.js runtime as a parent image
2
+ FROM node:18-alpine
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Install pnpm
8
+ RUN npm install -g pnpm
9
+
10
+ # Copy package.json and pnpm-workspace.yaml to leverage Docker cache
11
+ COPY package.json pnpm-workspace.yaml ./
12
+ COPY artifacts/api-server/package.json artifacts/api-server/
13
+ COPY lib/db/package.json lib/db/
14
+
15
+ # Install dependencies
16
+ RUN pnpm install --frozen-lockfile
17
+
18
+ # Copy the rest of the application code
19
+ COPY . .
20
+
21
+ # Build the api-server project
22
+ RUN pnpm run build --filter=@workspace/api-server
23
+
24
+ # Expose the port the app runs on
25
+ EXPOSE 3000
26
+
27
+ # Define the command to run the app
28
+ CMD ["node", "./artifacts/api-server/build/index.mjs"]
artifacts/api-server/build.mjs ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { build as esbuild } from "esbuild";
5
+ import esbuildPluginPino from "esbuild-plugin-pino";
6
+ import { rm } from "node:fs/promises";
7
+
8
+ // Plugins (e.g. 'esbuild-plugin-pino') may use `require` to resolve dependencies
9
+ globalThis.require = createRequire(import.meta.url);
10
+
11
+ const artifactDir = path.dirname(fileURLToPath(import.meta.url));
12
+
13
+ async function buildAll() {
14
+ const distDir = path.resolve(artifactDir, "dist");
15
+ await rm(distDir, { recursive: true, force: true });
16
+
17
+ await esbuild({
18
+ entryPoints: [path.resolve(artifactDir, "src/index.ts")],
19
+ platform: "node",
20
+ bundle: true,
21
+ format: "esm",
22
+ outdir: distDir,
23
+ outExtension: { ".js": ".mjs" },
24
+ logLevel: "info",
25
+ // Some packages may not be bundleable, so we externalize them, we can add more here as needed.
26
+ // Some of the packages below may not be imported or installed, but we're adding them in case they are in the future.
27
+ // Examples of unbundleable packages:
28
+ // - uses native modules and loads them dynamically (e.g. sharp)
29
+ // - use path traversal to read files (e.g. @google-cloud/secret-manager loads sibling .proto files)
30
+ external: [
31
+ "*.node",
32
+ "sharp",
33
+ "better-sqlite3",
34
+ "sqlite3",
35
+ "canvas",
36
+ "bcrypt",
37
+ "argon2",
38
+ "fsevents",
39
+ "re2",
40
+ "farmhash",
41
+ "xxhash-addon",
42
+ "bufferutil",
43
+ "utf-8-validate",
44
+ "ssh2",
45
+ "cpu-features",
46
+ "dtrace-provider",
47
+ "isolated-vm",
48
+ "lightningcss",
49
+ "pg-native",
50
+ "oracledb",
51
+ "mongodb-client-encryption",
52
+ "nodemailer",
53
+ "handlebars",
54
+ "knex",
55
+ "typeorm",
56
+ "protobufjs",
57
+ "onnxruntime-node",
58
+ "@tensorflow/*",
59
+ "@prisma/client",
60
+ "@mikro-orm/*",
61
+ "@grpc/*",
62
+ "@swc/*",
63
+ "@aws-sdk/*",
64
+ "@azure/*",
65
+ "@opentelemetry/*",
66
+ "@google-cloud/*",
67
+ "googleapis",
68
+ "firebase-admin",
69
+ "@parcel/watcher",
70
+ "@sentry/profiling-node",
71
+ "@tree-sitter/*",
72
+ "aws-sdk",
73
+ "classic-level",
74
+ "dd-trace",
75
+ "ffi-napi",
76
+ "grpc",
77
+ "hiredis",
78
+ "kerberos",
79
+ "leveldown",
80
+ "miniflare",
81
+ "mysql2",
82
+ "newrelic",
83
+ "odbc",
84
+ "piscina",
85
+ "realm",
86
+ "ref-napi",
87
+ "rocksdb",
88
+ "sass-embedded",
89
+ "sequelize",
90
+ "serialport",
91
+ "snappy",
92
+ "tinypool",
93
+ "usb",
94
+ "workerd",
95
+ "wrangler",
96
+ "zeromq",
97
+ "zeromq-prebuilt",
98
+ "playwright",
99
+ "puppeteer",
100
+ "puppeteer-core",
101
+ "electron",
102
+ ],
103
+ sourcemap: "linked",
104
+ plugins: [
105
+ // pino relies on workers to handle logging, instead of externalizing it we use a plugin to handle it
106
+ esbuildPluginPino({ transports: ["pino-pretty"] })
107
+ ],
108
+ // Make sure packages that are cjs only (e.g. express) but are bundled continue to work in our esm output file
109
+ banner: {
110
+ js: `import { createRequire as __bannerCrReq } from 'node:module';
111
+ import __bannerPath from 'node:path';
112
+ import __bannerUrl from 'node:url';
113
+
114
+ globalThis.require = __bannerCrReq(import.meta.url);
115
+ globalThis.__filename = __bannerUrl.fileURLToPath(import.meta.url);
116
+ globalThis.__dirname = __bannerPath.dirname(globalThis.__filename);
117
+ `,
118
+ },
119
+ });
120
+ }
121
+
122
+ buildAll().catch((err) => {
123
+ console.error(err);
124
+ process.exit(1);
125
+ });
artifacts/api-server/package.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@workspace/api-server",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "export NODE_ENV=development && pnpm run build && pnpm run start",
8
+ "build": "node ./build.mjs",
9
+ "start": "node --enable-source-maps ./dist/index.mjs",
10
+ "typecheck": "tsc -p tsconfig.json --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@workspace/api-zod": "workspace:*",
14
+ "@workspace/db": "workspace:*",
15
+ "@workspace/integrations-gemini-ai": "workspace:*",
16
+ "cookie-parser": "^1.4.7",
17
+ "cors": "^2",
18
+ "drizzle-orm": "catalog:",
19
+ "express": "^5",
20
+ "pino": "^9",
21
+ "pino-http": "^10"
22
+ },
23
+ "devDependencies": {
24
+ "@types/cookie-parser": "^1.4.10",
25
+ "@types/cors": "^2.8.19",
26
+ "@types/express": "^5.0.6",
27
+ "@types/node": "catalog:",
28
+ "esbuild": "^0.27.3",
29
+ "esbuild-plugin-pino": "^2.3.3",
30
+ "pino-pretty": "^13",
31
+ "thread-stream": "3.1.0"
32
+ }
33
+ }
artifacts/api-server/src/app.ts ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express, { type Express } from "express";
2
+ import cors from "cors";
3
+ import pinoHttp from "pino-http";
4
+ import router from "./routes";
5
+ import { logger } from "./lib/logger";
6
+
7
+ const app: Express = express();
8
+
9
+ app.use(
10
+ pinoHttp({
11
+ logger,
12
+ serializers: {
13
+ req(req) {
14
+ return {
15
+ id: req.id,
16
+ method: req.method,
17
+ url: req.url?.split("?")[0],
18
+ };
19
+ },
20
+ res(res) {
21
+ return {
22
+ statusCode: res.statusCode,
23
+ };
24
+ },
25
+ },
26
+ }),
27
+ );
28
+ app.use(cors());
29
+ app.use(express.json());
30
+ app.use(express.urlencoded({ extended: true }));
31
+
32
+ app.use("/api", router);
33
+
34
+ export default app;
artifacts/api-server/src/index.ts ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import app from "./app";
2
+ import { logger } from "./lib/logger";
3
+
4
+ const rawPort = process.env["PORT"];
5
+
6
+ if (!rawPort) {
7
+ throw new Error(
8
+ "PORT environment variable is required but was not provided.",
9
+ );
10
+ }
11
+
12
+ const port = Number(rawPort);
13
+
14
+ if (Number.isNaN(port) || port <= 0) {
15
+ throw new Error(`Invalid PORT value: "${rawPort}"`);
16
+ }
17
+
18
+ app.listen(port, (err) => {
19
+ if (err) {
20
+ logger.error({ err }, "Error listening on port");
21
+ process.exit(1);
22
+ }
23
+
24
+ logger.info({ port }, "Server listening");
25
+ });
artifacts/api-server/src/lib/logger.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pino from "pino";
2
+
3
+ const isProduction = process.env.NODE_ENV === "production";
4
+
5
+ export const logger = pino({
6
+ level: process.env.LOG_LEVEL ?? "info",
7
+ redact: [
8
+ "req.headers.authorization",
9
+ "req.headers.cookie",
10
+ "res.headers['set-cookie']",
11
+ ],
12
+ ...(isProduction
13
+ ? {}
14
+ : {
15
+ transport: {
16
+ target: "pino-pretty",
17
+ options: { colorize: true },
18
+ },
19
+ }),
20
+ });
artifacts/api-server/src/routes/health.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router, type IRouter } from "express";
2
+ import { HealthCheckResponse } from "@workspace/api-zod";
3
+
4
+ const router: IRouter = Router();
5
+
6
+ router.get("/healthz", (_req, res) => {
7
+ const data = HealthCheckResponse.parse({ status: "ok" });
8
+ res.json(data);
9
+ });
10
+
11
+ export default router;
artifacts/api-server/src/routes/index.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router, type IRouter } from "express";
2
+ import healthRouter from "./health";
3
+ import rasaRouter from "./rasa";
4
+
5
+ const router: IRouter = Router();
6
+
7
+ router.use(healthRouter);
8
+ router.use("/rasa", rasaRouter);
9
+
10
+ export default router;
artifacts/api-server/src/routes/rasa/index.ts ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Router } from "express";
2
+ import { db } from "@workspace/db";
3
+ import { rasaAnalysesTable } from "@workspace/db";
4
+ import { AnalyzeRasaBody, DeleteRasaHistoryParams, GetRasaHistoryQueryParams } from "@workspace/api-zod";
5
+ import { ai } from "@workspace/integrations-gemini-ai";
6
+ import { desc, eq } from "drizzle-orm";
7
+
8
+ const router = Router();
9
+
10
+ const SYSTEM_PROMPT = `You are a Vedic Rasa Analysis engine built on three ancient sutras from the Vedic Sutras Framework:
11
+
12
+ 1. **Natya Shastra / Rasa Sutras (Sutra 13)** — The Sentiment Analysis framework by Bharata Muni. The 9 primary Rasas (emotional essences) and their extended forms encode all human emotion into a deterministic matrix.
13
+
14
+ 2. **Yoga Sutras of Patanjali (Sutra 10)** — The Attention & Memory framework. Dharana (focused attention) enables precise analysis by eliminating noise (Vritti) and isolating the core emotional signal.
15
+
16
+ 3. **Nyaya Sutras (Sutra 3)** — The AI Logic & Inference framework by Akshapada Gautama. Apply the 5-step Pramana: Pratijna (hypothesis), Hetu (reason), Udaharana (example), Upanaya (application), Nigamana (conclusion).
17
+
18
+ **Rasa Detection:**
19
+ Identify the dominant Rasa from this extended set: Love, Laughter, Compassion, Fury, Heroism, Fear, Disgust, Wonder, Peace, Surprise, Sadness, Calm, Courage, Mystery, Wisdom
20
+
21
+ **Hallucination Detection:**
22
+ Analyze the text for factual inaccuracies, unsupported claims, logical fallacies, or fabricated information. Score from 0.0 (fully grounded) to 1.0 (completely hallucinated).
23
+
24
+ Return ONLY valid JSON with this exact structure (no markdown, no extra text):
25
+ {
26
+ "rasa": {
27
+ "name": "<one of the 15 Rasas>",
28
+ "confidence": <float 0.0-1.0>,
29
+ "explanation": "<brief Vedic-informed explanation of why this Rasa dominates>"
30
+ },
31
+ "hallucination": {
32
+ "score": <float 0.0-1.0>,
33
+ "severity": "<none|low|medium|high|critical>",
34
+ "problematic_statements": ["<statement1>", "<statement2>"]
35
+ },
36
+ "summary": "<1-2 sentence synthesis of the text's essence through the Vedic lens>"
37
+ }`;
38
+
39
+ router.post("/analyze", async (req, res) => {
40
+ const parsed = AnalyzeRasaBody.safeParse(req.body);
41
+ if (!parsed.success) {
42
+ return res.status(400).json({ error: "Invalid request body" });
43
+ }
44
+
45
+ const { text } = parsed.data;
46
+
47
+ if (!text || text.trim().length === 0) {
48
+ return res.status(400).json({ error: "Text is required" });
49
+ }
50
+
51
+ if (text.length > 10000) {
52
+ return res.status(400).json({ error: "Text must be 10,000 characters or less" });
53
+ }
54
+
55
+ try {
56
+ const response = await ai.models.generateContent({
57
+ model: "gemini-2.5-flash",
58
+ contents: [
59
+ {
60
+ role: "user",
61
+ parts: [{ text: `Analyze this text using the Vedic Rasa and Nyaya Sutra frameworks:\n\n${text}` }],
62
+ },
63
+ ],
64
+ config: {
65
+ systemInstruction: SYSTEM_PROMPT,
66
+ responseMimeType: "application/json",
67
+ maxOutputTokens: 8192,
68
+ },
69
+ });
70
+
71
+ const rawText = response.text ?? "{}";
72
+
73
+ let analysis: {
74
+ rasa: { name: string; confidence: number; explanation: string };
75
+ hallucination: { score: number; severity: string; problematic_statements: string[] };
76
+ summary: string;
77
+ };
78
+
79
+ try {
80
+ analysis = JSON.parse(rawText);
81
+ } catch {
82
+ return res.status(500).json({ error: "Failed to parse AI response" });
83
+ }
84
+
85
+ const timestamp = Date.now();
86
+
87
+ await db.insert(rasaAnalysesTable).values({
88
+ text,
89
+ rasa_name: analysis.rasa.name,
90
+ rasa_confidence: analysis.rasa.confidence,
91
+ rasa_explanation: analysis.rasa.explanation,
92
+ hallucination_score: analysis.hallucination.score,
93
+ hallucination_severity: analysis.hallucination.severity,
94
+ hallucination_problematic_statements: analysis.hallucination.problematic_statements,
95
+ summary: analysis.summary,
96
+ timestamp,
97
+ });
98
+
99
+ return res.json({
100
+ rasa: analysis.rasa,
101
+ hallucination: analysis.hallucination,
102
+ summary: analysis.summary,
103
+ text,
104
+ timestamp,
105
+ });
106
+ } catch (err) {
107
+ req.log.error({ err }, "Rasa analysis failed");
108
+ return res.status(500).json({ error: "Analysis failed. Please try again." });
109
+ }
110
+ });
111
+
112
+ router.get("/history", async (req, res) => {
113
+ const parsed = GetRasaHistoryQueryParams.safeParse(req.query);
114
+ const limit = parsed.success && parsed.data.limit ? parsed.data.limit : 50;
115
+
116
+ const records = await db
117
+ .select()
118
+ .from(rasaAnalysesTable)
119
+ .orderBy(desc(rasaAnalysesTable.created_at))
120
+ .limit(limit);
121
+
122
+ return res.json(
123
+ records.map((r) => ({
124
+ ...r,
125
+ created_at: r.created_at.toISOString(),
126
+ }))
127
+ );
128
+ });
129
+
130
+ router.delete("/history/:id", async (req, res) => {
131
+ const parsed = DeleteRasaHistoryParams.safeParse(req.params);
132
+ if (!parsed.success) {
133
+ return res.status(400).json({ error: "Invalid id" });
134
+ }
135
+
136
+ const { id } = parsed.data;
137
+
138
+ const existing = await db
139
+ .select()
140
+ .from(rasaAnalysesTable)
141
+ .where(eq(rasaAnalysesTable.id, id))
142
+ .limit(1);
143
+
144
+ if (existing.length === 0) {
145
+ return res.status(404).json({ error: "Record not found" });
146
+ }
147
+
148
+ await db.delete(rasaAnalysesTable).where(eq(rasaAnalysesTable.id, id));
149
+
150
+ return res.status(204).send();
151
+ });
152
+
153
+ export default router;
artifacts/api-server/tsconfig.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src"],
9
+ "references": [
10
+ {
11
+ "path": "../../lib/db"
12
+ },
13
+ {
14
+ "path": "../../lib/api-zod"
15
+ },
16
+ {
17
+ "path": "../../lib/integrations-gemini-ai"
18
+ }
19
+ ]
20
+ }
lib/api-spec/openapi.yaml ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ openapi: 3.1.0
2
+ info:
3
+ # Do not change the title, if the title changes, the import paths will be broken
4
+ title: Api
5
+ version: 0.1.0
6
+ description: API specification
7
+ servers:
8
+ - url: /api
9
+ description: Base API path
10
+ tags:
11
+ - name: health
12
+ description: Health operations
13
+ - name: rasa
14
+ description: Vedic Rasa analysis operations
15
+ paths:
16
+ /healthz:
17
+ get:
18
+ operationId: healthCheck
19
+ tags: [health]
20
+ summary: Health check
21
+ description: Returns server health status
22
+ responses:
23
+ "200":
24
+ description: Healthy
25
+ content:
26
+ application/json:
27
+ schema:
28
+ $ref: "#/components/schemas/HealthStatus"
29
+ /rasa/analyze:
30
+ post:
31
+ operationId: analyzeRasa
32
+ tags: [rasa]
33
+ summary: Analyze text for Vedic Rasa and hallucination detection
34
+ requestBody:
35
+ required: true
36
+ content:
37
+ application/json:
38
+ schema:
39
+ $ref: "#/components/schemas/AnalyzeRasaBody"
40
+ responses:
41
+ "200":
42
+ description: Analysis result
43
+ content:
44
+ application/json:
45
+ schema:
46
+ $ref: "#/components/schemas/AnalysisResult"
47
+ "400":
48
+ description: Bad request
49
+ content:
50
+ application/json:
51
+ schema:
52
+ $ref: "#/components/schemas/ApiError"
53
+ "500":
54
+ description: Server error
55
+ content:
56
+ application/json:
57
+ schema:
58
+ $ref: "#/components/schemas/ApiError"
59
+ /rasa/history:
60
+ get:
61
+ operationId: getRasaHistory
62
+ tags: [rasa]
63
+ summary: Get analysis history
64
+ parameters:
65
+ - name: limit
66
+ in: query
67
+ required: false
68
+ schema:
69
+ type: integer
70
+ responses:
71
+ "200":
72
+ description: List of past analyses
73
+ content:
74
+ application/json:
75
+ schema:
76
+ type: array
77
+ items:
78
+ $ref: "#/components/schemas/AnalysisRecord"
79
+ /rasa/history/{id}:
80
+ delete:
81
+ operationId: deleteRasaHistory
82
+ tags: [rasa]
83
+ summary: Delete a history entry
84
+ parameters:
85
+ - name: id
86
+ in: path
87
+ required: true
88
+ schema:
89
+ type: integer
90
+ responses:
91
+ "204":
92
+ description: Deleted
93
+ "404":
94
+ description: Not found
95
+ content:
96
+ application/json:
97
+ schema:
98
+ $ref: "#/components/schemas/ApiError"
99
+ components:
100
+ schemas:
101
+ HealthStatus:
102
+ type: object
103
+ properties:
104
+ status:
105
+ type: string
106
+ required:
107
+ - status
108
+ AnalyzeRasaBody:
109
+ type: object
110
+ properties:
111
+ text:
112
+ type: string
113
+ required:
114
+ - text
115
+ RasaResult:
116
+ type: object
117
+ properties:
118
+ name:
119
+ type: string
120
+ confidence:
121
+ type: number
122
+ explanation:
123
+ type: string
124
+ required:
125
+ - name
126
+ - confidence
127
+ - explanation
128
+ HallucinationResult:
129
+ type: object
130
+ properties:
131
+ score:
132
+ type: number
133
+ severity:
134
+ type: string
135
+ problematic_statements:
136
+ type: array
137
+ items:
138
+ type: string
139
+ required:
140
+ - score
141
+ - severity
142
+ - problematic_statements
143
+ AnalysisResult:
144
+ type: object
145
+ properties:
146
+ rasa:
147
+ $ref: "#/components/schemas/RasaResult"
148
+ hallucination:
149
+ $ref: "#/components/schemas/HallucinationResult"
150
+ summary:
151
+ type: string
152
+ text:
153
+ type: string
154
+ timestamp:
155
+ type: number
156
+ required:
157
+ - rasa
158
+ - hallucination
159
+ - summary
160
+ - text
161
+ - timestamp
162
+ AnalysisRecord:
163
+ type: object
164
+ properties:
165
+ id:
166
+ type: integer
167
+ text:
168
+ type: string
169
+ rasa_name:
170
+ type: string
171
+ rasa_confidence:
172
+ type: number
173
+ rasa_explanation:
174
+ type: string
175
+ hallucination_score:
176
+ type: number
177
+ hallucination_severity:
178
+ type: string
179
+ hallucination_problematic_statements:
180
+ type: array
181
+ items:
182
+ type: string
183
+ summary:
184
+ type: string
185
+ timestamp:
186
+ type: number
187
+ created_at:
188
+ type: string
189
+ required:
190
+ - id
191
+ - text
192
+ - rasa_name
193
+ - rasa_confidence
194
+ - rasa_explanation
195
+ - hallucination_score
196
+ - hallucination_severity
197
+ - hallucination_problematic_statements
198
+ - summary
199
+ - timestamp
200
+ - created_at
201
+ ApiError:
202
+ type: object
203
+ properties:
204
+ error:
205
+ type: string
206
+ required:
207
+ - error
lib/api-spec/orval.config.ts ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, InputTransformerFn } from "orval";
2
+ import path from "path";
3
+
4
+ const root = path.resolve(__dirname, "..", "..");
5
+ const apiClientReactSrc = path.resolve(root, "lib", "api-client-react", "src");
6
+ const apiZodSrc = path.resolve(root, "lib", "api-zod", "src");
7
+
8
+ // Our exports make assumptions about the title of the API being "Api" (i.e. generated output is `api.ts`).
9
+ const titleTransformer: InputTransformerFn = (config) => {
10
+ config.info ??= {};
11
+ config.info.title = "Api";
12
+
13
+ return config;
14
+ };
15
+
16
+ export default defineConfig({
17
+ "api-client-react": {
18
+ input: {
19
+ target: "./openapi.yaml",
20
+ override: {
21
+ transformer: titleTransformer,
22
+ },
23
+ },
24
+ output: {
25
+ workspace: apiClientReactSrc,
26
+ target: "generated",
27
+ client: "react-query",
28
+ mode: "split",
29
+ baseUrl: "/api",
30
+ clean: true,
31
+ prettier: true,
32
+ override: {
33
+ fetch: {
34
+ includeHttpResponseReturnType: false,
35
+ },
36
+ mutator: {
37
+ path: path.resolve(apiClientReactSrc, "custom-fetch.ts"),
38
+ name: "customFetch",
39
+ },
40
+ },
41
+ },
42
+ },
43
+ zod: {
44
+ input: {
45
+ target: "./openapi.yaml",
46
+ override: {
47
+ transformer: titleTransformer,
48
+ },
49
+ },
50
+ output: {
51
+ workspace: apiZodSrc,
52
+ client: "zod",
53
+ target: "generated",
54
+ schemas: { path: "generated/types", type: "typescript" },
55
+ mode: "split",
56
+ clean: true,
57
+ prettier: true,
58
+ override: {
59
+ zod: {
60
+ coerce: {
61
+ query: ['boolean', 'number', 'string'],
62
+ param: ['boolean', 'number', 'string'],
63
+ body: ['bigint', 'date'],
64
+ response: ['bigint', 'date'],
65
+ },
66
+ },
67
+ useDates: true,
68
+ useBigInt: true,
69
+ },
70
+ },
71
+ },
72
+ });
lib/api-spec/package.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@workspace/api-spec",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "codegen": "orval --config ./orval.config.ts"
7
+ },
8
+ "devDependencies": {
9
+ "orval": "^8.5.2"
10
+ }
11
+ }
lib/db/drizzle.config.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "drizzle-kit";
2
+ import path from "path";
3
+
4
+ if (!process.env.DATABASE_URL) {
5
+ throw new Error("DATABASE_URL, ensure the database is provisioned");
6
+ }
7
+
8
+ export default defineConfig({
9
+ schema: path.join(__dirname, "./src/schema/index.ts"),
10
+ dialect: "postgresql",
11
+ dbCredentials: {
12
+ url: process.env.DATABASE_URL,
13
+ },
14
+ });
lib/db/package.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@workspace/db",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./schema": "./src/schema/index.ts"
9
+ },
10
+ "scripts": {
11
+ "push": "drizzle-kit push --config ./drizzle.config.ts",
12
+ "push-force": "drizzle-kit push --force --config ./drizzle.config.ts"
13
+ },
14
+ "dependencies": {
15
+ "drizzle-orm": "catalog:",
16
+ "drizzle-zod": "^0.8.3",
17
+ "pg": "^8.20.0",
18
+ "zod": "catalog:"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "catalog:",
22
+ "@types/pg": "^8.18.0",
23
+ "drizzle-kit": "^0.31.9"
24
+ }
25
+ }
lib/db/src/index.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { drizzle } from "drizzle-orm/node-postgres";
2
+ import pg from "pg";
3
+ import * as schema from "./schema";
4
+
5
+ const { Pool } = pg;
6
+
7
+ if (!process.env.DATABASE_URL) {
8
+ throw new Error(
9
+ "DATABASE_URL must be set. Did you forget to provision a database?",
10
+ );
11
+ }
12
+
13
+ export const pool = new Pool({ connectionString: process.env.DATABASE_URL });
14
+ export const db = drizzle(pool, { schema });
15
+
16
+ export * from "./schema";
lib/db/src/schema/index.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Export your models here. Add one export per file
2
+ // export * from "./posts";
3
+ //
4
+ // Each model/table should ideally be split into different files.
5
+ // Each model/table should define a Drizzle table, insert schema, and types:
6
+ //
7
+ // import { pgTable, text, serial } from "drizzle-orm/pg-core";
8
+ // import { createInsertSchema } from "drizzle-zod";
9
+ // import { z } from "zod/v4";
10
+ //
11
+ // export const postsTable = pgTable("posts", {
12
+ // id: serial("id").primaryKey(),
13
+ // title: text("title").notNull(),
14
+ // });
15
+ //
16
+ // export const insertPostSchema = createInsertSchema(postsTable).omit({ id: true });
17
+ // export type InsertPost = z.infer<typeof insertPostSchema>;
18
+ // export type Post = typeof postsTable.$inferSelect;
19
+
20
+ export * from "./rasa_analyses";
lib/db/src/schema/rasa_analyses.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { pgTable, serial, text, real, jsonb, timestamp, bigint } from "drizzle-orm/pg-core";
2
+ import { createInsertSchema } from "drizzle-zod";
3
+ import { z } from "zod/v4";
4
+
5
+ export const rasaAnalysesTable = pgTable("rasa_analyses", {
6
+ id: serial("id").primaryKey(),
7
+ text: text("text").notNull(),
8
+ rasa_name: text("rasa_name").notNull(),
9
+ rasa_confidence: real("rasa_confidence").notNull(),
10
+ rasa_explanation: text("rasa_explanation").notNull(),
11
+ hallucination_score: real("hallucination_score").notNull(),
12
+ hallucination_severity: text("hallucination_severity").notNull(),
13
+ hallucination_problematic_statements: jsonb("hallucination_problematic_statements").$type<string[]>().notNull().default([]),
14
+ summary: text("summary").notNull(),
15
+ timestamp: bigint("timestamp", { mode: "number" }).notNull(),
16
+ created_at: timestamp("created_at").defaultNow().notNull(),
17
+ });
18
+
19
+ export const insertRasaAnalysisSchema = createInsertSchema(rasaAnalysesTable).omit({ id: true, created_at: true });
20
+ export type InsertRasaAnalysis = z.infer<typeof insertRasaAnalysisSchema>;
21
+ export type RasaAnalysis = typeof rasaAnalysesTable.$inferSelect;
lib/db/tsconfig.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "composite": true,
5
+ "declarationMap": true,
6
+ "emitDeclarationOnly": true,
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "types": ["node"]
10
+ },
11
+ "include": ["src"]
12
+ }
package.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "workspace",
3
+ "version": "0.0.0",
4
+ "license": "MIT",
5
+ "scripts": {
6
+ "preinstall": "sh -c 'rm -f package-lock.json yarn.lock; case \"$npm_config_user_agent\" in pnpm/*) ;; *) echo \"Use pnpm instead\" >&2; exit 1 ;; esac'",
7
+ "build": "pnpm run typecheck && pnpm -r --if-present run build",
8
+ "typecheck:libs": "tsc --build",
9
+ "typecheck": "pnpm run typecheck:libs && pnpm -r --filter \"./artifacts/**\" --filter \"./scripts\" --if-present run typecheck"
10
+ },
11
+ "private": true,
12
+ "devDependencies": {
13
+ "typescript": "~5.9.2",
14
+ "prettier": "^3.8.1"
15
+ }
16
+ }
pnpm-workspace.yaml ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================================
2
+ # SECURITY: Minimum release age for npm packages (supply-chain attack defense)
3
+ # ============================================================================
4
+ #
5
+ # This setting requires that any npm package version must have been published
6
+ # for at least 1 day (1440 minutes) before pnpm will allow installing it.
7
+ # This is a critical defense against supply-chain attacks. In most cases,
8
+ # malicious npm releases are discovered and pulled within hours, so a 1-day
9
+ # delay provides a strong safety buffer.
10
+ #
11
+ # DO NOT DISABLE THIS SETTING. Removing or setting it to 0 is considered
12
+ # extremely dangerous and leaves the entire workspace vulnerable to supply-
13
+ # chain attacks, which have been the #1 vector for npm ecosystem compromises.
14
+ #
15
+ # If you absolutely need to install a package before the 1-day window has
16
+ # passed (e.g. an urgent security bugfix), you can add it to the
17
+ # `minimumReleaseAgeExclude` allowlist below. Only consider doing this for
18
+ # packages released by trusted organizations with an impeccable security
19
+ # posture (e.g. Replit packsges, react from Meta, typescript from Microsoft). Even then,
20
+ # remove the exclusion once the 1-day window has passed.
21
+ #
22
+ # Example:
23
+ # minimumReleaseAgeExclude:
24
+ # - react
25
+ # - typescript
26
+ #
27
+ # ============================================================================
28
+ minimumReleaseAge: 1440
29
+
30
+ minimumReleaseAgeExclude:
31
+ # Exclude @replit scoped packages from the minimum release age check.
32
+ # These are published by Replit and trusted — the supply-chain attack vector
33
+ # this setting guards against does not apply to our own packages.
34
+ - '@replit/*'
35
+ - stripe-replit-sync
36
+
37
+ packages:
38
+ - artifacts/*
39
+ - lib/*
40
+ - lib/integrations/*
41
+ - scripts
42
+
43
+ catalog:
44
+ '@replit/vite-plugin-cartographer': ^0.5.1
45
+ '@replit/vite-plugin-dev-banner': ^0.1.1
46
+ '@replit/vite-plugin-runtime-error-modal': ^0.0.6
47
+ '@tailwindcss/vite': ^4.1.14
48
+ '@tanstack/react-query': ^5.90.21
49
+ '@types/node': ^25.3.3
50
+ '@types/react': ^19.2.0
51
+ '@types/react-dom': ^19.2.0
52
+ '@vitejs/plugin-react': ^5.0.4
53
+ class-variance-authority: ^0.7.1
54
+ clsx: ^2.1.1
55
+ drizzle-orm: ^0.45.1
56
+ framer-motion: ^12.23.24
57
+ lucide-react: ^0.545.0
58
+ # Must be this exact version because expo requires it
59
+ react: 19.1.0
60
+ # Must be this exact version because expo requires it
61
+ react-dom: 19.1.0
62
+ tailwind-merge: ^3.3.1
63
+ tailwindcss: ^4.1.14
64
+ tsx: ^4.21.0
65
+ vite: ^7.3.0
66
+ zod: ^3.25.76
67
+
68
+ autoInstallPeers: false
69
+
70
+ onlyBuiltDependencies:
71
+ - '@swc/core'
72
+ - esbuild
73
+ - msw
74
+ - unrs-resolver
75
+
76
+ overrides:
77
+ # replit uses linux-x64 only, we can exclude all other platforms
78
+ "esbuild>@esbuild/darwin-arm64": "-"
79
+ "esbuild>@esbuild/darwin-x64": "-"
80
+ "esbuild>@esbuild/freebsd-arm64": "-"
81
+ "esbuild>@esbuild/freebsd-x64": "-"
82
+ "esbuild>@esbuild/linux-arm": "-"
83
+ "esbuild>@esbuild/linux-arm64": "-"
84
+ "esbuild>@esbuild/linux-ia32": "-"
85
+ "esbuild>@esbuild/linux-loong64": "-"
86
+ "esbuild>@esbuild/linux-mips64el": "-"
87
+ "esbuild>@esbuild/linux-ppc64": "-"
88
+ "esbuild>@esbuild/linux-riscv64": "-"
89
+ "esbuild>@esbuild/linux-s390x": "-"
90
+ "esbuild>@esbuild/netbsd-arm64": "-"
91
+ "esbuild>@esbuild/netbsd-x64": "-"
92
+ "esbuild>@esbuild/openbsd-arm64": "-"
93
+ "esbuild>@esbuild/openbsd-x64": "-"
94
+ "esbuild>@esbuild/sunos-x64": "-"
95
+ "esbuild>@esbuild/win32-arm64": "-"
96
+ "esbuild>@esbuild/win32-ia32": "-"
97
+ "esbuild>@esbuild/win32-x64": "-"
98
+ "esbuild>@esbuild/aix-ppc64": '-'
99
+ "esbuild>@esbuild/android-arm": '-'
100
+ "esbuild>@esbuild/android-arm64": '-'
101
+ "esbuild>@esbuild/android-x64": '-'
102
+ "esbuild>@esbuild/openharmony-arm64": '-'
103
+ "lightningcss>lightningcss-android-arm64": "-"
104
+ "lightningcss>lightningcss-darwin-arm64": "-"
105
+ "lightningcss>lightningcss-darwin-x64": "-"
106
+ "lightningcss>lightningcss-freebsd-x64": "-"
107
+ "lightningcss>lightningcss-linux-arm-gnueabihf": "-"
108
+ "lightningcss>lightningcss-linux-arm64-gnu": "-"
109
+ "lightningcss>lightningcss-linux-arm64-musl": "-"
110
+ "lightningcss>lightningcss-linux-x64-musl": "-"
111
+ "lightningcss>lightningcss-win32-arm64-msvc": "-"
112
+ "lightningcss>lightningcss-win32-x64-msvc": "-"
113
+ "@tailwindcss/oxide>@tailwindcss/oxide-android-arm64": "-"
114
+ "@tailwindcss/oxide>@tailwindcss/oxide-darwin-arm64": "-"
115
+ "@tailwindcss/oxide>@tailwindcss/oxide-darwin-x64": "-"
116
+ "@tailwindcss/oxide>@tailwindcss/oxide-freebsd-x64": "-"
117
+ "@tailwindcss/oxide>@tailwindcss/oxide-linux-arm-gnueabihf": "-"
118
+ "@tailwindcss/oxide>@tailwindcss/oxide-linux-arm64-gnu": "-"
119
+ "@tailwindcss/oxide>@tailwindcss/oxide-linux-arm64-musl": "-"
120
+ "@tailwindcss/oxide>@tailwindcss/oxide-win32-arm64-msvc": "-"
121
+ "@tailwindcss/oxide>@tailwindcss/oxide-win32-x64-msvc": "-"
122
+ "@tailwindcss/oxide>@tailwindcss/oxide-linux-x64-musl": "-"
123
+ "rollup>@rollup/rollup-android-arm-eabi": "-"
124
+ "rollup>@rollup/rollup-android-arm64": "-"
125
+ "rollup>@rollup/rollup-darwin-arm64": "-"
126
+ "rollup>@rollup/rollup-darwin-x64": "-"
127
+ "rollup>@rollup/rollup-freebsd-arm64": "-"
128
+ "rollup>@rollup/rollup-freebsd-x64": "-"
129
+ "rollup>@rollup/rollup-linux-arm-gnueabihf": "-"
130
+ "rollup>@rollup/rollup-linux-arm-musleabihf": "-"
131
+ "rollup>@rollup/rollup-linux-arm64-gnu": "-"
132
+ "rollup>@rollup/rollup-linux-arm64-musl": "-"
133
+ "rollup>@rollup/rollup-linux-loong64-gnu": "-"
134
+ "rollup>@rollup/rollup-linux-loong64-musl": "-"
135
+ "rollup>@rollup/rollup-linux-ppc64-gnu": "-"
136
+ "rollup>@rollup/rollup-linux-ppc64-musl": "-"
137
+ "rollup>@rollup/rollup-linux-riscv64-gnu": "-"
138
+ "rollup>@rollup/rollup-linux-riscv64-musl": "-"
139
+ "rollup>@rollup/rollup-linux-s390x-gnu": "-"
140
+ "rollup>@rollup/rollup-linux-x64-musl": "-"
141
+ "rollup>@rollup/rollup-openbsd-x64": "-"
142
+ "rollup>@rollup/rollup-openharmony-arm64": "-"
143
+ "rollup>@rollup/rollup-win32-arm64-msvc": "-"
144
+ "rollup>@rollup/rollup-win32-ia32-msvc": "-"
145
+ "rollup>@rollup/rollup-win32-x64-gnu": "-"
146
+ "rollup>@rollup/rollup-win32-x64-msvc": "-"
147
+ "@expo/ngrok-bin>@expo/ngrok-bin-darwin-arm64": "-"
148
+ "@expo/ngrok-bin>@expo/ngrok-bin-darwin-x64": "-"
149
+ "@expo/ngrok-bin>@expo/ngrok-bin-freebsd-ia32": "-"
150
+ "@expo/ngrok-bin>@expo/ngrok-bin-freebsd-x64": "-"
151
+ "@expo/ngrok-bin>@expo/ngrok-bin-linux-arm64": "-"
152
+ "@expo/ngrok-bin>@expo/ngrok-bin-linux-arm": "-"
153
+ "@expo/ngrok-bin>@expo/ngrok-bin-linux-ia32": "-"
154
+ "@expo/ngrok-bin>@expo/ngrok-bin-sunos-x64": "-"
155
+ "@expo/ngrok-bin>@expo/ngrok-bin-win32-ia32": "-"
156
+ "@expo/ngrok-bin>@expo/ngrok-bin-win32-x64": "-"
157
+ # drizzle-kit uses esbuild internally on an older version that's vulnerable, this overrides it
158
+ "@esbuild-kit/esm-loader": "npm:tsx@^4.21.0"
159
+ esbuild: "0.27.3"
tsconfig.base.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "isolatedModules": true,
4
+ "lib": ["es2022"],
5
+ "module": "esnext",
6
+ "moduleResolution": "bundler",
7
+ "noEmitOnError": true,
8
+ "noFallthroughCasesInSwitch": true,
9
+ "noImplicitOverride": false,
10
+ "noImplicitReturns": true,
11
+ "noUnusedLocals": false,
12
+ "noImplicitAny": true,
13
+ "noImplicitThis": true,
14
+ "strictNullChecks": true,
15
+ "strictFunctionTypes": false,
16
+ "strictBindCallApply": true,
17
+ "strictPropertyInitialization": true,
18
+ "useUnknownInCatchVariables": true,
19
+ "alwaysStrict": true,
20
+ "skipLibCheck": true,
21
+ "target": "es2022",
22
+ "types": [],
23
+ "customConditions": ["workspace"]
24
+ }
25
+ }
tsconfig.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compileOnSave": false,
4
+ "files": [],
5
+ "references": [
6
+ {
7
+ "path": "./lib/db"
8
+ },
9
+ {
10
+ "path": "./lib/api-client-react"
11
+ },
12
+ {
13
+ "path": "./lib/api-zod"
14
+ },
15
+ {
16
+ "path": "./lib/integrations-gemini-ai"
17
+ }
18
+ ]
19
+ }