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):
|
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()
|
json_str = text.strip()
|
||||||
if json_str.startswith("```"):
|
if json_str.startswith("```"):
|
||||||
json_str = re.sub(r'^```(?:json)?\s*', '', json_str)
|
json_str = re.sub(r'^```(?:json)?\s*', '', json_str)
|
||||||
json_str = re.sub(r'\s*```$', '', json_str)
|
json_str = re.sub(r'\s*```$', '', json_str)
|
||||||
|
|
||||||
|
try:
|
||||||
return json.loads(json_str)
|
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": "system", "content": "You are a CV template designer that outputs only valid JSON."},
|
||||||
{"role": "user", "content": prompt}
|
{"role": "user", "content": prompt}
|
||||||
]
|
]
|
||||||
resp = llm_chat(messages, temperature=0.5, max_tokens=2000)
|
|
||||||
|
# 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)
|
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,20 +286,39 @@ async function createTemplate() {
|
|||||||
async function generateTemplate() {
|
async function generateTemplate() {
|
||||||
const desc = document.getElementById('tmpl-gen-desc').value;
|
const desc = document.getElementById('tmpl-gen-desc').value;
|
||||||
if (!desc) { toast('Description required', 'error'); return; }
|
if (!desc) { toast('Description required', 'error'); return; }
|
||||||
document.querySelector('.modal button').textContent = 'Generating...';
|
const btn = document.querySelector('.modal button');
|
||||||
document.querySelector('.modal button').disabled = true;
|
btn.textContent = 'Generating...';
|
||||||
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
await api('/api/templates/generate', 'POST', {
|
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',
|
name: document.getElementById('tmpl-gen-name').value || 'AI Generated Template',
|
||||||
description: desc
|
description: desc
|
||||||
|
}),
|
||||||
|
signal: controller.signal
|
||||||
});
|
});
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.text();
|
||||||
|
throw new Error(err);
|
||||||
|
}
|
||||||
closeModal();
|
closeModal();
|
||||||
toast('Template generated with AI');
|
toast('Template generated with AI');
|
||||||
loadTemplates();
|
loadTemplates();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
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');
|
toast('Generation failed: ' + e.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteTemplate(id) {
|
async function deleteTemplate(id) {
|
||||||
if (!confirm('Delete this template?')) return;
|
if (!confirm('Delete this template?')) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user