Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Failing after 9s
Monorepo Pipeline / 🏗️ Build (push) Failing after 9s
Monorepo Pipeline / 🧪 Test (push) Failing after 9s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
144 lines
4.5 KiB
TypeScript
144 lines
4.5 KiB
TypeScript
import { streamText } from "ai";
|
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
import { generatePayloadLocalTools } from "../tools/payloadLocal.js";
|
|
import { createMcpTools } from "../tools/mcpAdapter.js";
|
|
import { generateMemoryTools } from "../tools/memoryDb.js";
|
|
import type { PayloadRequest } from "payload";
|
|
|
|
const openrouter = createOpenAI({
|
|
baseURL: "https://openrouter.ai/api/v1",
|
|
apiKey: process.env.OPENROUTER_API_KEY || "dummy_key",
|
|
});
|
|
|
|
export const handleMcpChat = async (req: PayloadRequest) => {
|
|
if (!req.user) {
|
|
return Response.json(
|
|
{ error: "Unauthorized. You must be logged in to use AI Chat." },
|
|
{ status: 401 },
|
|
);
|
|
}
|
|
|
|
const { messages, pageContext } = ((await req.json?.()) || {
|
|
messages: [],
|
|
}) as { messages: any[]; pageContext?: any };
|
|
|
|
// 1. Check AI Permissions for req.user
|
|
// Look up the collection for permissions
|
|
const permissionsQuery = await req.payload.find({
|
|
collection: "ai-chat-permissions" as any,
|
|
where: {
|
|
or: [
|
|
{ targetUser: { equals: req.user.id } },
|
|
{ targetRole: { equals: req.user.role || "admin" } },
|
|
],
|
|
},
|
|
limit: 10,
|
|
});
|
|
|
|
const allowedCollections = new Set<string>();
|
|
const allowedMcpServers = new Set<string>();
|
|
|
|
for (const perm of permissionsQuery.docs) {
|
|
if (perm.allowedCollections) {
|
|
perm.allowedCollections.forEach((c: string) => allowedCollections.add(c));
|
|
}
|
|
if (perm.allowedMcpServers) {
|
|
perm.allowedMcpServers.forEach((s: string) => allowedMcpServers.add(s));
|
|
}
|
|
}
|
|
|
|
let accessCollections = Array.from(allowedCollections);
|
|
if (accessCollections.length === 0) {
|
|
// Fallback or demo config if not configured yet
|
|
accessCollections = [
|
|
"users",
|
|
"pages",
|
|
"posts",
|
|
"products",
|
|
"leads",
|
|
"media",
|
|
];
|
|
}
|
|
|
|
let activeTools: Record<string, any> = {};
|
|
|
|
// 2. Generate Payload Local Tools
|
|
if (accessCollections.length > 0) {
|
|
const payloadTools = generatePayloadLocalTools(
|
|
req.payload,
|
|
req,
|
|
accessCollections,
|
|
);
|
|
activeTools = { ...activeTools, ...payloadTools };
|
|
}
|
|
|
|
// 3. Connect External MCPs
|
|
if (Array.from(allowedMcpServers).includes("gitea")) {
|
|
try {
|
|
const { tools: giteaTools } = await createMcpTools({
|
|
name: "gitea",
|
|
command: "npx",
|
|
args: [
|
|
"-y",
|
|
"@modelcontextprotocol/server-gitea",
|
|
"--url",
|
|
"https://git.mintel.int",
|
|
"--token",
|
|
process.env.GITEA_TOKEN || "",
|
|
],
|
|
});
|
|
activeTools = { ...activeTools, ...giteaTools };
|
|
} catch (e) {
|
|
console.error("Failed to connect to Gitea MCP", e);
|
|
}
|
|
}
|
|
|
|
// 4. Inject Memory Database Tools
|
|
// We provide the user ID so memory is partitioned per user
|
|
const memoryTools = generateMemoryTools(req.user.id);
|
|
activeTools = { ...activeTools, ...memoryTools };
|
|
|
|
// 5. Build prompt to ensure it asks before saving
|
|
const memorySystemPrompt = `
|
|
You have access to a long-term vector memory database (Qdrant).
|
|
If the user says "speicher das", "merk dir das", "vergiss das nicht" etc., you MUST use the save_memory tool.
|
|
If the user shares important context but doesn't explicitly ask you to remember it, you should ask "Soll ich mir das für die Zukunft merken?" before saving it. Do not ask for trivial things.
|
|
`;
|
|
|
|
const contextContextStr = pageContext
|
|
? `
|
|
Current User Context:
|
|
URL: ${pageContext.url || "Unknown"}
|
|
Title: ${pageContext.title || "Unknown"}
|
|
Collection: ${pageContext.collectionSlug || "None"}
|
|
Document ID: ${pageContext.id || "None"}
|
|
You can use this to understand what the user is currently looking at.
|
|
`
|
|
: "";
|
|
|
|
try {
|
|
const result = streamText({
|
|
// @ts-expect-error - AI SDK type mismatch
|
|
model: openrouter("google/gemini-3.0-flash"),
|
|
messages,
|
|
tools: activeTools,
|
|
// @ts-expect-error - AI SDK type mismatch with maxSteps
|
|
maxSteps: 10,
|
|
system: `You are a helpful Payload CMS Agent orchestrating the local Mintel ecosystem.
|
|
You only have access to tools explicitly granted by the Admin.
|
|
You can completely control Payload CMS (read, create, update, delete documents).
|
|
If you need more details to fulfill a request (e.g. creating a blog post), you can ask the user.
|
|
${contextContextStr}
|
|
${memorySystemPrompt}`,
|
|
});
|
|
|
|
return result.toTextStreamResponse();
|
|
} catch (error) {
|
|
console.error("AI Error:", error);
|
|
return Response.json(
|
|
{ error: "Failed to process AI request" },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
};
|