Skip to content

Commit

Permalink
Merge pull request #41 from jepler/improve-splat-handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jepler authored Oct 25, 2024
2 parents 993b178 + 9340d35 commit 69566fd
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 20 deletions.
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
76 changes: 65 additions & 11 deletions src/chap/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
import pkgutil
import subprocess
import shlex
import textwrap
from dataclasses import MISSING, Field, dataclass, fields
from typing import (
Any,
AsyncGenerator,
Callable,
Optional,
Union,
IO,
cast,
get_origin,
get_args,
Expand All @@ -30,7 +32,6 @@
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

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -396,9 +403,44 @@ def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command:
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"):
Expand Down Expand Up @@ -433,11 +475,23 @@ def main(


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)


Expand All @@ -453,11 +507,11 @@ def convert(
),
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"),
Expand Down
2 changes: 2 additions & 0 deletions src/chap/key.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ class NoKeyAvailable(Exception):

@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]
Expand Down

0 comments on commit 69566fd

Please sign in to comment.