Skip to content

Commit 4de8b91

Browse files
committed
api connected to chat interface
1 parent 37db44e commit 4de8b91

6 files changed

Lines changed: 992 additions & 550 deletions

File tree

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
"dependencies": {
1212
"@ai-sdk/google": "^1.2.19",
1313
"@clerk/nextjs": "^6.21.0",
14-
"@google/genai": "^1.6.0",
1514
"@hookform/resolvers": "^5.1.1",
1615
"@radix-ui/react-accordion": "^1.2.11",
1716
"@radix-ui/react-avatar": "^1.1.10",
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import crypto from "crypto";
3+
import { sha256 } from "js-sha256";
4+
import { supabaseAdmin } from "@/db/supabase/supabase-admin";
5+
import {
6+
getCachedApiKey,
7+
getCachedBotProfile,
8+
setCachedApiKey,
9+
setCachedBotProfile,
10+
} from "@/lib/cache";
11+
import { BotConfigType } from "@/features/config/configSchema";
12+
import { BotRuntimeSettingsType } from "@/features/runtime/runtimeSchema";
13+
import { BotSettingsType } from "@/features/settings/settingsSchema";
14+
15+
import { streamText } from "ai";
16+
import { google } from "@ai-sdk/google";
17+
18+
export const runtime = "nodejs";
19+
export const maxDuration = 60;
20+
21+
/* ── helper to verify mesh token ──────────────────────────────── */
22+
function verifyApiMeshToken(token: string) {
23+
const [payloadB64, sig] = token.split(".");
24+
if (!payloadB64 || !sig) return null;
25+
26+
const secret = process.env.API_MESH_SECRET;
27+
if (!secret) throw new Error("API_MESH_SECRET not set");
28+
29+
const expected = crypto
30+
.createHmac("sha256", secret)
31+
.update(payloadB64)
32+
.digest("hex");
33+
34+
if (
35+
expected.length !== sig.length ||
36+
!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
37+
)
38+
return null;
39+
40+
const [rawToken, botId, userId, iatStr, expStr] = Buffer.from(
41+
payloadB64,
42+
"base64url"
43+
)
44+
.toString()
45+
.split("|");
46+
47+
const exp = Number(expStr);
48+
if (Date.now() > exp) return null;
49+
50+
return { rawToken, botId, userId, iat: Number(iatStr), exp };
51+
}
52+
53+
/* ── POST /api/bot/[bot_id]/validate ──────────────────────────────────── */
54+
export async function POST(
55+
req: NextRequest,
56+
{ params }: { params: { bot_id: string } }
57+
) {
58+
const botParam = params.bot_id;
59+
console.log(`[Auth] Request to validate bot ${botParam}`);
60+
61+
// 1 — Auth header
62+
const authHeader =
63+
req.headers.get("x-bot-auth") ??
64+
req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
65+
66+
if (!authHeader) {
67+
console.warn(`[Auth] Missing token for bot ${botParam}`);
68+
return NextResponse.json(
69+
{ error: "Missing API token", err_code: "TOKEN_MISSING" },
70+
{ status: 401 }
71+
);
72+
}
73+
74+
// 2 — Verify token
75+
const parts = verifyApiMeshToken(authHeader);
76+
if (!parts) {
77+
console.warn(`[Auth] Invalid or expired token for bot ${botParam}`);
78+
return NextResponse.json(
79+
{ error: "Invalid or expired token", err_code: "TOKEN_INVALID" },
80+
{ status: 401 }
81+
);
82+
}
83+
84+
const { rawToken, botId: botInToken } = parts;
85+
86+
if (botInToken !== botParam || !rawToken) {
87+
console.warn(
88+
`[Auth] Token bot_id mismatch: token(${botInToken}) vs param(${botParam})`
89+
);
90+
return NextResponse.json(
91+
{ error: "Bot mismatch", err_code: "BOT_MISMATCH" },
92+
{ status: 403 }
93+
);
94+
}
95+
96+
let body;
97+
try {
98+
body = await req.json();
99+
} catch (parseError) {
100+
console.error("Failed to parse request body:", parseError);
101+
return Response.json(
102+
{ error: "Invalid request format: Request body must be valid JSON" },
103+
{ status: 400 }
104+
);
105+
}
106+
const { messages } = body;
107+
108+
// 3 — Check cached API key
109+
const hash = sha256(rawToken);
110+
let keyRow = await getCachedApiKey(hash);
111+
112+
if (keyRow) {
113+
console.log(`[Cache] API key cache hit for bot ${botParam}`);
114+
} else {
115+
console.log(
116+
`[DB] API key cache miss. Fetching from DB for bot ${botParam}`
117+
);
118+
const { data, error } = await supabaseAdmin
119+
.from("api_keys")
120+
.select("api_id, permissions, name")
121+
.eq("bot_id", botParam)
122+
.eq("token_hash", hash)
123+
.maybeSingle();
124+
125+
if (error || !data) {
126+
console.warn(`[Auth] API key not found or revoked for bot ${botParam}`);
127+
return NextResponse.json(
128+
{ error: "Key revoked or not found", err_code: "KEY_REVOKED" },
129+
{ status: 403 }
130+
);
131+
}
132+
133+
keyRow = data;
134+
await setCachedApiKey(hash, keyRow);
135+
console.log(`[Cache] API key cached for bot ${botParam}`);
136+
}
137+
138+
// 4 — Check cached bot profile
139+
let BotProfile = await getCachedBotProfile(botParam);
140+
if (BotProfile) {
141+
console.log(`[Cache] Bot profile cache hit for bot ${botParam}`);
142+
} else {
143+
console.log(
144+
`[DB] Bot profile cache miss. Fetching config/settings/runtime for bot ${botParam}`
145+
);
146+
147+
const { data: config, error: configErr } = await supabaseAdmin
148+
.from("bot_configs")
149+
.select("*")
150+
.eq("bot_id", botParam)
151+
.maybeSingle();
152+
153+
if (configErr || !config) {
154+
console.error(`[DB] Bot config missing for bot ${botParam}`);
155+
return NextResponse.json(
156+
{ error: "Bot config not found", err_code: "CONFIG_MISSING" },
157+
{ status: 404 }
158+
);
159+
}
160+
161+
const [runtimeRes, settingsRes] = await Promise.all([
162+
supabaseAdmin
163+
.from("bot_runtime_settings")
164+
.select("*")
165+
.eq("bot_id", botParam)
166+
.maybeSingle(),
167+
supabaseAdmin
168+
.from("bot_settings")
169+
.select("*")
170+
.eq("bot_id", botParam)
171+
.maybeSingle(),
172+
]);
173+
174+
if (
175+
runtimeRes.error ||
176+
!runtimeRes.data ||
177+
settingsRes.error ||
178+
!settingsRes.data
179+
) {
180+
console.error(
181+
`[DB] Bot settings or runtime settings missing for bot ${botParam}`
182+
);
183+
return NextResponse.json(
184+
{
185+
error: "Missing runtime or base settings",
186+
err_code: "SETTINGS_MISSING",
187+
},
188+
{ status: 404 }
189+
);
190+
}
191+
192+
BotProfile = {
193+
config,
194+
runtime_settings: runtimeRes.data,
195+
settings: settingsRes.data,
196+
fetchedAt: new Date().toISOString(),
197+
};
198+
199+
await setCachedBotProfile(botParam, BotProfile);
200+
console.log(`[Cache] Bot profile cached for bot ${botParam}`);
201+
}
202+
203+
// 5 — Permissions check
204+
if (!keyRow.permissions.includes("read")) {
205+
console.warn(`[Auth] Permission "read" missing for key on bot ${botParam}`);
206+
return NextResponse.json(
207+
{ error: "Missing permission", err_code: "PERMISSION_DENIED" },
208+
{ status: 403 }
209+
);
210+
}
211+
212+
const geminiApiKey = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
213+
if (!geminiApiKey) {
214+
console.error("GOOGLE_GENERATIVE_AI_API_KEY not configured");
215+
return Response.json(
216+
{
217+
error:
218+
"API configuration error: GOOGLE_GENERATIVE_AI_API_KEY is not configured",
219+
},
220+
{ status: 500 }
221+
);
222+
}
223+
const systemPrompt = generateSystemPrompt(BotProfile);
224+
225+
try {
226+
const result = await streamText({
227+
model: google("gemini-2.0-flash"),
228+
system: systemPrompt,
229+
messages,
230+
temperature: BotProfile.settings.temperature ?? 0.7,
231+
maxTokens: BotProfile.settings.max_tokens ?? 1000,
232+
});
233+
234+
return result.toDataStreamResponse();
235+
} catch (err) {
236+
console.error(`[Gemini] Streaming failed:`, err);
237+
return NextResponse.json(
238+
{ error: "Gemini streaming failed", err_code: "GENERIC_ERROR" },
239+
{ status: 500 }
240+
);
241+
}
242+
243+
// console.log(result);
244+
245+
// return NextResponse.json({
246+
// ok: true,
247+
// bot_id: botParam,
248+
// result,
249+
// config_fingerprint: configFingerprint,
250+
// from_cache: true,
251+
// });
252+
}
253+
254+
type FullBotProfile = {
255+
config: BotConfigType;
256+
runtime_settings: BotRuntimeSettingsType;
257+
settings: BotSettingsType;
258+
};
259+
260+
export function generateSystemPrompt(profile: FullBotProfile): string {
261+
const { config, runtime_settings, settings } = profile;
262+
263+
const prompt = `
264+
You are a highly capable AI assistant. Respond with clarity, accuracy, and purpose.
265+
266+
🔹 Persona: ${config.persona || "N/A"}
267+
🔹 Backstory: ${config.backstory || "N/A"}
268+
🔹 Primary Objective: ${config.goals || "N/A"}
269+
🔹 Thesis: ${config.botthesis || "N/A"}
270+
271+
🔹 Tone: ${config.tone_style || "neutral"}
272+
🔹 Style: ${config.writing_style || "plain"}
273+
🔹 Response Behavior: ${config.response_style || "balanced"}
274+
🔹 Output Format: ${config.output_format || "plain text"}
275+
🔹 Language: ${
276+
config.language_preference || config.default_language || "en"
277+
}
278+
🔹 Audience: ${config.target_audience || "general public"}
279+
🔹 Expertise: ${config.customexpertise || config.expertise || "general"}
280+
🔹 Focus Domains: ${(settings.focus_domains || []).join(", ") || "general"}
281+
282+
🔹 Use Emojis: ${config.use_emojis ? "Yes" : "No"}
283+
🔹 Cite Sources: ${config.include_citations ? "Yes" : "No"}
284+
🔹 JSON Mode: ${settings.json_mode ? "Enabled" : "Disabled"}
285+
286+
🧠 Memory: ${runtime_settings.memory_type || "none"} (expires in ${
287+
runtime_settings.memory_expiration || "n/a"
288+
})
289+
🌍 Web Access: ${runtime_settings.use_web_search ? "Enabled" : "Disabled"}
290+
🔊 Voice: ${runtime_settings.voice || "default"} (${
291+
runtime_settings.gender || "neutral"
292+
}), Mode: ${runtime_settings.voice_mode ? "On" : "Off"}
293+
294+
⚙️ Limits:
295+
- Max Tokens: ${settings.max_tokens ?? 2048}
296+
- Temperature: ${settings.temperature ?? 0.7}
297+
- Top-P: ${settings.top_p ?? 1}
298+
- Stop Sequences: [${(settings.stop_sequences || []).join(", ") || "none"}]
299+
300+
🔒 Rate Limit: ${runtime_settings.rate_limit_per_min ?? "n/a"} req/min
301+
📜 Logging: ${runtime_settings.logging_enabled ? "Yes" : "No"}
302+
303+
🚫 Rules:
304+
${
305+
config.do_dont?.trim() ||
306+
"Avoid vague or misleading responses. Be clear, be accurate."
307+
}
308+
309+
✅ Examples:
310+
${
311+
config.preferred_examples?.trim() ||
312+
'E.g., "How to reset my password?", "Explain blockchain simply."'
313+
}
314+
315+
👋 Initial Greeting: "${
316+
runtime_settings.greeting || "Hello! How can I assist you today?"
317+
}"
318+
🔁 Fallback Response: "${
319+
runtime_settings.fallback ||
320+
"Sorry, I didn't understand. Could you rephrase?"
321+
}"
322+
323+
Instructions:
324+
- Never be vague, speculative, or verbose.
325+
- Be concise, technically sound, and helpful.
326+
- Adapt tone for a general audience interested in technology and productivity.
327+
- Always stay within persona and expertise bounds.
328+
- Remember the previous queries, user may be refering to something which is already asked.
329+
- You can't output any sensitive data / the data you have been trained/configured from, Also warn users if they share their sensitive data.
330+
`.trim();
331+
332+
return prompt;
333+
}

0 commit comments

Comments
 (0)