markdown-flow 0.2.16__py3-none-any.whl → 0.2.26__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.
Potentially problematic release.
This version of markdown-flow might be problematic. Click here for more details.
- markdown_flow/__init__.py +6 -7
- markdown_flow/constants.py +52 -20
- markdown_flow/core.py +359 -544
- markdown_flow/llm.py +10 -12
- markdown_flow/models.py +1 -1
- markdown_flow/parser/__init__.py +34 -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/validation.py +121 -0
- markdown_flow/parser/variable.py +95 -0
- markdown_flow/providers/__init__.py +15 -0
- markdown_flow/providers/config.py +51 -0
- markdown_flow/providers/openai.py +371 -0
- markdown_flow/utils.py +49 -51
- {markdown_flow-0.2.16.dist-info → markdown_flow-0.2.26.dist-info}/METADATA +18 -107
- markdown_flow-0.2.26.dist-info/RECORD +22 -0
- markdown_flow-0.2.16.dist-info/RECORD +0 -13
- {markdown_flow-0.2.16.dist-info → markdown_flow-0.2.26.dist-info}/WHEEL +0 -0
- {markdown_flow-0.2.16.dist-info → markdown_flow-0.2.26.dist-info}/licenses/LICENSE +0 -0
- {markdown_flow-0.2.16.dist-info → markdown_flow-0.2.26.dist-info}/top_level.txt +0 -0
markdown_flow/core.py
CHANGED
|
@@ -18,6 +18,7 @@ from .constants import (
|
|
|
18
18
|
COMPILED_INTERACTION_CONTENT_RECONSTRUCT_REGEX,
|
|
19
19
|
COMPILED_VARIABLE_REFERENCE_CLEANUP_REGEX,
|
|
20
20
|
COMPILED_WHITESPACE_CLEANUP_REGEX,
|
|
21
|
+
DEFAULT_BASE_SYSTEM_PROMPT,
|
|
21
22
|
DEFAULT_INTERACTION_ERROR_PROMPT,
|
|
22
23
|
DEFAULT_INTERACTION_PROMPT,
|
|
23
24
|
DEFAULT_VALIDATION_SYSTEM_MESSAGE,
|
|
@@ -28,13 +29,14 @@ from .constants import (
|
|
|
28
29
|
INTERACTION_PATTERN_SPLIT,
|
|
29
30
|
INTERACTION_RENDER_INSTRUCTIONS,
|
|
30
31
|
LLM_PROVIDER_REQUIRED_ERROR,
|
|
32
|
+
OUTPUT_INSTRUCTION_EXPLANATION,
|
|
31
33
|
UNSUPPORTED_PROMPT_TYPE_ERROR,
|
|
32
34
|
)
|
|
33
35
|
from .enums import BlockType
|
|
34
36
|
from .exceptions import BlockIndexError
|
|
35
37
|
from .llm import LLMProvider, LLMResult, ProcessMode
|
|
36
38
|
from .models import Block, InteractionValidationConfig
|
|
37
|
-
from .
|
|
39
|
+
from .parser import (
|
|
38
40
|
InteractionParser,
|
|
39
41
|
InteractionType,
|
|
40
42
|
extract_interaction_question,
|
|
@@ -59,48 +61,105 @@ class MarkdownFlow:
|
|
|
59
61
|
_document_prompt: str | None
|
|
60
62
|
_interaction_prompt: str | None
|
|
61
63
|
_interaction_error_prompt: str | None
|
|
64
|
+
_max_context_length: int
|
|
62
65
|
_blocks: list[Block] | None
|
|
63
66
|
_interaction_configs: dict[int, InteractionValidationConfig]
|
|
67
|
+
_model: str | None
|
|
68
|
+
_temperature: float | None
|
|
64
69
|
|
|
65
70
|
def __init__(
|
|
66
71
|
self,
|
|
67
72
|
document: str,
|
|
68
73
|
llm_provider: LLMProvider | None = None,
|
|
74
|
+
base_system_prompt: str | None = None,
|
|
69
75
|
document_prompt: str | None = None,
|
|
70
76
|
interaction_prompt: str | None = None,
|
|
71
77
|
interaction_error_prompt: str | None = None,
|
|
78
|
+
max_context_length: int = 0,
|
|
72
79
|
):
|
|
73
80
|
"""
|
|
74
81
|
Initialize MarkdownFlow instance.
|
|
75
82
|
|
|
76
83
|
Args:
|
|
77
84
|
document: Markdown document content
|
|
78
|
-
llm_provider: LLM provider
|
|
85
|
+
llm_provider: LLM provider (required for COMPLETE and STREAM modes)
|
|
86
|
+
base_system_prompt: MarkdownFlow base system prompt (framework-level, content blocks only)
|
|
79
87
|
document_prompt: Document-level system prompt
|
|
80
88
|
interaction_prompt: Interaction content rendering prompt
|
|
81
89
|
interaction_error_prompt: Interaction error rendering prompt
|
|
90
|
+
max_context_length: Maximum number of context messages to keep (0 = unlimited)
|
|
82
91
|
"""
|
|
83
92
|
self._document = document
|
|
84
93
|
self._llm_provider = llm_provider
|
|
94
|
+
self._base_system_prompt = base_system_prompt or DEFAULT_BASE_SYSTEM_PROMPT
|
|
85
95
|
self._document_prompt = document_prompt
|
|
86
96
|
self._interaction_prompt = interaction_prompt or DEFAULT_INTERACTION_PROMPT
|
|
87
97
|
self._interaction_error_prompt = interaction_error_prompt or DEFAULT_INTERACTION_ERROR_PROMPT
|
|
98
|
+
self._max_context_length = max_context_length
|
|
88
99
|
self._blocks = None
|
|
89
100
|
self._interaction_configs: dict[int, InteractionValidationConfig] = {}
|
|
101
|
+
self._model: str | None = None
|
|
102
|
+
self._temperature: float | None = None
|
|
90
103
|
|
|
91
104
|
def set_llm_provider(self, provider: LLMProvider) -> None:
|
|
92
105
|
"""Set LLM provider."""
|
|
93
106
|
self._llm_provider = provider
|
|
94
107
|
|
|
108
|
+
def set_model(self, model: str) -> "MarkdownFlow":
|
|
109
|
+
"""
|
|
110
|
+
Set model name for this instance.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
model: Model name to use
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Self for method chaining
|
|
117
|
+
"""
|
|
118
|
+
self._model = model
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def set_temperature(self, temperature: float) -> "MarkdownFlow":
|
|
122
|
+
"""
|
|
123
|
+
Set temperature for this instance.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
temperature: Temperature value (typically 0.0-2.0)
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Self for method chaining
|
|
130
|
+
"""
|
|
131
|
+
self._temperature = temperature
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
def get_model(self) -> str | None:
|
|
135
|
+
"""
|
|
136
|
+
Get model name for this instance.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Model name if set, None otherwise
|
|
140
|
+
"""
|
|
141
|
+
return self._model
|
|
142
|
+
|
|
143
|
+
def get_temperature(self) -> float | None:
|
|
144
|
+
"""
|
|
145
|
+
Get temperature for this instance.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Temperature value if set, None otherwise
|
|
149
|
+
"""
|
|
150
|
+
return self._temperature
|
|
151
|
+
|
|
95
152
|
def set_prompt(self, prompt_type: str, value: str | None) -> None:
|
|
96
153
|
"""
|
|
97
154
|
Set prompt template.
|
|
98
155
|
|
|
99
156
|
Args:
|
|
100
|
-
prompt_type: Prompt type ('document', 'interaction', 'interaction_error')
|
|
157
|
+
prompt_type: Prompt type ('base_system', 'document', 'interaction', 'interaction_error')
|
|
101
158
|
value: Prompt content
|
|
102
159
|
"""
|
|
103
|
-
if prompt_type == "
|
|
160
|
+
if prompt_type == "base_system":
|
|
161
|
+
self._base_system_prompt = value or DEFAULT_BASE_SYSTEM_PROMPT
|
|
162
|
+
elif prompt_type == "document":
|
|
104
163
|
self._document_prompt = value
|
|
105
164
|
elif prompt_type == "interaction":
|
|
106
165
|
self._interaction_prompt = value or DEFAULT_INTERACTION_PROMPT
|
|
@@ -109,6 +168,44 @@ class MarkdownFlow:
|
|
|
109
168
|
else:
|
|
110
169
|
raise ValueError(UNSUPPORTED_PROMPT_TYPE_ERROR.format(prompt_type=prompt_type))
|
|
111
170
|
|
|
171
|
+
def _truncate_context(
|
|
172
|
+
self,
|
|
173
|
+
context: list[dict[str, str]] | None,
|
|
174
|
+
) -> list[dict[str, str]] | None:
|
|
175
|
+
"""
|
|
176
|
+
Filter and truncate context to specified maximum length.
|
|
177
|
+
|
|
178
|
+
Processing steps:
|
|
179
|
+
1. Filter out messages with empty content (empty string or whitespace only)
|
|
180
|
+
2. Truncate to max_context_length if configured (0 = unlimited)
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
context: Original context list
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Filtered and truncated context. Returns None if no valid messages remain.
|
|
187
|
+
"""
|
|
188
|
+
if not context:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
# Step 1: Filter out messages with empty or whitespace-only content
|
|
192
|
+
filtered_context = [msg for msg in context if msg.get("content", "").strip()]
|
|
193
|
+
|
|
194
|
+
# Return None if no valid messages remain after filtering
|
|
195
|
+
if not filtered_context:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
# Step 2: Truncate to max_context_length if configured
|
|
199
|
+
if self._max_context_length == 0:
|
|
200
|
+
# No limit, return all filtered messages
|
|
201
|
+
return filtered_context
|
|
202
|
+
|
|
203
|
+
# Keep the most recent N messages
|
|
204
|
+
if len(filtered_context) > self._max_context_length:
|
|
205
|
+
return filtered_context[-self._max_context_length :]
|
|
206
|
+
|
|
207
|
+
return filtered_context
|
|
208
|
+
|
|
112
209
|
@property
|
|
113
210
|
def document(self) -> str:
|
|
114
211
|
"""Get document content."""
|
|
@@ -183,8 +280,7 @@ class MarkdownFlow:
|
|
|
183
280
|
context: list[dict[str, str]] | None = None,
|
|
184
281
|
variables: dict[str, str | list[str]] | None = None,
|
|
185
282
|
user_input: dict[str, list[str]] | None = None,
|
|
186
|
-
|
|
187
|
-
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
283
|
+
):
|
|
188
284
|
"""
|
|
189
285
|
Unified block processing interface.
|
|
190
286
|
|
|
@@ -194,11 +290,14 @@ class MarkdownFlow:
|
|
|
194
290
|
context: Context message list
|
|
195
291
|
variables: Variable mappings
|
|
196
292
|
user_input: User input (for interaction blocks)
|
|
197
|
-
dynamic_interaction_format: Dynamic interaction format for validation
|
|
198
293
|
|
|
199
294
|
Returns:
|
|
200
295
|
LLMResult or Generator[LLMResult, None, None]
|
|
201
296
|
"""
|
|
297
|
+
# Process base_system_prompt variable replacement
|
|
298
|
+
if self._base_system_prompt:
|
|
299
|
+
self._base_system_prompt = replace_variables_in_text(self._base_system_prompt, variables or {})
|
|
300
|
+
|
|
202
301
|
# Process document_prompt variable replacement
|
|
203
302
|
if self._document_prompt:
|
|
204
303
|
self._document_prompt = replace_variables_in_text(self._document_prompt, variables or {})
|
|
@@ -206,16 +305,12 @@ class MarkdownFlow:
|
|
|
206
305
|
block = self.get_block(block_index)
|
|
207
306
|
|
|
208
307
|
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
308
|
return self._process_content(block_index, mode, context, variables)
|
|
214
309
|
|
|
215
310
|
if block.block_type == BlockType.INTERACTION:
|
|
216
311
|
if user_input is None:
|
|
217
312
|
# Render interaction content
|
|
218
|
-
return self._process_interaction_render(block_index, mode, variables)
|
|
313
|
+
return self._process_interaction_render(block_index, mode, context, variables)
|
|
219
314
|
# Process user input
|
|
220
315
|
return self._process_interaction_input(block_index, user_input, mode, context, variables)
|
|
221
316
|
|
|
@@ -234,37 +329,27 @@ class MarkdownFlow:
|
|
|
234
329
|
mode: ProcessMode,
|
|
235
330
|
context: list[dict[str, str]] | None,
|
|
236
331
|
variables: dict[str, str | list[str]] | None,
|
|
237
|
-
)
|
|
332
|
+
):
|
|
238
333
|
"""Process content block."""
|
|
334
|
+
# Truncate context to configured maximum length
|
|
335
|
+
truncated_context = self._truncate_context(context)
|
|
239
336
|
|
|
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)
|
|
337
|
+
# Build messages with context
|
|
338
|
+
messages = self._build_content_messages(block_index, variables, truncated_context)
|
|
254
339
|
|
|
255
340
|
if mode == ProcessMode.COMPLETE:
|
|
256
341
|
if not self._llm_provider:
|
|
257
342
|
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
258
343
|
|
|
259
|
-
|
|
260
|
-
return LLMResult(content=
|
|
344
|
+
content = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
345
|
+
return LLMResult(content=content, prompt=messages[-1]["content"])
|
|
261
346
|
|
|
262
347
|
if mode == ProcessMode.STREAM:
|
|
263
348
|
if not self._llm_provider:
|
|
264
349
|
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
265
350
|
|
|
266
351
|
def stream_generator():
|
|
267
|
-
for chunk in self._llm_provider.stream(messages):
|
|
352
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
268
353
|
yield LLMResult(content=chunk, prompt=messages[-1]["content"])
|
|
269
354
|
|
|
270
355
|
return stream_generator()
|
|
@@ -281,7 +366,13 @@ class MarkdownFlow:
|
|
|
281
366
|
|
|
282
367
|
return LLMResult(content=content)
|
|
283
368
|
|
|
284
|
-
def _process_interaction_render(
|
|
369
|
+
def _process_interaction_render(
|
|
370
|
+
self,
|
|
371
|
+
block_index: int,
|
|
372
|
+
mode: ProcessMode,
|
|
373
|
+
context: list[dict[str, str]] | None = None,
|
|
374
|
+
variables: dict[str, str | list[str]] | None = None,
|
|
375
|
+
):
|
|
285
376
|
"""Process interaction content rendering."""
|
|
286
377
|
block = self.get_block(block_index)
|
|
287
378
|
|
|
@@ -298,24 +389,17 @@ class MarkdownFlow:
|
|
|
298
389
|
# Unable to extract, return processed content
|
|
299
390
|
return LLMResult(content=processed_block.content)
|
|
300
391
|
|
|
301
|
-
#
|
|
302
|
-
|
|
392
|
+
# Truncate context to configured maximum length
|
|
393
|
+
truncated_context = self._truncate_context(context)
|
|
303
394
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
prompt=messages[-1]["content"],
|
|
307
|
-
metadata={
|
|
308
|
-
"original_content": processed_block.content,
|
|
309
|
-
"question_text": question_text,
|
|
310
|
-
},
|
|
311
|
-
)
|
|
395
|
+
# Build render messages with context
|
|
396
|
+
messages = self._build_interaction_render_messages(question_text, truncated_context)
|
|
312
397
|
|
|
313
398
|
if mode == ProcessMode.COMPLETE:
|
|
314
399
|
if not self._llm_provider:
|
|
315
400
|
return LLMResult(content=processed_block.content) # Fallback processing
|
|
316
401
|
|
|
317
|
-
|
|
318
|
-
rendered_question = result.content
|
|
402
|
+
rendered_question = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
319
403
|
rendered_content = self._reconstruct_interaction_content(processed_block.content, rendered_question)
|
|
320
404
|
|
|
321
405
|
return LLMResult(
|
|
@@ -343,7 +427,7 @@ class MarkdownFlow:
|
|
|
343
427
|
# With LLM provider, collect full response then return once
|
|
344
428
|
def stream_generator():
|
|
345
429
|
full_response = ""
|
|
346
|
-
for chunk in self._llm_provider.stream(messages):
|
|
430
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
347
431
|
full_response += chunk
|
|
348
432
|
|
|
349
433
|
# Reconstruct final interaction content
|
|
@@ -366,14 +450,13 @@ class MarkdownFlow:
|
|
|
366
450
|
variables: dict[str, str | list[str]] | None = None,
|
|
367
451
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
368
452
|
"""Process interaction user input."""
|
|
369
|
-
_ = context # Mark as intentionally unused
|
|
370
453
|
block = self.get_block(block_index)
|
|
371
454
|
target_variable = block.variables[0] if block.variables else "user_input"
|
|
372
455
|
|
|
373
456
|
# Basic validation
|
|
374
457
|
if not user_input or not any(values for values in user_input.values()):
|
|
375
458
|
error_msg = INPUT_EMPTY_ERROR
|
|
376
|
-
return self._render_error(error_msg, mode)
|
|
459
|
+
return self._render_error(error_msg, mode, context)
|
|
377
460
|
|
|
378
461
|
# Get the target variable value from user_input
|
|
379
462
|
target_values = user_input.get(target_variable, [])
|
|
@@ -387,24 +470,99 @@ class MarkdownFlow:
|
|
|
387
470
|
|
|
388
471
|
if "error" in parse_result:
|
|
389
472
|
error_msg = INTERACTION_PARSE_ERROR.format(error=parse_result["error"])
|
|
390
|
-
return self._render_error(error_msg, mode)
|
|
473
|
+
return self._render_error(error_msg, mode, context)
|
|
391
474
|
|
|
392
475
|
interaction_type = parse_result.get("type")
|
|
393
476
|
|
|
394
477
|
# Process user input based on interaction type
|
|
395
478
|
if interaction_type in [
|
|
396
|
-
InteractionType.BUTTONS_ONLY,
|
|
397
479
|
InteractionType.BUTTONS_WITH_TEXT,
|
|
398
|
-
InteractionType.BUTTONS_MULTI_SELECT,
|
|
399
480
|
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
400
481
|
]:
|
|
401
|
-
#
|
|
482
|
+
# Buttons with text input: smart validation (match buttons first, then LLM validate custom text)
|
|
483
|
+
buttons = parse_result.get("buttons", [])
|
|
484
|
+
|
|
485
|
+
# Step 1: Match button values
|
|
486
|
+
matched_values, unmatched_values = self._match_button_values(buttons, target_values)
|
|
487
|
+
|
|
488
|
+
# Step 2: If there are unmatched values (custom text), validate with LLM
|
|
489
|
+
if unmatched_values:
|
|
490
|
+
# Create user_input for LLM validation (only custom text)
|
|
491
|
+
custom_input = {target_variable: unmatched_values}
|
|
492
|
+
|
|
493
|
+
validation_result = self._process_llm_validation(
|
|
494
|
+
block_index=block_index,
|
|
495
|
+
user_input=custom_input,
|
|
496
|
+
target_variable=target_variable,
|
|
497
|
+
mode=mode,
|
|
498
|
+
context=context,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Handle validation result based on mode
|
|
502
|
+
if mode == ProcessMode.COMPLETE:
|
|
503
|
+
# Check if validation passed
|
|
504
|
+
if isinstance(validation_result, LLMResult) and validation_result.variables:
|
|
505
|
+
validated_values = validation_result.variables.get(target_variable, [])
|
|
506
|
+
# Merge matched button values + validated custom text
|
|
507
|
+
all_values = matched_values + validated_values
|
|
508
|
+
return LLMResult(
|
|
509
|
+
content="",
|
|
510
|
+
variables={target_variable: all_values},
|
|
511
|
+
metadata={
|
|
512
|
+
"interaction_type": str(interaction_type),
|
|
513
|
+
"matched_button_values": matched_values,
|
|
514
|
+
"validated_custom_values": validated_values,
|
|
515
|
+
},
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
# Validation failed, return error
|
|
519
|
+
return validation_result
|
|
520
|
+
|
|
521
|
+
if mode == ProcessMode.STREAM:
|
|
522
|
+
# For stream mode, collect validation result
|
|
523
|
+
def stream_merge_generator():
|
|
524
|
+
# Consume the validation stream
|
|
525
|
+
for result in validation_result: # type: ignore[attr-defined]
|
|
526
|
+
if isinstance(result, LLMResult) and result.variables:
|
|
527
|
+
validated_values = result.variables.get(target_variable, [])
|
|
528
|
+
all_values = matched_values + validated_values
|
|
529
|
+
yield LLMResult(
|
|
530
|
+
content="",
|
|
531
|
+
variables={target_variable: all_values},
|
|
532
|
+
metadata={
|
|
533
|
+
"interaction_type": str(interaction_type),
|
|
534
|
+
"matched_button_values": matched_values,
|
|
535
|
+
"validated_custom_values": validated_values,
|
|
536
|
+
},
|
|
537
|
+
)
|
|
538
|
+
else:
|
|
539
|
+
# Validation failed
|
|
540
|
+
yield result
|
|
541
|
+
|
|
542
|
+
return stream_merge_generator()
|
|
543
|
+
else:
|
|
544
|
+
# All values matched buttons, return directly
|
|
545
|
+
return LLMResult(
|
|
546
|
+
content="",
|
|
547
|
+
variables={target_variable: matched_values},
|
|
548
|
+
metadata={
|
|
549
|
+
"interaction_type": str(interaction_type),
|
|
550
|
+
"all_matched_buttons": True,
|
|
551
|
+
},
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if interaction_type in [
|
|
555
|
+
InteractionType.BUTTONS_ONLY,
|
|
556
|
+
InteractionType.BUTTONS_MULTI_SELECT,
|
|
557
|
+
]:
|
|
558
|
+
# Pure button types: only basic button validation (no LLM)
|
|
402
559
|
return self._process_button_validation(
|
|
403
560
|
parse_result,
|
|
404
561
|
target_values,
|
|
405
562
|
target_variable,
|
|
406
563
|
mode,
|
|
407
564
|
interaction_type,
|
|
565
|
+
context,
|
|
408
566
|
)
|
|
409
567
|
|
|
410
568
|
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
@@ -420,19 +578,50 @@ class MarkdownFlow:
|
|
|
420
578
|
)
|
|
421
579
|
|
|
422
580
|
# Text-only input type: ?[%{{sys_user_nickname}}...question]
|
|
423
|
-
#
|
|
581
|
+
# Use LLM validation to check if input is relevant to the question
|
|
424
582
|
if target_values:
|
|
425
|
-
return
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
"values": target_values,
|
|
432
|
-
},
|
|
583
|
+
return self._process_llm_validation(
|
|
584
|
+
block_index=block_index,
|
|
585
|
+
user_input=user_input,
|
|
586
|
+
target_variable=target_variable,
|
|
587
|
+
mode=mode,
|
|
588
|
+
context=context,
|
|
433
589
|
)
|
|
434
590
|
error_msg = f"No input provided for variable '{target_variable}'"
|
|
435
|
-
return self._render_error(error_msg, mode)
|
|
591
|
+
return self._render_error(error_msg, mode, context)
|
|
592
|
+
|
|
593
|
+
def _match_button_values(
|
|
594
|
+
self,
|
|
595
|
+
buttons: list[dict[str, str]],
|
|
596
|
+
target_values: list[str],
|
|
597
|
+
) -> tuple[list[str], list[str]]:
|
|
598
|
+
"""
|
|
599
|
+
Match user input values against button options.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
buttons: List of button dictionaries with 'display' and 'value' keys
|
|
603
|
+
target_values: User input values to match
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
Tuple of (matched_values, unmatched_values)
|
|
607
|
+
- matched_values: Values that match button options (using button value)
|
|
608
|
+
- unmatched_values: Values that don't match any button
|
|
609
|
+
"""
|
|
610
|
+
matched_values = []
|
|
611
|
+
unmatched_values = []
|
|
612
|
+
|
|
613
|
+
for value in target_values:
|
|
614
|
+
matched = False
|
|
615
|
+
for button in buttons:
|
|
616
|
+
if value in [button["display"], button["value"]]:
|
|
617
|
+
matched_values.append(button["value"]) # Use button value
|
|
618
|
+
matched = True
|
|
619
|
+
break
|
|
620
|
+
|
|
621
|
+
if not matched:
|
|
622
|
+
unmatched_values.append(value)
|
|
623
|
+
|
|
624
|
+
return matched_values, unmatched_values
|
|
436
625
|
|
|
437
626
|
def _process_button_validation(
|
|
438
627
|
self,
|
|
@@ -441,6 +630,7 @@ class MarkdownFlow:
|
|
|
441
630
|
target_variable: str,
|
|
442
631
|
mode: ProcessMode,
|
|
443
632
|
interaction_type: InteractionType,
|
|
633
|
+
context: list[dict[str, str]] | None = None,
|
|
444
634
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
445
635
|
"""
|
|
446
636
|
Simplified button validation with new input format.
|
|
@@ -451,6 +641,7 @@ class MarkdownFlow:
|
|
|
451
641
|
target_variable: Target variable name
|
|
452
642
|
mode: Processing mode
|
|
453
643
|
interaction_type: Type of interaction
|
|
644
|
+
context: Conversation history context (optional)
|
|
454
645
|
"""
|
|
455
646
|
buttons = parse_result.get("buttons", [])
|
|
456
647
|
is_multi_select = interaction_type in [
|
|
@@ -476,9 +667,9 @@ class MarkdownFlow:
|
|
|
476
667
|
# Pure button mode requires input
|
|
477
668
|
button_displays = [btn["display"] for btn in buttons]
|
|
478
669
|
error_msg = f"Please select from: {', '.join(button_displays)}"
|
|
479
|
-
return self._render_error(error_msg, mode)
|
|
670
|
+
return self._render_error(error_msg, mode, context)
|
|
480
671
|
|
|
481
|
-
#
|
|
672
|
+
# Validate input values against available buttons
|
|
482
673
|
valid_values = []
|
|
483
674
|
invalid_values = []
|
|
484
675
|
|
|
@@ -491,30 +682,19 @@ class MarkdownFlow:
|
|
|
491
682
|
break
|
|
492
683
|
|
|
493
684
|
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:
|
|
685
|
+
if allow_text_input:
|
|
686
|
+
# Allow custom text in buttons+text mode
|
|
687
|
+
valid_values.append(value)
|
|
688
|
+
else:
|
|
689
|
+
invalid_values.append(value)
|
|
690
|
+
|
|
691
|
+
# Check for validation errors
|
|
692
|
+
if invalid_values and not allow_text_input:
|
|
513
693
|
button_displays = [btn["display"] for btn in buttons]
|
|
514
694
|
error_msg = f"Invalid options: {', '.join(invalid_values)}. Please select from: {', '.join(button_displays)}"
|
|
515
|
-
return self._render_error(error_msg, mode)
|
|
695
|
+
return self._render_error(error_msg, mode, context)
|
|
516
696
|
|
|
517
|
-
# Success: return validated
|
|
697
|
+
# Success: return validated values
|
|
518
698
|
return LLMResult(
|
|
519
699
|
content="",
|
|
520
700
|
variables={target_variable: valid_values},
|
|
@@ -524,7 +704,6 @@ class MarkdownFlow:
|
|
|
524
704
|
"valid_values": valid_values,
|
|
525
705
|
"invalid_values": invalid_values,
|
|
526
706
|
"total_input_count": len(target_values),
|
|
527
|
-
"llm_validated": False,
|
|
528
707
|
},
|
|
529
708
|
)
|
|
530
709
|
|
|
@@ -534,27 +713,18 @@ class MarkdownFlow:
|
|
|
534
713
|
user_input: dict[str, list[str]],
|
|
535
714
|
target_variable: str,
|
|
536
715
|
mode: ProcessMode,
|
|
716
|
+
context: list[dict[str, str]] | None = None,
|
|
537
717
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
538
718
|
"""Process LLM validation."""
|
|
539
719
|
# 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
|
-
)
|
|
720
|
+
messages = self._build_validation_messages(block_index, user_input, target_variable, context)
|
|
550
721
|
|
|
551
722
|
if mode == ProcessMode.COMPLETE:
|
|
552
723
|
if not self._llm_provider:
|
|
553
724
|
# Fallback processing, return variables directly
|
|
554
725
|
return LLMResult(content="", variables=user_input) # type: ignore[arg-type]
|
|
555
726
|
|
|
556
|
-
|
|
557
|
-
llm_response = result.content
|
|
727
|
+
llm_response = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
558
728
|
|
|
559
729
|
# Parse validation response and convert to LLMResult
|
|
560
730
|
# Use joined target values for fallback; avoids JSON string injection
|
|
@@ -568,7 +738,7 @@ class MarkdownFlow:
|
|
|
568
738
|
|
|
569
739
|
def stream_generator():
|
|
570
740
|
full_response = ""
|
|
571
|
-
for chunk in self._llm_provider.stream(messages):
|
|
741
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
572
742
|
full_response += chunk
|
|
573
743
|
|
|
574
744
|
# Parse complete response and convert to LLMResult
|
|
@@ -592,28 +762,15 @@ class MarkdownFlow:
|
|
|
592
762
|
mode: ProcessMode,
|
|
593
763
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
594
764
|
"""Process LLM validation with button options (third case)."""
|
|
595
|
-
_ = block_index # Mark as intentionally unused
|
|
596
765
|
# Build special validation messages containing button option information
|
|
597
766
|
messages = self._build_validation_messages_with_options(user_input, target_variable, options, question)
|
|
598
767
|
|
|
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
|
-
)
|
|
609
|
-
|
|
610
768
|
if mode == ProcessMode.COMPLETE:
|
|
611
769
|
if not self._llm_provider:
|
|
612
770
|
# Fallback processing, return variables directly
|
|
613
771
|
return LLMResult(content="", variables=user_input) # type: ignore[arg-type]
|
|
614
772
|
|
|
615
|
-
|
|
616
|
-
llm_response = result.content
|
|
773
|
+
llm_response = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
617
774
|
|
|
618
775
|
# Parse validation response and convert to LLMResult
|
|
619
776
|
# Use joined target values for fallback; avoids JSON string injection
|
|
@@ -627,7 +784,7 @@ class MarkdownFlow:
|
|
|
627
784
|
|
|
628
785
|
def stream_generator():
|
|
629
786
|
full_response = ""
|
|
630
|
-
for chunk in self._llm_provider.stream(messages):
|
|
787
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
631
788
|
full_response += chunk
|
|
632
789
|
# For validation scenario, don't output chunks in real-time, only final result
|
|
633
790
|
|
|
@@ -644,22 +801,24 @@ class MarkdownFlow:
|
|
|
644
801
|
|
|
645
802
|
return stream_generator()
|
|
646
803
|
|
|
647
|
-
def _render_error(
|
|
804
|
+
def _render_error(
|
|
805
|
+
self,
|
|
806
|
+
error_message: str,
|
|
807
|
+
mode: ProcessMode,
|
|
808
|
+
context: list[dict[str, str]] | None = None,
|
|
809
|
+
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
648
810
|
"""Render user-friendly error message."""
|
|
649
|
-
|
|
811
|
+
# Truncate context to configured maximum length
|
|
812
|
+
truncated_context = self._truncate_context(context)
|
|
650
813
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
prompt=messages[-1]["content"],
|
|
654
|
-
metadata={"original_error": error_message},
|
|
655
|
-
)
|
|
814
|
+
# Build error messages with context
|
|
815
|
+
messages = self._build_error_render_messages(error_message, truncated_context)
|
|
656
816
|
|
|
657
817
|
if mode == ProcessMode.COMPLETE:
|
|
658
818
|
if not self._llm_provider:
|
|
659
819
|
return LLMResult(content=error_message) # Fallback processing
|
|
660
820
|
|
|
661
|
-
|
|
662
|
-
friendly_error = result.content
|
|
821
|
+
friendly_error = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
663
822
|
return LLMResult(content=friendly_error, prompt=messages[-1]["content"])
|
|
664
823
|
|
|
665
824
|
if mode == ProcessMode.STREAM:
|
|
@@ -667,7 +826,7 @@ class MarkdownFlow:
|
|
|
667
826
|
return LLMResult(content=error_message)
|
|
668
827
|
|
|
669
828
|
def stream_generator():
|
|
670
|
-
for chunk in self._llm_provider.stream(messages):
|
|
829
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
671
830
|
yield LLMResult(content=chunk, prompt=messages[-1]["content"])
|
|
672
831
|
|
|
673
832
|
return stream_generator()
|
|
@@ -678,13 +837,15 @@ class MarkdownFlow:
|
|
|
678
837
|
self,
|
|
679
838
|
block_index: int,
|
|
680
839
|
variables: dict[str, str | list[str]] | None,
|
|
840
|
+
context: list[dict[str, str]] | None = None,
|
|
681
841
|
) -> list[dict[str, str]]:
|
|
682
842
|
"""Build content block messages."""
|
|
683
843
|
block = self.get_block(block_index)
|
|
684
844
|
block_content = block.content
|
|
685
845
|
|
|
686
|
-
# Process output instructions
|
|
687
|
-
|
|
846
|
+
# Process output instructions and detect if preserved content exists
|
|
847
|
+
# Returns: (processed_content, has_preserved_content)
|
|
848
|
+
block_content, has_preserved_content = process_output_instructions(block_content)
|
|
688
849
|
|
|
689
850
|
# Replace variables
|
|
690
851
|
block_content = replace_variables_in_text(block_content, variables or {})
|
|
@@ -692,22 +853,43 @@ class MarkdownFlow:
|
|
|
692
853
|
# Build message array
|
|
693
854
|
messages = []
|
|
694
855
|
|
|
695
|
-
#
|
|
856
|
+
# Build system message with XML tags
|
|
857
|
+
system_parts = []
|
|
858
|
+
|
|
859
|
+
# 1. Base system prompt (if exists and non-empty)
|
|
860
|
+
if self._base_system_prompt:
|
|
861
|
+
system_parts.append(f"<base_system>\n{self._base_system_prompt}\n</base_system>")
|
|
862
|
+
|
|
863
|
+
# 2. Document prompt (if exists and non-empty)
|
|
696
864
|
if self._document_prompt:
|
|
697
|
-
|
|
865
|
+
system_parts.append(f"<document_prompt>\n{self._document_prompt}\n</document_prompt>")
|
|
866
|
+
|
|
867
|
+
# 3. Output instruction (if preserved content exists)
|
|
868
|
+
# Note: OUTPUT_INSTRUCTION_EXPLANATION already contains <preserve_or_translate_instruction> tags
|
|
869
|
+
if has_preserved_content:
|
|
870
|
+
system_parts.append(OUTPUT_INSTRUCTION_EXPLANATION.strip())
|
|
698
871
|
|
|
699
|
-
#
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
872
|
+
# Combine all parts and add as system message
|
|
873
|
+
if system_parts:
|
|
874
|
+
system_msg = "\n\n".join(system_parts)
|
|
875
|
+
messages.append({"role": "system", "content": system_msg})
|
|
876
|
+
|
|
877
|
+
# Add conversation history context if provided
|
|
878
|
+
# Context is inserted after system message and before current user message
|
|
879
|
+
truncated_context = self._truncate_context(context)
|
|
880
|
+
if truncated_context:
|
|
881
|
+
messages.extend(truncated_context)
|
|
704
882
|
|
|
705
883
|
# Add processed content as user message (as instruction to LLM)
|
|
706
884
|
messages.append({"role": "user", "content": block_content})
|
|
707
885
|
|
|
708
886
|
return messages
|
|
709
887
|
|
|
710
|
-
def _build_interaction_render_messages(
|
|
888
|
+
def _build_interaction_render_messages(
|
|
889
|
+
self,
|
|
890
|
+
question_text: str,
|
|
891
|
+
context: list[dict[str, str]] | None = None,
|
|
892
|
+
) -> list[dict[str, str]]:
|
|
711
893
|
"""Build interaction rendering messages."""
|
|
712
894
|
# Check if using custom interaction prompt
|
|
713
895
|
if self._interaction_prompt != DEFAULT_INTERACTION_PROMPT:
|
|
@@ -721,15 +903,32 @@ class MarkdownFlow:
|
|
|
721
903
|
messages = []
|
|
722
904
|
|
|
723
905
|
messages.append({"role": "system", "content": render_prompt})
|
|
906
|
+
|
|
907
|
+
# NOTE: Context is temporarily disabled for interaction rendering
|
|
908
|
+
# Mixing conversation history with interaction content rewriting can cause issues
|
|
909
|
+
# The context parameter is kept in the signature for future use
|
|
910
|
+
# truncated_context = self._truncate_context(context)
|
|
911
|
+
# if truncated_context:
|
|
912
|
+
# messages.extend(truncated_context)
|
|
913
|
+
|
|
724
914
|
messages.append({"role": "user", "content": question_text})
|
|
725
915
|
|
|
726
916
|
return messages
|
|
727
917
|
|
|
728
|
-
def _build_validation_messages(
|
|
918
|
+
def _build_validation_messages(
|
|
919
|
+
self,
|
|
920
|
+
block_index: int,
|
|
921
|
+
user_input: dict[str, list[str]],
|
|
922
|
+
target_variable: str,
|
|
923
|
+
context: list[dict[str, str]] | None = None,
|
|
924
|
+
) -> list[dict[str, str]]:
|
|
729
925
|
"""Build validation messages."""
|
|
730
926
|
block = self.get_block(block_index)
|
|
731
927
|
config = self.get_interaction_validation_config(block_index)
|
|
732
928
|
|
|
929
|
+
# Truncate context to configured maximum length
|
|
930
|
+
truncated_context = self._truncate_context(context)
|
|
931
|
+
|
|
733
932
|
if config and config.validation_template:
|
|
734
933
|
# Use custom validation template
|
|
735
934
|
validation_prompt = config.validation_template
|
|
@@ -740,7 +939,8 @@ class MarkdownFlow:
|
|
|
740
939
|
system_message = DEFAULT_VALIDATION_SYSTEM_MESSAGE
|
|
741
940
|
else:
|
|
742
941
|
# Use smart default validation template
|
|
743
|
-
from .
|
|
942
|
+
from .parser import (
|
|
943
|
+
InteractionParser,
|
|
744
944
|
extract_interaction_question,
|
|
745
945
|
generate_smart_validation_template,
|
|
746
946
|
)
|
|
@@ -748,11 +948,17 @@ class MarkdownFlow:
|
|
|
748
948
|
# Extract interaction question
|
|
749
949
|
interaction_question = extract_interaction_question(block.content)
|
|
750
950
|
|
|
751
|
-
#
|
|
951
|
+
# Parse interaction to extract button information
|
|
952
|
+
parser = InteractionParser()
|
|
953
|
+
parse_result = parser.parse(block.content)
|
|
954
|
+
buttons = parse_result.get("buttons") if "buttons" in parse_result else None
|
|
955
|
+
|
|
956
|
+
# Generate smart validation template with context and buttons
|
|
752
957
|
validation_template = generate_smart_validation_template(
|
|
753
958
|
target_variable,
|
|
754
|
-
context=
|
|
959
|
+
context=truncated_context,
|
|
755
960
|
interaction_question=interaction_question,
|
|
961
|
+
buttons=buttons,
|
|
756
962
|
)
|
|
757
963
|
|
|
758
964
|
# Replace template variables
|
|
@@ -765,6 +971,11 @@ class MarkdownFlow:
|
|
|
765
971
|
messages = []
|
|
766
972
|
|
|
767
973
|
messages.append({"role": "system", "content": system_message})
|
|
974
|
+
|
|
975
|
+
# Add conversation history context if provided (only if not using custom template)
|
|
976
|
+
if truncated_context and not (config and config.validation_template):
|
|
977
|
+
messages.extend(truncated_context)
|
|
978
|
+
|
|
768
979
|
messages.append({"role": "user", "content": validation_prompt})
|
|
769
980
|
|
|
770
981
|
return messages
|
|
@@ -795,7 +1006,11 @@ class MarkdownFlow:
|
|
|
795
1006
|
|
|
796
1007
|
return messages
|
|
797
1008
|
|
|
798
|
-
def _build_error_render_messages(
|
|
1009
|
+
def _build_error_render_messages(
|
|
1010
|
+
self,
|
|
1011
|
+
error_message: str,
|
|
1012
|
+
context: list[dict[str, str]] | None = None,
|
|
1013
|
+
) -> list[dict[str, str]]:
|
|
799
1014
|
"""Build error rendering messages."""
|
|
800
1015
|
render_prompt = f"""{self._interaction_error_prompt}
|
|
801
1016
|
|
|
@@ -808,6 +1023,12 @@ Original Error: {error_message}
|
|
|
808
1023
|
messages.append({"role": "system", "content": self._document_prompt})
|
|
809
1024
|
|
|
810
1025
|
messages.append({"role": "system", "content": render_prompt})
|
|
1026
|
+
|
|
1027
|
+
# Add conversation history context if provided
|
|
1028
|
+
truncated_context = self._truncate_context(context)
|
|
1029
|
+
if truncated_context:
|
|
1030
|
+
messages.extend(truncated_context)
|
|
1031
|
+
|
|
811
1032
|
messages.append({"role": "user", "content": error_message})
|
|
812
1033
|
|
|
813
1034
|
return messages
|
|
@@ -827,411 +1048,5 @@ Original Error: {error_message}
|
|
|
827
1048
|
if match:
|
|
828
1049
|
prefix = match.group(1)
|
|
829
1050
|
suffix = match.group(2)
|
|
830
|
-
|
|
831
|
-
# suffix format is "original_question]", we only want "]"
|
|
832
|
-
if suffix.endswith("]"):
|
|
833
|
-
clean_suffix = "]"
|
|
834
|
-
else:
|
|
835
|
-
clean_suffix = suffix
|
|
836
|
-
|
|
837
|
-
return f"{prefix}{cleaned_question}{clean_suffix}"
|
|
1051
|
+
return f"{prefix}{cleaned_question}{suffix}"
|
|
838
1052
|
return original_content # type: ignore[unreachable]
|
|
839
|
-
|
|
840
|
-
# Dynamic Interaction Methods
|
|
841
|
-
|
|
842
|
-
def _process_with_dynamic_check(
|
|
843
|
-
self,
|
|
844
|
-
block_index: int,
|
|
845
|
-
mode: ProcessMode,
|
|
846
|
-
context: list[dict[str, str]] | None,
|
|
847
|
-
variables: dict[str, str | list[str]] | None,
|
|
848
|
-
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
849
|
-
"""Process content with dynamic interaction detection and conversion."""
|
|
850
|
-
|
|
851
|
-
block = self.get_block(block_index)
|
|
852
|
-
messages = self._build_dynamic_check_messages(block, context, variables)
|
|
853
|
-
|
|
854
|
-
# Define Function Calling tools with structured approach
|
|
855
|
-
tools = [
|
|
856
|
-
{
|
|
857
|
-
"type": "function",
|
|
858
|
-
"function": {
|
|
859
|
-
"name": "create_interaction_block",
|
|
860
|
-
"description": "Convert content to interaction block with structured data when it needs to collect user input",
|
|
861
|
-
"parameters": {
|
|
862
|
-
"type": "object",
|
|
863
|
-
"properties": {
|
|
864
|
-
"needs_interaction": {"type": "boolean", "description": "Whether this content needs to be converted to interaction block"},
|
|
865
|
-
"variable_name": {"type": "string", "description": "Name of the variable to collect (without {{}} brackets)"},
|
|
866
|
-
"interaction_type": {
|
|
867
|
-
"type": "string",
|
|
868
|
-
"enum": ["single_select", "multi_select", "text_input", "mixed"],
|
|
869
|
-
"description": "Type of interaction: single_select (|), multi_select (||), text_input (...), mixed (options + text)",
|
|
870
|
-
},
|
|
871
|
-
"options": {"type": "array", "items": {"type": "string"}, "description": "List of selectable options (3-4 specific options based on context)"},
|
|
872
|
-
"allow_text_input": {"type": "boolean", "description": "Whether to include a text input option for 'Other' cases"},
|
|
873
|
-
"text_input_prompt": {"type": "string", "description": "Prompt text for the text input option (e.g., '其他请输入', 'Other, please specify')"},
|
|
874
|
-
},
|
|
875
|
-
"required": ["needs_interaction"],
|
|
876
|
-
},
|
|
877
|
-
},
|
|
878
|
-
}
|
|
879
|
-
]
|
|
880
|
-
|
|
881
|
-
if not self._llm_provider:
|
|
882
|
-
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
883
|
-
|
|
884
|
-
# Call LLM with tools
|
|
885
|
-
result = self._llm_provider.complete(messages, tools)
|
|
886
|
-
|
|
887
|
-
# If interaction was generated through Function Calling, construct the MarkdownFlow format
|
|
888
|
-
if result.transformed_to_interaction and result.metadata and "tool_args" in result.metadata:
|
|
889
|
-
tool_args = result.metadata["tool_args"]
|
|
890
|
-
if tool_args.get("needs_interaction"):
|
|
891
|
-
# Construct MarkdownFlow format from structured data
|
|
892
|
-
interaction_content = self._build_interaction_format(tool_args)
|
|
893
|
-
result.content = interaction_content
|
|
894
|
-
|
|
895
|
-
# If transformed to interaction, return as is
|
|
896
|
-
if result.transformed_to_interaction:
|
|
897
|
-
return result
|
|
898
|
-
|
|
899
|
-
# If not transformed, continue with normal processing using standard content messages
|
|
900
|
-
normal_messages = self._build_content_messages(block_index, variables)
|
|
901
|
-
|
|
902
|
-
if mode == ProcessMode.STREAM:
|
|
903
|
-
|
|
904
|
-
def stream_wrapper():
|
|
905
|
-
stream_generator = self._llm_provider.stream(normal_messages)
|
|
906
|
-
for chunk in stream_generator:
|
|
907
|
-
yield LLMResult(content=chunk)
|
|
908
|
-
|
|
909
|
-
return stream_wrapper()
|
|
910
|
-
|
|
911
|
-
# Complete mode - use normal content processing
|
|
912
|
-
normal_result = self._llm_provider.complete(normal_messages)
|
|
913
|
-
return LLMResult(content=normal_result.content, prompt=normal_messages[-1]["content"], metadata=normal_result.metadata)
|
|
914
|
-
|
|
915
|
-
def _build_dynamic_check_messages(
|
|
916
|
-
self,
|
|
917
|
-
block: "Block",
|
|
918
|
-
context: list[dict[str, str]] | None,
|
|
919
|
-
variables: dict[str, str | list[str]] | None,
|
|
920
|
-
) -> list[dict[str, str]]:
|
|
921
|
-
"""Build messages for dynamic interaction detection."""
|
|
922
|
-
|
|
923
|
-
import json
|
|
924
|
-
|
|
925
|
-
# System prompt for detection
|
|
926
|
-
system_prompt = """You are an intelligent document processing assistant specializing in creating interactive forms.
|
|
927
|
-
|
|
928
|
-
Task: Analyze the given content block and determine if it needs to be converted to an interaction block to collect user information.
|
|
929
|
-
|
|
930
|
-
**ABSOLUTE RULE**: Convert ONLY when ALL THREE mandatory elements are explicitly present:
|
|
931
|
-
1. Storage action word + target connector + variable
|
|
932
|
-
2. No exceptions, no implications, no assumptions
|
|
933
|
-
|
|
934
|
-
**MANDATORY TRIPLE PATTERN (ALL REQUIRED):**
|
|
935
|
-
|
|
936
|
-
**Element 1: Storage Action Words**
|
|
937
|
-
- Chinese: "记录", "保存", "存储", "收集", "采集"
|
|
938
|
-
- English: "save", "store", "record", "collect", "gather"
|
|
939
|
-
|
|
940
|
-
**Element 2: Target Connection Words**
|
|
941
|
-
- Chinese: "到", "为", "在", "至"
|
|
942
|
-
- English: "to", "as", "in", "into"
|
|
943
|
-
|
|
944
|
-
**Element 3: Target Variable**
|
|
945
|
-
- Must contain {{variable_name}} syntax for NEW data storage
|
|
946
|
-
- Variable must be for collecting NEW information, not using existing data
|
|
947
|
-
|
|
948
|
-
**VALID CONVERSION FORMULA:**
|
|
949
|
-
[Storage Word] + [Connector] + {{new_variable}}
|
|
950
|
-
|
|
951
|
-
Examples of VALID patterns:
|
|
952
|
-
- "...记录到{{姓名}}"
|
|
953
|
-
- "...保存为{{偏好}}"
|
|
954
|
-
- "...存储在{{选择}}"
|
|
955
|
-
- "...save to {{preference}}"
|
|
956
|
-
- "...collect as {{user_input}}"
|
|
957
|
-
|
|
958
|
-
**STRICT EXCLUSION RULES:**
|
|
959
|
-
|
|
960
|
-
❌ NEVER convert if missing ANY element:
|
|
961
|
-
- No storage action word = NO conversion
|
|
962
|
-
- No target connector = NO conversion
|
|
963
|
-
- No {{variable}} = NO conversion
|
|
964
|
-
- Using existing {{variable}} instead of collecting new = NO conversion
|
|
965
|
-
|
|
966
|
-
❌ NEVER convert casual conversation:
|
|
967
|
-
- Simple questions without storage intent
|
|
968
|
-
- Introduction requests without persistence
|
|
969
|
-
- General inquiries without data collection
|
|
970
|
-
- Educational or exploratory content
|
|
971
|
-
|
|
972
|
-
❌ NEVER infer or assume storage intent:
|
|
973
|
-
- Don't assume "询问姓名" means "保存姓名"
|
|
974
|
-
- Don't assume "了解偏好" means "记录偏好"
|
|
975
|
-
- Don't assume data collection without explicit storage words
|
|
976
|
-
|
|
977
|
-
**PATTERN ANALYSIS METHOD:**
|
|
978
|
-
1. **Exact Pattern Match**: Search for [Storage Word] + [Connector] + {{variable}}
|
|
979
|
-
2. **No Pattern = No Conversion**: If exact pattern not found, return needs_interaction: false
|
|
980
|
-
3. **Zero Tolerance**: No partial matches, no similar meanings, no interpretations
|
|
981
|
-
|
|
982
|
-
**ULTRA-CONSERVATIVE APPROACH:**
|
|
983
|
-
- If there's ANY doubt about storage intent = DON'T convert
|
|
984
|
-
- If storage pattern is not 100% explicit = DON'T convert
|
|
985
|
-
- If you need to "interpret" or "infer" storage intent = DON'T convert
|
|
986
|
-
- Prefer false negatives over false positives
|
|
987
|
-
|
|
988
|
-
When exact pattern is found, generate structured interaction data. Otherwise, always return needs_interaction: false."""
|
|
989
|
-
|
|
990
|
-
# User message with content and context
|
|
991
|
-
# Build user prompt with document context
|
|
992
|
-
user_prompt_parts = []
|
|
993
|
-
|
|
994
|
-
# Add document-level prompt context if exists
|
|
995
|
-
if self._document_prompt:
|
|
996
|
-
user_prompt_parts.append(f"""Document-level instructions:
|
|
997
|
-
{self._document_prompt}
|
|
998
|
-
|
|
999
|
-
(Note: The above are the user's document-level instructions that provide context and requirements for processing.)
|
|
1000
|
-
""")
|
|
1001
|
-
|
|
1002
|
-
# Prepare content analysis with both original and resolved versions
|
|
1003
|
-
original_content = block.content
|
|
1004
|
-
|
|
1005
|
-
# Create resolved content with variable substitution for better context
|
|
1006
|
-
resolved_content = original_content
|
|
1007
|
-
if variables:
|
|
1008
|
-
from .utils import replace_variables_in_text
|
|
1009
|
-
|
|
1010
|
-
resolved_content = replace_variables_in_text(original_content, variables)
|
|
1011
|
-
|
|
1012
|
-
content_analysis = f"""Current content block to analyze:
|
|
1013
|
-
|
|
1014
|
-
**Original content (shows variable structure):**
|
|
1015
|
-
{original_content}
|
|
1016
|
-
|
|
1017
|
-
**Resolved content (with current variable values):**
|
|
1018
|
-
{resolved_content}
|
|
1019
|
-
|
|
1020
|
-
**Existing variable values:**
|
|
1021
|
-
{json.dumps(variables, ensure_ascii=False) if variables else "None"}"""
|
|
1022
|
-
|
|
1023
|
-
# Add different analysis based on whether content has variables
|
|
1024
|
-
if "{{" in original_content and "}}" in original_content:
|
|
1025
|
-
from .utils import extract_variables_from_text
|
|
1026
|
-
|
|
1027
|
-
content_variables = set(extract_variables_from_text(original_content))
|
|
1028
|
-
|
|
1029
|
-
# Find new variables (not yet collected)
|
|
1030
|
-
new_variables = content_variables - (set(variables.keys()) if variables else set())
|
|
1031
|
-
existing_used_variables = content_variables & (set(variables.keys()) if variables else set())
|
|
1032
|
-
|
|
1033
|
-
content_analysis += f"""
|
|
1034
|
-
|
|
1035
|
-
**Variable analysis:**
|
|
1036
|
-
- Variables used from previous steps: {list(existing_used_variables) if existing_used_variables else "None"}
|
|
1037
|
-
- New variables to collect: {list(new_variables) if new_variables else "None"}
|
|
1038
|
-
|
|
1039
|
-
**Context guidance:**
|
|
1040
|
-
- Use the resolved content to understand the actual context and requirements
|
|
1041
|
-
- Generate options based on the real variable values shown in the resolved content
|
|
1042
|
-
- Collect user input for the new variables identified above"""
|
|
1043
|
-
|
|
1044
|
-
user_prompt_parts.append(content_analysis)
|
|
1045
|
-
|
|
1046
|
-
# Add analysis requirements and structured output guide
|
|
1047
|
-
user_prompt_parts.append("""## Analysis Task:
|
|
1048
|
-
1. Determine if this content needs to be converted to an interaction block
|
|
1049
|
-
2. If conversion is needed, provide structured interaction data
|
|
1050
|
-
|
|
1051
|
-
## Context-based Analysis:
|
|
1052
|
-
- Use the "Resolved content" to understand actual context (e.g., if it shows "川菜", generate Sichuan dish options)
|
|
1053
|
-
- Extract the "New variables to collect" identified in the variable analysis above
|
|
1054
|
-
- Generate 3-4 specific options based on the resolved context and document-level instructions
|
|
1055
|
-
- Follow ALL document-level instruction requirements (language, domain, terminology)
|
|
1056
|
-
|
|
1057
|
-
## Selection Type Decision Logic:
|
|
1058
|
-
Ask: "Can a user realistically want/choose multiple of these options simultaneously?"
|
|
1059
|
-
|
|
1060
|
-
**Use MULTI_SELECT when:**
|
|
1061
|
-
- Food dishes (can order multiple: 宫保鸡丁, 麻婆豆腐)
|
|
1062
|
-
- Programming skills (can know multiple: Python, JavaScript)
|
|
1063
|
-
- Interests/hobbies (can have multiple: 读书, 运动, 旅游)
|
|
1064
|
-
- Product features (can want multiple: 定制颜色, 个性化logo)
|
|
1065
|
-
- Exercise types (can do multiple: 跑步, 游泳, 瑜伽)
|
|
1066
|
-
|
|
1067
|
-
**Use SINGLE_SELECT when:**
|
|
1068
|
-
- Job positions (usually apply for one: 软件工程师 OR 产品经理)
|
|
1069
|
-
- Experience levels (have one current level: Beginner OR Advanced)
|
|
1070
|
-
- Budget ranges (have one range: 5-10万 OR 10-20万)
|
|
1071
|
-
- Education levels (have one highest: Bachelor's OR Master's)
|
|
1072
|
-
|
|
1073
|
-
## Output Instructions:
|
|
1074
|
-
If this content needs interaction, use the create_interaction_block function with:
|
|
1075
|
-
- `needs_interaction`: true/false
|
|
1076
|
-
- `variable_name`: the variable to collect (from "New variables" above)
|
|
1077
|
-
- `interaction_type`: "single_select", "multi_select", "text_input", or "mixed"
|
|
1078
|
-
- `options`: array of 3-4 specific options based on context
|
|
1079
|
-
- `allow_text_input`: true if you want to include "other" option
|
|
1080
|
-
- `text_input_prompt`: text for the "other" option (in appropriate language)
|
|
1081
|
-
|
|
1082
|
-
Analyze the content and provide the structured interaction data.""")
|
|
1083
|
-
|
|
1084
|
-
user_prompt = "\n\n".join(user_prompt_parts)
|
|
1085
|
-
|
|
1086
|
-
messages = [{"role": "system", "content": system_prompt}]
|
|
1087
|
-
|
|
1088
|
-
# Add context if provided
|
|
1089
|
-
if context:
|
|
1090
|
-
messages.extend(context)
|
|
1091
|
-
|
|
1092
|
-
messages.append({"role": "user", "content": user_prompt})
|
|
1093
|
-
|
|
1094
|
-
return messages
|
|
1095
|
-
|
|
1096
|
-
def _build_interaction_format(self, tool_args: dict) -> str:
|
|
1097
|
-
"""Build MarkdownFlow interaction format from structured Function Calling data."""
|
|
1098
|
-
variable_name = tool_args.get("variable_name", "")
|
|
1099
|
-
interaction_type = tool_args.get("interaction_type", "single_select")
|
|
1100
|
-
options = tool_args.get("options", [])
|
|
1101
|
-
allow_text_input = tool_args.get("allow_text_input", False)
|
|
1102
|
-
text_input_prompt = tool_args.get("text_input_prompt", "...请输入")
|
|
1103
|
-
|
|
1104
|
-
if not variable_name:
|
|
1105
|
-
return ""
|
|
1106
|
-
|
|
1107
|
-
# For text_input type, options can be empty
|
|
1108
|
-
if interaction_type != "text_input" and not options:
|
|
1109
|
-
return ""
|
|
1110
|
-
|
|
1111
|
-
# Choose separator based on interaction type
|
|
1112
|
-
if interaction_type in ["multi_select", "mixed"]:
|
|
1113
|
-
separator = "||"
|
|
1114
|
-
else:
|
|
1115
|
-
separator = "|"
|
|
1116
|
-
|
|
1117
|
-
# Build options string
|
|
1118
|
-
if interaction_type == "text_input":
|
|
1119
|
-
# Text input only
|
|
1120
|
-
options_str = f"...{text_input_prompt}"
|
|
1121
|
-
else:
|
|
1122
|
-
# Options with potential text input
|
|
1123
|
-
options_str = separator.join(options)
|
|
1124
|
-
|
|
1125
|
-
if allow_text_input and text_input_prompt:
|
|
1126
|
-
# Ensure text input has ... prefix
|
|
1127
|
-
text_option = text_input_prompt if text_input_prompt.startswith("...") else f"...{text_input_prompt}"
|
|
1128
|
-
options_str += f"{separator}{text_option}"
|
|
1129
|
-
|
|
1130
|
-
return f"?[%{{{{{variable_name}}}}} {options_str}]"
|
|
1131
|
-
|
|
1132
|
-
def _process_dynamic_interaction_validation(
|
|
1133
|
-
self,
|
|
1134
|
-
block_index: int,
|
|
1135
|
-
interaction_format: str,
|
|
1136
|
-
user_input: dict[str, list[str]],
|
|
1137
|
-
mode: ProcessMode,
|
|
1138
|
-
context: list[dict[str, str]] | None,
|
|
1139
|
-
variables: dict[str, str | list[str]] | None,
|
|
1140
|
-
) -> LLMResult:
|
|
1141
|
-
"""Validate user input for dynamically generated interaction blocks using same logic as normal interactions."""
|
|
1142
|
-
_ = block_index # Mark as intentionally unused
|
|
1143
|
-
_ = context # Mark as intentionally unused
|
|
1144
|
-
|
|
1145
|
-
from .utils import InteractionParser
|
|
1146
|
-
|
|
1147
|
-
# Parse the interaction format using the same parser as normal interactions
|
|
1148
|
-
parser = InteractionParser()
|
|
1149
|
-
parse_result = parser.parse(interaction_format)
|
|
1150
|
-
|
|
1151
|
-
if "error" in parse_result:
|
|
1152
|
-
error_msg = f"Invalid interaction format: {parse_result['error']}"
|
|
1153
|
-
return self._render_error(error_msg, mode)
|
|
1154
|
-
|
|
1155
|
-
# Extract variable name and interaction type
|
|
1156
|
-
variable_name = parse_result.get("variable")
|
|
1157
|
-
interaction_type = parse_result.get("type")
|
|
1158
|
-
|
|
1159
|
-
if not variable_name:
|
|
1160
|
-
error_msg = f"No variable found in interaction format: {interaction_format}"
|
|
1161
|
-
return self._render_error(error_msg, mode)
|
|
1162
|
-
|
|
1163
|
-
# Get user input for the target variable
|
|
1164
|
-
target_values = user_input.get(variable_name, [])
|
|
1165
|
-
|
|
1166
|
-
# Basic validation - check if input is provided when required
|
|
1167
|
-
if not target_values:
|
|
1168
|
-
# Check if this is a text input or allows empty input
|
|
1169
|
-
allow_text_input = interaction_type in [
|
|
1170
|
-
InteractionType.BUTTONS_WITH_TEXT,
|
|
1171
|
-
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
1172
|
-
]
|
|
1173
|
-
|
|
1174
|
-
if allow_text_input:
|
|
1175
|
-
# Allow empty input for buttons+text mode - merge with existing variables
|
|
1176
|
-
merged_variables = dict(variables or {})
|
|
1177
|
-
merged_variables[variable_name] = []
|
|
1178
|
-
return LLMResult(
|
|
1179
|
-
content="",
|
|
1180
|
-
variables=merged_variables,
|
|
1181
|
-
metadata={
|
|
1182
|
-
"interaction_type": "dynamic_interaction",
|
|
1183
|
-
"empty_input": True,
|
|
1184
|
-
},
|
|
1185
|
-
)
|
|
1186
|
-
error_msg = f"No input provided for variable '{variable_name}'"
|
|
1187
|
-
return self._render_error(error_msg, mode)
|
|
1188
|
-
|
|
1189
|
-
# Use the same validation logic as normal interactions
|
|
1190
|
-
if interaction_type in [
|
|
1191
|
-
InteractionType.BUTTONS_ONLY,
|
|
1192
|
-
InteractionType.BUTTONS_WITH_TEXT,
|
|
1193
|
-
InteractionType.BUTTONS_MULTI_SELECT,
|
|
1194
|
-
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
1195
|
-
]:
|
|
1196
|
-
# Button validation - reuse the existing button validation logic
|
|
1197
|
-
button_result = self._process_button_validation(
|
|
1198
|
-
parse_result,
|
|
1199
|
-
target_values,
|
|
1200
|
-
variable_name,
|
|
1201
|
-
mode,
|
|
1202
|
-
interaction_type,
|
|
1203
|
-
)
|
|
1204
|
-
|
|
1205
|
-
# Merge with existing variables for dynamic interactions
|
|
1206
|
-
if hasattr(button_result, "variables") and button_result.variables is not None and variables:
|
|
1207
|
-
merged_variables = dict(variables)
|
|
1208
|
-
merged_variables.update(button_result.variables)
|
|
1209
|
-
return LLMResult(
|
|
1210
|
-
content=button_result.content,
|
|
1211
|
-
variables=merged_variables,
|
|
1212
|
-
metadata=button_result.metadata,
|
|
1213
|
-
)
|
|
1214
|
-
return button_result
|
|
1215
|
-
|
|
1216
|
-
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
1217
|
-
# Non-assignment buttons: don't set variables, keep existing ones
|
|
1218
|
-
return LLMResult(
|
|
1219
|
-
content="",
|
|
1220
|
-
variables=dict(variables or {}),
|
|
1221
|
-
metadata={
|
|
1222
|
-
"interaction_type": "non_assignment_button",
|
|
1223
|
-
"user_input": user_input,
|
|
1224
|
-
},
|
|
1225
|
-
)
|
|
1226
|
-
# Text-only input type - merge with existing variables
|
|
1227
|
-
merged_variables = dict(variables or {})
|
|
1228
|
-
merged_variables[variable_name] = target_values
|
|
1229
|
-
return LLMResult(
|
|
1230
|
-
content="",
|
|
1231
|
-
variables=merged_variables,
|
|
1232
|
-
metadata={
|
|
1233
|
-
"interaction_type": "text_only",
|
|
1234
|
-
"target_variable": variable_name,
|
|
1235
|
-
"values": target_values,
|
|
1236
|
-
},
|
|
1237
|
-
)
|