llmparse-py 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llmparse/__init__.py +12 -0
- llmparse/core.py +93 -0
- llmparse/repair.py +192 -0
- llmparse/scanner.py +92 -0
- llmparse_py-0.1.1.dist-info/METADATA +100 -0
- llmparse_py-0.1.1.dist-info/RECORD +9 -0
- llmparse_py-0.1.1.dist-info/WHEEL +5 -0
- llmparse_py-0.1.1.dist-info/licenses/LICENSE +21 -0
- llmparse_py-0.1.1.dist-info/top_level.txt +1 -0
llmparse/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""llmparse — parse the JSON your LLM meant to send.
|
|
2
|
+
|
|
3
|
+
Zero-dependency extraction and repair for JSON embedded in raw LLM
|
|
4
|
+
output: markdown fences, prose, single quotes, trailing commas,
|
|
5
|
+
Python literals, comments and truncated streams.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core import LLMParseError, extract, loads, parse
|
|
9
|
+
from .repair import repair
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.1"
|
|
12
|
+
__all__ = ["LLMParseError", "extract", "loads", "parse", "repair", "__version__"]
|
llmparse/core.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Extraction and parsing: find the JSON inside LLM output and load it."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .repair import RULES, repair
|
|
9
|
+
from .scanner import find_balanced_end
|
|
10
|
+
|
|
11
|
+
__all__ = ["LLMParseError", "extract", "parse", "loads", "repair"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LLMParseError(ValueError):
|
|
15
|
+
"""Raised when no JSON value can be recovered from the text."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _try_loads(text: str) -> tuple[bool, Any]:
|
|
19
|
+
try:
|
|
20
|
+
return True, json.loads(text)
|
|
21
|
+
except (ValueError, TypeError):
|
|
22
|
+
return False, None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_with_repairs(text: str) -> tuple[bool, Any]:
|
|
26
|
+
"""Try plain parsing, then re-try after each repair stage."""
|
|
27
|
+
ok, value = _try_loads(text.strip())
|
|
28
|
+
if ok:
|
|
29
|
+
return True, value
|
|
30
|
+
current = text
|
|
31
|
+
for rule in RULES:
|
|
32
|
+
current = rule(current)
|
|
33
|
+
ok, value = _try_loads(current.strip())
|
|
34
|
+
if ok:
|
|
35
|
+
return True, value
|
|
36
|
+
return False, None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def extract(text: str) -> str | None:
|
|
40
|
+
"""Return the first recoverable JSON substring of ``text``, or None.
|
|
41
|
+
|
|
42
|
+
Candidates start at every ``{`` or ``[`` outside the previous
|
|
43
|
+
candidate; each candidate is cut at its balanced end (or at end of
|
|
44
|
+
text when truncated) and tested with the full repair pipeline.
|
|
45
|
+
"""
|
|
46
|
+
if not text:
|
|
47
|
+
return None
|
|
48
|
+
ok, _ = _load_with_repairs(text)
|
|
49
|
+
if ok:
|
|
50
|
+
return repair(text).strip() if "```" in text else text.strip()
|
|
51
|
+
for start, ch in enumerate(text):
|
|
52
|
+
if ch not in "{[":
|
|
53
|
+
continue
|
|
54
|
+
end = find_balanced_end(text, start)
|
|
55
|
+
candidate = text[start:end] if end is not None else text[start:]
|
|
56
|
+
ok, _ = _load_with_repairs(candidate)
|
|
57
|
+
if ok:
|
|
58
|
+
return candidate
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
_SENTINEL = object()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse(text: str, *, default: Any = _SENTINEL) -> Any:
|
|
66
|
+
"""Parse JSON out of raw LLM output.
|
|
67
|
+
|
|
68
|
+
Handles markdown fences, surrounding prose, single quotes, bare
|
|
69
|
+
keys, Python literals, comments, trailing commas and truncation.
|
|
70
|
+
Returns ``default`` when given and nothing can be recovered;
|
|
71
|
+
otherwise raises :class:`LLMParseError`.
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(text, (dict, list)):
|
|
74
|
+
return text
|
|
75
|
+
if not isinstance(text, str) or not text.strip():
|
|
76
|
+
if default is not _SENTINEL:
|
|
77
|
+
return default
|
|
78
|
+
raise LLMParseError("no JSON found: empty input")
|
|
79
|
+
ok, value = _load_with_repairs(text)
|
|
80
|
+
if ok:
|
|
81
|
+
return value
|
|
82
|
+
candidate = extract(text)
|
|
83
|
+
if candidate is not None:
|
|
84
|
+
ok, value = _load_with_repairs(candidate)
|
|
85
|
+
if ok:
|
|
86
|
+
return value
|
|
87
|
+
if default is not _SENTINEL:
|
|
88
|
+
return default
|
|
89
|
+
raise LLMParseError(f"could not recover JSON from: {text[:120]!r}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Familiar alias for users coming from the json module.
|
|
93
|
+
loads = parse
|
llmparse/repair.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"""Repair rules for malformed JSON commonly produced by LLMs.
|
|
2
|
+
|
|
3
|
+
Each rule is a small ``str -> str`` function that fixes one class of
|
|
4
|
+
damage and is safe to apply to already-valid JSON (it must not change
|
|
5
|
+
valid input). Rules run in the order listed in ``RULES``; the parser
|
|
6
|
+
tries ``json.loads`` after every stage and stops as soon as it works.
|
|
7
|
+
|
|
8
|
+
Adding a rule is the easiest meaningful contribution to llmparse:
|
|
9
|
+
write the function, register it in ``RULES``, and add a test case with
|
|
10
|
+
a real-world-style broken input. See CONTRIBUTING.md.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from .scanner import in_string_at_end, iter_chars, unclosed_brackets
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def strip_code_fences(text: str) -> str:
|
|
21
|
+
"""Remove surrounding markdown ``` fences, if present."""
|
|
22
|
+
stripped = text.strip()
|
|
23
|
+
if "```" not in stripped:
|
|
24
|
+
return text
|
|
25
|
+
match = re.search(r"```(?:json|JSON)?\s*\n?(.*?)(?:```|$)", stripped, re.DOTALL)
|
|
26
|
+
return match.group(1).strip() if match else text
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def remove_comments(text: str) -> str:
|
|
30
|
+
"""Remove ``//`` line and ``/* */`` block comments outside strings."""
|
|
31
|
+
out: list[str] = []
|
|
32
|
+
i = 0
|
|
33
|
+
in_string = False
|
|
34
|
+
quote = ""
|
|
35
|
+
escaped = False
|
|
36
|
+
while i < len(text):
|
|
37
|
+
ch = text[i]
|
|
38
|
+
if in_string:
|
|
39
|
+
out.append(ch)
|
|
40
|
+
if escaped:
|
|
41
|
+
escaped = False
|
|
42
|
+
elif ch == "\\":
|
|
43
|
+
escaped = True
|
|
44
|
+
elif ch == quote:
|
|
45
|
+
in_string = False
|
|
46
|
+
i += 1
|
|
47
|
+
continue
|
|
48
|
+
if ch in ("'", '"'):
|
|
49
|
+
in_string, quote = True, ch
|
|
50
|
+
out.append(ch)
|
|
51
|
+
i += 1
|
|
52
|
+
continue
|
|
53
|
+
if ch == "/" and i + 1 < len(text) and text[i + 1] == "/":
|
|
54
|
+
while i < len(text) and text[i] != "\n":
|
|
55
|
+
i += 1
|
|
56
|
+
continue
|
|
57
|
+
if ch == "/" and i + 1 < len(text) and text[i + 1] == "*":
|
|
58
|
+
i += 2
|
|
59
|
+
while i + 1 < len(text) and not (text[i] == "*" and text[i + 1] == "/"):
|
|
60
|
+
i += 1
|
|
61
|
+
i += 2
|
|
62
|
+
continue
|
|
63
|
+
out.append(ch)
|
|
64
|
+
i += 1
|
|
65
|
+
return "".join(out)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def single_to_double_quotes(text: str) -> str:
|
|
69
|
+
"""Convert single-quoted strings to double-quoted strings."""
|
|
70
|
+
out: list[str] = []
|
|
71
|
+
in_string = False
|
|
72
|
+
quote = ""
|
|
73
|
+
escaped = False
|
|
74
|
+
for ch in text:
|
|
75
|
+
if in_string:
|
|
76
|
+
if escaped:
|
|
77
|
+
out.append(ch)
|
|
78
|
+
escaped = False
|
|
79
|
+
elif ch == "\\":
|
|
80
|
+
out.append(ch)
|
|
81
|
+
escaped = True
|
|
82
|
+
elif ch == quote:
|
|
83
|
+
out.append('"')
|
|
84
|
+
in_string = False
|
|
85
|
+
elif ch == '"' and quote == "'":
|
|
86
|
+
out.append('\\"')
|
|
87
|
+
else:
|
|
88
|
+
out.append(ch)
|
|
89
|
+
else:
|
|
90
|
+
if ch in ("'", '"'):
|
|
91
|
+
in_string, quote = True, ch
|
|
92
|
+
out.append('"')
|
|
93
|
+
else:
|
|
94
|
+
out.append(ch)
|
|
95
|
+
return "".join(out)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_LITERAL_MAP = {
|
|
99
|
+
"True": "true",
|
|
100
|
+
"False": "false",
|
|
101
|
+
"None": "null",
|
|
102
|
+
"undefined": "null",
|
|
103
|
+
"NaN": "null",
|
|
104
|
+
"Infinity": "null",
|
|
105
|
+
"-Infinity": "null",
|
|
106
|
+
}
|
|
107
|
+
_LITERAL_RE = re.compile(r"\b(True|False|None|undefined|NaN|Infinity|-Infinity)\b")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def fix_literals(text: str) -> str:
|
|
111
|
+
"""Replace Python/JavaScript literals with JSON ones, outside strings."""
|
|
112
|
+
out: list[str] = []
|
|
113
|
+
buffer: list[str] = []
|
|
114
|
+
for _, ch, in_string, _ in iter_chars(text):
|
|
115
|
+
if in_string:
|
|
116
|
+
if buffer:
|
|
117
|
+
out.append(_LITERAL_RE.sub(lambda m: _LITERAL_MAP[m.group(1)], "".join(buffer)))
|
|
118
|
+
buffer = []
|
|
119
|
+
out.append(ch)
|
|
120
|
+
else:
|
|
121
|
+
buffer.append(ch)
|
|
122
|
+
if buffer:
|
|
123
|
+
out.append(_LITERAL_RE.sub(lambda m: _LITERAL_MAP[m.group(1)], "".join(buffer)))
|
|
124
|
+
return "".join(out)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
_UNQUOTED_KEY_RE = re.compile(r"([{,]\s*)([A-Za-z_$][A-Za-z0-9_$]*)(\s*:)")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def quote_unquoted_keys(text: str) -> str:
|
|
131
|
+
"""Quote bare identifier keys: ``{name: 1}`` -> ``{"name": 1}``."""
|
|
132
|
+
out: list[str] = []
|
|
133
|
+
buffer: list[str] = []
|
|
134
|
+
for _, ch, in_string, _ in iter_chars(text):
|
|
135
|
+
if in_string:
|
|
136
|
+
if buffer:
|
|
137
|
+
out.append(_UNQUOTED_KEY_RE.sub(r'\1"\2"\3', "".join(buffer)))
|
|
138
|
+
buffer = []
|
|
139
|
+
out.append(ch)
|
|
140
|
+
else:
|
|
141
|
+
buffer.append(ch)
|
|
142
|
+
if buffer:
|
|
143
|
+
out.append(_UNQUOTED_KEY_RE.sub(r'\1"\2"\3', "".join(buffer)))
|
|
144
|
+
return "".join(out)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def remove_trailing_commas(text: str) -> str:
|
|
148
|
+
"""Drop commas that immediately precede ``}`` or ``]`` outside strings."""
|
|
149
|
+
chars = [(ch, in_string) for _, ch, in_string, _ in iter_chars(text)]
|
|
150
|
+
out: list[str] = []
|
|
151
|
+
for idx, (ch, in_string) in enumerate(chars):
|
|
152
|
+
if ch == "," and not in_string:
|
|
153
|
+
lookahead = idx + 1
|
|
154
|
+
while lookahead < len(chars) and chars[lookahead][0] in " \t\r\n":
|
|
155
|
+
lookahead += 1
|
|
156
|
+
if lookahead < len(chars) and chars[lookahead][0] in "}]":
|
|
157
|
+
continue
|
|
158
|
+
out.append(ch)
|
|
159
|
+
return "".join(out)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def close_truncated(text: str) -> str:
|
|
163
|
+
"""Close an unterminated string and any unclosed brackets, in order."""
|
|
164
|
+
result = text.rstrip()
|
|
165
|
+
if not result:
|
|
166
|
+
return result
|
|
167
|
+
# A dangling escape at the very end would swallow the closing quote.
|
|
168
|
+
if result.endswith("\\") and not result.endswith("\\\\"):
|
|
169
|
+
result = result[:-1]
|
|
170
|
+
closers = ""
|
|
171
|
+
if in_string_at_end(result):
|
|
172
|
+
closers += '"'
|
|
173
|
+
closers += "".join(unclosed_brackets(result))
|
|
174
|
+
return result + closers
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
RULES = [
|
|
178
|
+
strip_code_fences,
|
|
179
|
+
remove_comments,
|
|
180
|
+
single_to_double_quotes,
|
|
181
|
+
fix_literals,
|
|
182
|
+
quote_unquoted_keys,
|
|
183
|
+
remove_trailing_commas,
|
|
184
|
+
close_truncated,
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def repair(text: str) -> str:
|
|
189
|
+
"""Apply every repair rule in order and return the repaired text."""
|
|
190
|
+
for rule in RULES:
|
|
191
|
+
text = rule(text)
|
|
192
|
+
return text
|
llmparse/scanner.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Character scanners shared by extraction and repair.
|
|
2
|
+
|
|
3
|
+
Everything here walks the text once while tracking whether we are
|
|
4
|
+
inside a string, so brackets or comment markers inside string values
|
|
5
|
+
are never mistaken for structure. Strings may be double-quoted,
|
|
6
|
+
single-quoted, or (in truncated output) unterminated.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def iter_chars(text: str):
|
|
13
|
+
"""Yield ``(index, char, in_string, quote_char)`` for each position.
|
|
14
|
+
|
|
15
|
+
``in_string`` describes the state *at* the character: opening and
|
|
16
|
+
closing quote characters themselves are yielded with
|
|
17
|
+
``in_string == False`` for the opener and ``True`` for the closer.
|
|
18
|
+
Backslash escapes inside strings are honoured.
|
|
19
|
+
"""
|
|
20
|
+
in_string = False
|
|
21
|
+
quote = ""
|
|
22
|
+
escaped = False
|
|
23
|
+
for i, ch in enumerate(text):
|
|
24
|
+
if in_string:
|
|
25
|
+
yield i, ch, True, quote
|
|
26
|
+
if escaped:
|
|
27
|
+
escaped = False
|
|
28
|
+
elif ch == "\\":
|
|
29
|
+
escaped = True
|
|
30
|
+
elif ch == quote:
|
|
31
|
+
in_string = False
|
|
32
|
+
quote = ""
|
|
33
|
+
else:
|
|
34
|
+
yield i, ch, False, ""
|
|
35
|
+
if ch in ("'", '"'):
|
|
36
|
+
in_string = True
|
|
37
|
+
quote = ch
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def find_balanced_end(text: str, start: int) -> int | None:
|
|
41
|
+
"""Return the index just past the bracket matching ``text[start]``.
|
|
42
|
+
|
|
43
|
+
``text[start]`` must be ``{`` or ``[``. Returns ``None`` when the
|
|
44
|
+
structure is truncated (no matching close before end of text).
|
|
45
|
+
"""
|
|
46
|
+
pairs = {"{": "}", "[": "]"}
|
|
47
|
+
stack: list[str] = []
|
|
48
|
+
for i, ch, in_string, _ in iter_chars(text):
|
|
49
|
+
if i < start or in_string:
|
|
50
|
+
continue
|
|
51
|
+
if ch in pairs:
|
|
52
|
+
stack.append(pairs[ch])
|
|
53
|
+
elif ch in ("}", "]"):
|
|
54
|
+
if not stack or stack[-1] != ch:
|
|
55
|
+
return None
|
|
56
|
+
stack.pop()
|
|
57
|
+
if not stack:
|
|
58
|
+
return i + 1
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def unclosed_brackets(text: str) -> list[str]:
|
|
63
|
+
"""Return the closing brackets needed to balance ``text``, in order."""
|
|
64
|
+
pairs = {"{": "}", "[": "]"}
|
|
65
|
+
stack: list[str] = []
|
|
66
|
+
for _, ch, in_string, _ in iter_chars(text):
|
|
67
|
+
if in_string:
|
|
68
|
+
continue
|
|
69
|
+
if ch in pairs:
|
|
70
|
+
stack.append(pairs[ch])
|
|
71
|
+
elif ch in ("}", "]") and stack and stack[-1] == ch:
|
|
72
|
+
stack.pop()
|
|
73
|
+
return list(reversed(stack))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def in_string_at_end(text: str) -> bool:
|
|
77
|
+
"""True when ``text`` ends inside an unterminated string."""
|
|
78
|
+
in_string = False
|
|
79
|
+
quote = ""
|
|
80
|
+
escaped = False
|
|
81
|
+
for ch in text:
|
|
82
|
+
if in_string:
|
|
83
|
+
if escaped:
|
|
84
|
+
escaped = False
|
|
85
|
+
elif ch == "\\":
|
|
86
|
+
escaped = True
|
|
87
|
+
elif ch == quote:
|
|
88
|
+
in_string = False
|
|
89
|
+
elif ch in ("'", '"'):
|
|
90
|
+
in_string = True
|
|
91
|
+
quote = ch
|
|
92
|
+
return in_string
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llmparse-py
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Parse the JSON your LLM meant to send — zero-dependency extraction and repair for raw LLM output.
|
|
5
|
+
Author: gptfei
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/gptfei/llmparse
|
|
8
|
+
Project-URL: Repository, https://github.com/gptfei/llmparse
|
|
9
|
+
Project-URL: Issues, https://github.com/gptfei/llmparse/issues
|
|
10
|
+
Keywords: llm,json,parser,repair,openai,chatgpt,claude,structured-output
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Classifier: Topic :: Text Processing
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# llmparse
|
|
23
|
+
|
|
24
|
+
**Parse the JSON your LLM meant to send.**
|
|
25
|
+
|
|
26
|
+
LLMs promise JSON and deliver markdown fences, apologies, single
|
|
27
|
+
quotes, trailing commas, Python literals — and, when the token budget
|
|
28
|
+
runs out, JSON that stops mid-string. `llmparse` is a zero-dependency
|
|
29
|
+
Python library that extracts the JSON from raw model output and repairs
|
|
30
|
+
the damage, so `json.loads` stops being the most fragile line in your
|
|
31
|
+
pipeline.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from llmparse import parse
|
|
35
|
+
|
|
36
|
+
parse('Sure! Here you go:\n```json\n{name: "ada", tags: ["x", "y",], ok: True}\n```')
|
|
37
|
+
# {'name': 'ada', 'tags': ['x', 'y'], 'ok': True}
|
|
38
|
+
|
|
39
|
+
parse('{"name": "ada", "tags": ["x", "y"') # truncated stream
|
|
40
|
+
# {'name': 'ada', 'tags': ['x', 'y']}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install llmparse-py # once published to PyPI
|
|
47
|
+
pip install git+https://github.com/gptfei/llmparse
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Python 3.9+. No third-party dependencies, ever.
|
|
51
|
+
|
|
52
|
+
## What it fixes
|
|
53
|
+
|
|
54
|
+
| Damage in LLM output | Example |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| Markdown fences | ` ```json ... ``` ` |
|
|
57
|
+
| Prose around the JSON | `Here is the result: {...} hope it helps` |
|
|
58
|
+
| Single-quoted strings | `{'a': 'b'}` |
|
|
59
|
+
| Unquoted keys | `{name: "ada"}` |
|
|
60
|
+
| Python / JS literals | `True`, `None`, `undefined`, `NaN` |
|
|
61
|
+
| Comments | `{"a": 1} // note` |
|
|
62
|
+
| Trailing commas | `{"a": [1, 2,]}` |
|
|
63
|
+
| Truncation | `{"msg": "hello wor` → closed and parsed |
|
|
64
|
+
|
|
65
|
+
Brackets and comment markers *inside* string values are never touched.
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
- `parse(text, default=...)` — return the recovered Python object.
|
|
70
|
+
Raises `LLMParseError` when nothing is recoverable, unless `default`
|
|
71
|
+
is given. `loads` is an alias.
|
|
72
|
+
- `extract(text)` — return the first recoverable JSON substring, or `None`.
|
|
73
|
+
- `repair(text)` — return repaired JSON *text* (for when you need the
|
|
74
|
+
string, not the object).
|
|
75
|
+
|
|
76
|
+
Valid JSON is returned untouched: every repair rule is a no-op on
|
|
77
|
+
input that already parses.
|
|
78
|
+
|
|
79
|
+
## Contributing
|
|
80
|
+
|
|
81
|
+
The repair pipeline is a list of small, independent rules — adding one
|
|
82
|
+
is the easiest meaningful contribution in open source right now:
|
|
83
|
+
|
|
84
|
+
1. Find a way an LLM broke JSON that llmparse does not fix yet.
|
|
85
|
+
2. Write the rule in `src/llmparse/repair.py`, register it in `RULES`.
|
|
86
|
+
3. Add a test with the broken input.
|
|
87
|
+
|
|
88
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) and issues labelled
|
|
89
|
+
`good first issue`.
|
|
90
|
+
|
|
91
|
+
## 中文简介
|
|
92
|
+
|
|
93
|
+
大模型说好返回 JSON,实际经常带 markdown 围栏、解释文字、单引号、
|
|
94
|
+
尾随逗号、Python 的 `True/None`,甚至输出到一半被截断。`llmparse`
|
|
95
|
+
是一个零依赖的 Python 库:从原始输出里提取 JSON、逐项修复这些损坏、
|
|
96
|
+
再解析成 Python 对象。合法 JSON 不会被改动。
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
llmparse/__init__.py,sha256=YbcAqlgvNMQTRghyvOQP410hx3Vr_OtiIXzi1nq2exI,428
|
|
2
|
+
llmparse/core.py,sha256=UUp4659XQwAumRHSwKS8DZXYcFp1EhhMTUigHzs9tHM,2774
|
|
3
|
+
llmparse/repair.py,sha256=h10sSYRKoVMBXnGNlYFllEcBHOADQxxRWOhn4QV9c-8,5851
|
|
4
|
+
llmparse/scanner.py,sha256=ocIMyCSeiq-Cc_fno-2Cy1gflmXf5B0TLWg8H4IfMd8,2856
|
|
5
|
+
llmparse_py-0.1.1.dist-info/licenses/LICENSE,sha256=Cj0XVFIO6j89kpPNcfpffKbP5HMCbWcewAPMdY8yNVA,1063
|
|
6
|
+
llmparse_py-0.1.1.dist-info/METADATA,sha256=1ooBStwl7afNJpkY23tLomNioNw6r-LoySjMESaj11k,3505
|
|
7
|
+
llmparse_py-0.1.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
8
|
+
llmparse_py-0.1.1.dist-info/top_level.txt,sha256=DpTx5hVsGMrhzNpXRrrWbQcYUtEihokf2IyX2P-MGeM,9
|
|
9
|
+
llmparse_py-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gptfei
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
llmparse
|