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.
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/PKG-INFO +1 -1
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/__init__.py +1 -1
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/constants.py +52 -22
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/core.py +256 -36
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/llm.py +4 -2
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/utils.py +12 -1
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/PKG-INFO +1 -1
- markdown_flow-0.2.23/tests/test_preserved_simple.py +262 -0
- markdown_flow-0.2.18/tests/test_preserved_simple.py +0 -170
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/LICENSE +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/README.md +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/enums.py +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/exceptions.py +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow/models.py +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/SOURCES.txt +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/dependency_links.txt +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/markdown_flow.egg-info/top_level.txt +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/pyproject.toml +0 -0
- {markdown_flow-0.2.18 → markdown_flow-0.2.23}/setup.cfg +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
示例2 -
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
示例3 -
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
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
|
-
#
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|