diff --git a/connpy/_version.py b/connpy/_version.py index 4115f10..4813dd1 100644 --- a/connpy/_version.py +++ b/connpy/_version.py @@ -1 +1 @@ -__version__ = "6.0.0b4" +__version__ = "6.0.0b5" diff --git a/connpy/ai.py b/connpy/ai.py index 424fe56..97cb9d1 100755 --- a/connpy/ai.py +++ b/connpy/ai.py @@ -271,7 +271,7 @@ class ai: raise KeyboardInterrupt chunk_callback(delta.content) - if not debug and not chunk_callback: + if not chunk_callback: if not is_streaming_text: # Stop spinner definitively if status: @@ -279,9 +279,15 @@ class ai: status.stop() except Exception: pass + + # Create a stable, direct Console to bypass _ConsoleProxy recreation bugs + from rich.console import Console as RichConsole + from .printer import connpy_theme, get_original_stdout + stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout()) + live_display = Live( Panel(Markdown(full_content), title=title, border_style=border, expand=False), - console=self.console, + console=stable_console, refresh_per_second=8, transient=False ) @@ -303,7 +309,10 @@ class ai: ) except Exception: pass - live_display.stop() + try: + live_display.stop() + except Exception: + pass # Rebuild complete response from chunks try: @@ -989,7 +998,7 @@ class ai: streamed_response = False try: safe_messages = self._sanitize_messages(messages) - if stream and chunk_callback: + if stream: response, streamed_response = self._stream_completion( model=model, messages=safe_messages, tools=tools, api_key=key, status=status, label=label, debug=debug, num_retries=3, @@ -1028,8 +1037,8 @@ class ai: if msg_dict.get("tool_calls") and msg_dict.get("content") == "": msg_dict["content"] = None messages.append(msg_dict) - if debug and resp_msg.content: - # In CLI debug mode, only print intermediate reasoning if there are tool calls. + if debug and resp_msg.content and not streamed_response: + # In CLI debug mode, only print intermediate reasoning if there are tool calls AND it wasn't already streamed. # If there are no tool calls, this content is the final answer and will be printed by the caller. if resp_msg.tool_calls: if status: diff --git a/connpy/grpc_layer/stubs.py b/connpy/grpc_layer/stubs.py index a594edc..b15c710 100644 --- a/connpy/grpc_layer/stubs.py +++ b/connpy/grpc_layer/stubs.py @@ -604,21 +604,33 @@ class AIStub: if response.debug_message: if debug: + if live_display: + try: live_display.stop() + except: pass if status: try: status.stop() except: pass printer.console.print(Text.from_ansi(response.debug_message)) - if status: + if live_display: + try: live_display.start() + except: pass + elif status: try: status.start() except: pass continue if response.important_message: + if live_display: + try: live_display.stop() + except: pass if status: try: status.stop() except: pass printer.console.print(Text.from_ansi(response.important_message)) - if status: + if live_display: + try: live_display.start() + except: pass + elif status: try: status.start() except: pass continue @@ -627,14 +639,33 @@ class AIStub: if response.text_chunk: full_content += response.text_chunk - if status and not debug: - # Update the spinner line with a preview of the response - preview = full_content.replace("\n", " ").strip() - if len(preview) > 60: preview = preview[:57] + "..." - status.update(f"[ai_status]{preview}") + if not live_display: + if status: + try: status.stop() + except: pass + + from rich.console import Console as RichConsole + from ..printer import connpy_theme, get_original_stdout + stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout()) + + # We default to Engineer title during stream, final result will correct it if needed + live_display = Live( + Panel(Markdown(full_content), title="[bold engineer]Network Engineer[/bold engineer]", border_style="engineer", expand=False), + console=stable_console, + refresh_per_second=8, + transient=False + ) + live_display.start() + else: + live_display.update( + Panel(Markdown(full_content), title="[bold engineer]Network Engineer[/bold engineer]", border_style="engineer", expand=False) + ) continue if response.is_final: + if live_display: + try: live_display.stop() + except: pass # Final stop for status to ensure it disappears before the panel if status: try: status.stop() @@ -646,10 +677,13 @@ class AIStub: role_label = "Network Architect" if responder == "architect" else "Network Engineer" title = f"[bold {alias}]{role_label}[/bold {alias}]" - # Always print the final Panel content_to_print = full_content or final_result.get("response", "") if content_to_print: - printer.console.print(Panel(Markdown(content_to_print), title=title, border_style=alias, expand=False)) + if live_display: + # Re-render the final frame with correct title/colors + live_display.update(Panel(Markdown(content_to_print), title=title, border_style=alias, expand=False)) + else: + printer.console.print(Panel(Markdown(content_to_print), title=title, border_style=alias, expand=False)) break except Exception as e: # Check if it was a gRPC error that we should let handle_errors catch