From b939bba50fb90edb09eacffaa9d66df9a29bcb08 Mon Sep 17 00:00:00 2001 From: Jeff Epler Date: Wed, 23 Oct 2024 14:04:06 -0500 Subject: [PATCH] Improve docs & chap --help Incompatible change: @:FILE presets are now in $HOME/.config/chap/preset Incompatible change: -S :FILE prompts are now in $HOME/.config/chap/prompt Incompatible change: -s@FILE is no longer a special case, use -S FILE instead --- README.md | 23 +++++++++------ src/chap/core.py | 76 +++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 0d299e4..ebdab2a 100644 --- a/README.md +++ b/README.md @@ -89,21 +89,21 @@ configure a back-end. This functionality is implemented via `@FILE` arguments. Before any other command-line argument parsing is performed, `@FILE` arguments are expanded: * An `@FILE` argument is searched relative to the current directory - * An `@:FILE` argument is searched relative to the configuration directory (e.g., $HOME/.config/chap) + * An `@:FILE` argument is searched relative to the configuration directory (e.g., $HOME/.config/chap/presets) * If an argument starts with a literal `@`, double it: `@@` * `@.` stops processing any further `@FILE` arguments and leaves them unchanged. The contents of an `@FILE` are parsed according to `shlex.split(comments=True)`. Comments are supported. A typical content might look like this: ``` -# gpt-3.5.txt: Use cheaper gpt 3.5 and custom prompt +# cfg/gpt-4o: Use more expensive gpt 4o and custom prompt --backend openai-chatgpt --B model:gpt-3.5-turbo --s my-custom-system-message.txt +-B model:gpt-4o +-s :my-custom-system-message.txt ``` and you might use it with ``` -chap @:gpt-3.5.txt ask what version of gpt is this +chap @:cfg/gpt-4o ask what version of gpt is this ``` ## Interactive terminal usage @@ -144,21 +144,26 @@ an existing session with `-s`. Or, you can continue the last session with You can set the "system message" with the `-S` flag. You can select the text generating backend with the `-b` flag: - * openai-chatgpt: the default, paid API, best quality results + * openai-chatgpt: the default, paid API, best quality results. Also works with compatible API implementations including llama-cpp when the correct backend URL is specified. * llama-cpp: Works with [llama.cpp's http server](https://github.com/ggerganov/llama.cpp/blob/master/examples/server/README.md) and can run locally with various models, - though it is [optimized for models that use the llama2-style prompting](https://huggingface.co/blog/llama2#how-to-prompt-llama-2). - Set the server URL with `-B url:...`. + though it is [optimized for models that use the llama2-style prompting](https://huggingface.co/blog/llama2#how-to-prompt-llama-2). Consider using llama.cpp's OpenAI compatible API with the openai-chatgpt backend instead, in which case the server can apply the chat template. * textgen: Works with https://github.com/oobabooga/text-generation-webui and can run locally with various models. Needs the server URL in *$configuration_directory/textgen\_url*. + * mistral: Works with the [mistral paid API](https://docs.mistral.ai/). + * anthropic: Works with the [anthropic paid API](https://docs.anthropic.com/en/home). + * huggingface: Works with the [huggingface API](https://huggingface.co/docs/api-inference/index), which includes a free tier. * lorem: local non-AI lorem generator for testing +Backends have settings such as URLs and where API keys are stored. use `chap --backend + --help` to list settings for a particular backend. + ## Environment variables The backend can be set with the `CHAP_BACKEND` environment variable. Backend settings can be set with `CHAP__`, with `backend_name` and `parameter_name` all in caps. -For instance, `CHAP_LLAMA_CPP_URL=http://server.local:8080/completion` changes the default server URL for the llama-cpp back-end. +For instance, `CHAP_LLAMA_CPP_URL=http://server.local:8080/completion` changes the default server URL for the llama-cpp backend. ## Importing from ChatGPT diff --git a/src/chap/core.py b/src/chap/core.py index 67ed9f7..7a4ee2b 100644 --- a/src/chap/core.py +++ b/src/chap/core.py @@ -13,6 +13,7 @@ import pathlib import pkgutil import subprocess import shlex +import textwrap from dataclasses import MISSING, Field, dataclass, fields from typing import ( Any, @@ -20,6 +21,7 @@ from typing import ( Callable, Optional, Union, + IO, cast, get_origin, get_args, @@ -30,7 +32,6 @@ import click import platformdirs from simple_parsing.docstring import get_attribute_docstring from typing_extensions import Protocol - from . import backends, commands from .session import Message, Session, System, session_from_file @@ -187,9 +188,6 @@ def colonstr(arg: str) -> tuple[str, str]: def set_system_message(ctx: click.Context, param: click.Parameter, value: str) -> None: if value is None: return - if value.startswith("@"): - with open(value[1:], "r", encoding="utf-8") as f: - value = f.read().strip() ctx.obj.system_message = value @@ -340,6 +338,14 @@ class Obj: session_filename: Optional[pathlib.Path] = None +def maybe_add_txt_extension(fn: pathlib.Path) -> pathlib.Path: + if not fn.exists(): + fn1 = pathlib.Path(str(fn) + ".txt") + if fn1.exists(): + fn = fn1 + return fn + + def expand_splats(args: list[str]) -> list[str]: result = [] saw_at_dot = False @@ -357,9 +363,10 @@ def expand_splats(args: list[str]) -> list[str]: result.append(a) continue if a.startswith("@:"): - fn: pathlib.Path | str = configuration_path / a[2:] + fn: pathlib.Path = configuration_path / "preset" / a[2:] else: - fn = a[1:] + fn = pathlib.Path(a[1:]) + fn = maybe_add_txt_extension(fn) with open(fn, "r", encoding="utf-8") as f: content = f.read() parts = shlex.split(content) @@ -396,9 +403,44 @@ class MyCLI(click.Group): except ModuleNotFoundError as exc: raise click.UsageError(f"Invalid subcommand {cmd_name!r}", ctx) from exc + def format_splat_options( + self, ctx: click.Context, formatter: click.HelpFormatter + ) -> None: + with formatter.section("Splats"): + formatter.write_text( + "Before any other command-line argument parsing is performed, @FILE arguments are expanded:" + ) + formatter.write_paragraph() + formatter.indent() + formatter.write_dl( + [ + ("@FILE", "Argument is searched relative to the current directory"), + ( + "@:FILE", + "Argument is searched relative to the configuration directory (e.g., $HOME/.config/chap/preset)", + ), + ("@@…", "If an argument starts with a literal '@', double it"), + ( + "@.", + "Stops processing any further `@FILE` arguments and leaves them unchanged.", + ), + ] + ) + formatter.dedent() + formatter.write_paragraph() + formatter.write_text( + textwrap.dedent( + """\ + The contents of an `@FILE` are parsed by `shlex.split(comments=True)`. + Comments are supported. If the filename ends in .txt, + the extension may be omitted.""" + ) + ) + def format_options( self, ctx: click.Context, formatter: click.HelpFormatter ) -> None: + self.format_splat_options(ctx, formatter) super().format_options(ctx, formatter) api = ctx.obj.api or get_api(ctx) if hasattr(api, "parameters"): @@ -433,11 +475,23 @@ class MyCLI(click.Group): class ConfigRelativeFile(click.File): + def __init__(self, mode: str, where: str) -> None: + super().__init__(mode) + self.where = where + def convert( - self, value: Any, param: click.Parameter | None, ctx: click.Context | None + self, + value: str | os.PathLike[str] | IO[Any], + param: click.Parameter | None, + ctx: click.Context | None, ) -> Any: - if isinstance(value, str) and value.startswith(":"): - value = configuration_path / value[1:] + if isinstance(value, str): + if value.startswith(":"): + value = configuration_path / self.where / value[1:] + else: + value = pathlib.Path(value) + if isinstance(value, pathlib.Path): + value = maybe_add_txt_extension(value) return super().convert(value, param, ctx) @@ -453,11 +507,11 @@ main = MyCLI( ), click.Option( ("--system-message-file", "-s"), - type=ConfigRelativeFile("r"), + type=ConfigRelativeFile("r", where="prompt"), default=None, callback=set_system_message_from_file, expose_value=False, - help=f"Set the system message from a file. If the filename starts with `:` it is relative to the configuration path {configuration_path}.", + help=f"Set the system message from a file. If the filename starts with `:` it is relative to the {configuration_path}/prompt. If the filename ends in .txt, the extension may be omitted.", ), click.Option( ("--system-message", "-S"),