diff --git a/framework/logstore/migrations.go b/framework/logstore/migrations.go index eccedbd02a..2764758650 100644 --- a/framework/logstore/migrations.go +++ b/framework/logstore/migrations.go @@ -233,6 +233,9 @@ func triggerMigrations(ctx context.Context, db *gorm.DB) error { if err := migrationAddAttemptTrailColumn(ctx, db); err != nil { return err } + if err := migrationAddSelectedPromptColumns(ctx, db); err != nil { + return err + } return nil } @@ -2499,3 +2502,43 @@ func migrationAddAttemptTrailColumn(ctx context.Context, db *gorm.DB) error { return nil } +// migrationAddSelectedPromptColumns adds selected_prompt_name, selected_prompt_version, selected_prompt_id for logs UI. +func migrationAddSelectedPromptColumns(ctx context.Context, db *gorm.DB) error { + opts := *migrator.DefaultOptions + opts.UseTransaction = true + + columns := []string{"selected_prompt_name", "selected_prompt_version", "selected_prompt_id"} + + m := migrator.New(db, &opts, []*migrator.Migration{{ + ID: "logs_add_selected_prompt_columns", + Migrate: func(tx *gorm.DB) error { + tx = tx.WithContext(ctx) + mig := tx.Migrator() + for _, col := range columns { + if !mig.HasColumn(&Log{}, col) { + if err := mig.AddColumn(&Log{}, col); err != nil { + return err + } + } + } + return nil + }, + Rollback: func(tx *gorm.DB) error { + tx = tx.WithContext(ctx) + mig := tx.Migrator() + for _, col := range columns { + if mig.HasColumn(&Log{}, col) { + if err := mig.DropColumn(&Log{}, col); err != nil { + return err + } + } + } + return nil + }, + }}) + err := m.Migrate() + if err != nil { + return fmt.Errorf("error while adding selected prompt columns: %s", err.Error()) + } + return nil +} diff --git a/framework/logstore/tables.go b/framework/logstore/tables.go index f33c118008..c38c2bba43 100644 --- a/framework/logstore/tables.go +++ b/framework/logstore/tables.go @@ -121,6 +121,9 @@ type Log struct { RoutingEnginesUsedStr *string `gorm:"type:varchar(255);column:routing_engines_used" json:"-"` // Comma-separated routing engines RoutingRuleID *string `gorm:"type:varchar(255);index:idx_logs_routing_rule_id" json:"routing_rule_id"` RoutingRuleName *string `gorm:"type:varchar(255)" json:"routing_rule_name"` + SelectedPromptName *string `gorm:"type:varchar(255)" json:"selected_prompt_name"` + SelectedPromptVersion *string `gorm:"type:varchar(64)" json:"selected_prompt_version"` + SelectedPromptID *string `gorm:"type:varchar(36)" json:"selected_prompt_id"` UserID *string `gorm:"type:varchar(255);index:idx_logs_user_id" json:"user_id"` TeamID *string `gorm:"type:varchar(255);index:idx_logs_team_id" json:"team_id"` TeamName *string `gorm:"type:varchar(255)" json:"team_name"` diff --git a/plugins/logging/main.go b/plugins/logging/main.go index 6fdb05b468..ba4229d9a3 100644 --- a/plugins/logging/main.go +++ b/plugins/logging/main.go @@ -681,6 +681,9 @@ func (p *LoggerPlugin) PostLLMHook(ctx *schemas.BifrostContext, result *schemas. virtualKeyName := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeyGovernanceVirtualKeyName) routingRuleID := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeyGovernanceRoutingRuleID) routingRuleName := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeyGovernanceRoutingRuleName) + selectedPromptName := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeySelectedPromptName) + selectedPromptVersion := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeySelectedPromptVersion) + selectedPromptID := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeySelectedPromptID) teamID := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeyGovernanceTeamID) teamName := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeyGovernanceTeamName) customerID := bifrost.GetStringFromContext(ctx, schemas.BifrostContextKeyGovernanceCustomerID) @@ -765,7 +768,7 @@ func (p *LoggerPlugin) PostLLMHook(ctx *schemas.BifrostContext, result *schemas. if result != nil { latency = result.GetExtraFields().Latency } - applyOutputFieldsToEntry(entry, selectedKeyID, selectedKeyName, virtualKeyID, virtualKeyName, routingRuleID, routingRuleName, teamID, teamName, customerID, customerName, userID, businessUnitID, businessUnitName, numberOfRetries, latency, attemptTrail) + applyOutputFieldsToEntry(entry, selectedKeyID, selectedKeyName, virtualKeyID, virtualKeyName, routingRuleID, routingRuleName, selectedPromptID, selectedPromptName, selectedPromptVersion, teamID, teamName, customerID, customerName, userID, businessUnitID, businessUnitName, numberOfRetries, latency, attemptTrail) entry.MetadataParsed = pending.InitialData.Metadata entry.MetadataParsed = mergeRealtimeMetadata(entry.MetadataParsed, ctx) entry.RoutingEngineLogs = routingEngineLogs diff --git a/plugins/logging/writer.go b/plugins/logging/writer.go index 0cee7f778d..2aa4417e74 100644 --- a/plugins/logging/writer.go +++ b/plugins/logging/writer.go @@ -353,6 +353,7 @@ func applyOutputFieldsToEntry( selectedKeyID, selectedKeyName string, virtualKeyID, virtualKeyName string, routingRuleID, routingRuleName string, + selectedPromptID, selectedPromptName, selectedPromptVersion string, teamID, teamName string, customerID, customerName string, userID string, @@ -375,6 +376,15 @@ func applyOutputFieldsToEntry( if routingRuleName != "" { entry.RoutingRuleName = &routingRuleName } + if selectedPromptID != "" { + entry.SelectedPromptID = &selectedPromptID + } + if selectedPromptName != "" { + entry.SelectedPromptName = &selectedPromptName + } + if selectedPromptVersion != "" { + entry.SelectedPromptVersion = &selectedPromptVersion + } if teamID != "" { entry.TeamID = &teamID } diff --git a/transports/bifrost-http/handlers/prompts.go b/transports/bifrost-http/handlers/prompts.go index c5e5737f0f..9889acde0d 100644 --- a/transports/bifrost-http/handlers/prompts.go +++ b/transports/bifrost-http/handlers/prompts.go @@ -831,6 +831,12 @@ func (h *PromptsHandler) createSession(ctx *fasthttp.RequestCtx) { if len(req.ModelParams) == 0 { req.ModelParams = version.ModelParams } + if len(req.Variables) == 0 && len(version.Variables) > 0 { + req.Variables = make(tables.PromptVariables, len(version.Variables)) + for key := range version.Variables { + req.Variables[key] = "" + } + } } else { // Use provided messages for _, msg := range req.Messages { @@ -904,7 +910,9 @@ func (h *PromptsHandler) updateSession(ctx *fasthttp.RequestCtx) { session.ModelParams = req.ModelParams session.Provider = req.Provider session.Model = req.Model - session.Variables = req.Variables + if req.Variables != nil { + session.Variables = req.Variables + } // Update messages var messages []tables.TablePromptSessionMessage diff --git a/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentsAccordionItem.tsx b/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentsAccordionItem.tsx index d0eeb6e024..552fb6cd2c 100644 --- a/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentsAccordionItem.tsx +++ b/ui/app/_fallbacks/enterprise/components/prompt-deployments/promptDeploymentsAccordionItem.tsx @@ -27,8 +27,8 @@ export function PromptDeploymentsAccordionItem({ deploymentsOpen ? "min-h-0 grow overflow-hidden" : "shrink-0 grow-0", )} > - - Deployments + + Deployments { interface LogDetailViewProps { log: LogEntry | null; + resolvedSelectedPromptName?: string; // Current prompt name from prompt-repo when `selected_prompt_id` is set; falls back to stored log name loading?: boolean; handleDelete?: (log: LogEntry) => void; onClose?: () => void; @@ -86,6 +87,7 @@ interface LogDetailViewProps { export function LogDetailView({ log, + resolvedSelectedPromptName, loading = false, handleDelete, onClose, @@ -100,6 +102,8 @@ export function LogDetailView({ if (!log) return null; + const selectedPromptDisplayName = resolvedSelectedPromptName ?? log.selected_prompt_name ?? ""; + const isContainer = isContainerOperation(log.object); const isPassthrough = isPassthroughOperation(log.object); const passthroughParams = isPassthrough @@ -279,6 +283,19 @@ export function LogDetailView({ /> )} {log.selected_key && } + {(log.selected_prompt_id || log.selected_prompt_name || log.selected_prompt_version) && ( + + {selectedPromptDisplayName} + {selectedPromptDisplayName && log.selected_prompt_version ? " · " : ""} + {log.selected_prompt_version ? <>v{log.selected_prompt_version} : null} + + } + /> + )} {log.number_of_retries > 0 && ( )} diff --git a/ui/app/workspace/logs/sheets/logDetailsSheet.tsx b/ui/app/workspace/logs/sheets/logDetailsSheet.tsx index 1c5af8725a..c1c87244fd 100644 --- a/ui/app/workspace/logs/sheets/logDetailsSheet.tsx +++ b/ui/app/workspace/logs/sheets/logDetailsSheet.tsx @@ -1,10 +1,11 @@ -import { useEffect, useState } from "react"; -import { useHotkeys } from "react-hotkeys-hook"; -import { useGetLogByIdQuery } from "@/lib/store/apis/logsApi"; import { Button } from "@/components/ui/button"; import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet"; +import { useGetLogByIdQuery } from "@/lib/store/apis/logsApi"; +import { useGetPromptQuery } from "@/lib/store/apis/promptsApi"; import type { LogEntry } from "@/lib/types/logs"; import { ChevronDown, ChevronUp, Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; import { LogDetailView } from "./logDetailView"; interface LogDetailSheetProps { @@ -39,8 +40,16 @@ export function LogDetailSheet({ skip: !open || !log?.id, pollingInterval, }); + const shouldPoll = isError || fullLog?.status === "processing"; + const isFullDataReady = log != null && (isError || (fullLog?.id === log.id && !isLoading)); + // Prefer full log when loaded; otherwise list row — enables prompt fetch in parallel with getLogById + const selectedPromptId = log ? (fullLog?.id === log.id ? fullLog : log).selected_prompt_id : undefined; + const { data: selectedPromptData } = useGetPromptQuery(selectedPromptId ?? "", { + skip: !open || !selectedPromptId, + }); + useEffect(() => { setPollingInterval(shouldPoll ? 2000 : 0); }, [shouldPoll]); @@ -51,9 +60,10 @@ export function LogDetailSheet({ if (!log) return null; + // Show a loader only on the initial fetch, not during background polling refetches. - const isFullDataReady = fullLog?.id === log.id && !isLoading; - const displayLog = isFullDataReady ? fullLog : log; + const displayLog: LogEntry = isFullDataReady && fullLog ? fullLog : log; + const resolvedSelectedPromptName = selectedPromptData?.prompt?.name ?? displayLog.selected_prompt_name ?? ""; return ( @@ -66,6 +76,7 @@ export function LogDetailSheet({ ) : ( onOpenChange(false)} onFilterByParentRequestId={onFilterByParentRequestId} diff --git a/ui/app/workspace/prompt-repo/deployments/layout.tsx b/ui/app/workspace/prompt-repo/deployments/layout.tsx deleted file mode 100644 index 4d057ee790..0000000000 --- a/ui/app/workspace/prompt-repo/deployments/layout.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import { createFileRoute } from "@tanstack/react-router"; -import PromptDeploymentsPage from "./page"; - -export const Route = createFileRoute("/workspace/prompt-repo/deployments")({ - component: PromptDeploymentsPage, -}); \ No newline at end of file diff --git a/ui/app/workspace/prompt-repo/layout.tsx b/ui/app/workspace/prompt-repo/layout.tsx new file mode 100644 index 0000000000..b1d4b2ac84 --- /dev/null +++ b/ui/app/workspace/prompt-repo/layout.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from '@tanstack/react-router'; +import PromptsPage from "./page"; + +export const Route = createFileRoute('/workspace/prompt-repo')({ + component: PromptsPage, +}) diff --git a/ui/app/workspace/prompt-repo/prompts/page.tsx b/ui/app/workspace/prompt-repo/prompts/page.tsx index b2d6f8a7db..7852d2948e 100644 --- a/ui/app/workspace/prompt-repo/prompts/page.tsx +++ b/ui/app/workspace/prompt-repo/prompts/page.tsx @@ -1,9 +1,13 @@ -"use client"; - -import { redirect, useSearchParams } from "next/navigation"; +import { useLocation, useNavigate } from "@tanstack/react-router"; +import { useEffect } from "react"; export default function PromptsPage() { - const searchParams = useSearchParams(); - const queryString = searchParams.toString(); - redirect(`/workspace/prompt-repo${queryString ? `?${queryString}` : ""}`); + const navigate = useNavigate(); + const { searchStr } = useLocation(); + + useEffect(() => { + navigate({ to: `/workspace/prompt-repo${searchStr}`, replace: true }); + }, [navigate, searchStr]); + + return null; } diff --git a/ui/app/workspace/routing-rules/views/routingRuleSheet.tsx b/ui/app/workspace/routing-rules/views/routingRuleSheet.tsx index 0cd7bc055f..71681c8878 100644 --- a/ui/app/workspace/routing-rules/views/routingRuleSheet.tsx +++ b/ui/app/workspace/routing-rules/views/routingRuleSheet.tsx @@ -47,7 +47,7 @@ import { validateRoutingRules } from "@/lib/utils/celConverterRouting"; import { Plus, Save, Trash2, X } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; +import { lazy, Suspense, useCallback, useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { RuleGroupType } from "react-querybuilder"; import { toast } from "sonner"; diff --git a/ui/app/workspace/routing-rules/components/celBuilder/queryBuilderWrapper.css b/ui/components/ui/custom/celBuilder/queryBuilderWrapper.css similarity index 100% rename from ui/app/workspace/routing-rules/components/celBuilder/queryBuilderWrapper.css rename to ui/components/ui/custom/celBuilder/queryBuilderWrapper.css diff --git a/ui/components/ui/custom/celBuilder/valueEditor.tsx b/ui/components/ui/custom/celBuilder/valueEditor.tsx index 249b31be8b..3b43413314 100644 --- a/ui/components/ui/custom/celBuilder/valueEditor.tsx +++ b/ui/components/ui/custom/celBuilder/valueEditor.tsx @@ -14,16 +14,28 @@ import { getProviderLabel } from "@/lib/constants/logs"; import { useEffect, useState } from "react"; import { ValueEditorProps, ValueEditorType } from "react-querybuilder"; -export function ValueEditor({ value, handleOnChange, operator, fieldData, type }: ValueEditorProps) { +type CELValueEditorContext = { + validateRegex?: (pattern: string) => string | null; + menuPosition?: "absolute" | "fixed"; + menuPortalTarget?: HTMLElement | null; +}; + +export function ValueEditor({ + value, + handleOnChange, + operator, + fieldData, + type, + context, +}: ValueEditorProps & { context?: CELValueEditorContext }) { // Compute all conditions upfront before any early returns const isArrayOperator = operator === "in" || operator === "notIn"; const isRegexOperator = operator === "matches"; const isNullOperator = operator === "null" || operator === "notNull"; - // Get validateRegex from context if provided - const validateRegex: ((pattern: string) => string | null) | undefined = context?.validateRegex; - const menuPosition: "absolute" | "fixed" | undefined = context?.menuPosition; - const menuPortalTarget: HTMLElement | null | undefined = context?.menuPortalTarget; + const validateRegex = context?.validateRegex; + const menuPosition = context?.menuPosition; + const menuPortalTarget = context?.menuPortalTarget; // Get valueEditorType, handling both string and function types const valueEditorType = diff --git a/ui/lib/types/logs.ts b/ui/lib/types/logs.ts index 8bc7a98ddf..be59d83ab1 100644 --- a/ui/lib/types/logs.ts +++ b/ui/lib/types/logs.ts @@ -460,6 +460,9 @@ export interface LogEntry { fallback_index: number; attempt_trail?: KeyAttemptRecord[]; // Per-attempt key selection history selected_key_id?: string | null; + selected_prompt_id?: string; // Selected prompt ID (prompts plugin) + selected_prompt_name?: string; // Resolved prompt display name (prompts plugin) + selected_prompt_version?: string; // Resolved prompt version number as string (prompts plugin) team_name?: string; team_id?: string; customer_name?: string;