Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions internal/sync/reference/adapters/adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package adapters

// Adapter converts between an external prompt format and the common prompt
// representation used by reference syncing.
type Adapter interface {
Parse([]byte) (Prompt, error)
Render(Prompt) ([]byte, error)
}

// Prompt is the format-independent data an adapter may extract.
type Prompt struct {
Mode Mode
Key string
Name string
Messages []Message
}

// Mode identifies how the parsed prompt is represented in LaunchDarkly.
type Mode string

const (
ModeAgent Mode = "agent"
ModeCompletion Mode = "completion"
)

// Valid reports whether the mode is supported by prompt sync.
func (mode Mode) Valid() bool {
return mode == ModeAgent || mode == ModeCompletion
}

// Message is one role/content pair in a prompt.
type Message struct {
Role Role
Content string
}

// Role identifies the speaker for a prompt message.
type Role string

const (
RoleSystem Role = "system"
RoleUser Role = "user"
RoleAssistant Role = "assistant"
)

// Valid reports whether the role can be represented by LaunchDarkly.
func (role Role) Valid() bool {
return role == RoleSystem || role == RoleUser || role == RoleAssistant
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package plain_markdown

import (
"fmt"
"strings"

"github.com/launchdarkly/ldcli/internal/sync/reference/adapters"
)

// Adapter converts plain Markdown to and from the common prompt representation.
type Adapter struct{}

// Parse reads raw text as one system message. Raw files do not provide mode,
// key, or name metadata.
func (Adapter) Parse(content []byte) (adapters.Prompt, error) {
body := normalizeContent(string(content))
prompt := adapters.Prompt{}
if body != "" {
prompt.Messages = []adapters.Message{{Role: adapters.RoleSystem, Content: body}}
}
return prompt, nil
Comment thread
cursor[bot] marked this conversation as resolved.
}

// Render writes a prompt that contains at most one system message.
func (Adapter) Render(prompt adapters.Prompt) ([]byte, error) {
if len(prompt.Messages) > 1 || len(prompt.Messages) == 1 && prompt.Messages[0].Role != adapters.RoleSystem {
return nil, fmt.Errorf("plain-markdown supports at most one system message")
}
if len(prompt.Messages) == 0 {
return nil, nil
}
body := normalizeContent(prompt.Messages[0].Content)
if body == "" {
return nil, nil
}
return []byte(body + "\n"), nil
}

// normalizeContent gives equivalent text files one platform-independent
// representation before they are fingerprinted or written.
func normalizeContent(content string) string {
content = strings.ReplaceAll(content, "\r\n", "\n")
content = strings.ReplaceAll(content, "\r", "\n")
return strings.TrimSpace(content)
}

var _ adapters.Adapter = Adapter{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package plain_markdown

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/launchdarkly/ldcli/internal/sync/reference/adapters"
)

func TestAdapterParsesAndRendersRawPrompt(t *testing.T) {
prompt, err := (Adapter{}).Parse([]byte("\nBe helpful.\n"))

require.NoError(t, err)
require.Equal(t, adapters.Prompt{
Messages: []adapters.Message{{Role: adapters.RoleSystem, Content: "Be helpful."}},
}, prompt)

rendered, err := (Adapter{}).Render(prompt)
require.NoError(t, err)
require.Equal(t, "Be helpful.\n", string(rendered))
}

func TestAdapterNormalizesLineEndings(t *testing.T) {
prompt, err := (Adapter{}).Parse([]byte("First line.\r\nSecond line.\rThird line.\r\n"))

require.NoError(t, err)
require.Equal(t, adapters.Prompt{
Messages: []adapters.Message{{
Role: adapters.RoleSystem,
Content: "First line.\nSecond line.\nThird line.",
}},
}, prompt)

rendered, err := (Adapter{}).Render(adapters.Prompt{
Messages: []adapters.Message{{
Role: adapters.RoleSystem,
Content: "First line.\r\nSecond line.\rThird line.",
}},
})

require.NoError(t, err)
require.Equal(t, "First line.\nSecond line.\nThird line.\n", string(rendered))
}
112 changes: 112 additions & 0 deletions internal/sync/reference/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package reference

import (
"fmt"

syncdomain "github.com/launchdarkly/ldcli/internal/sync"
"github.com/launchdarkly/ldcli/internal/sync/reference/adapters"
"github.com/launchdarkly/ldcli/internal/sync/reference/adapters/plain_markdown"
)

// PlainMarkdown identifies the built-in plain Markdown adapter.
const PlainMarkdown = "plain-markdown"

// ValidateFormat reports whether a reference format has a registered adapter.
func ValidateFormat(format string) error {
_, err := adapterFor(format)
return err
}

// Parse converts referenced file content into the common adapter domain.
func Parse(format string, content []byte) (adapters.Prompt, error) {
adapter, err := adapterFor(format)
if err != nil {
return adapters.Prompt{}, err
}
return adapter.Parse(content)
}

// ApplyToVariation merges referenced prompt content into a variation's stored metadata.
func ApplyToVariation(format string, content []byte, variation *syncdomain.Variation) (adapters.Prompt, error) {
prompt, err := Parse(format, content)
if err != nil {
return adapters.Prompt{}, err
}
if prompt.Mode != "" {
if !prompt.Mode.Valid() {
return adapters.Prompt{}, fmt.Errorf("unsupported referenced prompt mode %q", prompt.Mode)
}
variation.Mode = syncdomain.VariationMode(prompt.Mode)
}
if prompt.Key != "" {
variation.Key = prompt.Key
}
if prompt.Name != "" {
variation.Name = prompt.Name
}

messages := make([]syncdomain.Message, 0, len(prompt.Messages))
for _, message := range prompt.Messages {
if !message.Role.Valid() {
return adapters.Prompt{}, fmt.Errorf("unsupported referenced prompt role %q", message.Role)
}
messages = append(messages, syncdomain.Message{Role: string(message.Role), Content: message.Content})
}
switch variation.Mode {
case syncdomain.VariationModeAgent:
if len(messages) > 1 || len(messages) == 1 && messages[0].Role != string(adapters.RoleSystem) {
return adapters.Prompt{}, fmt.Errorf("agent variation %q requires one system message from its reference", variation.Key)
}
variation.Instructions = ""
variation.Messages = nil
if len(messages) == 1 {
variation.Instructions = messages[0].Content
}
case syncdomain.VariationModeCompletion:
variation.Instructions = ""
variation.Messages = messages
default:
return adapters.Prompt{}, fmt.Errorf("referenced prompt does not specify a supported mode")
}
return prompt, nil
}

// Render converts a variation back to the selected external file format.
func Render(format string, variation syncdomain.Variation) ([]byte, error) {
adapter, err := adapterFor(format)
if err != nil {
return nil, err
}
prompt := adapters.Prompt{Mode: adapters.Mode(variation.Mode), Key: variation.Key, Name: variation.Name}
switch variation.Mode {
case syncdomain.VariationModeAgent:
if len(variation.Messages) != 0 {
return nil, fmt.Errorf("agent variation %q cannot be represented because it contains messages", variation.Key)
}
if variation.Instructions != "" {
prompt.Messages = []adapters.Message{{Role: adapters.RoleSystem, Content: variation.Instructions}}
}
case syncdomain.VariationModeCompletion:
for _, message := range variation.Messages {
role := adapters.Role(message.Role)
if !role.Valid() {
return nil, fmt.Errorf("variation %q has unsupported message role %q", variation.Key, message.Role)
}
prompt.Messages = append(prompt.Messages, adapters.Message{Role: role, Content: message.Content})
}
default:
return nil, fmt.Errorf("referenced prompt does not support variation mode %q", variation.Mode)
}
return adapter.Render(prompt)
}

// adapterFor is the single registry for external prompt formats. New formats
// plug into sync by implementing adapters.Adapter and adding one case here.
func adapterFor(format string) (adapters.Adapter, error) {
switch format {
case PlainMarkdown:
return plain_markdown.Adapter{}, nil
default:
return nil, fmt.Errorf("unsupported referenced prompt format %q", format)
}
}
80 changes: 80 additions & 0 deletions internal/sync/reference/format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package reference

import (
"testing"

"github.com/stretchr/testify/require"

syncdomain "github.com/launchdarkly/ldcli/internal/sync"
"github.com/launchdarkly/ldcli/internal/sync/reference/adapters"
)

func TestPlainMarkdownAdapterReturnsCommonPromptDomain(t *testing.T) {
prompt, err := Parse(PlainMarkdown, []byte("Be helpful."))

require.NoError(t, err)
require.Empty(t, prompt.Mode)
require.Empty(t, prompt.Key)
require.Empty(t, prompt.Name)
require.Equal(t, []adapters.Message{{Role: adapters.RoleSystem, Content: "Be helpful."}}, prompt.Messages)
}

func TestPlainMarkdownRoundTrip(t *testing.T) {
tests := []struct {
name string
variation syncdomain.Variation
assert func(*testing.T, syncdomain.Variation)
}{
{
name: "agent instructions",
variation: syncdomain.Variation{Mode: syncdomain.VariationModeAgent, Key: "prompt"},
assert: func(t *testing.T, variation syncdomain.Variation) {
require.Equal(t, "Be helpful.", variation.Instructions)
require.Empty(t, variation.Messages)
},
},
{
name: "completion system message",
variation: syncdomain.Variation{Mode: syncdomain.VariationModeCompletion, Key: "prompt"},
assert: func(t *testing.T, variation syncdomain.Variation) {
require.Empty(t, variation.Instructions)
require.Equal(t, []syncdomain.Message{{Role: "system", Content: "Be helpful."}}, variation.Messages)
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := ApplyToVariation(PlainMarkdown, []byte("\nBe helpful.\n"), &test.variation)
require.NoError(t, err)
test.assert(t, test.variation)

rendered, err := Render(PlainMarkdown, test.variation)
require.NoError(t, err)
require.Equal(t, "Be helpful.\n", string(rendered))
})
}
}

func TestPlainMarkdownRejectsUnrepresentableCompletion(t *testing.T) {
_, err := Render(PlainMarkdown, syncdomain.Variation{
Mode: syncdomain.VariationModeCompletion,
Key: "prompt",
Messages: []syncdomain.Message{
{Role: "system", Content: "System"},
{Role: "user", Content: "User"},
},
})

require.ErrorContains(t, err, "at most one system message")
}

func TestReferenceFormatRejectsUnknownFormat(t *testing.T) {
var variation syncdomain.Variation
err := ValidateFormat("anthropic-prompt")
require.ErrorContains(t, err, "unsupported referenced prompt format")
_, err = ApplyToVariation("anthropic-prompt", nil, &variation)
require.ErrorContains(t, err, "unsupported referenced prompt format")
_, err = Render("anthropic-prompt", variation)
require.ErrorContains(t, err, "unsupported referenced prompt format")
}
Loading
Loading