Skip to content
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

Console: ASCII control characters not correctly handled for chatlas responses in Python #5819

Closed
petetronic opened this issue Dec 18, 2024 · 6 comments
Assignees
Labels
area: console Issues related to Console category. bug Something isn't working lang: python

Comments

@petetronic
Copy link
Collaborator

petetronic commented Dec 18, 2024

System details:

Positron and OS details:

Positron Version: 2025.01.0 (Universal) build 39
Code - OSS Version: 1.93.0
Commit: c7ef34f

Interpreter details:

Python 3.11.7

Describe the issue:

Our Console handling of line clearing ASCII control sequences is not quite right, leaving superfluous newlines / whitespace.

Steps to reproduce the issue:

  1. Note: this scenario relies on an OpenAPI key to reproduce live, though with some logging information below you may not need to run this to devise a fix. Setup an env var for OPENAI_API_KEY. Ensure the key is permissioned for the model selected.

  2. Run this Python in Positron's Console. It uses the chatlas package to chat with OpenAPI:

from chatlas import ChatOpenAI
import os

# Setup your Open API key in an env var
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")

def chat_with_chatlas(prompt: str):
    chatlas_client = ChatOpenAI(api_key=OPENAI_API_KEY, model="gpt-4o-mini")
    try:
        response = chatlas_client.chat(prompt)
        return response
    except Exception as e:
        return f"An error occurred: {e}"

prompt = "What is the capital of France?"
response = chat_with_chatlas(prompt)
  1. Note the extra whitespace in the output in the console:
>>> %run "chatlastest.py"
                                                                                                      
                                                                                                      
                                                                                                      
                                                                                                      
                                                                                                      
                                                                                                      
                                                                                                      
The capital of France is Paris.                                                                       

>>>

Expected or desired behavior:

Here's what the same program run in an ipython Terminal looks like:

In [1]: %run chatlastest.py

The capital of France is Paris.                                                                                                                       

In [2]: 

Were there any error messages in the UI, Output panel, or Developer Tools console?

The chatlas package responds with partially complete lines as tokens are streamed from Open AI, replacing the output content using ASCII control chars. Here's the rough jupyter output message sequence, including escaped ASCII control chars in the JSON representation of messages:

