Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
BACKEND_URL=http://localhost:8000
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
GUARDRAILS_URL = http://localhost:8001
GUARDRAILS_TOKEN =
NEXT_PUBLIC_GOOGLE_CLIENT_ID=your-google-client-id.apps.googleusercontent.com
14 changes: 0 additions & 14 deletions app/(main)/coming-soon/guardrails/page.tsx

This file was deleted.

13 changes: 13 additions & 0 deletions app/(main)/configurations/prompt-editor/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ function PromptEditorContent() {
tools: config.tools || [],
},
},
...(config.input_guardrails?.length && {
input_guardrails: config.input_guardrails,
}),
...(config.output_guardrails?.length && {
output_guardrails: config.output_guardrails,
}),
});
setProvider(config.provider);
setTemperature(config.temperature);
Expand Down Expand Up @@ -294,6 +300,12 @@ function PromptEditorContent() {
}),
},
},
...(currentConfigBlob.input_guardrails?.length && {
input_guardrails: currentConfigBlob.input_guardrails,
}),
...(currentConfigBlob.output_guardrails?.length && {
output_guardrails: currentConfigBlob.output_guardrails,
}),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

const existingConfigMeta = allConfigMeta.find(
Expand Down Expand Up @@ -491,6 +503,7 @@ function PromptEditorContent() {
isSaving={isSaving}
collapsed={!showConfigPane}
onToggle={() => setShowConfigPane(!showConfigPane)}
apiKey={activeKey?.key ?? ""}
/>
</div>
</div>
Expand Down
225 changes: 225 additions & 0 deletions app/(main)/guardrails/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/**
* Guardrails — 2-panel layout:
* [LEFT: Config Form] | [RIGHT: Saved Configs List]
*/

"use client";

import { useState, useEffect, useCallback } from "react";
import Sidebar from "@/app/components/Sidebar";
import { useApp } from "@/app/lib/context/AppContext";
import { useAuth } from "@/app/lib/context/AuthContext";
import { useToast } from "@/app/components/Toast";
import { guardrailsFetch } from "@/app/lib/guardrailsClient";
import PageHeader from "@/app/components/PageHeader";
import {
Validator,
SavedValidatorConfig,
OrgContext,
} from "@/app/lib/types/guardrails";
import ValidatorConfigPanel from "@/app/components/guardrails/ValidatorConfigPanel";
import SavedConfigsList from "@/app/components/guardrails/SavedConfigsList";

export default function GuardrailsPage() {
const { sidebarCollapsed } = useApp();
const { isHydrated } = useAuth();
const toast = useToast();
const [orgContext, setOrgContext] = useState<OrgContext | null>(null);
const [validators, setValidators] = useState<Validator[]>([]);
const [validatorsLoading, setValidatorsLoading] = useState(true);
const [savedConfigs, setSavedConfigs] = useState<SavedValidatorConfig[]>([]);
const [savedConfigsLoading, setSavedConfigsLoading] = useState(true);
const [selectedValidatorType, setSelectedValidatorType] = useState<
string | null
>(null);
const [selectedSavedConfig, setSelectedSavedConfig] =
useState<SavedValidatorConfig | null>(null);
const [isSaving, setIsSaving] = useState(false);

useEffect(() => {
if (!isHydrated) return;
guardrailsFetch<{ data?: { organization_id: number; project_id: number } }>(
"/api/apikeys/verify", //need to change this in backend to /auth/verify
)
.then((data) => {
const org_id = data?.data?.organization_id;
const proj_id = data?.data?.project_id;
if (org_id != null && proj_id != null) {
setOrgContext({ organization_id: org_id, project_id: proj_id });
} else {
toast.error("Could not determine organization/project from session");
}
})
.catch((e: Error) =>
toast.error(e.message || "Session verification failed"),
);
}, [isHydrated]);

useEffect(() => {
setValidatorsLoading(true);
guardrailsFetch<{ validators?: Validator[] }>("/api/guardrails")
.then((data) => {
const list: Validator[] = Array.isArray(data?.validators)
? data.validators
: [];
setValidators(list);
})
.catch(() => toast.error("Failed to load validators"))
.finally(() => setValidatorsLoading(false));
}, []);

const configsQueryString = orgContext
? `?organization_id=${parseInt(String(orgContext.organization_id), 10)}&project_id=${parseInt(String(orgContext.project_id), 10)}`
: null;

const fetchSavedConfigs = useCallback(() => {
if (!configsQueryString) return;
setSavedConfigsLoading(true);
guardrailsFetch<{
data?: { configs?: SavedValidatorConfig[] } | SavedValidatorConfig[];
configs?: SavedValidatorConfig[];
}>(`/api/guardrails/validators/configs${configsQueryString}`)
.then((data) => {
const nested = data?.data;
const list: SavedValidatorConfig[] = Array.isArray(
(nested as { configs?: SavedValidatorConfig[] })?.configs,
)
? (nested as { configs: SavedValidatorConfig[] }).configs
: Array.isArray(nested)
? (nested as SavedValidatorConfig[])
: Array.isArray(data?.configs)
? data.configs!
: [];
setSavedConfigs(list);
})
.catch(() => toast.error("Failed to load saved configs"))
.finally(() => setSavedConfigsLoading(false));
}, [configsQueryString]);

useEffect(() => {
fetchSavedConfigs();
}, [fetchSavedConfigs]);

const handleSelectSavedConfig = (cfg: SavedValidatorConfig) => {
setSelectedSavedConfig(cfg);
setSelectedValidatorType(cfg.type);
};

const handleClearForm = () => {
setSelectedValidatorType(null);
setSelectedSavedConfig(null);
};

const handleDeleteConfig = async (configId: string) => {
if (!configsQueryString) return;
try {
await guardrailsFetch(
`/api/guardrails/validators/configs/${configId}${configsQueryString}`,
{ method: "DELETE" },
);
toast.success("Config deleted");
if (selectedSavedConfig?.id === configId) {
handleClearForm();
}
fetchSavedConfigs();
} catch {
toast.error("Failed to delete config");
}
};

const handleSaveConfig = async (
name: string,
configValues: Record<string, unknown>,
) => {
if (!name.trim()) {
toast.error("Please enter a config name");
return;
}
if (!configsQueryString) {
toast.error("API key not verified yet");
return;
}
setIsSaving(true);
try {
const isUpdate = !!selectedSavedConfig;
const base = `/api/guardrails/validators/configs`;
const url = isUpdate
? `${base}/${selectedSavedConfig!.id}${configsQueryString}`
: `${base}${configsQueryString}`;

const body = configValues;

await guardrailsFetch(url, {
method: isUpdate ? "PATCH" : "POST",
body: JSON.stringify(body),
});
toast.success(
isUpdate ? `Config "${name}" updated` : `Config "${name}" saved`,
);
fetchSavedConfigs();
setSelectedSavedConfig(null);
} catch (e) {
toast.error(e instanceof Error ? e.message : "Failed to save config");
} finally {
setIsSaving(false);
}
};

const existingValues = selectedSavedConfig
? (() => {
const {
id: _id,
name: _name,
type: _type,
config: _config,
created_at: _ca,
updated_at: _ua,
organization_id: _oid,
project_id: _pid,
...rest
} = selectedSavedConfig as SavedValidatorConfig &
Record<string, unknown>;
return rest;
})()
: null;

return (
<div className="w-full h-screen flex bg-bg-secondary">
<Sidebar collapsed={sidebarCollapsed} activeRoute="/guardrails" />

<div className="flex-1 flex flex-col overflow-hidden">
<PageHeader
title="Guardrails"
subtitle="Configure validators for safe and reliable AI"
/>

<div className="flex flex-1 overflow-hidden">
<div className="shrink-0 border-r border-border overflow-hidden w-[450px]">
<ValidatorConfigPanel
validators={validators}
validatorsLoading={validatorsLoading}
selectedType={selectedValidatorType}
onTypeChange={setSelectedValidatorType}
existingValues={existingValues}
existingName={selectedSavedConfig?.name}
isSaving={isSaving}
onSave={handleSaveConfig}
onClear={handleClearForm}
/>
</div>

<div className="flex-1 flex flex-col overflow-hidden">
<SavedConfigsList
configs={savedConfigs}
isLoading={savedConfigsLoading}
selectedConfigId={selectedSavedConfig?.id ?? null}
onSelectConfig={handleSelectSavedConfig}
onDeleteConfig={handleDeleteConfig}
onNewConfig={handleClearForm}
/>
</div>
</div>
</div>
</div>
);
}
14 changes: 14 additions & 0 deletions app/api/apikeys/verify/route.ts
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what the purpose of this route? apikeys/verify?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to get the organization id and project id of the used auth value, which now would be apikey and token both

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the /apiKey endpoint should be change /auth/verify.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we dont have auth/verify endpoint rn, and this would require changes in backend code as well as guardrail code, i will keep this as todo

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { apiClient } from "@/app/lib/apiClient";
import { NextResponse, NextRequest } from "next/server";

export async function GET(request: NextRequest) {
try {
const { status, data } = await apiClient(request, "/api/v1/apikeys/verify");
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : String(e) },
{ status: 500 },
);
}
}
67 changes: 67 additions & 0 deletions app/api/guardrails/ban_lists/[ban_list_id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { guardrailsUserClient } from "@/app/lib/guardrailsClient";
import { NextResponse, NextRequest } from "next/server";

export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ ban_list_id: string }> },
) {
try {
const { ban_list_id } = await params;
const { status, data } = await guardrailsUserClient(
request,
`/api/v1/guardrails/ban_lists/${ban_list_id}`,
);
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : String(e) },
{ status: 500 },
);
}
}

export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ ban_list_id: string }> },
) {
try {
const { ban_list_id } = await params;
const body = await request.json();
const { status, data } = await guardrailsUserClient(
request,
`/api/v1/guardrails/ban_lists/${ban_list_id}`,
{
method: "PUT",
body: JSON.stringify(body),
},
);
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : String(e) },
{ status: 500 },
);
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ ban_list_id: string }> },
) {
try {
const { ban_list_id } = await params;
const { status, data } = await guardrailsUserClient(
request,
`/api/v1/guardrails/ban_lists/${ban_list_id}`,
{
method: "DELETE",
},
);
return NextResponse.json(data, { status });
} catch (e: unknown) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : String(e) },
{ status: 500 },
);
}
}
Loading
Loading