Skip to content

Commit fbd55ff

Browse files
committed
fix: improved main loop logic, squashed bugs
1 parent 9b8f4e1 commit fbd55ff

File tree

5 files changed

+57
-27
lines changed

5 files changed

+57
-27
lines changed

CONTRIBUTING.md

+2
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@ Tests are currently covering:
77
- tools like shell and Python
88
- integration tests that make LLM calls, run the generated code, and checks the output
99
- this could be used as a LLM eval harness
10+
11+
There are also some integration tests in `./tests/test-integration.sh` for an alternative way to test.

gptme/cli.py

+35-21
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ def handle_cmd(
171171
yield msg
172172
yield from execute_msg(msg, ask=not no_confirm)
173173
case _:
174+
# first undo the '.help' command itself
175+
logmanager.undo(1, quiet=True)
176+
174177
print("Available commands:")
175178
for cmd, desc in action_descriptions.items():
176179
print(f" {cmd}: {desc}")
@@ -282,6 +285,13 @@ def main(
282285
else:
283286
promptmsgs = [Message("system", prompt_system)]
284287

288+
# we need to run this before checking stdin, since the interactive doesn't work with the switch back to interactive mode
289+
logfile = get_logfile(
290+
name, interactive=(not prompts and interactive) and sys.stdin.isatty()
291+
)
292+
print(f"Using logdir {logfile.parent}")
293+
log = LogManager.load(logfile, initial_msgs=promptmsgs, show_hidden=show_hidden)
294+
285295
# if stdin is not a tty, we're getting piped input
286296
if not sys.stdin.isatty():
287297
# fetch prompt from stdin
@@ -293,10 +303,6 @@ def main(
293303
sys.stdin.close()
294304
sys.stdin = open("/dev/tty")
295305

296-
logfile = get_logfile(name, interactive=not prompts and interactive)
297-
print(f"Using logdir {logfile.parent}")
298-
log = LogManager.load(logfile, initial_msgs=promptmsgs, show_hidden=show_hidden)
299-
300306
# print log
301307
log.print()
302308
print("--- ^^^ past messages ^^^ ---")
@@ -318,15 +324,35 @@ def main(
318324
# if prompts given on cli, insert next prompt into log
319325
if prompts:
320326
prompt = prompts.pop(0)
321-
log.append(Message("user", prompt))
327+
msg = Message("user", prompt)
328+
log.append(msg)
329+
# if prompt is a user-command, execute it
330+
if execute_cmd(msg, log):
331+
continue
332+
# if prompts exhausted and non-interactive, exit
333+
elif not interactive:
334+
logger.info("Non-interactive and exhausted prompts, exiting")
335+
exit(0)
322336

337+
# ask for input if no prompt, generate reply, and run tools
323338
for msg in loop(log, no_confirm, model, llm, stream=stream):
324339
log.append(msg)
340+
# run any user-commands, if msg is from user
341+
if msg.role == "user" and execute_cmd(msg, log):
342+
break
325343

326-
# if non-interactive and prompts have been exhausted, exit
327-
if not interactive and not prompts:
328-
logger.info("Non-interactive and exhausted prompts, exiting")
329-
exit(0)
344+
345+
def execute_cmd(msg, log):
346+
"""Executes any user-command, returns True if command was executed."""
347+
assert msg.role == "user"
348+
349+
# if message starts with ., treat as command
350+
# when command has been run,
351+
if msg.content[:1] in [".", "$", "/"]:
352+
for resp in handle_cmd(msg.content, log, no_confirm=True):
353+
log.append(resp)
354+
return True
355+
return False
330356

331357

332358
def loop(
@@ -342,18 +368,6 @@ def loop(
342368
if log[-1].role == "assistant":
343369
yield from execute_msg(log[-1], ask=not no_confirm)
344370

345-
# execute user command
346-
if log[-1].role == "user":
347-
inquiry = log[-1].content
348-
# if message starts with ., treat as command
349-
# when command has been run,
350-
if inquiry.startswith(".") or inquiry.startswith("$"):
351-
yield from handle_cmd(inquiry, log, no_confirm=no_confirm)
352-
# we need to re-assign `log` here since it may be replaced by `handle_cmd`
353-
# FIXME: this is pretty bad hack to get things working, needs to be refactored
354-
if inquiry != ".continue":
355-
return
356-
357371
# If last message was a response, ask for input.
358372
# If last message was from the user (such as from crash/edited log),
359373
# then skip asking for input and generate response

gptme/prompts.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
1+
import logging
12
import os
23
import shutil
34
import subprocess
45
from datetime import date
6+
from typing import Generator
57

68
from .config import get_config
79
from .message import Message
810

9-
from typing import Generator
10-
11-
import logging
12-
1311
USER = os.environ.get("USER", None)
1412

1513

@@ -97,7 +95,8 @@ def initial_prompt(short: bool = False) -> Generator[Message, None, None]:
9795
yield Message(
9896
"system",
9997
"""
100-
You are gptme, a AI assistant powered by large language models that helps the user and can run code and commands on their behalf.
98+
You are gptme, an AI assistant CLI tool powered powered by large language models that helps the user.
99+
You can run code and execute terminal commands on their local machine.
101100
The assistant shows the user to write code, interact with the system, and access the internet. The user will then choose to execute the suggested commands.
102101
All code should be copy-pasteable or saved, and runnable as-is. Do not use placeholders like `$REPO` unless they have been set.
103102
When the output of a command is of interest, end the code block so that the user can execute it before continuing.

gptme/tools/useredit.py

-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ def edit_text_with_editor(initial_text: str, ext=None) -> str:
3535
# Check that the user actually edited the file.
3636
if edited_text == initial_text:
3737
logger.info("No changes made, exiting.")
38-
print("No changes made, exiting.")
3938

4039
# Delete the temporary file.
4140
os.remove(temp_filename)

tests/test-integration.sh

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/bash
2+
3+
set -e
4+
set -x
5+
6+
# set this to indicate tests are run (non-interactive)
7+
export PYTEST_CURRENT_TEST=1
8+
9+
# test stdin and cli-provided prompt
10+
echo "The project mascot is a flying pig" | gptme "What is the project mascot?"
11+
12+
# test python command
13+
gptme ".python print('hello world')"
14+
15+
# test shell command
16+
gptme ".shell echo 'hello world'"

0 commit comments

Comments
 (0)