markdown-flow 0.2.19__tar.gz → 0.2.23__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.
Potentially problematic release.
This version of markdown-flow might be problematic. Click here for more details.
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/PKG-INFO +1 -1
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/__init__.py +1 -1
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/constants.py +6 -3
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/core.py +256 -36
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/llm.py +4 -2
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/utils.py +12 -1
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow.egg-info/PKG-INFO +1 -1
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/tests/test_preserved_simple.py +99 -5
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/LICENSE +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/README.md +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/enums.py +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/exceptions.py +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow/models.py +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow.egg-info/SOURCES.txt +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow.egg-info/dependency_links.txt +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/markdown_flow.egg-info/top_level.txt +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/pyproject.toml +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/setup.cfg +0 -0
- {markdown_flow-0.2.19 → markdown_flow-0.2.23}/tests/test_dynamic_interaction.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.23
|
|
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
|
|
@@ -172,9 +172,10 @@ SMART_VALIDATION_TEMPLATE = """# 任务
|
|
|
172
172
|
# 提取要求
|
|
173
173
|
1. 仔细阅读上述相关问题,理解这个问题想要获取什么信息
|
|
174
174
|
2. 从用户回答中提取与该问题相关的信息
|
|
175
|
-
3.
|
|
176
|
-
4.
|
|
177
|
-
5.
|
|
175
|
+
3. 如果提供了预定义选项,用户选择这些选项时都应该接受;自定义输入应与选项主题相关
|
|
176
|
+
4. 对于昵称/姓名类问题,任何非空的合理字符串(包括简短的如"ee"、"aa"、"007"等)都应该接受
|
|
177
|
+
5. 只有当用户回答完全无关、包含不当内容或明显不合理时才标记为不合法
|
|
178
|
+
6. 确保提取的信息准确、完整且符合预期格式"""
|
|
178
179
|
|
|
179
180
|
# Validation template for buttons with text input
|
|
180
181
|
BUTTONS_WITH_TEXT_VALIDATION_TEMPLATE = """用户针对以下问题进行了输入:
|
|
@@ -220,7 +221,9 @@ VARIABLE_DEFAULT_VALUE = "UNKNOWN"
|
|
|
220
221
|
# Context generation constants
|
|
221
222
|
CONTEXT_QUESTION_MARKER = "# 相关问题"
|
|
222
223
|
CONTEXT_CONVERSATION_MARKER = "# 对话上下文"
|
|
224
|
+
CONTEXT_BUTTON_OPTIONS_MARKER = "## 预定义选项"
|
|
223
225
|
|
|
224
226
|
# Context generation templates
|
|
225
227
|
CONTEXT_QUESTION_TEMPLATE = f"{CONTEXT_QUESTION_MARKER}\n{{question}}"
|
|
226
228
|
CONTEXT_CONVERSATION_TEMPLATE = f"{CONTEXT_CONVERSATION_MARKER}\n{{content}}"
|
|
229
|
+
CONTEXT_BUTTON_OPTIONS_TEMPLATE = f"{CONTEXT_BUTTON_OPTIONS_MARKER}\n可选的预定义选项包括:{{button_options}}\n注意:用户如果选择了这些选项,都应该接受;如果输入了自定义内容,应检查是否与选项主题相关。"
|
|
@@ -60,6 +60,7 @@ class MarkdownFlow:
|
|
|
60
60
|
_document_prompt: str | None
|
|
61
61
|
_interaction_prompt: str | None
|
|
62
62
|
_interaction_error_prompt: str | None
|
|
63
|
+
_max_context_length: int
|
|
63
64
|
_blocks: list[Block] | None
|
|
64
65
|
_interaction_configs: dict[int, InteractionValidationConfig]
|
|
65
66
|
|
|
@@ -70,6 +71,7 @@ class MarkdownFlow:
|
|
|
70
71
|
document_prompt: str | None = None,
|
|
71
72
|
interaction_prompt: str | None = None,
|
|
72
73
|
interaction_error_prompt: str | None = None,
|
|
74
|
+
max_context_length: int = 0,
|
|
73
75
|
):
|
|
74
76
|
"""
|
|
75
77
|
Initialize MarkdownFlow instance.
|
|
@@ -80,12 +82,14 @@ class MarkdownFlow:
|
|
|
80
82
|
document_prompt: Document-level system prompt
|
|
81
83
|
interaction_prompt: Interaction content rendering prompt
|
|
82
84
|
interaction_error_prompt: Interaction error rendering prompt
|
|
85
|
+
max_context_length: Maximum number of context messages to keep (0 = unlimited)
|
|
83
86
|
"""
|
|
84
87
|
self._document = document
|
|
85
88
|
self._llm_provider = llm_provider
|
|
86
89
|
self._document_prompt = document_prompt
|
|
87
90
|
self._interaction_prompt = interaction_prompt or DEFAULT_INTERACTION_PROMPT
|
|
88
91
|
self._interaction_error_prompt = interaction_error_prompt or DEFAULT_INTERACTION_ERROR_PROMPT
|
|
92
|
+
self._max_context_length = max_context_length
|
|
89
93
|
self._blocks = None
|
|
90
94
|
self._interaction_configs: dict[int, InteractionValidationConfig] = {}
|
|
91
95
|
|
|
@@ -110,6 +114,44 @@ class MarkdownFlow:
|
|
|
110
114
|
else:
|
|
111
115
|
raise ValueError(UNSUPPORTED_PROMPT_TYPE_ERROR.format(prompt_type=prompt_type))
|
|
112
116
|
|
|
117
|
+
def _truncate_context(
|
|
118
|
+
self,
|
|
119
|
+
context: list[dict[str, str]] | None,
|
|
120
|
+
) -> list[dict[str, str]] | None:
|
|
121
|
+
"""
|
|
122
|
+
Filter and truncate context to specified maximum length.
|
|
123
|
+
|
|
124
|
+
Processing steps:
|
|
125
|
+
1. Filter out messages with empty content (empty string or whitespace only)
|
|
126
|
+
2. Truncate to max_context_length if configured (0 = unlimited)
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
context: Original context list
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Filtered and truncated context. Returns None if no valid messages remain.
|
|
133
|
+
"""
|
|
134
|
+
if not context:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
# Step 1: Filter out messages with empty or whitespace-only content
|
|
138
|
+
filtered_context = [msg for msg in context if msg.get("content", "").strip()]
|
|
139
|
+
|
|
140
|
+
# Return None if no valid messages remain after filtering
|
|
141
|
+
if not filtered_context:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
# Step 2: Truncate to max_context_length if configured
|
|
145
|
+
if self._max_context_length == 0:
|
|
146
|
+
# No limit, return all filtered messages
|
|
147
|
+
return filtered_context
|
|
148
|
+
|
|
149
|
+
# Keep the most recent N messages
|
|
150
|
+
if len(filtered_context) > self._max_context_length:
|
|
151
|
+
return filtered_context[-self._max_context_length :]
|
|
152
|
+
|
|
153
|
+
return filtered_context
|
|
154
|
+
|
|
113
155
|
@property
|
|
114
156
|
def document(self) -> str:
|
|
115
157
|
"""Get document content."""
|
|
@@ -210,7 +252,7 @@ class MarkdownFlow:
|
|
|
210
252
|
if block.block_type == BlockType.INTERACTION:
|
|
211
253
|
if user_input is None:
|
|
212
254
|
# Render interaction content
|
|
213
|
-
return self._process_interaction_render(block_index, mode, variables)
|
|
255
|
+
return self._process_interaction_render(block_index, mode, context, variables)
|
|
214
256
|
# Process user input
|
|
215
257
|
return self._process_interaction_input(block_index, user_input, mode, context, variables)
|
|
216
258
|
|
|
@@ -231,8 +273,11 @@ class MarkdownFlow:
|
|
|
231
273
|
variables: dict[str, str | list[str]] | None,
|
|
232
274
|
):
|
|
233
275
|
"""Process content block."""
|
|
234
|
-
#
|
|
235
|
-
|
|
276
|
+
# Truncate context to configured maximum length
|
|
277
|
+
truncated_context = self._truncate_context(context)
|
|
278
|
+
|
|
279
|
+
# Build messages with context
|
|
280
|
+
messages = self._build_content_messages(block_index, variables, truncated_context)
|
|
236
281
|
|
|
237
282
|
if mode == ProcessMode.PROMPT_ONLY:
|
|
238
283
|
return LLMResult(prompt=messages[-1]["content"], metadata={"messages": messages})
|
|
@@ -266,7 +311,13 @@ class MarkdownFlow:
|
|
|
266
311
|
|
|
267
312
|
return LLMResult(content=content)
|
|
268
313
|
|
|
269
|
-
def _process_interaction_render(
|
|
314
|
+
def _process_interaction_render(
|
|
315
|
+
self,
|
|
316
|
+
block_index: int,
|
|
317
|
+
mode: ProcessMode,
|
|
318
|
+
context: list[dict[str, str]] | None = None,
|
|
319
|
+
variables: dict[str, str | list[str]] | None = None,
|
|
320
|
+
):
|
|
270
321
|
"""Process interaction content rendering."""
|
|
271
322
|
block = self.get_block(block_index)
|
|
272
323
|
|
|
@@ -283,8 +334,11 @@ class MarkdownFlow:
|
|
|
283
334
|
# Unable to extract, return processed content
|
|
284
335
|
return LLMResult(content=processed_block.content)
|
|
285
336
|
|
|
286
|
-
#
|
|
287
|
-
|
|
337
|
+
# Truncate context to configured maximum length
|
|
338
|
+
truncated_context = self._truncate_context(context)
|
|
339
|
+
|
|
340
|
+
# Build render messages with context
|
|
341
|
+
messages = self._build_interaction_render_messages(question_text, truncated_context)
|
|
288
342
|
|
|
289
343
|
if mode == ProcessMode.PROMPT_ONLY:
|
|
290
344
|
return LLMResult(
|
|
@@ -356,7 +410,7 @@ class MarkdownFlow:
|
|
|
356
410
|
# Basic validation
|
|
357
411
|
if not user_input or not any(values for values in user_input.values()):
|
|
358
412
|
error_msg = INPUT_EMPTY_ERROR
|
|
359
|
-
return self._render_error(error_msg, mode)
|
|
413
|
+
return self._render_error(error_msg, mode, context)
|
|
360
414
|
|
|
361
415
|
# Get the target variable value from user_input
|
|
362
416
|
target_values = user_input.get(target_variable, [])
|
|
@@ -370,24 +424,103 @@ class MarkdownFlow:
|
|
|
370
424
|
|
|
371
425
|
if "error" in parse_result:
|
|
372
426
|
error_msg = INTERACTION_PARSE_ERROR.format(error=parse_result["error"])
|
|
373
|
-
return self._render_error(error_msg, mode)
|
|
427
|
+
return self._render_error(error_msg, mode, context)
|
|
374
428
|
|
|
375
429
|
interaction_type = parse_result.get("type")
|
|
376
430
|
|
|
377
431
|
# Process user input based on interaction type
|
|
378
432
|
if interaction_type in [
|
|
379
|
-
InteractionType.BUTTONS_ONLY,
|
|
380
433
|
InteractionType.BUTTONS_WITH_TEXT,
|
|
381
|
-
InteractionType.BUTTONS_MULTI_SELECT,
|
|
382
434
|
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
383
435
|
]:
|
|
384
|
-
#
|
|
436
|
+
# Buttons with text input: smart validation (match buttons first, then LLM validate custom text)
|
|
437
|
+
buttons = parse_result.get("buttons", [])
|
|
438
|
+
|
|
439
|
+
# Step 1: Match button values
|
|
440
|
+
matched_values, unmatched_values = self._match_button_values(buttons, target_values)
|
|
441
|
+
|
|
442
|
+
# Step 2: If there are unmatched values (custom text), validate with LLM
|
|
443
|
+
if unmatched_values:
|
|
444
|
+
# Create user_input for LLM validation (only custom text)
|
|
445
|
+
custom_input = {target_variable: unmatched_values}
|
|
446
|
+
|
|
447
|
+
validation_result = self._process_llm_validation(
|
|
448
|
+
block_index=block_index,
|
|
449
|
+
user_input=custom_input,
|
|
450
|
+
target_variable=target_variable,
|
|
451
|
+
mode=mode,
|
|
452
|
+
context=context,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Handle validation result based on mode
|
|
456
|
+
if mode == ProcessMode.PROMPT_ONLY:
|
|
457
|
+
# Return validation prompt
|
|
458
|
+
return validation_result
|
|
459
|
+
|
|
460
|
+
if mode == ProcessMode.COMPLETE:
|
|
461
|
+
# Check if validation passed
|
|
462
|
+
if isinstance(validation_result, LLMResult) and validation_result.variables:
|
|
463
|
+
validated_values = validation_result.variables.get(target_variable, [])
|
|
464
|
+
# Merge matched button values + validated custom text
|
|
465
|
+
all_values = matched_values + validated_values
|
|
466
|
+
return LLMResult(
|
|
467
|
+
content="",
|
|
468
|
+
variables={target_variable: all_values},
|
|
469
|
+
metadata={
|
|
470
|
+
"interaction_type": str(interaction_type),
|
|
471
|
+
"matched_button_values": matched_values,
|
|
472
|
+
"validated_custom_values": validated_values,
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
# Validation failed, return error
|
|
477
|
+
return validation_result
|
|
478
|
+
|
|
479
|
+
if mode == ProcessMode.STREAM:
|
|
480
|
+
# For stream mode, collect validation result
|
|
481
|
+
def stream_merge_generator():
|
|
482
|
+
# Consume the validation stream
|
|
483
|
+
for result in validation_result: # type: ignore[attr-defined]
|
|
484
|
+
if isinstance(result, LLMResult) and result.variables:
|
|
485
|
+
validated_values = result.variables.get(target_variable, [])
|
|
486
|
+
all_values = matched_values + validated_values
|
|
487
|
+
yield LLMResult(
|
|
488
|
+
content="",
|
|
489
|
+
variables={target_variable: all_values},
|
|
490
|
+
metadata={
|
|
491
|
+
"interaction_type": str(interaction_type),
|
|
492
|
+
"matched_button_values": matched_values,
|
|
493
|
+
"validated_custom_values": validated_values,
|
|
494
|
+
},
|
|
495
|
+
)
|
|
496
|
+
else:
|
|
497
|
+
# Validation failed
|
|
498
|
+
yield result
|
|
499
|
+
|
|
500
|
+
return stream_merge_generator()
|
|
501
|
+
else:
|
|
502
|
+
# All values matched buttons, return directly
|
|
503
|
+
return LLMResult(
|
|
504
|
+
content="",
|
|
505
|
+
variables={target_variable: matched_values},
|
|
506
|
+
metadata={
|
|
507
|
+
"interaction_type": str(interaction_type),
|
|
508
|
+
"all_matched_buttons": True,
|
|
509
|
+
},
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
if interaction_type in [
|
|
513
|
+
InteractionType.BUTTONS_ONLY,
|
|
514
|
+
InteractionType.BUTTONS_MULTI_SELECT,
|
|
515
|
+
]:
|
|
516
|
+
# Pure button types: only basic button validation (no LLM)
|
|
385
517
|
return self._process_button_validation(
|
|
386
518
|
parse_result,
|
|
387
519
|
target_values,
|
|
388
520
|
target_variable,
|
|
389
521
|
mode,
|
|
390
522
|
interaction_type,
|
|
523
|
+
context,
|
|
391
524
|
)
|
|
392
525
|
|
|
393
526
|
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
@@ -403,19 +536,50 @@ class MarkdownFlow:
|
|
|
403
536
|
)
|
|
404
537
|
|
|
405
538
|
# Text-only input type: ?[%{{sys_user_nickname}}...question]
|
|
406
|
-
#
|
|
539
|
+
# Use LLM validation to check if input is relevant to the question
|
|
407
540
|
if target_values:
|
|
408
|
-
return
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
"values": target_values,
|
|
415
|
-
},
|
|
541
|
+
return self._process_llm_validation(
|
|
542
|
+
block_index=block_index,
|
|
543
|
+
user_input=user_input,
|
|
544
|
+
target_variable=target_variable,
|
|
545
|
+
mode=mode,
|
|
546
|
+
context=context,
|
|
416
547
|
)
|
|
417
548
|
error_msg = f"No input provided for variable '{target_variable}'"
|
|
418
|
-
return self._render_error(error_msg, mode)
|
|
549
|
+
return self._render_error(error_msg, mode, context)
|
|
550
|
+
|
|
551
|
+
def _match_button_values(
|
|
552
|
+
self,
|
|
553
|
+
buttons: list[dict[str, str]],
|
|
554
|
+
target_values: list[str],
|
|
555
|
+
) -> tuple[list[str], list[str]]:
|
|
556
|
+
"""
|
|
557
|
+
Match user input values against button options.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
buttons: List of button dictionaries with 'display' and 'value' keys
|
|
561
|
+
target_values: User input values to match
|
|
562
|
+
|
|
563
|
+
Returns:
|
|
564
|
+
Tuple of (matched_values, unmatched_values)
|
|
565
|
+
- matched_values: Values that match button options (using button value)
|
|
566
|
+
- unmatched_values: Values that don't match any button
|
|
567
|
+
"""
|
|
568
|
+
matched_values = []
|
|
569
|
+
unmatched_values = []
|
|
570
|
+
|
|
571
|
+
for value in target_values:
|
|
572
|
+
matched = False
|
|
573
|
+
for button in buttons:
|
|
574
|
+
if value in [button["display"], button["value"]]:
|
|
575
|
+
matched_values.append(button["value"]) # Use button value
|
|
576
|
+
matched = True
|
|
577
|
+
break
|
|
578
|
+
|
|
579
|
+
if not matched:
|
|
580
|
+
unmatched_values.append(value)
|
|
581
|
+
|
|
582
|
+
return matched_values, unmatched_values
|
|
419
583
|
|
|
420
584
|
def _process_button_validation(
|
|
421
585
|
self,
|
|
@@ -424,6 +588,7 @@ class MarkdownFlow:
|
|
|
424
588
|
target_variable: str,
|
|
425
589
|
mode: ProcessMode,
|
|
426
590
|
interaction_type: InteractionType,
|
|
591
|
+
context: list[dict[str, str]] | None = None,
|
|
427
592
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
428
593
|
"""
|
|
429
594
|
Simplified button validation with new input format.
|
|
@@ -434,6 +599,7 @@ class MarkdownFlow:
|
|
|
434
599
|
target_variable: Target variable name
|
|
435
600
|
mode: Processing mode
|
|
436
601
|
interaction_type: Type of interaction
|
|
602
|
+
context: Conversation history context (optional)
|
|
437
603
|
"""
|
|
438
604
|
buttons = parse_result.get("buttons", [])
|
|
439
605
|
is_multi_select = interaction_type in [
|
|
@@ -459,7 +625,7 @@ class MarkdownFlow:
|
|
|
459
625
|
# Pure button mode requires input
|
|
460
626
|
button_displays = [btn["display"] for btn in buttons]
|
|
461
627
|
error_msg = f"Please select from: {', '.join(button_displays)}"
|
|
462
|
-
return self._render_error(error_msg, mode)
|
|
628
|
+
return self._render_error(error_msg, mode, context)
|
|
463
629
|
|
|
464
630
|
# Validate input values against available buttons
|
|
465
631
|
valid_values = []
|
|
@@ -484,7 +650,7 @@ class MarkdownFlow:
|
|
|
484
650
|
if invalid_values and not allow_text_input:
|
|
485
651
|
button_displays = [btn["display"] for btn in buttons]
|
|
486
652
|
error_msg = f"Invalid options: {', '.join(invalid_values)}. Please select from: {', '.join(button_displays)}"
|
|
487
|
-
return self._render_error(error_msg, mode)
|
|
653
|
+
return self._render_error(error_msg, mode, context)
|
|
488
654
|
|
|
489
655
|
# Success: return validated values
|
|
490
656
|
return LLMResult(
|
|
@@ -505,10 +671,11 @@ class MarkdownFlow:
|
|
|
505
671
|
user_input: dict[str, list[str]],
|
|
506
672
|
target_variable: str,
|
|
507
673
|
mode: ProcessMode,
|
|
674
|
+
context: list[dict[str, str]] | None = None,
|
|
508
675
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
509
676
|
"""Process LLM validation."""
|
|
510
677
|
# Build validation messages
|
|
511
|
-
messages = self._build_validation_messages(block_index, user_input, target_variable)
|
|
678
|
+
messages = self._build_validation_messages(block_index, user_input, target_variable, context)
|
|
512
679
|
|
|
513
680
|
if mode == ProcessMode.PROMPT_ONLY:
|
|
514
681
|
return LLMResult(
|
|
@@ -612,9 +779,18 @@ class MarkdownFlow:
|
|
|
612
779
|
|
|
613
780
|
return stream_generator()
|
|
614
781
|
|
|
615
|
-
def _render_error(
|
|
782
|
+
def _render_error(
|
|
783
|
+
self,
|
|
784
|
+
error_message: str,
|
|
785
|
+
mode: ProcessMode,
|
|
786
|
+
context: list[dict[str, str]] | None = None,
|
|
787
|
+
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
616
788
|
"""Render user-friendly error message."""
|
|
617
|
-
|
|
789
|
+
# Truncate context to configured maximum length
|
|
790
|
+
truncated_context = self._truncate_context(context)
|
|
791
|
+
|
|
792
|
+
# Build error messages with context
|
|
793
|
+
messages = self._build_error_render_messages(error_message, truncated_context)
|
|
618
794
|
|
|
619
795
|
if mode == ProcessMode.PROMPT_ONLY:
|
|
620
796
|
return LLMResult(
|
|
@@ -645,6 +821,7 @@ class MarkdownFlow:
|
|
|
645
821
|
self,
|
|
646
822
|
block_index: int,
|
|
647
823
|
variables: dict[str, str | list[str]] | None,
|
|
824
|
+
context: list[dict[str, str]] | None = None,
|
|
648
825
|
) -> list[dict[str, str]]:
|
|
649
826
|
"""Build content block messages."""
|
|
650
827
|
block = self.get_block(block_index)
|
|
@@ -671,18 +848,22 @@ class MarkdownFlow:
|
|
|
671
848
|
# No document prompt but has preserved content, add explanation alone
|
|
672
849
|
messages.append({"role": "system", "content": OUTPUT_INSTRUCTION_EXPLANATION.strip()})
|
|
673
850
|
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
851
|
+
# Add conversation history context if provided
|
|
852
|
+
# Context is inserted after system message and before current user message
|
|
853
|
+
truncated_context = self._truncate_context(context)
|
|
854
|
+
if truncated_context:
|
|
855
|
+
messages.extend(truncated_context)
|
|
679
856
|
|
|
680
857
|
# Add processed content as user message (as instruction to LLM)
|
|
681
858
|
messages.append({"role": "user", "content": block_content})
|
|
682
859
|
|
|
683
860
|
return messages
|
|
684
861
|
|
|
685
|
-
def _build_interaction_render_messages(
|
|
862
|
+
def _build_interaction_render_messages(
|
|
863
|
+
self,
|
|
864
|
+
question_text: str,
|
|
865
|
+
context: list[dict[str, str]] | None = None,
|
|
866
|
+
) -> list[dict[str, str]]:
|
|
686
867
|
"""Build interaction rendering messages."""
|
|
687
868
|
# Check if using custom interaction prompt
|
|
688
869
|
if self._interaction_prompt != DEFAULT_INTERACTION_PROMPT:
|
|
@@ -696,15 +877,32 @@ class MarkdownFlow:
|
|
|
696
877
|
messages = []
|
|
697
878
|
|
|
698
879
|
messages.append({"role": "system", "content": render_prompt})
|
|
880
|
+
|
|
881
|
+
# NOTE: Context is temporarily disabled for interaction rendering
|
|
882
|
+
# Mixing conversation history with interaction content rewriting can cause issues
|
|
883
|
+
# The context parameter is kept in the signature for future use
|
|
884
|
+
# truncated_context = self._truncate_context(context)
|
|
885
|
+
# if truncated_context:
|
|
886
|
+
# messages.extend(truncated_context)
|
|
887
|
+
|
|
699
888
|
messages.append({"role": "user", "content": question_text})
|
|
700
889
|
|
|
701
890
|
return messages
|
|
702
891
|
|
|
703
|
-
def _build_validation_messages(
|
|
892
|
+
def _build_validation_messages(
|
|
893
|
+
self,
|
|
894
|
+
block_index: int,
|
|
895
|
+
user_input: dict[str, list[str]],
|
|
896
|
+
target_variable: str,
|
|
897
|
+
context: list[dict[str, str]] | None = None,
|
|
898
|
+
) -> list[dict[str, str]]:
|
|
704
899
|
"""Build validation messages."""
|
|
705
900
|
block = self.get_block(block_index)
|
|
706
901
|
config = self.get_interaction_validation_config(block_index)
|
|
707
902
|
|
|
903
|
+
# Truncate context to configured maximum length
|
|
904
|
+
truncated_context = self._truncate_context(context)
|
|
905
|
+
|
|
708
906
|
if config and config.validation_template:
|
|
709
907
|
# Use custom validation template
|
|
710
908
|
validation_prompt = config.validation_template
|
|
@@ -716,6 +914,7 @@ class MarkdownFlow:
|
|
|
716
914
|
else:
|
|
717
915
|
# Use smart default validation template
|
|
718
916
|
from .utils import (
|
|
917
|
+
InteractionParser,
|
|
719
918
|
extract_interaction_question,
|
|
720
919
|
generate_smart_validation_template,
|
|
721
920
|
)
|
|
@@ -723,11 +922,17 @@ class MarkdownFlow:
|
|
|
723
922
|
# Extract interaction question
|
|
724
923
|
interaction_question = extract_interaction_question(block.content)
|
|
725
924
|
|
|
726
|
-
#
|
|
925
|
+
# Parse interaction to extract button information
|
|
926
|
+
parser = InteractionParser()
|
|
927
|
+
parse_result = parser.parse(block.content)
|
|
928
|
+
buttons = parse_result.get("buttons") if "buttons" in parse_result else None
|
|
929
|
+
|
|
930
|
+
# Generate smart validation template with context and buttons
|
|
727
931
|
validation_template = generate_smart_validation_template(
|
|
728
932
|
target_variable,
|
|
729
|
-
context=
|
|
933
|
+
context=truncated_context,
|
|
730
934
|
interaction_question=interaction_question,
|
|
935
|
+
buttons=buttons,
|
|
731
936
|
)
|
|
732
937
|
|
|
733
938
|
# Replace template variables
|
|
@@ -740,6 +945,11 @@ class MarkdownFlow:
|
|
|
740
945
|
messages = []
|
|
741
946
|
|
|
742
947
|
messages.append({"role": "system", "content": system_message})
|
|
948
|
+
|
|
949
|
+
# Add conversation history context if provided (only if not using custom template)
|
|
950
|
+
if truncated_context and not (config and config.validation_template):
|
|
951
|
+
messages.extend(truncated_context)
|
|
952
|
+
|
|
743
953
|
messages.append({"role": "user", "content": validation_prompt})
|
|
744
954
|
|
|
745
955
|
return messages
|
|
@@ -770,7 +980,11 @@ class MarkdownFlow:
|
|
|
770
980
|
|
|
771
981
|
return messages
|
|
772
982
|
|
|
773
|
-
def _build_error_render_messages(
|
|
983
|
+
def _build_error_render_messages(
|
|
984
|
+
self,
|
|
985
|
+
error_message: str,
|
|
986
|
+
context: list[dict[str, str]] | None = None,
|
|
987
|
+
) -> list[dict[str, str]]:
|
|
774
988
|
"""Build error rendering messages."""
|
|
775
989
|
render_prompt = f"""{self._interaction_error_prompt}
|
|
776
990
|
|
|
@@ -783,6 +997,12 @@ Original Error: {error_message}
|
|
|
783
997
|
messages.append({"role": "system", "content": self._document_prompt})
|
|
784
998
|
|
|
785
999
|
messages.append({"role": "system", "content": render_prompt})
|
|
1000
|
+
|
|
1001
|
+
# Add conversation history context if provided
|
|
1002
|
+
truncated_context = self._truncate_context(context)
|
|
1003
|
+
if truncated_context:
|
|
1004
|
+
messages.extend(truncated_context)
|
|
1005
|
+
|
|
786
1006
|
messages.append({"role": "user", "content": error_message})
|
|
787
1007
|
|
|
788
1008
|
return messages
|
|
@@ -43,7 +43,8 @@ class LLMProvider(ABC):
|
|
|
43
43
|
Non-streaming LLM call.
|
|
44
44
|
|
|
45
45
|
Args:
|
|
46
|
-
messages: Message list in format [{"role": "system/user/assistant", "content": "..."}]
|
|
46
|
+
messages: Message list in format [{"role": "system/user/assistant", "content": "..."}].
|
|
47
|
+
This list already includes conversation history context merged by MarkdownFlow.
|
|
47
48
|
|
|
48
49
|
Returns:
|
|
49
50
|
str: LLM response content
|
|
@@ -58,7 +59,8 @@ class LLMProvider(ABC):
|
|
|
58
59
|
Streaming LLM call.
|
|
59
60
|
|
|
60
61
|
Args:
|
|
61
|
-
messages: Message list in format [{"role": "system/user/assistant", "content": "..."}]
|
|
62
|
+
messages: Message list in format [{"role": "system/user/assistant", "content": "..."}].
|
|
63
|
+
This list already includes conversation history context merged by MarkdownFlow.
|
|
62
64
|
|
|
63
65
|
Yields:
|
|
64
66
|
str: Incremental LLM response content
|
|
@@ -19,6 +19,7 @@ from .constants import (
|
|
|
19
19
|
COMPILED_PERCENT_VARIABLE_REGEX,
|
|
20
20
|
COMPILED_PRESERVE_FENCE_REGEX,
|
|
21
21
|
COMPILED_SINGLE_PIPE_SPLIT_REGEX,
|
|
22
|
+
CONTEXT_BUTTON_OPTIONS_TEMPLATE,
|
|
22
23
|
CONTEXT_CONVERSATION_TEMPLATE,
|
|
23
24
|
CONTEXT_QUESTION_MARKER,
|
|
24
25
|
CONTEXT_QUESTION_TEMPLATE,
|
|
@@ -479,6 +480,7 @@ def generate_smart_validation_template(
|
|
|
479
480
|
target_variable: str,
|
|
480
481
|
context: list[dict[str, Any]] | None = None,
|
|
481
482
|
interaction_question: str | None = None,
|
|
483
|
+
buttons: list[dict[str, str]] | None = None,
|
|
482
484
|
) -> str:
|
|
483
485
|
"""
|
|
484
486
|
Generate smart validation template based on context and question.
|
|
@@ -487,19 +489,28 @@ def generate_smart_validation_template(
|
|
|
487
489
|
target_variable: Target variable name
|
|
488
490
|
context: Context message list with role and content fields
|
|
489
491
|
interaction_question: Question text from interaction block
|
|
492
|
+
buttons: Button options list with display and value fields
|
|
490
493
|
|
|
491
494
|
Returns:
|
|
492
495
|
Generated validation template
|
|
493
496
|
"""
|
|
494
497
|
# Build context information
|
|
495
498
|
context_info = ""
|
|
496
|
-
if interaction_question or context:
|
|
499
|
+
if interaction_question or context or buttons:
|
|
497
500
|
context_parts = []
|
|
498
501
|
|
|
499
502
|
# Add question information (most important, put first)
|
|
500
503
|
if interaction_question:
|
|
501
504
|
context_parts.append(CONTEXT_QUESTION_TEMPLATE.format(question=interaction_question))
|
|
502
505
|
|
|
506
|
+
# Add button options information
|
|
507
|
+
if buttons:
|
|
508
|
+
button_displays = [btn.get("display", "") for btn in buttons if btn.get("display")]
|
|
509
|
+
if button_displays:
|
|
510
|
+
button_options_str = ", ".join(button_displays)
|
|
511
|
+
button_info = CONTEXT_BUTTON_OPTIONS_TEMPLATE.format(button_options=button_options_str)
|
|
512
|
+
context_parts.append(button_info)
|
|
513
|
+
|
|
503
514
|
# Add conversation context
|
|
504
515
|
if context:
|
|
505
516
|
for msg in context:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown-flow
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.23
|
|
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
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
增强的固定输出测试框架
|
|
3
3
|
|
|
4
4
|
使用方法:
|
|
5
5
|
1. 修改 document 变量,写入你的 MarkdownFlow 文档
|
|
6
6
|
2. 修改 block_index,指定要测试的块索引
|
|
7
7
|
3. 修改 variables,设置变量值(如果需要)
|
|
8
|
-
4.
|
|
8
|
+
4. 修改 context,添加历史对话(如果需要)
|
|
9
|
+
5. 修改 max_context_length,控制 context 长度(如果需要)
|
|
10
|
+
6. 运行测试,查看输出
|
|
9
11
|
|
|
10
12
|
测试重点:
|
|
11
13
|
- 检查 XML 标记 <preserve_or_translate> 是否正确使用
|
|
12
14
|
- 检查 system 消息中是否包含约束提示词
|
|
13
15
|
- 检查 user 消息中是否不包含约束提示词
|
|
14
16
|
- 检查 LLM 输出是否不包含 XML 标记
|
|
17
|
+
- 检查 context 是否正确合并到 messages 中
|
|
18
|
+
- 检查 max_context_length 是否正确截断 context
|
|
19
|
+
- 检查变量替换是否正确
|
|
15
20
|
"""
|
|
16
21
|
|
|
17
22
|
import os
|
|
@@ -57,7 +62,25 @@ def test_preserved_output():
|
|
|
57
62
|
block_index = 4
|
|
58
63
|
|
|
59
64
|
# 变量(如果需要)
|
|
60
|
-
|
|
65
|
+
# 支持字符串或字符串列表
|
|
66
|
+
variables: dict[str, str | list[str]] = {
|
|
67
|
+
"mbti": "INFP", # 单个值
|
|
68
|
+
"攻略": "热恋", # 单个值
|
|
69
|
+
# "skills": ["Python", "JavaScript"], # 多选值示例
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# 历史对话 context(如果需要)
|
|
73
|
+
# Context 会被自动合并到 messages 中,插入到 system 消息之后、当前 user 消息之前
|
|
74
|
+
context: list[dict[str, str]] | None = [
|
|
75
|
+
{"role": "user", "content": "你好,我是 INFP 类型的人"},
|
|
76
|
+
{"role": "assistant", "content": "你好!INFP 通常充满创造力和理想主义,很高兴认识你!"},
|
|
77
|
+
{"role": "user", "content": "我想了解七夕约会的建议"},
|
|
78
|
+
{"role": "assistant", "content": "太好了!七夕是个浪漫的节日,我会为你量身定制约会攻略。"},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
# Context 长度控制(0 = 不限制)
|
|
82
|
+
# 如果 context 太长,可以设置这个参数只保留最近 N 条消息
|
|
83
|
+
max_context_length: int = 0 # 0 表示不限制,可以设为 5、10 等
|
|
61
84
|
|
|
62
85
|
# 文档提示词(如果需要)
|
|
63
86
|
document_prompt: str | None = """你扮演七夕的月老,让这一天的天下有情人都能甜蜜约会,永浴爱河。
|
|
@@ -78,11 +101,20 @@ def test_preserved_output():
|
|
|
78
101
|
try:
|
|
79
102
|
llm_provider = create_llm_provider()
|
|
80
103
|
|
|
81
|
-
#
|
|
104
|
+
# 打印测试配置
|
|
105
|
+
print("\n📋 测试配置")
|
|
106
|
+
print("-" * 60)
|
|
107
|
+
print(f"Block Index: {block_index}")
|
|
108
|
+
print(f"Variables: {variables if variables else '无'}")
|
|
109
|
+
print(f"Context: {len(context) if context else 0} 条历史消息")
|
|
110
|
+
print(f"Max Context Length: {max_context_length} {'(不限制)' if max_context_length == 0 else f'(最多保留 {max_context_length} 条)'}")
|
|
111
|
+
|
|
112
|
+
# 创建 MarkdownFlow 实例(添加 max_context_length 参数)
|
|
82
113
|
mf = MarkdownFlow(
|
|
83
114
|
document,
|
|
84
115
|
llm_provider=llm_provider,
|
|
85
116
|
document_prompt=document_prompt if document_prompt else None,
|
|
117
|
+
max_context_length=max_context_length,
|
|
86
118
|
)
|
|
87
119
|
|
|
88
120
|
# 测试 PROMPT_ONLY 模式 - 查看消息结构
|
|
@@ -92,6 +124,7 @@ def test_preserved_output():
|
|
|
92
124
|
result_prompt_raw = mf.process(
|
|
93
125
|
block_index=block_index,
|
|
94
126
|
mode=ProcessMode.PROMPT_ONLY,
|
|
127
|
+
context=context if context else None,
|
|
95
128
|
variables=variables if variables else None,
|
|
96
129
|
)
|
|
97
130
|
|
|
@@ -102,7 +135,19 @@ def test_preserved_output():
|
|
|
102
135
|
# 打印消息结构
|
|
103
136
|
if result_prompt.metadata and "messages" in result_prompt.metadata:
|
|
104
137
|
messages = result_prompt.metadata["messages"]
|
|
105
|
-
print(f"\n消息数量: {len(messages)}
|
|
138
|
+
print(f"\n消息数量: {len(messages)}")
|
|
139
|
+
|
|
140
|
+
# 检查 context 是否被正确合并
|
|
141
|
+
if context:
|
|
142
|
+
expected_context_count = min(len(context), max_context_length) if max_context_length > 0 else len(context)
|
|
143
|
+
context_messages = [m for m in messages if m.get("role") in ["user", "assistant"] and m != messages[-1]]
|
|
144
|
+
actual_context_count = len(context_messages)
|
|
145
|
+
print(f"Context 消息: {actual_context_count} 条 (预期: {expected_context_count} 条)")
|
|
146
|
+
if actual_context_count == expected_context_count:
|
|
147
|
+
print("✅ Context 正确合并到 messages")
|
|
148
|
+
else:
|
|
149
|
+
print(f"⚠️ Context 数量不匹配")
|
|
150
|
+
print()
|
|
106
151
|
|
|
107
152
|
for i, msg in enumerate(messages, 1):
|
|
108
153
|
role = msg.get("role", "")
|
|
@@ -125,6 +170,19 @@ def test_preserved_output():
|
|
|
125
170
|
print(f"✅ user 包含 XML 标记: {has_xml_tag}")
|
|
126
171
|
print(f"❌ user 不应包含说明(应在system): {not has_explanation}")
|
|
127
172
|
|
|
173
|
+
# 检查变量是否被正确替换
|
|
174
|
+
if variables:
|
|
175
|
+
replaced_vars = []
|
|
176
|
+
for var_name, var_value in variables.items():
|
|
177
|
+
if isinstance(var_value, list):
|
|
178
|
+
var_str = ", ".join(var_value)
|
|
179
|
+
else:
|
|
180
|
+
var_str = var_value
|
|
181
|
+
if var_str in content:
|
|
182
|
+
replaced_vars.append(f"{var_name}={var_str}")
|
|
183
|
+
if replaced_vars:
|
|
184
|
+
print(f"✅ 变量已替换: {', '.join(replaced_vars)}")
|
|
185
|
+
|
|
128
186
|
print()
|
|
129
187
|
|
|
130
188
|
# 测试 COMPLETE 模式 - 查看 LLM 输出
|
|
@@ -134,6 +192,7 @@ def test_preserved_output():
|
|
|
134
192
|
result_complete_raw = mf.process(
|
|
135
193
|
block_index=block_index,
|
|
136
194
|
mode=ProcessMode.COMPLETE,
|
|
195
|
+
context=context if context else None,
|
|
137
196
|
variables=variables if variables else None,
|
|
138
197
|
)
|
|
139
198
|
|
|
@@ -157,6 +216,41 @@ def test_preserved_output():
|
|
|
157
216
|
if usage:
|
|
158
217
|
print(f"📊 Token 使用: {usage.get('total_tokens', 0)} tokens")
|
|
159
218
|
|
|
219
|
+
# 测试总结
|
|
220
|
+
print("\n" + "=" * 60)
|
|
221
|
+
print("📊 测试总结")
|
|
222
|
+
print("=" * 60)
|
|
223
|
+
test_results = []
|
|
224
|
+
|
|
225
|
+
# 检查变量替换
|
|
226
|
+
if variables:
|
|
227
|
+
for var_name, var_value in variables.items():
|
|
228
|
+
var_str = ", ".join(var_value) if isinstance(var_value, list) else var_value
|
|
229
|
+
if var_str in result_complete.content or "{{" + var_name + "}}" not in document:
|
|
230
|
+
test_results.append(f"✅ 变量 '{var_name}' 已正确处理")
|
|
231
|
+
else:
|
|
232
|
+
test_results.append(f"❌ 变量 '{var_name}' 未被替换")
|
|
233
|
+
|
|
234
|
+
# 检查 context
|
|
235
|
+
if context:
|
|
236
|
+
if max_context_length > 0:
|
|
237
|
+
test_results.append(f"✅ Context 长度控制: {max_context_length} 条")
|
|
238
|
+
else:
|
|
239
|
+
test_results.append(f"✅ Context 全部保留: {len(context)} 条")
|
|
240
|
+
|
|
241
|
+
# 检查 XML 标记
|
|
242
|
+
if not has_xml_in_output:
|
|
243
|
+
test_results.append("✅ LLM 输出不包含 XML 标记")
|
|
244
|
+
else:
|
|
245
|
+
test_results.append("❌ LLM 输出包含 XML 标记(应该被过滤)")
|
|
246
|
+
|
|
247
|
+
for result in test_results:
|
|
248
|
+
print(result)
|
|
249
|
+
|
|
250
|
+
print("\n" + "=" * 60)
|
|
251
|
+
print("✨ 测试完成!")
|
|
252
|
+
print("=" * 60)
|
|
253
|
+
|
|
160
254
|
except Exception as e:
|
|
161
255
|
print(f"\n❌ 测试失败: {e}")
|
|
162
256
|
import traceback
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|