Skip to content

Commit

Permalink
Merge pull request #30 from jepler/multiline2
Browse files Browse the repository at this point in the history
Switch chap tui to a multiline text field
  • Loading branch information
jepler authored Dec 12, 2023
2 parents 59c0ae7 + 86059a5 commit 9f6ace3
Show file tree
Hide file tree
Showing 21 changed files with 205 additions and 144 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ __pycache__
/dist
/src/chap/__version__.py
/venv
/keys.log
29 changes: 11 additions & 18 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,29 @@ default_language_version:
python: python3

repos:
- repo: https://github.com/psf/black
rev: 23.1.0
hooks:
- id: black
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.5.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
exclude: tests
- id: trailing-whitespace
exclude: tests
- repo: https://github.com/codespell-project/codespell
rev: v2.2.4
rev: v2.2.6
hooks:
- id: codespell
args: [-w]
- repo: https://github.com/fsfe/reuse-tool
rev: v1.1.2
rev: v2.1.0
hooks:
- id: reuse
- repo: https://github.com/pycqa/isort
rev: 5.12.0
hooks:
- id: isort
name: isort (python)
args: ['--profile', 'black']
- repo: https://github.com/pycqa/pylint
rev: v2.17.0
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.6
hooks:
- id: pylint
additional_dependencies: [click,httpx,lorem-text,simple-parsing,'textual>=0.18.0',tiktoken,websockets]
args: ['--source-roots', 'src']
# Run the linter.
- id: ruff
args: [ --fix, --preview ]
# Run the formatter.
- id: ruff-format
12 changes: 0 additions & 12 deletions .pylintrc

This file was deleted.

27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ SPDX-License-Identifier: MIT

# chap - A Python interface to chatgpt and other LLMs, including a terminal user interface (tui)

