fix: JSON truncation recovery + 2min timeout on AI template generation
This commit is contained in:
Binary file not shown.
@@ -37,12 +37,30 @@ def llm_chat(messages, temperature=0.3, max_tokens=4000, response_format=None):
|
||||
|
||||
|
||||
def extract_json(text):
|
||||
"""Extract JSON from LLM response, handling markdown code blocks."""
|
||||
"""Extract JSON from LLM response, handling markdown code blocks and truncation."""
|
||||
json_str = text.strip()
|
||||
if json_str.startswith("```"):
|
||||
json_str = re.sub(r'^```(?:json)?\s*', '', json_str)
|
||||
json_str = re.sub(r'\s*```$', '', json_str)
|
||||
return json.loads(json_str)
|
||||
|
||||
try:
|
||||
return json.loads(json_str)
|
||||
except json.JSONDecodeError:
|
||||
# Try to fix truncated JSON by closing open braces/brackets
|
||||
open_b = json_str.count('{')
|
||||
close_b = json_str.count('}')
|
||||
open_arr = json_str.count('[')
|
||||
close_arr = json_str.count(']')
|
||||
if open_b > close_b or open_arr > close_arr:
|
||||
# Remove any trailing partial content after last complete element
|
||||
# then close the structures
|
||||
json_str = json_str.rstrip()
|
||||
# Remove trailing incomplete key-value (e.g. cut off mid-string)
|
||||
json_str = re.sub(r'[\s,]*"[^"]*":\s*"?[^",}\]]*$', '', json_str)
|
||||
json_str += '}' * max(0, open_b - json_str.count('}'))
|
||||
json_str += ']' * max(0, open_arr - json_str.count(']'))
|
||||
return json.loads(json_str)
|
||||
raise
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -333,8 +351,34 @@ def generate_template(description: str) -> dict:
|
||||
{"role": "system", "content": "You are a CV template designer that outputs only valid JSON."},
|
||||
{"role": "user", "content": prompt}
|
||||
]
|
||||
resp = llm_chat(messages, temperature=0.5, max_tokens=2000)
|
||||
return extract_json(resp)
|
||||
|
||||
# Retry with increasing max_tokens if JSON parsing fails
|
||||
for attempt, max_tok in enumerate([2000, 3000, 4000]):
|
||||
try:
|
||||
resp = llm_chat(messages, temperature=0.5, max_tokens=max_tok)
|
||||
return extract_json(resp)
|
||||
except json.JSONDecodeError as e:
|
||||
if attempt < 2:
|
||||
# Retry with more tokens
|
||||
continue
|
||||
# Last attempt failed — try to salvage partial JSON
|
||||
try:
|
||||
# Attempt to fix truncated JSON by closing braces
|
||||
json_str = resp.strip()
|
||||
if json_str.startswith("```"):
|
||||
json_str = re.sub(r'^```(?:json)?\s*', '', json_str)
|
||||
json_str = re.sub(r'\s*```$', '', json_str)
|
||||
# Count open vs close braces
|
||||
open_b = json_str.count('{')
|
||||
close_b = json_str.count('}')
|
||||
open_arr = json_str.count('[')
|
||||
close_arr = json_str.count(']')
|
||||
# Append missing closers
|
||||
json_str += '}' * (open_b - close_b)
|
||||
json_str += ']' * (open_arr - close_arr)
|
||||
return json.loads(json_str)
|
||||
except:
|
||||
raise ValueError(f"AI returned invalid JSON (truncated at {max_tok} tokens). Please try again with a shorter description.")
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -286,18 +286,37 @@ async function createTemplate() {
|
||||
async function generateTemplate() {
|
||||
const desc = document.getElementById('tmpl-gen-desc').value;
|
||||
if (!desc) { toast('Description required', 'error'); return; }
|
||||
document.querySelector('.modal button').textContent = 'Generating...';
|
||||
document.querySelector('.modal button').disabled = true;
|
||||
const btn = document.querySelector('.modal button');
|
||||
btn.textContent = 'Generating...';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
await api('/api/templates/generate', 'POST', {
|
||||
name: document.getElementById('tmpl-gen-name').value || 'AI Generated Template',
|
||||
description: desc
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 120000); // 2 min timeout
|
||||
const resp = await fetch(API + '/api/templates/generate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: document.getElementById('tmpl-gen-name').value || 'AI Generated Template',
|
||||
description: desc
|
||||
}),
|
||||
signal: controller.signal
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
if (!resp.ok) {
|
||||
const err = await resp.text();
|
||||
throw new Error(err);
|
||||
}
|
||||
closeModal();
|
||||
toast('Template generated with AI');
|
||||
loadTemplates();
|
||||
} catch(e) {
|
||||
toast('Generation failed: ' + e.message, 'error');
|
||||
btn.textContent = 'Generate with AI';
|
||||
btn.disabled = false;
|
||||
if (e.name === 'AbortError') {
|
||||
toast('Generation timed out after 2 minutes. Try a shorter description.', 'error');
|
||||
} else {
|
||||
toast('Generation failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user