Skip to content
4 changes: 4 additions & 0 deletions frontend/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6796,6 +6796,10 @@ snapshots:
hash: v1.k794b7964.6f9f209d5daaf8573020fb50615421d426055c8090d0ee73a2629487d30cbf29.stsWopH6BI50ASXjhDxCWn-Xa46oqy_cH4v0xAO4VIQ
scenes-app-ai-observability-instrumentation-checklist-card--warnings-with-a-dismissal--light:
hash: v1.k794b7964.b0e06944ee7afe4af8cb39363280135111e71ba215aab56a3e410c79de62238b.WtW8suUOWanaTrvjxhV2Z_ANVDRmEW3bqHpTrTe5GoI
scenes-app-ai-observability-promptreferencetags--label-and-version-references--dark:
hash: v1.k794b7964.4cc699d7489063a64cf4ec1f930d11fc062eb0d9c9e46150f939503945bb8841.jKmH2ZmjZex98fImM00HE_xBBayalWKHtqXbVlhE_VM
scenes-app-ai-observability-promptreferencetags--label-and-version-references--light:
hash: v1.k794b7964.b406b4c5f2cc872222ab4156f8179c50db3a873b1b75d332e826ea9dc37eaa12.BHAFV67Jt-fG7F3xlWVwu0Th-yvY_DepLdasKhNQA30
scenes-app-ai-observability-prompts--empty-state--dark:
hash: v1.k794b7964.3881018cfbf85cf1beed0dd610d747267571381277a983c7b8239acc5d372b86.yIUKSXpWrhD6XV_uZ1Sk-kb3tJbbBLfdcYML7kWkw40
scenes-app-ai-observability-prompts--empty-state--light:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useActions, useValues } from 'kea'
import { useState } from 'react'

import { IconPlusSmall } from '@posthog/icons'
import { LemonButton, LemonModal } from '@posthog/lemon-ui'

import { insertPromptReferenceLogic } from './insertPromptReferenceLogic'
import { PromptReferencePicker } from './PromptReferencePicker'

