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