Skip to content

Instantly share code, notes, and snippets.

@imran31415
Last active December 24, 2025 17:51
Show Gist options
  • Select an option

  • Save imran31415/369c0d9b3bd5afa849fc0b100bdcd7ae to your computer and use it in GitHub Desktop.

Select an option

Save imran31415/369c0d9b3bd5afa849fc0b100bdcd7ae to your computer and use it in GitHub Desktop.
package llm_server
import (
"comments/models"
pb "comments/proto/llm/proto"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// HandleFormGreeting generates a natural language greeting message for form fields
func (s *LLMServer) HandleFormGreeting(
ctx context.Context,
req *pb.AskQuestionRequest,
user *models.User,
startTime time.Time,
) (*pb.AskQuestionResponse, error) {
env := os.Getenv("ENVIRONMENT")
var greeting string
var displayTemplate string
var copyTemplate string
switch env {
case "", "development", "local":
// Dev mode - generate greeting and templates from form fields
greeting, displayTemplate, copyTemplate = s.generateMockFormGreetingWithTemplate(req)
default:
// Production mode - use LLM
prompt := s.buildFormGreetingPrompt(req)
ollamaReq := OllamaRequest{
Model: "phi3:mini",
Prompt: prompt,
Stream: false,
NumPredict: 300,
Temperature: 0.7,
TopP: 0.9,
Threads: 3,
}
jsonData, err := json.Marshal(ollamaReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %v", err)
}
ollamaURL := os.Getenv("OLLAMA_API_URL")
if ollamaURL == "" {
ollamaURL = "envurl/api/generate"
}
client := &http.Client{
Timeout: 30 * time.Second,
}
resp, err := client.Post(ollamaURL, "application/json", strings.NewReader(string(jsonData)))
if err != nil {
// Fallback to mock on error
greeting, displayTemplate, copyTemplate = s.generateMockFormGreetingWithTemplate(req)
} else {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body)
if err == nil {
var ollamaResp struct {
Response string `json:"response"`
}
if json.Unmarshal(bodyBytes, &ollamaResp) == nil && ollamaResp.Response != "" {
greeting = strings.TrimSpace(ollamaResp.Response)
}
}
}
if greeting == "" {
greeting, displayTemplate, copyTemplate = s.generateMockFormGreetingWithTemplate(req)
} else {
// Generate templates separately for production
displayTemplate, copyTemplate = s.generateFormTemplates(req)
}
}
}
return &pb.AskQuestionResponse{
Response: &pb.LLMResponse{
Answer: greeting,
ConfidenceScore: 0.9,
ModelName: "phi3:mini",
TokensUsed: 50,
LatencyMs: time.Since(startTime).Milliseconds(),
UserPromptTemplate: displayTemplate,
UserPromptTemplateSimple: copyTemplate,
},
}, nil
}
// generateFormTemplates creates two template strings: one with help text for display, one simplified for copy
func (s *LLMServer) generateFormTemplates(req *pb.AskQuestionRequest) (displayTemplate string, copyTemplate string) {
fields := req.GetFormFields()
var displayLines []string
var copyLines []string
for _, field := range fields {
if !field.Required {
continue
}
// Use AI hint or convert field name to readable format
label := field.AiHint
if label == "" {
name := strings.ReplaceAll(field.FieldName, "_", " ")
name = strings.ReplaceAll(name, "String", "")
// Title case
words := strings.Fields(name)
for i, word := range words {
if len(word) > 0 {
words[i] = strings.ToUpper(string(word[0])) + strings.ToLower(word[1:])
}
}
label = strings.Join(words, " ")
} else {
// Extract just the label part from AI hint
if idx := strings.Index(label, "("); idx > 0 {
label = strings.TrimSpace(label[:idx])
}
}
// Skip confirmation fields
if strings.Contains(strings.ToLower(label), "confirm") {
continue
}
// Build the simple template line (just field name)
simpleLine := label + ": "
copyLines = append(copyLines, simpleLine)
// Build the display template line with options
displayLine := label + ":"
if len(field.Options) > 0 && (field.InputType == "select" || field.InputType == "tagSelect") {
// Show options in parentheses
optionsStr := strings.Join(field.Options, ", ")
displayLine = label + " (" + optionsStr + "):"
}
displayLines = append(displayLines, displayLine+" ")
}
return strings.Join(displayLines, "\n"), strings.Join(copyLines, "\n")
}
// generateFormTemplate creates a template string for users to fill out (kept for backward compatibility)
func (s *LLMServer) generateFormTemplate(req *pb.AskQuestionRequest) string {
display, _ := s.generateFormTemplates(req)
return display
}
// generateMockFormGreetingWithTemplate generates a friendly greeting and both templates for dev mode
func (s *LLMServer) generateMockFormGreetingWithTemplate(req *pb.AskQuestionRequest) (greeting string, displayTemplate string, copyTemplate string) {
fields := req.GetFormFields()
fmt.Printf("[FORM_GREETING] Received %d fields\n", len(fields))
// Extract field labels for natural language
var fieldDescriptions []string
for _, field := range fields {
fmt.Printf("[FORM_GREETING] Field: %s, Required: %t, AiHint: %s\n", field.FieldName, field.Required, field.AiHint)
if !field.Required {
continue
}
// Use AI hint or create from field name
desc := field.AiHint
if desc == "" {
// Convert field name to readable format
name := strings.ReplaceAll(field.FieldName, "_", " ")
name = strings.ReplaceAll(name, "String", "")
desc = strings.ToLower(name)
} else {
// Extract just the label part
if idx := strings.Index(desc, "("); idx > 0 {
desc = strings.TrimSpace(desc[:idx])
}
desc = strings.ToLower(desc)
}
if desc != "" && desc != "confirmation" {
fieldDescriptions = append(fieldDescriptions, desc)
}
}
// Limit to first 4 fields
if len(fieldDescriptions) > 4 {
fieldDescriptions = fieldDescriptions[:4]
}
var fieldList string
if len(fieldDescriptions) > 0 {
if len(fieldDescriptions) == 1 {
fieldList = fieldDescriptions[0]
} else if len(fieldDescriptions) == 2 {
fieldList = fieldDescriptions[0] + " and " + fieldDescriptions[1]
} else {
last := fieldDescriptions[len(fieldDescriptions)-1]
fieldList = strings.Join(fieldDescriptions[:len(fieldDescriptions)-1], ", ") + ", and " + last
}
}
greeting = "Hi! I can help you fill out this form quickly. Just describe what you want to create in natural language, and I'll extract the relevant information."
if fieldList != "" {
greeting += fmt.Sprintf("\n\nI'll need details like %s.", fieldList)
}
greeting += "\n\nYou can also copy the template below and fill it out:"
// Generate both templates
displayTemplate, copyTemplate = s.generateFormTemplates(req)
return greeting, displayTemplate, copyTemplate
}
// buildFormGreetingPrompt builds the prompt for generating a form greeting
func (s *LLMServer) buildFormGreetingPrompt(req *pb.AskQuestionRequest) string {
fields := req.GetFormFields()
var sb strings.Builder
sb.WriteString("You are a friendly AI assistant helping users fill out forms. Generate a brief, welcoming greeting message.\n\n")
sb.WriteString("FORM FIELDS:\n")
for _, field := range fields {
if field.Required {
sb.WriteString(fmt.Sprintf("- %s", field.AiHint))
if field.AiHint == "" {
sb.WriteString(field.FieldName)
}
sb.WriteString("\n")
}
}
sb.WriteString("\nGENERATE a friendly greeting that:\n")
sb.WriteString("1. Welcomes the user warmly\n")
sb.WriteString("2. Explains they can describe what they want in natural language\n")
sb.WriteString("3. Mentions 3-4 key pieces of information needed (in plain English, not field names)\n")
sb.WriteString("4. Gives a brief example\n")
sb.WriteString("5. Keep it under 100 words\n\n")
sb.WriteString("Return ONLY the greeting message, no extra formatting.")
return sb.String()
}
// HandleFormSynthesis handles generic form field extraction from conversation
func (s *LLMServer) HandleFormSynthesis(
ctx context.Context,
req *pb.AskQuestionRequest,
user *models.User,
startTime time.Time,
) (*pb.AskQuestionResponse, error) {
var answer string
var formData *pb.FormSynthesisData
// Always use LLM for form synthesis - it's critical functionality
systemPrompt := s.buildFormSynthesisPrompt(req)
userPrompt := s.buildFormSynthesisUserPrompt(req)
fullPrompt := systemPrompt + "\n\nUser: " + userPrompt + "\n\nAssistant:"
ollamaReq := OllamaRequest{
Model: "phi3:mini",
Prompt: fullPrompt,
Stream: false,
NumPredict: 1000,
Temperature: 0.1, // Very low for deterministic JSON output
TopP: 0.9,
Threads: 3,
}
jsonData, err := json.Marshal(ollamaReq)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %v", err)
}
ollamaURL := os.Getenv("OLLAMA_API_URL")
if ollamaURL == "" {
ollamaURL = "http://envurl/api/generate"
}
client := &http.Client{
Timeout: 120 * time.Second,
}
resp, err := client.Post(ollamaURL, "application/json", strings.NewReader(string(jsonData)))
if err != nil {
return nil, fmt.Errorf("failed to call Ollama: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("Ollama service returned non-200: %d. Response: %s", resp.StatusCode, string(bodyBytes))
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %v", err)
}
var ollamaResp struct {
Response string `json:"response"`
}
if err := json.Unmarshal(bodyBytes, &ollamaResp); err != nil {
return nil, fmt.Errorf("failed to decode JSON response: %v. Raw response: %s", err, string(bodyBytes[:Min(200, len(bodyBytes))]))
}
if ollamaResp.Response == "" {
return nil, fmt.Errorf("received empty response from Ollama service")
}
fmt.Printf("[FORM_SYNTHESIS] Raw LLM Response: %s\n", ollamaResp.Response)
// Parse the form synthesis response
formData, answer = s.parseFormSynthesisResponse(ollamaResp.Response, req.GetFormFields())
fmt.Printf("[FORM_SYNTHESIS] System prompt: %d chars | Fields: %d | Complete: %t | Latency: %d ms\n",
len(systemPrompt), len(req.GetFormFields()), formData.IsComplete, time.Since(startTime).Milliseconds())
return &pb.AskQuestionResponse{
Response: &pb.LLMResponse{
Answer: answer,
ConfidenceScore: 0.85,
ModelName: "phi3:mini",
TokensUsed: 100,
LatencyMs: time.Since(startTime).Milliseconds(),
},
FormSynthesisData: formData,
}, nil
}
// generateMockFormSynthesis generates mock form synthesis data for development
func (s *LLMServer) generateMockFormSynthesis(req *pb.AskQuestionRequest) *pb.FormSynthesisData {
fields := req.GetFormFields()
currentData := req.GetCurrentFormData()
question := req.GetQuestion()
conversationHistory := req.GetConversationHistory()
extractedFields := make([]*pb.ExtractedField, 0)
missingFields := make([]string, 0)
// In dev mode, try to extract some fields based on simple heuristics
questionLower := strings.ToLower(question)
// Try to extract field values from patterns like "fieldName is value" or "fieldName: value"
extractedFromMessage := s.extractFieldValuesFromMessage(questionLower, fields)
// Check conversation history to understand context - if the last assistant message
// was asking about a specific field and the user response is just a value, map it
contextField := s.extractContextFromHistory(conversationHistory, fields, question)
if contextField != "" {
// User provided a bare value in response to a specific question
if _, exists := extractedFromMessage[contextField]; !exists {
extractedFromMessage[contextField] = strings.TrimSpace(question)
fmt.Printf("[FORM_SYNTHESIS] Extracted '%s' for field '%s' from conversation context\n", question, contextField)
}
}
for _, field := range fields {
// Skip if already filled
if val, exists := currentData[field.FieldName]; exists && val != "" {
extractedFields = append(extractedFields, &pb.ExtractedField{
FieldName: field.FieldName,
Value: val,
Confidence: 1.0,
})
continue
}
// Check if this field was extracted from the message
var extracted string
var confidence float32
if val, exists := extractedFromMessage[field.FieldName]; exists {
extracted = val
confidence = 0.9
}
// If not extracted from message patterns, try other heuristics
if extracted == "" {
switch field.InputType {
case "text":
if strings.Contains(field.FieldName, "name") || strings.Contains(field.FieldName, "title") {
// Extract first quoted string or capitalized phrase
if idx := strings.Index(question, "\""); idx != -1 {
endIdx := strings.Index(question[idx+1:], "\"")
if endIdx != -1 {
extracted = question[idx+1 : idx+1+endIdx]
confidence = 0.9
}
}
}
case "datetime":
// Check for common time patterns
if strings.Contains(questionLower, "today") || strings.Contains(questionLower, "tomorrow") ||
strings.Contains(questionLower, "pm") || strings.Contains(questionLower, "am") {
loc, _ := time.LoadLocation("America/Los_Angeles")
t := time.Now().In(loc).Add(2 * time.Hour)
extracted = t.Format(time.RFC3339)
confidence = 0.7
}
case "select":
// Check if any option is mentioned
for _, opt := range field.Options {
if strings.Contains(questionLower, strings.ToLower(opt)) {
extracted = opt
confidence = 0.9
break
}
}
}
}
if extracted != "" {
extractedFields = append(extractedFields, &pb.ExtractedField{
FieldName: field.FieldName,
Value: extracted,
Confidence: confidence,
})
} else {
// Use default value or skip value instead of marking as missing
var defaultVal string
var defaultConfidence float32 = 0.5
if field.DefaultValue != "" {
defaultVal = field.DefaultValue
} else if field.SkipValue != "" {
defaultVal = field.SkipValue
} else if field.CanUseDefault {
// Generate sensible defaults based on field type/name
switch {
case strings.Contains(strings.ToLower(field.FieldName), "location") || strings.Contains(strings.ToLower(field.FieldName), "city"):
defaultVal = "Santa Barbara"
case strings.Contains(strings.ToLower(field.FieldName), "address"):
defaultVal = "Santa Barbara, CA"
case strings.Contains(strings.ToLower(field.FieldName), "duration"):
defaultVal = "2"
case strings.Contains(strings.ToLower(field.FieldName), "public") || strings.Contains(strings.ToLower(field.FieldName), "visibility"):
if len(field.Options) > 0 {
defaultVal = field.Options[0]
} else {
defaultVal = "true"
}
case strings.Contains(strings.ToLower(field.FieldName), "confirm"):
defaultVal = "yes"
case strings.Contains(strings.ToLower(field.FieldName), "tag") || strings.Contains(strings.ToLower(field.FieldName), "interest"):
defaultVal = ""
case len(field.Options) > 0:
defaultVal = field.Options[0]
default:
defaultVal = ""
}
}
if defaultVal != "" || field.CanUseDefault {
extractedFields = append(extractedFields, &pb.ExtractedField{
FieldName: field.FieldName,
Value: defaultVal,
Confidence: defaultConfidence,
})
} else if field.Required {
// Only mark as missing if truly required and no default available
missingFields = append(missingFields, field.FieldName)
}
}
}
// In dev mode with defaults, most forms should be complete
isComplete := len(missingFields) == 0
// Generate follow-up question only if there are truly missing required fields
var followupQuestion string
if len(missingFields) > 0 {
followupQuestion = s.generateFollowupQuestion(missingFields, fields)
}
// Generate summary
var summary strings.Builder
summary.WriteString("DEV MODE - Extracted fields: ")
for i, field := range extractedFields {
if i > 0 {
summary.WriteString(", ")
}
summary.WriteString(fmt.Sprintf("%s=%s", field.FieldName, field.Value))
}
return &pb.FormSynthesisData{
ExtractedFields: extractedFields,
MissingFields: missingFields,
FollowupQuestion: followupQuestion,
IsComplete: isComplete,
Summary: summary.String(),
}
}
// buildFormSynthesisPrompt builds the system prompt for form synthesis
func (s *LLMServer) buildFormSynthesisPrompt(req *pb.AskQuestionRequest) string {
fields := req.GetFormFields()
currentData := req.GetCurrentFormData()
loc, _ := time.LoadLocation("America/Los_Angeles")
now := time.Now().In(loc)
nowFormatted := now.Format("Mon Jan 2, 2006 3:04 PM MST")
todayDate := now.Format("2006-01-02")
var sb strings.Builder
sb.WriteString("You are a helpful form-filling assistant. Your goal is to FILL OUT AS MUCH AS POSSIBLE using sensible defaults. The user can always edit later.\n\n")
sb.WriteString("CURRENT DATE & TIME: ")
sb.WriteString(nowFormatted)
sb.WriteString("\n\n")
sb.WriteString("FORM FIELDS TO FILL:\n")
for _, field := range fields {
sb.WriteString(fmt.Sprintf("- %s (%s)", field.FieldName, field.InputType))
if field.Required {
sb.WriteString(" [REQUIRED]")
} else {
sb.WriteString(" [OPTIONAL]")
}
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf(" Prompt: %s\n", field.Prompt))
if field.AiHint != "" {
sb.WriteString(fmt.Sprintf(" Hint: %s\n", field.AiHint))
}
if len(field.Options) > 0 {
sb.WriteString(fmt.Sprintf(" Options: %s\n", strings.Join(field.Options, ", ")))
}
// Show default value info
if field.CanUseDefault {
if field.DefaultValue != "" {
sb.WriteString(fmt.Sprintf(" DEFAULT: Use '%s' if not specified\n", field.DefaultValue))
} else if field.SkipValue != "" {
sb.WriteString(fmt.Sprintf(" SKIPPABLE: Use '%s' to skip\n", field.SkipValue))
} else {
sb.WriteString(" CAN DEFAULT: Use sensible default if not specified\n")
}
}
}
if len(currentData) > 0 {
sb.WriteString("\nALREADY FILLED FIELDS (keep these values):\n")
for key, value := range currentData {
sb.WriteString(fmt.Sprintf("- %s: %s\n", key, value))
}
}
sb.WriteString("\nGUIDELINES:\n")
sb.WriteString("- If the user provides a value, use it\n")
sb.WriteString("- Ask follow-up questions for important missing fields (name, date/time, location)\n")
sb.WriteString("- If a field has a DEFAULT value, use it when user doesn't specify after 1-2 exchanges\n")
sb.WriteString("- For select fields, default to the first option if not specified\n")
sb.WriteString(fmt.Sprintf("- Today's date is: %s\n", todayDate))
sb.WriteString("- If the conversation has gone 3+ exchanges, fill remaining fields with defaults and complete the form\n")
sb.WriteString("- Balance being helpful (asking questions) with being efficient (using defaults)\n\n")
sb.WriteString("RESPONSE FORMAT - Use this EXACT format:\n")
sb.WriteString("```\n")
sb.WriteString("FIELD: field_name = value\n")
sb.WriteString("FIELD: another_field = another value\n")
sb.WriteString("SUMMARY: Brief description\n")
sb.WriteString("```\n\n")
sb.WriteString("RULES:\n")
sb.WriteString("- Output one FIELD line for EVERY field\n")
sb.WriteString("- Values must be CLEAN - no explanations, no parentheses, no annotations\n")
sb.WriteString("- WRONG: FIELD: name = My Business (default value)\n")
sb.WriteString("- RIGHT: FIELD: name = My Business\n")
sb.WriteString("- Use the exact field_name from the form fields\n")
sb.WriteString("- For datetime fields, use ISO 8601 format (e.g., ")
sb.WriteString(todayDate)
sb.WriteString("T14:00:00-08:00)\n")
sb.WriteString("- For select fields, use values from the provided options list\n")
sb.WriteString("- Keep already-filled field values unless user explicitly changes them\n")
sb.WriteString("- End with a SUMMARY line\n")
return sb.String()
}
// buildFormSynthesisUserPrompt builds the user prompt including conversation history
func (s *LLMServer) buildFormSynthesisUserPrompt(req *pb.AskQuestionRequest) string {
var sb strings.Builder
// Include conversation history
if len(req.GetConversationHistory()) > 0 {
sb.WriteString("CONVERSATION HISTORY:\n")
for _, msg := range req.GetConversationHistory() {
sb.WriteString(fmt.Sprintf("%s: %s\n", strings.Title(msg.Role), msg.Content))
}
sb.WriteString("\n")
}
sb.WriteString("LATEST USER MESSAGE:\n")
sb.WriteString(req.GetQuestion())
return sb.String()
}
// parseFormSynthesisResponse parses the LLM response for form synthesis
// Expects format: "FIELD: field_name = value" lines
func (s *LLMServer) parseFormSynthesisResponse(response string, fields []*pb.FormFieldDefinition) (*pb.FormSynthesisData, string) {
fmt.Printf("[FORM_SYNTHESIS] Parsing response: %s\n", response)
// Build a map of valid field names for quick lookup
validFields := make(map[string]bool)
fmt.Printf("[FORM_SYNTHESIS] Expected fields: ")
for i, field := range fields {
validFields[field.FieldName] = true
// Also add lowercase version
validFields[strings.ToLower(field.FieldName)] = true
if i > 0 {
fmt.Printf(", ")
}
fmt.Printf("%s", field.FieldName)
}
fmt.Printf("\n")
// Parse the simple line-based format
extractedFields := make([]*pb.ExtractedField, 0)
extractedFieldNames := make(map[string]bool)
var summary string
lines := strings.Split(response, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
continue
}
// Parse FIELD: field_name = value
if strings.HasPrefix(line, "FIELD:") {
rest := strings.TrimPrefix(line, "FIELD:")
rest = strings.TrimSpace(rest)
// Split on first "="
parts := strings.SplitN(rest, "=", 2)
if len(parts) == 2 {
fieldName := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
// Find the actual field name (case-insensitive match)
actualFieldName := fieldName
for _, field := range fields {
if strings.EqualFold(field.FieldName, fieldName) {
actualFieldName = field.FieldName
break
}
}
if value != "" && value != "null" && value != "N/A" {
extractedFields = append(extractedFields, &pb.ExtractedField{
FieldName: actualFieldName,
Value: value,
Confidence: 0.9,
})
extractedFieldNames[actualFieldName] = true
}
}
}
// Parse SUMMARY: text
if strings.HasPrefix(line, "SUMMARY:") {
summary = strings.TrimSpace(strings.TrimPrefix(line, "SUMMARY:"))
}
}
// Find missing required fields
missingFields := make([]string, 0)
for _, field := range fields {
if field.Required && !extractedFieldNames[field.FieldName] {
missingFields = append(missingFields, field.FieldName)
fmt.Printf("[FORM_SYNTHESIS] Missing required field: %s\n", field.FieldName)
}
}
// Determine if complete
isComplete := len(missingFields) == 0
// Generate follow-up question if needed
var followupQuestion string
if !isComplete {
followupQuestion = s.generateFollowupQuestion(missingFields, fields)
}
if summary == "" {
summary = fmt.Sprintf("Extracted %d fields", len(extractedFields))
}
formData := &pb.FormSynthesisData{
ExtractedFields: extractedFields,
MissingFields: missingFields,
FollowupQuestion: followupQuestion,
IsComplete: isComplete,
Summary: summary,
}
var answer string
if isComplete {
answer = fmt.Sprintf("Form complete! %s", summary)
} else {
answer = fmt.Sprintf("Extracted %d fields. %s", len(extractedFields), followupQuestion)
}
fmt.Printf("[FORM_SYNTHESIS] Parsed %d fields, %d missing, complete: %t\n",
len(extractedFields), len(missingFields), isComplete)
return formData, answer
}
// getRequiredFieldNames returns the names of all required fields
func (s *LLMServer) getRequiredFieldNames(fields []*pb.FormFieldDefinition) []string {
required := make([]string, 0)
for _, field := range fields {
if field.Required {
required = append(required, field.FieldName)
}
}
return required
}
// generateFollowupQuestion generates a natural follow-up question for missing fields
func (s *LLMServer) generateFollowupQuestion(missingFields []string, fields []*pb.FormFieldDefinition) string {
if len(missingFields) == 0 {
return ""
}
// Get the prompts and hints for missing fields
fieldInfo := make(map[string]*pb.FormFieldDefinition)
for _, field := range fields {
fieldInfo[field.FieldName] = field
}
// Convert field names to natural language descriptions
getNaturalName := func(fieldName string) string {
field := fieldInfo[fieldName]
if field == nil {
return fieldName
}
// Use AI hint if available, it's usually more descriptive
if field.AiHint != "" {
hint := strings.ToLower(field.AiHint)
// Extract just the label part before any parentheses
if idx := strings.Index(hint, "("); idx > 0 {
hint = strings.TrimSpace(hint[:idx])
}
if hint != "" {
return hint
}
}
// Convert common field names to natural language
nameLower := strings.ToLower(fieldName)
switch {
case strings.Contains(nameLower, "addressstring") || strings.Contains(nameLower, "address"):
return "the address"
case strings.Contains(nameLower, "duration"):
return "the duration"
case strings.Contains(nameLower, "name"):
return "a name"
case strings.Contains(nameLower, "description"):
return "a description"
case strings.Contains(nameLower, "location") || strings.Contains(nameLower, "city"):
return "the location"
case strings.Contains(nameLower, "time") || strings.Contains(nameLower, "date"):
return "the date and time"
case strings.Contains(nameLower, "image"):
return "an image"
case strings.Contains(nameLower, "interest") || strings.Contains(nameLower, "tag"):
return "the categories"
default:
return fieldName
}
}
if len(missingFields) == 1 {
// For single field, ask directly
naturalName := getNaturalName(missingFields[0])
return fmt.Sprintf("Could you tell me %s?", naturalName)
}
// Multiple missing fields - create a combined question
var sb strings.Builder
sb.WriteString("I still need a few details: ")
for i, fieldName := range missingFields {
naturalName := getNaturalName(fieldName)
if i > 0 {
if i == len(missingFields)-1 {
sb.WriteString(", and ")
} else {
sb.WriteString(", ")
}
}
sb.WriteString(naturalName)
}
sb.WriteString(". Could you provide these?")
return sb.String()
}
// extractContextFromHistory checks the conversation history to determine what field
// the assistant was asking about, so we can map bare user responses to the correct field
func (s *LLMServer) extractContextFromHistory(history []*pb.ConversationMessage, fields []*pb.FormFieldDefinition, userResponse string) string {
if len(history) == 0 {
return ""
}
// Find the last assistant message
var lastAssistantMsg string
for i := len(history) - 1; i >= 0; i-- {
if history[i].Role == "assistant" {
lastAssistantMsg = strings.ToLower(history[i].Content)
break
}
}
if lastAssistantMsg == "" {
return ""
}
// Check if the user response looks like just a value (no field names mentioned)
userResponseLower := strings.ToLower(userResponse)
hasFieldMention := false
for _, field := range fields {
fieldNameLower := strings.ToLower(field.FieldName)
if strings.Contains(userResponseLower, fieldNameLower) {
hasFieldMention = true
break
}
}
// If user mentioned a field name, let the normal extraction handle it
if hasFieldMention {
return ""
}
// Find which field the assistant was asking about
// Check for patterns like "Could you tell me X?" or "I still need X"
for _, field := range fields {
fieldNameLower := strings.ToLower(field.FieldName)
// Convert camelCase/snake_case to readable form for matching
readableName := strings.ReplaceAll(fieldNameLower, "_", " ")
readableName = strings.ReplaceAll(readableName, "string", "")
readableName = strings.TrimSpace(readableName)
// Check various patterns that indicate the assistant was asking about this field
patterns := []string{
"could you tell me " + readableName,
"could you tell me " + fieldNameLower,
"could you provide " + readableName,
"could you provide " + fieldNameLower,
"what is the " + readableName,
"what is the " + fieldNameLower,
"what's the " + readableName,
"what's the " + fieldNameLower,
"need " + readableName,
"need " + fieldNameLower,
"missing: " + readableName,
"missing: " + fieldNameLower,
}
// Also check AI hints
if field.AiHint != "" {
hintLower := strings.ToLower(field.AiHint)
// Extract key phrase from hint
if idx := strings.Index(hintLower, "("); idx > 0 {
hintLower = strings.TrimSpace(hintLower[:idx])
}
patterns = append(patterns, "could you tell me "+hintLower)
patterns = append(patterns, "need "+hintLower)
}
for _, pattern := range patterns {
if strings.Contains(lastAssistantMsg, pattern) {
fmt.Printf("[FORM_SYNTHESIS] Found context: assistant asked about '%s' (field: %s)\n", pattern, field.FieldName)
return field.FieldName
}
}
}
// Fallback: if the assistant message ends with asking about exactly one missing field
// Look for "I still need a few details: X" or "Could you tell me X?"
for _, field := range fields {
fieldNameLower := strings.ToLower(field.FieldName)
readableName := strings.ReplaceAll(fieldNameLower, "_", " ")
readableName = strings.ReplaceAll(readableName, "string", "")
readableName = strings.TrimSpace(readableName)
// Check if this is the ONLY field mentioned in the question
if strings.Contains(lastAssistantMsg, readableName+"?") || strings.HasSuffix(strings.TrimSpace(lastAssistantMsg), readableName+"?") {
fmt.Printf("[FORM_SYNTHESIS] Found single-field question for '%s'\n", field.FieldName)
return field.FieldName
}
}
return ""
}
func (s *LLMServer) extractFieldValuesFromMessage(message string, fields []*pb.FormFieldDefinition) map[string]string {
extracted := make(map[string]string)
// Build a list of all possible field labels (for finding boundaries)
var allFieldLabels []string
for _, field := range fields {
fieldNameLower := strings.ToLower(field.FieldName)
readableName := strings.ReplaceAll(fieldNameLower, "_", " ")
readableName = strings.ReplaceAll(readableName, "string", "")
readableName = strings.TrimSpace(readableName)
allFieldLabels = append(allFieldLabels, fieldNameLower)
if readableName != fieldNameLower {
allFieldLabels = append(allFieldLabels, readableName)
}
}
for _, field := range fields {
fieldNameLower := strings.ToLower(field.FieldName)
// Also try readable name (e.g., "business name" instead of "name")
readableName := strings.ReplaceAll(fieldNameLower, "_", " ")
readableName = strings.ReplaceAll(readableName, "string", "")
readableName = strings.TrimSpace(readableName)
// Try multiple patterns to extract values
patterns := []string{
fieldNameLower + " is ",
fieldNameLower + ": ",
fieldNameLower + "=",
fieldNameLower + " = ",
}
// Also add readable name patterns
if readableName != fieldNameLower {
patterns = append(patterns,
readableName + " is ",
readableName + ": ",
readableName + "=",
readableName + " = ",
)
}
// Also match patterns with options in parentheses like "status (active, inactive):"
patterns = append(patterns, fieldNameLower+" (")
if readableName != fieldNameLower {
patterns = append(patterns, readableName+" (")
}
for _, pattern := range patterns {
idx := strings.Index(message, pattern)
if idx != -1 {
// If pattern ends with "(", find the closing ")" and then ":"
valueStart := idx + len(pattern)
if strings.HasSuffix(pattern, "(") {
// Find closing parenthesis
closeIdx := strings.Index(message[valueStart:], "):")
if closeIdx != -1 {
valueStart = valueStart + closeIdx + 2 // Skip past "):"
} else {
continue // Malformed, skip this pattern
}
}
valueEnd := len(message)
// Find end of value - check for newline first (template format)
newlineIdx := strings.Index(message[valueStart:], "\n")
if newlineIdx != -1 {
valueEnd = valueStart + newlineIdx
}
// Also check for next field patterns on same line
for _, nextLabel := range allFieldLabels {
for _, nextPattern := range []string{" and " + nextLabel, ", " + nextLabel, "; " + nextLabel, "\n" + nextLabel} {
nextIdx := strings.Index(message[valueStart:], nextPattern)
if nextIdx != -1 && valueStart+nextIdx < valueEnd {
valueEnd = valueStart + nextIdx
}
}
}
value := strings.TrimSpace(message[valueStart:valueEnd])
// Clean up trailing punctuation
value = strings.TrimSuffix(value, ".")
value = strings.TrimSuffix(value, ",")
value = strings.TrimSuffix(value, ";")
if value != "" {
extracted[field.FieldName] = value
}
break
}
}
}
return extracted
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment