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:
|
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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue