markdown-flow 0.2.10__py3-none-any.whl → 0.2.30__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markdown_flow/__init__.py +7 -7
- markdown_flow/constants.py +212 -49
- markdown_flow/core.py +614 -591
- markdown_flow/llm.py +10 -12
- markdown_flow/models.py +1 -17
- markdown_flow/parser/__init__.py +38 -0
- markdown_flow/parser/code_fence_utils.py +190 -0
- markdown_flow/parser/interaction.py +354 -0
- markdown_flow/parser/json_parser.py +50 -0
- markdown_flow/parser/output.py +215 -0
- markdown_flow/parser/preprocessor.py +151 -0
- markdown_flow/parser/validation.py +100 -0
- markdown_flow/parser/variable.py +95 -0
- markdown_flow/providers/__init__.py +16 -0
- markdown_flow/providers/config.py +46 -0
- markdown_flow/providers/openai.py +369 -0
- markdown_flow/utils.py +49 -51
- {markdown_flow-0.2.10.dist-info → markdown_flow-0.2.30.dist-info}/METADATA +18 -107
- markdown_flow-0.2.30.dist-info/RECORD +24 -0
- markdown_flow-0.2.10.dist-info/RECORD +0 -13
- {markdown_flow-0.2.10.dist-info → markdown_flow-0.2.30.dist-info}/WHEEL +0 -0
- {markdown_flow-0.2.10.dist-info → markdown_flow-0.2.30.dist-info}/licenses/LICENSE +0 -0
- {markdown_flow-0.2.10.dist-info → markdown_flow-0.2.30.dist-info}/top_level.txt +0 -0
markdown_flow/core.py
CHANGED
|
@@ -13,31 +13,30 @@ from typing import Any
|
|
|
13
13
|
from .constants import (
|
|
14
14
|
BLOCK_INDEX_OUT_OF_RANGE_ERROR,
|
|
15
15
|
BLOCK_SEPARATOR,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
COMPILED_VARIABLE_REFERENCE_CLEANUP_REGEX,
|
|
20
|
-
COMPILED_WHITESPACE_CLEANUP_REGEX,
|
|
16
|
+
CONTEXT_BUTTON_OPTIONS_TEMPLATE,
|
|
17
|
+
CONTEXT_QUESTION_TEMPLATE,
|
|
18
|
+
DEFAULT_BASE_SYSTEM_PROMPT,
|
|
21
19
|
DEFAULT_INTERACTION_ERROR_PROMPT,
|
|
22
20
|
DEFAULT_INTERACTION_PROMPT,
|
|
23
|
-
DEFAULT_VALIDATION_SYSTEM_MESSAGE,
|
|
24
21
|
INPUT_EMPTY_ERROR,
|
|
25
22
|
INTERACTION_ERROR_RENDER_INSTRUCTIONS,
|
|
26
23
|
INTERACTION_PARSE_ERROR,
|
|
27
24
|
INTERACTION_PATTERN_NON_CAPTURING,
|
|
28
25
|
INTERACTION_PATTERN_SPLIT,
|
|
29
|
-
INTERACTION_RENDER_INSTRUCTIONS,
|
|
30
26
|
LLM_PROVIDER_REQUIRED_ERROR,
|
|
27
|
+
OUTPUT_INSTRUCTION_EXPLANATION,
|
|
31
28
|
UNSUPPORTED_PROMPT_TYPE_ERROR,
|
|
29
|
+
VALIDATION_REQUIREMENTS_TEMPLATE,
|
|
30
|
+
VALIDATION_TASK_TEMPLATE,
|
|
32
31
|
)
|
|
33
32
|
from .enums import BlockType
|
|
34
33
|
from .exceptions import BlockIndexError
|
|
35
34
|
from .llm import LLMProvider, LLMResult, ProcessMode
|
|
36
|
-
from .models import Block
|
|
37
|
-
from .
|
|
35
|
+
from .models import Block
|
|
36
|
+
from .parser import (
|
|
37
|
+
CodeBlockPreprocessor,
|
|
38
38
|
InteractionParser,
|
|
39
39
|
InteractionType,
|
|
40
|
-
extract_interaction_question,
|
|
41
40
|
extract_preserved_content,
|
|
42
41
|
extract_variables_from_text,
|
|
43
42
|
is_preserved_content_block,
|
|
@@ -56,51 +55,156 @@ class MarkdownFlow:
|
|
|
56
55
|
|
|
57
56
|
_llm_provider: LLMProvider | None
|
|
58
57
|
_document: str
|
|
58
|
+
_processed_document: str
|
|
59
59
|
_document_prompt: str | None
|
|
60
60
|
_interaction_prompt: str | None
|
|
61
61
|
_interaction_error_prompt: str | None
|
|
62
|
+
_max_context_length: int
|
|
62
63
|
_blocks: list[Block] | None
|
|
63
|
-
|
|
64
|
+
_model: str | None
|
|
65
|
+
_temperature: float | None
|
|
66
|
+
_preprocessor: CodeBlockPreprocessor
|
|
64
67
|
|
|
65
68
|
def __init__(
|
|
66
69
|
self,
|
|
67
70
|
document: str,
|
|
68
71
|
llm_provider: LLMProvider | None = None,
|
|
72
|
+
base_system_prompt: str | None = None,
|
|
69
73
|
document_prompt: str | None = None,
|
|
70
74
|
interaction_prompt: str | None = None,
|
|
71
75
|
interaction_error_prompt: str | None = None,
|
|
76
|
+
max_context_length: int = 0,
|
|
72
77
|
):
|
|
73
78
|
"""
|
|
74
79
|
Initialize MarkdownFlow instance.
|
|
75
80
|
|
|
76
81
|
Args:
|
|
77
82
|
document: Markdown document content
|
|
78
|
-
llm_provider: LLM provider
|
|
83
|
+
llm_provider: LLM provider (required for COMPLETE and STREAM modes)
|
|
84
|
+
base_system_prompt: MarkdownFlow base system prompt (framework-level, content blocks only)
|
|
79
85
|
document_prompt: Document-level system prompt
|
|
80
86
|
interaction_prompt: Interaction content rendering prompt
|
|
81
87
|
interaction_error_prompt: Interaction error rendering prompt
|
|
88
|
+
max_context_length: Maximum number of context messages to keep (0 = unlimited)
|
|
82
89
|
"""
|
|
83
90
|
self._document = document
|
|
84
91
|
self._llm_provider = llm_provider
|
|
92
|
+
self._base_system_prompt = base_system_prompt or DEFAULT_BASE_SYSTEM_PROMPT
|
|
85
93
|
self._document_prompt = document_prompt
|
|
86
94
|
self._interaction_prompt = interaction_prompt or DEFAULT_INTERACTION_PROMPT
|
|
87
95
|
self._interaction_error_prompt = interaction_error_prompt or DEFAULT_INTERACTION_ERROR_PROMPT
|
|
96
|
+
self._max_context_length = max_context_length
|
|
88
97
|
self._blocks = None
|
|
89
|
-
self.
|
|
98
|
+
self._model: str | None = None
|
|
99
|
+
self._temperature: float | None = None
|
|
100
|
+
|
|
101
|
+
# Preprocess document: extract code blocks and replace with placeholders
|
|
102
|
+
# This is done once during initialization, similar to Go implementation
|
|
103
|
+
self._preprocessor = CodeBlockPreprocessor()
|
|
104
|
+
self._processed_document = self._preprocessor.extract_code_blocks(document)
|
|
90
105
|
|
|
91
106
|
def set_llm_provider(self, provider: LLMProvider) -> None:
|
|
92
107
|
"""Set LLM provider."""
|
|
93
108
|
self._llm_provider = provider
|
|
94
109
|
|
|
110
|
+
def get_processed_document(self) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Get preprocessed document (for debugging and testing).
|
|
113
|
+
|
|
114
|
+
Returns the document content after code blocks have been replaced with placeholders.
|
|
115
|
+
|
|
116
|
+
Use cases:
|
|
117
|
+
- Verify that code block preprocessing was executed correctly
|
|
118
|
+
- Check placeholder format (__MDFLOW_CODE_BLOCK_N__)
|
|
119
|
+
- Debug preprocessing stage issues
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Preprocessed document string
|
|
123
|
+
"""
|
|
124
|
+
return self._processed_document
|
|
125
|
+
|
|
126
|
+
def get_content_messages(
|
|
127
|
+
self,
|
|
128
|
+
block_index: int,
|
|
129
|
+
variables: dict[str, str | list[str]] | None,
|
|
130
|
+
context: list[dict[str, str]] | None = None,
|
|
131
|
+
) -> list[dict[str, str]]:
|
|
132
|
+
"""
|
|
133
|
+
Get content messages (for debugging and inspection).
|
|
134
|
+
|
|
135
|
+
Builds and returns the complete message list that will be sent to LLM.
|
|
136
|
+
|
|
137
|
+
Use cases:
|
|
138
|
+
- Debug: View actual content sent to LLM
|
|
139
|
+
- Verify: Check if code blocks are correctly restored
|
|
140
|
+
- Inspect: Verify variable replacement and prompt building logic
|
|
141
|
+
- Review: Confirm system/user message assembly results
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
block_index: Block index
|
|
145
|
+
variables: Variable mapping
|
|
146
|
+
context: Context message list
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
List of message dictionaries
|
|
150
|
+
"""
|
|
151
|
+
return self._build_content_messages(block_index, variables, context)
|
|
152
|
+
|
|
153
|
+
def set_model(self, model: str) -> "MarkdownFlow":
|
|
154
|
+
"""
|
|
155
|
+
Set model name for this instance.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
model: Model name to use
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Self for method chaining
|
|
162
|
+
"""
|
|
163
|
+
self._model = model
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def set_temperature(self, temperature: float) -> "MarkdownFlow":
|
|
167
|
+
"""
|
|
168
|
+
Set temperature for this instance.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
temperature: Temperature value (typically 0.0-2.0)
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Self for method chaining
|
|
175
|
+
"""
|
|
176
|
+
self._temperature = temperature
|
|
177
|
+
return self
|
|
178
|
+
|
|
179
|
+
def get_model(self) -> str | None:
|
|
180
|
+
"""
|
|
181
|
+
Get model name for this instance.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Model name if set, None otherwise
|
|
185
|
+
"""
|
|
186
|
+
return self._model
|
|
187
|
+
|
|
188
|
+
def get_temperature(self) -> float | None:
|
|
189
|
+
"""
|
|
190
|
+
Get temperature for this instance.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Temperature value if set, None otherwise
|
|
194
|
+
"""
|
|
195
|
+
return self._temperature
|
|
196
|
+
|
|
95
197
|
def set_prompt(self, prompt_type: str, value: str | None) -> None:
|
|
96
198
|
"""
|
|
97
199
|
Set prompt template.
|
|
98
200
|
|
|
99
201
|
Args:
|
|
100
|
-
prompt_type: Prompt type ('document', 'interaction', 'interaction_error')
|
|
202
|
+
prompt_type: Prompt type ('base_system', 'document', 'interaction', 'interaction_error')
|
|
101
203
|
value: Prompt content
|
|
102
204
|
"""
|
|
103
|
-
if prompt_type == "
|
|
205
|
+
if prompt_type == "base_system":
|
|
206
|
+
self._base_system_prompt = value or DEFAULT_BASE_SYSTEM_PROMPT
|
|
207
|
+
elif prompt_type == "document":
|
|
104
208
|
self._document_prompt = value
|
|
105
209
|
elif prompt_type == "interaction":
|
|
106
210
|
self._interaction_prompt = value or DEFAULT_INTERACTION_PROMPT
|
|
@@ -109,6 +213,44 @@ class MarkdownFlow:
|
|
|
109
213
|
else:
|
|
110
214
|
raise ValueError(UNSUPPORTED_PROMPT_TYPE_ERROR.format(prompt_type=prompt_type))
|
|
111
215
|
|
|
216
|
+
def _truncate_context(
|
|
217
|
+
self,
|
|
218
|
+
context: list[dict[str, str]] | None,
|
|
219
|
+
) -> list[dict[str, str]] | None:
|
|
220
|
+
"""
|
|
221
|
+
Filter and truncate context to specified maximum length.
|
|
222
|
+
|
|
223
|
+
Processing steps:
|
|
224
|
+
1. Filter out messages with empty content (empty string or whitespace only)
|
|
225
|
+
2. Truncate to max_context_length if configured (0 = unlimited)
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
context: Original context list
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Filtered and truncated context. Returns None if no valid messages remain.
|
|
232
|
+
"""
|
|
233
|
+
if not context:
|
|
234
|
+
return None
|
|
235
|
+
|
|
236
|
+
# Step 1: Filter out messages with empty or whitespace-only content
|
|
237
|
+
filtered_context = [msg for msg in context if msg.get("content", "").strip()]
|
|
238
|
+
|
|
239
|
+
# Return None if no valid messages remain after filtering
|
|
240
|
+
if not filtered_context:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Step 2: Truncate to max_context_length if configured
|
|
244
|
+
if self._max_context_length == 0:
|
|
245
|
+
# No limit, return all filtered messages
|
|
246
|
+
return filtered_context
|
|
247
|
+
|
|
248
|
+
# Keep the most recent N messages
|
|
249
|
+
if len(filtered_context) > self._max_context_length:
|
|
250
|
+
return filtered_context[-self._max_context_length :]
|
|
251
|
+
|
|
252
|
+
return filtered_context
|
|
253
|
+
|
|
112
254
|
@property
|
|
113
255
|
def document(self) -> str:
|
|
114
256
|
"""Get document content."""
|
|
@@ -124,8 +266,9 @@ class MarkdownFlow:
|
|
|
124
266
|
if self._blocks is not None:
|
|
125
267
|
return self._blocks
|
|
126
268
|
|
|
127
|
-
|
|
128
|
-
|
|
269
|
+
# Parse the preprocessed document (code blocks already replaced with placeholders)
|
|
270
|
+
# The preprocessing was done once during initialization
|
|
271
|
+
segments = re.split(BLOCK_SEPARATOR, self._processed_document)
|
|
129
272
|
final_blocks: list[Block] = []
|
|
130
273
|
|
|
131
274
|
for segment in segments:
|
|
@@ -166,14 +309,6 @@ class MarkdownFlow:
|
|
|
166
309
|
"""Extract all variable names from the document."""
|
|
167
310
|
return extract_variables_from_text(self._document)
|
|
168
311
|
|
|
169
|
-
def set_interaction_validation_config(self, block_index: int, config: InteractionValidationConfig) -> None:
|
|
170
|
-
"""Set validation config for specified interaction block."""
|
|
171
|
-
self._interaction_configs[block_index] = config
|
|
172
|
-
|
|
173
|
-
def get_interaction_validation_config(self, block_index: int) -> InteractionValidationConfig | None:
|
|
174
|
-
"""Get validation config for specified interaction block."""
|
|
175
|
-
return self._interaction_configs.get(block_index)
|
|
176
|
-
|
|
177
312
|
# Core unified interface
|
|
178
313
|
|
|
179
314
|
def process(
|
|
@@ -183,8 +318,7 @@ class MarkdownFlow:
|
|
|
183
318
|
context: list[dict[str, str]] | None = None,
|
|
184
319
|
variables: dict[str, str | list[str]] | None = None,
|
|
185
320
|
user_input: dict[str, list[str]] | None = None,
|
|
186
|
-
|
|
187
|
-
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
321
|
+
):
|
|
188
322
|
"""
|
|
189
323
|
Unified block processing interface.
|
|
190
324
|
|
|
@@ -194,11 +328,14 @@ class MarkdownFlow:
|
|
|
194
328
|
context: Context message list
|
|
195
329
|
variables: Variable mappings
|
|
196
330
|
user_input: User input (for interaction blocks)
|
|
197
|
-
dynamic_interaction_format: Dynamic interaction format for validation
|
|
198
331
|
|
|
199
332
|
Returns:
|
|
200
333
|
LLMResult or Generator[LLMResult, None, None]
|
|
201
334
|
"""
|
|
335
|
+
# Process base_system_prompt variable replacement
|
|
336
|
+
if self._base_system_prompt:
|
|
337
|
+
self._base_system_prompt = replace_variables_in_text(self._base_system_prompt, variables or {})
|
|
338
|
+
|
|
202
339
|
# Process document_prompt variable replacement
|
|
203
340
|
if self._document_prompt:
|
|
204
341
|
self._document_prompt = replace_variables_in_text(self._document_prompt, variables or {})
|
|
@@ -206,16 +343,12 @@ class MarkdownFlow:
|
|
|
206
343
|
block = self.get_block(block_index)
|
|
207
344
|
|
|
208
345
|
if block.block_type == BlockType.CONTENT:
|
|
209
|
-
# Check if this is dynamic interaction validation
|
|
210
|
-
if dynamic_interaction_format and user_input:
|
|
211
|
-
return self._process_dynamic_interaction_validation(block_index, dynamic_interaction_format, user_input, mode, context, variables)
|
|
212
|
-
# Normal content processing (possibly with dynamic conversion)
|
|
213
346
|
return self._process_content(block_index, mode, context, variables)
|
|
214
347
|
|
|
215
348
|
if block.block_type == BlockType.INTERACTION:
|
|
216
349
|
if user_input is None:
|
|
217
350
|
# Render interaction content
|
|
218
|
-
return self._process_interaction_render(block_index, mode, variables)
|
|
351
|
+
return self._process_interaction_render(block_index, mode, context, variables)
|
|
219
352
|
# Process user input
|
|
220
353
|
return self._process_interaction_input(block_index, user_input, mode, context, variables)
|
|
221
354
|
|
|
@@ -234,37 +367,27 @@ class MarkdownFlow:
|
|
|
234
367
|
mode: ProcessMode,
|
|
235
368
|
context: list[dict[str, str]] | None,
|
|
236
369
|
variables: dict[str, str | list[str]] | None,
|
|
237
|
-
)
|
|
370
|
+
):
|
|
238
371
|
"""Process content block."""
|
|
372
|
+
# Truncate context to configured maximum length
|
|
373
|
+
truncated_context = self._truncate_context(context)
|
|
239
374
|
|
|
240
|
-
#
|
|
241
|
-
|
|
242
|
-
messages = self._build_content_messages(block_index, variables)
|
|
243
|
-
return LLMResult(prompt=messages[-1]["content"], metadata={"messages": messages})
|
|
244
|
-
|
|
245
|
-
# For COMPLETE and STREAM modes with LLM provider, use dynamic interaction check
|
|
246
|
-
# LLM will decide whether content needs to be converted to interaction block
|
|
247
|
-
if self._llm_provider:
|
|
248
|
-
block = self.get_block(block_index)
|
|
249
|
-
if block.block_type == BlockType.CONTENT:
|
|
250
|
-
return self._process_with_dynamic_check(block_index, mode, context, variables)
|
|
251
|
-
|
|
252
|
-
# Fallback: Build messages using standard content processing
|
|
253
|
-
messages = self._build_content_messages(block_index, variables)
|
|
375
|
+
# Build messages with context
|
|
376
|
+
messages = self._build_content_messages(block_index, variables, truncated_context)
|
|
254
377
|
|
|
255
378
|
if mode == ProcessMode.COMPLETE:
|
|
256
379
|
if not self._llm_provider:
|
|
257
380
|
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
258
381
|
|
|
259
|
-
|
|
260
|
-
return LLMResult(content=
|
|
382
|
+
content = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
383
|
+
return LLMResult(content=content, prompt=messages[-1]["content"])
|
|
261
384
|
|
|
262
385
|
if mode == ProcessMode.STREAM:
|
|
263
386
|
if not self._llm_provider:
|
|
264
387
|
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
265
388
|
|
|
266
389
|
def stream_generator():
|
|
267
|
-
for chunk in self._llm_provider.stream(messages):
|
|
390
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
268
391
|
yield LLMResult(content=chunk, prompt=messages[-1]["content"])
|
|
269
392
|
|
|
270
393
|
return stream_generator()
|
|
@@ -279,9 +402,18 @@ class MarkdownFlow:
|
|
|
279
402
|
# Replace variables
|
|
280
403
|
content = replace_variables_in_text(content, variables or {})
|
|
281
404
|
|
|
405
|
+
# Restore code blocks (replace placeholders with original code blocks)
|
|
406
|
+
content = self._preprocessor.restore_code_blocks(content)
|
|
407
|
+
|
|
282
408
|
return LLMResult(content=content)
|
|
283
409
|
|
|
284
|
-
def _process_interaction_render(
|
|
410
|
+
def _process_interaction_render(
|
|
411
|
+
self,
|
|
412
|
+
block_index: int,
|
|
413
|
+
mode: ProcessMode,
|
|
414
|
+
context: list[dict[str, str]] | None = None,
|
|
415
|
+
variables: dict[str, str | list[str]] | None = None,
|
|
416
|
+
):
|
|
285
417
|
"""Process interaction content rendering."""
|
|
286
418
|
block = self.get_block(block_index)
|
|
287
419
|
|
|
@@ -292,67 +424,92 @@ class MarkdownFlow:
|
|
|
292
424
|
processed_block = copy(block)
|
|
293
425
|
processed_block.content = processed_content
|
|
294
426
|
|
|
295
|
-
#
|
|
296
|
-
|
|
297
|
-
if not
|
|
298
|
-
#
|
|
299
|
-
return LLMResult(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
427
|
+
# 提取可翻译内容(JSON 格式)
|
|
428
|
+
translatable_json, interaction_info = self._extract_translatable_content(processed_block.content)
|
|
429
|
+
if not interaction_info:
|
|
430
|
+
# 解析失败,返回原始内容
|
|
431
|
+
return LLMResult(
|
|
432
|
+
content=processed_block.content,
|
|
433
|
+
metadata={
|
|
434
|
+
"block_type": "interaction",
|
|
435
|
+
"block_index": block_index,
|
|
436
|
+
},
|
|
437
|
+
)
|
|
303
438
|
|
|
304
|
-
|
|
439
|
+
# 如果没有可翻译内容,直接返回
|
|
440
|
+
if not translatable_json or translatable_json == "{}":
|
|
305
441
|
return LLMResult(
|
|
306
|
-
|
|
442
|
+
content=processed_block.content,
|
|
307
443
|
metadata={
|
|
308
|
-
"
|
|
309
|
-
"
|
|
444
|
+
"block_type": "interaction",
|
|
445
|
+
"block_index": block_index,
|
|
310
446
|
},
|
|
311
447
|
)
|
|
312
448
|
|
|
449
|
+
# 构建翻译消息
|
|
450
|
+
messages = self._build_translation_messages(translatable_json)
|
|
451
|
+
|
|
313
452
|
if mode == ProcessMode.COMPLETE:
|
|
314
453
|
if not self._llm_provider:
|
|
315
|
-
return LLMResult(
|
|
454
|
+
return LLMResult(
|
|
455
|
+
content=processed_block.content,
|
|
456
|
+
metadata={
|
|
457
|
+
"block_type": "interaction",
|
|
458
|
+
"block_index": block_index,
|
|
459
|
+
},
|
|
460
|
+
)
|
|
316
461
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
462
|
+
# 调用 LLM 进行翻译
|
|
463
|
+
translated_json = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
464
|
+
|
|
465
|
+
# 使用翻译结果重构交互内容
|
|
466
|
+
translated_content = self._reconstruct_with_translation(processed_block.content, translatable_json, translated_json, interaction_info)
|
|
320
467
|
|
|
321
468
|
return LLMResult(
|
|
322
|
-
content=
|
|
469
|
+
content=translated_content,
|
|
323
470
|
prompt=messages[-1]["content"],
|
|
324
471
|
metadata={
|
|
325
|
-
"
|
|
326
|
-
"
|
|
472
|
+
"block_type": "interaction",
|
|
473
|
+
"block_index": block_index,
|
|
474
|
+
"original_content": translatable_json,
|
|
475
|
+
"translated_content": translated_json,
|
|
327
476
|
},
|
|
328
477
|
)
|
|
329
478
|
|
|
330
479
|
if mode == ProcessMode.STREAM:
|
|
331
480
|
if not self._llm_provider:
|
|
332
|
-
#
|
|
333
|
-
rendered_content = self._reconstruct_interaction_content(processed_block.content, question_text or "")
|
|
334
|
-
|
|
481
|
+
# 降级处理,返回处理后的内容
|
|
335
482
|
def stream_generator():
|
|
336
483
|
yield LLMResult(
|
|
337
|
-
content=
|
|
484
|
+
content=processed_block.content,
|
|
338
485
|
prompt=messages[-1]["content"],
|
|
486
|
+
metadata={
|
|
487
|
+
"block_type": "interaction",
|
|
488
|
+
"block_index": block_index,
|
|
489
|
+
},
|
|
339
490
|
)
|
|
340
491
|
|
|
341
492
|
return stream_generator()
|
|
342
493
|
|
|
343
|
-
#
|
|
494
|
+
# 有 LLM 提供者,收集完整响应后返回一次
|
|
344
495
|
def stream_generator():
|
|
345
496
|
full_response = ""
|
|
346
|
-
for chunk in self._llm_provider.stream(messages):
|
|
497
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
347
498
|
full_response += chunk
|
|
348
499
|
|
|
349
|
-
#
|
|
350
|
-
|
|
500
|
+
# 使用翻译结果重构交互内容
|
|
501
|
+
translated_content = self._reconstruct_with_translation(processed_block.content, translatable_json, full_response, interaction_info)
|
|
351
502
|
|
|
352
|
-
#
|
|
503
|
+
# 一次性返回完整内容(不是增量)
|
|
353
504
|
yield LLMResult(
|
|
354
|
-
content=
|
|
505
|
+
content=translated_content,
|
|
355
506
|
prompt=messages[-1]["content"],
|
|
507
|
+
metadata={
|
|
508
|
+
"block_type": "interaction",
|
|
509
|
+
"block_index": block_index,
|
|
510
|
+
"original_content": translatable_json,
|
|
511
|
+
"translated_content": full_response,
|
|
512
|
+
},
|
|
356
513
|
)
|
|
357
514
|
|
|
358
515
|
return stream_generator()
|
|
@@ -366,14 +523,13 @@ class MarkdownFlow:
|
|
|
366
523
|
variables: dict[str, str | list[str]] | None = None,
|
|
367
524
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
368
525
|
"""Process interaction user input."""
|
|
369
|
-
_ = context # Mark as intentionally unused
|
|
370
526
|
block = self.get_block(block_index)
|
|
371
527
|
target_variable = block.variables[0] if block.variables else "user_input"
|
|
372
528
|
|
|
373
529
|
# Basic validation
|
|
374
530
|
if not user_input or not any(values for values in user_input.values()):
|
|
375
531
|
error_msg = INPUT_EMPTY_ERROR
|
|
376
|
-
return self._render_error(error_msg, mode)
|
|
532
|
+
return self._render_error(error_msg, mode, context)
|
|
377
533
|
|
|
378
534
|
# Get the target variable value from user_input
|
|
379
535
|
target_values = user_input.get(target_variable, [])
|
|
@@ -387,24 +543,98 @@ class MarkdownFlow:
|
|
|
387
543
|
|
|
388
544
|
if "error" in parse_result:
|
|
389
545
|
error_msg = INTERACTION_PARSE_ERROR.format(error=parse_result["error"])
|
|
390
|
-
return self._render_error(error_msg, mode)
|
|
546
|
+
return self._render_error(error_msg, mode, context)
|
|
391
547
|
|
|
392
548
|
interaction_type = parse_result.get("type")
|
|
393
549
|
|
|
394
550
|
# Process user input based on interaction type
|
|
395
551
|
if interaction_type in [
|
|
396
|
-
InteractionType.BUTTONS_ONLY,
|
|
397
552
|
InteractionType.BUTTONS_WITH_TEXT,
|
|
398
|
-
InteractionType.BUTTONS_MULTI_SELECT,
|
|
399
553
|
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
400
554
|
]:
|
|
401
|
-
#
|
|
555
|
+
# Buttons with text input: smart validation (match buttons first, then LLM validate custom text)
|
|
556
|
+
buttons = parse_result.get("buttons", [])
|
|
557
|
+
|
|
558
|
+
# Step 1: Match button values
|
|
559
|
+
matched_values, unmatched_values = self._match_button_values(buttons, target_values)
|
|
560
|
+
|
|
561
|
+
# Step 2: If there are unmatched values (custom text), validate with LLM
|
|
562
|
+
if unmatched_values:
|
|
563
|
+
# Create user_input for LLM validation (only custom text)
|
|
564
|
+
custom_input = {target_variable: unmatched_values}
|
|
565
|
+
|
|
566
|
+
validation_result = self._process_llm_validation(
|
|
567
|
+
block_index=block_index,
|
|
568
|
+
user_input=custom_input,
|
|
569
|
+
target_variable=target_variable,
|
|
570
|
+
mode=mode,
|
|
571
|
+
context=context,
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
# Handle validation result based on mode
|
|
575
|
+
if mode == ProcessMode.COMPLETE:
|
|
576
|
+
# Check if validation passed
|
|
577
|
+
if isinstance(validation_result, LLMResult) and validation_result.variables:
|
|
578
|
+
validated_values = validation_result.variables.get(target_variable, [])
|
|
579
|
+
# Merge matched button values + validated custom text
|
|
580
|
+
all_values = matched_values + validated_values
|
|
581
|
+
return LLMResult(
|
|
582
|
+
content="",
|
|
583
|
+
variables={target_variable: all_values},
|
|
584
|
+
metadata={
|
|
585
|
+
"interaction_type": str(interaction_type),
|
|
586
|
+
"matched_button_values": matched_values,
|
|
587
|
+
"validated_custom_values": validated_values,
|
|
588
|
+
},
|
|
589
|
+
)
|
|
590
|
+
# Validation failed, return error
|
|
591
|
+
return validation_result
|
|
592
|
+
|
|
593
|
+
if mode == ProcessMode.STREAM:
|
|
594
|
+
# For stream mode, collect validation result
|
|
595
|
+
def stream_merge_generator():
|
|
596
|
+
# Consume the validation stream
|
|
597
|
+
for result in validation_result: # type: ignore[attr-defined]
|
|
598
|
+
if isinstance(result, LLMResult) and result.variables:
|
|
599
|
+
validated_values = result.variables.get(target_variable, [])
|
|
600
|
+
all_values = matched_values + validated_values
|
|
601
|
+
yield LLMResult(
|
|
602
|
+
content="",
|
|
603
|
+
variables={target_variable: all_values},
|
|
604
|
+
metadata={
|
|
605
|
+
"interaction_type": str(interaction_type),
|
|
606
|
+
"matched_button_values": matched_values,
|
|
607
|
+
"validated_custom_values": validated_values,
|
|
608
|
+
},
|
|
609
|
+
)
|
|
610
|
+
else:
|
|
611
|
+
# Validation failed
|
|
612
|
+
yield result
|
|
613
|
+
|
|
614
|
+
return stream_merge_generator()
|
|
615
|
+
else:
|
|
616
|
+
# All values matched buttons, return directly
|
|
617
|
+
return LLMResult(
|
|
618
|
+
content="",
|
|
619
|
+
variables={target_variable: matched_values},
|
|
620
|
+
metadata={
|
|
621
|
+
"interaction_type": str(interaction_type),
|
|
622
|
+
"all_matched_buttons": True,
|
|
623
|
+
},
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
if interaction_type in [
|
|
627
|
+
InteractionType.BUTTONS_ONLY,
|
|
628
|
+
InteractionType.BUTTONS_MULTI_SELECT,
|
|
629
|
+
]:
|
|
630
|
+
# Pure button types: only basic button validation (no LLM)
|
|
402
631
|
return self._process_button_validation(
|
|
403
632
|
parse_result,
|
|
404
633
|
target_values,
|
|
405
634
|
target_variable,
|
|
406
635
|
mode,
|
|
407
636
|
interaction_type,
|
|
637
|
+
context,
|
|
408
638
|
)
|
|
409
639
|
|
|
410
640
|
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
@@ -420,19 +650,50 @@ class MarkdownFlow:
|
|
|
420
650
|
)
|
|
421
651
|
|
|
422
652
|
# Text-only input type: ?[%{{sys_user_nickname}}...question]
|
|
423
|
-
#
|
|
653
|
+
# Use LLM validation to check if input is relevant to the question
|
|
424
654
|
if target_values:
|
|
425
|
-
return
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
"values": target_values,
|
|
432
|
-
},
|
|
655
|
+
return self._process_llm_validation(
|
|
656
|
+
block_index=block_index,
|
|
657
|
+
user_input=user_input,
|
|
658
|
+
target_variable=target_variable,
|
|
659
|
+
mode=mode,
|
|
660
|
+
context=context,
|
|
433
661
|
)
|
|
434
662
|
error_msg = f"No input provided for variable '{target_variable}'"
|
|
435
|
-
return self._render_error(error_msg, mode)
|
|
663
|
+
return self._render_error(error_msg, mode, context)
|
|
664
|
+
|
|
665
|
+
def _match_button_values(
|
|
666
|
+
self,
|
|
667
|
+
buttons: list[dict[str, str]],
|
|
668
|
+
target_values: list[str],
|
|
669
|
+
) -> tuple[list[str], list[str]]:
|
|
670
|
+
"""
|
|
671
|
+
Match user input values against button options.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
buttons: List of button dictionaries with 'display' and 'value' keys
|
|
675
|
+
target_values: User input values to match
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
Tuple of (matched_values, unmatched_values)
|
|
679
|
+
- matched_values: Values that match button options (using button value)
|
|
680
|
+
- unmatched_values: Values that don't match any button
|
|
681
|
+
"""
|
|
682
|
+
matched_values = []
|
|
683
|
+
unmatched_values = []
|
|
684
|
+
|
|
685
|
+
for value in target_values:
|
|
686
|
+
matched = False
|
|
687
|
+
for button in buttons:
|
|
688
|
+
if value in [button["display"], button["value"]]:
|
|
689
|
+
matched_values.append(button["value"]) # Use button value
|
|
690
|
+
matched = True
|
|
691
|
+
break
|
|
692
|
+
|
|
693
|
+
if not matched:
|
|
694
|
+
unmatched_values.append(value)
|
|
695
|
+
|
|
696
|
+
return matched_values, unmatched_values
|
|
436
697
|
|
|
437
698
|
def _process_button_validation(
|
|
438
699
|
self,
|
|
@@ -441,6 +702,7 @@ class MarkdownFlow:
|
|
|
441
702
|
target_variable: str,
|
|
442
703
|
mode: ProcessMode,
|
|
443
704
|
interaction_type: InteractionType,
|
|
705
|
+
context: list[dict[str, str]] | None = None,
|
|
444
706
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
445
707
|
"""
|
|
446
708
|
Simplified button validation with new input format.
|
|
@@ -451,6 +713,7 @@ class MarkdownFlow:
|
|
|
451
713
|
target_variable: Target variable name
|
|
452
714
|
mode: Processing mode
|
|
453
715
|
interaction_type: Type of interaction
|
|
716
|
+
context: Conversation history context (optional)
|
|
454
717
|
"""
|
|
455
718
|
buttons = parse_result.get("buttons", [])
|
|
456
719
|
is_multi_select = interaction_type in [
|
|
@@ -476,9 +739,9 @@ class MarkdownFlow:
|
|
|
476
739
|
# Pure button mode requires input
|
|
477
740
|
button_displays = [btn["display"] for btn in buttons]
|
|
478
741
|
error_msg = f"Please select from: {', '.join(button_displays)}"
|
|
479
|
-
return self._render_error(error_msg, mode)
|
|
742
|
+
return self._render_error(error_msg, mode, context)
|
|
480
743
|
|
|
481
|
-
#
|
|
744
|
+
# Validate input values against available buttons
|
|
482
745
|
valid_values = []
|
|
483
746
|
invalid_values = []
|
|
484
747
|
|
|
@@ -491,30 +754,19 @@ class MarkdownFlow:
|
|
|
491
754
|
break
|
|
492
755
|
|
|
493
756
|
if not matched:
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return self._process_llm_validation_with_options(
|
|
503
|
-
block_index=0, # Not used in the method
|
|
504
|
-
user_input={target_variable: target_values},
|
|
505
|
-
target_variable=target_variable,
|
|
506
|
-
options=button_displays,
|
|
507
|
-
question=question,
|
|
508
|
-
mode=mode
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
# Check for validation errors in pure button mode or when text input not allowed
|
|
512
|
-
if invalid_values:
|
|
757
|
+
if allow_text_input:
|
|
758
|
+
# Allow custom text in buttons+text mode
|
|
759
|
+
valid_values.append(value)
|
|
760
|
+
else:
|
|
761
|
+
invalid_values.append(value)
|
|
762
|
+
|
|
763
|
+
# Check for validation errors
|
|
764
|
+
if invalid_values and not allow_text_input:
|
|
513
765
|
button_displays = [btn["display"] for btn in buttons]
|
|
514
766
|
error_msg = f"Invalid options: {', '.join(invalid_values)}. Please select from: {', '.join(button_displays)}"
|
|
515
|
-
return self._render_error(error_msg, mode)
|
|
767
|
+
return self._render_error(error_msg, mode, context)
|
|
516
768
|
|
|
517
|
-
# Success: return validated
|
|
769
|
+
# Success: return validated values
|
|
518
770
|
return LLMResult(
|
|
519
771
|
content="",
|
|
520
772
|
variables={target_variable: valid_values},
|
|
@@ -524,7 +776,6 @@ class MarkdownFlow:
|
|
|
524
776
|
"valid_values": valid_values,
|
|
525
777
|
"invalid_values": invalid_values,
|
|
526
778
|
"total_input_count": len(target_values),
|
|
527
|
-
"llm_validated": False,
|
|
528
779
|
},
|
|
529
780
|
)
|
|
530
781
|
|
|
@@ -534,27 +785,18 @@ class MarkdownFlow:
|
|
|
534
785
|
user_input: dict[str, list[str]],
|
|
535
786
|
target_variable: str,
|
|
536
787
|
mode: ProcessMode,
|
|
788
|
+
context: list[dict[str, str]] | None = None,
|
|
537
789
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
538
790
|
"""Process LLM validation."""
|
|
539
791
|
# Build validation messages
|
|
540
|
-
messages = self._build_validation_messages(block_index, user_input, target_variable)
|
|
541
|
-
|
|
542
|
-
if mode == ProcessMode.PROMPT_ONLY:
|
|
543
|
-
return LLMResult(
|
|
544
|
-
prompt=messages[-1]["content"],
|
|
545
|
-
metadata={
|
|
546
|
-
"validation_target": user_input,
|
|
547
|
-
"target_variable": target_variable,
|
|
548
|
-
},
|
|
549
|
-
)
|
|
792
|
+
messages = self._build_validation_messages(block_index, user_input, target_variable, context)
|
|
550
793
|
|
|
551
794
|
if mode == ProcessMode.COMPLETE:
|
|
552
795
|
if not self._llm_provider:
|
|
553
796
|
# Fallback processing, return variables directly
|
|
554
797
|
return LLMResult(content="", variables=user_input) # type: ignore[arg-type]
|
|
555
798
|
|
|
556
|
-
|
|
557
|
-
llm_response = result.content
|
|
799
|
+
llm_response = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
558
800
|
|
|
559
801
|
# Parse validation response and convert to LLMResult
|
|
560
802
|
# Use joined target values for fallback; avoids JSON string injection
|
|
@@ -568,7 +810,7 @@ class MarkdownFlow:
|
|
|
568
810
|
|
|
569
811
|
def stream_generator():
|
|
570
812
|
full_response = ""
|
|
571
|
-
for chunk in self._llm_provider.stream(messages):
|
|
813
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
572
814
|
full_response += chunk
|
|
573
815
|
|
|
574
816
|
# Parse complete response and convert to LLMResult
|
|
@@ -592,28 +834,15 @@ class MarkdownFlow:
|
|
|
592
834
|
mode: ProcessMode,
|
|
593
835
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
594
836
|
"""Process LLM validation with button options (third case)."""
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
messages = self._build_validation_messages_with_options(user_input, target_variable, options, question)
|
|
598
|
-
|
|
599
|
-
if mode == ProcessMode.PROMPT_ONLY:
|
|
600
|
-
return LLMResult(
|
|
601
|
-
prompt=messages[-1]["content"],
|
|
602
|
-
metadata={
|
|
603
|
-
"validation_target": user_input,
|
|
604
|
-
"target_variable": target_variable,
|
|
605
|
-
"options": options,
|
|
606
|
-
"question": question,
|
|
607
|
-
},
|
|
608
|
-
)
|
|
837
|
+
# Use unified validation message builder (button context will be included automatically)
|
|
838
|
+
messages = self._build_validation_messages(block_index, user_input, target_variable, context=None)
|
|
609
839
|
|
|
610
840
|
if mode == ProcessMode.COMPLETE:
|
|
611
841
|
if not self._llm_provider:
|
|
612
842
|
# Fallback processing, return variables directly
|
|
613
843
|
return LLMResult(content="", variables=user_input) # type: ignore[arg-type]
|
|
614
844
|
|
|
615
|
-
|
|
616
|
-
llm_response = result.content
|
|
845
|
+
llm_response = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
617
846
|
|
|
618
847
|
# Parse validation response and convert to LLMResult
|
|
619
848
|
# Use joined target values for fallback; avoids JSON string injection
|
|
@@ -627,7 +856,7 @@ class MarkdownFlow:
|
|
|
627
856
|
|
|
628
857
|
def stream_generator():
|
|
629
858
|
full_response = ""
|
|
630
|
-
for chunk in self._llm_provider.stream(messages):
|
|
859
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
631
860
|
full_response += chunk
|
|
632
861
|
# For validation scenario, don't output chunks in real-time, only final result
|
|
633
862
|
|
|
@@ -644,22 +873,24 @@ class MarkdownFlow:
|
|
|
644
873
|
|
|
645
874
|
return stream_generator()
|
|
646
875
|
|
|
647
|
-
def _render_error(
|
|
876
|
+
def _render_error(
|
|
877
|
+
self,
|
|
878
|
+
error_message: str,
|
|
879
|
+
mode: ProcessMode,
|
|
880
|
+
context: list[dict[str, str]] | None = None,
|
|
881
|
+
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
648
882
|
"""Render user-friendly error message."""
|
|
649
|
-
|
|
883
|
+
# Truncate context to configured maximum length
|
|
884
|
+
truncated_context = self._truncate_context(context)
|
|
650
885
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
prompt=messages[-1]["content"],
|
|
654
|
-
metadata={"original_error": error_message},
|
|
655
|
-
)
|
|
886
|
+
# Build error messages with context
|
|
887
|
+
messages = self._build_error_render_messages(error_message, truncated_context)
|
|
656
888
|
|
|
657
889
|
if mode == ProcessMode.COMPLETE:
|
|
658
890
|
if not self._llm_provider:
|
|
659
891
|
return LLMResult(content=error_message) # Fallback processing
|
|
660
892
|
|
|
661
|
-
|
|
662
|
-
friendly_error = result.content
|
|
893
|
+
friendly_error = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
663
894
|
return LLMResult(content=friendly_error, prompt=messages[-1]["content"])
|
|
664
895
|
|
|
665
896
|
if mode == ProcessMode.STREAM:
|
|
@@ -667,7 +898,7 @@ class MarkdownFlow:
|
|
|
667
898
|
return LLMResult(content=error_message)
|
|
668
899
|
|
|
669
900
|
def stream_generator():
|
|
670
|
-
for chunk in self._llm_provider.stream(messages):
|
|
901
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
671
902
|
yield LLMResult(content=chunk, prompt=messages[-1]["content"])
|
|
672
903
|
|
|
673
904
|
return stream_generator()
|
|
@@ -678,502 +909,294 @@ class MarkdownFlow:
|
|
|
678
909
|
self,
|
|
679
910
|
block_index: int,
|
|
680
911
|
variables: dict[str, str | list[str]] | None,
|
|
912
|
+
context: list[dict[str, str]] | None = None,
|
|
681
913
|
) -> list[dict[str, str]]:
|
|
682
914
|
"""Build content block messages."""
|
|
683
915
|
block = self.get_block(block_index)
|
|
684
916
|
block_content = block.content
|
|
685
917
|
|
|
686
|
-
# Process output instructions
|
|
687
|
-
|
|
918
|
+
# Process output instructions and detect if preserved content exists
|
|
919
|
+
# Returns: (processed_content, has_preserved_content)
|
|
920
|
+
block_content, has_preserved_content = process_output_instructions(block_content)
|
|
688
921
|
|
|
689
922
|
# Replace variables
|
|
690
923
|
block_content = replace_variables_in_text(block_content, variables or {})
|
|
691
924
|
|
|
925
|
+
# Restore code blocks (让 LLM 看到真实的代码块内容)
|
|
926
|
+
# Code block preprocessing is to prevent the parser from misinterpreting
|
|
927
|
+
# MarkdownFlow syntax inside code blocks, but the LLM needs to see
|
|
928
|
+
# the real content to correctly understand and generate responses
|
|
929
|
+
block_content = self._preprocessor.restore_code_blocks(block_content)
|
|
930
|
+
|
|
692
931
|
# Build message array
|
|
693
932
|
messages = []
|
|
694
933
|
|
|
695
|
-
#
|
|
696
|
-
|
|
697
|
-
|
|
934
|
+
# Build system message with XML tags
|
|
935
|
+
# Priority order: preserve_or_translate_instruction > base_system > document_prompt
|
|
936
|
+
system_parts = []
|
|
698
937
|
|
|
699
|
-
#
|
|
700
|
-
#
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
# messages.extend(context)
|
|
938
|
+
# 1. Output instruction (highest priority - if preserved content exists)
|
|
939
|
+
# Note: OUTPUT_INSTRUCTION_EXPLANATION already contains <preserve_or_translate_instruction> tags
|
|
940
|
+
if has_preserved_content:
|
|
941
|
+
system_parts.append(OUTPUT_INSTRUCTION_EXPLANATION.strip())
|
|
704
942
|
|
|
705
|
-
#
|
|
706
|
-
|
|
943
|
+
# 2. Base system prompt (if exists and non-empty)
|
|
944
|
+
if self._base_system_prompt:
|
|
945
|
+
system_parts.append(f"<base_system>\n{self._base_system_prompt}\n</base_system>")
|
|
707
946
|
|
|
708
|
-
|
|
947
|
+
# 3. Document prompt (if exists and non-empty)
|
|
948
|
+
if self._document_prompt:
|
|
949
|
+
system_parts.append(f"<document_prompt>\n{self._document_prompt}\n</document_prompt>")
|
|
709
950
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
# User custom prompt + mandatory direction protection
|
|
715
|
-
render_prompt = f"""{self._interaction_prompt}"""
|
|
716
|
-
else:
|
|
717
|
-
# Use default prompt and instructions
|
|
718
|
-
render_prompt = f"""{self._interaction_prompt}
|
|
719
|
-
{INTERACTION_RENDER_INSTRUCTIONS}"""
|
|
951
|
+
# Combine all parts and add as system message
|
|
952
|
+
if system_parts:
|
|
953
|
+
system_msg = "\n\n".join(system_parts)
|
|
954
|
+
messages.append({"role": "system", "content": system_msg})
|
|
720
955
|
|
|
721
|
-
|
|
956
|
+
# Add conversation history context if provided
|
|
957
|
+
# Context is inserted after system message and before current user message
|
|
958
|
+
truncated_context = self._truncate_context(context)
|
|
959
|
+
if truncated_context:
|
|
960
|
+
messages.extend(truncated_context)
|
|
722
961
|
|
|
723
|
-
|
|
724
|
-
messages.append({"role": "user", "content":
|
|
962
|
+
# Add processed content as user message (as instruction to LLM)
|
|
963
|
+
messages.append({"role": "user", "content": block_content})
|
|
725
964
|
|
|
726
965
|
return messages
|
|
727
966
|
|
|
728
|
-
def
|
|
729
|
-
"""
|
|
730
|
-
block = self.get_block(block_index)
|
|
731
|
-
config = self.get_interaction_validation_config(block_index)
|
|
732
|
-
|
|
733
|
-
if config and config.validation_template:
|
|
734
|
-
# Use custom validation template
|
|
735
|
-
validation_prompt = config.validation_template
|
|
736
|
-
user_input_str = json.dumps(user_input, ensure_ascii=False)
|
|
737
|
-
validation_prompt = validation_prompt.replace("{sys_user_input}", user_input_str)
|
|
738
|
-
validation_prompt = validation_prompt.replace("{block_content}", block.content)
|
|
739
|
-
validation_prompt = validation_prompt.replace("{target_variable}", target_variable)
|
|
740
|
-
system_message = DEFAULT_VALIDATION_SYSTEM_MESSAGE
|
|
741
|
-
else:
|
|
742
|
-
# Use smart default validation template
|
|
743
|
-
from .utils import (
|
|
744
|
-
extract_interaction_question,
|
|
745
|
-
generate_smart_validation_template,
|
|
746
|
-
)
|
|
967
|
+
def _extract_translatable_content(self, interaction_content: str) -> tuple[str, dict[str, Any] | None]:
|
|
968
|
+
"""提取交互内容中需要翻译的部分为 JSON 格式
|
|
747
969
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
# Generate smart validation template
|
|
752
|
-
validation_template = generate_smart_validation_template(
|
|
753
|
-
target_variable,
|
|
754
|
-
context=None, # Could consider passing context here
|
|
755
|
-
interaction_question=interaction_question,
|
|
756
|
-
)
|
|
757
|
-
|
|
758
|
-
# Replace template variables
|
|
759
|
-
user_input_str = json.dumps(user_input, ensure_ascii=False)
|
|
760
|
-
validation_prompt = validation_template.replace("{sys_user_input}", user_input_str)
|
|
761
|
-
validation_prompt = validation_prompt.replace("{block_content}", block.content)
|
|
762
|
-
validation_prompt = validation_prompt.replace("{target_variable}", target_variable)
|
|
763
|
-
system_message = DEFAULT_VALIDATION_SYSTEM_MESSAGE
|
|
970
|
+
Args:
|
|
971
|
+
interaction_content: 交互内容字符串
|
|
764
972
|
|
|
765
|
-
|
|
973
|
+
Returns:
|
|
974
|
+
tuple: (JSON 字符串, InteractionInfo 字典)
|
|
975
|
+
"""
|
|
976
|
+
# 解析交互内容
|
|
977
|
+
interaction_parser = InteractionParser()
|
|
978
|
+
interaction_info = interaction_parser.parse(interaction_content)
|
|
979
|
+
if not interaction_info:
|
|
980
|
+
return "{}", None
|
|
766
981
|
|
|
767
|
-
|
|
768
|
-
messages.append({"role": "user", "content": validation_prompt})
|
|
982
|
+
translatable = {}
|
|
769
983
|
|
|
770
|
-
|
|
984
|
+
# 提取按钮的 Display 文本
|
|
985
|
+
if interaction_info.get("buttons"):
|
|
986
|
+
button_texts = [btn["display"] for btn in interaction_info["buttons"]]
|
|
987
|
+
translatable["buttons"] = button_texts
|
|
771
988
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
target_variable: str,
|
|
776
|
-
options: list[str],
|
|
777
|
-
question: str,
|
|
778
|
-
) -> list[dict[str, str]]:
|
|
779
|
-
"""Build validation messages with button options (third case)."""
|
|
780
|
-
# Use validation template from constants
|
|
781
|
-
user_input_str = json.dumps(user_input, ensure_ascii=False)
|
|
782
|
-
validation_prompt = BUTTONS_WITH_TEXT_VALIDATION_TEMPLATE.format(
|
|
783
|
-
question=question,
|
|
784
|
-
options=", ".join(options),
|
|
785
|
-
user_input=user_input_str,
|
|
786
|
-
target_variable=target_variable,
|
|
787
|
-
)
|
|
989
|
+
# 提取问题文本
|
|
990
|
+
if interaction_info.get("question"):
|
|
991
|
+
translatable["question"] = interaction_info["question"]
|
|
788
992
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
messages.append({"role": "system", "content": self._document_prompt})
|
|
993
|
+
# 转换为 JSON
|
|
994
|
+
import json
|
|
792
995
|
|
|
793
|
-
|
|
794
|
-
messages.append({"role": "user", "content": validation_prompt})
|
|
996
|
+
json_str = json.dumps(translatable, ensure_ascii=False)
|
|
795
997
|
|
|
796
|
-
return
|
|
998
|
+
return json_str, interaction_info
|
|
797
999
|
|
|
798
|
-
def
|
|
799
|
-
"""
|
|
800
|
-
render_prompt = f"""{self._interaction_error_prompt}
|
|
801
|
-
|
|
802
|
-
Original Error: {error_message}
|
|
1000
|
+
def _build_translation_messages(self, translatable_json: str) -> list[dict[str, str]]:
|
|
1001
|
+
"""构建翻译用的消息列表
|
|
803
1002
|
|
|
804
|
-
|
|
1003
|
+
Args:
|
|
1004
|
+
translatable_json: 可翻译内容的 JSON 字符串
|
|
805
1005
|
|
|
1006
|
+
Returns:
|
|
1007
|
+
list: 消息列表
|
|
1008
|
+
"""
|
|
806
1009
|
messages = []
|
|
807
|
-
if self._document_prompt:
|
|
808
|
-
messages.append({"role": "system", "content": self._document_prompt})
|
|
809
|
-
|
|
810
|
-
messages.append({"role": "system", "content": render_prompt})
|
|
811
|
-
messages.append({"role": "user", "content": error_message})
|
|
812
|
-
|
|
813
|
-
return messages
|
|
814
|
-
|
|
815
|
-
# Helper methods
|
|
816
1010
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1011
|
+
# 构建 system message:交互翻译提示词 + 文档提示词(XML 格式)
|
|
1012
|
+
# interaction_prompt: 定义翻译规则和 JSON 格式要求(包含 <interaction_translation_rules> 标签)
|
|
1013
|
+
# document_prompt: 提供语言指令(如"使用英语输出"),包装在 <document_context> 标签中供 LLM 检测
|
|
1014
|
+
system_content = self._interaction_prompt
|
|
1015
|
+
if self._document_prompt:
|
|
1016
|
+
# 将文档提示词包装在 <document_context> 标签中
|
|
1017
|
+
system_content = f"{self._interaction_prompt}\n\n<document_context>\n{self._document_prompt}\n</document_context>"
|
|
824
1018
|
|
|
825
|
-
|
|
1019
|
+
messages.append({"role": "system", "content": system_content})
|
|
826
1020
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
suffix = match.group(2)
|
|
830
|
-
return f"{prefix}{cleaned_question}{suffix}"
|
|
831
|
-
return original_content # type: ignore[unreachable]
|
|
1021
|
+
# 添加可翻译内容作为 user message
|
|
1022
|
+
messages.append({"role": "user", "content": translatable_json})
|
|
832
1023
|
|
|
833
|
-
|
|
1024
|
+
return messages
|
|
834
1025
|
|
|
835
|
-
def
|
|
1026
|
+
def _reconstruct_with_translation(
|
|
836
1027
|
self,
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
) ->
|
|
842
|
-
"""
|
|
843
|
-
|
|
844
|
-
block = self.get_block(block_index)
|
|
845
|
-
messages = self._build_dynamic_check_messages(block, context, variables)
|
|
846
|
-
|
|
847
|
-
# Define Function Calling tools with structured approach
|
|
848
|
-
tools = [
|
|
849
|
-
{
|
|
850
|
-
"type": "function",
|
|
851
|
-
"function": {
|
|
852
|
-
"name": "create_interaction_block",
|
|
853
|
-
"description": "Convert content to interaction block with structured data when it needs to collect user input",
|
|
854
|
-
"parameters": {
|
|
855
|
-
"type": "object",
|
|
856
|
-
"properties": {
|
|
857
|
-
"needs_interaction": {"type": "boolean", "description": "Whether this content needs to be converted to interaction block"},
|
|
858
|
-
"variable_name": {"type": "string", "description": "Name of the variable to collect (without {{}} brackets)"},
|
|
859
|
-
"interaction_type": {
|
|
860
|
-
"type": "string",
|
|
861
|
-
"enum": ["single_select", "multi_select", "text_input", "mixed"],
|
|
862
|
-
"description": "Type of interaction: single_select (|), multi_select (||), text_input (...), mixed (options + text)",
|
|
863
|
-
},
|
|
864
|
-
"options": {"type": "array", "items": {"type": "string"}, "description": "List of selectable options (3-4 specific options based on context)"},
|
|
865
|
-
"allow_text_input": {"type": "boolean", "description": "Whether to include a text input option for 'Other' cases"},
|
|
866
|
-
"text_input_prompt": {"type": "string", "description": "Prompt text for the text input option (e.g., '其他请输入', 'Other, please specify')"},
|
|
867
|
-
},
|
|
868
|
-
"required": ["needs_interaction"],
|
|
869
|
-
},
|
|
870
|
-
},
|
|
871
|
-
}
|
|
872
|
-
]
|
|
873
|
-
|
|
874
|
-
if not self._llm_provider:
|
|
875
|
-
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
876
|
-
|
|
877
|
-
# Call LLM with tools
|
|
878
|
-
result = self._llm_provider.complete(messages, tools)
|
|
1028
|
+
original_content: str,
|
|
1029
|
+
original_json: str,
|
|
1030
|
+
translated_json: str,
|
|
1031
|
+
interaction_info: dict[str, Any],
|
|
1032
|
+
) -> str:
|
|
1033
|
+
"""使用翻译后的内容重构交互块
|
|
879
1034
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
interaction_content = self._build_interaction_format(tool_args)
|
|
886
|
-
result.content = interaction_content
|
|
887
|
-
|
|
888
|
-
# If transformed to interaction, return as is
|
|
889
|
-
if result.transformed_to_interaction:
|
|
890
|
-
return result
|
|
891
|
-
|
|
892
|
-
# If not transformed, continue with normal processing using standard content messages
|
|
893
|
-
normal_messages = self._build_content_messages(block_index, variables)
|
|
1035
|
+
Args:
|
|
1036
|
+
original_content: 原始交互内容
|
|
1037
|
+
original_json: 原始的可翻译 JSON(翻译前)
|
|
1038
|
+
translated_json: 翻译后的 JSON 字符串
|
|
1039
|
+
interaction_info: 交互信息字典
|
|
894
1040
|
|
|
895
|
-
|
|
1041
|
+
Returns:
|
|
1042
|
+
str: 重构后的交互内容
|
|
1043
|
+
"""
|
|
1044
|
+
import json
|
|
896
1045
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
1046
|
+
# 解析原始 JSON
|
|
1047
|
+
try:
|
|
1048
|
+
original = json.loads(original_json)
|
|
1049
|
+
except json.JSONDecodeError:
|
|
1050
|
+
return original_content
|
|
1051
|
+
|
|
1052
|
+
# 解析翻译后的 JSON
|
|
1053
|
+
try:
|
|
1054
|
+
translated = json.loads(translated_json)
|
|
1055
|
+
except json.JSONDecodeError:
|
|
1056
|
+
return original_content
|
|
1057
|
+
|
|
1058
|
+
reconstructed = original_content
|
|
1059
|
+
|
|
1060
|
+
# 替换按钮 Display 文本(智能处理 Value)
|
|
1061
|
+
if "buttons" in translated and interaction_info.get("buttons"):
|
|
1062
|
+
for i, button in enumerate(interaction_info["buttons"]):
|
|
1063
|
+
if i < len(translated["buttons"]):
|
|
1064
|
+
old_display = button["display"]
|
|
1065
|
+
new_display = translated["buttons"][i]
|
|
1066
|
+
|
|
1067
|
+
# 检测是否发生了翻译
|
|
1068
|
+
translation_happened = False
|
|
1069
|
+
if "buttons" in original and i < len(original["buttons"]):
|
|
1070
|
+
if original["buttons"][i] != new_display:
|
|
1071
|
+
translation_happened = True
|
|
1072
|
+
|
|
1073
|
+
# 如果有 Value 分离(display//value 格式),保留 value
|
|
1074
|
+
if button["display"] != button["value"]:
|
|
1075
|
+
# 已有 value 分离,按原逻辑处理
|
|
1076
|
+
# 替换格式:oldDisplay//value -> newDisplay//value
|
|
1077
|
+
old_pattern = f"{old_display}//{button['value']}"
|
|
1078
|
+
new_pattern = f"{new_display}//{button['value']}"
|
|
1079
|
+
reconstructed = reconstructed.replace(old_pattern, new_pattern, 1)
|
|
1080
|
+
elif translation_happened:
|
|
1081
|
+
# 没有 value 分离,但发生了翻译
|
|
1082
|
+
# 自动添加 value:翻译后//原始
|
|
1083
|
+
old_pattern = old_display
|
|
1084
|
+
new_pattern = f"{new_display}//{old_display}"
|
|
1085
|
+
reconstructed = reconstructed.replace(old_pattern, new_pattern, 1)
|
|
1086
|
+
else:
|
|
1087
|
+
# 没有翻译,保持原样
|
|
1088
|
+
reconstructed = reconstructed.replace(old_display, new_display, 1)
|
|
901
1089
|
|
|
902
|
-
|
|
1090
|
+
# 替换问题文本
|
|
1091
|
+
if "question" in translated and interaction_info.get("question"):
|
|
1092
|
+
old_question = interaction_info["question"]
|
|
1093
|
+
new_question = translated["question"]
|
|
1094
|
+
reconstructed = reconstructed.replace(f"...{old_question}", f"...{new_question}", 1)
|
|
903
1095
|
|
|
904
|
-
|
|
905
|
-
normal_result = self._llm_provider.complete(normal_messages)
|
|
906
|
-
return LLMResult(content=normal_result.content, prompt=normal_messages[-1]["content"], metadata=normal_result.metadata)
|
|
1096
|
+
return reconstructed
|
|
907
1097
|
|
|
908
|
-
def
|
|
1098
|
+
def _build_validation_messages(
|
|
909
1099
|
self,
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1100
|
+
block_index: int,
|
|
1101
|
+
user_input: dict[str, list[str]],
|
|
1102
|
+
target_variable: str,
|
|
1103
|
+
context: list[dict[str, str]] | None = None,
|
|
913
1104
|
) -> list[dict[str, str]]:
|
|
914
|
-
"""
|
|
915
|
-
|
|
916
|
-
import json
|
|
917
|
-
|
|
918
|
-
# System prompt for detection
|
|
919
|
-
system_prompt = """You are an intelligent document processing assistant specializing in creating interactive forms.
|
|
920
|
-
|
|
921
|
-
Task: Analyze the given content block and determine if it needs to be converted to an interaction block to collect user information.
|
|
922
|
-
|
|
923
|
-
Judgment criteria:
|
|
924
|
-
1. Does the content imply the need to ask users for information?
|
|
925
|
-
2. Does it need to collect detailed information based on previous variable values?
|
|
926
|
-
3. Does it mention "recording" or "saving" information to variables?
|
|
927
|
-
|
|
928
|
-
If conversion is needed, generate a STANDARD interaction block format with SPECIFIC options based on the document-level instructions and context provided in the user message."""
|
|
929
|
-
|
|
930
|
-
# User message with content and context
|
|
931
|
-
# Build user prompt with document context
|
|
932
|
-
user_prompt_parts = []
|
|
933
|
-
|
|
934
|
-
# Add document-level prompt context if exists
|
|
935
|
-
if self._document_prompt:
|
|
936
|
-
user_prompt_parts.append(f"""Document-level instructions:
|
|
937
|
-
{self._document_prompt}
|
|
938
|
-
|
|
939
|
-
(Note: The above are the user's document-level instructions that provide context and requirements for processing.)
|
|
940
|
-
""")
|
|
941
|
-
|
|
942
|
-
# Prepare content analysis with both original and resolved versions
|
|
943
|
-
original_content = block.content
|
|
944
|
-
|
|
945
|
-
# Create resolved content with variable substitution for better context
|
|
946
|
-
resolved_content = original_content
|
|
947
|
-
if variables:
|
|
948
|
-
from .utils import replace_variables_in_text
|
|
949
|
-
|
|
950
|
-
resolved_content = replace_variables_in_text(original_content, variables)
|
|
951
|
-
|
|
952
|
-
content_analysis = f"""Current content block to analyze:
|
|
953
|
-
|
|
954
|
-
**Original content (shows variable structure):**
|
|
955
|
-
{original_content}
|
|
956
|
-
|
|
957
|
-
**Resolved content (with current variable values):**
|
|
958
|
-
{resolved_content}
|
|
959
|
-
|
|
960
|
-
**Existing variable values:**
|
|
961
|
-
{json.dumps(variables, ensure_ascii=False) if variables else "None"}"""
|
|
962
|
-
|
|
963
|
-
# Add different analysis based on whether content has variables
|
|
964
|
-
if "{{" in original_content and "}}" in original_content:
|
|
965
|
-
from .utils import extract_variables_from_text
|
|
966
|
-
|
|
967
|
-
content_variables = set(extract_variables_from_text(original_content))
|
|
968
|
-
|
|
969
|
-
# Find new variables (not yet collected)
|
|
970
|
-
new_variables = content_variables - (set(variables.keys()) if variables else set())
|
|
971
|
-
existing_used_variables = content_variables & (set(variables.keys()) if variables else set())
|
|
972
|
-
|
|
973
|
-
content_analysis += f"""
|
|
974
|
-
|
|
975
|
-
**Variable analysis:**
|
|
976
|
-
- Variables used from previous steps: {list(existing_used_variables) if existing_used_variables else "None"}
|
|
977
|
-
- New variables to collect: {list(new_variables) if new_variables else "None"}
|
|
978
|
-
|
|
979
|
-
**Context guidance:**
|
|
980
|
-
- Use the resolved content to understand the actual context and requirements
|
|
981
|
-
- Generate options based on the real variable values shown in the resolved content
|
|
982
|
-
- Collect user input for the new variables identified above"""
|
|
983
|
-
|
|
984
|
-
user_prompt_parts.append(content_analysis)
|
|
985
|
-
|
|
986
|
-
# Add analysis requirements and structured output guide
|
|
987
|
-
user_prompt_parts.append("""## Analysis Task:
|
|
988
|
-
1. Determine if this content needs to be converted to an interaction block
|
|
989
|
-
2. If conversion is needed, provide structured interaction data
|
|
1105
|
+
"""
|
|
1106
|
+
Build validation messages with new structure.
|
|
990
1107
|
|
|
991
|
-
|
|
992
|
-
-
|
|
993
|
-
-
|
|
994
|
-
-
|
|
995
|
-
-
|
|
1108
|
+
System message contains:
|
|
1109
|
+
- VALIDATION_TASK_TEMPLATE (includes task description and output language rules)
|
|
1110
|
+
- Question context (if exists)
|
|
1111
|
+
- Button options context (if exists)
|
|
1112
|
+
- VALIDATION_REQUIREMENTS_TEMPLATE
|
|
1113
|
+
- document_prompt wrapped in <document_context> tags (if exists)
|
|
996
1114
|
|
|
997
|
-
|
|
998
|
-
|
|
1115
|
+
User message contains:
|
|
1116
|
+
- User input only
|
|
1117
|
+
"""
|
|
1118
|
+
from .parser import InteractionParser, extract_interaction_question
|
|
999
1119
|
|
|
1000
|
-
|
|
1001
|
-
- Food dishes (can order multiple: 宫保鸡丁, 麻婆豆腐)
|
|
1002
|
-
- Programming skills (can know multiple: Python, JavaScript)
|
|
1003
|
-
- Interests/hobbies (can have multiple: 读书, 运动, 旅游)
|
|
1004
|
-
- Product features (can want multiple: 定制颜色, 个性化logo)
|
|
1005
|
-
- Exercise types (can do multiple: 跑步, 游泳, 瑜伽)
|
|
1120
|
+
block = self.get_block(block_index)
|
|
1006
1121
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
- Budget ranges (have one range: 5-10万 OR 10-20万)
|
|
1011
|
-
- Education levels (have one highest: Bachelor's OR Master's)
|
|
1122
|
+
# Extract user input values for target variable
|
|
1123
|
+
target_values = user_input.get(target_variable, [])
|
|
1124
|
+
user_input_str = ", ".join(target_values) if target_values else ""
|
|
1012
1125
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
- `interaction_type`: "single_select", "multi_select", "text_input", or "mixed"
|
|
1018
|
-
- `options`: array of 3-4 specific options based on context
|
|
1019
|
-
- `allow_text_input`: true if you want to include "other" option
|
|
1020
|
-
- `text_input_prompt`: text for the "other" option (in appropriate language)
|
|
1126
|
+
# Build System Message (contains all validation rules and context)
|
|
1127
|
+
# VALIDATION_TASK_TEMPLATE already includes system message, directly replace variables
|
|
1128
|
+
task_template = VALIDATION_TASK_TEMPLATE.replace("{target_variable}", target_variable)
|
|
1129
|
+
system_parts = [task_template]
|
|
1021
1130
|
|
|
1022
|
-
|
|
1131
|
+
# Extract interaction question
|
|
1132
|
+
interaction_question = extract_interaction_question(block.content)
|
|
1023
1133
|
|
|
1024
|
-
|
|
1134
|
+
# Add question context (if exists)
|
|
1135
|
+
if interaction_question:
|
|
1136
|
+
question_context = CONTEXT_QUESTION_TEMPLATE.format(question=interaction_question)
|
|
1137
|
+
system_parts.append("")
|
|
1138
|
+
system_parts.append(question_context)
|
|
1025
1139
|
|
|
1026
|
-
|
|
1140
|
+
# Parse interaction to extract button information
|
|
1141
|
+
parser = InteractionParser()
|
|
1142
|
+
parse_result = parser.parse(block.content)
|
|
1143
|
+
buttons = parse_result.get("buttons") if "buttons" in parse_result else None
|
|
1144
|
+
|
|
1145
|
+
# Add button options context (if exists)
|
|
1146
|
+
if buttons:
|
|
1147
|
+
button_displays = [btn.get("display", "") for btn in buttons if btn.get("display")]
|
|
1148
|
+
if button_displays:
|
|
1149
|
+
button_options = "、".join(button_displays)
|
|
1150
|
+
button_context = CONTEXT_BUTTON_OPTIONS_TEMPLATE.format(button_options=button_options)
|
|
1151
|
+
system_parts.append("")
|
|
1152
|
+
system_parts.append(button_context)
|
|
1153
|
+
|
|
1154
|
+
# Add extraction requirements (using template)
|
|
1155
|
+
system_parts.append("")
|
|
1156
|
+
system_parts.append(VALIDATION_REQUIREMENTS_TEMPLATE)
|
|
1157
|
+
|
|
1158
|
+
# Add document_prompt (if exists)
|
|
1159
|
+
if self._document_prompt:
|
|
1160
|
+
system_parts.append("")
|
|
1161
|
+
system_parts.append("<document_context>")
|
|
1162
|
+
system_parts.append(self._document_prompt)
|
|
1163
|
+
system_parts.append("</document_context>")
|
|
1027
1164
|
|
|
1028
|
-
|
|
1029
|
-
if context:
|
|
1030
|
-
messages.extend(context)
|
|
1165
|
+
system_content = "\n".join(system_parts)
|
|
1031
1166
|
|
|
1032
|
-
|
|
1167
|
+
# Build message list
|
|
1168
|
+
messages = [
|
|
1169
|
+
{"role": "system", "content": system_content},
|
|
1170
|
+
{"role": "user", "content": user_input_str}, # Only user input
|
|
1171
|
+
]
|
|
1033
1172
|
|
|
1034
1173
|
return messages
|
|
1035
1174
|
|
|
1036
|
-
def
|
|
1037
|
-
"""Build MarkdownFlow interaction format from structured Function Calling data."""
|
|
1038
|
-
variable_name = tool_args.get("variable_name", "")
|
|
1039
|
-
interaction_type = tool_args.get("interaction_type", "single_select")
|
|
1040
|
-
options = tool_args.get("options", [])
|
|
1041
|
-
allow_text_input = tool_args.get("allow_text_input", False)
|
|
1042
|
-
text_input_prompt = tool_args.get("text_input_prompt", "...请输入")
|
|
1043
|
-
|
|
1044
|
-
if not variable_name:
|
|
1045
|
-
return ""
|
|
1046
|
-
|
|
1047
|
-
# For text_input type, options can be empty
|
|
1048
|
-
if interaction_type != "text_input" and not options:
|
|
1049
|
-
return ""
|
|
1050
|
-
|
|
1051
|
-
# Choose separator based on interaction type
|
|
1052
|
-
if interaction_type in ["multi_select", "mixed"]:
|
|
1053
|
-
separator = "||"
|
|
1054
|
-
else:
|
|
1055
|
-
separator = "|"
|
|
1056
|
-
|
|
1057
|
-
# Build options string
|
|
1058
|
-
if interaction_type == "text_input":
|
|
1059
|
-
# Text input only
|
|
1060
|
-
options_str = f"...{text_input_prompt}"
|
|
1061
|
-
else:
|
|
1062
|
-
# Options with potential text input
|
|
1063
|
-
options_str = separator.join(options)
|
|
1064
|
-
|
|
1065
|
-
if allow_text_input and text_input_prompt:
|
|
1066
|
-
# Ensure text input has ... prefix
|
|
1067
|
-
text_option = text_input_prompt if text_input_prompt.startswith("...") else f"...{text_input_prompt}"
|
|
1068
|
-
options_str += f"{separator}{text_option}"
|
|
1069
|
-
|
|
1070
|
-
return f"?[%{{{{{variable_name}}}}} {options_str}]"
|
|
1071
|
-
|
|
1072
|
-
def _process_dynamic_interaction_validation(
|
|
1175
|
+
def _build_error_render_messages(
|
|
1073
1176
|
self,
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
variables: dict[str, str | list[str]] | None,
|
|
1080
|
-
) -> LLMResult:
|
|
1081
|
-
"""Validate user input for dynamically generated interaction blocks using same logic as normal interactions."""
|
|
1082
|
-
_ = block_index # Mark as intentionally unused
|
|
1083
|
-
_ = context # Mark as intentionally unused
|
|
1084
|
-
|
|
1085
|
-
from .utils import InteractionParser
|
|
1086
|
-
|
|
1087
|
-
# Parse the interaction format using the same parser as normal interactions
|
|
1088
|
-
parser = InteractionParser()
|
|
1089
|
-
parse_result = parser.parse(interaction_format)
|
|
1090
|
-
|
|
1091
|
-
if "error" in parse_result:
|
|
1092
|
-
error_msg = f"Invalid interaction format: {parse_result['error']}"
|
|
1093
|
-
return self._render_error(error_msg, mode)
|
|
1177
|
+
error_message: str,
|
|
1178
|
+
context: list[dict[str, str]] | None = None,
|
|
1179
|
+
) -> list[dict[str, str]]:
|
|
1180
|
+
"""Build error rendering messages."""
|
|
1181
|
+
render_prompt = f"""{self._interaction_error_prompt}
|
|
1094
1182
|
|
|
1095
|
-
|
|
1096
|
-
variable_name = parse_result.get("variable")
|
|
1097
|
-
interaction_type = parse_result.get("type")
|
|
1183
|
+
Original Error: {error_message}
|
|
1098
1184
|
|
|
1099
|
-
|
|
1100
|
-
error_msg = f"No variable found in interaction format: {interaction_format}"
|
|
1101
|
-
return self._render_error(error_msg, mode)
|
|
1185
|
+
{INTERACTION_ERROR_RENDER_INSTRUCTIONS}"""
|
|
1102
1186
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1187
|
+
messages = []
|
|
1188
|
+
if self._document_prompt:
|
|
1189
|
+
messages.append({"role": "system", "content": self._document_prompt})
|
|
1105
1190
|
|
|
1106
|
-
|
|
1107
|
-
if not target_values:
|
|
1108
|
-
# Check if this is a text input or allows empty input
|
|
1109
|
-
allow_text_input = interaction_type in [
|
|
1110
|
-
InteractionType.BUTTONS_WITH_TEXT,
|
|
1111
|
-
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
1112
|
-
]
|
|
1191
|
+
messages.append({"role": "system", "content": render_prompt})
|
|
1113
1192
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
return LLMResult(
|
|
1119
|
-
content="",
|
|
1120
|
-
variables=merged_variables,
|
|
1121
|
-
metadata={
|
|
1122
|
-
"interaction_type": "dynamic_interaction",
|
|
1123
|
-
"empty_input": True,
|
|
1124
|
-
},
|
|
1125
|
-
)
|
|
1126
|
-
else:
|
|
1127
|
-
error_msg = f"No input provided for variable '{variable_name}'"
|
|
1128
|
-
return self._render_error(error_msg, mode)
|
|
1193
|
+
# Add conversation history context if provided
|
|
1194
|
+
truncated_context = self._truncate_context(context)
|
|
1195
|
+
if truncated_context:
|
|
1196
|
+
messages.extend(truncated_context)
|
|
1129
1197
|
|
|
1130
|
-
|
|
1131
|
-
if interaction_type in [
|
|
1132
|
-
InteractionType.BUTTONS_ONLY,
|
|
1133
|
-
InteractionType.BUTTONS_WITH_TEXT,
|
|
1134
|
-
InteractionType.BUTTONS_MULTI_SELECT,
|
|
1135
|
-
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
1136
|
-
]:
|
|
1137
|
-
# Button validation - reuse the existing button validation logic
|
|
1138
|
-
button_result = self._process_button_validation(
|
|
1139
|
-
parse_result,
|
|
1140
|
-
target_values,
|
|
1141
|
-
variable_name,
|
|
1142
|
-
mode,
|
|
1143
|
-
interaction_type,
|
|
1144
|
-
)
|
|
1198
|
+
messages.append({"role": "user", "content": error_message})
|
|
1145
1199
|
|
|
1146
|
-
|
|
1147
|
-
if hasattr(button_result, 'variables') and button_result.variables is not None and variables:
|
|
1148
|
-
merged_variables = dict(variables)
|
|
1149
|
-
merged_variables.update(button_result.variables)
|
|
1150
|
-
return LLMResult(
|
|
1151
|
-
content=button_result.content,
|
|
1152
|
-
variables=merged_variables,
|
|
1153
|
-
metadata=button_result.metadata,
|
|
1154
|
-
)
|
|
1155
|
-
return button_result
|
|
1200
|
+
return messages
|
|
1156
1201
|
|
|
1157
|
-
|
|
1158
|
-
# Non-assignment buttons: don't set variables, keep existing ones
|
|
1159
|
-
return LLMResult(
|
|
1160
|
-
content="",
|
|
1161
|
-
variables=dict(variables or {}),
|
|
1162
|
-
metadata={
|
|
1163
|
-
"interaction_type": "non_assignment_button",
|
|
1164
|
-
"user_input": user_input,
|
|
1165
|
-
},
|
|
1166
|
-
)
|
|
1167
|
-
else:
|
|
1168
|
-
# Text-only input type - merge with existing variables
|
|
1169
|
-
merged_variables = dict(variables or {})
|
|
1170
|
-
merged_variables[variable_name] = target_values
|
|
1171
|
-
return LLMResult(
|
|
1172
|
-
content="",
|
|
1173
|
-
variables=merged_variables,
|
|
1174
|
-
metadata={
|
|
1175
|
-
"interaction_type": "text_only",
|
|
1176
|
-
"target_variable": variable_name,
|
|
1177
|
-
"values": target_values,
|
|
1178
|
-
},
|
|
1179
|
-
)
|
|
1202
|
+
# Helper methods
|