buildSystemPrompt function

String buildSystemPrompt(
  1. List<QuestionsModel> questions
)

Builds the system prompt for the AI form assistant (options, custom rules, and how to interpret each field's QuestionsModel.type and question text).

Implementation

String buildSystemPrompt(List<QuestionsModel> questions) {
  final fieldList = questions.map((q) {
    final typePart = q.type != null ? 'type: "${q.type!.name}"' : null;
    final opts = QuestionsModel.normalizeOptionsList(q.options);
    final optionsPart = opts.isEmpty
        ? null
        : 'options (pick one): ${opts.map((o) => '"${_escapeForPrompt(o)}"').join(', ')}';
    final vm = q.validationMessage;
    final validationPart = vm != null && vm.trim().isNotEmpty
        ? 'CUSTOM RULE (override everything): "${vm.trim()}"'
        : null;

    final segments = <String>[
      'key: "${q.key}"',
      if (typePart != null) typePart,
      if (optionsPart != null) optionsPart,
      'question: "${q.question}"',
      if (validationPart != null) validationPart,
    ];
    return '- ${segments.join(' | ')}';
  }).join('\n');

  final requiredKeys = questions.map((q) => '"${q.key}"').join(', ');

  return '''
You are a friendly registration assistant collecting user info through conversation.

Collect these fields in order:
$fieldList

Use each field's type and question text to judge format (email, phone, URL, numeric, date, text) and completeness (e.g. if the question asks for first and last name, require both unless a CUSTOM RULE says otherwise).

RESPONSE FORMAT — always return ONLY valid JSON, no markdown, no extra text:
{
  "result": true|false,
  "reason": string|null,
  "nextQuestion": string|null,
  "done": true|false,
  "data": object|null
}

VALIDATION HIERARCHY (top rule wins):

1. OPTIONS — if a field has "options (pick one)", accept only one of those exact strings (trim whitespace). On reject, list the valid choices conversationally.

2. CUSTOM RULE — if a field has a CUSTOM RULE, that is the only test for accept/reject. Apply it literally. Numeric ranges stated as "between A and B" or "from A to B" are inclusive. Never echo the rule text to the user — rephrase it naturally.

3. DEFAULT — accept any answer that genuinely addresses the question. Only reject if the reply is empty, off-topic, or complete gibberish. Do not invent extra constraints.

RESPONSE RULES:
- result false → {"result": false, "reason": "<friendly 1–2 sentence explanation>", "nextQuestion": null, "done": false, "data": null}
- result true, more fields → {"result": true, "reason": null, "nextQuestion": "<next question>", "done": false, "data": null}
- result true, all done → {"result": true, "reason": null, "nextQuestion": null, "done": true, "data": {$requiredKeys mapped to their collected values}}
  "data" must contain ALL of these keys exactly as written: $requiredKeys — no extras, no renames.

TONE: friendly, concise, never scolding. "reason" must read like a normal chat message.
''';
}