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/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 .utils import (
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, if None only PROMPT_ONLY mode is available
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 == "document":
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
- # Build messages
235
- messages = self._build_content_messages(block_index, variables)
334
+ # Truncate context to configured maximum length
335
+ truncated_context = self._truncate_context(context)
236
336
 
237
- if mode == ProcessMode.PROMPT_ONLY:
238
- return LLMResult(prompt=messages[-1]["content"], metadata={"messages": messages})
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(self, block_index: int, mode: ProcessMode, variables: dict[str, str | list[str]] | None = None):
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
- # Build render messages
287
- messages = self._build_interaction_render_messages(question_text)
392
+ # Truncate context to configured maximum length
393
+ truncated_context = self._truncate_context(context)
288
394
 
289
- if mode == ProcessMode.PROMPT_ONLY:
290
- return LLMResult(
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
- # All button types: validate user input against available buttons
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
- # For text-only inputs, directly use the target variable values
581
+ # Use LLM validation to check if input is relevant to the question
407
582
  if target_values:
408
- return LLMResult(
409
- content="",
410
- variables={target_variable: target_values},
411
- metadata={
412
- "interaction_type": "text_only",
413
- "target_variable": target_variable,
414
- "values": target_values,
415
- },
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(self, error_message: str, mode: ProcessMode) -> LLMResult | Generator[LLMResult, None, None]:
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
- messages = self._build_error_render_messages(error_message)
811
+ # Truncate context to configured maximum length
812
+ truncated_context = self._truncate_context(context)
618
813
 
619
- if mode == ProcessMode.PROMPT_ONLY:
620
- return LLMResult(
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
- # Conditionally add system prompts
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
- system_msg = self._document_prompt
666
- # Only add output instruction explanation when preserved content detected
667
- if has_preserved_content:
668
- system_msg += "\n\n" + OUTPUT_INSTRUCTION_EXPLANATION.strip()
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
- # For most content blocks, historical conversation context is not needed
675
- # because each document block is an independent instruction
676
- # If future specific scenarios need context, logic can be added here
677
- # if context:
678
- # messages.extend(context)
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(self, question_text: str) -> list[dict[str, str]]:
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(self, block_index: int, user_input: dict[str, list[str]], target_variable: str) -> list[dict[str, str]]:
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 .utils import (
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
- # Generate smart validation template
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=None, # Could consider passing context here
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(self, error_message: str) -> list[dict[str, str]]:
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