markdown-flow 0.2.18__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 +3 -4
- markdown_flow/constants.py +47 -40
- markdown_flow/core.py +340 -94
- markdown_flow/llm.py +4 -3
- 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 +43 -43
- {markdown_flow-0.2.18.dist-info → markdown_flow-0.2.26.dist-info}/METADATA +45 -52
- markdown_flow-0.2.26.dist-info/RECORD +22 -0
- markdown_flow-0.2.18.dist-info/RECORD +0 -13
- {markdown_flow-0.2.18.dist-info → markdown_flow-0.2.26.dist-info}/WHEEL +0 -0
- {markdown_flow-0.2.18.dist-info → markdown_flow-0.2.26.dist-info}/licenses/LICENSE +0 -0
- {markdown_flow-0.2.18.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,
|
|
@@ -35,7 +36,7 @@ from .enums import BlockType
|
|
|
35
36
|
from .exceptions import BlockIndexError
|
|
36
37
|
from .llm import LLMProvider, LLMResult, ProcessMode
|
|
37
38
|
from .models import Block, InteractionValidationConfig
|
|
38
|
-
from .
|
|
39
|
+
from .parser import (
|
|
39
40
|
InteractionParser,
|
|
40
41
|
InteractionType,
|
|
41
42
|
extract_interaction_question,
|
|
@@ -60,48 +61,105 @@ class MarkdownFlow:
|
|
|
60
61
|
_document_prompt: str | None
|
|
61
62
|
_interaction_prompt: str | None
|
|
62
63
|
_interaction_error_prompt: str | None
|
|
64
|
+
_max_context_length: int
|
|
63
65
|
_blocks: list[Block] | None
|
|
64
66
|
_interaction_configs: dict[int, InteractionValidationConfig]
|
|
67
|
+
_model: str | None
|
|
68
|
+
_temperature: float | None
|
|
65
69
|
|
|
66
70
|
def __init__(
|
|
67
71
|
self,
|
|
68
72
|
document: str,
|
|
69
73
|
llm_provider: LLMProvider | None = None,
|
|
74
|
+
base_system_prompt: str | None = None,
|
|
70
75
|
document_prompt: str | None = None,
|
|
71
76
|
interaction_prompt: str | None = None,
|
|
72
77
|
interaction_error_prompt: str | None = None,
|
|
78
|
+
max_context_length: int = 0,
|
|
73
79
|
):
|
|
74
80
|
"""
|
|
75
81
|
Initialize MarkdownFlow instance.
|
|
76
82
|
|
|
77
83
|
Args:
|
|
78
84
|
document: Markdown document content
|
|
79
|
-
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)
|
|
80
87
|
document_prompt: Document-level system prompt
|
|
81
88
|
interaction_prompt: Interaction content rendering prompt
|
|
82
89
|
interaction_error_prompt: Interaction error rendering prompt
|
|
90
|
+
max_context_length: Maximum number of context messages to keep (0 = unlimited)
|
|
83
91
|
"""
|
|
84
92
|
self._document = document
|
|
85
93
|
self._llm_provider = llm_provider
|
|
94
|
+
self._base_system_prompt = base_system_prompt or DEFAULT_BASE_SYSTEM_PROMPT
|
|
86
95
|
self._document_prompt = document_prompt
|
|
87
96
|
self._interaction_prompt = interaction_prompt or DEFAULT_INTERACTION_PROMPT
|
|
88
97
|
self._interaction_error_prompt = interaction_error_prompt or DEFAULT_INTERACTION_ERROR_PROMPT
|
|
98
|
+
self._max_context_length = max_context_length
|
|
89
99
|
self._blocks = None
|
|
90
100
|
self._interaction_configs: dict[int, InteractionValidationConfig] = {}
|
|
101
|
+
self._model: str | None = None
|
|
102
|
+
self._temperature: float | None = None
|
|
91
103
|
|
|
92
104
|
def set_llm_provider(self, provider: LLMProvider) -> None:
|
|
93
105
|
"""Set LLM provider."""
|
|
94
106
|
self._llm_provider = provider
|
|
95
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
|
+
|
|
96
152
|
def set_prompt(self, prompt_type: str, value: str | None) -> None:
|
|
97
153
|
"""
|
|
98
154
|
Set prompt template.
|
|
99
155
|
|
|
100
156
|
Args:
|
|
101
|
-
prompt_type: Prompt type ('document', 'interaction', 'interaction_error')
|
|
157
|
+
prompt_type: Prompt type ('base_system', 'document', 'interaction', 'interaction_error')
|
|
102
158
|
value: Prompt content
|
|
103
159
|
"""
|
|
104
|
-
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":
|
|
105
163
|
self._document_prompt = value
|
|
106
164
|
elif prompt_type == "interaction":
|
|
107
165
|
self._interaction_prompt = value or DEFAULT_INTERACTION_PROMPT
|
|
@@ -110,6 +168,44 @@ class MarkdownFlow:
|
|
|
110
168
|
else:
|
|
111
169
|
raise ValueError(UNSUPPORTED_PROMPT_TYPE_ERROR.format(prompt_type=prompt_type))
|
|
112
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
|
+
|
|
113
209
|
@property
|
|
114
210
|
def document(self) -> str:
|
|
115
211
|
"""Get document content."""
|
|
@@ -198,6 +294,10 @@ class MarkdownFlow:
|
|
|
198
294
|
Returns:
|
|
199
295
|
LLMResult or Generator[LLMResult, None, None]
|
|
200
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
|
+
|
|
201
301
|
# Process document_prompt variable replacement
|
|
202
302
|
if self._document_prompt:
|
|
203
303
|
self._document_prompt = replace_variables_in_text(self._document_prompt, variables or {})
|
|
@@ -210,7 +310,7 @@ class MarkdownFlow:
|
|
|
210
310
|
if block.block_type == BlockType.INTERACTION:
|
|
211
311
|
if user_input is None:
|
|
212
312
|
# Render interaction content
|
|
213
|
-
return self._process_interaction_render(block_index, mode, variables)
|
|
313
|
+
return self._process_interaction_render(block_index, mode, context, variables)
|
|
214
314
|
# Process user input
|
|
215
315
|
return self._process_interaction_input(block_index, user_input, mode, context, variables)
|
|
216
316
|
|
|
@@ -231,17 +331,17 @@ class MarkdownFlow:
|
|
|
231
331
|
variables: dict[str, str | list[str]] | None,
|
|
232
332
|
):
|
|
233
333
|
"""Process content block."""
|
|
234
|
-
#
|
|
235
|
-
|
|
334
|
+
# Truncate context to configured maximum length
|
|
335
|
+
truncated_context = self._truncate_context(context)
|
|
236
336
|
|
|
237
|
-
|
|
238
|
-
|
|
337
|
+
# Build messages with context
|
|
338
|
+
messages = self._build_content_messages(block_index, variables, truncated_context)
|
|
239
339
|
|
|
240
340
|
if mode == ProcessMode.COMPLETE:
|
|
241
341
|
if not self._llm_provider:
|
|
242
342
|
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
243
343
|
|
|
244
|
-
content = self._llm_provider.complete(messages)
|
|
344
|
+
content = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
245
345
|
return LLMResult(content=content, prompt=messages[-1]["content"])
|
|
246
346
|
|
|
247
347
|
if mode == ProcessMode.STREAM:
|
|
@@ -249,7 +349,7 @@ class MarkdownFlow:
|
|
|
249
349
|
raise ValueError(LLM_PROVIDER_REQUIRED_ERROR)
|
|
250
350
|
|
|
251
351
|
def stream_generator():
|
|
252
|
-
for chunk in self._llm_provider.stream(messages): # type: ignore[attr-defined]
|
|
352
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
253
353
|
yield LLMResult(content=chunk, prompt=messages[-1]["content"])
|
|
254
354
|
|
|
255
355
|
return stream_generator()
|
|
@@ -266,7 +366,13 @@ class MarkdownFlow:
|
|
|
266
366
|
|
|
267
367
|
return LLMResult(content=content)
|
|
268
368
|
|
|
269
|
-
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
|
+
):
|
|
270
376
|
"""Process interaction content rendering."""
|
|
271
377
|
block = self.get_block(block_index)
|
|
272
378
|
|
|
@@ -283,23 +389,17 @@ class MarkdownFlow:
|
|
|
283
389
|
# Unable to extract, return processed content
|
|
284
390
|
return LLMResult(content=processed_block.content)
|
|
285
391
|
|
|
286
|
-
#
|
|
287
|
-
|
|
392
|
+
# Truncate context to configured maximum length
|
|
393
|
+
truncated_context = self._truncate_context(context)
|
|
288
394
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
prompt=messages[-1]["content"],
|
|
292
|
-
metadata={
|
|
293
|
-
"original_content": processed_block.content,
|
|
294
|
-
"question_text": question_text,
|
|
295
|
-
},
|
|
296
|
-
)
|
|
395
|
+
# Build render messages with context
|
|
396
|
+
messages = self._build_interaction_render_messages(question_text, truncated_context)
|
|
297
397
|
|
|
298
398
|
if mode == ProcessMode.COMPLETE:
|
|
299
399
|
if not self._llm_provider:
|
|
300
400
|
return LLMResult(content=processed_block.content) # Fallback processing
|
|
301
401
|
|
|
302
|
-
rendered_question = self._llm_provider.complete(messages)
|
|
402
|
+
rendered_question = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
303
403
|
rendered_content = self._reconstruct_interaction_content(processed_block.content, rendered_question)
|
|
304
404
|
|
|
305
405
|
return LLMResult(
|
|
@@ -327,7 +427,7 @@ class MarkdownFlow:
|
|
|
327
427
|
# With LLM provider, collect full response then return once
|
|
328
428
|
def stream_generator():
|
|
329
429
|
full_response = ""
|
|
330
|
-
for chunk in self._llm_provider.stream(messages): # type: ignore[attr-defined]
|
|
430
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
331
431
|
full_response += chunk
|
|
332
432
|
|
|
333
433
|
# Reconstruct final interaction content
|
|
@@ -356,7 +456,7 @@ class MarkdownFlow:
|
|
|
356
456
|
# Basic validation
|
|
357
457
|
if not user_input or not any(values for values in user_input.values()):
|
|
358
458
|
error_msg = INPUT_EMPTY_ERROR
|
|
359
|
-
return self._render_error(error_msg, mode)
|
|
459
|
+
return self._render_error(error_msg, mode, context)
|
|
360
460
|
|
|
361
461
|
# Get the target variable value from user_input
|
|
362
462
|
target_values = user_input.get(target_variable, [])
|
|
@@ -370,24 +470,99 @@ class MarkdownFlow:
|
|
|
370
470
|
|
|
371
471
|
if "error" in parse_result:
|
|
372
472
|
error_msg = INTERACTION_PARSE_ERROR.format(error=parse_result["error"])
|
|
373
|
-
return self._render_error(error_msg, mode)
|
|
473
|
+
return self._render_error(error_msg, mode, context)
|
|
374
474
|
|
|
375
475
|
interaction_type = parse_result.get("type")
|
|
376
476
|
|
|
377
477
|
# Process user input based on interaction type
|
|
378
478
|
if interaction_type in [
|
|
379
|
-
InteractionType.BUTTONS_ONLY,
|
|
380
479
|
InteractionType.BUTTONS_WITH_TEXT,
|
|
381
|
-
InteractionType.BUTTONS_MULTI_SELECT,
|
|
382
480
|
InteractionType.BUTTONS_MULTI_WITH_TEXT,
|
|
383
481
|
]:
|
|
384
|
-
#
|
|
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)
|
|
385
559
|
return self._process_button_validation(
|
|
386
560
|
parse_result,
|
|
387
561
|
target_values,
|
|
388
562
|
target_variable,
|
|
389
563
|
mode,
|
|
390
564
|
interaction_type,
|
|
565
|
+
context,
|
|
391
566
|
)
|
|
392
567
|
|
|
393
568
|
if interaction_type == InteractionType.NON_ASSIGNMENT_BUTTON:
|
|
@@ -403,19 +578,50 @@ class MarkdownFlow:
|
|
|
403
578
|
)
|
|
404
579
|
|
|
405
580
|
# Text-only input type: ?[%{{sys_user_nickname}}...question]
|
|
406
|
-
#
|
|
581
|
+
# Use LLM validation to check if input is relevant to the question
|
|
407
582
|
if target_values:
|
|
408
|
-
return
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
"values": target_values,
|
|
415
|
-
},
|
|
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,
|
|
416
589
|
)
|
|
417
590
|
error_msg = f"No input provided for variable '{target_variable}'"
|
|
418
|
-
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
|
|
419
625
|
|
|
420
626
|
def _process_button_validation(
|
|
421
627
|
self,
|
|
@@ -424,6 +630,7 @@ class MarkdownFlow:
|
|
|
424
630
|
target_variable: str,
|
|
425
631
|
mode: ProcessMode,
|
|
426
632
|
interaction_type: InteractionType,
|
|
633
|
+
context: list[dict[str, str]] | None = None,
|
|
427
634
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
428
635
|
"""
|
|
429
636
|
Simplified button validation with new input format.
|
|
@@ -434,6 +641,7 @@ class MarkdownFlow:
|
|
|
434
641
|
target_variable: Target variable name
|
|
435
642
|
mode: Processing mode
|
|
436
643
|
interaction_type: Type of interaction
|
|
644
|
+
context: Conversation history context (optional)
|
|
437
645
|
"""
|
|
438
646
|
buttons = parse_result.get("buttons", [])
|
|
439
647
|
is_multi_select = interaction_type in [
|
|
@@ -459,7 +667,7 @@ class MarkdownFlow:
|
|
|
459
667
|
# Pure button mode requires input
|
|
460
668
|
button_displays = [btn["display"] for btn in buttons]
|
|
461
669
|
error_msg = f"Please select from: {', '.join(button_displays)}"
|
|
462
|
-
return self._render_error(error_msg, mode)
|
|
670
|
+
return self._render_error(error_msg, mode, context)
|
|
463
671
|
|
|
464
672
|
# Validate input values against available buttons
|
|
465
673
|
valid_values = []
|
|
@@ -484,7 +692,7 @@ class MarkdownFlow:
|
|
|
484
692
|
if invalid_values and not allow_text_input:
|
|
485
693
|
button_displays = [btn["display"] for btn in buttons]
|
|
486
694
|
error_msg = f"Invalid options: {', '.join(invalid_values)}. Please select from: {', '.join(button_displays)}"
|
|
487
|
-
return self._render_error(error_msg, mode)
|
|
695
|
+
return self._render_error(error_msg, mode, context)
|
|
488
696
|
|
|
489
697
|
# Success: return validated values
|
|
490
698
|
return LLMResult(
|
|
@@ -505,26 +713,18 @@ class MarkdownFlow:
|
|
|
505
713
|
user_input: dict[str, list[str]],
|
|
506
714
|
target_variable: str,
|
|
507
715
|
mode: ProcessMode,
|
|
716
|
+
context: list[dict[str, str]] | None = None,
|
|
508
717
|
) -> LLMResult | Generator[LLMResult, None, None]:
|
|
509
718
|
"""Process LLM validation."""
|
|
510
719
|
# Build validation messages
|
|
511
|
-
messages = self._build_validation_messages(block_index, user_input, target_variable)
|
|
512
|
-
|
|
513
|
-
if mode == ProcessMode.PROMPT_ONLY:
|
|
514
|
-
return LLMResult(
|
|
515
|
-
prompt=messages[-1]["content"],
|
|
516
|
-
metadata={
|
|
517
|
-
"validation_target": user_input,
|
|
518
|
-
"target_variable": target_variable,
|
|
519
|
-
},
|
|
520
|
-
)
|
|
720
|
+
messages = self._build_validation_messages(block_index, user_input, target_variable, context)
|
|
521
721
|
|
|
522
722
|
if mode == ProcessMode.COMPLETE:
|
|
523
723
|
if not self._llm_provider:
|
|
524
724
|
# Fallback processing, return variables directly
|
|
525
725
|
return LLMResult(content="", variables=user_input) # type: ignore[arg-type]
|
|
526
726
|
|
|
527
|
-
llm_response = self._llm_provider.complete(messages)
|
|
727
|
+
llm_response = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
528
728
|
|
|
529
729
|
# Parse validation response and convert to LLMResult
|
|
530
730
|
# Use joined target values for fallback; avoids JSON string injection
|
|
@@ -538,7 +738,7 @@ class MarkdownFlow:
|
|
|
538
738
|
|
|
539
739
|
def stream_generator():
|
|
540
740
|
full_response = ""
|
|
541
|
-
for chunk in self._llm_provider.stream(messages): # type: ignore[attr-defined]
|
|
741
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
542
742
|
full_response += chunk
|
|
543
743
|
|
|
544
744
|
# Parse complete response and convert to LLMResult
|
|
@@ -565,23 +765,12 @@ class MarkdownFlow:
|
|
|
565
765
|
# Build special validation messages containing button option information
|
|
566
766
|
messages = self._build_validation_messages_with_options(user_input, target_variable, options, question)
|
|
567
767
|
|
|
568
|
-
if mode == ProcessMode.PROMPT_ONLY:
|
|
569
|
-
return LLMResult(
|
|
570
|
-
prompt=messages[-1]["content"],
|
|
571
|
-
metadata={
|
|
572
|
-
"validation_target": user_input,
|
|
573
|
-
"target_variable": target_variable,
|
|
574
|
-
"options": options,
|
|
575
|
-
"question": question,
|
|
576
|
-
},
|
|
577
|
-
)
|
|
578
|
-
|
|
579
768
|
if mode == ProcessMode.COMPLETE:
|
|
580
769
|
if not self._llm_provider:
|
|
581
770
|
# Fallback processing, return variables directly
|
|
582
771
|
return LLMResult(content="", variables=user_input) # type: ignore[arg-type]
|
|
583
772
|
|
|
584
|
-
llm_response = self._llm_provider.complete(messages)
|
|
773
|
+
llm_response = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
585
774
|
|
|
586
775
|
# Parse validation response and convert to LLMResult
|
|
587
776
|
# Use joined target values for fallback; avoids JSON string injection
|
|
@@ -595,7 +784,7 @@ class MarkdownFlow:
|
|
|
595
784
|
|
|
596
785
|
def stream_generator():
|
|
597
786
|
full_response = ""
|
|
598
|
-
for chunk in self._llm_provider.stream(messages): # type: ignore[attr-defined]
|
|
787
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
599
788
|
full_response += chunk
|
|
600
789
|
# For validation scenario, don't output chunks in real-time, only final result
|
|
601
790
|
|
|
@@ -612,21 +801,24 @@ class MarkdownFlow:
|
|
|
612
801
|
|
|
613
802
|
return stream_generator()
|
|
614
803
|
|
|
615
|
-
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]:
|
|
616
810
|
"""Render user-friendly error message."""
|
|
617
|
-
|
|
811
|
+
# Truncate context to configured maximum length
|
|
812
|
+
truncated_context = self._truncate_context(context)
|
|
618
813
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
prompt=messages[-1]["content"],
|
|
622
|
-
metadata={"original_error": error_message},
|
|
623
|
-
)
|
|
814
|
+
# Build error messages with context
|
|
815
|
+
messages = self._build_error_render_messages(error_message, truncated_context)
|
|
624
816
|
|
|
625
817
|
if mode == ProcessMode.COMPLETE:
|
|
626
818
|
if not self._llm_provider:
|
|
627
819
|
return LLMResult(content=error_message) # Fallback processing
|
|
628
820
|
|
|
629
|
-
friendly_error = self._llm_provider.complete(messages)
|
|
821
|
+
friendly_error = self._llm_provider.complete(messages, model=self._model, temperature=self._temperature)
|
|
630
822
|
return LLMResult(content=friendly_error, prompt=messages[-1]["content"])
|
|
631
823
|
|
|
632
824
|
if mode == ProcessMode.STREAM:
|
|
@@ -634,7 +826,7 @@ class MarkdownFlow:
|
|
|
634
826
|
return LLMResult(content=error_message)
|
|
635
827
|
|
|
636
828
|
def stream_generator():
|
|
637
|
-
for chunk in self._llm_provider.stream(messages): # type: ignore[attr-defined]
|
|
829
|
+
for chunk in self._llm_provider.stream(messages, model=self._model, temperature=self._temperature): # type: ignore[attr-defined]
|
|
638
830
|
yield LLMResult(content=chunk, prompt=messages[-1]["content"])
|
|
639
831
|
|
|
640
832
|
return stream_generator()
|
|
@@ -645,6 +837,7 @@ class MarkdownFlow:
|
|
|
645
837
|
self,
|
|
646
838
|
block_index: int,
|
|
647
839
|
variables: dict[str, str | list[str]] | None,
|
|
840
|
+
context: list[dict[str, str]] | None = None,
|
|
648
841
|
) -> list[dict[str, str]]:
|
|
649
842
|
"""Build content block messages."""
|
|
650
843
|
block = self.get_block(block_index)
|
|
@@ -660,29 +853,43 @@ class MarkdownFlow:
|
|
|
660
853
|
# Build message array
|
|
661
854
|
messages = []
|
|
662
855
|
|
|
663
|
-
#
|
|
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)
|
|
664
864
|
if self._document_prompt:
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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())
|
|
871
|
+
|
|
872
|
+
# Combine all parts and add as system message
|
|
873
|
+
if system_parts:
|
|
874
|
+
system_msg = "\n\n".join(system_parts)
|
|
669
875
|
messages.append({"role": "system", "content": system_msg})
|
|
670
|
-
elif has_preserved_content:
|
|
671
|
-
# No document prompt but has preserved content, add explanation alone
|
|
672
|
-
messages.append({"role": "system", "content": OUTPUT_INSTRUCTION_EXPLANATION.strip()})
|
|
673
876
|
|
|
674
|
-
#
|
|
675
|
-
#
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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)
|
|
679
882
|
|
|
680
883
|
# Add processed content as user message (as instruction to LLM)
|
|
681
884
|
messages.append({"role": "user", "content": block_content})
|
|
682
885
|
|
|
683
886
|
return messages
|
|
684
887
|
|
|
685
|
-
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]]:
|
|
686
893
|
"""Build interaction rendering messages."""
|
|
687
894
|
# Check if using custom interaction prompt
|
|
688
895
|
if self._interaction_prompt != DEFAULT_INTERACTION_PROMPT:
|
|
@@ -696,15 +903,32 @@ class MarkdownFlow:
|
|
|
696
903
|
messages = []
|
|
697
904
|
|
|
698
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
|
+
|
|
699
914
|
messages.append({"role": "user", "content": question_text})
|
|
700
915
|
|
|
701
916
|
return messages
|
|
702
917
|
|
|
703
|
-
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]]:
|
|
704
925
|
"""Build validation messages."""
|
|
705
926
|
block = self.get_block(block_index)
|
|
706
927
|
config = self.get_interaction_validation_config(block_index)
|
|
707
928
|
|
|
929
|
+
# Truncate context to configured maximum length
|
|
930
|
+
truncated_context = self._truncate_context(context)
|
|
931
|
+
|
|
708
932
|
if config and config.validation_template:
|
|
709
933
|
# Use custom validation template
|
|
710
934
|
validation_prompt = config.validation_template
|
|
@@ -715,7 +939,8 @@ class MarkdownFlow:
|
|
|
715
939
|
system_message = DEFAULT_VALIDATION_SYSTEM_MESSAGE
|
|
716
940
|
else:
|
|
717
941
|
# Use smart default validation template
|
|
718
|
-
from .
|
|
942
|
+
from .parser import (
|
|
943
|
+
InteractionParser,
|
|
719
944
|
extract_interaction_question,
|
|
720
945
|
generate_smart_validation_template,
|
|
721
946
|
)
|
|
@@ -723,11 +948,17 @@ class MarkdownFlow:
|
|
|
723
948
|
# Extract interaction question
|
|
724
949
|
interaction_question = extract_interaction_question(block.content)
|
|
725
950
|
|
|
726
|
-
#
|
|
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
|
|
727
957
|
validation_template = generate_smart_validation_template(
|
|
728
958
|
target_variable,
|
|
729
|
-
context=
|
|
959
|
+
context=truncated_context,
|
|
730
960
|
interaction_question=interaction_question,
|
|
961
|
+
buttons=buttons,
|
|
731
962
|
)
|
|
732
963
|
|
|
733
964
|
# Replace template variables
|
|
@@ -740,6 +971,11 @@ class MarkdownFlow:
|
|
|
740
971
|
messages = []
|
|
741
972
|
|
|
742
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
|
+
|
|
743
979
|
messages.append({"role": "user", "content": validation_prompt})
|
|
744
980
|
|
|
745
981
|
return messages
|
|
@@ -770,7 +1006,11 @@ class MarkdownFlow:
|
|
|
770
1006
|
|
|
771
1007
|
return messages
|
|
772
1008
|
|
|
773
|
-
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]]:
|
|
774
1014
|
"""Build error rendering messages."""
|
|
775
1015
|
render_prompt = f"""{self._interaction_error_prompt}
|
|
776
1016
|
|
|
@@ -783,6 +1023,12 @@ Original Error: {error_message}
|
|
|
783
1023
|
messages.append({"role": "system", "content": self._document_prompt})
|
|
784
1024
|
|
|
785
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
|
+
|
|
786
1032
|
messages.append({"role": "user", "content": error_message})
|
|
787
1033
|
|
|
788
1034
|
return messages
|