markdown-flow 0.2.79__tar.gz → 0.2.81__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.
Files changed (49) hide show
  1. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/PKG-INFO +1 -1
  2. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/__init__.py +1 -2
  3. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/core.py +45 -34
  4. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/system_prompt.md +3 -3
  5. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow.egg-info/PKG-INFO +1 -1
  6. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow.egg-info/SOURCES.txt +1 -0
  7. markdown_flow-0.2.81/tests/test_error_render_context.py +58 -0
  8. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/LICENSE +0 -0
  9. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/README.md +0 -0
  10. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/constants.py +0 -0
  11. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/constants_system_prompt.py +0 -0
  12. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/enums.py +0 -0
  13. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/exceptions.py +0 -0
  14. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/formatter/__init__.py +0 -0
  15. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/formatter/classifier.py +0 -0
  16. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/formatter/format.py +0 -0
  17. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/formatter/patterns.py +0 -0
  18. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/formatter/stream.py +0 -0
  19. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/formatter/types.py +0 -0
  20. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/llm.py +0 -0
  21. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/models.py +0 -0
  22. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/__init__.py +0 -0
  23. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/code_fence_utils.py +0 -0
  24. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/html_comment_utils.py +0 -0
  25. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/interaction.py +0 -0
  26. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/json_parser.py +0 -0
  27. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/output.py +0 -0
  28. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/preprocessor.py +0 -0
  29. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/validation.py +0 -0
  30. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/parser/variable.py +0 -0
  31. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/providers/__init__.py +0 -0
  32. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/providers/config.py +0 -0
  33. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/providers/openai.py +0 -0
  34. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/tag_filter.py +0 -0
  35. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow/utils.py +0 -0
  36. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow.egg-info/dependency_links.txt +0 -0
  37. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/markdown_flow.egg-info/top_level.txt +0 -0
  38. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/pyproject.toml +0 -0
  39. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/setup.cfg +0 -0
  40. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_dynamic_interaction.py +0 -0
  41. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_formatter.py +0 -0
  42. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_formatter_stream.py +0 -0
  43. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_html_comment_utils.py +0 -0
  44. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_markdownflow_basic.py +0 -0
  45. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_parser_interaction.py +0 -0
  46. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_parser_output.py +0 -0
  47. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_parser_variable.py +0 -0
  48. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_preprocessor.py +0 -0
  49. {markdown_flow-0.2.79 → markdown_flow-0.2.81}/tests/test_preserved_simple.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-flow
3
- Version: 0.2.79
3
+ Version: 0.2.81
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
@@ -88,5 +88,4 @@ __all__ = [
88
88
  "replace_variables_in_text",
89
89
  ]
90
90
 
91
- __version__ = "0.2.79"
92
- # __version__ = "0.2.45-alpha-1"
91
+ __version__ = "0.2.81"
@@ -666,43 +666,45 @@ class MarkdownFlow:
666
666
 
667
667
  # Extract translatable content (JSON format)
668
668
  translatable_json, interaction_info = self._extract_translatable_content(processed_block.content)
669
- if not interaction_info:
670
- # Parse failed, return original content
671
- return LLMResult(
669
+
670
+ def _render_as_is(*, translation_skipped: bool = False):
671
+ """Return the interaction content unchanged, honoring the mode.
672
+
673
+ STREAM mode must return a generator yielding LLMResult (callers
674
+ iterate the result); COMPLETE returns the LLMResult directly.
675
+ """
676
+ metadata = {
677
+ "block_type": "interaction",
678
+ "block_index": block_index,
679
+ }
680
+ if translation_skipped:
681
+ metadata["translation_skipped"] = True
682
+ result = LLMResult(
672
683
  content=processed_block.content,
673
684
  type=ElementType.INTERACTION,
674
685
  number=block_index,
675
- metadata={
676
- "block_type": "interaction",
677
- "block_index": block_index,
678
- },
686
+ metadata=metadata,
679
687
  )
688
+ if mode == ProcessMode.STREAM:
689
+
690
+ def stream_generator():
691
+ yield result
692
+
693
+ return stream_generator()
694
+ return result
695
+
696
+ if not interaction_info:
697
+ # Parse failed, return original content
698
+ return _render_as_is()
680
699
 
681
700
  # If no translatable content, return directly
682
701
  if not translatable_json or translatable_json == "{}":
683
- return LLMResult(
684
- content=processed_block.content,
685
- type=ElementType.INTERACTION,
686
- number=block_index,
687
- metadata={
688
- "block_type": "interaction",
689
- "block_index": block_index,
690
- },
691
- )
702
+ return _render_as_is()
692
703
 
693
704
  # If no output language is configured, skip translation entirely:
694
705
  # return the interaction content as-is without any LLM call.
695
706
  if not self._output_language:
696
- return LLMResult(
697
- content=processed_block.content,
698
- type=ElementType.INTERACTION,
699
- number=block_index,
700
- metadata={
701
- "block_type": "interaction",
702
- "block_index": block_index,
703
- "translation_skipped": True,
704
- },
705
- )
707
+ return _render_as_is(translation_skipped=True)
706
708
 
707
709
  # Build translation messages
708
710
  messages = self._build_translation_messages(translatable_json)
@@ -801,7 +803,7 @@ class MarkdownFlow:
801
803
  # Basic validation
802
804
  if not user_input or not any(values for values in user_input.values()):
803
805
  error_msg = INPUT_EMPTY_ERROR
804
- return self._render_error(error_msg, mode, context)
806
+ return self._render_error(error_msg, mode, context, variables)
805
807
 
806
808
  # Get the target variable value from user_input
807
809
  target_values = user_input.get(target_variable, [])
@@ -815,7 +817,7 @@ class MarkdownFlow:
815
817
 
816
818
  if "error" in parse_result:
817
819
  error_msg = INTERACTION_PARSE_ERROR.format(error=parse_result["error"])
818
- return self._render_error(error_msg, mode, context)
820
+ return self._render_error(error_msg, mode, context, variables)
819
821
 
820
822
  interaction_type = parse_result.get("type")
821
823
 
@@ -940,6 +942,7 @@ class MarkdownFlow:
940
942
  mode,
941
943
  interaction_type,
942
944
  context,
945
+ variables,
943
946
  )
