File size: 6,023 Bytes
63bca44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import axios from 'axios';

/**

 * Formats "messed up" text into a structured JSON format using AI.

 * @param {string} text - The raw text to format.

 * @param {string} apiKey - The LLM API key.

 * @param {string} provider - The LLM provider (e.g., 'openai', 'anthropic').

 * @returns {Promise<Array>} - A structured array of document elements.

 */
/**

 * Splits text into chunks based on character count while trying to preserve paragraphs.

 */
/**

 * Splits text into chunks based on character count while trying to preserve paragraphs.

 */
const chunkText = (text, maxLength = 5000) => {
  const paragraphs = text.split(/\n\n+/);
  const chunks = [];
  let currentChunk = "";

  for (const p of paragraphs) {
    if ((currentChunk + p).length > maxLength && currentChunk.length > 0) {
      chunks.push(currentChunk.trim());
      currentChunk = p;
    } else {
      currentChunk += (currentChunk ? "\n\n" : "") + p;
    }
  }
  if (currentChunk) chunks.push(currentChunk.trim());
  return chunks;
};

/**

 * Formats "messed up" text into a structured JSON format using AI.

 */
export const formatWithAI = async (text, apiKey, provider = 'openai', onProgress) => {
  if (!apiKey) return mockFormat(text);

  const chunks = chunkText(text);
  let allElements = [];

  for (let i = 0; i < chunks.length; i++) {
    if (onProgress) onProgress(i + 1, chunks.length);

    const chunk = chunks[i];
    const prompt = `

      You are an expert RFP document formatter. I will provide you with a segment of "messed up" text.

      Your task is to reformat it into a clean, structured JSON format.

      

      Segment ${i + 1} of ${chunks.length}.

      

      CRITICAL INSTRUCTIONS (ZERO ADDITION POLICY): 

      - DO NOT ADD ANY INFORMATION THAT IS NOT IN THE SOURCE TEXT. 

      - DO NOT invent answers, do not provide filler text, and do not guess.

      - DO NOT add greetings, introductions, or concluding remarks.

      - DO NOT SUMMARIZE OR OMIT ANY TEXT. YOU MUST PROCESS THE ENTIRE SEGMENT.

      - REPRODUCE EVERY SINGLE PIECE OF INFORMATION FROM THE INPUT.

      - DO NOT SKIP ANY PARAGRAPHS OR CLAUSES.

      - RFP RECOGNITION: If you see a question (e.g., text ending in "?" or text that looks like an RFP requirement), format it as an 'heading2' or 'heading3' so it stands out.

      - If you see a question followed by an answer, the question should be a heading and the answer a paragraph.

      - Main headers (like "MASTER CONTRACTOR AGREEMENT") must ALWAYS be typed as 'heading1'.

      - If you see a list of key-value pairs (like Company Name: Value), format it as a 2-column TABLE.

      - In lists, REMOVE any existing numbering or bullet prefixes (e.g., '(a)', '1.', '-', '•') from the start of each item.

      - Identify List Style: For 'numberedList', add a 'listType' field. Use 'lowerAlpha' if the source uses (a), (b), (c) or a., b., c. Use 'decimal' if it uses 1, 2, 3.

      - Identify Centered Titles: If a heading (like "MASTER CONTRACTOR AGREEMENT") should be centered, add an 'alignment' field set to 'center'. Default is 'left'.

      

      Return a JSON object with an 'elements' key containing an array of objects.

      Each object must have:

      - 'type': one of 'heading1', 'heading2', 'heading3', 'paragraph', 'bulletList', 'numberedList', 'table'

      - 'listType': (for 'numberedList' only) 'decimal' or 'lowerAlpha'

      - 'alignment': 'left' or 'center' (default is 'left')

      - 'content': (string for text, array of strings for lists, array of arrays for tables)

      

      Text segment to format:

      ${chunk}

    `;

    let attempts = 0;
    let success = false;

    while (attempts < 3 && !success) {
      attempts++;
      try {
        let response;
        const config = {
          headers: {
            'Authorization': `Bearer ${apiKey}`,
            'Content-Type': 'application/json',
          },
          timeout: 90000 // 90s timeout for better reliability
        };

        if (provider === 'openai') {
          response = await axios.post(
            'https://api.openai.com/v1/chat/completions',
            {
              model: 'gpt-4o',
              messages: [{ role: 'user', content: prompt }],
              response_format: { type: 'json_object' },
              max_tokens: 4096,
            },
            config
          );
        } else if (provider === 'groq') {
          response = await axios.post(
            'https://api.groq.com/openai/v1/chat/completions',
            {
              model: 'llama-3.3-70b-versatile',
              messages: [{ role: 'user', content: prompt }],
              response_format: { type: 'json_object' },
              max_tokens: 4096,
            },
            config
          );
        }

        const cleanJson = response.data.choices[0].message.content.replace(/```json\n?|\n?```/g, '').trim();
        const elements = JSON.parse(cleanJson).elements || [];
        allElements = [...allElements, ...elements];
        success = true;

      } catch (error) {
        console.error(`AI Formatting Error on chunk ${i + 1} (Attempt ${attempts}):`, error);
        if (attempts === 3) {
          throw new Error(`Failed to format document segment ${i + 1} after 3 attempts. Please try again or check your connection.`);
        }
        // Wait 1s before retry
        await new Promise(resolve => setTimeout(resolve, 1000));
      }
    }
  }

  return allElements;
};

const mockFormat = (text) => {
  // Simple mock logic: split by double newlines and guess types
  const lines = text.split('\n').filter(line => line.trim() !== '');
  return lines.map((line, index) => {
    if (index === 0) return { type: 'heading1', content: line };
    if (line.startsWith('* ') || line.startsWith('- ')) {
      return { type: 'bulletList', content: [line.substring(2)] };
    }
    return { type: 'paragraph', content: line };
  });
};