markdown-flow 0.2.1__tar.gz → 1.0.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-flow
3
- Version: 0.2.1
3
+ Version: 1.0.0
4
4
  Summary: An agent library designed to parse and process MarkdownFlow documents
5
5
  Project-URL: Homepage, https://github.com/ai-shifu/markdown-flow-agent-py
6
6
  Project-URL: Bug Tracker, https://github.com/ai-shifu/markdown-flow-agent-py/issues
@@ -241,7 +241,7 @@ Enumeration of different block types in MarkdownFlow documents.
241
241
  class BlockType(Enum):
242
242
  CONTENT = "content" # Regular markdown content
243
243
  INTERACTION = "interaction" # User interaction blocks (?[...])
244
- PRESERVED_CONTENT = "preserved_content" # Content wrapped in === markers
244
+ PRESERVED_CONTENT = "preserved_content" # Content wrapped in !=== markers (inline or multiline)
245
245
  ```
246
246
 
247
247
  **Block Structure:**
@@ -259,10 +259,11 @@ Hello {{name}}! Welcome to our platform.
259
259
 
260
260
  # Preserved content - output as-is
261
261
  """
262
- ===
262
+ # Multiline fence with leading '!'
263
+ !===
263
264
  This content is preserved exactly as written.
264
265
  No LLM processing or variable replacement.
265
- ===
266
+ !===
266
267
  """
267
268
  ```
268
269
 
@@ -223,7 +223,7 @@ Enumeration of different block types in MarkdownFlow documents.
223
223
  class BlockType(Enum):
224
224
  CONTENT = "content" # Regular markdown content
225
225
  INTERACTION = "interaction" # User interaction blocks (?[...])
226
- PRESERVED_CONTENT = "preserved_content" # Content wrapped in === markers
226
+ PRESERVED_CONTENT = "preserved_content" # Content wrapped in !=== markers (inline or multiline)
227
227
  ```
228
228
 
229
229
  **Block Structure:**
@@ -241,10 +241,11 @@ Hello {{name}}! Welcome to our platform.
241
241
 
242
242
  # Preserved content - output as-is
243
243
  """
244
- ===
244
+ # Multiline fence with leading '!'
245
+ !===
245
246
  This content is preserved exactly as written.
246
247
  No LLM processing or variable replacement.
247
- ===
248
+ !===
248
249
  """
249
250
  ```
250
251
 
@@ -21,23 +21,25 @@ INTERACTION_PATTERN_SPLIT = r"((?<!\\)\?\[[^\]]*\](?!\())" # Pattern for re.spl
21
21
  COMPILED_INTERACTION_REGEX = re.compile(INTERACTION_PATTERN) # Main interaction pattern matcher
22
22
  COMPILED_LAYER1_INTERACTION_REGEX = COMPILED_INTERACTION_REGEX # Layer 1: Basic format validation (alias)
23
23
  COMPILED_LAYER2_VARIABLE_REGEX = re.compile(r"^%\{\{([^}]+)\}\}(.*)$") # Layer 2: Variable detection
24
- COMPILED_LAYER3_ELLIPSIS_REGEX = re.compile(r"^(.*?)\.\.\.(.*)") # Layer 3: Split content around ellipsis
25
- COMPILED_LAYER3_BUTTON_VALUE_REGEX = re.compile(r"^(.+?)//(.+)$") # Layer 3: Parse Button//value format
24
+ COMPILED_LAYER3_ELLIPSIS_REGEX = re.compile(r"^(.*)\.\.\.(.*)") # Layer 3: Split content around ellipsis
25
+ COMPILED_LAYER3_BUTTON_VALUE_REGEX = re.compile(r"^(.+)//(.+)$") # Layer 3: Parse Button//value format
26
26
  COMPILED_BRACE_VARIABLE_REGEX = re.compile(
27
27
  r"(?<!%)\{\{([^}]+)\}\}" # Match {{variable}} format for replaceable variables
28
28
  )
29
29
  COMPILED_INTERACTION_CONTENT_RECONSTRUCT_REGEX = re.compile(
30
- r"(\?\[.*?\.\.\.).*?(\])" # Reconstruct interaction content: prefix + question + suffix
30
+ r"(\?\[[^]]*\.\.\.)([^]]*\])" # Reconstruct interaction content: prefix + question + suffix
31
31
  )
32
32
  COMPILED_BRACKETS_CLEANUP_REGEX = re.compile(r"[\[\]()]")
33
- COMPILED_VARIABLE_REFERENCE_CLEANUP_REGEX = re.compile(r"%\{\{.*?\}\}")
33
+ COMPILED_VARIABLE_REFERENCE_CLEANUP_REGEX = re.compile(r"%\{\{[^}]*\}\}")
34
34
  COMPILED_WHITESPACE_CLEANUP_REGEX = re.compile(r"\s+")
35
35
 
36
36
  # Document parsing constants (using shared INTERACTION_PATTERN defined above)
37
37
 
38
38
  # Separators
39
39
  BLOCK_SEPARATOR = r"\n\s*---\s*\n"
40
- TRIPLE_EQUALS_DELIMITER = "==="
40
+ # Multiline preserved block fence: starts with '!' followed by 3 or more '='
41
+ PRESERVE_FENCE_PATTERN = r"^!={3,}\s*$"
42
+ COMPILED_PRESERVE_FENCE_REGEX = re.compile(PRESERVE_FENCE_PATTERN)
41
43
 
42
44
  # Output instruction markers
43
45
  OUTPUT_INSTRUCTION_PREFIX = "[输出]"
@@ -257,7 +257,7 @@ class MarkdownFlow:
257
257
  """Process preserved content block, output as-is without LLM call."""
258
258
  block = self.get_block(block_index)
259
259
 
260
- # Extract preserved content (remove === markers)
260
+ # Extract preserved content (remove !=== markers)
261
261
  content = extract_preserved_content(block.content)
262
262
 
263
263
  # Replace variables
@@ -27,4 +27,4 @@ class BlockType(Enum):
27
27
 
28
28
  CONTENT = "content" # Regular document content blocks
29
29
  INTERACTION = "interaction" # Interactive blocks requiring user input
