Skip to content

Commit 869b4b7

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 869b4b7

File tree

1 file changed

+159
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)