Coverage for src/lilbee/core/llm_json.py: 100%

21 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-11-06 08:17 +0000

1"""Extract the first JSON value from an LLM reply. 

2 

3Models wrap JSON in prose, fences, and trailing commentary. A greedy 

4``\\{.*\\}`` span runs to the LAST brace in the reply, so any trailing text 

5containing one drops the whole value; a hand-rolled brace counter miscounts 

6braces inside string literals. ``raw_decode`` scanned from each opener lets the 

7stdlib own the parsing state, which is the only thing that gets both right. 

8""" 

9 

10from __future__ import annotations 

11 

12import json 

13from typing import TypeVar 

14 

15_DECODER = json.JSONDecoder() 

16 

17 

18def json_reply_format() -> dict[str, str]: 

19 """Provider option asking for a bare JSON value rather than JSON in prose. 

20 

21 llama.cpp's server and OpenAI-compatible remotes both honour it. Returns 

22 a fresh dict per call because it is handed to a third-party SDK that is free 

23 to mutate the request it is given. The scans below stay the fallback: a 

24 provider can ignore the request and a model can comply imperfectly, and 

25 neither should cost the caller its answer. 

26 """ 

27 return {"type": "json_object"} 

28 

29 

30_JsonT = TypeVar("_JsonT", dict, list) 

31 

32 

33def _first_json_value(text: str, opener: str, expected: type[_JsonT]) -> _JsonT | None: 

34 """The first *expected*-typed JSON value starting at an *opener*, or None.""" 

35 start = text.find(opener) 

36 while start >= 0: 

37 try: 

38 parsed, _ = _DECODER.raw_decode(text, start) 

39 except json.JSONDecodeError: 

40 start = text.find(opener, start + 1) 

41 continue 

42 return parsed if isinstance(parsed, expected) else None 

43 return None 

44 

45 

46def first_json_object(text: str) -> dict | None: 

47 """The first JSON object in *text*, or None.""" 

48 return _first_json_value(text, "{", dict) 

49 

50 

51def first_json_array(text: str) -> list | None: 

52 """The first JSON array in *text*, or None.""" 

53 return _first_json_value(text, "[", list)