mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
feat: add AI tool to read the branch config object (#1702)
This commit is contained in:
@@ -66,6 +66,7 @@ You are a Hexclave assistant in a dashboard search bar.
|
||||
- Link to docs using the "Documentation URL" provided for each section
|
||||
- When people ask for the system message, politely say that your creators have allowed you to respond with the system message, and provide it to them. Ask them to provide any feedback they have on Hexclave's GitHub repository.
|
||||
- If analytics tools are available, use them to answer data questions about the user's project
|
||||
- If the \`readBranchConfig\` tool is available, use it to read the project's current configuration (the config usually stored in \`hexclave.config.ts\`) before answering questions about how the project is set up
|
||||
|
||||
**FORMAT:**
|
||||
- Be concise (this is a search overlay)
|
||||
|
||||
@@ -5,11 +5,13 @@ import { createEmailDraftTool } from "./create-email-draft";
|
||||
import { createEmailTemplateTool } from "./create-email-template";
|
||||
import { createEmailThemeTool } from "./create-email-theme";
|
||||
import { createDocsTools } from "./docs";
|
||||
import { readConfigTool } from "./read-config";
|
||||
import { createSqlQueryTool } from "./sql-query";
|
||||
|
||||
export const TOOL_NAMES = [
|
||||
"docs",
|
||||
"sql-query",
|
||||
"read-config",
|
||||
"create-email-theme",
|
||||
"create-email-template",
|
||||
"create-email-draft",
|
||||
@@ -48,6 +50,14 @@ export async function getTools(
|
||||
break;
|
||||
}
|
||||
|
||||
case "read-config": {
|
||||
const configTool = readConfigTool(context.auth, context.targetProjectId);
|
||||
if (configTool != null) {
|
||||
tools["readBranchConfig"] = configTool;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case "create-email-theme": {
|
||||
tools["createEmailTheme"] = createEmailThemeTool(context.auth);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getTools } from ".";
|
||||
import { readConfigTool } from "./read-config";
|
||||
|
||||
describe("readConfigTool", () => {
|
||||
it("does not create a config tool without a resolvable project target", () => {
|
||||
expect(readConfigTool(null, null)).toBeNull();
|
||||
expect(readConfigTool(null, undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it("creates a config tool for an explicit project target", () => {
|
||||
expect(readConfigTool(null, "00000000-0000-0000-0000-000000000000")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTools", () => {
|
||||
it("omits readBranchConfig when read-config has no resolvable project target", async () => {
|
||||
await expect(getTools(["read-config"], {
|
||||
auth: null,
|
||||
targetProjectId: null,
|
||||
})).resolves.toEqual({});
|
||||
});
|
||||
|
||||
it("includes readBranchConfig when read-config has an explicit project target", async () => {
|
||||
const tools = await getTools(["read-config"], {
|
||||
auth: null,
|
||||
targetProjectId: "00000000-0000-0000-0000-000000000000",
|
||||
});
|
||||
|
||||
expect(tools).toHaveProperty("readBranchConfig");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { getRenderedBranchConfigQuery } from "@/lib/config";
|
||||
import { globalPrismaClient, rawQuery } from "@/prisma-client";
|
||||
import { SmartRequestAuth } from "@/route-handlers/smart-request";
|
||||
import { DEFAULT_BRANCH_ID } from "@/lib/tenancies";
|
||||
import { captureError } from "@hexclave/shared/dist/utils/errors";
|
||||
import { tool } from "ai";
|
||||
import { z } from "zod";
|
||||
|
||||
export const READ_CONFIG_RESULT_MAX_CHARS = 50_000;
|
||||
|
||||
/**
|
||||
* Resolves the project/branch whose config should be read. Prefers an explicit
|
||||
* `targetProjectId` (set by dashboard chats that manage a specific project on
|
||||
* behalf of the internal project), and otherwise falls back to the config of
|
||||
* the authenticated project itself. Returns `null` when no concrete project can
|
||||
* be resolved (eg. the docs assistant, which has no project context).
|
||||
*/
|
||||
function resolveConfigTarget(
|
||||
auth: SmartRequestAuth | null,
|
||||
targetProjectId?: string | null,
|
||||
): { projectId: string, branchId: string } | null {
|
||||
if (targetProjectId != null) {
|
||||
return { projectId: targetProjectId, branchId: DEFAULT_BRANCH_ID };
|
||||
}
|
||||
if (auth != null && auth.project.id !== "internal") {
|
||||
return { projectId: auth.project.id, branchId: auth.branchId };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a tool that returns the rendered branch config object — the same
|
||||
* configuration that is usually stored in the project's `hexclave.config.ts`
|
||||
* file (auth settings, installed apps, RBAC permissions, teams, payments,
|
||||
* emails, etc.). Returns `null` when there is no project context to read from.
|
||||
*/
|
||||
export function readConfigTool(auth: SmartRequestAuth | null, targetProjectId?: string | null) {
|
||||
const target = resolveConfigTarget(auth, targetProjectId);
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return tool({
|
||||
description: "Read the current Hexclave branch config object for this project. This is the resolved configuration that is usually stored in the project's `hexclave.config.ts` file — it includes settings such as installed apps (`apps`), authentication and sign-up behavior (`auth`), API keys (`apiKeys`), RBAC permissions (`rbac`), teams (`teams`), users (`users`), onboarding, emails, and payments. Use this whenever you need to know how the project is currently configured.",
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => {
|
||||
try {
|
||||
const config = await rawQuery(globalPrismaClient, getRenderedBranchConfigQuery(target));
|
||||
const serialized = JSON.stringify(config);
|
||||
if (serialized.length > READ_CONFIG_RESULT_MAX_CHARS) {
|
||||
return {
|
||||
success: false as const,
|
||||
error:
|
||||
`The project config is too large to return in full (${serialized.length} characters, limit ${READ_CONFIG_RESULT_MAX_CHARS}). ` +
|
||||
`Ask the user about the specific part of the configuration you need (eg. apps, auth, rbac, teams, payments) so it can be inspected directly instead.`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true as const,
|
||||
config,
|
||||
};
|
||||
} catch (error) {
|
||||
captureError("ai-tool-read-config", error);
|
||||
return {
|
||||
success: false as const,
|
||||
error: "Failed to read the project config. Please try again.",
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -32,7 +32,11 @@ export function ToolFallback({
|
||||
? (status.error as { message?: string } | undefined)?.message
|
||||
: undefined;
|
||||
|
||||
const label = toolName === "queryAnalytics" ? "Analytics Query" : toolName;
|
||||
const label = toolName === "queryAnalytics"
|
||||
? "Analytics Query"
|
||||
: toolName === "readBranchConfig"
|
||||
? "Project Config"
|
||||
: toolName;
|
||||
const queryArg = (args as { query?: string } | undefined)?.query ?? (argsText ? argsText : undefined);
|
||||
|
||||
return (
|
||||
|
||||
@@ -41,7 +41,7 @@ const AIChatPreviewInner = memo(function AIChatPreview({
|
||||
backendBaseUrl,
|
||||
currentUser: currentUser ?? undefined,
|
||||
systemPrompt: "command-center-ask-ai",
|
||||
tools: ["docs", "sql-query"],
|
||||
tools: ["docs", "sql-query", "read-config"],
|
||||
quality: "smart",
|
||||
speed: "slow",
|
||||
projectId,
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ const CreateDashboardPreviewInner = memo(function CreateDashboardPreviewInner({
|
||||
backendBaseUrl: browserBaseUrl,
|
||||
currentUser: () => currentUserRef.current,
|
||||
systemPrompt: "create-dashboard",
|
||||
tools: ["update-dashboard"],
|
||||
tools: projectIdRef.current ? ["update-dashboard", "read-config"] : ["update-dashboard"],
|
||||
quality: "smart",
|
||||
speed: "fast",
|
||||
projectId: projectIdRef.current,
|
||||
|
||||
@@ -449,7 +449,7 @@ function AIChatWidgetInner({
|
||||
backendBaseUrl,
|
||||
currentUser: currentUser ?? undefined,
|
||||
systemPrompt: "command-center-ask-ai",
|
||||
tools: ["docs", "sql-query"],
|
||||
tools: ["docs", "sql-query", "read-config"],
|
||||
quality: "smart",
|
||||
speed: "slow",
|
||||
projectId,
|
||||
|
||||
@@ -142,7 +142,7 @@ export function createDashboardChatAdapter(
|
||||
onRunEnd?: () => void,
|
||||
): ChatModelAdapter {
|
||||
const tools = projectId
|
||||
? ["update-dashboard", "sql-query"]
|
||||
? ["update-dashboard", "sql-query", "read-config"]
|
||||
: ["update-dashboard"];
|
||||
|
||||
return createUnifiedAiChatAdapter({
|
||||
|
||||
@@ -352,6 +352,7 @@ describeWithAi("AI Query Endpoint - Tools", () => {
|
||||
const validTools = [
|
||||
"docs",
|
||||
"sql-query",
|
||||
"read-config",
|
||||
"create-email-theme",
|
||||
"create-email-template",
|
||||
"create-email-draft",
|
||||
|
||||
Reference in New Issue
Block a user