markdown-flow 0.2.85__tar.gz → 0.3.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.
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/PKG-INFO +32 -2
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/README.md +31 -1
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/__init__.py +13 -2
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/constants.py +9 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/constants_system_prompt.py +1 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/core.py +80 -18
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/formatter/format.py +12 -1
- markdown_flow-0.3.0/markdown_flow/higher_priority_prompt.md +5 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/interaction.py +41 -6
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow.egg-info/PKG-INFO +32 -2
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow.egg-info/SOURCES.txt +1 -1
- markdown_flow-0.2.85/markdown_flow/utils.py +0 -795
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/LICENSE +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/enums.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/exceptions.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/formatter/__init__.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/formatter/classifier.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/formatter/patterns.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/formatter/sanitize.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/formatter/stream.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/formatter/types.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/llm.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/models.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/__init__.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/code_fence_utils.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/html_comment_utils.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/json_parser.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/output.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/preprocessor.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/validation.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/parser/variable.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/providers/__init__.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/providers/config.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/providers/openai.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/system_prompt.md +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow/tag_filter.py +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow.egg-info/dependency_links.txt +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/markdown_flow.egg-info/top_level.txt +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/pyproject.toml +0 -0
- {markdown_flow-0.2.85 → markdown_flow-0.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -313,10 +313,40 @@ class InteractionType(NamedTuple):
|
|
|
313
313
|
# BUTTONS_MULTI_WITH_TEXT: Multi-select with text fallback
|
|
314
314
|
"?[%{{frameworks}} React||Vue||Angular||Please specify others...]"
|
|
315
315
|
|
|
316
|
-
# NON_ASSIGNMENT_BUTTON:
|
|
316
|
+
# NON_ASSIGNMENT_BUTTON: No-variable interaction. Supports the same shapes as
|
|
317
|
+
# variable interactions; the answer is not assigned to a variable but fed back
|
|
318
|
+
# into the conversation context (see "Non-assignment interactions" below).
|
|
317
319
|
"?[Continue | Cancel | Go Back]"
|
|
320
|
+
"?[JavaScript||TypeScript||Python]"
|
|
321
|
+
"?[...Anything to add?]"
|
|
322
|
+
"?[Option A | Option B | ...Other, please specify]"
|
|
318
323
|
```
|
|
319
324
|
|
|
325
|
+
### Non-assignment interactions
|
|
326
|
+
|
|
327
|
+
Interactions without a `%{{variable}}` collect an answer that is not stored in
|
|
328
|
+
any variable — it flows back into the conversation context instead:
|
|
329
|
+
|
|
330
|
+
1. Clients submit the answer under the canonical key `input`
|
|
331
|
+
(`DEFAULT_NON_ASSIGNMENT_INPUT_KEY`); the library tolerantly merges values
|
|
332
|
+
from any other key.
|
|
333
|
+
2. `process()` normalizes values matching a button (by display or value) to the
|
|
334
|
+
button value and accepts anything else as free text. The normalized answer
|
|
335
|
+
is returned in `result.metadata["answer"]` (`list[str]`).
|
|
336
|
+
3. Callers persist that answer and attach it to the interaction's context
|
|
337
|
+
message via the `user_answer` extension field (`USER_ANSWER_CONTEXT_KEY`):
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
context = [
|
|
341
|
+
{"role": "assistant", "content": "?[Option A | Option B]", "user_answer": "Option A"},
|
|
342
|
+
]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
When building LLM messages the library expands this into
|
|
346
|
+
`{user: "Option A"}` + `{assistant: "ok"}`. An empty `user_answer` skips the
|
|
347
|
+
turn entirely; omitting the field keeps the legacy `{user: "ok"}` behavior.
|
|
348
|
+
Extension fields are stripped before messages reach the LLM.
|
|
349
|
+
|
|
320
350
|
### Utility Functions
|
|
321
351
|
|
|
322
352
|
#### Variable Operations
|
|
@@ -295,10 +295,40 @@ class InteractionType(NamedTuple):
|
|
|
295
295
|
# BUTTONS_MULTI_WITH_TEXT: Multi-select with text fallback
|
|
296
296
|
"?[%{{frameworks}} React||Vue||Angular||Please specify others...]"
|
|
297
297
|
|
|
298
|
-
# NON_ASSIGNMENT_BUTTON:
|
|
298
|
+
# NON_ASSIGNMENT_BUTTON: No-variable interaction. Supports the same shapes as
|
|
299
|
+
# variable interactions; the answer is not assigned to a variable but fed back
|
|
300
|
+
# into the conversation context (see "Non-assignment interactions" below).
|
|
299
301
|
"?[Continue | Cancel | Go Back]"
|
|
302
|
+
"?[JavaScript||TypeScript||Python]"
|
|
303
|
+
"?[...Anything to add?]"
|
|
304
|
+
"?[Option A | Option B | ...Other, please specify]"
|
|
300
305
|
```
|
|
301
306
|
|
|
307
|
+
### Non-assignment interactions
|
|
308
|
+
|
|
309
|
+
Interactions without a `%{{variable}}` collect an answer that is not stored in
|
|
310
|
+
any variable — it flows back into the conversation context instead:
|
|
311
|
+
|
|
312
|
+
1. Clients submit the answer under the canonical key `input`
|
|
313
|
+
(`DEFAULT_NON_ASSIGNMENT_INPUT_KEY`); the library tolerantly merges values
|
|
314
|
+
from any other key.
|
|
315
|
+
2. `process()` normalizes values matching a button (by display or value) to the
|
|
316
|
+
button value and accepts anything else as free text. The normalized answer
|
|
317
|
+
is returned in `result.metadata["answer"]` (`list[str]`).
|
|
318
|
+
3. Callers persist that answer and attach it to the interaction's context
|
|
319
|
+
message via the `user_answer` extension field (`USER_ANSWER_CONTEXT_KEY`):
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
context = [
|
|
323
|
+
{"role": "assistant", "content": "?[Option A | Option B]", "user_answer": "Option A"},
|
|
324
|
+
]
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
When building LLM messages the library expands this into
|
|
328
|
+
`{user: "Option A"}` + `{assistant: "ok"}`. An empty `user_answer` skips the
|
|
329
|
+
turn entirely; omitting the field keeps the legacy `{user: "ok"}` behavior.
|
|
330
|
+
Extension fields are stripped before messages reach the LLM.
|
|
331
|
+
|
|
302
332
|
### Utility Functions
|
|
303
333
|
|
|
304
334
|
#### Variable Operations
|
|
@@ -17,7 +17,11 @@ Supported Interaction Types:
|
|
|
17
17
|
- BUTTONS_WITH_TEXT: ?[%{{var}} A|B|...question] - Buttons + text input
|
|
18
18
|
- BUTTONS_MULTI_SELECT: ?[%{{var}} A||B||C] - Multi-select buttons
|
|
19
19
|
- BUTTONS_MULTI_WITH_TEXT: ?[%{{var}} A||B||...question] - Multi-select + text input
|
|
20
|
-
- NON_ASSIGNMENT_BUTTON: ?[Continue|Cancel] -
|
|
20
|
+
- NON_ASSIGNMENT_BUTTON: ?[Continue|Cancel] - No-variable interaction.
|
|
21
|
+
Supports the same shapes as variable interactions (?[A|B], ?[A||B],
|
|
22
|
+
?[...question], ?[A|B|...question]); the answer is not assigned to a
|
|
23
|
+
variable but fed back into the conversation context via the
|
|
24
|
+
"user_answer" context message field.
|
|
21
25
|
|
|
22
26
|
Basic Usage:
|
|
23
27
|
from markdown_flow import MarkdownFlow, ProcessMode
|
|
@@ -49,6 +53,10 @@ Import Guide:
|
|
|
49
53
|
"""
|
|
50
54
|
|
|
51
55
|
# Import core classes and enums
|
|
56
|
+
from .constants import (
|
|
57
|
+
DEFAULT_NON_ASSIGNMENT_INPUT_KEY,
|
|
58
|
+
USER_ANSWER_CONTEXT_KEY,
|
|
59
|
+
)
|
|
52
60
|
from .core import MarkdownFlow
|
|
53
61
|
from .enums import BlockType, InputType
|
|
54
62
|
from .formatter import (
|
|
@@ -88,6 +96,9 @@ __all__ = [
|
|
|
88
96
|
"StreamFormatter",
|
|
89
97
|
"format_content",
|
|
90
98
|
"strip_untagged_code_fences",
|
|
99
|
+
# Non-assignment interaction contract
|
|
100
|
+
"DEFAULT_NON_ASSIGNMENT_INPUT_KEY",
|
|
101
|
+
"USER_ANSWER_CONTEXT_KEY",
|
|
91
102
|
# Main utility functions
|
|
92
103
|
"generate_smart_validation_template",
|
|
93
104
|
"extract_interaction_question",
|
|
@@ -95,4 +106,4 @@ __all__ = [
|
|
|
95
106
|
"replace_variables_in_text",
|
|
96
107
|
]
|
|
97
108
|
|
|
98
|
-
__version__ = "0.
|
|
109
|
+
__version__ = "0.3.0"
|
|
@@ -28,6 +28,15 @@ COMPILED_BRACE_VARIABLE_REGEX = re.compile(
|
|
|
28
28
|
)
|
|
29
29
|
COMPILED_SINGLE_PIPE_SPLIT_REGEX = re.compile(r"(?<!\|)\|(?!\|)") # Split on single | but not ||
|
|
30
30
|
|
|
31
|
+
# Canonical user_input key for non-assignment interactions (?[A|B] without a
|
|
32
|
+
# variable). Clients should submit {DEFAULT_NON_ASSIGNMENT_INPUT_KEY: values};
|
|
33
|
+
# the library also tolerantly merges values from any other key.
|
|
34
|
+
DEFAULT_NON_ASSIGNMENT_INPUT_KEY = "input"
|
|
35
|
+
|
|
36
|
+
# Context message extension field carrying the user's answer to a
|
|
37
|
+
# non-assignment interaction. Stripped before messages reach the LLM.
|
|
38
|
+
USER_ANSWER_CONTEXT_KEY = "user_answer"
|
|
39
|
+
|
|
31
40
|
# Document parsing constants (using shared INTERACTION_PATTERN defined above)
|
|
32
41
|
|
|
33
42
|
# Separators
|
|
@@ -17,6 +17,7 @@ from .constants import (
|
|
|
17
17
|
CONTEXT_QUESTION_TEMPLATE,
|
|
18
18
|
DEFAULT_INTERACTION_ERROR_PROMPT,
|
|
19
19
|
DEFAULT_INTERACTION_PROMPT,
|
|
20
|
+
DEFAULT_NON_ASSIGNMENT_INPUT_KEY,
|
|
20
21
|
INPUT_EMPTY_ERROR,
|
|
21
22
|
INTERACTION_ERROR_RENDER_INSTRUCTIONS,
|
|
22
23
|
INTERACTION_PARSE_ERROR,
|
|
@@ -36,12 +37,13 @@ from .constants import (
|
|
|
36
37
|
OUTPUT_LANGUAGE_INSTRUCTION_BOTTOM,
|
|
37
38
|
OUTPUT_LANGUAGE_INSTRUCTION_TOP,
|
|
38
39
|
UNSUPPORTED_PROMPT_TYPE_ERROR,
|
|
40
|
+
USER_ANSWER_CONTEXT_KEY,
|
|
39
41
|
VALIDATION_REQUIREMENTS_TEMPLATE,
|
|
40
42
|
VALIDATION_TASK_BASE,
|
|
41
43
|
VALIDATION_TASK_TEMPLATE,
|
|
42
44
|
VALIDATION_TASK_WITH_LANGUAGE,
|
|
43
45
|
)
|
|
44
|
-
from .constants_system_prompt import DEFAULT_MDF_SYSTEM_PROMPT
|
|
46
|
+
from .constants_system_prompt import DEFAULT_MDF_SYSTEM_PROMPT, HIGHER_PRIORITY_INSTRUCTIONS_PROMPT
|
|
45
47
|
from .enums import BlockType
|
|
46
48
|
from .exceptions import BlockIndexError
|
|
47
49
|
from .formatter import ElementType, StreamFormatter, format_content
|
|
@@ -363,10 +365,18 @@ class MarkdownFlow:
|
|
|
363
365
|
Transform context messages containing interaction syntax into LLM-friendly format.
|
|
364
366
|
|
|
365
367
|
For assistant messages containing ?[...] syntax:
|
|
366
|
-
- No variable
|
|
368
|
+
- No variable + "user_answer" field present and non-empty: replace with
|
|
369
|
+
{user: answer} + {assistant: "ok"}
|
|
370
|
+
- No variable + "user_answer" field present but empty: skip the message
|
|
371
|
+
(the interaction was never answered; do not fabricate an "ok")
|
|
372
|
+
- No variable + no "user_answer" field: replace with {user: "ok"} +
|
|
373
|
+
{assistant: "ok"} (legacy behavior for callers unaware of the field)
|
|
367
374
|
- Has variable + value found: replace with {user: value} + {assistant: "ok"}
|
|
368
375
|
- Has variable + value not found: skip the message
|
|
369
376
|
|
|
377
|
+
Non-standard fields such as "user_answer" are stripped from the
|
|
378
|
+
returned messages; the output only ever contains {role, content}.
|
|
379
|
+
|
|
370
380
|
Args:
|
|
371
381
|
context: Context message list
|
|
372
382
|
variables: Variable mappings
|
|
@@ -382,7 +392,9 @@ class MarkdownFlow:
|
|
|
382
392
|
|
|
383
393
|
for msg in context:
|
|
384
394
|
if msg.get("role") != "assistant" or not has_interaction(msg.get("content", "")):
|
|
385
|
-
|
|
395
|
+
# Whitelist {role, content}: callers may attach extension
|
|
396
|
+
# fields (e.g. user_answer) that must not reach the LLM.
|
|
397
|
+
result.append({"role": msg.get("role", ""), "content": msg.get("content", "")})
|
|
386
398
|
continue
|
|
387
399
|
|
|
388
400
|
# Parse interaction syntax
|
|
@@ -395,8 +407,23 @@ class MarkdownFlow:
|
|
|
395
407
|
|
|
396
408
|
variable_name = parse_result.get("variable")
|
|
397
409
|
|
|
398
|
-
# No variable interaction (e.g. ?[Continue])
|
|
410
|
+
# No variable interaction (e.g. ?[Continue] or ?[A | B])
|
|
399
411
|
if not variable_name:
|
|
412
|
+
if USER_ANSWER_CONTEXT_KEY in msg:
|
|
413
|
+
# Accept both a flattened string and a list of values
|
|
414
|
+
# (callers may pass metadata["answer"] through directly,
|
|
415
|
+
# beyond the declared str value type).
|
|
416
|
+
raw_answer: Any = msg.get(USER_ANSWER_CONTEXT_KEY)
|
|
417
|
+
if isinstance(raw_answer, list):
|
|
418
|
+
user_answer = ", ".join(str(value) for value in raw_answer if value is not None).strip()
|
|
419
|
+
else:
|
|
420
|
+
user_answer = str(raw_answer or "").strip()
|
|
421
|
+
if user_answer:
|
|
422
|
+
result.append({"role": "user", "content": user_answer})
|
|
423
|
+
result.append({"role": "assistant", "content": "ok"})
|
|
424
|
+
# Empty answer: the interaction was never answered, skip it
|
|
425
|
+
continue
|
|
426
|
+
# Legacy behavior for callers that do not supply user_answer
|
|
400
427
|
result.append({"role": "user", "content": "ok"})
|
|
401
428
|
result.append({"role": "assistant", "content": "ok"})
|
|
402
429
|
continue
|
|
@@ -806,16 +833,12 @@ class MarkdownFlow:
|
|
|
806
833
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
807
834
|
"""Process interaction user input."""
|
|
808
835
|
block = self.get_block(block_index)
|
|
809
|
-
target_variable = block.variables[0] if block.variables else "user_input"
|
|
810
836
|
|
|
811
837
|
# Basic validation
|
|
812
838
|
if not user_input or not any(values for values in user_input.values()):
|
|
813
839
|
error_msg = INPUT_EMPTY_ERROR
|
|
814
840
|
return self._render_error(error_msg, mode, context, variables)
|
|
815
841
|
|
|
816
|
-
# Get the target variable value from user_input
|
|
817
|
-
target_values = user_input.get(target_variable, [])
|
|
818
|
-
|
|
819
842
|
# Apply variable replacement to interaction content
|
|
820
843
|
processed_content = replace_variables_in_text(block.content, variables or {})
|
|
821
844
|
|
|
@@ -829,6 +852,20 @@ class MarkdownFlow:
|
|
|
829
852
|
|
|
830
853
|
interaction_type = parse_result.get("type")
|
|
831
854
|
|
|
855
|
+
# Derive the target variable from the parsed interaction, not from
|
|
856
|
+
# block.variables: text interpolation (e.g. ?[{{userName}} Continue])
|
|
857
|
+
# must not be mistaken for the assignment target.
|
|
858
|
+
target_variable = parse_result.get("variable") or DEFAULT_NON_ASSIGNMENT_INPUT_KEY
|
|
859
|
+
|
|
860
|
+
# Get the target variable value from user_input
|
|
861
|
+
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
862
|
+
# Non-assignment interactions have no variable name for clients to
|
|
863
|
+
# key on; accept the answer from any key ("", "input", legacy
|
|
864
|
+
# "user_input", ...) by merging all submitted values.
|
|
865
|
+
target_values = [value for values in user_input.values() for value in values]
|
|
866
|
+
else:
|
|
867
|
+
target_values = user_input.get(target_variable, [])
|
|
868
|
+
|
|
832
869
|
# Process user input based on interaction type
|
|
833
870
|
if interaction_type in [
|
|
834
871
|
InteractionType.BUTTONS_WITH_TEXT,
|
|
@@ -954,14 +991,22 @@ class MarkdownFlow:
|
|
|
954
991
|
)
|
|
955
992
|
|
|
956
993
|
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
957
|
-
# Non-assignment
|
|
958
|
-
#
|
|
994
|
+
# Non-assignment interactions: ?[Continue], ?[A|B], ?[A||B],
|
|
995
|
+
# ?[...question], ?[A|B|...question]. The answer is not assigned
|
|
996
|
+
# to a variable; values matching a button (by display or value)
|
|
997
|
+
# are normalized to the button value, anything else is accepted
|
|
998
|
+
# as free text. Callers should persist metadata["answer"] and
|
|
999
|
+
# feed it back through the "user_answer" context field so the
|
|
1000
|
+
# choice reaches the next LLM request.
|
|
1001
|
+
buttons = parse_result.get("buttons", [])
|
|
1002
|
+
matched_values, unmatched_values = self._match_button_values(buttons, target_values)
|
|
959
1003
|
result = LLMResult(
|
|
960
1004
|
content="", # Empty content indicates interaction complete
|
|
961
|
-
variables={}, # Non-assignment
|
|
1005
|
+
variables={}, # Non-assignment interactions don't set variables
|
|
962
1006
|
metadata={
|
|
963
1007
|
"interaction_type": "non_assignment_button",
|
|
964
1008
|
"user_input": user_input,
|
|
1009
|
+
"answer": matched_values + unmatched_values,
|
|
965
1010
|
},
|
|
966
1011
|
)
|
|
967
1012
|
# Return generator for STREAM mode, direct result for COMPLETE mode
|
|
@@ -1247,8 +1292,7 @@ class MarkdownFlow:
|
|
|
1247
1292
|
# Build message array
|
|
1248
1293
|
messages = []
|
|
1249
1294
|
|
|
1250
|
-
# Build system message
|
|
1251
|
-
# Two parts: MDF internal prompt + Document prompt
|
|
1295
|
+
# Build system message from the MDF prompt and optional higher-priority instructions.
|
|
1252
1296
|
system_parts = []
|
|
1253
1297
|
|
|
1254
1298
|
# Part 1: MDF internal prompt (content rules + visual rules)
|
|
@@ -1259,9 +1303,10 @@ class MarkdownFlow:
|
|
|
1259
1303
|
if self._viewing_mode_prompt:
|
|
1260
1304
|
system_parts.append(self._viewing_mode_prompt)
|
|
1261
1305
|
|
|
1262
|
-
# Part 2: Document prompt
|
|
1263
|
-
|
|
1264
|
-
|
|
1306
|
+
# Part 2: Document prompt, wrapped as higher-priority instructions. This
|
|
1307
|
+
# part remains last so every supplied Markdown heading stays in scope.
|
|
1308
|
+
if self._document_prompt and self._document_prompt.strip():
|
|
1309
|
+
system_parts.append(f"{HIGHER_PRIORITY_INSTRUCTIONS_PROMPT}\n\n{self._document_prompt}")
|
|
1265
1310
|
|
|
1266
1311
|
# Combine all parts and add as system message
|
|
1267
1312
|
if system_parts:
|
|
@@ -1327,7 +1372,12 @@ class MarkdownFlow:
|
|
|
1327
1372
|
buttons = parse_result.get("buttons") or []
|
|
1328
1373
|
option_displays = [button.get("display", "").strip() for button in buttons if button.get("display", "").strip()]
|
|
1329
1374
|
|
|
1330
|
-
detail = self._format_next_interaction_detail(
|
|
1375
|
+
detail = self._format_next_interaction_detail(
|
|
1376
|
+
parse_result.get("type"),
|
|
1377
|
+
question,
|
|
1378
|
+
option_displays,
|
|
1379
|
+
parse_result.get("is_multi_select", False),
|
|
1380
|
+
)
|
|
1331
1381
|
if not detail:
|
|
1332
1382
|
return ""
|
|
1333
1383
|
|
|
@@ -1338,17 +1388,29 @@ class MarkdownFlow:
|
|
|
1338
1388
|
interaction_type: InteractionType | None,
|
|
1339
1389
|
question: str,
|
|
1340
1390
|
option_displays: list[str],
|
|
1391
|
+
is_multi_select: bool = False,
|
|
1341
1392
|
) -> str:
|
|
1342
1393
|
"""Format the type-specific part of the next interaction prompt."""
|
|
1343
1394
|
options = json.dumps(option_displays, ensure_ascii=False) if option_displays else ""
|
|
1344
1395
|
question_text = json.dumps(question, ensure_ascii=False) if question else ""
|
|
1345
1396
|
|
|
1397
|
+
# Non-assignment interactions share the same shapes as variable ones
|
|
1398
|
+
# (buttons / multi-select / text input / buttons+text); map to the
|
|
1399
|
+
# equivalent variable type so the prompt wording matches.
|
|
1400
|
+
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
1401
|
+
if option_displays and question:
|
|
1402
|
+
interaction_type = InteractionType.BUTTONS_MULTI_WITH_TEXT if is_multi_select else InteractionType.BUTTONS_WITH_TEXT
|
|
1403
|
+
elif question:
|
|
1404
|
+
interaction_type = InteractionType.TEXT_ONLY
|
|
1405
|
+
else:
|
|
1406
|
+
interaction_type = InteractionType.BUTTONS_MULTI_SELECT if is_multi_select else InteractionType.BUTTONS_ONLY
|
|
1407
|
+
|
|
1346
1408
|
if interaction_type == InteractionType.TEXT_ONLY:
|
|
1347
1409
|
if not question:
|
|
1348
1410
|
return ""
|
|
1349
1411
|
return NEXT_INTERACTION_TEXT_INPUT_TEMPLATE.format(question=question_text)
|
|
1350
1412
|
|
|
1351
|
-
if interaction_type
|
|
1413
|
+
if interaction_type == InteractionType.BUTTONS_ONLY:
|
|
1352
1414
|
if not options:
|
|
1353
1415
|
return ""
|
|
1354
1416
|
return NEXT_INTERACTION_SINGLE_CHOICE_TEMPLATE.format(options=options)
|
|
@@ -6,14 +6,20 @@ Mirrors Go formatter/format.go.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .classifier import Classifier
|
|
9
|
+
from .sanitize import strip_untagged_code_fences
|
|
9
10
|
from .types import ClassifyResult, ElementType, FormattedElement, is_text_family
|
|
10
11
|
|
|
11
12
|
|
|
12
|
-
def format_content(content: str) -> list[FormattedElement]:
|
|
13
|
+
def format_content(content: str, *, strip_untagged_fences: bool = True) -> list[FormattedElement]:
|
|
13
14
|
"""Format complete content into structured elements.
|
|
14
15
|
|
|
15
16
|
Args:
|
|
16
17
|
content: The full content string to format.
|
|
18
|
+
strip_untagged_fences: When True (default), untagged (language-less)
|
|
19
|
+
code-fence markers are removed before classification, so HTML/SVG
|
|
20
|
+
an LLM wrapped in a bare ``` renders as its real type instead of a
|
|
21
|
+
code block. Language-tagged fences (``` ```html ```, ``` ```mermaid ```,
|
|
22
|
+
...) are always preserved. Set False to keep raw CommonMark behaviour.
|
|
17
23
|
|
|
18
24
|
Returns:
|
|
19
25
|
A list of FormattedElement instances.
|
|
@@ -21,6 +27,11 @@ def format_content(content: str) -> list[FormattedElement]:
|
|
|
21
27
|
if not content:
|
|
22
28
|
return []
|
|
23
29
|
|
|
30
|
+
if strip_untagged_fences:
|
|
31
|
+
content = strip_untagged_code_fences(content)
|
|
32
|
+
if not content:
|
|
33
|
+
return []
|
|
34
|
+
|
|
24
35
|
c = Classifier()
|
|
25
36
|
elements: list[FormattedElement] = []
|
|
26
37
|
current_number = 0
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
The remainder of this system message contains higher-priority instructions.
|
|
2
|
+
|
|
3
|
+
Treat everything after the marker below—including every Markdown heading and section—as one instruction block. Within this system message, instructions in that block take precedence over all instructions above it. If there is a conflict, follow the instruction in the block. If there is no conflict, follow all applicable instructions. Do not reproduce the marker.
|
|
4
|
+
|
|
5
|
+
[BEGIN HIGHER-PRIORITY INSTRUCTIONS]
|
|
@@ -209,7 +209,12 @@ class InteractionParser:
|
|
|
209
209
|
|
|
210
210
|
def _layer3_parse_display_buttons(self, content: str) -> dict[str, Any]:
|
|
211
211
|
"""
|
|
212
|
-
Layer 3: Parse
|
|
212
|
+
Layer 3: Parse non-assignment interactions (no variable).
|
|
213
|
+
|
|
214
|
+
Mirrors the variable branch: supports button groups (single/multi
|
|
215
|
+
select) and the `...` text-input suffix. The collected answer is not
|
|
216
|
+
assigned to a variable; callers feed it back into the conversation
|
|
217
|
+
context instead.
|
|
213
218
|
|
|
214
219
|
Args:
|
|
215
220
|
content: Content to parse
|
|
@@ -224,13 +229,43 @@ class InteractionParser:
|
|
|
224
229
|
"buttons": [{"display": "", "value": ""}],
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
ellipsis_match = COMPILED_LAYER3_ELLIPSIS_REGEX.match(content)
|
|
233
|
+
if ellipsis_match:
|
|
234
|
+
before_ellipsis = ellipsis_match.group(1).strip()
|
|
235
|
+
question = ellipsis_match.group(2).strip()
|
|
236
|
+
|
|
237
|
+
if before_ellipsis:
|
|
238
|
+
# Buttons + text input: ?[A | B | ...question]
|
|
239
|
+
buttons, is_multi_select = self._parse_buttons(before_ellipsis)
|
|
240
|
+
return {
|
|
241
|
+
"type": InteractionType.NON_ASSIGNMENT_BUTTON,
|
|
242
|
+
"buttons": buttons,
|
|
243
|
+
"question": question,
|
|
244
|
+
"is_multi_select": is_multi_select,
|
|
245
|
+
}
|
|
246
|
+
# Pure text input: ?[...question]
|
|
247
|
+
return {
|
|
248
|
+
"type": InteractionType.NON_ASSIGNMENT_BUTTON,
|
|
249
|
+
"buttons": [],
|
|
250
|
+
"question": question,
|
|
251
|
+
"is_multi_select": False,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if "|" in content: # type: ignore[unreachable]
|
|
255
|
+
# Button group: ?[A | B] or ?[A || B]
|
|
256
|
+
buttons, is_multi_select = self._parse_buttons(content)
|
|
257
|
+
return {
|
|
258
|
+
"type": InteractionType.NON_ASSIGNMENT_BUTTON,
|
|
259
|
+
"buttons": buttons,
|
|
260
|
+
"is_multi_select": is_multi_select,
|
|
261
|
+
}
|
|
231
262
|
# Single button
|
|
232
263
|
button = self._parse_single_button(content)
|
|
233
|
-
return {
|
|
264
|
+
return {
|
|
265
|
+
"type": InteractionType.NON_ASSIGNMENT_BUTTON,
|
|
266
|
+
"buttons": [button],
|
|
267
|
+
"is_multi_select": False,
|
|
268
|
+
}
|
|
234
269
|
|
|
235
270
|
def _parse_buttons(self, content: str) -> tuple[list[dict[str, str]], bool]:
|
|
236
271
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -313,10 +313,40 @@ class InteractionType(NamedTuple):
|
|
|
313
313
|
# BUTTONS_MULTI_WITH_TEXT: Multi-select with text fallback
|
|
314
314
|
"?[%{{frameworks}} React||Vue||Angular||Please specify others...]"
|
|
315
315
|
|
|
316
|
-
# NON_ASSIGNMENT_BUTTON:
|
|
316
|
+
# NON_ASSIGNMENT_BUTTON: No-variable interaction. Supports the same shapes as
|
|
317
|
+
# variable interactions; the answer is not assigned to a variable but fed back
|
|
318
|
+
# into the conversation context (see "Non-assignment interactions" below).
|
|
317
319
|
"?[Continue | Cancel | Go Back]"
|
|
320
|
+
"?[JavaScript||TypeScript||Python]"
|
|
321
|
+
"?[...Anything to add?]"
|
|
322
|
+
"?[Option A | Option B | ...Other, please specify]"
|
|
318
323
|
```
|
|
319
324
|
|
|
325
|
+
### Non-assignment interactions
|
|
326
|
+
|
|
327
|
+
Interactions without a `%{{variable}}` collect an answer that is not stored in
|
|
328
|
+
any variable — it flows back into the conversation context instead:
|
|
329
|
+
|
|
330
|
+
1. Clients submit the answer under the canonical key `input`
|
|
331
|
+
(`DEFAULT_NON_ASSIGNMENT_INPUT_KEY`); the library tolerantly merges values
|
|
332
|
+
from any other key.
|
|
333
|
+
2. `process()` normalizes values matching a button (by display or value) to the
|
|
334
|
+
button value and accepts anything else as free text. The normalized answer
|
|
335
|
+
is returned in `result.metadata["answer"]` (`list[str]`).
|
|
336
|
+
3. Callers persist that answer and attach it to the interaction's context
|
|
337
|
+
message via the `user_answer` extension field (`USER_ANSWER_CONTEXT_KEY`):
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
context = [
|
|
341
|
+
{"role": "assistant", "content": "?[Option A | Option B]", "user_answer": "Option A"},
|
|
342
|
+
]
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
When building LLM messages the library expands this into
|
|
346
|
+
`{user: "Option A"}` + `{assistant: "ok"}`. An empty `user_answer` skips the
|
|
347
|
+
turn entirely; omitting the field keeps the legacy `{user: "ok"}` behavior.
|
|
348
|
+
Extension fields are stripped before messages reach the LLM.
|
|
349
|
+
|
|
320
350
|
### Utility Functions
|
|
321
351
|
|
|
322
352
|
#### Variable Operations
|
|
@@ -7,11 +7,11 @@ markdown_flow/constants_system_prompt.py
|
|
|
7
7
|
markdown_flow/core.py
|
|
8
8
|
markdown_flow/enums.py
|
|
9
9
|
markdown_flow/exceptions.py
|
|
10
|
+
markdown_flow/higher_priority_prompt.md
|
|
10
11
|
markdown_flow/llm.py
|
|
11
12
|
markdown_flow/models.py
|
|
12
13
|
markdown_flow/system_prompt.md
|
|
13
14
|
markdown_flow/tag_filter.py
|
|
14
|
-
markdown_flow/utils.py
|
|
15
15
|
markdown_flow.egg-info/PKG-INFO
|
|
16
16
|
markdown_flow.egg-info/SOURCES.txt
|
|
17
17
|
markdown_flow.egg-info/dependency_links.txt
|