30
- PRESERVED_CONTENT = "preserved_content" # Special content blocks marked with === delimiters
30
+ PRESERVED_CONTENT = "preserved_content" # Special blocks: inline !===content!=== or multiline !===...!===
@@ -17,6 +17,7 @@ from .constants import (
17
17
  COMPILED_LAYER3_BUTTON_VALUE_REGEX,
18
18
  COMPILED_LAYER3_ELLIPSIS_REGEX,
19
19
  COMPILED_PERCENT_VARIABLE_REGEX,
20
+ COMPILED_PRESERVE_FENCE_REGEX,
20
21
  CONTEXT_CONVERSATION_TEMPLATE,
21
22
  CONTEXT_QUESTION_MARKER,
22
23
  CONTEXT_QUESTION_TEMPLATE,
@@ -25,7 +26,6 @@ from .constants import (
25
26
  OUTPUT_INSTRUCTION_PREFIX,
26
27
  OUTPUT_INSTRUCTION_SUFFIX,
27
28
  SMART_VALIDATION_TEMPLATE,
28
- TRIPLE_EQUALS_DELIMITER,
29
29
  VALIDATION_ILLEGAL_DEFAULT_REASON,
30
30
  VALIDATION_RESPONSE_ILLEGAL,
31
31
  VALIDATION_RESPONSE_OK,
@@ -66,14 +66,14 @@ def is_preserved_content_block(content: str) -> bool:
66
66
  """
67
67
  Check if content is completely preserved content block.
68
68
 
69
- Preserved blocks are entirely wrapped by === markers with no external content.
70
- Supports inline (===content===) and multiline formats.
69
+ Preserved blocks are entirely wrapped by markers with no external content.
70
+ Supports inline (!===content!===) and multiline (!=== ... !===) formats.
71
71
 
72
72
  Args:
73
73
  content: Content to check
74
74
 
75
75
  Returns:
76
- True if content is fully wrapped by === markers
76
+ True if content is fully wrapped by preserved markers
77
77
  """
78
78
  content = content.strip()
79
79
  if not content:
@@ -81,7 +81,7 @@ def is_preserved_content_block(content: str) -> bool:
81
81
 
82
82
  lines = content.split("\n")
83
83
 
84
- # Check if all non-empty lines are inline format
84
+ # Check if all non-empty lines are inline format (!===content!===)
85
85
  all_inline_format = True
86
86
  has_any_content = False
87
87
 
@@ -89,22 +89,22 @@ def is_preserved_content_block(content: str) -> bool:
89
89
  stripped_line = line.strip()
90
90
  if stripped_line: # Non-empty line
91
91
  has_any_content = True
92
- # Check if inline format
93
- import re
94
-
95
- match = re.match(r"^===(.+)===$", stripped_line)
96
- if not match:
97
- all_inline_format = False # type: ignore[unreachable]
98
- break
99
- # Ensure inner content exists and contains no equals signs
100
- inner_content = match.group(1).strip()
101
- if not inner_content or "=" in inner_content:
92
+ # Check if inline format: !===content!===
93
+ match = re.match(r"^!===(.+)!=== *$", stripped_line)
94
+ if match:
95
+ # Ensure inner content exists and contains no !===
96
+ inner_content = match.group(1).strip()
97
+ if not inner_content or "!==" in inner_content:
98
+ all_inline_format = False
99
+ break
100
+ else:
102
101
  all_inline_format = False # type: ignore[unreachable]
103
102
  break
104
103
 
105
104
  # If all lines are inline format, return directly
106
105
  if has_any_content and all_inline_format:
107
106
  return True
107
+
108
108
  # Check multiline format using state machine
109
109
  state = "OUTSIDE" # States: OUTSIDE, INSIDE
110
110
  has_content_outside = False # Has external content
@@ -113,7 +113,7 @@ def is_preserved_content_block(content: str) -> bool:
113
113
  for line in lines:
114
114
  stripped_line = line.strip()
115
115
 
116
- if stripped_line == TRIPLE_EQUALS_DELIMITER:
116
+ if COMPILED_PRESERVE_FENCE_REGEX.match(stripped_line):
117
117
  if state == "OUTSIDE":
118
118
  # Enter preserve block
119
119
  state = "INSIDE"
@@ -121,13 +121,14 @@ def is_preserved_content_block(content: str) -> bool:
121
121
  elif state == "INSIDE":
122
122
  # Exit preserve block
123
123
  state = "OUTSIDE"
124
- # === lines don't count as external content
124
+ # !=== lines don't count as external content
125
125
  else:
126
- # Non-=== lines
127
- if stripped_line: # Non-empty line
126
+ # Non-!=== lines
127
+ if stripped_line: # type: ignore[unreachable] # Non-empty line
128
128
  if state == "OUTSIDE":
129
129
  # External content found
130
130
  has_content_outside = True
131
+ break
131
132
  # Internal content doesn't affect judgment
132
133
 
133
134
  # Judgment conditions:
@@ -204,6 +205,7 @@ class InteractionParser:
204
205
 
205
206
  # Layer 3: Specific content parsing
206
207
  if has_variable:
208
+ assert variable_name is not None, "variable_name should not be None when has_variable is True"
207
209
  return self._layer3_parse_variable_interaction(variable_name, remaining_content)
208
210
  return self._layer3_parse_display_buttons(inner_content)
209
211
 
@@ -485,15 +487,15 @@ def parse_json_response(response_text: str) -> dict[str, Any]:
485
487
 
486
488
  def process_output_instructions(content: str) -> str:
487
489
  """
488
- Process output instruction markers, converting === format to [output] format.
490
+ Process output instruction markers, converting !=== format to [output] format.
489
491
 
490
- Uses unified state machine to handle inline (===content===) and multiline formats.
492
+ Uses unified state machine to handle inline (!===content!===) and multiline (!===...!===) formats.
491
493
 
492
494
  Args:
493
495
  content: Raw content containing output instructions
494
496
 
495
497
  Returns:
496
- Processed content with === markers converted to [output] format
498
+ Processed content with !=== markers converted to [output] format
497
499
  """
498
500
  lines = content.split("\n")
499
501
  result_lines = []
@@ -503,72 +505,67 @@ def process_output_instructions(content: str) -> str:
503
505
  while i < len(lines):
504
506
  line = lines[i]
505
507
 
506
- # Check if contains === markers
507
- if "===" in line:
508
- # Check inline format: ===content===
509
- inline_match = re.search(r"===\s*([^=]+?)\s*===", line)
510
- if inline_match and line.count("===") >= 2:
511
- # Process inline format
512
- full_match = inline_match.group(0)
513
- inner_content = inline_match.group(1).strip()
514
-
515
- # Build output instruction - keep inline format on same line
516
- output_instruction = f"{OUTPUT_INSTRUCTION_PREFIX}{inner_content}{OUTPUT_INSTRUCTION_SUFFIX}"
517
-
518
- # Replace === part in original line
519
- processed_line = line.replace(full_match, output_instruction)
520
- result_lines.append(processed_line)
521
- has_output_instruction = True
522
- i += 1
508
+ # Check if contains preserved markers (inline !===...!=== or multiline !===...)
509
+ # Check inline format first: !===content!===
510
+ inline_match = re.search(r"!===\s*([^!]+)\s*!===", line)
511
+ if inline_match and line.count("!===") >= 2:
512
+ # Process inline format
513
+ full_match = inline_match.group(0)
514
+ inner_content = inline_match.group(1).strip()
515
+
516
+ # Build output instruction - keep inline format on same line
517
+ output_instruction = f"{OUTPUT_INSTRUCTION_PREFIX}{inner_content}{OUTPUT_INSTRUCTION_SUFFIX}"
518
+
519
+ # Replace !===...!=== part in original line
520
+ processed_line = line.replace(full_match, output_instruction)
521
+ result_lines.append(processed_line)
522
+ has_output_instruction = True
523
+ i += 1
523
524
 
524
- elif line.strip() == TRIPLE_EQUALS_DELIMITER:
525
- # Multiline format start
526
- i += 1
527
- output_content_lines: list[str] = []
528
-
529
- # Collect multiline content
530
- while i < len(lines):
531
- current_line = lines[i]
532
- if current_line.strip() == TRIPLE_EQUALS_DELIMITER:
533
- # Found end marker, process collected content
534
- output_content = "\n".join(output_content_lines).strip()
535
-
536
- # Special handling for title format (maintain original logic)
537
- hash_prefix = ""
538
- if output_content.startswith("#"):
539
- first_space = output_content.find(" ")
540
- first_newline = output_content.find("\n")
541
-
542
- if first_space != -1 and (first_newline == -1 or first_space < first_newline):
543
- hash_prefix = output_content[: first_space + 1]
544
- output_content = output_content[first_space + 1 :].strip()
545
- elif first_newline != -1:
546
- hash_prefix = output_content[: first_newline + 1]
547
- output_content = output_content[first_newline + 1 :].strip()
548
-
549
- # Build output instruction
550
- if hash_prefix:
551
- result_lines.append(f"{OUTPUT_INSTRUCTION_PREFIX}{hash_prefix}{output_content}{OUTPUT_INSTRUCTION_SUFFIX}")
552
- else:
553
- result_lines.append(f"{OUTPUT_INSTRUCTION_PREFIX}{output_content}{OUTPUT_INSTRUCTION_SUFFIX}")
554
-
555
- has_output_instruction = True
556
- i += 1
557
- break
558
- # Continue collecting content
559
- output_content_lines.append(current_line)
525
+ elif COMPILED_PRESERVE_FENCE_REGEX.match(line.strip()):
526
+ # Multiline format start
527
+ i += 1
528
+ output_content_lines: list[str] = []
529
+
530
+ # Collect multiline content
531
+ while i < len(lines):
532
+ current_line = lines[i]
533
+ if COMPILED_PRESERVE_FENCE_REGEX.match(current_line.strip()):
534
+ # Found end marker, process collected content
535
+ output_content = "\n".join(output_content_lines).strip()
536
+
537
+ # Special handling for title format (maintain original logic)
538
+ hash_prefix = ""
539
+ if output_content.startswith("#"):
540
+ first_space = output_content.find(" ")
541
+ first_newline = output_content.find("\n")
542
+
543
+ if first_space != -1 and (first_newline == -1 or first_space < first_newline):
544
+ hash_prefix = output_content[: first_space + 1]
545
+ output_content = output_content[first_space + 1 :].strip()
546
+ elif first_newline != -1:
547
+ hash_prefix = output_content[: first_newline + 1]
548
+ output_content = output_content[first_newline + 1 :].strip()
549
+
550
+ # Build output instruction
551
+ if hash_prefix:
552
+ result_lines.append(f"{OUTPUT_INSTRUCTION_PREFIX}{hash_prefix}{output_content}{OUTPUT_INSTRUCTION_SUFFIX}")
553
+ else:
554
+ result_lines.append(f"{OUTPUT_INSTRUCTION_PREFIX}{output_content}{OUTPUT_INSTRUCTION_SUFFIX}")
555
+
556
+ has_output_instruction = True
560
557
  i += 1
561
- else:
562
- # No end marker found, rollback processing
563
- result_lines.append(lines[i - len(output_content_lines) - 1])
564
- result_lines.extend(output_content_lines)
565
- else:
566
- # Contains === but not valid format, treat as normal line
567
- result_lines.append(line)
558
+ break
559
+ # Continue collecting content
560
+ output_content_lines.append(current_line) # type: ignore[unreachable]
568
561
  i += 1
562
+ else:
563
+ # No end marker found, rollback processing
564
+ result_lines.append(lines[i - len(output_content_lines) - 1])
565
+ result_lines.extend(output_content_lines)
569
566
  else:
570
567
  # Normal line
571
- result_lines.append(line)
568
+ result_lines.append(line) # type: ignore[unreachable]
572
569
  i += 1
573
570
 
574
571
  # Assemble final content
@@ -583,15 +580,15 @@ def process_output_instructions(content: str) -> str:
583
580
 
584
581
  def extract_preserved_content(content: str) -> str:
585
582
  """
586
- Extract actual content from preserved content blocks, removing === markers.
583
+ Extract actual content from preserved content blocks, removing markers.
587
584
 
588
- Handles inline (===content===) and multiline formats.
585
+ Handles inline (!===content!===) and multiline (!===...!===) formats.
589
586
 
590
587
  Args:
591
- content: Preserved content containing === markers
588
+ content: Preserved content containing preserved markers
592
589
 
593
590
  Returns:
594
- Actual content with === markers removed
591
+ Actual content with !=== markers removed
595
592
  """
596
593
  content = content.strip()
597
594
  if not content:
@@ -603,14 +600,14 @@ def extract_preserved_content(content: str) -> str:
603
600
  for line in lines:
604
601
  stripped_line = line.strip()
605
602
 
606
- # Check inline format
607
- match = re.match(r"^===(.+)===$", stripped_line)
608
- if match:
603
+ # Check inline format: !===content!===
604
+ inline_match = re.match(r"^!===(.+)!=== *$", stripped_line)
605
+ if inline_match:
609
606
  # Inline format, extract middle content
610
- inner_content = match.group(1).strip()
611
- if inner_content and "=" not in inner_content:
607
+ inner_content = inline_match.group(1).strip()
608
+ if inner_content and "!==" not in inner_content:
612
609
  result_lines.append(inner_content)
613
- elif stripped_line == TRIPLE_EQUALS_DELIMITER: # type: ignore[unreachable]
610
+ elif COMPILED_PRESERVE_FENCE_REGEX.match(stripped_line): # type: ignore[unreachable]
614
611
  # Multiline format delimiter, skip
615
612
  continue
616
613
  else:
@@ -694,7 +691,7 @@ def replace_variables_in_text(text: str, variables: dict[str, str]) -> str:
694
691
  variables = {}
695
692
 
696
693
  # Find all {{variable}} format variable references
697
- variable_pattern = r"\{\{([^{}]+?)\}\}"
694
+ variable_pattern = r"\{\{([^{}]+)\}\}"
698
695
  matches = re.findall(variable_pattern, text)
699
696
 
700
697
  # Assign "UNKNOWN" to undefined variables
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-flow
3
- Version: 0.2.1
3
+ Version: 1.0.0
4
4
  Summary: An agent library designed to parse and process MarkdownFlow documents
5
5
  Project-URL: Homepage, https://github.com/ai-shifu/markdown-flow-agent-py
6
6
  Project-URL: Bug Tracker, https://github.com/ai-shifu/markdown-flow-agent-py/issues
@@ -241,7 +241,7 @@ Enumeration of different block types in MarkdownFlow documents.
241
241
  class BlockType(Enum):
242
242
  CONTENT = "content" # Regular markdown content
243
243
  INTERACTION = "interaction" # User interaction blocks (?[...])
244
- PRESERVED_CONTENT = "preserved_content" # Content wrapped in === markers
244
+ PRESERVED_CONTENT = "preserved_content" # Content wrapped in !=== markers (inline or multiline)
245
245
  ```
246
246
 
247
247
  **Block Structure:**
@@ -259,10 +259,11 @@ Hello {{name}}! Welcome to our platform.
259
259
 
260
260
  # Preserved content - output as-is
261
261
  """
262
- ===
262
+ # Multiline fence with leading '!'
263
+ !===
263
264
  This content is preserved exactly as written.
264
265
  No LLM processing or variable replacement.
265
- ===
266
+ !===
266
267
  """
267
268
  ```
268
269
 
@@ -129,7 +129,7 @@ exclude = [
129
129
  "dist/",
130
130
  ".venv/",
131
131
  "venv/",
132
- "*.egg-info/",
132
+ ".*\\.egg-info/",
133
133
  ]
134
134
 
135
135
  # Per-module options
File without changes
File without changes