From 692b383aa59276b8f8b7c5c8019cfff3fdc7afbf Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Jul 2026 08:58:31 +0000 Subject: [PATCH] fix: robust JSON parsing with 4-level repair + retry with higher max_tokens for CV parsing --- __pycache__/ai_service.cpython-312.pyc | Bin 15511 -> 17508 bytes ai_service.py | 63 +++++++++++++++--- .../433a0593-d3cf-414e-8763-ab5f87cbc75c.docx | Bin 40675 -> 40668 bytes renderer/carbone-templates/registry.json | 2 +- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/__pycache__/ai_service.cpython-312.pyc b/__pycache__/ai_service.cpython-312.pyc index bf7ea1f0141e4550ecbbc05e70c58090dec32e3b..ff6af19ee0396cff12b85a65e6afba7e17e09646 100644 GIT binary patch delta 3111 zcmZ`*dvFuS8Q(kUPN$P(NtT8DB0d7f7MOss4YnaT!cQnRu5BEgz$mKjY|F@!vv-bd zRYsW%)2g(c;9RCQX<9tf$3S3;J?>24k2W}MLJ9xKm<~qcKbQ_P9XkC(JlbKL>CkUa zG3L25_uJj?wcmdG?YB2Rayw3CT(wwu1nu`Xe0?s@bjI%(lLe+a`0kro_0P;*n~p+V zD5M`k!@4dsL>bj-_EYNEOpAe~xONoES&I}-op$GEuS57*Srny4Sv6!RC^L!XD0`OJ zg*M7Y**<4_{3MDNG2<+qrhwi%&H^iYmP*gt(egs}=N3}E?AlDxdd{}2yduo6LnkO~ zA2mcNwYAh4voG5l<)VhZ>?c=Gk8$k9bR}Uj@{19-!W z=`Y=5=1X1Gwvq!jLzEcH8s(J&vH)qK^Tmt&5{avTBWOZT0dQPVUd$A;a}kVlo^a=J zEP$T-E8}uxKA_Su7@|meawI?9d690lmwUaqL{RWx$nR4E;ZTX-3yFe1+}|G#3H$an z3wzu5w+Ir(VJxeE*q))hrD7uq+APzCchC1)m}W10hL>(A7f3%D3Y14}wy^MynR?{0$T%F1}$f7!

