markdown-flow 0.2.18__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.

Files changed (20) hide show
  1. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/PKG-INFO +1 -1
  2. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/__init__.py +1 -1
  3. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/constants.py +52 -22
  4. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/core.py +256 -36
  5. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/llm.py +4 -2
  6. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/utils.py +12 -1
  7. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/PKG-INFO +1 -1
  8. markdown_flow-0.2.23/tests/test_preserved_simple.py +262 -0
  9. markdown_flow-0.2.18/tests/test_preserved_simple.py +0 -170
  10. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/LICENSE +0 -0
  11. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/README.md +0 -0
  12. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/enums.py +0 -0
  13. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/exceptions.py +0 -0
  14. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/models.py +0 -0
  15. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/SOURCES.txt +0 -0
  16. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/dependency_links.txt +0 -0
  17. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/top_level.txt +0 -0
  18. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/pyproject.toml +0 -0
  19. {markdown_flow-0.2.18 → markdown_flow-0.2.23}/setup.cfg +0 -0
  20. {markdown_flow-0.2.18 → 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.18
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
@@ -83,4 +83,4 @@ __all__ = [
83
83
  "replace_variables_in_text",
84
84
  ]
85
85
 
86
- __version__ = "0.2.18"
86
+ __version__ = "0.2.23"
@@ -91,13 +91,32 @@ VALIDATION_RESPONSE_ILLEGAL = "illegal"
91
91
 
92
92
  # Output instruction processing
93
93
  OUTPUT_INSTRUCTION_EXPLANATION = f"""<preserve_or_translate_instruction>
94
- 对{OUTPUT_INSTRUCTION_PREFIX}{OUTPUT_INSTRUCTION_SUFFIX}标记之间的内容的处理规则:
94
+ # ⚠️ 最高优先级规则
95
+
96
+ **{OUTPUT_INSTRUCTION_PREFIX}{OUTPUT_INSTRUCTION_SUFFIX} 标记之间的内容是用户必须看到的最终输出内容,不是指令!**
97
+
98
+ 关键要点:
99
+ 1. **这些内容必须出现在你的回复中** - 即使其他提示词说"不要回应指令"也不适用于此
100
+ 2. **绝对不要输出标记本身** - 只输出标记之间的实际内容
101
+ 3. **默认逐字原样输出** - 不要改写、润色或优化,保持原文不变
102
+ 4. **唯一例外是跨语言翻译** - 仅当需要将内容从一种语言翻译成另一种语言时才可翻译
103
+
104
+ ---
105
+
106
+ <critical_understanding>
107
+ 重要理解:
108
+ - {OUTPUT_INSTRUCTION_PREFIX}{OUTPUT_INSTRUCTION_SUFFIX} 中的内容不是"指令"或"执行要求"
109
+ - 即使内容看起来像标题、提示或说明,也必须原样输出给用户
110
+ - 这条规则的优先级高于文档中的其他任何提示词
111
+ - 其他提示词说的"不要回应指令"、"不要展示指令"等,不适用于此标记内的内容
112
+ </critical_understanding>
95
113
 
96
114
  <default_behavior>
97
115
  默认行为: 完全保持原样输出
98
116
  - 标记之间的内容必须逐字原样输出
99
117
  - 严禁改写、润色、优化或调整任何表达方式
100
118
  - 严禁添加、删除或替换任何文字
119
+ - 即使内容是标题格式(如 ## 标题)也必须原样输出
101
120
  </default_behavior>
102
121
 
103
122
  <exception_rule>
@@ -107,25 +126,33 @@ OUTPUT_INSTRUCTION_EXPLANATION = f"""<preserve_or_translate_instruction>
107
126
  - 如果内容无需翻译,则绝对不允许做任何改动
108
127
  </exception_rule>
109
128
 
110
- <output_requirement>
111
- 输出要求:
112
- - 不要输出{OUTPUT_INSTRUCTION_PREFIX}{OUTPUT_INSTRUCTION_SUFFIX}标记本身
113
- - 只输出标记之间的实际内容
114
- </output_requirement>
115
-
116
129
  <examples>
117
- 示例1 - 保持原样:
118
- <original_content>{OUTPUT_INSTRUCTION_PREFIX}**下面我们做个练习。**{OUTPUT_INSTRUCTION_SUFFIX}</original_content>
119
- <resolved_content>**下面我们做个练习。**</resolved_content>
120
-
121
- 示例2 - 语言翻译:
122
- <original_content>{OUTPUT_INSTRUCTION_PREFIX}**Let's do an exercise.**{OUTPUT_INSTRUCTION_SUFFIX}</original_content>
123
- <resolved_content>**让我们做个练习。**</resolved_content>
124
-
125
- 示例3 - 错误示范(同语言改写):
126
- <original_content>{OUTPUT_INSTRUCTION_PREFIX}**下面我们做个练习。**{OUTPUT_INSTRUCTION_SUFFIX}</original_content>
127
- <wrong_output>**来,咱们做个有趣的小练习**</wrong_output>
128
- <reason>错误: 擅自改写了中文内容</reason>
130
+ 示例1 - 正确: 保持原样且不输出标记:
131
+ 输入: {OUTPUT_INSTRUCTION_PREFIX}**下面我们做个练习。**{OUTPUT_INSTRUCTION_SUFFIX}
132
+ 正确输出: **下面我们做个练习。**
133
+
134
+ 示例2 - 正确: 标题也要原样输出:
135
+ 输入: {OUTPUT_INSTRUCTION_PREFIX}## 专属指南 for 用户{OUTPUT_INSTRUCTION_SUFFIX}
136
+ 正确输出: ## 专属指南 for 用户
137
+
138
+ 示例3 - 正确: 语言翻译且不输出标记:
139
+ 输入: {OUTPUT_INSTRUCTION_PREFIX}**Let's do an exercise.**{OUTPUT_INSTRUCTION_SUFFIX}
140
+ 正确输出: **让我们做个练习。**
141
+
142
+ ❌ 示例4 - 错误: 输出了XML标记:
143
+ 输入: {OUTPUT_INSTRUCTION_PREFIX}## 标题内容{OUTPUT_INSTRUCTION_SUFFIX}
144
+ 错误输出: {OUTPUT_INSTRUCTION_PREFIX}## 标题内容{OUTPUT_INSTRUCTION_SUFFIX}
145
+ 错误原因: 不应该输出标记本身!
146
+
147
+ ❌ 示例5 - 错误: 同语言改写:
148
+ 输入: {OUTPUT_INSTRUCTION_PREFIX}**下面我们做个练习。**{OUTPUT_INSTRUCTION_SUFFIX}
149
+ 错误输出: **来,咱们做个有趣的小练习**
150
+ 错误原因: 擅自改写了中文内容
151
+
152
+ ❌ 示例6 - 错误: 没有输出固定内容:
153
+ 输入: {OUTPUT_INSTRUCTION_PREFIX}## 攻略|专属指南{OUTPUT_INSTRUCTION_SUFFIX}
154
+ 错误输出: (什么都不输出,或者跳过这部分)
155
+ 错误原因: 必须输出标记之间的内容!
129
156
  </examples>
130
157
  </preserve_or_translate_instruction>
131
158
 
@@ -145,9 +172,10 @@ SMART_VALIDATION_TEMPLATE = """# 任务
145
172
  # 提取要求
146
173
  1. 仔细阅读上述相关问题,理解这个问题想要获取什么信息
147
174
  2. 从用户回答中提取与该问题相关的信息
148
- 3. 对于昵称/姓名类问题,任何非空的合理字符串(包括简短的如"ee"、"aa"、"007"等)都应该接受
149
- 4. 只有当用户回答完全无关、包含不当内容或明显不合理时才标记为不合法
150
- 5. 确保提取的信息准确、完整且符合预期格式"""
175
+ 3. 如果提供了预定义选项,用户选择这些选项时都应该接受;自定义输入应与选项主题相关
176
+ 4. 对于昵称/姓名类问题,任何非空的合理字符串(包括简短的如"ee"、"aa"、"007"等)都应该接受
177
+ 5. 只有当用户回答完全无关、包含不当内容或明显不合理时才标记为不合法
178
+ 6. 确保提取的信息准确、完整且符合预期格式"""
151
179
 
152
180
  # Validation template for buttons with text input
153
181
  BUTTONS_WITH_TEXT_VALIDATION_TEMPLATE = """用户针对以下问题进行了输入:
@@ -193,7 +221,9 @@ VARIABLE_DEFAULT_VALUE = "UNKNOWN"
193
221
  # Context generation constants
194
222
  CONTEXT_QUESTION_MARKER = "# 相关问题"
195
223
  CONTEXT_CONVERSATION_MARKER = "# 对话上下文"
224
+ CONTEXT_BUTTON_OPTIONS_MARKER = "## 预定义选项"
196
225
 
197
226
  # Context generation templates
198
227
  CONTEXT_QUESTION_TEMPLATE = f"{CONTEXT_QUESTION_MARKER}\n{{question}}"
199
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
- # Build messages
235
- messages = self._build_content_messages(block_index, variables)
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(self, block_index: int, mode: ProcessMode, variables: dict[str, str | list[str]] | None = None):
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
- # Build render messages
287
- messages = self._build_interaction_render_messages(question_text)
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
- # All button types: validate user input against available buttons
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
- # For text-only inputs, directly use the target variable values
539
+ # Use LLM validation to check if input is relevant to the question
407
540
  if target_values:
408
- return LLMResult(
409
- content="",
410
- variables={target_variable: target_values},
411
- metadata={
412
- "interaction_type": "text_only",
413
- "target_variable": target_variable,
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(self, error_message: str, mode: ProcessMode) -> LLMResult | Generator[LLMResult, None, None]:
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
- messages = self._build_error_render_messages(error_message)
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
- # For most content blocks, historical conversation context is not needed
675
- # because each document block is an independent instruction
676
- # If future specific scenarios need context, logic can be added here
677
- # if context:
678
- # messages.extend(context)
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(self, question_text: str) -> list[dict[str, str]]:
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(self, block_index: int, user_input: dict[str, list[str]], target_variable: str) -> list[dict[str, str]]:
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
- # Generate smart validation template
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=None, # Could consider passing context here
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(self, error_message: str) -> list[dict[str, str]]:
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.18
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
@@ -0,0 +1,262 @@
1
+ """
2
+ 增强的固定输出测试框架
3
+
4
+ 使用方法:
5
+ 1. 修改 document 变量,写入你的 MarkdownFlow 文档
6
+ 2. 修改 block_index,指定要测试的块索引
7
+ 3. 修改 variables,设置变量值(如果需要)
8
+ 4. 修改 context,添加历史对话(如果需要)
9
+ 5. 修改 max_context_length,控制 context 长度(如果需要)
10
+ 6. 运行测试,查看输出
11
+
12
+ 测试重点:
13
+ - 检查 XML 标记 <preserve_or_translate> 是否正确使用
14
+ - 检查 system 消息中是否包含约束提示词
15
+ - 检查 user 消息中是否不包含约束提示词
16
+ - 检查 LLM 输出是否不包含 XML 标记
17
+ - 检查 context 是否正确合并到 messages 中
18
+ - 检查 max_context_length 是否正确截断 context
19
+ - 检查变量替换是否正确
20
+ """
21
+
22
+ import os
23
+ import sys
24
+
25
+
26
+ # 添加项目路径
27
+ project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28
+ sys.path.insert(0, project_root)
29
+
30
+ from llm import create_llm_provider # noqa: E402
31
+
32
+ from markdown_flow import MarkdownFlow, ProcessMode # noqa: E402
33
+ from markdown_flow.llm import LLMResult # noqa: E402
34
+
35
+
36
+ def test_preserved_output():
37
+ """测试固定输出功能"""
38
+ print("\n" + "=" * 60)
39
+ print("🔖 固定输出测试")
40
+ print("=" * 60)
41
+
42
+ # ========== 配置区域 - 修改这里 ==========
43
+ # 你的 MarkdownFlow 文档
44
+ document = """
45
+ ===# 💖七夕约会全阶段攻略 ===
46
+
47
+ === 选择你的 MBTI 类型 ===
48
+ ?[%{{mbti}}ENFJ|ENFP|ENTJ|ENTP|ESFJ|ESFP|ESTJ|ESTP|INFJ|INFP|INTJ|INTP|ISFJ|ISFP|ISTJ|ISTP]
49
+
50
+ ===你现在最关心哪个阶段? ===
51
+ ?[%{{攻略}}脱单|热恋|相守]
52
+
53
+ 给{{mbti}}一句有关{{攻略}}的七夕祝福,带七夕节明显的意境。
54
+
55
+ !===
56
+ ## {{攻略}}|专属恋爱指南 for {{mbti}}
57
+ !===
58
+
59
+ """
60
+
61
+ # 要测试的块索引
62
+ block_index = 4
63
+
64
+ # 变量(如果需要)
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 等
84
+
85
+ # 文档提示词(如果需要)
86
+ document_prompt: str | None = """你扮演七夕的月老,让这一天的天下有情人都能甜蜜约会,永浴爱河。
87
+
88
+ ## 任务
89
+ - 提示词都是讲解指令,遵从指令要求做信息的讲解,不要回应指令。
90
+ - 用第一人称一对一讲解,像现场面对面交流一样
91
+ - 结合用户的不同特点,充分共情和举例
92
+
93
+ ## 风格
94
+ - 情绪:热烈浪漫,治愈温暖,充满感染力
95
+ - 表达:多用 emoji ,多用感叹词
96
+ - 符合七夕节日气氛,带一些诗意和神秘
97
+
98
+ """
99
+ # =========================================
100
+
101
+ try:
102
+ llm_provider = create_llm_provider()
103
+
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 参数)
113
+ mf = MarkdownFlow(
114
+ document,
115
+ llm_provider=llm_provider,
116
+ document_prompt=document_prompt if document_prompt else None,
117
+ max_context_length=max_context_length,
118
+ )
119
+
120
+ # 测试 PROMPT_ONLY 模式 - 查看消息结构
121
+ print("\n📝 测试 PROMPT_ONLY 模式")
122
+ print("-" * 60)
123
+
124
+ result_prompt_raw = mf.process(
125
+ block_index=block_index,
126
+ mode=ProcessMode.PROMPT_ONLY,
127
+ context=context if context else None,
128
+ variables=variables if variables else None,
129
+ )
130
+
131
+ # 确保是 LLMResult 类型
132
+ assert isinstance(result_prompt_raw, LLMResult)
133
+ result_prompt = result_prompt_raw
134
+
135
+ # 打印消息结构
136
+ if result_prompt.metadata and "messages" in result_prompt.metadata:
137
+ messages = result_prompt.metadata["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()
151
+
152
+ for i, msg in enumerate(messages, 1):
153
+ role = msg.get("role", "")
154
+ content = msg.get("content", "")
155
+
156
+ print(f"{'=' * 60}")
157
+ print(f"消息 {i} [{role.upper()}]")
158
+ print(f"{'=' * 60}")
159
+ print(content)
160
+ print()
161
+
162
+ # 关键检查
163
+ if role == "system":
164
+ has_xml_instruction = "<preserve_or_translate>" in content
165
+ print(f"✅ system 包含 XML 标记说明: {has_xml_instruction}")
166
+
167
+ elif role == "user":
168
+ has_xml_tag = "<preserve_or_translate>" in content
169
+ has_explanation = "不要输出<preserve_or_translate>" in content
170
+ print(f"✅ user 包含 XML 标记: {has_xml_tag}")
171
+ print(f"❌ user 不应包含说明(应在system): {not has_explanation}")
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
+
186
+ print()
187
+
188
+ # 测试 COMPLETE 模式 - 查看 LLM 输出
189
+ print("\n📝 测试 COMPLETE 模式")
190
+ print("-" * 60)
191
+
192
+ result_complete_raw = mf.process(
193
+ block_index=block_index,
194
+ mode=ProcessMode.COMPLETE,
195
+ context=context if context else None,
196
+ variables=variables if variables else None,
197
+ )
198
+
199
+ # 确保是 LLMResult 类型
200
+ assert isinstance(result_complete_raw, LLMResult)
201
+ result_complete = result_complete_raw
202
+
203
+ print("\n" + "=" * 60)
204
+ print("LLM 输出结果")
205
+ print("=" * 60)
206
+ print(result_complete.content)
207
+ print("=" * 60)
208
+
209
+ # 输出检查
210
+ has_xml_in_output = "<preserve_or_translate>" in result_complete.content
211
+ print(f"\n✅ 输出不包含 XML 标记: {not has_xml_in_output}")
212
+
213
+ # 使用统计
214
+ if result_complete.metadata and "usage" in result_complete.metadata:
215
+ usage = result_complete.metadata["usage"]
216
+ if usage:
217
+ print(f"📊 Token 使用: {usage.get('total_tokens', 0)} tokens")
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
+
254
+ except Exception as e:
255
+ print(f"\n❌ 测试失败: {e}")
256
+ import traceback
257
+
258
+ traceback.print_exc()
259
+
260
+
261
+ if __name__ == "__main__":
262
+ test_preserved_output()
@@ -1,170 +0,0 @@
1
- """
2
- 简单的固定输出测试框架
3
-
4
- 使用方法:
5
- 1. 修改 document 变量,写入你的 MarkdownFlow 文档
6
- 2. 修改 block_index,指定要测试的块索引
7
- 3. 修改 variables,设置变量值(如果需要)
8
- 4. 运行测试,查看输出
9
-
10
- 测试重点:
11
- - 检查 XML 标记 <preserve_or_translate> 是否正确使用
12
- - 检查 system 消息中是否包含约束提示词
13
- - 检查 user 消息中是否不包含约束提示词
14
- - 检查 LLM 输出是否不包含 XML 标记
15
- """
16
-
17
- import os
18
- import sys
19
-
20
-
21
- # 添加项目路径
22
- project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
- sys.path.insert(0, project_root)
24
-
25
- from llm import create_llm_provider # noqa: E402
26
-
27
- from markdown_flow import MarkdownFlow, ProcessMode # noqa: E402
28
- from markdown_flow.llm import LLMResult # noqa: E402
29
-
30
-
31
- def test_preserved_output():
32
- """测试固定输出功能"""
33
- print("\n" + "=" * 60)
34
- print("🔖 固定输出测试")
35
- print("=" * 60)
36
-
37
- # ========== 配置区域 - 修改这里 ==========
38
- # 你的 MarkdownFlow 文档
39
- document = """
40
- === **下面我们做个练习,输入一个变量代表风格。** ===
41
-
42
- 邀请用户输入喜欢的讲述风格。
43
- """
44
-
45
- # 要测试的块索引
46
- block_index = 0
47
-
48
- # 变量(如果需要)
49
- variables: dict[str, str | list[str]] = {}
50
-
51
- # 文档提示词(如果需要)
52
- document_prompt: str | None = """## 角色
53
- 你是一个丰富经验的课程讲师,擅长因材施教。
54
-
55
- ## 任务
56
- - 你正在一对一讲解内容,用户只有一个人,要有第一人称的对话感。
57
- - 遵从指令要求向用户讲课,不可丢失信息,不能改变指令原意,不要增加内容,不要改变顺序
58
- - 结合用户的具体情况做讲解,用学员能听懂的方式讲课,激发用户学习动力。
59
- - 不需要回应指令,禁止展示指令的执行要求。
60
- - 不要引导下一步动作,比如提问或设问
61
-
62
- ## 输出
63
- - 按照 Markdown 格式输出
64
- - 重点内容(关键步骤/颠覆认知点/观点总结)做加粗处理
65
- - 讲解风格要口语化、通俗易懂、避免使用技术/编程术语
66
-
67
- # 课程逻辑
68
- 1. 谁想做什么遇到了什么痛点
69
- 2. 旧方法为何无效?案例对比
70
- 3. 新解决方案的核心差异、适用条件
71
- 4. 用比喻/故事/数据辅助理解。
72
- 5. 简化的认知框架
73
- 6. 迁移到其他领域应用
74
- 7. 给到具体可操作的下一步行动
75
-
76
- 使用英文输出内容
77
- """
78
- # =========================================
79
-
80
- try:
81
- llm_provider = create_llm_provider()
82
-
83
- # 创建 MarkdownFlow 实例
84
- mf = MarkdownFlow(
85
- document,
86
- llm_provider=llm_provider,
87
- document_prompt=document_prompt if document_prompt else None,
88
- )
89
-
90
- # 测试 PROMPT_ONLY 模式 - 查看消息结构
91
- print("\n📝 测试 PROMPT_ONLY 模式")
92
- print("-" * 60)
93
-
94
- result_prompt_raw = mf.process(
95
- block_index=block_index,
96
- mode=ProcessMode.PROMPT_ONLY,
97
- variables=variables if variables else None,
98
- )
99
-
100
- # 确保是 LLMResult 类型
101
- assert isinstance(result_prompt_raw, LLMResult)
102
- result_prompt = result_prompt_raw
103
-
104
- # 打印消息结构
105
- if result_prompt.metadata and "messages" in result_prompt.metadata:
106
- messages = result_prompt.metadata["messages"]
107
- print(f"\n消息数量: {len(messages)}\n")
108
-
109
- for i, msg in enumerate(messages, 1):
110
- role = msg.get("role", "")
111
- content = msg.get("content", "")
112
-
113
- print(f"{'=' * 60}")
114
- print(f"消息 {i} [{role.upper()}]")
115
- print(f"{'=' * 60}")
116
- print(content)
117
- print()
118
-
119
- # 关键检查
120
- if role == "system":
121
- has_xml_instruction = "<preserve_or_translate>" in content
122
- print(f"✅ system 包含 XML 标记说明: {has_xml_instruction}")
123
-
124
- elif role == "user":
125
- has_xml_tag = "<preserve_or_translate>" in content
126
- has_explanation = "不要输出<preserve_or_translate>" in content
127
- print(f"✅ user 包含 XML 标记: {has_xml_tag}")
128
- print(f"❌ user 不应包含说明(应在system): {not has_explanation}")
129
-
130
- print()
131
-
132
- # 测试 COMPLETE 模式 - 查看 LLM 输出
133
- print("\n📝 测试 COMPLETE 模式")
134
- print("-" * 60)
135
-
136
- result_complete_raw = mf.process(
137
- block_index=block_index,
138
- mode=ProcessMode.COMPLETE,
139
- variables=variables if variables else None,
140
- )
141
-
142
- # 确保是 LLMResult 类型
143
- assert isinstance(result_complete_raw, LLMResult)
144
- result_complete = result_complete_raw
145
-
146
- print("\n" + "=" * 60)
147
- print("LLM 输出结果")
148
- print("=" * 60)
149
- print(result_complete.content)
150
- print("=" * 60)
151
-
152
- # 输出检查
153
- has_xml_in_output = "<preserve_or_translate>" in result_complete.content
154
- print(f"\n✅ 输出不包含 XML 标记: {not has_xml_in_output}")
155
-
156
- # 使用统计
157
- if result_complete.metadata and "usage" in result_complete.metadata:
158
- usage = result_complete.metadata["usage"]
159
- if usage:
160
- print(f"📊 Token 使用: {usage.get('total_tokens', 0)} tokens")
161
-
162
- except Exception as e:
163
- print(f"\n❌ 测试失败: {e}")
164
- import traceback
165
-
166
- traceback.print_exc()
167
-
168
-
169
- if __name__ == "__main__":
170
- test_preserved_output()
File without changes
File without changes
File without changes