diff --git a/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx b/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx
index 16a36d1ad5..206a068a78 100644
--- a/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx
+++ b/studio/frontend/src/features/recipe-studio/components/inline/inline-expression.tsx
@@ -9,6 +9,7 @@ import {
import type { ReactElement } from "react";
import { useRecipeStudioStore } from "../../stores/recipe-studio";
import type { ExpressionConfig, ExpressionDtype } from "../../types";
+import { findInvalidJinjaReferences } from "../../utils/refs";
import { getAvailableVariableEntries } from "../../utils/variables";
import { AvailableReferencesInline } from "../shared/available-references-inline";
import { InlineField } from "./inline-field";
@@ -26,6 +27,10 @@ export function InlineExpression({
}: InlineExpressionProps): ReactElement {
const configs = useRecipeStudioStore((state) => state.configs);
const vars = getAvailableVariableEntries(configs, config.id);
+ const invalidRefs = findInvalidJinjaReferences(
+ config.expr,
+ vars.map((entry) => entry.name),
+ );
return (
@@ -52,6 +57,7 @@ export function InlineExpression({
0}
placeholder="{{ column_name }}"
value={config.expr}
onChange={(event) => onUpdate({ expr: event.target.value })}
diff --git a/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx b/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx
index 2aa1988102..bbb5dead41 100644
--- a/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx
+++ b/studio/frontend/src/features/recipe-studio/components/recipe-graph-aux-node.tsx
@@ -13,6 +13,7 @@ import { useRecipeStudioStore } from "../stores/recipe-studio";
import type { LlmConfig, Score, ScoreOption } from "../types";
import { AUX_HANDLE_CLASS } from "../utils/handle-layout";
import { HANDLE_IDS } from "../utils/handles";
+import { findInvalidJinjaReferences } from "../utils/refs";
import { getAvailableVariableEntries } from "../utils/variables";
import { AvailableReferencesInline } from "./shared/available-references-inline";
import { BaseNode, BaseNodeContent, BaseNodeHeader, BaseNodeHeaderTitle } from "./rf-ui/base-node";
@@ -56,10 +57,12 @@ function updateOptionAt(
);
}
-function AuxVariableBadges({ llmId }: { llmId: string }): ReactElement | null {
- const configs = useRecipeStudioStore((state) => state.configs);
- const vars = getAvailableVariableEntries(configs, llmId);
- return ;
+function AuxVariableBadges({
+ entries,
+}: {
+ entries: ReturnType;
+}): ReactElement | null {
+ return ;
}
function AuxNodeBase({
@@ -67,6 +70,7 @@ function AuxNodeBase({
data,
}: NodeProps): ReactElement | null {
const config = useRecipeStudioStore((state) => state.configs[data.llmId]);
+ const configs = useRecipeStudioStore((state) => state.configs);
const updateConfig = useRecipeStudioStore((state) => state.updateConfig);
const updateNodeInternals = useUpdateNodeInternals();
@@ -117,6 +121,10 @@ function AuxNodeBase({
if (data.kind === "llm-prompt-input") {
const value = data.field === "prompt" ? config.prompt : config.system_prompt;
+ const variableEntries = getAvailableVariableEntries(configs, data.llmId);
+ const availableRefs = variableEntries.map((entry) => entry.name);
+ const hasInvalidRefs =
+ findInvalidJinjaReferences(value, availableRefs).length > 0;
return (
@@ -125,6 +133,7 @@ function AuxNodeBase({
{sourceHandles}
diff --git a/studio/frontend/src/features/recipe-studio/dialogs/expression/expression-dialog.tsx b/studio/frontend/src/features/recipe-studio/dialogs/expression/expression-dialog.tsx
index 76579eff38..c734f67fae 100644
--- a/studio/frontend/src/features/recipe-studio/dialogs/expression/expression-dialog.tsx
+++ b/studio/frontend/src/features/recipe-studio/dialogs/expression/expression-dialog.tsx
@@ -7,7 +7,11 @@ import {
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import type { ReactElement } from "react";
+import { useMemo } from "react";
+import { useRecipeStudioStore } from "../../stores/recipe-studio";
import type { ExpressionConfig, ExpressionDtype } from "../../types";
+import { findInvalidJinjaReferences } from "../../utils/refs";
+import { getAvailableVariables } from "../../utils/variables";
import { AvailableVariables } from "../shared/available-variables";
import { FieldLabel } from "../shared/field-label";
import { NameField } from "../shared/name-field";
@@ -23,8 +27,21 @@ export function ExpressionDialog({
config,
onUpdate,
}: ExpressionDialogProps): ReactElement {
+ const configs = useRecipeStudioStore((state) => state.configs);
const dtypeId = `${config.id}-dtype`;
const exprId = `${config.id}-expr`;
+ const validReferences = useMemo(
+ () => getAvailableVariables(configs, config.id),
+ [configs, config.id],
+ );
+ const invalidExprRefs = useMemo(
+ () => findInvalidJinjaReferences(config.expr, validReferences),
+ [config.expr, validReferences],
+ );
+ const invalidExprText = invalidExprRefs
+ .slice(0, 3)
+ .map((ref) => `{{ ${ref} }}`)
+ .join(", ");
const updateField = (
key: K,
value: ExpressionConfig[K],
@@ -71,10 +88,19 @@ export function ExpressionDialog({
);
diff --git a/studio/frontend/src/features/recipe-studio/utils/refs.ts b/studio/frontend/src/features/recipe-studio/utils/refs.ts
index 91f0e8e869..50f6c5b25f 100644
--- a/studio/frontend/src/features/recipe-studio/utils/refs.ts
+++ b/studio/frontend/src/features/recipe-studio/utils/refs.ts
@@ -1,4 +1,7 @@
const JINJA_REF_RE = /{{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g;
+const JINJA_EXPR_RE = /{{\s*([^{}]+?)\s*}}/g;
+const SIMPLE_JINJA_EXPR_RE = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
+const PLAIN_JINJA_EXPR_RE = /^[a-zA-Z0-9_.\s-]+$/;
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -17,6 +20,37 @@ export function extractRefs(template: string): string[] {
return Array.from(refs);
}
+export function findInvalidJinjaReferences(
+ template: string,
+ validReferences: string[],
+): string[] {
+ if (!template) {
+ return [];
+ }
+ const validSet = new Set(
+ validReferences.map((name) => name.trim()).filter(Boolean),
+ );
+ const invalid = new Set();
+
+ for (const match of template.matchAll(JINJA_EXPR_RE)) {
+ const expr = (match[1] ?? "").trim();
+ if (!expr) {
+ continue;
+ }
+ if (SIMPLE_JINJA_EXPR_RE.test(expr)) {
+ if (!validSet.has(expr)) {
+ invalid.add(expr);
+ }
+ continue;
+ }
+ if (PLAIN_JINJA_EXPR_RE.test(expr)) {
+ invalid.add(expr);
+ }
+ }
+
+ return Array.from(invalid);
+}
+
export function replaceRef(
template: string,
from: string,