-
Notifications
You must be signed in to change notification settings - Fork 94
RHIDP-12952: persist interrupted conversation #1971
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
0114dd0
215308f
2a5f579
485e520
4fda381
13718e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| """Utilities for repairing truncated markdown content. | ||
|
|
||
| Used when a streaming response is interrupted mid-content to close | ||
| any open markdown constructs (code fences, HTML block tags) that | ||
| would otherwise break rendering. | ||
| """ | ||
|
|
||
| import re | ||
| from typing import Final | ||
|
|
||
| BLOCK_HTML_TAGS: Final[frozenset[str]] = frozenset( | ||
| { | ||
| "div", | ||
| "table", | ||
| "tr", | ||
| "td", | ||
| "th", | ||
| "thead", | ||
| "tbody", | ||
| "details", | ||
| "summary", | ||
| "pre", | ||
| } | ||
| ) | ||
|
|
||
| _FENCE_RE: Final[re.Pattern[str]] = re.compile(r"^(\s{0,3})((`{3,})|(~{3,}))") | ||
| _TAG_RE: Final[re.Pattern[str]] = re.compile(r"<(/?)(\w+)([^>]*?)(/?)>") | ||
|
|
||
|
|
||
| def _process_html_tags(line: str, html_stack: list[str]) -> None: | ||
| """Update *html_stack* with block-level HTML open/close tags found in *line*. | ||
|
|
||
| Parameters: | ||
| line: A single line of text to scan for HTML tags. | ||
| html_stack: Mutable stack tracking open block-level tags. | ||
| """ | ||
|
Comment on lines
+31
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Align new function docstrings with required Google docstring sections. The new docstrings should use the repository’s required Google format ( As per coding guidelines, "Follow Google Python docstring conventions with required sections: Parameters, Returns, Raises, and Attributes for classes." Also applies to: 53-64 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| for tag_match in _TAG_RE.finditer(line): | ||
| is_closing = tag_match.group(1) == "/" | ||
| tag_name = tag_match.group(2).lower() | ||
| is_self_closing = tag_match.group(4) == "/" | ||
|
|
||
| if tag_name not in BLOCK_HTML_TAGS or is_self_closing: | ||
| continue | ||
|
|
||
| if is_closing: | ||
| if html_stack and html_stack[-1] == tag_name: | ||
| html_stack.pop() | ||
| else: | ||
| html_stack.append(tag_name) | ||
|
|
||
|
Comment on lines
+30
to
+50
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Avoid in-place mutation of function parameters in
As per coding guidelines, "Avoid in-place parameter modification anti-patterns; return new data structures instead of modifying function parameters." Proposed refactor-def _process_html_tags(line: str, html_stack: list[str]) -> None:
+def _process_html_tags(line: str, html_stack: list[str]) -> list[str]:
@@
- for tag_match in _TAG_RE.finditer(line):
+ updated_stack = [*html_stack]
+ for tag_match in _TAG_RE.finditer(line):
@@
- if is_closing:
- if html_stack and html_stack[-1] == tag_name:
- html_stack.pop()
+ if is_closing:
+ if updated_stack and updated_stack[-1] == tag_name:
+ updated_stack.pop()
else:
- html_stack.append(tag_name)
+ updated_stack.append(tag_name)
+ return updated_stack
@@
- _process_html_tags(line, html_stack)
+ html_stack = _process_html_tags(line, html_stack)Also applies to: 77-79 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
|
|
||
| def close_open_markdown(text: str) -> str: | ||
| """Return a suffix that closes any open markdown constructs in *text*. | ||
|
|
||
| Scans for unclosed fenced code blocks and unclosed HTML block-level | ||
| tags. Returns only the closing characters (callers append the result). | ||
| Returns an empty string when nothing needs closing. | ||
|
|
||
| Parameters: | ||
| text: Partial markdown content that may contain open constructs. | ||
|
|
||
| Returns: | ||
| A suffix string to append that closes open constructs. | ||
| """ | ||
| if not text or not text.strip(): | ||
| return "" | ||
|
|
||
| lines = text.split("\n") | ||
| in_code_fence = False | ||
| fence_char = "" | ||
| fence_len = 0 | ||
| html_stack: list[str] = [] | ||
|
|
||
| for line in lines: | ||
| fence_match = _FENCE_RE.match(line) | ||
| if not fence_match: | ||
| if not in_code_fence: | ||
| _process_html_tags(line, html_stack) | ||
| continue | ||
|
|
||
| group_3 = fence_match.group(3) | ||
| group_4 = fence_match.group(4) | ||
| matched_group = group_3 or group_4 | ||
| char = "`" if group_3 else "~" | ||
| if not in_code_fence: | ||
| in_code_fence = True | ||
| fence_char = char | ||
| fence_len = len(matched_group) | ||
| elif ( | ||
| char == fence_char | ||
| and len(matched_group) >= fence_len | ||
| and line[fence_match.end() :].strip(" \t") == "" | ||
| ): | ||
| in_code_fence = False | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| fence_char = "" | ||
| fence_len = 0 | ||
|
|
||
| suffix_parts: list[str] = [] | ||
| if in_code_fence: | ||
| suffix_parts.append(f"\n{fence_char * fence_len}") | ||
|
|
||
| for tag in reversed(html_stack): | ||
| suffix_parts.append(f"\n</{tag}>") | ||
|
|
||
| return "".join(suffix_parts) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,7 @@ | |
| from models.common.responses.types import ResponseInput | ||
| from models.common.turn_summary import TurnSummary | ||
| from utils.conversations import append_turn_items_to_conversation | ||
| from utils.markdown_repair import close_open_markdown | ||
| from utils.query import store_query_results, update_conversation_topic_summary | ||
| from utils.responses import get_topic_summary | ||
| from utils.shields import append_turn_to_conversation | ||
|
|
@@ -215,6 +216,28 @@ async def background_update_topic_summary( | |
| ) | ||
|
|
||
|
|
||
| def build_interrupted_response(partial_tokens: list[str]) -> tuple[str, str]: | ||
| """Build the final interrupted response text from accumulated tokens. | ||
|
|
||
| Joins partial tokens, repairs any open markdown constructs, and appends | ||
| an italicized interruption indicator. | ||
|
|
||
| Parameters: | ||
| partial_tokens: List of text deltas accumulated during streaming. | ||
|
|
||
| Returns: | ||
| A tuple of (full_response_text, suffix_to_emit) where full_response_text | ||
| is the complete message for persistence and suffix_to_emit is the new | ||
| content to send as a final SSE token event. | ||
| """ | ||
|
Comment on lines
+219
to
+232
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win Use required Google-style docstring sections in Please update the new helper docstring to the required convention ( As per coding guidelines, "Follow Google Python docstring conventions with required sections: Parameters, Returns, Raises, and Attributes for classes." 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| partial_text = "".join(partial_tokens) | ||
| repaired_text = close_open_markdown(partial_text) | ||
| interrupted_indicator = f"\n\n*{INTERRUPTED_RESPONSE_MESSAGE}*" | ||
| suffix = repaired_text + interrupted_indicator | ||
| final_text = partial_text + suffix | ||
| return final_text, suffix | ||
|
|
||
|
|
||
| async def persist_interrupted_turn( | ||
| context: ResponseGeneratorContext, | ||
| responses_params: ResponsesApiParams, | ||
|
|
@@ -251,7 +274,7 @@ async def persist_interrupted_turn( | |
| original_input, | ||
| [ | ||
| OpenAIResponseMessage( | ||
| role="assistant", content=INTERRUPTED_RESPONSE_MESSAGE | ||
| role="assistant", content=turn_summary.llm_response | ||
| ) | ||
| ], | ||
| ) | ||
|
|
@@ -260,7 +283,7 @@ async def persist_interrupted_turn( | |
| context.client, | ||
| responses_params.conversation, | ||
| cast(str, responses_params.input), | ||
| INTERRUPTED_RESPONSE_MESSAGE, | ||
| turn_summary.llm_response, | ||
| ) | ||
| except Exception: # pylint: disable=broad-except | ||
| logger.exception( | ||
|
|
@@ -342,7 +365,8 @@ async def _on_interrupt() -> None: | |
| if guard[0]: | ||
| return | ||
| guard[0] = True | ||
| turn_summary.llm_response = INTERRUPTED_RESPONSE_MESSAGE | ||
| full_text, _ = build_interrupted_response(turn_summary.partial_tokens) | ||
| turn_summary.llm_response = full_text | ||
| await persist_interrupted_turn( | ||
| context, | ||
| responses_params, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.