2024-12-18 17:07:13.692 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\u001b[?25l"}
2024-12-18 17:07:14.167 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\nThe                                                                                                                              "}
2024-12-18 17:07:14.170 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\u001b[1A\u001b[2K\nThe capital                                                                                                                      "}
2024-12-18 17:07:14.181 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\u001b[1A\u001b[2K\nThe capital of                                                                                                                   "}
2024-12-18 17:07:14.183 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\u001b[1A\u001b[2K\nThe capital of France                                                                                                            "}
2024-12-18 17:07:14.215 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\u001b[1A\u001b[2K\nThe capital of France is                                                                                                         "}
2024-12-18 17:07:14.217 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\u001b[1A\u001b[2K\nThe capital of France is Paris                                                                                                   "}
2024-12-18 17:07:14.223 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\u001b[1A\u001b[2K\nThe capital of France is Paris.                                                                                                  "}
2024-12-18 17:07:14.225 [debug] <<< RECV stream [iopub]: {"name":"stdout","text":"\r\u001b[2K\u001b[1A\u001b[2K\nThe capital of France is Paris.                                                                                                  \n\u001b[?25h"}
@petetronic petetronic added area: console Issues related to Console category. lang: python bug Something isn't working labels Dec 18, 2024
@petetronic petetronic changed the title Console: ASCII control escape characters not correctly handled for chat turns using chatlas in Python Console: ASCII control characters not correctly handled for chat turns using chatlas in Python Dec 18, 2024
@petetronic petetronic changed the title Console: ASCII control characters not correctly handled for chat turns using chatlas in Python Console: ASCII control characters not correctly handled for chatlas responses in Python Dec 18, 2024
@petetronic
Copy link
Collaborator Author

For reference, he's an explanation of the chars involved:

  1. \r:
  • ASCII carriage return (CR, 0x0D).
  • Moves the cursor to the beginning of the current line without advancing to the next line.
  1. \u001b[2K:
  • ANSI escape sequence to clear the current line.
  • \u001b is the escape character (ESC, 0x1B).
  • [2K specifies "Erase Line," where 2 clears the entire line (including the cursor position).
  1. \u001b[1A:
  • ANSI escape sequence to move the cursor up one line.
  • [1A specifies "Cursor Up," where 1 is the number of lines to move.
  1. \u001b[2K (repeated):
  • Clears the entire line again. At this point, the cursor has already moved up one line due to the previous command.
  1. \n:
  • ASCII line feed (LF, 0x0A).
  • Moves the cursor down to the beginning of the next line.
  1. The capital of France is Paris.:
  • Regular text output displayed in the terminal.
  1. Whitespace Padding:
  • The large number of spaces at the end of the text ensures that any previously displayed content on the same line is overwritten.
  1. \n:
  • Another line feed to move the cursor to a new line.
  1. \u001b[?25h:
  • ANSI escape sequence to show the cursor.
  • \u001b[?25h is a terminal control code where:
    • ?25 refers to the cursor visibility setting.
    • h enables the setting (show the cursor).

@juliasilge juliasilge added this to the 2025.05 Pre-Release milestone Jan 6, 2025
@jcheng5
Copy link
Collaborator

jcheng5 commented Jan 28, 2025

Here's a simpler repro using the very popular rich package (which is what chatlas uses):

import rich.live as Live
import time

with Live.Live() as live:
    for line in ["a", "a\nb", "a\nb\nc"]:
        live.update(line)
        time.sleep(0.5)

With "Run Python File in Terminal" the result is correct:

a
b
c

but with "Run Python File in Console" the result is:

a
a
a
b
a
b
a
b
c

@jcheng5
Copy link
Collaborator

jcheng5 commented Feb 3, 2025

It looks to me like the console actually does know how to handle these characters to some degree. But they are only correctly handled within an ActivityItemOutputStream instance, not across multiple instances, which is what's happening here. I tweaked the HTML to include the ActivityItemOutputStream.id and the below is what you get for the above example.

<div class="runtime-activity">
    <div class="activity-input">
        <div><span class="prompt" style="width: 29px;">&gt;&gt;&gt; </span><span>import rich.live as Live</span></div>
        <div><span class="prompt" style="width: 29px;">... </span><span>import time</span></div>
        <div><span class="prompt" style="width: 29px;">... </span></div>
        <div><span class="prompt" style="width: 29px;">... </span><span>with Live.Live() as live:</span></div>
        <div><span class="prompt" style="width: 29px;">... </span><span> for line in ["a", "a\nb", "a\nb\nc"]:</span>
        </div>
        <div><span class="prompt" style="width: 29px;">... </span><span> live.update(line)</span></div>
        <div><span class="prompt" style="width: 29px;">... </span><span> time.sleep(0.5)</span></div>
        <div><span class="prompt" style="width: 29px;">... </span><span> </span></div>
    </div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_342"><span>a</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_344"><span>a</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_345"><span>a</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_345"><span>b</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_346"><span>a</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_346"><span>b</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_347"><span>a</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_347"><span>b</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_347"><span>c</span></div>
    <div data-activity-id="d1dd156e-90a6d58ab7a9a508f1dc4af4_11657_348"><br></div>
</div>

@jcheng5
Copy link
Collaborator

jcheng5 commented Feb 3, 2025

Aha, there's code that combines consecutive ActivityItemStreams. It's choosing to return additional items in this case, maybe that's what's wrong?

@jcheng5
Copy link
Collaborator

jcheng5 commented Feb 3, 2025

Rewriting addActivityItemStream like this fixes the problem, by (almost) always combining activity item streams:

	public addActivityItemStream(activityItemStream: this): this | undefined {
		// If this ActivityItemStream is terminated, copy its styles to the ActivityItemStream being
		// added and return it as the remainder ActivityItemStream to be processed.
		if (this.terminated) {
			activityItemStream.ansiOutput.copyStylesFrom(this.ansiOutput);
			return activityItemStream;
		}

		this.activityItemStreams.push(activityItemStream);
		return undefined;
	}

But #1635 intentionally moved away from this approach, breaking output into separate little activity item streams to make it easy to trim large amounts of uninterrupted console output. Not sure how to reconcile these two requirements.

(Maybe scan for cursor movement ANSI codes and always combine in that case? Even that might not be enough though, you might emit "a\n", "b\n", resulting in two separate activity item streams, and then emit "\u001b[1A\u001b[1Ahello" which would need to coalesce the previous two.)

(Maybe console trimming needs to be handled differently? Implementation here)

@softwarenerd, any thoughts?

@testlabauto
Copy link
Contributor

testlabauto commented Feb 24, 2025

Verified Fixed

Positron Version(s) : 2025.03.0-83
OS Version          : OSX

Test scenario(s)

Response now looks like:

Image

(Ran from console and from a file to console)

Link(s) to TestRail test cases run or created:

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 11, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: console Issues related to Console category. bug Something isn't working lang: python
Projects
None yet
Development

No branches or pull requests

5 participants