Merge pull request #41 from jepler/improve-splat-handling
This commit is contained in:
commit
69566fddbe
3 changed files with 81 additions and 20 deletions
23
README.md
23
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
|
||||
<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_<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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ if USE_PASSWORD_STORE.exists():
|
|||
|
||||
@functools.cache
|
||||
def get_key(name: str, what: str = "api key") -> str:
|
||||
if name == "-":
|
||||
return "-"
|
||||
key_path = f"{pass_prefix}{name}"
|
||||
command = pass_command + [key_path]
|
||||
return subprocess.check_output(command, encoding="utf-8").split("\n")[0]
|
||||
|
|
|
|||
Loading…
Reference in a new issue