agent-json-reliability 0.1.0__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.
- agent_json_reliability-0.1.0.dist-info/METADATA +128 -0
- agent_json_reliability-0.1.0.dist-info/RECORD +30 -0
- agent_json_reliability-0.1.0.dist-info/WHEEL +5 -0
- agent_json_reliability-0.1.0.dist-info/entry_points.txt +3 -0
- agent_json_reliability-0.1.0.dist-info/licenses/LICENSE +5 -0
- agent_json_reliability-0.1.0.dist-info/top_level.txt +2 -0
- products/__init__.py +1 -0
- products/agent_json_reliability/__init__.py +7 -0
- products/agent_json_reliability/inspect.py +62 -0
- products/agent_json_reliability/reliable.py +66 -0
- products/agent_json_reliability/repair.py +237 -0
- products/agent_json_reliability/validate.py +181 -0
- products/beta/__init__.py +0 -0
- products/beta/app.py +337 -0
- products/beta/batch.py +20 -0
- products/beta/catalog.py +61 -0
- products/beta/identity.py +154 -0
- products/beta/landing.py +113 -0
- products/beta/security.py +77 -0
- products/beta/selfcheck.py +18 -0
- products/beta/settings.py +94 -0
- products/beta/store.py +137 -0
- products/common/__init__.py +7 -0
- products/common/timing.py +16 -0
- products/gateway/__init__.py +0 -0
- products/gateway/manifest.py +138 -0
- products/gateway/mcp_server.py +64 -0
- products/gateway/mcp_stdio.py +53 -0
- products/gateway/rate_limit.py +26 -0
- serve.py +29 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-json-reliability
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Deterministic JSON repair and JSON Schema validation for AI agents and software. No LLM required.
|
|
5
|
+
Author: Agent JSON Reliability contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/toninovo4249-ai/agent-json-reliability
|
|
8
|
+
Keywords: json,json-schema,json-repair,ai-agents,mcp,structured-output
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: fastapi>=0.115.0
|
|
17
|
+
Requires-Dist: uvicorn>=0.30.0
|
|
18
|
+
Requires-Dist: jsonschema>=4.23.0
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# Agent JSON Reliability
|
|
22
|
+
|
|
23
|
+
STATUS=BETA version `0.1.0`
|
|
24
|
+
|
|
25
|
+
AI agents and software frequently emit malformed JSON: markdown fences, trailing commas, single quotes, `True`/`False`/`None`, and extra prose around one object.
|
|
26
|
+
|
|
27
|
+
**Agent JSON Reliability** is a deterministic pipeline for that failure mode:
|
|
28
|
+
|
|
29
|
+
1. **inspect** — diagnose whether the text is JSON and which failures are present
|
|
30
|
+
2. **safe repair** — apply only structural, unambiguous fixes
|
|
31
|
+
3. **validate** — optional JSON Schema check on the result
|
|
32
|
+
4. **structured diagnostics** — `valid_original`, `repaired`, `valid_final`, `schema_valid`, `unsafe_or_ambiguous`, `changes`, `errors`
|
|
33
|
+
|
|
34
|
+
This is not a generic jsonschema wrapper. The primary value is turning **malformed agent output** into reliable structured data **without an LLM** and **without inventing semantic values**.
|
|
35
|
+
|
|
36
|
+
When repair would require guessing (truncated objects, missing values, competing JSON documents), the service **refuses**. That is correct behavior.
|
|
37
|
+
|
|
38
|
+
Primary HTTP endpoint: `POST /v1/json/reliable`
|
|
39
|
+
|
|
40
|
+
MCP tools (same core functions, no duplicate implementation): `json_reliable` / `reliable_json`, `validate_json`, `repair_json`, `inspect_json`
|
|
41
|
+
|
|
42
|
+
<!-- mcp-name: io.github.toninovo4249-ai/agent-json-reliability -->
|
|
43
|
+
|
|
44
|
+
## Before / after (safe repair)
|
|
45
|
+
|
|
46
|
+
Malformed agent output:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"text": "```json\n{\"name\":\"alice\",\"age\":30,}\n```"
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Expected result (semantically equivalent valid JSON):
|
|
55
|
+
|
|
56
|
+
- `valid_original=false`
|
|
57
|
+
- `repaired=true`
|
|
58
|
+
- `valid_final=true`
|
|
59
|
+
- `unsafe_or_ambiguous=false`
|
|
60
|
+
- `json` → `{"name":"alice","age":30}`
|
|
61
|
+
|
|
62
|
+
If a schema is supplied and satisfied: `schema_valid=true`.
|
|
63
|
+
|
|
64
|
+
## Safe refusal (ambiguous)
|
|
65
|
+
|
|
66
|
+
Truncated input such as `{"user":` is JSON-intended but not deterministically repairable without inventing keys or values.
|
|
67
|
+
|
|
68
|
+
Expected:
|
|
69
|
+
|
|
70
|
+
- HTTP `200` (documented safe response)
|
|
71
|
+
- `valid_final=false`
|
|
72
|
+
- `unsafe_or_ambiguous=true` **or** `repaired=false`
|
|
73
|
+
|
|
74
|
+
Do not treat a correct refusal as a product failure.
|
|
75
|
+
|
|
76
|
+
## Run locally (no Hunter)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
python -m pip install -r requirements.txt
|
|
80
|
+
python serve.py
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Binds `127.0.0.1:8770` by default. Set `PUBLIC_BASE_URL` to an HTTPS origin only when you expose the process yourself. Do not commit a temporary tunnel hostname as the canonical URL.
|
|
84
|
+
|
|
85
|
+
## HTTP examples
|
|
86
|
+
|
|
87
|
+
See `examples/curl.md`, `examples/python.py`, `examples/javascript.js`.
|
|
88
|
+
|
|
89
|
+
Remote examples use `${PUBLIC_BASE_URL}`. Local default is `http://127.0.0.1:8770`.
|
|
90
|
+
|
|
91
|
+
Share URLs (optional, unverified telemetry tags):
|
|
92
|
+
|
|
93
|
+
- `/?source=github`
|
|
94
|
+
- `/?source=mcp-registry`
|
|
95
|
+
- `/?source=api-directory`
|
|
96
|
+
|
|
97
|
+
## MCP (stdio — durable)
|
|
98
|
+
|
|
99
|
+
Package transport does not depend on a temporary public URL.
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"mcpServers": {
|
|
104
|
+
"agent-json-reliability": {
|
|
105
|
+
"command": "python",
|
|
106
|
+
"args": ["-m", "products.gateway.mcp_stdio"]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
After you publish to PyPI (free): `uvx agent-json-reliability`.
|
|
113
|
+
|
|
114
|
+
Remote HTTP MCP (`POST /mcp`) is for a running instance. Prefer stdio/package for Official MCP Registry metadata.
|
|
115
|
+
|
|
116
|
+
## Machine discovery
|
|
117
|
+
|
|
118
|
+
- `GET /.well-known/agent-services.json`
|
|
119
|
+
- `GET /openapi.json`
|
|
120
|
+
- `GET /llms.txt`
|
|
121
|
+
- `GET /AGENTS.md`
|
|
122
|
+
- `POST /mcp` JSON-RPC (`tools/list`, `tools/call`)
|
|
123
|
+
|
|
124
|
+
## What this package does not include
|
|
125
|
+
|
|
126
|
+
No Hunter market database, no private reports, no Windows user paths, no collectors, no wallets, no x402 payment.
|
|
127
|
+
|
|
128
|
+
Free beta: no auth, no payment.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
serve.py,sha256=q5a-jhErzovqZ-XGMRrUV8Eepy5SNJ0WfmFQaJZmBWo,958
|
|
2
|
+
agent_json_reliability-0.1.0.dist-info/licenses/LICENSE,sha256=3dC8g3pbUlohK3QJEGaAn12Qg30ciDaELSTd0BXIuGE,516
|
|
3
|
+
products/__init__.py,sha256=HuOKG1roMpOoMIYohAffe21pBS3DEVoTt4mF4nmSMEo,70
|
|
4
|
+
products/agent_json_reliability/__init__.py,sha256=wMoZTPy4uhabZCyc4xe0qX_6Q17qvU4zlG4CHtjfh0Q,298
|
|
5
|
+
products/agent_json_reliability/inspect.py,sha256=LcymxHNKlv-H6gxyovl_yhwva8QJDUW6El75nBSWnBc,2196
|
|
6
|
+
products/agent_json_reliability/reliable.py,sha256=8gGopZG6EMW3hGgWh_QOBRVzMWAPVc7LjVhR0Ui5Q4M,2592
|
|
7
|
+
products/agent_json_reliability/repair.py,sha256=P1XTciJbVlqYmcwYJCZ6vyCYHZ9od91MdLInoUI8Oi0,7395
|
|
8
|
+
products/agent_json_reliability/validate.py,sha256=ffBiHE6leHlGqTWXVunD2qSjeVsCsmBOSF3c7LN4VAU,7030
|
|
9
|
+
products/beta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
products/beta/app.py,sha256=rhJ7m-l_0CaVnyEknfDL_ZI2VQB1puC0SLCG3bhMksA,12702
|
|
11
|
+
products/beta/batch.py,sha256=Lg3ZKxOcENZyJMKv0NQlGNF_MxWjbEVlzzTEOg3doAI,917
|
|
12
|
+
products/beta/catalog.py,sha256=94Gmxi-HQHjBcn1WX1aB8VFxNNAVDmctMuKjJf2sdow,2080
|
|
13
|
+
products/beta/identity.py,sha256=ouXpqNQ4pjcBhzc9dYk6znBUe2y4pO7mSLQzmKwZGl8,4343
|
|
14
|
+
products/beta/landing.py,sha256=EReK7hINodUIPPe-Y2ixEfizXA48JUrgekZTc_Gfbkc,4669
|
|
15
|
+
products/beta/security.py,sha256=6YJbw4ahcR_G8n1pq04ndS3gP9AeWunaZRfxJQphdvc,2597
|
|
16
|
+
products/beta/selfcheck.py,sha256=G_iF5LqAReqLyKImScXurc5Ud99zmGfUcCyqCL5AqhU,1011
|
|
17
|
+
products/beta/settings.py,sha256=W6yfqC2yyj_-xulhPF3Ery8tO8f5ChlpuAUVvzF9h6U,2973
|
|
18
|
+
products/beta/store.py,sha256=kLwTrL-iNFG2pLyML8ApTWDJyYdfU-U0sW7YnLvwcSE,4732
|
|
19
|
+
products/common/__init__.py,sha256=rJlpYCJdYduuCGz_7RftJexTnJF3WrdekL4yzG1glko,175
|
|
20
|
+
products/common/timing.py,sha256=8F3x3x7n5jrp1e-l2SEjWaOPq9FOCREkipJINnf7Wv0,313
|
|
21
|
+
products/gateway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
+
products/gateway/manifest.py,sha256=sDFQsA71EKrF4sFrSS98oe5IpsTEQiLm7hBUCqf1isc,6993
|
|
23
|
+
products/gateway/mcp_server.py,sha256=YmSLbjr9D6Ej8YubSPj0aOKjHqlnjqM1QmZj_4vVKis,2732
|
|
24
|
+
products/gateway/mcp_stdio.py,sha256=B0japGfz3QKOgYrfNXtj-wXWcoUKrW6OFjlLAlylh3I,1508
|
|
25
|
+
products/gateway/rate_limit.py,sha256=LwcIyqk7BWTxAaJWhm5uTrXaWo1AOD_WYbpNSRGADsg,885
|
|
26
|
+
agent_json_reliability-0.1.0.dist-info/METADATA,sha256=V4eMP8eTvNfaqV4Fith5UUaksZE8N_omc8aGVeQtxZI,4252
|
|
27
|
+
agent_json_reliability-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
28
|
+
agent_json_reliability-0.1.0.dist-info/entry_points.txt,sha256=nr7RzsB4dHKHEQjtA1M1ieHro5HRfdOwTlNTwUxrS0M,117
|
|
29
|
+
agent_json_reliability-0.1.0.dist-info/top_level.txt,sha256=WdV6O4or29KOsiwfII1hnZH5pdU0DnRVfpa1hp1Vdtc,15
|
|
30
|
+
agent_json_reliability-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
|
products/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Local zero-cost product prototypes. Independent of collectors."""
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from products.agent_json_reliability.inspect import inspect_json
|
|
4
|
+
from products.agent_json_reliability.repair import repair_json
|
|
5
|
+
from products.agent_json_reliability.validate import validate_json
|
|
6
|
+
|
|
7
|
+
__all__ = ["validate_json", "repair_json", "inspect_json"]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from products.agent_json_reliability.repair import repair_json
|
|
8
|
+
from products.common.timing import Clock
|
|
9
|
+
|
|
10
|
+
_FAILURES = [
|
|
11
|
+
("markdown_fence", re.compile(r"```")),
|
|
12
|
+
("trailing_comma", re.compile(r",\s*[}\]]")),
|
|
13
|
+
("single_quotes", re.compile(r"'")),
|
|
14
|
+
("python_literals", re.compile(r"\b(True|False|None)\b")),
|
|
15
|
+
("comments", re.compile(r"//|/\*")),
|
|
16
|
+
("leading_prose", None),
|
|
17
|
+
("truncated", None),
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def inspect_json(text: str) -> dict[str, Any]:
|
|
22
|
+
with Clock() as sw:
|
|
23
|
+
if not isinstance(text, str):
|
|
24
|
+
text = str(text)
|
|
25
|
+
raw = text.strip()
|
|
26
|
+
valid = False
|
|
27
|
+
err_pos = None
|
|
28
|
+
try:
|
|
29
|
+
json.loads(raw)
|
|
30
|
+
valid = True
|
|
31
|
+
except json.JSONDecodeError as e:
|
|
32
|
+
err_pos = e.pos
|
|
33
|
+
types: list[str] = []
|
|
34
|
+
if re.search(r"```", raw):
|
|
35
|
+
types.append("markdown_fence")
|
|
36
|
+
if re.search(r",\s*[}\]]", raw):
|
|
37
|
+
types.append("trailing_comma")
|
|
38
|
+
if "'" in raw and '"' not in raw:
|
|
39
|
+
types.append("single_quotes")
|
|
40
|
+
if re.search(r"\b(True|False|None)\b", raw):
|
|
41
|
+
types.append("python_literals")
|
|
42
|
+
if "//" in raw or "/*" in raw:
|
|
43
|
+
types.append("comments")
|
|
44
|
+
stripped = raw.lstrip()
|
|
45
|
+
if stripped and stripped[0] not in "{[":
|
|
46
|
+
types.append("leading_prose")
|
|
47
|
+
if raw.count("{") > raw.count("}") or raw.count("[") > raw.count("]"):
|
|
48
|
+
types.append("truncated")
|
|
49
|
+
if re.search(r"\}\s*\{", raw):
|
|
50
|
+
types.append("multiple_objects")
|
|
51
|
+
likely = valid or bool(re.search(r"[\{\[]", raw))
|
|
52
|
+
rep = repair_json(raw)
|
|
53
|
+
complexity = min(100, raw.count("{") + raw.count("[") + raw.count(":") + len(raw) // 200)
|
|
54
|
+
return {
|
|
55
|
+
"valid_json": valid,
|
|
56
|
+
"likely_json": likely,
|
|
57
|
+
"error_position": err_pos,
|
|
58
|
+
"detected_failure_types": types,
|
|
59
|
+
"repair_possible": bool(rep.get("repaired") or valid),
|
|
60
|
+
"estimated_complexity": complexity,
|
|
61
|
+
"processing_ms": sw.ms,
|
|
62
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from products.agent_json_reliability.inspect import inspect_json
|
|
6
|
+
from products.agent_json_reliability.repair import repair_json
|
|
7
|
+
from products.agent_json_reliability.validate import validate_json
|
|
8
|
+
from products.common.timing import Clock
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def reliable_json(text: str, schema: dict | None = None, mode: str = "safe") -> dict[str, Any]:
|
|
12
|
+
"""inspect → safe repair if needed → optional schema validate. Same primitives as HTTP tools."""
|
|
13
|
+
with Clock() as sw:
|
|
14
|
+
insp = inspect_json(text)
|
|
15
|
+
valid_original = bool(insp.get("valid_json"))
|
|
16
|
+
changes: list[str] = []
|
|
17
|
+
repaired = False
|
|
18
|
+
payload: Any = None
|
|
19
|
+
unsafe = False
|
|
20
|
+
if valid_original:
|
|
21
|
+
import json
|
|
22
|
+
|
|
23
|
+
payload = json.loads(text.strip().lstrip("\ufeff"))
|
|
24
|
+
else:
|
|
25
|
+
rep = repair_json(text, mode)
|
|
26
|
+
repaired = bool(rep.get("repaired"))
|
|
27
|
+
payload = rep.get("json")
|
|
28
|
+
changes = list(rep.get("changes") or [])
|
|
29
|
+
unsafe = bool(rep.get("unsafe_or_ambiguous"))
|
|
30
|
+
valid_final = payload is not None
|
|
31
|
+
schema_valid = None
|
|
32
|
+
errors: list = []
|
|
33
|
+
if valid_final and schema is not None:
|
|
34
|
+
v = validate_json(payload, schema)
|
|
35
|
+
schema_valid = v.get("schema_valid")
|
|
36
|
+
errors = v.get("errors") or []
|
|
37
|
+
valid_final = bool(schema_valid)
|
|
38
|
+
elif not valid_final:
|
|
39
|
+
errors = [{"message": "unrepaired_or_invalid"}]
|
|
40
|
+
return {
|
|
41
|
+
"valid_original": valid_original,
|
|
42
|
+
"repaired": repaired,
|
|
43
|
+
"json": payload,
|
|
44
|
+
"valid_final": valid_final and payload is not None and (schema_valid is not False),
|
|
45
|
+
"schema_valid": schema_valid,
|
|
46
|
+
"unsafe_or_ambiguous": unsafe,
|
|
47
|
+
"changes": changes,
|
|
48
|
+
"errors": errors,
|
|
49
|
+
"inspect": insp,
|
|
50
|
+
"processing_ms": sw.ms,
|
|
51
|
+
"outcome_class": _outcome(valid_original, repaired, payload, schema_valid, errors),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _outcome(valid_original, repaired, payload, schema_valid, errors) -> str:
|
|
56
|
+
if schema_valid is False:
|
|
57
|
+
return "SCHEMA_INVALID"
|
|
58
|
+
if schema_valid is True:
|
|
59
|
+
return "SCHEMA_VALID"
|
|
60
|
+
if valid_original:
|
|
61
|
+
return "ORIGINAL_VALID"
|
|
62
|
+
if repaired and payload is not None:
|
|
63
|
+
return "SAFE_REPAIR_SUCCESS"
|
|
64
|
+
if any((e.get("message") or "").startswith("schema") for e in errors or []):
|
|
65
|
+
return "INVALID_SCHEMA"
|
|
66
|
+
return "SAFE_REPAIR_REFUSED"
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ast
|
|
4
|
+
import json
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from products.common.timing import Clock
|
|
9
|
+
|
|
10
|
+
_FENCE = re.compile(r"```(?:json|JSON)?\s*([\s\S]*?)```", re.MULTILINE)
|
|
11
|
+
_TRAIL_COMMA = re.compile(r",(\s*[}\]])")
|
|
12
|
+
_COMMENT_LINE = re.compile(r"(^|[^:])//.*?$", re.MULTILINE)
|
|
13
|
+
_COMMENT_BLOCK = re.compile(r"/\*[\s\S]*?\*/")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _extract_fenced(text: str, changes: list[str]) -> str:
|
|
17
|
+
m = _FENCE.search(text)
|
|
18
|
+
if m:
|
|
19
|
+
changes.append("strip_markdown_fence")
|
|
20
|
+
return m.group(1).strip()
|
|
21
|
+
return text
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _extract_balanced(text: str, changes: list[str]) -> str:
|
|
25
|
+
starts = [i for i, ch in enumerate(text) if ch in "{["]
|
|
26
|
+
if not starts:
|
|
27
|
+
return text.strip()
|
|
28
|
+
first = starts[0]
|
|
29
|
+
chunk = text[first:]
|
|
30
|
+
if first > 0 and text[:first].strip():
|
|
31
|
+
changes.append("strip_leading_prose")
|
|
32
|
+
depth_obj = depth_arr = 0
|
|
33
|
+
in_str = False
|
|
34
|
+
esc = False
|
|
35
|
+
last = -1
|
|
36
|
+
for i, ch in enumerate(chunk):
|
|
37
|
+
if in_str:
|
|
38
|
+
if esc:
|
|
39
|
+
esc = False
|
|
40
|
+
elif ch == "\\":
|
|
41
|
+
esc = True
|
|
42
|
+
elif ch == '"':
|
|
43
|
+
in_str = False
|
|
44
|
+
continue
|
|
45
|
+
if ch == '"':
|
|
46
|
+
in_str = True
|
|
47
|
+
continue
|
|
48
|
+
if ch == "{":
|
|
49
|
+
depth_obj += 1
|
|
50
|
+
elif ch == "}":
|
|
51
|
+
depth_obj -= 1
|
|
52
|
+
elif ch == "[":
|
|
53
|
+
depth_arr += 1
|
|
54
|
+
elif ch == "]":
|
|
55
|
+
depth_arr -= 1
|
|
56
|
+
if depth_obj == 0 and depth_arr == 0 and ch in "}]":
|
|
57
|
+
last = i
|
|
58
|
+
break
|
|
59
|
+
if last >= 0:
|
|
60
|
+
tail = chunk[last + 1 :]
|
|
61
|
+
st = tail.strip()
|
|
62
|
+
if st.startswith("{") or st.startswith("["):
|
|
63
|
+
changes.append("multiple_json_values")
|
|
64
|
+
return chunk
|
|
65
|
+
if st:
|
|
66
|
+
changes.append("strip_trailing_prose")
|
|
67
|
+
return chunk[: last + 1]
|
|
68
|
+
return chunk
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _strip_comments(text: str, changes: list[str]) -> str:
|
|
72
|
+
if "//" not in text and "/*" not in text:
|
|
73
|
+
return text
|
|
74
|
+
nxt = _COMMENT_BLOCK.sub("", text)
|
|
75
|
+
nxt = _COMMENT_LINE.sub(lambda m: m.group(1), nxt)
|
|
76
|
+
if nxt != text:
|
|
77
|
+
changes.append("strip_comments")
|
|
78
|
+
return nxt
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _py_literals(text: str, changes: list[str]) -> str:
|
|
82
|
+
nxt = re.sub(r"\bTrue\b", "true", text)
|
|
83
|
+
nxt = re.sub(r"\bFalse\b", "false", nxt)
|
|
84
|
+
nxt = re.sub(r"\bNone\b", "null", nxt)
|
|
85
|
+
if nxt != text:
|
|
86
|
+
changes.append("python_literals")
|
|
87
|
+
return nxt
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _trailing_commas(text: str, changes: list[str]) -> str:
|
|
91
|
+
nxt, n = _TRAIL_COMMA.subn(r"\1", text)
|
|
92
|
+
if n:
|
|
93
|
+
changes.append("strip_trailing_commas")
|
|
94
|
+
return nxt
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _close_truncated(text: str, changes: list[str]) -> str | None:
|
|
98
|
+
"""Close extra { [ only when the remainder is empty of required values."""
|
|
99
|
+
in_str = False
|
|
100
|
+
esc = False
|
|
101
|
+
stack: list[str] = []
|
|
102
|
+
last_sig = ""
|
|
103
|
+
for ch in text:
|
|
104
|
+
if in_str:
|
|
105
|
+
if esc:
|
|
106
|
+
esc = False
|
|
107
|
+
elif ch == "\\":
|
|
108
|
+
esc = True
|
|
109
|
+
elif ch == '"':
|
|
110
|
+
in_str = False
|
|
111
|
+
continue
|
|
112
|
+
if ch == '"':
|
|
113
|
+
in_str = True
|
|
114
|
+
last_sig = '"'
|
|
115
|
+
continue
|
|
116
|
+
if ch in "{[":
|
|
117
|
+
stack.append("}" if ch == "{" else "]")
|
|
118
|
+
last_sig = ch
|
|
119
|
+
elif ch in "}]":
|
|
120
|
+
if not stack or stack[-1] != ch:
|
|
121
|
+
return None
|
|
122
|
+
stack.pop()
|
|
123
|
+
last_sig = ch
|
|
124
|
+
elif ch == ":":
|
|
125
|
+
last_sig = ":"
|
|
126
|
+
elif ch == ",":
|
|
127
|
+
last_sig = ","
|
|
128
|
+
elif not ch.isspace():
|
|
129
|
+
last_sig = "v"
|
|
130
|
+
if in_str:
|
|
131
|
+
return None
|
|
132
|
+
if not stack:
|
|
133
|
+
return None
|
|
134
|
+
if last_sig in {":"}:
|
|
135
|
+
return None # would invent a value
|
|
136
|
+
closed = text + "".join(reversed(stack))
|
|
137
|
+
changes.append("close_truncated_brackets")
|
|
138
|
+
return closed
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _try_load(text: str) -> Any:
|
|
142
|
+
return json.loads(text)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _literal_eval_jsonish(text: str) -> Any | None:
|
|
146
|
+
try:
|
|
147
|
+
val = ast.literal_eval(text)
|
|
148
|
+
except (SyntaxError, ValueError, MemoryError):
|
|
149
|
+
return None
|
|
150
|
+
if isinstance(val, (dict, list, str, int, float, bool)) or val is None:
|
|
151
|
+
return val
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def repair_json(text: str, mode: str = "safe") -> dict[str, Any]:
|
|
156
|
+
with Clock() as sw:
|
|
157
|
+
changes: list[str] = []
|
|
158
|
+
if not isinstance(text, str):
|
|
159
|
+
return _fail(sw, "text_not_string", changes)
|
|
160
|
+
if len(text.encode("utf-8")) > 262_144:
|
|
161
|
+
return _fail(sw, "too_large", changes)
|
|
162
|
+
raw = text.strip().lstrip("\ufeff")
|
|
163
|
+
if raw != text.strip() or text[:1] == "\ufeff" or text != text.strip():
|
|
164
|
+
changes.append("strip_bom_or_whitespace")
|
|
165
|
+
if not raw:
|
|
166
|
+
return _fail(sw, "empty", changes)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
parsed = _try_load(raw)
|
|
170
|
+
return _ok(parsed, bool(changes), changes, "high", False, sw)
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
work = _extract_fenced(raw, changes)
|
|
175
|
+
work = _extract_balanced(work, changes)
|
|
176
|
+
if "multiple_json_values" in changes:
|
|
177
|
+
return _fail(sw, "multiple_json_values", changes, unsafe=True)
|
|
178
|
+
work = _strip_comments(work, changes)
|
|
179
|
+
|
|
180
|
+
looks_py = ("True" in work or "False" in work or "None" in work or ("'" in work and '"' not in work))
|
|
181
|
+
if looks_py:
|
|
182
|
+
lit = _literal_eval_jsonish(work)
|
|
183
|
+
if lit is not None:
|
|
184
|
+
changes.append("python_literal_eval")
|
|
185
|
+
return _ok(lit, True, changes, "medium", False, sw)
|
|
186
|
+
|
|
187
|
+
work = _py_literals(work, changes)
|
|
188
|
+
work = _trailing_commas(work, changes)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
parsed = _try_load(work)
|
|
192
|
+
return _ok(parsed, True, changes, "high", False, sw)
|
|
193
|
+
except json.JSONDecodeError:
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
closed = _close_truncated(work, changes)
|
|
197
|
+
if closed is not None:
|
|
198
|
+
closed = _trailing_commas(closed, changes)
|
|
199
|
+
try:
|
|
200
|
+
parsed = _try_load(closed)
|
|
201
|
+
return _ok(parsed, True, changes, "medium", False, sw)
|
|
202
|
+
except json.JSONDecodeError:
|
|
203
|
+
changes[:] = [c for c in changes if c != "close_truncated_brackets"]
|
|
204
|
+
|
|
205
|
+
decoder = json.JSONDecoder()
|
|
206
|
+
try:
|
|
207
|
+
first, idx = decoder.raw_decode(work)
|
|
208
|
+
rest = work[idx:].strip()
|
|
209
|
+
if rest:
|
|
210
|
+
return _fail(sw, "multiple_json_values", changes, unsafe=True)
|
|
211
|
+
return _ok(first, True, changes, "medium", False, sw)
|
|
212
|
+
except json.JSONDecodeError:
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
return _fail(sw, "unambiguous_repair_impossible", changes, unsafe=True)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _ok(parsed, repaired, changes, confidence, unsafe, sw):
|
|
219
|
+
return {
|
|
220
|
+
"repaired": repaired,
|
|
221
|
+
"json": parsed,
|
|
222
|
+
"changes": changes,
|
|
223
|
+
"confidence": confidence,
|
|
224
|
+
"unsafe_or_ambiguous": unsafe,
|
|
225
|
+
"processing_ms": sw.ms,
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _fail(sw, reason: str, changes: list[str], unsafe: bool = False):
|
|
230
|
+
return {
|
|
231
|
+
"repaired": False,
|
|
232
|
+
"json": None,
|
|
233
|
+
"changes": changes + [reason],
|
|
234
|
+
"confidence": "low",
|
|
235
|
+
"unsafe_or_ambiguous": True if unsafe else False,
|
|
236
|
+
"processing_ms": sw.ms,
|
|
237
|
+
}
|