de30497f59
- Implemented tenant and user management features including: - Tenant creation and management with associated migrations. - User creation and management with associated migrations. - Tenant membership management with associated migrations. - Platform user roles management with associated migrations. - Quota management with associated migrations. - Article and template management with associated migrations. - Added HTTP handlers for templates and workspaces. - Created tests for protected and public routes. - Introduced a script to check tenant scope in SQL queries. - Documented task plan for backend completion and frontend foundation.
181 lines
4.0 KiB
Go
181 lines
4.0 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var promptVariablePattern = regexp.MustCompile(`\{\{\s*([a-zA-Z0-9_]+)\s*\}\}`)
|
|
|
|
func buildGenerationPrompt(templateName string, promptTemplate *string, params map[string]interface{}) string {
|
|
basePrompt := strings.TrimSpace(renderPromptTemplate(promptTemplate, params))
|
|
if basePrompt == "" {
|
|
basePrompt = fmt.Sprintf("Write a complete article in markdown for the template %q.", templateName)
|
|
}
|
|
|
|
var sections []string
|
|
sections = append(sections, basePrompt)
|
|
|
|
contextBlock := buildPromptContext(params)
|
|
if contextBlock != "" {
|
|
sections = append(sections, "Request context:\n"+contextBlock)
|
|
}
|
|
|
|
sections = append(sections, strings.Join([]string{
|
|
"Output requirements:",
|
|
"- Return only article markdown, no surrounding explanation.",
|
|
"- Use the provided title when available.",
|
|
"- Follow the provided outline sections and key points when available.",
|
|
"- Keep structure clear with headings, short paragraphs, and concrete details.",
|
|
}, "\n"))
|
|
|
|
return strings.Join(sections, "\n\n")
|
|
}
|
|
|
|
func renderPromptTemplate(promptTemplate *string, params map[string]interface{}) string {
|
|
if promptTemplate == nil {
|
|
return ""
|
|
}
|
|
|
|
return promptVariablePattern.ReplaceAllStringFunc(*promptTemplate, func(match string) string {
|
|
submatches := promptVariablePattern.FindStringSubmatch(match)
|
|
if len(submatches) != 2 {
|
|
return match
|
|
}
|
|
|
|
value, ok := params[submatches[1]]
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return formatPromptValue(value)
|
|
})
|
|
}
|
|
|
|
func buildPromptContext(params map[string]interface{}) string {
|
|
if len(params) == 0 {
|
|
return ""
|
|
}
|
|
|
|
orderedKeys := []string{
|
|
"locale",
|
|
"title",
|
|
"topic",
|
|
"product_name",
|
|
"subject",
|
|
"brand_name",
|
|
"brand",
|
|
"category",
|
|
"count",
|
|
"depth",
|
|
"outline_sections",
|
|
"key_points",
|
|
"keywords",
|
|
"competitors",
|
|
}
|
|
|
|
used := make(map[string]struct{}, len(params))
|
|
lines := make([]string, 0, len(params))
|
|
|
|
appendLine := func(key string, value interface{}) {
|
|
if value == nil {
|
|
return
|
|
}
|
|
formatted := strings.TrimSpace(formatPromptValue(value))
|
|
if formatted == "" {
|
|
return
|
|
}
|
|
lines = append(lines, fmt.Sprintf("- %s: %s", key, formatted))
|
|
used[key] = struct{}{}
|
|
}
|
|
|
|
for _, key := range orderedKeys {
|
|
appendLine(key, params[key])
|
|
}
|
|
|
|
for key, value := range params {
|
|
if _, ok := used[key]; ok {
|
|
continue
|
|
}
|
|
appendLine(key, value)
|
|
}
|
|
|
|
return strings.Join(lines, "\n")
|
|
}
|
|
|
|
func formatPromptValue(value interface{}) string {
|
|
switch v := value.(type) {
|
|
case nil:
|
|
return ""
|
|
case string:
|
|
return strings.TrimSpace(v)
|
|
case []string:
|
|
return strings.Join(v, ", ")
|
|
case []interface{}:
|
|
parts := make([]string, 0, len(v))
|
|
for _, item := range v {
|
|
formatted := strings.TrimSpace(formatPromptValue(item))
|
|
if formatted != "" {
|
|
parts = append(parts, formatted)
|
|
}
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
default:
|
|
bytes, err := json.Marshal(v)
|
|
if err != nil {
|
|
return fmt.Sprint(v)
|
|
}
|
|
return string(bytes)
|
|
}
|
|
}
|
|
|
|
func resolveArticleTitle(params map[string]interface{}, markdown string) string {
|
|
if title := strings.TrimSpace(stringValue(params["title"])); title != "" {
|
|
return title
|
|
}
|
|
if topic := strings.TrimSpace(stringValue(params["topic"])); topic != "" {
|
|
return topic
|
|
}
|
|
if product := strings.TrimSpace(stringValue(params["product_name"])); product != "" {
|
|
return product + " Review"
|
|
}
|
|
if subject := strings.TrimSpace(stringValue(params["subject"])); subject != "" {
|
|
return subject + " Report"
|
|
}
|
|
|
|
for _, line := range strings.Split(markdown, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if strings.HasPrefix(line, "#") {
|
|
return strings.TrimSpace(strings.TrimLeft(line, "#"))
|
|
}
|
|
}
|
|
return "Generated Article"
|
|
}
|
|
|
|
func estimateWordCount(markdown string) int {
|
|
trimmed := strings.TrimSpace(markdown)
|
|
if trimmed == "" {
|
|
return 0
|
|
}
|
|
|
|
words := len(strings.Fields(trimmed))
|
|
if words > 0 {
|
|
return words
|
|
}
|
|
|
|
return utf8.RuneCountInString(trimmed)
|
|
}
|
|
|
|
func stringValue(value interface{}) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
text, ok := value.(string)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
return text
|
|
}
|