diff --git a/ai-hub/app/core/orchestration/agent_loop.py b/ai-hub/app/core/orchestration/agent_loop.py index 494c30c..4db1952 100644 --- a/ai-hub/app/core/orchestration/agent_loop.py +++ b/ai-hub/app/core/orchestration/agent_loop.py @@ -164,7 +164,9 @@ from sqlalchemy.orm.attributes import flag_modified flag_modified(instance, "tool_call_counts") - final_reasoning = instance.last_reasoning + # 4.3: Post-processing to compress boilerplate from reasoning + final_reasoning = self._compress_reasoning(instance.last_reasoning or "") + # Clear reasoning as the task is now complete instance.last_reasoning = None db.commit() @@ -193,3 +195,18 @@ finally: heartbeat_task.cancel() db.close() + + @staticmethod + def _compress_reasoning(text: str) -> str: + """Deduplicates consecutive identical turn markers and boilerplate strategies.""" + import re + # 1. Deduplicate consecutive turn headers that have no content between them + # (e.g. Turn 1 followed immediately by Turn 2) + turn_pattern = r"(\n\n---\nšŸ›°ļø \*\*\[Turn \d+\] thinking\.\.\.\*\*\n\n)(?=\n\n---\nšŸ›°ļø \*\*\[Turn \d+\] thinking\.\.\.\*\*\n\n)" + text = re.sub(turn_pattern, "", text) + + # 2. Deduplicate consecutive identical 'Strategy:' boilerplate + strategy_pattern = r"(Strategy: Executing orchestrated tasks in progress\.\.\.\n?)(?=\s*Strategy: Executing orchestrated tasks in progress\.\.\.)" + text = re.sub(strategy_pattern, "", text) + + return text.strip() diff --git a/ai-hub/app/core/orchestration/stream.py b/ai-hub/app/core/orchestration/stream.py index 98237c5..22cc1d1 100644 --- a/ai-hub/app/core/orchestration/stream.py +++ b/ai-hub/app/core/orchestration/stream.py @@ -54,7 +54,11 @@ async for event in self._flush_prefix("", turn_header): yield event else: - strategy_part = self._apply_turn_header(self.tag_buffer, turn_header) + if not self.header_sent: + self.header_sent = True + yield {"type": "reasoning", "content": turn_header} + + strategy_part = self._apply_turn_header(self.tag_buffer) if strategy_part: yield {"type": "content", "content": strategy_part} self.tag_buffer = "" @@ -91,13 +95,17 @@ self.tag_buffer = "" async def _flush_prefix(self, extra_text: str, header: str) -> AsyncGenerator[Dict[str, Any], None]: + if not self.header_sent: + self.header_sent = True + yield {"type": "reasoning", "content": header} + full_text = self.prefix_buffer + extra_text self.prefix_buffer = "" # Clear it - processed = self._apply_turn_header(full_text, header) + processed = self._apply_turn_header(full_text) if processed: yield {"type": "content", "content": processed} - def _apply_turn_header(self, text: str, header: str) -> Optional[str]: + def _apply_turn_header(self, text: str) -> Optional[str]: # List of patterns to strip (hallucinated headers from various LLM generations) strip_patterns = [ r"(?i)^.*\[Turn\s*\d+\].*$", @@ -117,34 +125,17 @@ for pattern in strip_patterns: text = re.sub(pattern, "", text, flags=re.IGNORECASE | re.MULTILINE) - # Dynamic Title Extraction - if not self.header_sent: - # 1. Primary: Look for "Title: [Title Text]" - title_match = re.search(r"(?i)Title:\s*(.*?)\n", text) - if title_match: - custom_title = title_match.group(1).strip() - header = re.sub(r"Thinking\.\.\.", custom_title, header) - text = text[:title_match.start()] + text[title_match.end():] - else: - # 2. Secondary: If AI uses a Markdown header like "### šŸš€ My Title" - md_header_match = re.search(r"(?i)^###\s*.*?\s*(.*?)\n", text, re.MULTILINE) - if md_header_match: - custom_title = md_header_match.group(1).strip() - header = re.sub(r"Thinking\.\.\.", custom_title, header) - text = text[:md_header_match.start()] + text[md_header_match.end():] + # Dynamic Title Extraction (We skip this for now as we separated reasoning, but keep the strip logic) + # 1. Primary: Look for "Title: [Title Text]" + title_match = re.search(r"(?i)Title:\s*(.*?)\n", text) + if title_match: + text = text[:title_match.start()] + text[title_match.end():] + else: + # 2. Secondary: If AI uses a Markdown header like "### šŸš€ My Title" + md_header_match = re.search(r"(?i)^###\s*.*?\s*(.*?)\n", text, re.MULTILINE) + if md_header_match: + text = text[:md_header_match.start()] + text[md_header_match.end():] - if not text.strip(): - # If after stripping the text is empty, but we must send the header, do it now - if not self.header_sent: - self.header_sent = True - return header - return "" - - if not self.header_sent: - self.header_sent = True - # Prepend the system's authoritative header - return header + text.lstrip() - return text async def end_stream(self, turn: int) -> AsyncGenerator[Dict[str, Any], None]: """Flushes any remaining buffered text at the very end of the stream.""" @@ -160,7 +151,7 @@ if self._in_thinking_tag: yield {"type": "reasoning", "content": self.tag_buffer} else: - processed = self._apply_turn_header(self.tag_buffer, turn_header) + processed = self._apply_turn_header(self.tag_buffer) if processed: yield {"type": "content", "content": processed} self.tag_buffer = ""