markdown-flow 0.2.19__py3-none-any.whl → 0.2.30__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.
markdown_flow/llm.py CHANGED
@@ -15,7 +15,6 @@ from .constants import NO_LLM_PROVIDER_ERROR
15
15
  class ProcessMode(Enum):
16
16
  """LLM processing modes."""
17
17
 
18
- PROMPT_ONLY = "prompt_only" # Return prompt only, no LLM call
19
18
  COMPLETE = "complete" # Complete processing (non-streaming)
20
19
  STREAM = "stream" # Streaming processing
21
20
 
@@ -43,7 +42,8 @@ class LLMProvider(ABC):
43
42
  Non-streaming LLM call.
44
43
 
45
44
  Args:
46
- messages: Message list in format [{"role": "system/user/assistant", "content": "..."}]
45
+ messages: Message list in format [{"role": "system/user/assistant", "content": "..."}].
46
+ This list already includes conversation history context merged by MarkdownFlow.
47
47
 
48
48
  Returns:
49
49
  str: LLM response content
@@ -58,7 +58,8 @@ class LLMProvider(ABC):
58
58
  Streaming LLM call.
59
59
 
60
60
  Args:
61
- messages: Message list in format [{"role": "system/user/assistant", "content": "..."}]
61
+ messages: Message list in format [{"role": "system/user/assistant", "content": "..."}].
62
+ This list already includes conversation history context merged by MarkdownFlow.
62
63
 
63
64
  Yields:
64
65
  str: Incremental LLM response content
markdown_flow/models.py CHANGED
@@ -7,7 +7,7 @@ Simplified and refactored data models focused on core functionality.
7
7
  from dataclasses import dataclass, field
8
8
 
9
9
  from .enums import BlockType, InputType
10
- from .utils import extract_variables_from_text
10
+ from .parser import extract_variables_from_text
11
11
 
12
12
 
13
13
  @dataclass
@@ -26,22 +26,6 @@ class UserInput:
26
26
  is_multi_select: bool = False
27
27
 
28
28
 
29
- @dataclass
30
- class InteractionValidationConfig:
31
- """
32
- Simplified interaction validation configuration.
33
-
34
- Attributes:
35
- validation_template (Optional[str]): Validation prompt template
36
- target_variable (Optional[str]): Target variable name
37
- enable_custom_validation (bool): Enable custom validation, defaults to True
38
- """
39
-
40
- validation_template: str | None = None
41
- target_variable: str | None = None
42
- enable_custom_validation: bool = True
43
-
44
-
45
29
  @dataclass
46
30
  class Block:
