Spaces:
Sleeping
Sleeping
| 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 }; | |
| }); | |
| }; | |