Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

319 wiersze
9.4KB

  1. package mapping
  2. import (
  3. "context"
  4. "fmt"
  5. "sort"
  6. "strings"
  7. "qctextbuilder/internal/domain"
  8. "qctextbuilder/internal/qcclient"
  9. )
  10. type SuggestionGenerator interface {
  11. Generate(ctx context.Context, req SuggestionRequest) (SuggestionResult, error)
  12. }
  13. type RuleBasedSuggestionGenerator struct{}
  14. func NewRuleBasedSuggestionGenerator() *RuleBasedSuggestionGenerator {
  15. return &RuleBasedSuggestionGenerator{}
  16. }
  17. func (g *RuleBasedSuggestionGenerator) Generate(_ context.Context, req SuggestionRequest) (SuggestionResult, error) {
  18. result := SuggestFieldValuesRuleBased(req)
  19. return result, nil
  20. }
  21. type LLMSuggestionGenerator struct {
  22. qc qcclient.Client
  23. }
  24. func NewLLMSuggestionGenerator(qc qcclient.Client) *LLMSuggestionGenerator {
  25. return &LLMSuggestionGenerator{qc: qc}
  26. }
  27. func (g *LLMSuggestionGenerator) Generate(ctx context.Context, req SuggestionRequest) (SuggestionResult, error) {
  28. if g == nil || g.qc == nil {
  29. return SuggestionResult{}, fmt.Errorf("llm generator is not configured")
  30. }
  31. if req.TemplateID <= 0 {
  32. return SuggestionResult{}, fmt.Errorf("template id is required for llm suggestions")
  33. }
  34. fieldByPath := make(map[string]domain.TemplateField, len(req.Fields))
  35. for _, field := range req.Fields {
  36. if !field.IsEnabled || !strings.EqualFold(strings.TrimSpace(field.FieldKind), "text") {
  37. continue
  38. }
  39. fieldByPath[field.Path] = field
  40. }
  41. targets := collectSuggestionTargets(req.Fields, req.Existing, req.IncludeFilled)
  42. customTemplateData := map[string]any{
  43. "_autofill": map[string]any{
  44. "masterPrompt": strings.TrimSpace(req.MasterPrompt),
  45. "promptBlocks": enabledPromptBlocks(req.PromptBlocks),
  46. "draftContext": llmDraftContextMap(req.DraftContext),
  47. "semanticSlots": func() map[string]string {
  48. out := make(map[string]string, len(targets))
  49. for _, target := range targets {
  50. out[target.FieldPath] = target.Slot
  51. }
  52. return out
  53. }(),
  54. },
  55. }
  56. resp, _, err := g.qc.GenerateContent(ctx, qcclient.GenerateContentRequest{
  57. TemplateID: req.TemplateID,
  58. GlobalData: req.GlobalData,
  59. Empty: true,
  60. ToneOfVoice: contentTone(req.DraftContext),
  61. TargetAudience: targetAudience(req),
  62. CustomTemplateData: customTemplateData,
  63. })
  64. if err != nil {
  65. return SuggestionResult{}, err
  66. }
  67. out := SuggestionResult{
  68. Suggestions: make([]Suggestion, 0, len(targets)),
  69. ByFieldPath: map[string]Suggestion{},
  70. }
  71. for _, target := range targets {
  72. field, ok := fieldByPath[target.FieldPath]
  73. if !ok {
  74. continue
  75. }
  76. sectionData := resp[field.Section]
  77. if sectionData == nil {
  78. continue
  79. }
  80. raw, ok := sectionData[field.KeyName]
  81. if !ok {
  82. continue
  83. }
  84. value := normalizeLLMValue(raw)
  85. if value == "" {
  86. continue
  87. }
  88. suggestion := Suggestion{
  89. FieldPath: target.FieldPath,
  90. Slot: target.Slot,
  91. Value: value,
  92. Reason: "llm suggestion from template content generation",
  93. Source: domain.DraftSuggestionSourceLLM,
  94. }
  95. out.Suggestions = append(out.Suggestions, suggestion)
  96. out.ByFieldPath[target.FieldPath] = suggestion
  97. }
  98. return out, nil
  99. }
  100. type CompositeSuggestionGenerator struct {
  101. Primary SuggestionGenerator
  102. Fallback SuggestionGenerator
  103. }
  104. func NewCompositeSuggestionGenerator(primary, fallback SuggestionGenerator) *CompositeSuggestionGenerator {
  105. return &CompositeSuggestionGenerator{
  106. Primary: primary,
  107. Fallback: fallback,
  108. }
  109. }
  110. func (g *CompositeSuggestionGenerator) Generate(ctx context.Context, req SuggestionRequest) (SuggestionResult, error) {
  111. if g == nil {
  112. return SuggestionResult{}, fmt.Errorf("suggestion generator is not configured")
  113. }
  114. if g.Primary == nil {
  115. return generateFallback(ctx, g.Fallback, req)
  116. }
  117. primaryResult, err := g.Primary.Generate(ctx, req)
  118. if err != nil {
  119. return generateFallback(ctx, g.Fallback, req)
  120. }
  121. primaryResult = normalizeSuggestionResult(primaryResult, req.Fields, req.Existing, req.IncludeFilled)
  122. if g.Fallback == nil {
  123. return primaryResult, nil
  124. }
  125. fallbackResult, fbErr := g.Fallback.Generate(ctx, req)
  126. if fbErr != nil {
  127. return primaryResult, nil
  128. }
  129. fallbackResult = normalizeSuggestionResult(fallbackResult, req.Fields, req.Existing, req.IncludeFilled)
  130. merged := primaryResult
  131. if merged.ByFieldPath == nil {
  132. merged.ByFieldPath = map[string]Suggestion{}
  133. }
  134. for _, suggestion := range fallbackResult.Suggestions {
  135. if _, exists := merged.ByFieldPath[suggestion.FieldPath]; exists {
  136. continue
  137. }
  138. merged.Suggestions = append(merged.Suggestions, suggestion)
  139. merged.ByFieldPath[suggestion.FieldPath] = suggestion
  140. }
  141. sort.SliceStable(merged.Suggestions, func(i, j int) bool {
  142. return merged.Suggestions[i].FieldPath < merged.Suggestions[j].FieldPath
  143. })
  144. return merged, nil
  145. }
  146. func generateFallback(ctx context.Context, fallback SuggestionGenerator, req SuggestionRequest) (SuggestionResult, error) {
  147. if fallback == nil {
  148. return SuggestionResult{}, fmt.Errorf("fallback suggestion generator is not configured")
  149. }
  150. result, err := fallback.Generate(ctx, req)
  151. if err != nil {
  152. return SuggestionResult{}, err
  153. }
  154. return normalizeSuggestionResult(result, req.Fields, req.Existing, req.IncludeFilled), nil
  155. }
  156. func normalizeSuggestionResult(result SuggestionResult, fields []domain.TemplateField, existing map[string]string, includeFilled bool) SuggestionResult {
  157. allowed := make(map[string]SemanticSlotTarget)
  158. for _, target := range collectSuggestionTargets(fields, existing, includeFilled) {
  159. if _, exists := allowed[target.FieldPath]; exists {
  160. continue
  161. }
  162. allowed[target.FieldPath] = target
  163. }
  164. out := SuggestionResult{
  165. Suggestions: make([]Suggestion, 0, len(result.Suggestions)),
  166. ByFieldPath: map[string]Suggestion{},
  167. }
  168. for _, suggestion := range result.Suggestions {
  169. fieldPath := strings.TrimSpace(suggestion.FieldPath)
  170. if fieldPath == "" {
  171. continue
  172. }
  173. target, ok := allowed[fieldPath]
  174. if !ok {
  175. continue
  176. }
  177. value := strings.TrimSpace(suggestion.Value)
  178. if value == "" {
  179. continue
  180. }
  181. normalized := suggestion
  182. normalized.FieldPath = fieldPath
  183. if strings.TrimSpace(normalized.Slot) == "" {
  184. normalized.Slot = target.Slot
  185. }
  186. normalized.Value = value
  187. if strings.TrimSpace(normalized.Source) == "" {
  188. normalized.Source = domain.DraftSuggestionSourceFallbackRuleBased
  189. }
  190. if _, exists := out.ByFieldPath[fieldPath]; exists {
  191. continue
  192. }
  193. out.Suggestions = append(out.Suggestions, normalized)
  194. out.ByFieldPath[fieldPath] = normalized
  195. }
  196. sort.SliceStable(out.Suggestions, func(i, j int) bool {
  197. return out.Suggestions[i].FieldPath < out.Suggestions[j].FieldPath
  198. })
  199. return out
  200. }
  201. func collectSuggestionTargets(fields []domain.TemplateField, existing map[string]string, includeFilled bool) []SemanticSlotTarget {
  202. normalizedExisting := existing
  203. if normalizedExisting == nil {
  204. normalizedExisting = map[string]string{}
  205. }
  206. mappingResult := MapTemplateFieldsToSemanticSlots(fields)
  207. targets := append([]SemanticSlotTarget(nil), mappingResult.Targets...)
  208. sort.SliceStable(targets, func(i, j int) bool {
  209. if targets[i].FieldPath == targets[j].FieldPath {
  210. return targets[i].Slot < targets[j].Slot
  211. }
  212. return targets[i].FieldPath < targets[j].FieldPath
  213. })
  214. out := make([]SemanticSlotTarget, 0, len(targets))
  215. seen := map[string]struct{}{}
  216. for _, target := range targets {
  217. if _, exists := seen[target.FieldPath]; exists {
  218. continue
  219. }
  220. if !includeFilled && strings.TrimSpace(normalizedExisting[target.FieldPath]) != "" {
  221. continue
  222. }
  223. out = append(out, target)
  224. seen[target.FieldPath] = struct{}{}
  225. }
  226. return out
  227. }
  228. func enabledPromptBlocks(blocks []domain.PromptBlockConfig) []map[string]string {
  229. out := make([]map[string]string, 0, len(blocks))
  230. for _, block := range blocks {
  231. if !block.Enabled {
  232. continue
  233. }
  234. entry := map[string]string{"id": strings.TrimSpace(block.ID)}
  235. if label := strings.TrimSpace(block.Label); label != "" {
  236. entry["label"] = label
  237. }
  238. if instruction := strings.TrimSpace(block.Instruction); instruction != "" {
  239. entry["instruction"] = instruction
  240. }
  241. out = append(out, entry)
  242. }
  243. return out
  244. }
  245. func llmDraftContextMap(ctx *domain.DraftContext) map[string]any {
  246. if ctx == nil {
  247. return map[string]any{}
  248. }
  249. return map[string]any{
  250. "businessType": strings.TrimSpace(ctx.LLM.BusinessType),
  251. "websiteUrl": strings.TrimSpace(ctx.LLM.WebsiteURL),
  252. "websiteSummary": strings.TrimSpace(ctx.LLM.WebsiteSummary),
  253. "styleProfile": map[string]string{
  254. "localeStyle": strings.TrimSpace(ctx.LLM.StyleProfile.LocaleStyle),
  255. "marketStyle": strings.TrimSpace(ctx.LLM.StyleProfile.MarketStyle),
  256. "addressMode": strings.TrimSpace(ctx.LLM.StyleProfile.AddressMode),
  257. "contentTone": strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone),
  258. "promptInstructions": strings.TrimSpace(ctx.LLM.StyleProfile.PromptInstructions),
  259. },
  260. }
  261. }
  262. func contentTone(ctx *domain.DraftContext) string {
  263. if ctx == nil {
  264. return ""
  265. }
  266. return strings.TrimSpace(ctx.LLM.StyleProfile.ContentTone)
  267. }
  268. func targetAudience(req SuggestionRequest) string {
  269. ctx := suggestionContextFrom(req.GlobalData, req.DraftContext)
  270. parts := make([]string, 0, 4)
  271. if ctx.BusinessType != "" {
  272. parts = append(parts, "businessType="+ctx.BusinessType)
  273. }
  274. if ctx.LocaleStyle != "" {
  275. parts = append(parts, "locale="+ctx.LocaleStyle)
  276. }
  277. if ctx.MarketStyle != "" {
  278. parts = append(parts, "market="+ctx.MarketStyle)
  279. }
  280. if ctx.AddressMode != "" {
  281. parts = append(parts, "addressMode="+ctx.AddressMode)
  282. }
  283. return strings.Join(parts, ", ")
  284. }
  285. func normalizeLLMValue(raw any) string {
  286. switch value := raw.(type) {
  287. case string:
  288. return strings.TrimSpace(value)
  289. default:
  290. return ""
  291. }
  292. }