Ik4pmV4(5Zj=dV$1pWvs4clR! zG)uz@_W2cWpBxV179bVDC;x+1FB)t0td6mK#OGyFjO8Y0PU)auN^xe!vY;054VDF0 z!FX&eHt#BqH!Z3P-yKVsi&7>els|l6MrPX$Q^B06;2wHP-Nu2sEj!NKwq=g9W9*5e zlY^(Fd0Tm$xn*;XTgEIC-IFjci8G5<$A^)hL?#O7twMqm9xj@!FEq*7m`<7q$B15A*t(gat!B7YxcfJBN#_MGvIv5X02-nK5zESSt6RvxdMpiXfox!=~FW`48xTJ~$V zTzTUoV0;A`J&A1%b88yrU5)Xk6s@y37eOO3 z7MU!bid9^9RsG#o^>ESQy5T6Aa}>emEzX;^EQp=O39&;POU@U6Ry_6Y=fl&(e=WN- z_?PNy+nY}um?%9}d9rfSbF%uHbA6({GtMt&WGC_}rhK3GPWMi|bpc;mbGdM~@RBRh zbTHB7Pl&w{TrSl zW0R*0CRcYc`*bt^YAvc0`KbP!N!_re z=xrL_2kk8*EXACbK&UTcd9>1Z(@-gG@CxZg^O0hxhtvn~#)kL1gPTk$L#Ri1k6cAr@BwuIhX*7a91$eh?;C)!P#g*i zgK!Vivv&u>iX@i^PZ2}Xkaj2CubrWjdK?Z)n3U0^eo&UMTX$#Dicny{h0vV?Zvdm` zlar)ftT{vDRUn6fgYZEBH-~G8qPf-6)?V*zZQI}6+JScx_F4kuMq>{F@(|%l0Jj0} zSKr9WFV~E`O2~TwWD?}>%0s#~a9XkR`rpHcfpwn9oL7%!y&i+iDe~yD8)$mPC=IK~<0Q?$Y8Ty*43G)=9&wBfp=IbOHuD45jGI|{Fv3-200 z=-~qn+4DZFyOFzW0d^R_wb+msP#OEPiyi)>Fmoi&@E zvzuXNE7;wL`4TZO_UqIHl{I5_{+p)pW_8IGrTpqp_GYFJl&;R4&2FRwGIZ;bCizGp z81%}D4=YJydLo5H+^c5h7Be9b`qxZN?k*kGJ9Bh3LsRYQv4ZAU2hedRz;{P+rRL5B zD5$uL80E2^u)P3DV?YjfhjG78Nt&BI9koqQoe4RFBqI*?56IX@bRHiPoJyDw1_<;L zF?bQW!A*=0A*8knyVxjDt<=oN0>$SmL`)`PRDxLuvzQ1hI_ zH*4^lMCVzD@vvs3vA(6g&C~I825Bin{!5Oy& zH&!q|0#4h^xsCT&h~%lQy9*yj64E>YIfsXc0pi;~;e&Ro26w4!*=N*Fb-FC8TFcb%an)pn*UmfhKiV`6gxzhVhw! z^8e^7+6k4o7ctg`Lkhf%)qLQ;j~+d0!3PKnE@C}q2tL?2^JzL$iN89ULM2I9uQZK?!h<5of zNnT8n6OU+|U;r|MBm_Q2fJ7PpoB-Jm{)O7S<5m9GFr?s^WN`09v8>#n^VusTpjq>`<^J_U0`SPU7mg+{?1<^^h^6&!tpYp zNB1<+k2;-n*>STm$7i@n?nQ2aTNE-Th00r|M@(e;M0Y(M!I@ za2I!}E7}>8g|9q(Os*X9tvV!;#p(lcG|+7JnGWWtSy|AWn(IU9B8Hj0U^AU{#&Koa zMN;RJoZ_Z^EmLHGzS%O6xxiaCC>3<@v#@2GH*}bO;~d(jp^dj?j=R9ab{2vi3>484 z3VP9UPlQvGF?AyKL}DUsIY6H)WOF)nU>+_-5n+_Rzq2*i&MpRa96$pfV^+gl77Sg@ z9fkzEzKR($^!CmXYVYgka{^r>jg|G*-*|q5(C_?ZlBV%Mk9d&fov$nh;v|4n5iH3# zlrI!i!_0zdIqMg_s6mEaZwrblOZvPbw~q=WQ%QD;Jc-bBcVZ>V{19VsXUcCw8=pwb z70043uwi(Df#u8_rKu9kW=+eT2&bd_?~DuMnDQv5h}!(@oB=6x+@0M4&!QuXkYRxP znPP1M9ALT$1V`<&%zK@yXhGs?)iF)OG+9+su_vDzOT`kgWLi}rLXYhZ{jcGxA=roS z+}kkZYenL*WGod<|NC+7b_<8_t$TWHsP|gLPzhlUi@2xtqN-(0opmj+wrQldUtD03 z&PuuWmYr>~G#KgpZH6cJ5*)l?$>fr~QUPgKx;T44Y8Cb%s zF-_kIH9LbE;MjpdE?%lF!`J7o( zJ)njGH3~DeNFmPzg1Uft1Y9;8LBNUNByEbeyIyA$e_~@h-5Y(pt%-Q6qfv67 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 + except json.JSONDecodeError: + pass + + # Third attempt: fix common LLM JSON errors + # - Unescaped newlines inside string values + # - Single quotes instead of double quotes + # - Trailing commas before closing brackets + # - Unescaped quotes inside string values (the "Expecting ',' delimiter" error) + try: + fixed = json_str + # Remove trailing commas + fixed = re.sub(r',\s*([}\]])', r'\1', fixed) + # Fix unescaped newlines inside strings + fixed = fixed.replace('\\\n', '\\n') + return json.loads(fixed) + except json.JSONDecodeError: + pass + + # Fourth attempt: line-by-line repair — find the error location and try to fix it + try: + return json.loads(json_str, strict=False) + except json.JSONDecodeError as e: + # Last resort: try to salvage by removing the problematic section + # and parsing what we can + lines = json_str.split('\n') + if e.lineno and e.lineno <= len(lines): + # Remove the problematic line and try again + repaired = '\n'.join(lines[:e.lineno-1] + lines[e.lineno:]) + try: + return json.loads(repaired, strict=False) + except json.JSONDecodeError: + pass + + # If all else fails, raise with context + error_line = lines[e.lineno-1] if e.lineno and e.lineno <= len(lines) else '?' + raise ValueError(f"JSON parse failed at line {e.lineno}: {e.msg}\nProblem line: {error_line[:200]}\nFull response length: {len(text)} chars") # ============================================================ @@ -144,11 +180,20 @@ def parse_cv(cv_text: str) -> dict: prompt = PARSE_PROMPT.replace("__CV_TEXT__", cv_text) messages = [ - {"role": "system", "content": "You are a precise CV parser that outputs only valid JSON."}, + {"role": "system", "content": "You are a precise CV parser that outputs only valid JSON. Make sure all string values are properly escaped (no unescaped quotes, no unescaped newlines)."}, {"role": "user", "content": prompt} ] - resp = llm_chat(messages, temperature=0.1, max_tokens=4000) - return extract_json(resp) + + # Retry with increasing max_tokens in case of truncation + for max_tokens in [4000, 6000, 8000]: + try: + resp = llm_chat(messages, temperature=0.1, max_tokens=max_tokens) + return extract_json(resp) + except (ValueError, json.JSONDecodeError) as e: + if max_tokens == 8000: + raise + # Try again with more tokens + continue # ============================================================ diff --git a/renderer/carbone-templates/433a0593-d3cf-414e-8763-ab5f87cbc75c.docx b/renderer/carbone-templates/433a0593-d3cf-414e-8763-ab5f87cbc75c.docx index c55c03a8e7933af0014c4fb68eef5f038d89d67c..d53dcf82f1e801551f9def455ae38d13357d1608 100644 GIT binary patch delta 2331 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