Merge pull request #41 from jepler/improve-splat-handling

This commit is contained in:
Jeff Epler 2024-10-25 08:22:48 -05:00 committed by GitHub
commit 69566fddbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 20 deletions

View file

@ -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: 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 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: `@@` * If an argument starts with a literal `@`, double it: `@@`
* `@.` stops processing any further `@FILE` arguments and leaves them unchanged. * `@.` stops processing any further `@FILE` arguments and leaves them unchanged.
The contents of an `@FILE` are parsed according to `shlex.split(comments=True)`. The contents of an `@FILE` are parsed according to `shlex.split(comments=True)`.
Comments are supported. Comments are supported.
A typical content might look like this: 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 --backend openai-chatgpt
-B model:gpt-3.5-turbo -B model:gpt-4o
-s my-custom-system-message.txt -s :my-custom-system-message.txt
``` ```
and you might use it with 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 ## 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 set the "system message" with the `-S` flag.
You can select the text generating backend with the `-b` 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, * 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). 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.
Set the server URL with `-B url:...`.
* textgen: Works with https://github.com/oobabooga/text-generation-webui and can run locally with various models. * 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*. 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 * lorem: local non-AI lorem generator for testing
Backends have settings such as URLs and where API keys are stored. use `chap --backend
<BACKEND> --help` to list settings for a particular backend.
## Environment variables ## Environment variables
The backend can be set with the `CHAP_BACKEND` environment variable. The backend can be set with the `CHAP_BACKEND` environment variable.
Backend settings can be set with `CHAP_<backend_name>_<parameter_name>`, with `backend_name` and `parameter_name` all in caps. Backend settings can be set with `CHAP_<backend_name>_<parameter_name>`, 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 ## Importing from ChatGPT

View file

@ -13,6 +13,7 @@ import pathlib
import pkgutil import pkgutil
import subprocess import subprocess
import shlex import shlex
import textwrap
from dataclasses import MISSING, Field, dataclass, fields from dataclasses import MISSING, Field, dataclass, fields
from typing import ( from typing import (
Any, Any,
@ -20,6 +21,7 @@ from typing import (
Callable, Callable,
Optional, Optional,
Union, Union,
IO,
cast, cast,
get_origin, get_origin,
get_args, get_args,
@ -30,7 +32,6 @@ import click
import platformdirs import platformdirs
from simple_parsing.docstring import get_attribute_docstring from simple_parsing.docstring import get_attribute_docstring
from typing_extensions import Protocol from typing_extensions import Protocol
from . import backends, commands from . import backends, commands
from .session import Message, Session, System, session_from_file 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: def set_system_message(ctx: click.Context, param: click.Parameter, value: str) -> None:
if value is None: if value is None:
return return
if value.startswith("@"):
with open(value[1:], "r", encoding="utf-8") as f:
value = f.read().strip()
ctx.obj.system_message = value ctx.obj.system_message = value
@ -340,6 +338,14 @@ class Obj:
session_filename: Optional[pathlib.Path] = None 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]: def expand_splats(args: list[str]) -> list[str]:
result = [] result = []
saw_at_dot = False saw_at_dot = False
@ -357,9 +363,10 @@ def expand_splats(args: list[str]) -> list[str]:
result.append(a) result.append(a)
continue continue
if a.startswith("@:"): if a.startswith("@:"):
fn: pathlib.Path | str = configuration_path / a[2:] fn: pathlib.Path = configuration_path / "preset" / a[2:]
else: else:
fn = a[1:] fn = pathlib.Path(a[1:])
fn = maybe_add_txt_extension(fn)
with open(fn, "r", encoding="utf-8") as f: with open(fn, "r", encoding="utf-8") as f:
content = f.read() content = f.read()
parts = shlex.split(content) parts = shlex.split(content)
@ -396,9 +403,44 @@ class MyCLI(click.Group):
except ModuleNotFoundError as exc: except ModuleNotFoundError as exc:
raise click.UsageError(f"Invalid subcommand {cmd_name!r}", ctx) from 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( def format_options(
self, ctx: click.Context, formatter: click.HelpFormatter self, ctx: click.Context, formatter: click.HelpFormatter
) -> None: ) -> None:
self.format_splat_options(ctx, formatter)
super().format_options(ctx, formatter) super().format_options(ctx, formatter)
api = ctx.obj.api or get_api(ctx) api = ctx.obj.api or get_api(ctx)
if hasattr(api, "parameters"): if hasattr(api, "parameters"):
@ -433,11 +475,23 @@ class MyCLI(click.Group):
class ConfigRelativeFile(click.File): class ConfigRelativeFile(click.File):
def __init__(self, mode: str, where: str) -> None:
super().__init__(mode)
self.where = where
def convert( 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: ) -> Any:
if isinstance(value, str) and value.startswith(":"): if isinstance(value, str):
value = configuration_path / value[1:] 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) return super().convert(value, param, ctx)
@ -453,11 +507,11 @@ main = MyCLI(
), ),
click.Option( click.Option(
("--system-message-file", "-s"), ("--system-message-file", "-s"),
type=ConfigRelativeFile("r"), type=ConfigRelativeFile("r", where="prompt"),
default=None, default=None,
callback=set_system_message_from_file, callback=set_system_message_from_file,
expose_value=False, 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( click.Option(
("--system-message", "-S"), ("--system-message", "-S"),

View file

@ -44,6 +44,8 @@ if USE_PASSWORD_STORE.exists():
@functools.cache @functools.cache
def get_key(name: str, what: str = "api key") -> str: def get_key(name: str, what: str = "api key") -> str:
if name == "-":
return "-"
key_path = f"{pass_prefix}{name}" key_path = f"{pass_prefix}{name}"
command = pass_command + [key_path] command = pass_command + [key_path]
return subprocess.check_output(command, encoding="utf-8").split("\n")[0] return subprocess.check_output(command, encoding="utf-8").split("\n")[0]