944
947
 
945
948
  if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
@@ -993,7 +996,7 @@ class MarkdownFlow:
993
996
  context=context,
994
997
  )
995
998
  error_msg = f"No input provided for variable '{target_variable}'"
996
- return self._render_error(error_msg, mode, context)
999
+ return self._render_error(error_msg, mode, context, variables)
997
1000
 
998
1001
  def _match_button_values(
999
1002
  self,
@@ -1036,6 +1039,7 @@ class MarkdownFlow:
1036
1039
  mode: ProcessMode,
1037
1040
  interaction_type: InteractionType,
1038
1041
  context: list[dict[str, str]] | None = None,
1042
+ variables: dict[str, str | list[str]] | None = None,
1039
1043
  ) -> LLMResult | Generator[LLMResult, None, None]:
1040
1044
  """
1041
1045
  Simplified button validation with new input format.
@@ -1080,7 +1084,7 @@ class MarkdownFlow:
1080
1084
  # Pure button mode requires input
1081
1085
  button_displays = [btn["display"] for btn in buttons]
1082
1086
  error_msg = f"Please select from: {', '.join(button_displays)}"
1083
- return self._render_error(error_msg, mode, context)
1087
+ return self._render_error(error_msg, mode, context, variables)
1084
1088
 
1085
1089
  # Validate input values against available buttons
1086
1090
  valid_values = []
@@ -1105,7 +1109,7 @@ class MarkdownFlow:
1105
1109
  if invalid_values and not allow_text_input:
1106
1110
  button_displays = [btn["display"] for btn in buttons]
1107
1111
  error_msg = f"Invalid options: {', '.join(invalid_values)}. Please select from: {', '.join(button_displays)}"
1108
- return self._render_error(error_msg, mode, context)
1112
+ return self._render_error(error_msg, mode, context, variables)
1109
1113
 
1110
1114
  # Success: return validated values
1111
1115
  result = LLMResult(
@@ -1178,13 +1182,14 @@ class MarkdownFlow:
1178
1182
  error_message: str,
1179
1183
  mode: ProcessMode,
1180
1184
  context: list[dict[str, str]] | None = None,
1185
+ variables: dict[str, str | list[str]] | None = None,
1181
1186
  ) -> LLMResult | Generator[LLMResult, None, None]:
1182
1187
  """Render user-friendly error message."""
1183
1188
  # Truncate context to configured maximum length
1184
1189
  truncated_context = self._truncate_context(context)
1185
1190
 
1186
1191
  # Build error messages with context
1187
- messages = self._build_error_render_messages(error_message, truncated_context)
1192
+ messages = self._build_error_render_messages(error_message, truncated_context, variables)
1188
1193
 
1189
1194
  if mode == ProcessMode.COMPLETE:
1190
1195
  if not self._llm_provider:
@@ -1529,6 +1534,7 @@ class MarkdownFlow:
1529
1534
  self,
1530
1535
  error_message: str,
1531
1536
  context: list[dict[str, str]] | None = None,
1537
+ variables: dict[str, str | list[str]] | None = None,
1532
1538
  ) -> list[dict[str, str]]:
1533
1539
  """Build error rendering messages."""
1534
1540
  render_prompt = f"""{self._interaction_error_prompt}
@@ -1543,10 +1549,15 @@ Original Error: {error_message}
1543
1549
 
1544
1550
  messages.append({"role": "system", "content": render_prompt})
1545
1551
 
