Skip to content

Commit

Permalink
PromptL compiler v1 - Chapter 4: xml support (#581)
Browse files Browse the repository at this point in the history
  • Loading branch information
csansoon authored Nov 11, 2024
1 parent 7c68732 commit cf43568
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 115 deletions.
7 changes: 2 additions & 5 deletions packages/promptl/src/compiler/base/nodes/tags/content.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { removeCommonIndent } from '$promptl/compiler/utils'
import {
CUSTOM_CONTENT_TAG,
CUSTOM_CONTENT_TYPE_ATTR,
} from '$promptl/constants'
import { CUSTOM_CONTENT_TYPE_ATTR, TAG_NAMES } from '$promptl/constants'
import errors from '$promptl/error/errors'
import { ContentTag } from '$promptl/parser/interfaces'
import { ContentType, ContentTypeTagName } from '$promptl/types'
Expand Down Expand Up @@ -39,7 +36,7 @@ export async function compile(
const textContent = removeCommonIndent(popStrayText())

let type: ContentType
if (node.name === CUSTOM_CONTENT_TAG) {
if (node.name === TAG_NAMES.content) {
if (attributes[CUSTOM_CONTENT_TYPE_ATTR] === undefined) {
baseNodeError(errors.messageTagWithoutRole, node)
}
Expand Down
28 changes: 0 additions & 28 deletions packages/promptl/src/compiler/base/nodes/tags/message.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,6 @@ describe('messages', async () => {
])
})

it('fails when using an unknown tag', async () => {
const prompt = `
<foo>message</foo>
`
const action = () =>
render({
prompt: removeCommonIndent(prompt),
parameters: {},
})
const error = await getExpectedError(action, CompileError)
expect(error.code).toBe('unknown-tag')
})

it('can create messages with the common message tag', async () => {
const prompt = `
<message role=${CUSTOM_TAG_START}role${CUSTOM_TAG_END}>message</message>
Expand Down Expand Up @@ -215,21 +202,6 @@ describe('message contents', async () => {
)
})

it('fails when using an invalid content type', async () => {
const prompt = `
<system>
<foo>text content</foo>
</system>
`
const action = () =>
render({
prompt: removeCommonIndent(prompt),
parameters: {},
})
const error = await getExpectedError(action, CompileError)
expect(error.code).toBe('unknown-tag')
})

it('creates a text content when no content tag is present', async () => {
const prompt = `
<system>
Expand Down
7 changes: 2 additions & 5 deletions packages/promptl/src/compiler/base/nodes/tags/message.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import {
CUSTOM_MESSAGE_ROLE_ATTR,
CUSTOM_MESSAGE_TAG,
} from '$promptl/constants'
import { CUSTOM_MESSAGE_ROLE_ATTR, TAG_NAMES } from '$promptl/constants'
import errors from '$promptl/error/errors'
import { MessageTag, TemplateNode } from '$promptl/parser/interfaces'
import {
Expand Down Expand Up @@ -38,7 +35,7 @@ export async function compile(
groupContent()

let role = node.name as MessageRole
if (node.name === CUSTOM_MESSAGE_TAG) {
if (node.name === TAG_NAMES.message) {
if (attributes[CUSTOM_MESSAGE_ROLE_ATTR] === undefined) {
baseNodeError(errors.messageTagWithoutRole, node)
}
Expand Down
6 changes: 3 additions & 3 deletions packages/promptl/src/compiler/chain.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CHAIN_STEP_TAG } from '$promptl/constants'
import { TAG_NAMES } from '$promptl/constants'
import CompileError from '$promptl/error/error'
import { getExpectedError } from '$promptl/test/helpers'
import {
Expand Down Expand Up @@ -491,7 +491,7 @@ describe('chain', async () => {
{{i}}.{{j}}
</user>
<${CHAIN_STEP_TAG} />
<${TAG_NAMES.step} />
{{foo = i * j}}
{{endfor}}
Expand Down Expand Up @@ -536,7 +536,7 @@ describe('chain', async () => {

it('saves the response in a variable', async () => {
const prompt = removeCommonIndent(`
<${CHAIN_STEP_TAG} as="response" />
<${TAG_NAMES.step} as="response" />
{{response}}
`)
Expand Down
13 changes: 0 additions & 13 deletions packages/promptl/src/compiler/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,6 @@ describe(`all compilation errors that don't require value resolution are caught
})
})

it('unknown-tag', async () => {
const prompt = `
<foo>
Foo
</foo>
`

await expectBothErrors({
code: 'unknown-tag',
prompt,
})
})

it('step-tag-inside-step', async () => {
const prompt = `
<step>
Expand Down
9 changes: 6 additions & 3 deletions packages/promptl/src/compiler/readMetadata.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TAG_NAMES } from '$promptl/constants'
import CompileError from '$promptl/error/error'
import { describe, expect, it } from 'vitest'
import { z } from 'zod'
Expand Down Expand Up @@ -595,8 +596,8 @@ describe('syntax errors', async () => {
`),
child: removeCommonIndent(`
This is the child prompt.
Error:
<unknownTag />
Error: (close unopened tag)
</${TAG_NAMES.message}>
`),
}

Expand All @@ -611,6 +612,8 @@ describe('syntax errors', async () => {
expect(metadata.errors[0]!.message).contains(
'The referenced prompt contains an error:',
)
expect(metadata.errors[0]!.message).contains(`Unknown tag: 'unknownTag'`)
expect(metadata.errors[0]!.message).contains(
`Unexpected closing tag for ${TAG_NAMES.message}`,
)
})
})
8 changes: 4 additions & 4 deletions packages/promptl/src/compiler/readMetadata.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import {
CUSTOM_MESSAGE_ROLE_ATTR,
CUSTOM_MESSAGE_TAG,
REFERENCE_DEPTH_LIMIT,
REFERENCE_PROMPT_ATTR,
REFERENCE_PROMPT_TAG,
TAG_NAMES,
} from '$promptl/constants'
import CompileError, { error } from '$promptl/error/error'
import errors from '$promptl/error/errors'
Expand Down Expand Up @@ -462,7 +461,7 @@ export class ReadMetadata {
})

const role = node.name as MessageRole
if (node.name === CUSTOM_MESSAGE_TAG) {
if (node.name === TAG_NAMES.message) {
if (!attributes.has(CUSTOM_MESSAGE_ROLE_ATTR)) {
this.baseNodeError(errors.messageTagWithoutRole, node)
return
Expand Down Expand Up @@ -542,7 +541,7 @@ export class ReadMetadata {
.map((node) => node.data)
.join('')

let resolvedRefPrompt = `/* <${REFERENCE_PROMPT_TAG} ${REFERENCE_PROMPT_ATTR}="${refPromptPath}" /> */`
let resolvedRefPrompt = `/* <${TAG_NAMES.prompt} ${REFERENCE_PROMPT_ATTR}="${refPromptPath}" /> */`
const currentReferences = this.references[this.fullPath] ?? []

const resolveRef = async () => {
Expand Down Expand Up @@ -649,6 +648,7 @@ export class ReadMetadata {
return
}

// Should not be reachable, as non-recognized tags are caught by the parser
this.baseNodeError(errors.unknownTag(node.name), node)
return
}
Expand Down
15 changes: 5 additions & 10 deletions packages/promptl/src/compiler/utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
CHAIN_STEP_TAG,
CUSTOM_CONTENT_TAG,
CUSTOM_MESSAGE_TAG,
REFERENCE_PROMPT_TAG,
} from '$promptl/constants'
import { TAG_NAMES } from '$promptl/constants'
import {
ChainStepTag,
ContentTag,
Expand Down Expand Up @@ -43,23 +38,23 @@ export function removeCommonIndent(text: string): string {
}

export function isMessageTag(tag: ElementTag): tag is MessageTag {
if (tag.name === CUSTOM_MESSAGE_TAG) return true
if (tag.name === TAG_NAMES.message) return true
return Object.values(MessageRole).includes(tag.name as MessageRole)
}

export function isContentTag(tag: ElementTag): tag is ContentTag {
if (tag.name === CUSTOM_CONTENT_TAG) return true
if (tag.name === TAG_NAMES.content) return true
return Object.values(ContentTypeTagName).includes(
tag.name as ContentTypeTagName,
)
}

export function isRefTag(tag: ElementTag): tag is ReferenceTag {
return tag.name === REFERENCE_PROMPT_TAG
return tag.name === TAG_NAMES.prompt
}

export function isChainStepTag(tag: ElementTag): tag is ChainStepTag {
return tag.name === CHAIN_STEP_TAG
return tag.name === TAG_NAMES.step
}

export function tagAttributeIsLiteral(tag: ElementTag, name: string): boolean {
Expand Down
27 changes: 17 additions & 10 deletions packages/promptl/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { ContentTypeTagName, MessageRole } from './types'

export const CUSTOM_TAG_START = '{{'
export const CUSTOM_TAG_END = '}}'

// <message role="…">
export const CUSTOM_MESSAGE_TAG = 'message' as const
export const CUSTOM_MESSAGE_ROLE_ATTR = 'role' as const
export enum TAG_NAMES {
message = 'message',
system = MessageRole.system,
user = MessageRole.user,
assistant = MessageRole.assistant,
tool = MessageRole.tool,
content = 'content',
text = ContentTypeTagName.text,
image = ContentTypeTagName.image,
toolCall = ContentTypeTagName.toolCall,
prompt = 'prompt',
step = 'step',
}

export const CUSTOM_CONTENT_TAG = 'content' as const
export const CUSTOM_MESSAGE_ROLE_ATTR = 'role' as const
export const CUSTOM_CONTENT_TYPE_ATTR = 'type' as const

// <prompt path="…" />
export const REFERENCE_PROMPT_TAG = 'prompt' as const
export const REFERENCE_PROMPT_ATTR = 'path' as const
export const REFERENCE_DEPTH_LIMIT = 50

// <response as="…" />
export const CHAIN_STEP_TAG = 'step' as const
export const CHAIN_STEP_ISOLATED_ATTR = 'isolated' as const

export enum KEYWORDS {
Expand All @@ -31,3 +37,4 @@ export enum KEYWORDS {
}

export const RESERVED_KEYWORDS = Object.values(KEYWORDS)
export const RESERVED_TAGS = Object.values(TAG_NAMES)
15 changes: 5 additions & 10 deletions packages/promptl/src/parser/interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import {
CHAIN_STEP_TAG,
CUSTOM_CONTENT_TAG,
CUSTOM_MESSAGE_TAG,
REFERENCE_PROMPT_TAG,
} from '$promptl/constants'
import { TAG_NAMES } from '$promptl/constants'
import { ContentTypeTagName, MessageRole } from '$promptl/types'
import { Identifier, type Node as LogicalExpression } from 'estree'

Expand Down Expand Up @@ -45,13 +40,13 @@ type IElementTag<T extends string> = BaseNode & {

export type MessageTag =
| IElementTag<MessageRole>
| IElementTag<typeof CUSTOM_MESSAGE_TAG>
| IElementTag<typeof TAG_NAMES.message>
export type ContentTag =
| IElementTag<ContentTypeTagName>
| IElementTag<typeof CUSTOM_CONTENT_TAG>
| IElementTag<typeof TAG_NAMES.content>

export type ReferenceTag = IElementTag<typeof REFERENCE_PROMPT_TAG>
export type ChainStepTag = IElementTag<typeof CHAIN_STEP_TAG>
export type ReferenceTag = IElementTag<typeof TAG_NAMES.prompt>
export type ChainStepTag = IElementTag<typeof TAG_NAMES.step>
export type ElementTag =
| ContentTag
| MessageTag
Expand Down
Loading

0 comments on commit cf43568

Please sign in to comment.