add ai feature
This commit is contained in:
197
internal/ai/flow/rename_flow.go
Normal file
197
internal/ai/flow/rename_flow.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"github.com/firebase/genkit/go/core"
|
||||
"github.com/firebase/genkit/go/genkit"
|
||||
)
|
||||
|
||||
//go:embed prompt.tmpl
|
||||
var promptTemplateSource string
|
||||
|
||||
var (
|
||||
promptTemplate = template.Must(template.New("renameFlowPrompt").Parse(promptTemplateSource))
|
||||
)
|
||||
|
||||
// RenameFlowInput mirrors the JSON payload passed into the Genkit flow.
|
||||
type RenameFlowInput struct {
|
||||
FileNames []string `json:"fileNames"`
|
||||
UserPrompt string `json:"userPrompt"`
|
||||
SequenceSeparator string `json:"sequenceSeparator,omitempty"`
|
||||
}
|
||||
|
||||
// Validate ensures the flow input is well formed.
|
||||
func (in *RenameFlowInput) Validate() error {
|
||||
if in == nil {
|
||||
return errors.New("rename flow input cannot be nil")
|
||||
}
|
||||
if len(in.FileNames) == 0 {
|
||||
return errors.New("no file names provided to rename flow")
|
||||
}
|
||||
if len(in.FileNames) > 200 {
|
||||
return fmt.Errorf("rename flow supports up to 200 files per invocation (received %d)", len(in.FileNames))
|
||||
}
|
||||
normalized := make([]string, len(in.FileNames))
|
||||
for i, name := range in.FileNames {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return fmt.Errorf("file name at index %d is empty", i)
|
||||
}
|
||||
normalized[i] = toSlash(trimmed)
|
||||
}
|
||||
// Ensure no duplicates to simplify downstream validation.
|
||||
if dup := firstDuplicate(normalized); dup != "" {
|
||||
return fmt.Errorf("duplicate file name %q detected in flow input", dup)
|
||||
}
|
||||
in.FileNames = normalized
|
||||
|
||||
sep := strings.TrimSpace(in.SequenceSeparator)
|
||||
if sep == "" {
|
||||
sep = "."
|
||||
}
|
||||
if strings.ContainsAny(sep, "/\\") {
|
||||
return fmt.Errorf("sequence separator %q cannot contain path separators", sep)
|
||||
}
|
||||
if strings.ContainsAny(sep, "\n\r") {
|
||||
return errors.New("sequence separator cannot contain newline characters")
|
||||
}
|
||||
in.SequenceSeparator = sep
|
||||
return nil
|
||||
}
|
||||
|
||||
// RenderPrompt materialises the prompt template for the provided input.
|
||||
func RenderPrompt(input RenameFlowInput) (string, error) {
|
||||
if err := input.Validate(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
if err := promptTemplate.Execute(&builder, input); err != nil {
|
||||
return "", fmt.Errorf("render rename prompt: %w", err)
|
||||
}
|
||||
return builder.String(), nil
|
||||
}
|
||||
|
||||
// Define registers the rename flow on the supplied Genkit instance.
|
||||
func Define(g *genkit.Genkit) *core.Flow[*RenameFlowInput, *Output, struct{}] {
|
||||
if g == nil {
|
||||
panic("genkit instance cannot be nil")
|
||||
}
|
||||
return genkit.DefineFlow(g, "renameFlow", flowFn)
|
||||
}
|
||||
|
||||
func flowFn(ctx context.Context, input *RenameFlowInput) (*Output, error) {
|
||||
if err := input.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefix := slugify(input.UserPrompt)
|
||||
suggestions := make([]Suggestion, 0, len(input.FileNames))
|
||||
dirCounters := make(map[string]int)
|
||||
|
||||
for _, name := range input.FileNames {
|
||||
suggestion := deterministicSuggestion(name, prefix, dirCounters, input.SequenceSeparator)
|
||||
suggestions = append(suggestions, Suggestion{
|
||||
Original: name,
|
||||
Suggested: suggestion,
|
||||
})
|
||||
}
|
||||
|
||||
sort.SliceStable(suggestions, func(i, j int) bool {
|
||||
return suggestions[i].Original < suggestions[j].Original
|
||||
})
|
||||
|
||||
return &Output{Suggestions: suggestions}, nil
|
||||
}
|
||||
|
||||
func deterministicSuggestion(rel string, promptPrefix string, dirCounters map[string]int, separator string) string {
|
||||
rel = toSlash(rel)
|
||||
dir := path.Dir(rel)
|
||||
if dir == "." {
|
||||
dir = ""
|
||||
}
|
||||
|
||||
base := path.Base(rel)
|
||||
ext := path.Ext(base)
|
||||
name := strings.TrimSuffix(base, ext)
|
||||
|
||||
sanitizedName := slugify(name)
|
||||
|
||||
candidate := sanitizedName
|
||||
if promptPrefix != "" {
|
||||
switch {
|
||||
case candidate == "":
|
||||
candidate = promptPrefix
|
||||
default:
|
||||
candidate = fmt.Sprintf("%s-%s", promptPrefix, candidate)
|
||||
}
|
||||
}
|
||||
|
||||
if candidate == "" {
|
||||
candidate = "renamed"
|
||||
}
|
||||
|
||||
counterKey := dir
|
||||
dirCounters[counterKey]++
|
||||
seq := dirCounters[counterKey]
|
||||
|
||||
sep := separator
|
||||
if sep == "" {
|
||||
sep = "."
|
||||
}
|
||||
numbered := fmt.Sprintf("%02d%s%s", seq, sep, candidate)
|
||||
proposed := numbered + ext
|
||||
if dir != "" {
|
||||
return path.Join(dir, proposed)
|
||||
}
|
||||
return proposed
|
||||
}
|
||||
|
||||
func slugify(value string) string {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
var b strings.Builder
|
||||
b.Grow(len(value))
|
||||
lastHyphen := false
|
||||
for _, r := range value {
|
||||
switch {
|
||||
case unicode.IsLetter(r) || unicode.IsDigit(r):
|
||||
b.WriteRune(unicode.ToLower(r))
|
||||
lastHyphen = false
|
||||
case r == ' ' || r == '-' || r == '_' || r == '.':
|
||||
if !lastHyphen && b.Len() > 0 {
|
||||
b.WriteRune('-')
|
||||
lastHyphen = true
|
||||
}
|
||||
}
|
||||
}
|
||||
result := strings.Trim(b.String(), "-")
|
||||
return result
|
||||
}
|
||||
|
||||
func toSlash(pathStr string) string {
|
||||
return strings.ReplaceAll(pathStr, "\\", "/")
|
||||
}
|
||||
|
||||
func firstDuplicate(values []string) string {
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, v := range values {
|
||||
lower := strings.ToLower(v)
|
||||
if _, exists := seen[lower]; exists {
|
||||
return v
|
||||
}
|
||||
seen[lower] = struct{}{}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user