Skip to content

Commit

Permalink
switch to GPT4All
Browse files Browse the repository at this point in the history
  • Loading branch information
adbenitez committed Apr 28, 2024
1 parent 7cae6cf commit 07dbe6a
Show file tree
Hide file tree
Showing 14 changed files with 393 additions and 416 deletions.
11 changes: 2 additions & 9 deletions .github/workflows/python-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ name: CI

on:
push:
branches: [ master ]
branches: [ master, main ]
tags:
- 'v*.*.*'
pull_request:
branches: [ master ]
branches: [ master, main ]

jobs:
test:
Expand Down Expand Up @@ -54,10 +54,3 @@ jobs:
pip: true
# only upload if a tag is pushed (otherwise just build & check)
upload: ${{ github.event_name == 'push' && steps.check-tag.outputs.match == 'true' }}
- name: Create GitHub release
if: ${{ github.event_name == 'push' && steps.check-tag.outputs.match == 'true' }}
uses: Roang-zero1/github-create-release-action@master
with:
version_regex: ^v[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5 changes: 0 additions & 5 deletions CHANGELOG.md

This file was deleted.

File renamed without changes.
40 changes: 3 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
# ChatBot

[![Latest Release](https://img.shields.io/pypi/v/deltachat-chatbot.svg)](https://pypi.org/project/deltachat-chatbot)
[![CI](https://github.com/deltachat-bot/chatbot/actions/workflows/python-ci.yml/badge.svg)](https://github.com/deltachat-bot/chatbot/actions/workflows/python-ci.yml)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

Conversational chat-bot for Delta Chat, using OpenAI API.
Conversational chat-bot for Delta Chat, using GPT4All.

## Install

```sh
pip install git+https://github.com/deltachat-bot/chatbot.git
pip install deltachat-chatbot
```

### Installing deltachat-rpc-server

This program depends on a standalone Delta Chat RPC server `deltachat-rpc-server` program that must be
available in your `PATH`. To install it check:
https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-server

## Usage

Configure the bot:
Expand All @@ -25,39 +20,10 @@ Configure the bot:
chatbot init [email protected] PASSWORD
```

Create a configuration file named `config.json` in the program folder, for example:

```
{
"api_key": "sk-...",
"global_monthly_quota": 10000000,
"user_hourly_tokens_quota": 4000,
"user_hourly_queries_quota": 60,
"openai": {
"model": "gpt-3.5-turbo",
"max_tokens": 500,
"messages": [{"role": "system", "content": "You are a helpful assistant inside Delta Chat messenger."}],
"temperature": 0.7
}
}
```

**Note:** On GNU/Linux the program folder is at `~/.config/chatbot/`, when configuring the bot it will
print the path to the accounts folder that is inside the configuration folder.

Start the bot:

```sh
chatbot serve
```

Run `chatbot --help` to see all available options.

To change log level set the enviroment variable `CHATBOT_LOG_LEVEL` to one of:
`debug`, `info`, `warning` or `error`


## Using the bot in groups

To use the bot in groups just add it to a group and send a message starting with @BotName to
mention it or quote-reply a previous message from the the bot.
5 changes: 2 additions & 3 deletions deltachat_chatbot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"""chat bot."""
import asyncio
"""Chat bot."""

from .hooks import cli


def main() -> None:
"""Run the application."""
try:
asyncio.run(cli.start())
cli.start()
except KeyboardInterrupt:
pass
1 change: 1 addition & 0 deletions deltachat_chatbot/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Support for package execution."""

from . import main

main()
207 changes: 207 additions & 0 deletions deltachat_chatbot/gpt4all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
# pylama:ignore=C0114,W0212,R0913,R0914,C0301,R1735,
import ctypes
import warnings
from typing import Any

from gpt4all import GPT4All as _GPT4All
from gpt4all._pyllmodel import (
LLModel,
PromptCallback,
RecalculateCallback,
ResponseCallback,
ResponseCallbackType,
empty_response_callback,
llmodel,
)
from gpt4all.gpt4all import MessageType


def prompt_model(
self,
prompt: str,
prompt_template: str,
callback: ResponseCallbackType,
n_predict: int = 4096,
top_k: int = 40,
top_p: float = 0.9,
min_p: float = 0.0,
temp: float = 0.1,
n_batch: int = 8,
repeat_penalty: float = 1.2,
repeat_last_n: int = 10,
context_erase: float = 0.75,
reset_context: bool = False,
special: bool = False,
fake_reply: str = "",
):
"""
Generate response from model from a prompt.
Parameters
----------
prompt: str
Question, task, or conversation for model to respond to
callback(token_id:int, response:str): bool
The model sends response tokens to callback
Returns
-------
None
"""

if self.model is None:
self._raise_closed()

self.buffer.clear()
self.buff_expecting_cont_bytes = 0

self._set_context(
n_predict=n_predict,
top_k=top_k,
top_p=top_p,
min_p=min_p,
temp=temp,
n_batch=n_batch,
repeat_penalty=repeat_penalty,
repeat_last_n=repeat_last_n,
context_erase=context_erase,
reset_context=reset_context,
)

llmodel.llmodel_prompt(
self.model,
ctypes.c_char_p(prompt.encode()),
ctypes.c_char_p(prompt_template.encode()),
PromptCallback(self._prompt_callback),
ResponseCallback(self._callback_decoder(callback)),
RecalculateCallback(self._recalculate_callback),
self.context,
special,
ctypes.c_char_p(fake_reply.encode()) if fake_reply else ctypes.c_char_p(),
)


LLModel.prompt_model = prompt_model


class GPT4All(_GPT4All):
"""Patch GPT4All to support fake_reply parameter.
See https://github.com/nomic-ai/gpt4all/issues/1959
"""

def generate( # noqa
self,
prompt: str,
*,
max_tokens: int = 200,
temp: float = 0.7,
top_k: int = 40,
top_p: float = 0.4,
min_p: float = 0.0,
repeat_penalty: float = 1.18,
repeat_last_n: int = 64,
n_batch: int = 8,
n_predict: int | None = None,
callback: ResponseCallbackType = empty_response_callback,
fake_reply="",
) -> Any:
"""
Generate outputs from any GPT4All model.
Args:
prompt: The prompt for the model the complete.
max_tokens: The maximum number of tokens to generate.
temp: The model temperature. Larger values increase creativity but decrease factuality.
top_k: Randomly sample from the top_k most likely tokens at each generation step. Set this to 1 for greedy decoding.
top_p: Randomly sample at each generation step from the top most likely tokens whose probabilities add up to top_p.
min_p: Randomly sample at each generation step from the top most likely tokens whose probabilities are at least min_p.
repeat_penalty: Penalize the model for repetition. Higher values result in less repetition.
repeat_last_n: How far in the models generation history to apply the repeat penalty.
n_batch: Number of prompt tokens processed in parallel. Larger values decrease latency but increase resource requirements.
n_predict: Equivalent to max_tokens, exists for backwards compatibility.
callback: A function with arguments token_id:int and response:str, which receives the tokens from the model as they are generated and stops the generation by returning False.
Returns:
Either the entire completion or a generator that yields the completion token by token.
"""

# Preparing the model request
generate_kwargs: dict[str, Any] = dict(
temp=temp,
top_k=top_k,
top_p=top_p,
min_p=min_p,
repeat_penalty=repeat_penalty,
repeat_last_n=repeat_last_n,
n_batch=n_batch,
n_predict=n_predict if n_predict is not None else max_tokens,
fake_reply=fake_reply,
)

if self._history is not None:
# check if there is only one message, i.e. system prompt:
reset = len(self._history) == 1
self._history.append({"role": "user", "content": prompt})

fct_func = self._format_chat_prompt_template.__func__ # type: ignore[attr-defined]
if fct_func is GPT4All._format_chat_prompt_template:
if reset:
# ingest system prompt
# use "%1%2" and not "%1" to avoid implicit whitespace
self.model.prompt_model(
self._history[0]["content"],
"%1%2",
empty_response_callback,
n_batch=n_batch,
n_predict=0,
reset_context=True,
special=True,
)
prompt_template = self._current_prompt_template.format("%1", "%2")
else:
warnings.warn(
"_format_chat_prompt_template is deprecated. Please use a chat session with a prompt template.",
DeprecationWarning,
)
# special tokens won't be processed
prompt = self._format_chat_prompt_template(
self._history[-1:],
self._history[0]["content"] if reset else "",
)
prompt_template = "%1"
generate_kwargs["reset_context"] = reset
else:
prompt_template = "%1"
generate_kwargs["reset_context"] = True

# Prepare the callback, process the model response
output_collector: list[MessageType]
output_collector = [
{"content": ""}
] # placeholder for the self._history if chat session is not activated

if self._history is not None:
self._history.append({"role": "assistant", "content": ""})
output_collector = self._history

def _callback_wrapper(
callback: ResponseCallbackType,
output_collector: list[MessageType],
) -> ResponseCallbackType:
def _callback(token_id: int, response: str) -> bool:
nonlocal callback, output_collector

output_collector[-1]["content"] += response

return callback(token_id, response)

return _callback

self.model.prompt_model( # noqa
prompt,
prompt_template,
_callback_wrapper(callback, output_collector),
**generate_kwargs,
)

return output_collector[-1]["content"]
Loading

0 comments on commit 07dbe6a

Please sign in to comment.