Skip to content

Commit 86103ca

Browse files
committed
feat: many changes, now supports local inference via llama_cpp.server, some refactoring, improved README
1 parent f60cfd0 commit 86103ca

File tree

10 files changed

+376
-125
lines changed

10 files changed

+376
-125
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
*.log
44
__pycache__
55
.coverage
6+
*logs*

README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,41 @@ Just me playing with large language models, langchain, etc.
88

99
## gptme
1010

11-
An interactive CLI to let you chat with ChatGPT, with extra tools like:
11+
An interactive CLI to let you interact with LLMs in a Chat-style interface.
1212

13-
- Execute shell/Python code on the local machine.
14-
- Command output (stdout & stderr + error code) will be feeded back to the agent, making it able to self-correct errors etc.
15-
- Handle long context sizes through summarization.
13+
With **features** like:
14+
15+
- Supports OpenAI and **any model that runs in llama**
16+
- Thanks to llama-cpp-server!
17+
- Tools
18+
- Access to the local machine
19+
- Execute shell/Python code on the local machine.
20+
- Command output (stdout & stderr + error code) will be feeded back to the agent, making it able to self-correct errors etc.
21+
- Can handle long context sizes through summarization.
1622
- (not very well developed)
1723

1824

25+
### Usage
26+
27+
Install deps:
28+
29+
```sh
30+
poetry install
31+
```
32+
33+
To use locally, you need to start llama-cpp-server:
34+
35+
```sh
36+
poetry run python -m llama_cpp.server --model ~/ML/Manticore-13B.ggmlv3.q4_1.bin
37+
```
38+
39+
Then you can interact with it using:
40+
```sh
41+
gptme --llm llama
42+
43+
```
44+
45+
1946
## TODO
2047

2148
Ideas for things to try:

gpt_playground/shell.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,33 @@
33
"""
44

55
from copy import copy
6-
import openai
6+
import subprocess
7+
import sys
78

89
import click
10+
import openai
11+
12+
13+
# TODO: use this generation in gptme
14+
if sys.platform == "linux":
15+
system_info = """
16+
OS: Arch Linux
17+
"""
18+
elif sys.platform == "darwin":
19+
system_info = f"""
20+
$ uname -a
21+
{subprocess.call(["uname", "-a"])}
22+
Darwin erb-m2.localdomain 21.6.0 Darwin Kernel Version 21.6.0: Sat Jun 18 17:07:28 PDT 2022; root:xnu-8020.140.41~1/RELEASE_ARM64_T8110 arm64 arm Darwin
23+
$ sw_vers
24+
{subprocess.call(["sw_vers"])}
25+
ProductName: macOS
26+
ProductVersion: 12.5
27+
BuildVersion: 21G72
28+
"""
29+
else:
30+
system_info = "Unknown/unsupported OS (Windows?)"
931

10-
# TODO: Generate automatically
11-
system_info_arch = """
12-
OS: Arch Linux
13-
"""
1432

15-
system_info_macos = """
16-
$ uname -a
17-
Darwin erb-m2.localdomain 21.6.0 Darwin Kernel Version 21.6.0: Sat Jun 18 17:07:28 PDT 2022; root:xnu-8020.140.41~1/RELEASE_ARM64_T8110 arm64 arm Darwin
18-
$ sw_vers
19-
ProductName: macOS
20-
ProductVersion: 12.5
21-
BuildVersion: 21G72
22-
"""
2333

2434
initial_messages = [
2535
{
@@ -29,7 +39,7 @@
2939
},
3040
{
3141
"role": "system",
32-
"content": """First, let's check the environment:\n""" + system_info_macos
42+
"content": """First, let's check the environment:\n""" + system_info
3343
}
3444
]
3545

gptme/cli.py

Lines changed: 143 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,67 @@
11
"""
2-
This is a long-living agent that is designed to be a companion to the user.
2+
GPTMe
3+
=====
4+
5+
This is a long-living AI language model called GPTMe, it is designed to be a helpful companion.
36
47
It should be able to help the user in various ways, such as:
58
6-
- Acting as an executive assistant
7-
- Answering questions
8-
- Helping strategize
9-
- Giving advice
109
- Writing code
10+
- Using the shell
11+
- Assisting with technical tasks
1112
- Writing prose (such as email, code docs, etc.)
12-
- Providing companionship
13+
- Acting as an executive assistant
1314
1415
The agent should be able to learn from the user and adapt to their needs.
15-
The agent should try to always output information using markdown formatting, preferably using GitHub Flavored Markdown.
16+
The agent should always output information using GitHub Flavored Markdown.
17+
THe agent should always output code and commands in markdown code blocks with the appropriate language tag.
1618
1719
Since the agent is long-living, it should be able to remember things that the user has told it,
1820
to do so, it needs to be able to store and query past conversations in a database.
1921
"""
22+
# The above docstring is the first message that the agent will see.
2023