![Chap screencast](https://github.com/jepler/chap/blob/main/chap.gif)
![Chap screencast](https://raw.githubusercontent.com/jepler/chap/main/chap.gif)

## System requirements

Chap is developed on Linux with Python 3.11. Due to use of the `X | Y` style of type hints, it is known to not work on Python 3.9 and older. The target minimum Python version is 3.11 (debian stable).
Chap is primarily developed on Linux with Python 3.11. Moderate effort will be made to support versions back to Python 3.9 (Debian oldstable).

## Installation

Expand Down Expand Up @@ -82,7 +82,28 @@ Put your OpenAI API key in the platform configuration directory for chap, e.g.,
* `chap grep needle`

## Interactive terminal usage
* `chap tui`
The interactive terminal mode is accessed via `chap tui`.

There are a variety of keyboard shortcuts to be aware of:
* tab/shift-tab to move between the entry field and the conversation, or between conversation items
* While in the text box, F9 or (if supported by your terminal) alt+enter to submit multiline text
* while on a conversation item:
* ctrl+x to re-draft the message. This
* saves a copy of the session in an auto-named file in the conversations folder
* removes the conversation from this message to the end
* puts the user's message in the text box to edit
* ctrl+x to re-submit the message. This
* saves a copy of the session in an auto-named file in the conversations folder
* removes the conversation from this message to the end
* puts the user's message in the text box
* and submits it immediately
* ctrl+y to yank the message. This places the response part of the current
interaction in the operating system clipboard to be pasted (e..g, with
ctrl+v or command+v in other software)
* ctrl+q to toggle whether this message may be included in the contextual history for a future query.
The exact way history is submitted is determined by the back-end, often by
counting messages or tokens, but the ctrl+q toggle ensures this message (both the user
and assistant message parts) are not considered.

## Sessions & Command-line Parameters

Expand Down
1 change: 0 additions & 1 deletion chap-dev.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
sys.path.insert(0, str(src_path))

if __name__ == "__main__":
# pylint: disable=import-error,no-name-in-module
from chap.core import main

main()
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ name="chap"
authors = [{name = "Jeff Epler", email = "[email protected]"}]
description = "Interact with the OpenAI ChatGPT API (and other text generators)"
dynamic = ["readme","version","dependencies"]
requires-python = ">=3.10"
requires-python = ">=3.9"
keywords = ["llm", "tui", "chatgpt"]
classifiers = [
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ click
httpx
lorem-text
platformdirs
pyperclip
simple_parsing
textual>=0.18.0
textual[syntax]
tiktoken
websockets
4 changes: 1 addition & 3 deletions src/chap/backends/huggingface.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ async def aask(
*,
max_query_size: int = 5,
timeout: float = 180,
) -> AsyncGenerator[
str, None
]: # pylint: disable=unused-argument,too-many-locals,too-many-branches
) -> AsyncGenerator[str, None]:
new_content: list[str] = []
inputs = self.make_full_query(session + [User(query)], max_query_size)
try:
Expand Down
4 changes: 1 addition & 3 deletions src/chap/backends/llama_cpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ async def aask(
*,
max_query_size: int = 5,
timeout: float = 180,
) -> AsyncGenerator[
str, None
]: # pylint: disable=unused-argument,too-many-locals,too-many-branches
) -> AsyncGenerator[str, None]:
params = {
"prompt": self.make_full_query(session + [User(query)], max_query_size),
"stream": True,
Expand Down
4 changes: 2 additions & 2 deletions src/chap/backends/lorem.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ async def aask(
session: Session,
query: str,
) -> AsyncGenerator[str, None]:
data = self.ask(session, query)[-1]
data = self.ask(session, query)
for word, opt_sep in ipartition(data):
yield word + opt_sep
await asyncio.sleep(
Expand All @@ -56,7 +56,7 @@ def ask(
self,
session: Session,
query: str,
) -> str: # pylint: disable=unused-argument
) -> str:
new_content = cast(
str,
lorem.paragraphs(
Expand Down
2 changes: 1 addition & 1 deletion src/chap/backends/openai_chatgpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def ask(self, session: Session, query: str, *, timeout: float = 60) -> str:
json={
"model": self.parameters.model,
"messages": session_to_list(full_prompt),
}, # pylint: disable=no-member
},
headers={
"Authorization": f"Bearer {self.get_key()}",
},
Expand Down
13 changes: 6 additions & 7 deletions src/chap/backends/textgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self) -> None:
AI: Hello! How can I assist you today?"""

async def aask( # pylint: disable=unused-argument,too-many-locals,too-many-branches
async def aask(
self,
session: Session,
query: str,
Expand Down Expand Up @@ -60,12 +60,11 @@ async def aask( # pylint: disable=unused-argument,too-many-locals,too-many-bran
}
full_prompt = session + [User(query)]
del full_prompt[1:-max_query_size]
new_data = old_data = full_query = (
"\n".join(f"{role_map.get(q.role,'')}{q.content}\n" for q in full_prompt)
+ f"\n{role_map.get('assistant')}"
)
new_data = old_data = full_query = "\n".join(
f"{role_map.get(q.role,'')}{q.content}\n" for q in full_prompt
) + f"\n{role_map.get('assistant')}"
try:
async with websockets.connect( # pylint: disable=no-member
async with websockets.connect(
f"ws://{self.parameters.server_hostname}:7860/queue/join"
) as websocket:
while content := json.loads(await websocket.recv()):
Expand Down Expand Up @@ -127,7 +126,7 @@ async def aask( # pylint: disable=unused-argument,too-many-locals,too-many-bran
# stop generation by closing the websocket here
if content["msg"] == "process_completed":
break
except Exception as e: # pylint: disable=broad-exception-caught
except Exception as e:
content = f"\nException: {e!r}"
new_data += content
yield content
Expand Down
6 changes: 3 additions & 3 deletions src/chap/commands/ask.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import asyncio
import sys
from typing import Iterable, Protocol
from typing import Iterable, Optional, Protocol

import click
import rich
Expand Down Expand Up @@ -40,7 +40,7 @@ def add(self, s: str) -> None:


class WrappingPrinter:
def __init__(self, width: int | None = None) -> None:
def __init__(self, width: Optional[int] = None) -> None:
self._width = width or rich.get_console().width
self._column = 0
self._line = ""
Expand Down Expand Up @@ -122,4 +122,4 @@ def main(obj: Obj, prompt: str, print_prompt: bool) -> None:


if __name__ == "__main__":
main() # pylint: disable=no-value-for-parameter
main()
2 changes: 1 addition & 1 deletion src/chap/commands/cat.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ def main(obj: Obj, no_system: bool) -> None:


if __name__ == "__main__":
main() # pylint: disable=no-value-for-parameter
main()
6 changes: 3 additions & 3 deletions src/chap/commands/grep.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ def list_files_matching_rx(
"*.json"
):
try:
session = session_from_file(conversation) # pylint: disable=no-member
except Exception as e: # pylint: disable=broad-exception-caught
session = session_from_file(conversation)
except Exception as e:
print(f"Failed to read {conversation}: {e}", file=sys.stderr)
continue

Expand Down Expand Up @@ -67,4 +67,4 @@ def main(


if __name__ == "__main__":
main() # pylint: disable=no-value-for-parameter
main()
2 changes: 1 addition & 1 deletion src/chap/commands/import.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ def main(output_directory: pathlib.Path, files: list[TextIO]) -> None:


if __name__ == "__main__":
main() # pylint: disable=no-value-for-parameter
main()
2 changes: 1 addition & 1 deletion src/chap/commands/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ def main(obj: Obj, no_system: bool) -> None:


if __name__ == "__main__":
main() # pylint: disable=no-value-for-parameter
main()
2 changes: 2 additions & 0 deletions src/chap/commands/tui.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@ Input {
Markdown {
margin: 0 1 0 0;
}

SubmittableTextArea { height: 3 }
Loading

0 comments on commit 9f6ace3

Please sign in to comment.