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/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/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/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" }, - }} - /> - -
-
- -
-
- - -
-