export function InsertPromptReferenceButton({
currentPromptName,
onInsert,
}: {
currentPromptName: string
onInsert: (tag: string) => void
}): JSX.Element {
const { promptOptionsLoading } = useValues(insertPromptReferenceLogic)
const { loadPromptOptions } = useActions(insertPromptReferenceLogic)
const [isOpen, setIsOpen] = useState(false)

return (
<>
<LemonButton
size="small"
icon={<IconPlusSmall />}
loading={!isOpen && promptOptionsLoading}
tooltip="Insert a reference to another prompt. Its content replaces the tag when the prompt is fetched."
data-attr="llma-prompt-insert-reference-button"
onClick={(e) => {
e.preventDefault()
if (promptOptionsLoading) {
return
}
setIsOpen(true)
loadPromptOptions('')
}}
>
Comment thread
jurajmajerik marked this conversation as resolved.
Resolved
Insert reference
</LemonButton>
<LemonModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Insert prompt reference"
description="Reference a label to pick up its future moves, or pin a version to freeze the content."
width={480}
>
<PromptReferencePicker
currentPromptName={currentPromptName}
onSelect={(tag) => {
onInsert(tag)
setIsOpen(false)
}}
/>
</LemonModal>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useActions, useValues } from 'kea'
import { useState } from 'react'

import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from 'lib/ui/quill'

import { insertPromptReferenceLogic } from './insertPromptReferenceLogic'
import { buildPromptReferenceTag } from './promptReferences'

interface ReferenceOption {
tag: string
promptName: string
selectorLabel: string
}

export function PromptReferencePicker({
currentPromptName,
onSelect,
}: {
currentPromptName: string
onSelect: (tag: string) => void
}): JSX.Element {
const { promptOptions, promptOptionsLoading } = useValues(insertPromptReferenceLogic)
const { loadPromptOptions } = useActions(insertPromptReferenceLogic)
const [inputValue, setInputValue] = useState('')

// Keys double as display strings so the combobox's text filtering matches
// what the item shows. The "(pinned)" suffix cannot collide with a label
// row: label names allow no spaces or parentheses.
const optionsByKey = new Map<string, ReferenceOption>()
for (const prompt of promptOptions) {
if (prompt.name === currentPromptName) {
continue
}
for (const label of prompt.all_labels) {
optionsByKey.set(`${prompt.name} @ ${label.name}`, {
tag: buildPromptReferenceTag(prompt.name, { label: label.name }),
promptName: prompt.name,
selectorLabel: `@ ${label.name} (follows the label)`,
})
}
optionsByKey.set(`${prompt.name} @ v${prompt.latest_version} (pinned)`, {
tag: buildPromptReferenceTag(prompt.name, { version: prompt.latest_version }),
promptName: prompt.name,
selectorLabel: `@ v${prompt.latest_version} (pinned)`,
})
}
const items = [...optionsByKey.keys()]

return (
<Combobox
autoHighlight
items={items}
value={null}
onValueChange={(next: string | null) => {
const option = next ? optionsByKey.get(next) : undefined
if (option) {
onSelect(option.tag)
}
}}
inputValue={inputValue}
onInputValueChange={(value: string) => {
setInputValue(value)
loadPromptOptions(value)
}}
defaultOpen
>
<ComboboxInput
placeholder="Search prompts"
autoFocus
showTrigger={false}
data-attr="llma-prompt-insert-reference-input"
/>
<ComboboxContent>
<ComboboxEmpty>{promptOptionsLoading ? 'Loading prompts...' : 'No matching prompts'}</ComboboxEmpty>
<ComboboxList>
{(item: string) => {
const option = optionsByKey.get(item)
return (
<ComboboxItem key={item} value={item} className="ps-2">
<span className="font-medium">{option?.promptName}</span>
<span className="text-xs text-secondary">{option?.selectorLabel}</span>
</ComboboxItem>
)
}}
</ComboboxList>
</ComboboxContent>
</Combobox>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react'

import { PromptReferenceTags } from './PromptReferenceTags'

const meta: Meta<typeof PromptReferenceTags> = {
title: 'Scenes-App/AI Observability/PromptReferenceTags',
component: PromptReferenceTags,
}
export default meta

type Story = StoryObj<typeof PromptReferenceTags>

export const LabelAndVersionReferences: Story = {
args: {
text: 'Intro.\n@@@prompt:name=guardrails|label=production@@@\nMore text.\n@@@prompt:name=tone|version=3@@@',
},
}
28 changes: 28 additions & 0 deletions products/ai_observability/frontend/prompts/PromptReferenceTags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { LemonTag, Link } from '@posthog/lemon-ui'

import { urls } from 'scenes/urls'

import { extractPromptReferences } from './promptReferences'

export function PromptReferenceTags({ text }: { text: string }): JSX.Element | null {
const references = extractPromptReferences(text)
if (references.length === 0) {
return null
}
return (
<div className="flex flex-wrap items-center gap-1">
<span className="text-xs text-secondary">References:</span>
{references.map((reference) => (
<Link
key={reference.raw}
to={urls.aiObservabilityPrompt(reference.name)}
data-attr="llma-prompt-reference-tag"
>
<LemonTag type="completion" size="small">
{reference.name} @ {reference.label ?? `v${reference.version}`}
</LemonTag>
</Link>
))}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { MakeLogicType, kea, path } from 'kea'
import { loaders } from 'kea-loaders'

import { ApiConfig } from '~/lib/api'

import { llmPromptsList } from '../generated/api'
import type { LLMPromptListApi } from '../generated/api.schemas'

// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface insertPromptReferenceLogicValues {
promptOptions: LLMPromptListApi[]
promptOptionsLoading: boolean
}

// Generated by kea-typegen. Update if you're an agent, ignore if you're human.
export interface insertPromptReferenceLogicActions {
loadPromptOptions: (search: string) => string
loadPromptOptionsFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadPromptOptionsSuccess: (
promptOptions: LLMPromptListApi[],
payload?: string
) => {
promptOptions: LLMPromptListApi[]
payload?: string
}
}

export type insertPromptReferenceLogicType = MakeLogicType<
insertPromptReferenceLogicValues,
insertPromptReferenceLogicActions
>

export const insertPromptReferenceLogic = kea<insertPromptReferenceLogicType>([
path(['scenes', 'ai-observability', 'insertPromptReferenceLogic']),
loaders({
promptOptions: [
[] as LLMPromptListApi[],
{
loadPromptOptions: async (search: string, breakpoint) => {
if (search) {
await breakpoint(250)
}
const response = await llmPromptsList(String(ApiConfig.getCurrentTeamId()), {
content: 'none',
order_by: 'name',
limit: 100,
...(search ? { search } : {}),
})
breakpoint()
return response.results
Comment thread
jurajmajerik marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
},
},
],
}),
])
67 changes: 66 additions & 1 deletion products/ai_observability/frontend/prompts/llmPromptLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@
llmPromptsNameLabelsDestroy,
llmPromptsNameLabelsUpdate,
llmPromptsNamePartialUpdate,
llmPromptsNameRetrieve,
llmPromptsResolveNameRetrieve,
} from '../generated/api'
import type { LLMPromptLabelApi, LLMPromptResolveResponseApi } from '../generated/api.schemas'
import type { LLMPromptLabelApi, LLMPromptPublicApi, LLMPromptResolveResponseApi } from '../generated/api.schemas'
import { llmPromptsLogic } from './llmPromptsLogic'
import { LLM_PROMPTS_FORCE_RELOAD_PARAM } from './llmPromptsLogic'
import { LLMPrompt, LLMPromptVersionSummary } from './types'
Expand Down Expand Up @@ -232,6 +233,7 @@
isPromptMissing: boolean
isPublishReviewOpen: boolean
isRenderingMarkdown: boolean
isShowingResolvedPreview: boolean
isViewMode: boolean
labelPickerVersion: number | null
labelsByVersion: Record<number, LLMPromptLabelApi[]>
Expand All @@ -256,6 +258,8 @@
publishConflict: PublishConflict | null
relatedTracesQuery: DataTableNode | null
relatedTracesQueryOverride: DataTableNode | null
resolvedPreview: LLMPromptPublicApi | null
resolvedPreviewLoading: boolean
shouldDisplaySkeleton: boolean
showPromptFormErrors: boolean
snippetLanguage: PromptSnippetLanguage
Expand Down Expand Up @@ -321,6 +325,21 @@
prompt: ResolvedLLMPrompt
payload?: any
}
loadResolvedPreview: () => any
loadResolvedPreviewFailure: (
error: string,
errorObject?: any
) => {
error: string
errorObject?: any
}
loadResolvedPreviewSuccess: (
resolvedPreview: LLMPromptPublicApi,
payload?: any
) => {
resolvedPreview: LLMPromptPublicApi
payload?: any
}
openLabelPicker: (version: number) => {
version: number
}
Expand Down Expand Up @@ -421,6 +440,9 @@
toggleOutlineExpanded: () => {
value: true
}
toggleResolvedPreview: () => {
value: true
}
touchPromptFormField: (key: string) => {
key: string
}
Expand Down Expand Up @@ -522,6 +544,7 @@
setAnalyticsScope: (analyticsScope: PromptAnalyticsScope) => ({ analyticsScope }),
setRelatedTracesQuery: (query: DataTableNode) => ({ query }),
toggleMarkdownRendering: true,
toggleResolvedPreview: true,
setCompareVersion: (compareVersion: number | null) => ({ compareVersion }),
toggleOutlineExpanded: true,
showConfigEditor: true,
Expand Down Expand Up @@ -575,6 +598,24 @@
setRelatedTracesQuery: (_, { query }) => query,
},
],
// Cleared the moment a prompt load starts: every path to different
// content (navigation, version switch, refresh, failure) begins with
// loadPrompt, so clearing on the trigger closes the whole class of
// stale-preview states instead of patching individual outcomes.
resolvedPreview: {
loadPrompt: () => null,
setMode: () => null,
},
isShowingResolvedPreview: [
false,
{
toggleResolvedPreview: (state: boolean) => !state,
loadPrompt: () => false,
setMode: () => false,
// A failed resolution must not present the raw source as resolved content.
loadResolvedPreviewFailure: () => false,
},
],
isRenderingMarkdown: [
props.promptName === 'new' ? false : (props.mode ?? PromptMode.View) !== PromptMode.Edit,
{
Expand Down Expand Up @@ -678,6 +719,22 @@
},
})),