2124
from typing import Literal, Generator
2225
from datetime import datetime
2326
import logging
2427
import os
2528
import sys
2629
import shutil
30+
import readline # noqa: F401
2731
import itertools
2832
from pathlib import Path
2933

30-
from termcolor import colored
34+
from termcolor import colored # type: ignore
3135
import openai
3236
import click
3337

34-
import typing
3538

3639
from .constants import role_color
37-
from .tools import _execute_linecmd, _execute_codeblock, _execute_save, _execute_shell, _execute_python
40+
from .tools import (
41+
_execute_linecmd,
42+
_execute_codeblock,
43+
_execute_save,
44+
_execute_shell,
45+
_execute_python,
46+
)
3847
from .util import msgs2dicts
3948
from .message import Message
4049
from .logmanager import LogManager
50+
from .prompts import initial_prompt
4151

4252
logger = logging.getLogger(__name__)
4353
logging.basicConfig(level=logging.INFO)
4454

4555

56+
LLMChoice = Literal["openai", "llama"]
57+
58+
readline.add_history("What is love?")
59+
readline.add_history("Have you heard about an open-source app called ActivityWatch?")
60+
readline.add_history(
61+
"Explain the 'Attention is All You Need' paper in the style of Andrej Karpathy."
62+
)
63+
64+
4665
def get_logfile(logdir: str) -> str:
4766
logdir = logdir + "/"
4867
if not os.path.exists(logdir):
@@ -106,28 +125,52 @@ def handle_cmd(cmd: str, logmanager: LogManager) -> Generator[Message, None, Non
106125
sys.exit(0)
107126
case _:
108127
print("Available commands:")
109-
for cmd in typing.get_args(Actions):
110-
desc = action_descriptions.get(cmd, default="missing description")
128+
for cmd, desc in action_descriptions.items():
111129
print(f" {cmd}: {desc}")
112130

113131

114132
@click.group()
115133
def cli():
116134
pass
117135

136+
118137
script_path = Path(os.path.realpath(__file__))
119138

139+
120140
@cli.command()
121-
@click.argument("command" , default=None, required=False)
141+
@click.argument("command", default=None, required=False)
142+
@click.option(
143+
"--logs",
144+
default=script_path.parent.parent / "logs",
145+
help="Folder where conversation logs are stored",
146+
)
147+
@click.option("--llm", default="openai", help="LLM to use")
122148
@click.option(
123-
"--logs", default=script_path.parent.parent / "logs", help="Folder where conversation logs are stored"
149+
"--stream",
150+
is_flag=True,
151+
default=True,
152+
help="Wether to use streaming (only supported for openai atm)",
124153
)
125-
def main(command: str | None, logs: str):
126-
"""Main interactivity loop."""
154+
@click.option(
155+
"--prompt",
156+
default="short",
157+
help="Can be 'short', 'full', or a custom prompt",
158+
)
159+
def main(command: str | None, logs: str, llm: LLMChoice, stream: bool, prompt: str):
160+
"""
161+
GPTMe, a CLI interface for LLMs.
162+
"""
127163
openai.api_key = os.environ["OPENAI_API_KEY"]
164+
openai.api_base = "http://localhost:8000/v1"
128165

166+
if prompt in ["full", "short"]:
167+
promptmsgs = initial_prompt(short=prompt == "short")
168+
else:
169+
promptmsgs = [Message("system", prompt)]
170+
171+
print(f"Using logdir {logs}")
129172
logfile = get_logfile(logs)
130-
logmanager = LogManager.load(logfile)
173+
logmanager = LogManager.load(logfile, initial_msgs=promptmsgs)
131174
logmanager.print()
132175
print("--- ^^^ past messages ^^^ ---")
133176

@@ -143,53 +186,117 @@ def main(command: str | None, logs: str):
143186
while True:
144187
# if non-interactive command given on cli, exit
145188
if command_triggered:
189+
print("Command triggered, exiting")
146190
break
147191

148192
# If last message was a response, ask for input.
149-
# If last message was from the user (such as from crash/edited log),
193+
# If last message was from the user (such as from crash/edited log),
150194
# then skip asking for input and generate response
151-
if log[-1].role in ["system", "assistant"]:
152-
prompt = colored("User", role_color["user"]) + ": "
195+
last_msg = log[-1] if log else None
196+
if not last_msg or (
197+
(last_msg.role in ["system", "assistant"])
198+
or (log[-1].role == "user" and log[-1].content.startswith("."))
199+
):
200+
inquiry = prompt_user(command)
153201
if command:
154-
print(prompt + command)
155-
inquiry = command
156202
command = None
157203
command_triggered = True
158-
else:
159-
inquiry = input(prompt)
160-
204+
161205
if not inquiry:
206+
print("Continue 1 (rare!)")
162207
continue
163208
logmanager.append(Message("user", inquiry))
164209

165210
assert log[-1].role == "user"
166211
inquiry = log[-1].content
167212
# if message starts with ., treat as command
168-
# when command has been run,
213+
# when command has been run,
169214
if inquiry.startswith("."):
170215
for msg in handle_cmd(inquiry, logmanager):
171216
logmanager.append(msg)
217+
if command:
218+
command_triggered = True
219+
print("Continue 2")
172220
continue
173221

174222
# if large context, try to reduce/summarize
175223
# print response
176-
msg_response = reply(logmanager.prepare_messages())
224+
try:
225+
msg_response = reply(logmanager.prepare_messages(), stream)
226+
227+
# log response and run tools
228+
if msg_response:
229+
for msg in itertools.chain([msg_response], execute_msg(msg_response)):
230+
logmanager.append(msg)
231+
except KeyboardInterrupt:
232+
print("Interrupted")
233+
234+
235+
def prompt_user(value=None) -> str:
236+
return prompt_input(colored("User", role_color["user"]) + ": ", value)
237+
238+
239+
def prompt_input(prompt: str, value=None) -> str:
240+
if value:
241+
print(prompt + value)
242+
else:
243+
value = input(prompt)
244+
return value
245+
177246

178-
# log response and run tools
179-
for msg in itertools.chain([msg_response], execute_msg(msg_response)):
180-
logmanager.append(msg)
247+
def reply(messages: list[Message], stream: bool = False) -> Message:
248+
if stream:
249+
return reply_stream(messages)
250+
else:
251+
prefix = colored("Assistant", "green", attrs=["bold"])
252+
print(f"{prefix}: Thinking...", end="\r")
253+
response = _chat_complete(messages)
254+
print(" " * shutil.get_terminal_size().columns, end="\r")
255+
return Message("assistant", response)
181256

182257

183-
def reply(messages: list[Message]) -> Message:
184-
# print in-progress indicator
185-
print(colored("Assistant", "green", attrs=["bold"]) + ": Thinking...", end="\r")
186-
response = openai.ChatCompletion.create(
258+
def _chat_complete(messages: list[Message]) -> str:
259+
response = openai.ChatCompletion.create( # type: ignore
187260
model="gpt-3.5-turbo",
188261
messages=msgs2dicts(messages),
189262
temperature=0,
190263
)
191-
print(" " * shutil.get_terminal_size().columns, end="\r")
192-
return Message("assistant", response.choices[0].message.content)
264+
return response.choices[0].message.content
265+
266+
267+
def reply_stream(messages: list[Message]) -> Message:
268+
prefix = colored("Assistant", "green", attrs=["bold"])
269+
print(f"{prefix}: Thinking...", end="\r")
270+
response = openai.ChatCompletion.create( # type: ignore
271+
model="gpt-3.5-turbo",
272+
messages=msgs2dicts(messages),
273+
temperature=0,
274+
stream=True,
275+
max_tokens=1000,
276+
)
277+
278+
def deltas_to_str(deltas: list[dict]):
279+
return "".join([d.get("content", "") for d in deltas])
280+
281+
def print_clear():
282+
print(" " * shutil.get_terminal_size().columns, end="\r")
283+
284+
deltas: list[dict] = []
285+
print_clear()
286+
print(f"{prefix}: ", end="")
287+
stop_reason = None
288+
for chunk in response:
289+
delta = chunk["choices"][0]["delta"]
290+
deltas.append(delta)
291+
stop_reason = chunk["choices"][0]["finish_reason"]
292+
print(deltas_to_str([delta]), end="")
293+
# need to flush stdout to get the print to show up
294+
sys.stdout.flush()
295+
print_clear()
296+
verbose = True
297+
if verbose:
298+
print(f" - Stop reason: {stop_reason}")
299+
return Message("assistant", deltas_to_str(deltas))
193300

194301

195302
if __name__ == "__main__":

0 commit comments

Comments
 (0)