47
31
  """
@@ -0,0 +1,38 @@
1
+ """
2
+ Markdown-Flow Parser Module
3
+
4
+ Provides specialized parsers for different aspects of MarkdownFlow document processing.
5
+ """
6
+
7
+ from .interaction import InteractionParser, InteractionType, extract_interaction_question
8
+ from .json_parser import parse_json_response
9
+ from .output import (
10
+ extract_preserved_content,
11
+ is_preserved_content_block,
12
+ process_output_instructions,
13
+ )
14
+ from .preprocessor import CodeBlockPreprocessor
15
+ from .validation import generate_smart_validation_template, parse_validation_response
16
+ from .variable import extract_variables_from_text, replace_variables_in_text
17
+
18
+
19
+ __all__ = [
20
+ # Variable parsing
21
+ "extract_variables_from_text",
22
+ "replace_variables_in_text",
23
+ # Interaction parsing
24
+ "InteractionParser",
25
+ "InteractionType",
26
+ "extract_interaction_question",
27
+ # Output and preserved content
28
+ "is_preserved_content_block",
29
+ "extract_preserved_content",
30
+ "process_output_instructions",
31
+ # Code block preprocessing
32
+ "CodeBlockPreprocessor",
33
+ # Validation
34
+ "generate_smart_validation_template",
35
+ "parse_validation_response",
36
+ # JSON parsing
37
+ "parse_json_response",
38
+ ]
@@ -0,0 +1,190 @@
1
+ """
2
+ Code Fence Utilities
3
+
4
+ Provides CommonMark-compliant code fence parsing utility functions.
5
+ """
6
+
7
+ from dataclasses import dataclass
8
+
9
+ from ..constants import (
10
+ COMPILED_CODE_FENCE_END_REGEX,
11
+ COMPILED_CODE_FENCE_START_REGEX,
12
+ )
13
+
14
+
15
+ @dataclass
16
+ class CodeFenceInfo:
17
+ """
18
+ Code fence information
19
+
20
+ Used to track the opening fence of a code block for proper matching with closing fence.
21
+
22
+ Attributes:
23
+ char: Fence character ('`' or '~')
24
+ length: Fence length (≥3)
25
+ indent: Number of indent spaces (≤3)
26
+ line: Full opening fence line (including info string, e.g., language identifier)
27
+ """
28
+
29
+ char: str
30
+ length: int
31
+ indent: int
32
+ line: str
33
+
34
+
35
+ def validate_fence_characters(fence_str: str) -> bool:
36
+ """
37
+ Validate that all characters in the fence string are the same
38
+
39
+ CommonMark specification: fence must consist of the same character (all ` or all ~)
40
+
41
+ Args:
42
+ fence_str: Fence string (e.g., "```" or "~~~~")
43
+
44
+ Returns:
45
+ True if all characters are the same, False otherwise
46
+
47
+ Examples:
48
+ >>> validate_fence_characters("```")
49
+ True
50
+ >>> validate_fence_characters("~~~~")
51
+ True
52
+ >>> validate_fence_characters("``~")
53
+ False
54
+ >>> validate_fence_characters("")
55
+ False
56
+ """
57
+ if not fence_str:
58
+ return False
59
+
60
+ fence_char = fence_str[0]
61
+ return all(ch == fence_char for ch in fence_str)
62
+
63
+
64
+ def parse_code_fence_start(line: str) -> CodeFenceInfo | None:
65
+ """
66
+ Parse code block opening fence marker
67
+
68
+ CommonMark specification:
69
+ - 0-3 spaces indent
70
+ - At least 3 consecutive ` or ~ characters
71
+ - All characters must be the same
72
+ - Optional info string (language identifier)
73
+
74
+ Args:
75
+ line: Line to detect
76
+
77
+ Returns:
78
+ CodeFenceInfo if valid opening fence marker, None otherwise
79
+
80
+ Examples:
81
+ >>> parse_code_fence_start("```")
82
+ CodeFenceInfo(char='`', length=3, ...)
83
+ >>> parse_code_fence_start("```go")
84
+ CodeFenceInfo(char='`', length=3, line="```go", ...)
85
+ >>> parse_code_fence_start(" ~~~python")
86
+ CodeFenceInfo(char='~', length=3, indent=3, ...)
87
+ >>> parse_code_fence_start(" ```")
88
+ None # indent > 3
89
+ >>> parse_code_fence_start("``~")
90
+ None # mixed characters
91
+ """
92
+ match = COMPILED_CODE_FENCE_START_REGEX.match(line)
93
+ if not match:
94
+ return None
95
+
96
+ # match.group(1) is the fence string (e.g., ```, ~~~~)
97
+ # match.group(2) is the info string (e.g., go, python)
98
+ fence_str = match.group(1)
99
+
100
+ # Validate all characters are the same (backticks or tildes)
101
+ if not validate_fence_characters(fence_str):
102
+ return None
103
+
104
+ # Calculate indent
105
+ indent = len(line) - len(line.lstrip(" "))
106
+
107
+ # Validate indent ≤ 3 (CommonMark specification)
108
+ if indent > 3:
109
+ return None
110
+
111
+ # Fence length
112
+ fence_length = len(fence_str)
113
+
114
+ # Validate fence length ≥ 3 (regex already ensures this, but check to be safe)
115
+ if fence_length < 3:
116
+ return None
117
+
118
+ return CodeFenceInfo(
119
+ char=fence_str[0],
120
+ length=fence_length,
121
+ indent=indent,
122
+ line=line,
123
+ )
124
+
125
+
126
+ def is_code_fence_end(line: str, start_fence: CodeFenceInfo) -> bool:
127
+ """
128
+ Detect if line is a matching code block closing fence marker
129
+
130
+ CommonMark specification:
131
+ - Use same type of fence character (` or ~)
132
+ - Fence length ≥ opening fence
133
+ - 0-3 spaces indent
134
+ - Only contains fence characters and whitespace
135
+
136
+ Args:
137
+ line: Line to detect
138
+ start_fence: Opening fence information
139
+
140
+ Returns:
141
+ Whether line is a matching closing fence
142
+
143
+ Examples:
144
+ >>> start = CodeFenceInfo(char='`', length=3, indent=0, line="```")
145
+ >>> is_code_fence_end("```", start)
146
+ True
147
+ >>> is_code_fence_end("````", start)
148
+ True # length ≥ opening fence
149
+ >>> is_code_fence_end("~~~", start)
150
+ False # character type mismatch
151
+ >>> is_code_fence_end("``", start)
152
+ False # length < opening fence
153
+ >>> is_code_fence_end(" ```", start)
154
+ False # indent > 3
155
+ """
156
+ match = COMPILED_CODE_FENCE_END_REGEX.match(line)
157
+ if not match:
158
+ return False
159
+
160
+ # Extract indent
161
+ indent = len(line) - len(line.lstrip(" "))
162
+
163
+ # Validate indent ≤ 3
164
+ if indent > 3:
165
+ return False
166
+
167
+ # Extract fence string (remove indent and trailing whitespace)
168
+ fence_str = line.strip()
169
+
170
+ # Validate non-empty
171
+ if not fence_str:
172
+ return False
173
+
174
+ first_char = fence_str[0]
175
+
176
+ # Character type must match
177
+ if first_char != start_fence.char:
178
+ return False
179
+
180
+ # Calculate fence length (count consecutive same characters)
181
+ fence_length = 0
182
+ for ch in fence_str:
183
+ if ch == first_char:
184
+ fence_length += 1
185
+ else:
186
+ # Contains other characters, not a valid closing fence
187
+ return False
188
+
189
+ # Length must be ≥ opening fence
190
+ return fence_length >= start_fence.length
@@ -0,0 +1,354 @@
1
+ """
2
+ Interaction Parser Module
3
+
4
+ Provides three-layer interaction parsing for MarkdownFlow ?[] format validation,
5
+ variable detection, and content parsing.
6
+ """
7
+
8
+ from enum import Enum
9
+ from typing import Any
10
+
11
+ from ..constants import (
12
+ COMPILED_INTERACTION_REGEX,
13
+ COMPILED_LAYER1_INTERACTION_REGEX,
14
+ COMPILED_LAYER2_VARIABLE_REGEX,
15
+ COMPILED_LAYER3_ELLIPSIS_REGEX,
16
+ COMPILED_SINGLE_PIPE_SPLIT_REGEX,
17
+ )
18
+
19
+
20
+ class InteractionType(Enum):
21
+ """Interaction input type enumeration."""
22
+
23
+ TEXT_ONLY = "text_only" # Text input only: ?[%{{var}}...question]
24
+ BUTTONS_ONLY = "buttons_only" # Button selection only: ?[%{{var}} A|B]
25
+ BUTTONS_WITH_TEXT = "buttons_with_text" # Buttons + text: ?[%{{var}} A|B|...question]
26
+ BUTTONS_MULTI_SELECT = "buttons_multi_select" # Multi-select buttons: ?[%{{var}} A||B]
27
+ BUTTONS_MULTI_WITH_TEXT = "buttons_multi_with_text" # Multi-select + text: ?[%{{var}} A||B||...question]
28
+ NON_ASSIGNMENT_BUTTON = "non_assignment_button" # Display buttons: ?[Continue|Cancel]
29
+
30
+
31
+ def extract_interaction_question(content: str) -> str | None:
32
+ """
33
+ Extract question text from interaction block content.
34
+
35
+ Args:
36
+ content: Raw interaction block content
37
+
38
+ Returns:
39
+ Question text if found, None otherwise
40
+ """
41
+ # Match interaction format: ?[...] using pre-compiled regex
42
+ match = COMPILED_INTERACTION_REGEX.match(content.strip())
43
+ if not match:
44
+ return None # type: ignore[unreachable]
45
+
46
+ # Extract interaction content (remove ?[ and ])
47
+ interaction_content = match.group(1) if match.groups() else match.group(0)[2:-1]
48
+
49
+ # Find ... separator, question text follows
50
+ if "..." in interaction_content:
51
+ # Split and get question part
52
+ parts = interaction_content.split("...", 1)
53
+ if len(parts) > 1:
54
+ return parts[1].strip()
55
+
56
+ return None # type: ignore[unreachable]
57
+
58
+
59
+ class InteractionParser:
60
+ """
61
+ Three-layer interaction parser for ?[] format validation,
62
+ variable detection, and content parsing.
63
+ """
64
+
65
+ def __init__(self):
66
+ """Initialize parser."""
67
+
68
+ def parse(self, content: str) -> dict[str, Any]:
69
+ """
70
+ Main parsing method.
71
+
72
+ Args:
73
+ content: Raw interaction block content
74
+
75
+ Returns:
76
+ Standardized parsing result with type, variable, buttons, and question fields
77
+ """
78
+ try:
79
+ # Layer 1: Validate basic format
80
+ inner_content = self._layer1_validate_format(content)
81
+ if inner_content is None:
82
+ return self._create_error_result(f"Invalid interaction format: {content}")
83
+
84
+ # Layer 2: Variable detection and pattern classification
85
+ has_variable, variable_name, remaining_content = self._layer2_detect_variable(inner_content)
86
+
87
+ # Layer 3: Specific content parsing
88
+ if has_variable:
89
+ assert variable_name is not None, "variable_name should not be None when has_variable is True"
90
+ return self._layer3_parse_variable_interaction(variable_name, remaining_content)
91
+ return self._layer3_parse_display_buttons(inner_content)
92
+
93
+ except Exception as e:
94
+ return self._create_error_result(f"Parsing error: {str(e)}")
95
+
96
+ def _layer1_validate_format(self, content: str) -> str | None:
97
+ """
98
+ Layer 1: Validate ?[] format and extract content.
99
+
100
+ Args:
101
+ content: Raw content
102
+
103
+ Returns:
104
+ Extracted bracket content, None if validation fails
105
+ """
106
+ content = content.strip()
107
+ match = COMPILED_LAYER1_INTERACTION_REGEX.search(content)
108
+
109
+ if not match:
110
+ return None # type: ignore[unreachable]
111
+
112
+ # Ensure matched content is complete (no other text)
113
+ matched_text = match.group(0)
114
+ if matched_text.strip() != content:
115
+ return None
116
+
117
+ return match.group(1)
118
+
119
+ def _layer2_detect_variable(self, inner_content: str) -> tuple[bool, str | None, str]:
120
+ """
121
+ Layer 2: Detect variables and classify patterns.
122
+
123
+ Args:
124
+ inner_content: Content extracted from layer 1
125
+
126
+ Returns:
127
+ Tuple of (has_variable, variable_name, remaining_content)
128
+ """
129
+ match = COMPILED_LAYER2_VARIABLE_REGEX.match(inner_content)
130
+
131
+ if not match:
132
+ # No variable, use entire content for display button parsing
133
+ return False, None, inner_content # type: ignore[unreachable]
134
+
135
+ variable_name = match.group(1).strip()
136
+ remaining_content = match.group(2).strip()
137
+
138
+ return True, variable_name, remaining_content
139
+
140
+ def _layer3_parse_variable_interaction(self, variable_name: str, content: str) -> dict[str, Any]:
141
+ """
142
+ Layer 3: Parse variable interactions (variable assignment type).
143
+
144
+ Args:
145
+ variable_name: Variable name
146
+ content: Content after variable
147
+
148
+ Returns:
149
+ Parsing result dictionary
150
+ """
151
+ # Detect ... separator
152
+ ellipsis_match = COMPILED_LAYER3_ELLIPSIS_REGEX.match(content)
153
+
154
+ if ellipsis_match:
155
+ # Has ... separator
156
+ before_ellipsis = ellipsis_match.group(1).strip()
157
+ question = ellipsis_match.group(2).strip()
158
+
159
+ if before_ellipsis:
160
+ # Has prefix content (buttons or single option) + text input
161
+ buttons, is_multi_select = self._parse_buttons(before_ellipsis)
162
+ interaction_type = InteractionType.BUTTONS_MULTI_WITH_TEXT if is_multi_select else InteractionType.BUTTONS_WITH_TEXT
163
+ return {
164
+ "type": interaction_type,
165
+ "variable": variable_name,
166
+ "buttons": buttons,
167
+ "question": question,
168
+ "is_multi_select": is_multi_select,
169
+ }
170
+ # Pure text input
171
+ return {
172
+ "type": InteractionType.TEXT_ONLY,
173
+ "variable": variable_name,
174
+ "question": question,
175
+ "is_multi_select": False,
176
+ }
177
+ # No ... separator
178
+ if ("|" in content or "||" in content) and content: # type: ignore[unreachable]
179
+ # Pure button group
180
+ buttons, is_multi_select = self._parse_buttons(content)
181
+ interaction_type = InteractionType.BUTTONS_MULTI_SELECT if is_multi_select else InteractionType.BUTTONS_ONLY
182
+ return {
183
+ "type": interaction_type,
184
+ "variable": variable_name,
185
+ "buttons": buttons,
186
+ "is_multi_select": is_multi_select,
187
+ }
188
+ if content: # type: ignore[unreachable]
189
+ # Single button
190
+ button = self._parse_single_button(content)
191
+ return {
192
+ "type": InteractionType.BUTTONS_ONLY,
193
+ "variable": variable_name,
194
+ "buttons": [button],
195
+ "is_multi_select": False,
196
+ }
197
+ # Pure text input (no hint)
198
+ return {
199
+ "type": InteractionType.TEXT_ONLY,
200
+ "variable": variable_name,
201
+ "question": "",
202
+ "is_multi_select": False,
203
+ }
204
+
205
+ def _layer3_parse_display_buttons(self, content: str) -> dict[str, Any]:
206
+ """
207
+ Layer 3: Parse display buttons (non-variable assignment type).
208
+
209
+ Args:
210
+ content: Content to parse
211
+
212
+ Returns:
213
+ Parsing result dictionary
214
+ """
215
+ if not content:
216
+ # Empty content: ?[]
217
+ return {
218
+ "type": InteractionType.NON_ASSIGNMENT_BUTTON,
219
+ "buttons": [{"display": "", "value": ""}],
220
+ }
221
+
222
+ if "|" in content:
223
+ # Multiple buttons
224
+ buttons, _ = self._parse_buttons(content) # Display buttons don't use multi-select
225
+ return {"type": InteractionType.NON_ASSIGNMENT_BUTTON, "buttons": buttons}
226
+ # Single button
227
+ button = self._parse_single_button(content)
228
+ return {"type": InteractionType.NON_ASSIGNMENT_BUTTON, "buttons": [button]}
229
+
230
+ def _parse_buttons(self, content: str) -> tuple[list[dict[str, str]], bool]:
231
+ """
232
+ Parse button group with fault tolerance.
233
+
234
+ Args:
235
+ content: Button content separated by | or ||
236
+
237
+ Returns:
238
+ Tuple of (button list, is_multi_select)
239
+ """
240
+ if not content or not isinstance(content, str):
241
+ return [], False
242
+
243
+ _, is_multi_select = self._detect_separator_type(content)
244
+
245
+ buttons = []
246
+ try:
247
+ # Use different splitting logic based on separator type
248
+ if is_multi_select:
249
+ # Multi-select mode: split on ||, preserve single |
250
+ button_parts = content.split("||")
251
+ else:
252
+ # Single-select mode: split on single |, but preserve ||
253
+ # Use pre-compiled regex from constants
254
+ button_parts = COMPILED_SINGLE_PIPE_SPLIT_REGEX.split(content)
255
+
256
+ for button_text in button_parts:
257
+ button_text = button_text.strip()
258
+ if button_text:
259
+ button = self._parse_single_button(button_text)
260
+ buttons.append(button)
261
+ except (TypeError, ValueError):
262
+ # Fallback to treating entire content as single button
263
+ return [{"display": content.strip(), "value": content.strip()}], False
264
+
265
+ # For empty content (like just separators), return empty list
266
+ if not buttons and (content.strip() == "||" or content.strip() == "|"):
267
+ return [], is_multi_select
268
+
269
+ # Ensure at least one button exists (but only if there's actual content)
270
+ if not buttons and content.strip():
271
+ buttons = [{"display": content.strip(), "value": content.strip()}]
272
+
273
+ return buttons, is_multi_select
274
+
275
+ def _parse_single_button(self, button_text: str) -> dict[str, str]:
276
+ """
277
+ Parse single button with fault tolerance, supports Button//value format.
278
+
279
+ Args:
280
+ button_text: Button text
281
+
282
+ Returns:
283
+ Dictionary with display and value keys
284
+ """
285
+ if not button_text or not isinstance(button_text, str):
286
+ return {"display": "", "value": ""}
287
+
288
+ button_text = button_text.strip()
289
+ if not button_text:
290
+ return {"display": "", "value": ""}
291
+
292
+ try:
293
+ # Detect Button//value format - split only on first //
294
+ if "//" in button_text:
295
+ parts = button_text.split("//", 1) # Split only on first //
296
+ display = parts[0].strip()
297
+ value = parts[1] if len(parts) > 1 else ""
298
+ # Don't strip value to preserve intentional spacing/formatting
299
+ return {"display": display, "value": value}
300
+ except (ValueError, IndexError):
301
+ # Fallback: use text as both display and value
302
+ pass
303
+
304
+ return {"display": button_text, "value": button_text}
305
+
306
+ def _detect_separator_type(self, content: str) -> tuple[str, bool]:
307
+ """
308
+ Detect separator type and whether it's multi-select.
309
+
310
+ Implements fault tolerance: first separator type encountered determines the behavior.
311
+ Mixed separators are handled by treating the rest as literal text.
312
+
313
+ Args:
314
+ content: Button content to analyze
315
+
316
+ Returns:
317
+ Tuple of (separator, is_multi_select) where separator is '|' or '||'
318
+ """
319
+ if not content or not isinstance(content, str):
320
+ return "|", False
321
+
322
+ # Find first occurrence of separators
323
+ single_pos = content.find("|")
324
+ double_pos = content.find("||")
325
+
326
+ # If no separators found
327
+ if single_pos == -1 and double_pos == -1:
328
+ return "|", False
329
+
330
+ # If only single separator found
331
+ if double_pos == -1:
332
+ return "|", False
333
+
334
+ # If only double separator found
335
+ if single_pos == -1:
336
+ return "||", True
337
+
338
+ # Both found - fault tolerance: first occurrence wins
339
+ # This handles mixed cases like "A||B|C" (multi-select) and "A|B||C" (single-select)
340
+ if double_pos <= single_pos:
341
+ return "||", True
342
+ return "|", False
343
+
344
+ def _create_error_result(self, error_message: str) -> dict[str, Any]:
345
+ """
346
+ Create error result.
347
+
348
+ Args:
349
+ error_message: Error message
350
+
351
+ Returns:
352
+ Error result dictionary
353
+ """
354
+ return {"type": None, "error": error_message} # type: ignore[unreachable]
@@ -0,0 +1,50 @@
1
+ """
2
+ JSON Parser Module
3
+
4
+ Provides robust JSON parsing with support for code blocks and mixed text formats.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ from typing import Any
10
+
11
+ from ..constants import JSON_PARSE_ERROR
12
+
13
+
14
+ def parse_json_response(response_text: str) -> dict[str, Any]:
15
+ """
16
+ Parse JSON response supporting multiple formats.
17
+
18
+ Supports pure JSON strings, ```json code blocks, and mixed text formats.
19
+
20
+ Args:
21
+ response_text: Response text to parse
22
+
23
+ Returns:
24
+ Parsed dictionary object
25
+
26
+ Raises:
27
+ ValueError: When JSON cannot be parsed
28
+ """
29
+ text = response_text.strip()
30
+
31
+ # Extract JSON code block
32
+ if "```json" in text:
33
+ start_idx = text.find("```json") + 7
34
+ end_idx = text.find("```", start_idx)
35
+ if end_idx != -1:
36
+ text = text[start_idx:end_idx].strip()
37
+ elif "```" in text:
38
+ start_idx = text.find("```") + 3
39
+ end_idx = text.find("```", start_idx)
40
+ if end_idx != -1:
41
+ text = text[start_idx:end_idx].strip()
42
+
43
+ try:
44
+ return json.loads(text)
45
+ except json.JSONDecodeError:
46
+ # Try to extract first JSON object
47
+ json_match = re.search(r"\{[^}]+\}", text)
48
+ if json_match:
49
+ return json.loads(json_match.group())
50
+ raise ValueError(JSON_PARSE_ERROR)