loaders(({ props }) => ({
resolvedPreview: {
__default: null as LLMPromptPublicApi | null,
loadResolvedPreview: async () => {
// Version from the router, like loadPrompt: values.prompt still holds
// the previous version while a back/forward navigation is loading.
const urlVersion = getSelectedVersionFromUrl()
return await llmPromptsNameRetrieve(
String(ApiConfig.getCurrentTeamId()),
props.promptName,
urlVersion !== undefined ? { version: urlVersion } : undefined
)
},
},
})),

forms(({ actions, props, values }) => ({
promptForm: {
defaults: DEFAULT_PROMPT_FORM_VALUES,
Expand All @@ -689,7 +746,7 @@
config: parsePromptConfig(config ?? '').error,
}),

submit: async (formValues) => {

Check warning on line 749 in products/ai_observability/frontend/prompts/llmPromptLogic.ts

View workflow job for this annotation

GitHub Actions / Frontend formatting

lint:complexity

`submit` has cyclomatic complexity 14 (warn >10)
const isNew = props.promptName === 'new'

try {
Expand Down Expand Up @@ -1191,6 +1248,14 @@
}),

listeners(({ actions, asyncActions, props, values }) => ({
toggleResolvedPreview: () => {
if (values.isShowingResolvedPreview) {
actions.loadResolvedPreview()
}
},
loadResolvedPreviewFailure: ({ errorObject }) => {
lemonToast.error(getApiErrorDetail(errorObject) ?? 'Could not resolve this prompt. Try again.')
},
requestSetLabel: ({ labelName, version }) => {
const existing = values.promptLabels.find((label) => label.name === labelName)
if (existing?.version === version) {
Expand Down
Loading
Loading