1546
- # Add conversation history context if provided
1552
+ # Add conversation history context if provided.
1553
+ # Transform interaction syntax in the context the same way the content
1554
+ # path does, so raw ?[...] blocks are expanded to {user}/{assistant}
1555
+ # pairs instead of leaking into this auxiliary LLM call.
1547
1556
  truncated_context = self._truncate_context(context)
1548
1557
  if truncated_context:
1549
- messages.extend(truncated_context)
1558
+ transformed_context = self._transform_context_messages(truncated_context, variables)
1559
+ if transformed_context:
1560
+ messages.extend(transformed_context)
1550
1561
 
1551
1562
  messages.append({"role": "user", "content": error_message})
1552
1563
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  # 二、html展示内容生成规则
11
11
 
12
- 仅当用户要求生成视觉内容(PPT/页面/HTML/图表)时启用。如果用户要求只生成内容,则不启用该规则。
12
+ 仅当用户要求生成视觉内容(PPT/页面/HTML/图表/图片)时启用。如果用户要求只生成内容,则不启用该规则。
13
13
 
14
14
  ## 1. 渲染机制
15
15
 
@@ -26,7 +26,7 @@
26
26
 
27
27
  每屏 = 一个铺满视口的固定容器,不可滚动。外层容器写法:
28
28
 
29
- ```
29
+ ```text
30
30
  <div style="width:100%; min-height:100vh; overflow-x:hidden; overflow-y:auto; display:flex; flex-direction:column; align-items:center; padding:1em; font-size:clamp(12px,calc(100vw/48),3vh)">
31
31
  <!-- 内容 -->
32
32
  </div>
@@ -34,7 +34,7 @@
34
34
 
35
35
  每屏 HTML 后必须紧跟:
36
36
 
37
- ```
37
+ ```text
38
38
  <style>
39
39
  *,*::before,*::after{box-sizing:border-box;overflow-wrap:break-word;word-wrap:break-word}
40
40
  </style>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-flow
3
- Version: 0.2.79
3
+ Version: 0.2.81
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
@@ -35,6 +35,7 @@ markdown_flow/providers/__init__.py
35
35
  markdown_flow/providers/config.py
36
36
  markdown_flow/providers/openai.py
37
37
  tests/test_dynamic_interaction.py
38
+ tests/test_error_render_context.py
38
39
  tests/test_formatter.py
39
40
  tests/test_formatter_stream.py
40
41
  tests/test_html_comment_utils.py
@@ -0,0 +1,58 @@
1
+ """
2
+ Unit tests for context transformation in the error-render path.
3
+
4
+ The error-render path is an auxiliary LLM call triggered when interaction
5
+ input validation fails. It must transform interaction syntax in the provided
6
+ context the same way the content path does, so raw ?[...] blocks are expanded
7
+ into {user}/{assistant} pairs instead of leaking into the prompt.
8
+ """
9
+
10
+ from markdown_flow import MarkdownFlow
11
+
12
+
13
+ def _build_messages(context, variables):
14
+ mf = MarkdownFlow("Doc", llm_provider=None)
15
+ truncated = mf._truncate_context(context)
16
+ return mf._build_error_render_messages("some error", truncated, variables)
17
+
18
+
19
+ def test_error_render_expands_interaction_with_variable():
20
+ """An interaction with a resolvable variable becomes user(value)+assistant(ok)."""
21
+ context = [
22
+ {"role": "user", "content": "What is your name?"},
23
+ {"role": "assistant", "content": "?[%{{nickname}} ...What is your nickname?]"},
24
+ ]
25
+ variables = {"nickname": "Alice"}
26
+
27
+ messages = _build_messages(context, variables)
28
+
29
+ # Raw interaction syntax must not leak into the prompt.
30
+ assert all("?[" not in m["content"] for m in messages)
31
+ # The interaction is expanded to a clean user/assistant pair.
32
+ assert {"role": "user", "content": "Alice"} in messages
33
+ expanded = [m for m in messages if m["role"] == "assistant" and m["content"] == "ok"]
34
+ assert expanded, "expected an assistant 'ok' acknowledgement"
35
+
36
+
37
+ def test_error_render_skips_interaction_without_value():
38
+ """A variable interaction with no resolvable value is dropped, not leaked."""
39
+ context = [
40
+ {"role": "assistant", "content": "?[%{{nickname}} ...What is your nickname?]"},
41
+ ]
42
+
43
+ messages = _build_messages(context, variables={})
44
+
45
+ assert all("?[" not in m["content"] for m in messages)
46
+ assert all(m["content"] != "Alice" for m in messages)
47
+
48
+
49
+ def test_error_render_no_variable_interaction_becomes_ok():
50
+ """A button-only interaction (no variable) becomes ok/ok."""
51
+ context = [
52
+ {"role": "assistant", "content": "?[Continue|Cancel]"},
53
+ ]
54
+
55
+ messages = _build_messages(context, variables=None)
56
+
57
+ assert all("?[" not in m["content"] for m in messages)
58
+ assert {"role": "user", "content": "ok"} in messages
File without changes
File without changes
File without changes