markdown-flow 0.2.81__tar.gz → 0.2.82__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.81 → markdown_flow-0.2.82}/PKG-INFO +1 -1
  2. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/__init__.py +1 -1
  3. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/constants.py +13 -0
  4. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/core.py +91 -6
  5. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow.egg-info/PKG-INFO +1 -1
  6. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow.egg-info/SOURCES.txt +0 -1
  7. markdown_flow-0.2.81/tests/test_error_render_context.py +0 -58
  8. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/LICENSE +0 -0
  9. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/README.md +0 -0
  10. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/constants_system_prompt.py +0 -0
  11. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/enums.py +0 -0
  12. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/exceptions.py +0 -0
  13. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/formatter/__init__.py +0 -0
  14. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/formatter/classifier.py +0 -0
  15. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/formatter/format.py +0 -0
  16. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/formatter/patterns.py +0 -0
  17. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/formatter/stream.py +0 -0
  18. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/formatter/types.py +0 -0
  19. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/llm.py +0 -0
  20. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/models.py +0 -0
  21. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/__init__.py +0 -0
  22. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/code_fence_utils.py +0 -0
  23. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/html_comment_utils.py +0 -0
  24. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/interaction.py +0 -0
  25. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/json_parser.py +0 -0
  26. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/output.py +0 -0
  27. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/preprocessor.py +0 -0
  28. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/validation.py +0 -0
  29. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/parser/variable.py +0 -0
  30. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/providers/__init__.py +0 -0
  31. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/providers/config.py +0 -0
  32. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/providers/openai.py +0 -0
  33. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/system_prompt.md +0 -0
  34. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/tag_filter.py +0 -0
  35. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow/utils.py +0 -0
  36. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow.egg-info/dependency_links.txt +0 -0
  37. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/markdown_flow.egg-info/top_level.txt +0 -0
  38. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/pyproject.toml +0 -0
  39. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/setup.cfg +0 -0
  40. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_dynamic_interaction.py +0 -0
  41. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_formatter.py +0 -0
  42. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_formatter_stream.py +0 -0
  43. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_html_comment_utils.py +0 -0
  44. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_markdownflow_basic.py +0 -0
  45. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_parser_interaction.py +0 -0
  46. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_parser_output.py +0 -0
  47. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_parser_variable.py +0 -0
  48. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/tests/test_preprocessor.py +0 -0
  49. {markdown_flow-0.2.81 → markdown_flow-0.2.82}/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.81
3
+ Version: 0.2.82
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,4 +88,4 @@ __all__ = [
88
88
  "replace_variables_in_text",
89
89
  ]
90
90
 
91
- __version__ = "0.2.81"
91
+ __version__ = "0.2.82"
@@ -237,3 +237,16 @@ CONTEXT_CONVERSATION_TEMPLATE = f"{CONTEXT_CONVERSATION_MARKER}\n{{content}}"
237
237
  CONTEXT_BUTTON_OPTIONS_TEMPLATE = (
238
238
  f"{CONTEXT_BUTTON_OPTIONS_MARKER}\n可选的预定义选项包括:{{button_options}}\n注意:用户如果选择了这些选项,都应该接受;如果输入了自定义内容,只要是对问题的合理回答即可接受。"
239
239
  )
