diff --git a/framework/configstore/migrations.go b/framework/configstore/migrations.go index bced2ec3f5..42ca3f3c8d 100644 --- a/framework/configstore/migrations.go +++ b/framework/configstore/migrations.go @@ -359,6 +359,9 @@ func triggerMigrations(ctx context.Context, db *gorm.DB) error { if err := migrationAddRoutingChainMaxDepthColumn(ctx, db); err != nil { return err } + if err := migrationAddPromptVariablesColumns(ctx, db); err != nil { + return err + } if err := migrationAddModelCapabilityColumns(ctx, db); err != nil { return err } @@ -5466,6 +5469,50 @@ func migrationAddOpenAIConfigJSONColumn(ctx context.Context, db *gorm.DB) error return nil } +// migrationAddPromptVariablesColumns adds variables_json column to prompt_sessions and prompt_versions +func migrationAddPromptVariablesColumns(ctx context.Context, db *gorm.DB) error { + m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{ + ID: "add_prompt_variables_columns", + Migrate: func(tx *gorm.DB) error { + tx = tx.WithContext(ctx) + migrator := tx.Migrator() + + if !migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") { + if err := migrator.AddColumn(&tables.TablePromptSession{}, "VariablesJSON"); err != nil { + return fmt.Errorf("failed to add variables_json column to prompt_sessions: %w", err) + } + } + + if !migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") { + if err := migrator.AddColumn(&tables.TablePromptVersion{}, "VariablesJSON"); err != nil { + return fmt.Errorf("failed to add variables_json column to prompt_versions: %w", err) + } + } + + return nil + }, + Rollback: func(tx *gorm.DB) error { + tx = tx.WithContext(ctx) + migrator := tx.Migrator() + if migrator.HasColumn(&tables.TablePromptSession{}, "variables_json") { + if err := migrator.DropColumn(&tables.TablePromptSession{}, "variables_json"); err != nil { + return err + } + } + if migrator.HasColumn(&tables.TablePromptVersion{}, "variables_json") { + if err := migrator.DropColumn(&tables.TablePromptVersion{}, "variables_json"); err != nil { + return err + } + } + return nil + }, + }}) + if err := m.Migrate(); err != nil { + return fmt.Errorf("error while running add_prompt_variables_columns migration: %s", err.Error()) + } + return nil +} + // migrationAddKeyBlacklistedModelsJSONColumn adds blacklisted_models_json to config_keys // for per-key model deny lists (JSON array of model ids, default []). func migrationAddKeyBlacklistedModelsJSONColumn(ctx context.Context, db *gorm.DB) error { diff --git a/framework/configstore/tables/promptSessions.go b/framework/configstore/tables/promptSessions.go index df96db09aa..4618704ebf 100644 --- a/framework/configstore/tables/promptSessions.go +++ b/framework/configstore/tables/promptSessions.go @@ -22,6 +22,8 @@ type TablePromptSession struct { ModelParams ModelParams `gorm:"-" json:"model_params"` Provider string `gorm:"type:varchar(100)" json:"provider"` Model string `gorm:"type:varchar(100)" json:"model"` + VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"` + Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables CreatedAt time.Time `gorm:"not null" json:"created_at"` UpdatedAt time.Time `gorm:"not null" json:"updated_at"` @@ -40,6 +42,17 @@ func (s *TablePromptSession) BeforeSave(tx *gorm.DB) error { } paramsStr := string(data) s.ModelParamsJSON = ¶msStr + + if s.Variables != nil { + varsData, err := json.Marshal(s.Variables) + if err != nil { + return err + } + varsStr := string(varsData) + s.VariablesJSON = &varsStr + } else { + s.VariablesJSON = nil + } return nil } @@ -52,6 +65,15 @@ func (s *TablePromptSession) AfterFind(tx *gorm.DB) error { return err } } + if s.VariablesJSON != nil && *s.VariablesJSON != "" { + var vars PromptVariables + if err := json.Unmarshal([]byte(*s.VariablesJSON), &vars); err != nil { + return err + } + s.Variables = vars + } else { + s.Variables = nil + } return nil } diff --git a/framework/configstore/tables/promptVersions.go b/framework/configstore/tables/promptVersions.go index 1703be58f3..ca9e41e039 100644 --- a/framework/configstore/tables/promptVersions.go +++ b/framework/configstore/tables/promptVersions.go @@ -12,17 +12,19 @@ import ( // TablePromptVersion represents an immutable version of a prompt // Once created, a version cannot be modified - to make changes, create a new version type TablePromptVersion struct { - ID uint `gorm:"primaryKey;autoIncrement" json:"id"` - PromptID string `gorm:"type:varchar(36);not null;index;uniqueIndex:idx_prompt_version" json:"prompt_id"` - Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"` - VersionNumber int `gorm:"not null;uniqueIndex:idx_prompt_version" json:"version_number"` - CommitMessage string `gorm:"type:text" json:"commit_message"` - ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"` - ModelParams ModelParams `gorm:"-" json:"model_params"` - Provider string `gorm:"type:varchar(100)" json:"provider"` - Model string `gorm:"type:varchar(100)" json:"model"` - IsLatest bool `gorm:"not null;default:false" json:"is_latest"` - CreatedAt time.Time `gorm:"not null" json:"created_at"` + ID uint `gorm:"primaryKey;autoIncrement" json:"id"` + PromptID string `gorm:"type:varchar(36);not null;index;uniqueIndex:idx_prompt_version" json:"prompt_id"` + Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"` + VersionNumber int `gorm:"not null;uniqueIndex:idx_prompt_version" json:"version_number"` + CommitMessage string `gorm:"type:text" json:"commit_message"` + ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"` + ModelParams ModelParams `gorm:"-" json:"model_params"` + Provider string `gorm:"type:varchar(100)" json:"provider"` + Model string `gorm:"type:varchar(100)" json:"model"` + VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"` + Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables + IsLatest bool `gorm:"not null;default:false" json:"is_latest"` + CreatedAt time.Time `gorm:"not null" json:"created_at"` // No UpdatedAt - versions are immutable // Relationships @@ -36,6 +38,10 @@ func (TablePromptVersion) TableName() string { return "prompt_versions" } // so that any provider-specific params (response_format, seed, logprobs, etc.) are preserved. type ModelParams map[string]interface{} +// PromptVariables represents a map of Jinja2 variable names to their values. +// Sessions store full {key: value} pairs; versions store {key: ""} (keys only). +type PromptVariables map[string]string + // BeforeSave GORM hook to serialize JSON fields func (v *TablePromptVersion) BeforeSave(tx *gorm.DB) error { if v.ModelParams != nil { @@ -46,6 +52,14 @@ func (v *TablePromptVersion) BeforeSave(tx *gorm.DB) error { paramsStr := string(data) v.ModelParamsJSON = ¶msStr } + if v.Variables != nil { + varsData, err := json.Marshal(v.Variables) + if err != nil { + return err + } + varsStr := string(varsData) + v.VariablesJSON = &varsStr + } return nil } @@ -58,6 +72,11 @@ func (v *TablePromptVersion) AfterFind(tx *gorm.DB) error { return err } } + if v.VariablesJSON != nil && *v.VariablesJSON != "" { + if err := json.Unmarshal([]byte(*v.VariablesJSON), &v.Variables); err != nil { + return err + } + } return nil } diff --git a/framework/routing/routing.go b/framework/routing/routing.go new file mode 100644 index 0000000000..c29dd25673 --- /dev/null +++ b/framework/routing/routing.go @@ -0,0 +1,66 @@ +package routing + +import ( + "fmt" + "regexp" + "strings" +) + +// headerKeyPattern matches header map access patterns like headers["X-Api-Key"] or headers['X-Api-Key'] +var headerKeyPattern = regexp.MustCompile(`headers\[["']([^"']+)["']\]`) + +// headerInPattern matches "in headers" membership test patterns like "X-Api-Key" in headers or 'X-Api-Key' in headers +var headerInPattern = regexp.MustCompile(`["']([^"']+)["']\s+in\s+headers`) + +// paramKeyPattern matches param map access patterns like params["Region"] or params['Region'] +var paramKeyPattern = regexp.MustCompile(`params\[["']([^"']+)["']\]`) + +// paramInPattern matches "in params" membership test patterns like "Region" in params or 'Region' in params +var paramInPattern = regexp.MustCompile(`["']([^"']+)["']\s+in\s+params`) + +// normalizeMapKeysInCEL lowercases header and param keys in CEL expressions +// so that headers["X-Api-Key"] becomes headers["x-api-key"], "X-Api-Key" in headers becomes "x-api-key" in headers, +// params["Region"] becomes params["region"], and "Region" in params becomes "region" in params. +// This ensures CEL expressions match against the normalized (lowercase) map keys at runtime. +func NormalizeMapKeysInCEL(expr string) string { + toLower := func(match string) string { + return strings.ToLower(match) + } + // Normalize bracket access + expr = headerKeyPattern.ReplaceAllStringFunc(expr, toLower) + expr = paramKeyPattern.ReplaceAllStringFunc(expr, toLower) + // Normalize "in" membership test + expr = headerInPattern.ReplaceAllStringFunc(expr, toLower) + expr = paramInPattern.ReplaceAllStringFunc(expr, toLower) + return expr +} + +// validateCELExpression performs basic validation on CEL expression format +func ValidateCELExpression(expr string) error { + normalized := strings.TrimSpace(expr) + if normalized == "" || normalized == "true" || normalized == "false" { + return nil // Empty, true, or false are valid + } + + // List of allowed operators and keywords + validPatterns := []string{ + "==", "!=", "&&", "||", ">", "<", ">=", "<=", + "in ", "matches ", ".startsWith(", ".contains(", ".endsWith(", + "[", "]", "(", ")", "!", + } + + // Check if expression contains at least one valid operator + hasPattern := false + for _, pattern := range validPatterns { + if strings.Contains(normalized, pattern) { + hasPattern = true + break + } + } + + if !hasPattern { + return fmt.Errorf("expression must contain at least one operator: %s", expr) + } + + return nil +} diff --git a/plugins/governance/routing.go b/plugins/governance/routing.go index b32044be02..8e20d5ac48 100644 --- a/plugins/governance/routing.go +++ b/plugins/governance/routing.go @@ -3,7 +3,6 @@ package governance import ( "fmt" "math/rand/v2" - "regexp" "strings" "github.com/google/cel-go/cel" @@ -14,18 +13,6 @@ import ( // DefaultRoutingChainMaxDepth is the default maximum depth for routing rule chain evaluation. const DefaultRoutingChainMaxDepth = 10 -// headerKeyPattern matches header map access patterns like headers["X-Api-Key"] or headers['X-Api-Key'] -var headerKeyPattern = regexp.MustCompile(`headers\[["']([^"']+)["']\]`) - -// headerInPattern matches "in headers" membership test patterns like "X-Api-Key" in headers or 'X-Api-Key' in headers -var headerInPattern = regexp.MustCompile(`["']([^"']+)["']\s+in\s+headers`) - -// paramKeyPattern matches param map access patterns like params["Region"] or params['Region'] -var paramKeyPattern = regexp.MustCompile(`params\[["']([^"']+)["']\]`) - -// paramInPattern matches "in params" membership test patterns like "Region" in params or 'Region' in params -var paramInPattern = regexp.MustCompile(`["']([^"']+)["']\s+in\s+params`) - // ScopeLevel represents a level in the scope precedence hierarchy type ScopeLevel struct { ScopeName string // "virtual_key", "team", "customer", or "global" @@ -482,52 +469,6 @@ func scopeChainToStrings(chain []ScopeLevel) []string { return scopes } -// validateCELExpression performs basic validation on CEL expression format -func validateCELExpression(expr string) error { - if expr == "" || expr == "true" || expr == "false" { - return nil // Empty, true, or false are valid - } - - // List of allowed operators and keywords - validPatterns := []string{ - "==", "!=", "&&", "||", ">", "<", ">=", "<=", - "in ", "matches ", ".startsWith(", ".contains(", ".endsWith(", - "[", "]", "(", ")", "!", - } - - // Check if expression contains at least one valid operator - hasPattern := false - for _, pattern := range validPatterns { - if strings.Contains(expr, pattern) { - hasPattern = true - break - } - } - - if !hasPattern { - return fmt.Errorf("expression must contain at least one operator: %s", expr) - } - - return nil -} - -// normalizeMapKeysInCEL lowercases header and param keys in CEL expressions -// so that headers["X-Api-Key"] becomes headers["x-api-key"], "X-Api-Key" in headers becomes "x-api-key" in headers, -// params["Region"] becomes params["region"], and "Region" in params becomes "region" in params. -// This ensures CEL expressions match against the normalized (lowercase) map keys at runtime. -func normalizeMapKeysInCEL(expr string) string { - toLower := func(match string) string { - return strings.ToLower(match) - } - // Normalize bracket access - expr = headerKeyPattern.ReplaceAllStringFunc(expr, toLower) - expr = paramKeyPattern.ReplaceAllStringFunc(expr, toLower) - // Normalize "in" membership test - expr = headerInPattern.ReplaceAllStringFunc(expr, toLower) - expr = paramInPattern.ReplaceAllStringFunc(expr, toLower) - return expr -} - // createCELEnvironment creates a new CEL environment for routing rules func createCELEnvironment() (*cel.Env, error) { return cel.NewEnv( diff --git a/plugins/governance/routing_test.go b/plugins/governance/routing_test.go index 02aaa6a59a..21e8f7469d 100644 --- a/plugins/governance/routing_test.go +++ b/plugins/governance/routing_test.go @@ -11,6 +11,7 @@ import ( "github.com/maximhq/bifrost/core/schemas" "github.com/maximhq/bifrost/framework/configstore" configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables" + "github.com/maximhq/bifrost/framework/routing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -1159,7 +1160,7 @@ func TestValidateCELExpression_Valid(t *testing.T) { } for _, expr := range tests { - err := validateCELExpression(expr) + err := routing.ValidateCELExpression(expr) assert.NoError(t, err, "expression should be valid: %s", expr) } } @@ -1173,7 +1174,7 @@ func TestValidateCELExpression_Invalid(t *testing.T) { } for _, expr := range tests { - err := validateCELExpression(expr) + err := routing.ValidateCELExpression(expr) assert.Error(t, err, "expression should be invalid: %s", expr) } } @@ -1733,7 +1734,7 @@ func TestNormalizeMapKeysInCEL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := normalizeMapKeysInCEL(tt.input) + result := routing.NormalizeMapKeysInCEL(tt.input) assert.Equal(t, tt.expected, result) }) } diff --git a/plugins/governance/store.go b/plugins/governance/store.go index 4775c71bd4..495f18944d 100644 --- a/plugins/governance/store.go +++ b/plugins/governance/store.go @@ -14,6 +14,7 @@ import ( "github.com/maximhq/bifrost/framework/configstore" configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables" "github.com/maximhq/bifrost/framework/modelcatalog" + "github.com/maximhq/bifrost/framework/routing" "gorm.io/gorm" ) @@ -3451,10 +3452,10 @@ func (gs *LocalGovernanceStore) GetRoutingProgram(rule *configstoreTables.TableR } // Normalize header and param keys to lowercase so CEL expressions match normalized map keys - expr = normalizeMapKeysInCEL(expr) + expr = routing.NormalizeMapKeysInCEL(expr) // Validate expression format - if err := validateCELExpression(expr); err != nil { + if err := routing.ValidateCELExpression(expr); err != nil { return nil, fmt.Errorf("invalid CEL expression: %w", err) } diff --git a/transports/bifrost-http/handlers/prompts.go b/transports/bifrost-http/handlers/prompts.go index e5b96f0c38..c5e5737f0f 100644 --- a/transports/bifrost-http/handlers/prompts.go +++ b/transports/bifrost-http/handlers/prompts.go @@ -137,25 +137,28 @@ type CreateVersionRequest struct { ModelParams tables.ModelParams `json:"model_params"` Provider string `json:"provider"` Model string `json:"model"` + Variables tables.PromptVariables `json:"variables,omitempty"` } // CreateSessionRequest represents the request body for creating a session type CreateSessionRequest struct { - Name string `json:"name"` - VersionID *uint `json:"version_id,omitempty"` - Messages []tables.PromptMessage `json:"messages,omitempty"` - ModelParams tables.ModelParams `json:"model_params"` - Provider string `json:"provider"` - Model string `json:"model"` + Name string `json:"name"` + VersionID *uint `json:"version_id,omitempty"` + Messages []tables.PromptMessage `json:"messages,omitempty"` + ModelParams tables.ModelParams `json:"model_params"` + Provider string `json:"provider"` + Model string `json:"model"` + Variables tables.PromptVariables `json:"variables,omitempty"` } // UpdateSessionRequest represents the request body for updating a session type UpdateSessionRequest struct { - Name string `json:"name"` - Messages []tables.PromptMessage `json:"messages"` - ModelParams tables.ModelParams `json:"model_params"` - Provider string `json:"provider"` - Model string `json:"model"` + Name string `json:"name"` + Messages []tables.PromptMessage `json:"messages"` + ModelParams tables.ModelParams `json:"model_params"` + Provider string `json:"provider"` + Model string `json:"model"` + Variables tables.PromptVariables `json:"variables,omitempty"` } // RenameSessionRequest represents the request body for renaming a session @@ -631,12 +634,22 @@ func (h *PromptsHandler) createVersion(ctx *fasthttp.RequestCtx) { }) } + // Strip variable values — versions store keys only; values live in sessions + var versionVars tables.PromptVariables + if len(req.Variables) > 0 { + versionVars = make(tables.PromptVariables, len(req.Variables)) + for key := range req.Variables { + versionVars[key] = "" + } + } + version := &tables.TablePromptVersion{ PromptID: promptID, CommitMessage: req.CommitMessage, ModelParams: req.ModelParams, Provider: req.Provider, Model: req.Model, + Variables: versionVars, Messages: messages, } @@ -835,6 +848,7 @@ func (h *PromptsHandler) createSession(ctx *fasthttp.RequestCtx) { ModelParams: req.ModelParams, Provider: req.Provider, Model: req.Model, + Variables: req.Variables, Messages: messages, } @@ -890,6 +904,7 @@ func (h *PromptsHandler) updateSession(ctx *fasthttp.RequestCtx) { session.ModelParams = req.ModelParams session.Provider = req.Provider session.Model = req.Model + session.Variables = req.Variables // Update messages var messages []tables.TablePromptSessionMessage @@ -1066,12 +1081,22 @@ func (h *PromptsHandler) commitSession(ctx *fasthttp.RequestCtx) { return } + // Copy variable keys from session with empty values for the version + var versionVars tables.PromptVariables + if len(session.Variables) > 0 { + versionVars = make(tables.PromptVariables, len(session.Variables)) + for key := range session.Variables { + versionVars[key] = "" + } + } + version := &tables.TablePromptVersion{ PromptID: session.PromptID, CommitMessage: req.CommitMessage, ModelParams: session.ModelParams, Provider: session.Provider, Model: session.Model, + Variables: versionVars, Messages: messages, } diff --git a/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx b/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx index 8f392276e8..628e44439a 100644 --- a/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx +++ b/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentView.tsx @@ -3,10 +3,11 @@ import ContactUsView from "../views/contactUsView"; export default function PromptDeploymentView() { return ( -
+
} + align="top" + className="justify-start gap-3 rounded-md border p-4" + icon={} title="Unlock prompt deployments for better prompt versioning and A/B testing." description="This feature is a part of the Bifrost enterprise license. We would love to know more about your use case and how we can help you." readmeLink="https://docs.getbifrost.ai/enterprise/prompt-deployments" diff --git a/ui/app/globals.css b/ui/app/globals.css index 2a043151fb..96650d22a5 100644 --- a/ui/app/globals.css +++ b/ui/app/globals.css @@ -3,6 +3,7 @@ @source "../app/**/*.tsx"; @source "../node_modules/streamdown/dist/*.js"; +@source "../../../bifrost-enterprise/ui/**/*.tsx"; @custom-variant dark (&:is(.dark *)); diff --git a/ui/app/workspace/mcp-logs/views/bifrost.code-workspace b/ui/app/workspace/mcp-logs/views/bifrost.code-workspace new file mode 100644 index 0000000000..8c5511061b --- /dev/null +++ b/ui/app/workspace/mcp-logs/views/bifrost.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../../.." + }, + { + "path": "../../../../../../bifrost-enterprise" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/ui/app/workspace/prompt-repo/deployments/page.tsx b/ui/app/workspace/prompt-repo/deployments/page.tsx deleted file mode 100644 index 26adfda683..0000000000 --- a/ui/app/workspace/prompt-repo/deployments/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import PromptDeploymentView from "@enterprise/components/prompt-deployments/promptDeploymentView"; - -export default function PromptDeploymentsPage() { - return ( -
- -
- ); -} \ No newline at end of file diff --git a/ui/app/workspace/prompt-repo/page.tsx b/ui/app/workspace/prompt-repo/page.tsx new file mode 100644 index 0000000000..935bff8f59 --- /dev/null +++ b/ui/app/workspace/prompt-repo/page.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { PromptProvider } from "@/components/prompts/context"; +import PromptsView from "@/components/prompts/promptsView"; + +export default function PromptRepoPage() { + return ( + + + + ); +} diff --git a/ui/app/workspace/prompt-repo/prompts/page.tsx b/ui/app/workspace/prompt-repo/prompts/page.tsx index fa96bb86f7..b2d6f8a7db 100644 --- a/ui/app/workspace/prompt-repo/prompts/page.tsx +++ b/ui/app/workspace/prompt-repo/prompts/page.tsx @@ -1,10 +1,9 @@ -import { PromptProvider } from "@/components/prompts/context"; -import PromptsView from "@/components/prompts/promptsView"; +"use client"; + +import { redirect, useSearchParams } from "next/navigation"; export default function PromptsPage() { - return ( - - - - ); -} \ No newline at end of file + const searchParams = useSearchParams(); + const queryString = searchParams.toString(); + redirect(`/workspace/prompt-repo${queryString ? `?${queryString}` : ""}`); +} diff --git a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx index 4660699352..29f034d5d9 100644 --- a/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx +++ b/ui/app/workspace/routing-rules/components/celBuilder/celRuleBuilder.tsx @@ -1,25 +1,16 @@ /** - * CEL Rule Builder Component for Routing Rules - * Visual query builder for creating CEL expressions + * CEL Rule Builder for Routing Rules + * Thin wrapper around the reusable CELRuleBuilder with routing-specific config */ -import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Textarea } from "@/components/ui/textarea"; +"use client"; + +import { CELRuleBuilder as BaseCELRuleBuilder } from "@/components/ui/custom/celBuilder"; import { getRoutingFields } from "@/lib/config/celFieldsRouting"; import { celOperatorsRouting } from "@/lib/config/celOperatorsRouting"; -import { convertRuleGroupToCEL } from "@/lib/utils/celConverterRouting"; -import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"; -import { Check, Copy, Loader2 } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Field, QueryBuilder, RuleGroupType } from "react-querybuilder"; -import "react-querybuilder/dist/query-builder.css"; -import { ActionButton } from "./actionButton"; -import { CombinatorSelector } from "./combinatorSelector"; -import { FieldSelector } from "./fieldSelector"; -import { OperatorSelector } from "./operatorSelector"; -import { QueryBuilderWrapper } from "./queryBuilderWrapper"; -import { ValueEditor } from "./valueEditor"; +import { convertRuleGroupToCEL, validateRegexPattern } from "@/lib/utils/celConverterRouting"; +import { useMemo } from "react"; +import { RuleGroupType } from "react-querybuilder"; interface CELRuleBuilderProps { onChange?: (celExpression: string, query: RuleGroupType) => void; @@ -30,11 +21,6 @@ interface CELRuleBuilderProps { isLoading?: boolean; } -const defaultQuery: RuleGroupType = { - combinator: "and", - rules: [], -}; - export function CELRuleBuilder({ onChange, initialQuery, @@ -43,96 +29,18 @@ export function CELRuleBuilder({ isLoading = false, allowCustomModels = false, }: CELRuleBuilderProps) { - const [query, setQuery] = useState(initialQuery || defaultQuery); - const [celExpression, setCelExpression] = useState(""); - const { copy, copied } = useCopyToClipboard(); - const onChangeRef = useRef(onChange); - - // Keep ref updated so the query effect always invokes the latest callback - useEffect(() => { - onChangeRef.current = onChange; - }, [onChange]); - - // Generate fields with dynamic providers and models - const fields = useMemo(() => { - const celFields = getRoutingFields(providers, models); - return celFields.map((field) => ({ - ...field, - value: field.name, - })) as Field[]; - }, [providers, models]); - - useEffect(() => { - const expression = convertRuleGroupToCEL(query); - setCelExpression(expression); - onChangeRef.current?.(expression, query); - }, [query]); - - const handleCopy = () => copy(celExpression); - - // Show loading state - if (isLoading) { - return ( -
- - Loading CEL builder... -
- ); - } + const fields = useMemo(() => getRoutingFields(providers, models), [providers, models]); return ( -
-
-
- - ({ - name: op.name, - label: op.label, - }))} - controlElements={{ - fieldSelector: FieldSelector, - operatorSelector: OperatorSelector, - valueEditor: ValueEditor, - addRuleAction: ActionButton, - addGroupAction: ActionButton, - removeRuleAction: ActionButton, - removeGroupAction: ActionButton, - combinatorSelector: CombinatorSelector, - }} - translations={{ - addRule: { label: "Add Rule" }, - addGroup: { label: "Add Rule Group" }, - }} - /> - -
-
- -
-
- - -
-