- golem15_golem_models (encrypted write-only api_key) with its cabana list and form under golem15.golem.access_settings, and the Settings lookups (Models, DefaultModel with PHP's position-0 quirk, Vision, Image, File, ByName), every one Trusted - golem:import-settings copies the WinterCMS golem_settings repeater once - AnthropicAdapter; SendStream, SendFile/SendFilePath/SendFileReader, SendWithModel, SendToImageModel, SendToVisionModel, GenerateImage, Ask - PromptFactory; README documents the config keys, the environment variable, the command and the admin screen
213 lines
7.4 KiB
Go
213 lines
7.4 KiB
Go
// Package factories ports the WinterCMS plugin's PromptFactory: ready-made
|
|
// prompts for form field suggestions, categorisation, chats and
|
|
// completions.
|
|
package factories
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"git.golem15.com/golem15/sm-golem-plugin/classes/valueobjects"
|
|
)
|
|
|
|
// Pair is one ordered key and value: a context field, a dropdown option.
|
|
type Pair struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
// FormField is one field to suggest a value for.
|
|
type FormField struct {
|
|
Name string
|
|
Type string
|
|
}
|
|
|
|
// Category is one existing category an item may be assigned to.
|
|
type Category struct {
|
|
ID string
|
|
Name string
|
|
}
|
|
|
|
var fieldStrategies = map[string]string{
|
|
"text": "Generate a concise, descriptive text value",
|
|
"textarea": "Generate longer form text content",
|
|
"slug": "Generate a URL-friendly slug (lowercase, hyphens instead of spaces, no special characters)",
|
|
"richeditor": "Generate formatted HTML content with appropriate tags",
|
|
"markdown": "Generate Markdown-formatted content",
|
|
"code": "Generate code in the appropriate language",
|
|
"taglist": "Generate comma-separated tags/keywords",
|
|
"dropdown": "Select the most appropriate option from available choices",
|
|
}
|
|
|
|
// PromptFactory builds prompts (PHP PromptFactory's static methods).
|
|
type PromptFactory struct{}
|
|
|
|
// CreateSuggestionForField is the prompt for one form field's value.
|
|
// currentValue is nil when the field is empty; options are listed only
|
|
// for dropdown, radio and checkboxlist fields.
|
|
func (PromptFactory) CreateSuggestionForField(fieldName, fieldType string, currentValue *string, context []Pair, options []Pair) valueobjects.Prompt {
|
|
strategy, ok := fieldStrategies[fieldType]
|
|
if !ok {
|
|
strategy = "Generate an appropriate value"
|
|
}
|
|
system := "You are a helpful assistant that generates content for form fields. " +
|
|
"Your task is to: " + strategy + ". " +
|
|
"Respond with ONLY the value, no explanations or quotes."
|
|
return valueobjects.NewPrompt().
|
|
WithSystem(system).
|
|
AddUserMessage(buildFieldPrompt(fieldName, fieldType, currentValue, context, options))
|
|
}
|
|
|
|
// SuggestFieldsValues is the prompt for a JSON object of values for
|
|
// several fields. notes may be nil.
|
|
func (PromptFactory) SuggestFieldsValues(fields []FormField, context []Pair, notes *string) valueobjects.Prompt {
|
|
system := "You are an expert content generation assistant. " +
|
|
"Based on the provided context, suggest appropriate values for the specified fields. " +
|
|
"Respond in JSON format with field names as keys and generated values as values. " +
|
|
"Do not include any explanations or additional text, respond in raw json without any markdown. Never enclose result in ``` markdown."
|
|
var b strings.Builder
|
|
b.WriteString("Fields to generate values for:\n")
|
|
for _, f := range fields {
|
|
name, typ := f.Name, f.Type
|
|
if name == "" {
|
|
name = "unnamed"
|
|
}
|
|
if typ == "" {
|
|
typ = "text"
|
|
}
|
|
b.WriteString("- " + name + " (type: " + typ + ")\n")
|
|
}
|
|
if len(context) > 0 {
|
|
b.WriteString("\nContext to base the suggestions on:\n")
|
|
writeContext(&b, context, "- ", "\n")
|
|
}
|
|
if notes != nil && *notes != "" {
|
|
b.WriteString("\nAdditional notes:\n" + *notes + "\n")
|
|
}
|
|
return valueobjects.NewPrompt().WithSystem(system).AddUserMessage(b.String())
|
|
}
|
|
|
|
// SuggestCategoriesForItem is the prompt for the ids of the existing
|
|
// categories that fit an item. notes may be nil.
|
|
func (PromptFactory) SuggestCategoriesForItem(categories []Category, context []Pair, notes *string) valueobjects.Prompt {
|
|
system := "You are an expert content categorization assistant. " +
|
|
"Based on the provided context, suggest relevant categories from the existing list. " +
|
|
"Respond with a comma-separated list of category ids that best fit the content. " +
|
|
"Do not suggest categories that are not in the existing list. Respond with ids only, no quotes or explanations."
|
|
var b strings.Builder
|
|
b.WriteString("Existing categories:\n")
|
|
for _, c := range categories {
|
|
id, name := c.ID, c.Name
|
|
if id == "" {
|
|
id = "unknown"
|
|
}
|
|
if name == "" {
|
|
name = "unnamed"
|
|
}
|
|
b.WriteString("- " + id + ": " + name + "\n")
|
|
}
|
|
if len(context) > 0 {
|
|
b.WriteString("\nContext to assign categories to:\n")
|
|
writeContext(&b, context, "- ", "\n")
|
|
}
|
|
if notes != nil && *notes != "" {
|
|
b.WriteString("\nAdditional notes:\n" + *notes + "\n")
|
|
}
|
|
return valueobjects.NewPrompt().WithSystem(system).AddUserMessage(b.String())
|
|
}
|
|
|
|
// CreateChat is an empty chat with an optional system prompt.
|
|
func (PromptFactory) CreateChat(system string) valueobjects.Prompt {
|
|
p := valueobjects.NewPrompt()
|
|
if system != "" {
|
|
p = p.WithSystem(system)
|
|
}
|
|
return p
|
|
}
|
|
|
|
// CreateCompletion is a one-message prompt with an optional system prompt.
|
|
func (f PromptFactory) CreateCompletion(userMessage, system string) valueobjects.Prompt {
|
|
return f.CreateChat(system).AddUserMessage(userMessage)
|
|
}
|
|
|
|
func buildFieldPrompt(fieldName, fieldType string, currentValue *string, context, options []Pair) string {
|
|
var b strings.Builder
|
|
b.WriteString("Generate a value for the '" + HumanizeFieldName(fieldName) + "' field.")
|
|
if len(context) > 0 {
|
|
b.WriteString("\n\nContext from related fields:")
|
|
writeContext(&b, context, "\n- ", "")
|
|
}
|
|
if currentValue != nil && *currentValue != "" {
|
|
b.WriteString("\n\nCurrent value (improve or keep if already good): " + *currentValue)
|
|
}
|
|
if len(options) > 0 && (fieldType == "dropdown" || fieldType == "radio" || fieldType == "checkboxlist") {
|
|
b.WriteString("\n\nAvailable options:")
|
|
for _, o := range options {
|
|
b.WriteString("\n- " + o.Key + ": " + o.Value)
|
|
}
|
|
b.WriteString("\n\nRespond with ONLY the option key.")
|
|
}
|
|
switch fieldType {
|
|
case "slug":
|
|
b.WriteString("\n\nFormat: lowercase letters, numbers, and hyphens only. No spaces or special characters.")
|
|
case "taglist":
|
|
b.WriteString("\n\nFormat: comma-separated list of relevant keywords/tags.")
|
|
case "richeditor":
|
|
b.WriteString("\n\nFormat: valid HTML with appropriate tags (p, h2, h3, ul, li, strong, em).")
|
|
case "markdown":
|
|
b.WriteString("\n\nFormat: valid Markdown syntax.")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// writeContext writes each non-empty context value (PHP skips empty()
|
|
// values, "0" included) as prefix + humanized key + ": " + value + suffix.
|
|
func writeContext(b *strings.Builder, context []Pair, prefix, suffix string) {
|
|
for _, c := range context {
|
|
if c.Value == "" || c.Value == "0" {
|
|
continue
|
|
}
|
|
b.WriteString(prefix + HumanizeFieldName(c.Key) + ": " + c.Value + suffix)
|
|
}
|
|
}
|
|
|
|
var (
|
|
fieldPrefix = regexp.MustCompile(`^(field_|input_|txt_)`)
|
|
camelBreak = regexp.MustCompile(`([a-z])([A-Z])`)
|
|
)
|
|
|
|
// HumanizeFieldName turns a field name into words: the field_, input_ or
|
|
// txt_ prefix dropped, camelCase split, underscores and hyphens to
|
|
// spaces, each word capitalised (PHP ucwords(strtolower(...))).
|
|
func HumanizeFieldName(name string) string {
|
|
name = fieldPrefix.ReplaceAllString(name, "")
|
|
name = camelBreak.ReplaceAllString(name, "$1 $2")
|
|
name = strings.NewReplacer("_", " ", "-", " ").Replace(name)
|
|
return ucwords(asciiLower(name))
|
|
}
|
|
|
|
// asciiLower is PHP 8's strtolower: ASCII letters only.
|
|
func asciiLower(s string) string {
|
|
b := []byte(s)
|
|
for i, c := range b {
|
|
if c >= 'A' && c <= 'Z' {
|
|
b[i] = c + 'a' - 'A'
|
|
}
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
// ucwords upper-cases the first ASCII letter of each word, words being
|
|
// separated by PHP's default delimiters.
|
|
func ucwords(s string) string {
|
|
b := []byte(s)
|
|
start := true
|
|
for i, c := range b {
|
|
if start && c >= 'a' && c <= 'z' {
|
|
b[i] = c - 'a' + 'A'
|
|
}
|
|
start = c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\f' || c == '\v'
|
|
}
|
|
return string(b)
|
|
}
|