From 8d9506efcff8f84360e7b263643112713f677255 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 08:01:51 +0000 Subject: [PATCH] fix: JSON truncation recovery + 2min timeout on AI template generation --- __pycache__/ai_service.cpython-312.pyc | Bin 13483 -> 15511 bytes ai_service.py | 52 +++++++++++++++++++++++-- static/app.js | 31 ++++++++++++--- 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/__pycache__/ai_service.cpython-312.pyc b/__pycache__/ai_service.cpython-312.pyc index b93d229a9f8ca4d2f1e7b416d4510b109d58df2f..bf7ea1f0141e4550ecbbc05e70c58090dec32e3b 100644 GIT binary patch delta 2911 zcmai0eM}qY8NWOGeEtI49N#$jg2CVb4gt~-lEC=zAPiheNL32d@wjU+A2xe^e8e%e zRCOKQrWSgC0Bbj_Xw#UOv~*}1(zGdPjn-| zw(1Jcd3-!4qDkM7NA&uAjswMw8#fM&uV5@|tRD&ZefGwIz?m3vz~Byhix0@#2juJl z*?&L|22b@51S$s46!pJSgs~n|iW;$9F*`6gc%~S^m)W&aTxEYXX6U%~ysK3h^7Dcd zAwP=IL;gvh7*nBuh`bXq#kgn2u8Ju|VMdIp;mvM55L5Xl1fTo6F=lAY9}uviPB=UW z#WYD5HUlbH4cw$ECR=v}KfqQ7%1P-3ra2r}n&|Xh1N%99bY_M*rt&ewgh6g%J zRA+fe80hppQ{H0XLSaN9n~LXDI~nsrYv0Fkbb4l_3P~>#v#ns{8_ffh-kiHR+t6HKZ zEplONG`BUXZ=Gx3Wpftm7wVUWWwvx%U%I1Ci(Bw_90cI_ZJIKhY{aImrNn7a+^5qh z+^zqTsn@7sY3wv=koJxBbQga3(R!KbO9k00aam{Se(dVu$Y!b1+*Q(o{m6;o=1-QPecZUep~J2=?6PZ-5{A2_74xY} z9blfr5Tp6Y@reNH0f!YJkr9J#fb9Y3!w_ST@G5LS>Xg1Te~TW+zQ3 zlu8wi0_huxs*{QOV8{^};f8k-;zkM2;? z^$OR(O@de5ArEz?l^Em*VgTQjS;eeULx>EfCR&K%nUM0=>XgFJq9!$#lEWd2#CKas z+|ptl)q;+4SV#9~RUrkh8O31{-G{|!R`r_d$4wd{Oph3_fE9$95Hn&r40qJ?y2Hef z<4A-?GLIOC7O&^iLQE<-=KrwMkF&%0^*M*3j(Xm3`1)D(2@GS3p*ewJN-?x2Ff!Jf z4y%bS#PG(DI-~&&O^GmG9X=zZ4e5@1MSQ@MZ6sInup~?kY6B<*Cd2V{VK5p784s#wZFziR7fH+!uRD!0ut(gY9PZ(00+R?FklrxEkHGfnD#}_*krPV*!5@z z{G9+y=3~~?JtbEaiuY>liDk= ztkf^pzjt|FjSJ5M))r-Lk;zZl%3aIpm8s>at+4IlM)k&wY-yiwf9i{j+%390EL$4q z+xPNLuM{p9t}5ibih0#ee$k3!*|EyX`IYm`PJwl$VYy+|Di=6#baU~FYuUA0Bb(38 zYxaz}OD&P-R@l% zkufhWUr@KuBmh(c^cnyhC-jyi)D&so#U`jwAX{3fxj^TV>G1~XlbYq6BC?Z;)Wiws QS8E?7`gjj1-KwqrA2(>T%>V!Z delta 1050 zcmZWnO-vI}5Z>)}+iuIB{3J+(HVDds0p*`oT7fkD*p^a?1{Jo17ATY!XV)JZl#3S= z6HVTMz(r$XLQ*cIG0}^6^g`mvjR$YWTN9$*#2K1MgqOT;=FQA|Z+^aw)l9tTQ(>W% z!!^Efb8-91ZqXajCgM@ReejWEAH>%zQekJ;Es|-Rlf|@~Em(T%o6|xh5=oa#BQ|)P zi)tzI+r%1Kx>_BVv;0x%Ld)3#U5(9g@P1S1J!_UI~;HZgfS@(yFC&K*JCapbB{GAO=-SnsxjO6Y^(51MVnoGg*jHVa(`@#c5N-#9)4~D@- zA&nFwM(-`Js4zf=^8`mFm|=|c5Wxt7Zid(#bqRVojs+cVaSE;bS>`ZU&)uM?35qIK zl#q99VmRdWdjnxbfi||==sA_|7Y}sNos;>(oGJIY~s!1%=>ccs_Z0(==H;3W%k^y5(mVn(ACL_Js7GD4>t;oXre<-7 z#1m*>8Rz6Mkpr9rGzHxljn8U8JO?5)5MklgktY#Hwh&1mMnJ8HWdbS*ROWZpTGvpe m&Z?zs)zu}Mct@^^y>)GtS@@b)Z8;9bcKZQ|Tn&p|ZT$;^6#uaR diff --git a/ai_service.py b/ai_service.py index ec1b03e..ce4b057 100644 --- a/ai_service.py +++ b/ai_service.py @@ -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.") # ============================================================ diff --git a/static/app.js b/static/app.js index 074989a..ba3f3b5 100644 --- a/static/app.js +++ b/static/app.js @@ -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'); + } } }