Skip to content

Commit c680c9f

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 c680c9f

File tree

1 file changed

+157
-0
lines changed

1 file changed

+157
-0
lines changed

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

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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 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+
48+
def get_suggestion(self, buffer, document):
49+
if document.line_count == 1:
50+
return Suggestion(" (Add a few new lines to see multiline completion)")
51+
cursor_line = document.cursor_position_row
52+
text = document.text.split("\n")[cursor_line]
53+
if not text.strip():
54+
return None
55+
index = None
56+
for i, l in enumerate(universal_declaration_of_human_rights):
57+
if l.startswith(text):
58+
index = i
59+
break
60+
if index is None:
61+
return None
62+
return Suggestion(
63+
universal_declaration_of_human_rights[index][len(text) :]
64+
+ "\n"
65+
+ "\n".join(universal_declaration_of_human_rights[index + 1 :])
66+
)
67+
68+
69+
class AppendMultilineAutoSuggestionInAnyLine(Processor):
70+
71+
def __init__(self, style: str = "class:auto-suggestion") -> None:
72+
self.style = style
73+
74+
def apply_transformation(self, ti: TransformationInput) -> Transformation:
75+
76+
# a convenient noop transformation that does nothing.
77+
noop = Transformation(fragments=ti.fragments)
78+
79+
# We get out of the way if the prompt is only one line, and let prompt_toolkit handle the rest.
80+
if ti.document.line_count == 1:
81+
return noop
82+
83+
# first everything before the current line is unchanged.
84+
if ti.lineno < ti.document.cursor_position_row:
85+
return noop
86+
87+
buffer = ti.buffer_control.buffer
88+
if not buffer.suggestion or not ti.document.is_cursor_at_the_end_of_line:
89+
return noop
90+
91+
# compute the number delta between the current cursor line and line we are transforming
92+
# transformed line can either be suggestions, or an existing line that is shifted.
93+
delta = ti.lineno - ti.document.cursor_position_row
94+
95+
# convert the suggestion into a list of lines
96+
suggestions = buffer.suggestion.text.splitlines()
97+
if not suggestions:
98+
return noop
99+
100+
if delta == 0:
101+
# append suggestion to current line
102+
suggestion = suggestions[0]
103+
return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
104+
elif delta < len(suggestions):
105+
# append a line with the nth line of the suggestion
106+
suggestion = suggestions[delta]
107+
assert "\n" not in suggestion
108+
return Transformation([(self.style, suggestion)])
109+
else:
110+
# return the line that is by delta-1 suggestion (first suggestion does not shifts)
111+
shift = ti.lineno - len(suggestions) + 1
112+
return Transformation(ti.get_line(shift))
113+
114+
115+
def main():
116+
# Create some history first. (Easy for testing.)
117+
118+
autosuggest = FakeLLMAutoSuggest()
119+
# Print help.
120+
print("This CLI has fish-style auto-suggestion enabled across multiple lines.")
121+
print("This will try to complete the universal declaration of human rights.")
122+
print("")
123+
print(" " + "\n ".join(universal_declaration_of_human_rights))
124+
print("")
125+
print("Add a few new lines to see multiline completion, and start typing.")
126+
print("Press Control-C to retry. Control-D to exit.")
127+
print()
128+
129+
session = PromptSession(
130+
auto_suggest=autosuggest,
131+
enable_history_search=False,
132+
reserve_space_for_menu=5,
133+
multiline=True,
134+
prompt_continuation="... ",
135+
input_processors=[
136+
ConditionalProcessor(
137+
processor=AppendMultilineAutoSuggestionInAnyLine(),
138+
filter=HasFocus(DEFAULT_BUFFER) & ~IsDone(),
139+
),
140+
],
141+
)
142+
143+
while True:
144+
try:
145+
text = session.prompt(
146+
"Say something (Esc-enter : accept, enter : new line): "
147+
)
148+
except KeyboardInterrupt:
149+
pass # Ctrl-C pressed. Try again.
150+
else:
151+
break
152+
153+
print(f"You said: {text}")
154+
155+
156+
if __name__ == "__main__":
157+
main()

0 commit comments

Comments
 (0)