240
+
241
+ # Next interaction context prompt templates
242
+ NEXT_INTERACTION_CONTEXT_INTRO = (
243
+ "The next interaction will appear immediately after this content. When generating the current content, connect to it naturally. "
244
+ "You may briefly restate, explain, or set up the available choices so the user understands what they will decide next, but do not output the interaction syntax or answer on the user's behalf."
245
+ )
246
+ NEXT_INTERACTION_TEXT_INPUT_TEMPLATE = "The next interaction asks the user to answer in text: {question}"
247
+ NEXT_INTERACTION_SINGLE_CHOICE_TEMPLATE = "The next interaction is a single-choice question. The user will choose one option from: {options}"
248
+ NEXT_INTERACTION_MULTIPLE_CHOICE_TEMPLATE = "The next interaction is a multiple-choice question. The user can choose one or more options from: {options}"
249
+ NEXT_INTERACTION_SINGLE_CHOICE_WITH_TEXT_INTRO = "The next interaction is a single-choice question with an optional custom text answer."
250
+ NEXT_INTERACTION_MULTIPLE_CHOICE_WITH_TEXT_INTRO = "The next interaction is a multiple-choice question with an optional custom text answer."
251
+ NEXT_INTERACTION_PREDEFINED_OPTIONS_TEMPLATE = "The predefined options are: {options}."
252
+ NEXT_INTERACTION_CUSTOM_TEXT_PROMPT_TEMPLATE = "The custom text prompt is: {question}"
@@ -25,6 +25,14 @@ from .constants import (
25
25
  INTERACTION_PROMPT_BASE,
26
26
  INTERACTION_PROMPT_WITH_TRANSLATION,
27
27
  LLM_PROVIDER_REQUIRED_ERROR,
28
+ NEXT_INTERACTION_CONTEXT_INTRO,
29
+ NEXT_INTERACTION_CUSTOM_TEXT_PROMPT_TEMPLATE,
30
+ NEXT_INTERACTION_MULTIPLE_CHOICE_TEMPLATE,
31
+ NEXT_INTERACTION_MULTIPLE_CHOICE_WITH_TEXT_INTRO,
32
+ NEXT_INTERACTION_PREDEFINED_OPTIONS_TEMPLATE,
33
+ NEXT_INTERACTION_SINGLE_CHOICE_TEMPLATE,
34
+ NEXT_INTERACTION_SINGLE_CHOICE_WITH_TEXT_INTRO,
35
+ NEXT_INTERACTION_TEXT_INPUT_TEMPLATE,
28
36
  OUTPUT_LANGUAGE_INSTRUCTION_BOTTOM,
29
37
  OUTPUT_LANGUAGE_INSTRUCTION_TOP,
30
38
  UNSUPPORTED_PROMPT_TYPE_ERROR,
@@ -1286,11 +1294,88 @@ class MarkdownFlow:
1286
1294
  if self._output_language:
1287
1295
  user_content = f"<output_language_instruction>\n🚨 OUTPUT: 100% {self._output_language} - Translate ALL non-{self._output_language} words/phrases to {self._output_language} 🚨\n</output_language_instruction>\n\n{user_content}"
1288
1296
 
1297
+ next_interaction_context = self._build_next_interaction_context_prompt(block_index, variables)
1298
+ if next_interaction_context:
1299
+ user_content = f"{user_content}\n\n{next_interaction_context}"
1300
+
1289
1301
  # Add processed content as user message (as instruction to LLM)
1290
1302
  messages.append({"role": "user", "content": user_content})
1291
1303
 
1292
1304
  return messages
1293
1305
 
1306
+ def _build_next_interaction_context_prompt(
1307
+ self,
1308
+ block_index: int,
1309
+ variables: dict[str, str | list[str]] | None,
1310
+ ) -> str:
1311
+ """Build natural prompt context from the immediately following interaction block."""
1312
+ blocks = self.get_all_blocks()
1313
+ next_index = block_index + 1
1314
+ if next_index >= len(blocks):
1315
+ return ""
1316
+
1317
+ next_block = blocks[next_index]
1318
+ if next_block.block_type != BlockType.INTERACTION:
1319
+ return ""
1320
+
1321
+ interaction_content = replace_variables_in_text(next_block.content, variables or {})
1322
+ parse_result = InteractionParser().parse(interaction_content)
1323
+ if parse_result.get("error"):
1324
+ return ""
1325
+
1326
+ question = parse_result.get("question", "").strip()
1327
+ buttons = parse_result.get("buttons") or []
1328
+ option_displays = [button.get("display", "").strip() for button in buttons if button.get("display", "").strip()]
1329
+
1330
+ detail = self._format_next_interaction_detail(parse_result.get("type"), question, option_displays)
1331
+ if not detail:
1332
+ return ""
1333
+
1334
+ return f"{NEXT_INTERACTION_CONTEXT_INTRO}\n{detail}"
1335
+
1336
+ def _format_next_interaction_detail(
1337
+ self,
1338
+ interaction_type: InteractionType | None,
1339
+ question: str,
1340
+ option_displays: list[str],
1341
+ ) -> str:
1342
+ """Format the type-specific part of the next interaction prompt."""
1343
+ options = json.dumps(option_displays, ensure_ascii=False) if option_displays else ""
1344
+ question_text = json.dumps(question, ensure_ascii=False) if question else ""
1345
+
1346
+ if interaction_type == InteractionType.TEXT_ONLY:
1347
+ if not question:
1348
+ return ""
1349
+ return NEXT_INTERACTION_TEXT_INPUT_TEMPLATE.format(question=question_text)
1350
+
1351
+ if interaction_type in [InteractionType.BUTTONS_ONLY, InteractionType.NON_ASSIGNMENT_BUTTON]:
1352
+ if not options:
1353
+ return ""
1354
+ return NEXT_INTERACTION_SINGLE_CHOICE_TEMPLATE.format(options=options)
1355
+
1356
+ if interaction_type == InteractionType.BUTTONS_MULTI_SELECT:
1357
+ if not options:
1358
+ return ""
1359
+ return NEXT_INTERACTION_MULTIPLE_CHOICE_TEMPLATE.format(options=options)
1360
+
1361
+ if interaction_type == InteractionType.BUTTONS_WITH_TEXT:
1362
+ detail_parts = [NEXT_INTERACTION_SINGLE_CHOICE_WITH_TEXT_INTRO]
1363
+ if options:
1364
+ detail_parts.append(NEXT_INTERACTION_PREDEFINED_OPTIONS_TEMPLATE.format(options=options))
1365
+ if question:
1366
+ detail_parts.append(NEXT_INTERACTION_CUSTOM_TEXT_PROMPT_TEMPLATE.format(question=question_text))
1367
+ return " ".join(detail_parts)
1368
+
1369
+ if interaction_type == InteractionType.BUTTONS_MULTI_WITH_TEXT:
1370
+ detail_parts = [NEXT_INTERACTION_MULTIPLE_CHOICE_WITH_TEXT_INTRO]
1371
+ if options:
1372
+ detail_parts.append(NEXT_INTERACTION_PREDEFINED_OPTIONS_TEMPLATE.format(options=options))
1373
+ if question:
1374
+ detail_parts.append(NEXT_INTERACTION_CUSTOM_TEXT_PROMPT_TEMPLATE.format(question=question_text))
1375
+ return " ".join(detail_parts)
1376
+
1377
+ return ""
1378
+
1294
1379
  def _extract_translatable_content(self, interaction_content: str) -> tuple[str, dict[str, Any] | None]:
1295
1380
  """Extract translatable parts from interaction content as JSON format
1296
1381
 
@@ -1550,12 +1635,12 @@ Original Error: {error_message}
1550
1635
  messages.append({"role": "system", "content": render_prompt})
1551
1636
 
1552
1637
  # 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.
1556
- truncated_context = self._truncate_context(context)
1557
- if truncated_context:
1558
- transformed_context = self._transform_context_messages(truncated_context, variables)
1638
+ # Context is already truncated by the caller (_render_error), so reuse it
1639
+ # directly here. Transform interaction syntax in the context the same way
1640
+ # the content path does, so raw ?[...] blocks are expanded to
1641
+ # {user}/{assistant} pairs instead of leaking into this auxiliary LLM call.
1642
+ if context:
1643
+ transformed_context = self._transform_context_messages(context, variables)
1559
1644
  if transformed_context:
1560
1645
  messages.extend(transformed_context)
1561
1646
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markdown-flow
3
- Version: 0.2.81
3
+ Version: 0.2.82
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,7 +35,6 @@ 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
39
38
  tests/test_formatter.py
40
39
  tests/test_formatter_stream.py
41
40
  tests/test_html_comment_utils.py
@@ -1,58 +0,0 @@
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