Skip to content

Commit 61188a9

Browse files
committed
Basic multiline suggestion example
Unline inlne completion; this is a bit more tricky to set up, in particular it is hard to choose default that will suit everyone – whether to shift existing line, how to accept/reject, show elision... So we just for now add an example on how this can be used. I will most likely make use of it in IPython in the next few weeks/month, and can report back on the usability.
1 parent cd7c6a2 commit 61188a9

File tree

2 files changed

+156
-0
lines changed

2 files changed

+156
-0
lines changed

Diff for: examples/prompts/multiline-autosuggest.py

+154
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python
2+
"""
3+
A more complex example of a CLI that demonstrates fish-style auto suggestion
4+
across multiple lines.
5+
6+
This can typically be used for LLM that may return multi-line responses.
7+
8+
Note that unlike simple autosuggest, using multiline autosuggest requires more
9+
care as it may shift the buffer layout, and care must taken ton consider the
10+
various case when the number iof suggestions lines is longer than the number of
11+
lines in the buffer, what happens to the existing text (is it pushed down, or
12+
hidden until the suggestion is accepted) Etc.
13+
14+
So generally multiline autosuggest will require a custom processor to handle the
15+
different use case and user experience.
16+
17+
We also have not hooked any keys to accept the suggestion, so it will be up to you
18+
to decide how and when to accept the suggestion, accept it as a whole, like by line, or
19+
token by token.
20+
"""
21+
22+
from prompt_toolkit import PromptSession
23+
from prompt_toolkit.auto_suggest import AutoSuggest, Suggestion
24+
from prompt_toolkit.enums import DEFAULT_BUFFER
25+
from prompt_toolkit.filters import HasFocus, IsDone
26+
from prompt_toolkit.layout.processors import (
27+
ConditionalProcessor,
28+
Processor,
29+
Transformation,
30+
TransformationInput,
31+
)
32+
33+
universal_declaration_of_human_rights = """
34+
All human beings are born free and equal in dignity and rights.
35+
They are endowed with reason and conscience and should act towards one another
36+
in a spirit of brotherhood
37+
Everyone is entitled to all the rights and freedoms set forth in this
38+
Declaration, without distinction of any kind, such as race, colour, sex,
39+
language, religion, political or other opinion, national or social origin,
40+
property, birth or other status. Furthermore, no distinction shall be made on
41+
the basis of the political, jurisdictional or international status of the
42+
country or territory to which a person belongs, whether it be independent,
43+
trust, non-self-governing or under any other limitation of sovereignty.""".strip().splitlines()
44+
45+
46+
class FakeLLMAutoSuggest(AutoSuggest):
47+
def get_suggestion(self, buffer, document):
48+
if document.line_count == 1:
49+
return Suggestion(" (Add a few new lines to see multiline completion)")
50+
cursor_line = document.cursor_position_row
51+
text = document.text.split("\n")[cursor_line]
52+
if not text.strip():
53+
return None
54+
index = None
55+
for i, l in enumerate(universal_declaration_of_human_rights):
56+
if l.startswith(text):
57+
index = i
58+
break
59+
if index is None:
60+
return None
61+
return Suggestion(
62+
universal_declaration_of_human_rights[index][len(text) :]
63+
+ "\n"
64+
+ "\n".join(universal_declaration_of_human_rights[index + 1 :])
65+
)
66+
67+
68+
class AppendMultilineAutoSuggestionInAnyLine(Processor):
69+
def __init__(self, style: str = "class:auto-suggestion") -> None:
70+
self.style = style
71+
72+
def apply_transformation(self, ti: TransformationInput) -> Transformation:
73+
# a convenient noop transformation that does nothing.
74+
noop = Transformation(fragments=ti.fragments)
75+
76+
# We get out of the way if the prompt is only one line, and let prompt_toolkit handle the rest.
77+
if ti.document.line_count == 1:
78+
return noop
79+
80+
# first everything before the current line is unchanged.
81+
if ti.lineno < ti.document.cursor_position_row:
82+
return noop
83+
84+
buffer = ti.buffer_control.buffer
85+
if not buffer.suggestion or not ti.document.is_cursor_at_the_end_of_line:
86+
return noop
87+
88+
# compute the number delta between the current cursor line and line we are transforming
89+
# transformed line can either be suggestions, or an existing line that is shifted.
90+
delta = ti.lineno - ti.document.cursor_position_row
91+
92+
# convert the suggestion into a list of lines
93+
suggestions = buffer.suggestion.text.splitlines()
94+
if not suggestions:
95+
return noop
96+
97+
if delta == 0:
98+
# append suggestion to current line
99+
suggestion = suggestions[0]
100+
return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
101+
elif delta < len(suggestions):
102+
# append a line with the nth line of the suggestion
103+
suggestion = suggestions[delta]
104+
assert "\n" not in suggestion
105+
return Transformation([(self.style, suggestion)])
106+
else:
107+
# return the line that is by delta-1 suggestion (first suggestion does not shifts)
108+
shift = ti.lineno - len(suggestions) + 1
109+
return Transformation(ti.get_line(shift))
110+
111+
112+
def main():
113+
# Create some history first. (Easy for testing.)
114+
115+
autosuggest = FakeLLMAutoSuggest()
116+
# Print help.
117+
print("This CLI has fish-style auto-suggestion enabled across multiple lines.")
118+
print("This will try to complete the universal declaration of human rights.")
119+
print("")
120+
print(" " + "\n ".join(universal_declaration_of_human_rights))
121+
print("")
122+
print("Add a few new lines to see multiline completion, and start typing.")
123+
print("Press Control-C to retry. Control-D to exit.")
124+
print()
125+
126+
session = PromptSession(
127+
auto_suggest=autosuggest,
128+
enable_history_search=False,
129+
reserve_space_for_menu=5,
130+
multiline=True,
131+
prompt_continuation="... ",
132+
input_processors=[
133+
ConditionalProcessor(
134+
processor=AppendMultilineAutoSuggestionInAnyLine(),
135+
filter=HasFocus(DEFAULT_BUFFER) & ~IsDone(),
136+
),
137+
],
138+
)
139+
140+
while True:
141+
try:
142+
text = session.prompt(
143+
"Say something (Esc-enter : accept, enter : new line): "
144+
)
145+
except KeyboardInterrupt:
146+
pass # Ctrl-C pressed. Try again.
147+
else:
148+
break
149+
150+
print(f"You said: {text}")
151+
152+
153+
if __name__ == "__main__":
154+
main()

Diff for: pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,6 @@ extend-exclude = [
6464
"tests/test_buffer.py",
6565
"tests/test_cli.py",
6666
"tests/test_regular_languages.py",
67+
# complains about some spelling in human right declaration.
68+
"examples/prompts/multiline-autosuggest.py",
6769
]

0 commit comments

Comments
 (0)