Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,16 @@ poetry install
poetry run python ./xoxo/main.py --user_name YOUR_NAME
```

## Run with docker-compose
1. git clone the project
2. copy .env.example from docker to the root of the project and name it .env
3. put your values into the respective fields in .env file
4. run the project from the root of the file using `docker-compose -f docker/docker-compose.yml build && docker-compose -f docker/docker-compose.yml run xoxo`

## Ideas for near-term development:
- flushing conversation history when the max context length is close to being overflown
- add the local memory ~ cashing and retrieving information from previous conversations
- ui
- support other LLMs besides gpt

Contributions welcome !
Contributions welcome !
3 changes: 3 additions & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
BING_SUBSCRIPTION_KEY=enter_your_azure_key
OPENAI_API_KEY=enter_your_openai_key

30 changes: 30 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
FROM python:3.9-bullseye as pybase

WORKDIR /app

ARG LANDSCAPE \
UID=1000 \
GID=1000

RUN groupadd -g "${GID}" -r xoxo \
&& useradd -d '/app' -g xoxo -l -r -u "${UID}" xoxo \
&& chown xoxo:xoxo -R '/app'


RUN pip install poetry

COPY ../poetry.lock ../pyproject.toml ./

RUN poetry config virtualenvs.create false \
&& poetry install $(test "$LANDSCAPE" == production && echo "--no-dev") --no-interaction --no-ansi

COPY --chown=xoxo:xoxo ./docker/entrypoint.sh .
COPY --chown=xoxo:xoxo ./xoxo ./xoxo

ENV PYTHONPATH /app
RUN chmod +x './entrypoint.sh'

USER xoxo

ENTRYPOINT ./entrypoint.sh

14 changes: 14 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
version: "3.9"
services:

xoxo:
build:
context: ../
dockerfile: ./docker/Dockerfile
image: xoxo:latest
entrypoint: bash ./entrypoint.sh
env_file:
- ../.env
stdin_open: true
tty: true

2 changes: 2 additions & 0 deletions docker/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
python3 xoxo/main.py

55 changes: 52 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ license = "MIT"
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
python = "^3.8"
openai = "^0.27.2"
colorama = "^0.4.6"
asteval = "^0.9.29"
duckduckgo-search = "^2.8.5"


[build-system]
Expand Down
2 changes: 0 additions & 2 deletions xoxo/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
from xoxo.utils import Message, SearchResult
from xoxo.retriever import Retriever
22 changes: 22 additions & 0 deletions xoxo/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os

XOXO_SUMMARY_PROMPT = """
You are given a query and retrieved search results. Summarize the given search results keeping the user request in mind.
If you enumerate things, list them from bullets in new lines with urls in parethesies so that the user could click on them.

RESULTS:
{results}

QUERY: {query}

ANSWER:

"""



BING_SUBSCRIPTION_KEY = os.environ["BING_SUBSCRIPTION_KEY"]
BING_ENDPOINT_URL = "https://api.bing.microsoft.com/v7.0/search"

OPENAI_API_KEY = os.environ["OPENAI_API_KEY"]

92 changes: 52 additions & 40 deletions xoxo/main.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,31 @@
from typing import List
import sys
import os
import re
import argparse
import logging

import colorama
import openai
import asteval

from colorama import Fore, Style
from datetime import datetime
import openai
from xoxo import Retriever, Message
from typing import List

colorama.init()
from xoxo import retriever, models, config, search

openai.api_key = os.environ["OPENAI_API_KEY"]
logging.basicConfig(level=logging.DEBUG, encoding='utf-8')
logger = logging.getLogger("main")

colorama.init()

# https://stackoverflow.com/questions/2371436/evaluating-a-mathematical-expression-in-a-string
_re_simple_eval = re.compile(rb"d([\x00-\xFF]+)S\x00")

def calendar() -> str:
""" Returns a string representing current date and time """
now = datetime.now() # current date and time
date_time_str = now.strftime("today's date: %A %d.%m.%Y, time right now: %I:%M %p")
return date_time_str

def simple_math_eval(expr: str) -> str:
""" Attempts to evaluate the math expression passed as string """
try:
c = compile(expr, "userinput", "eval")
except SyntaxError:
raise ValueError(f"Malformed expression: {expr}")
m = _re_simple_eval.fullmatch(c.co_code)
if not m:
raise ValueError(f"Not a simple algebraic expression: {expr}")
try:
return c.co_consts[int.from_bytes(m.group(1), sys.byteorder)]
except IndexError:
raise ValueError(f"Expression not evaluated as constant: {expr}")

xoxo_prompt = """
You are the brain of general purpose assistant, named xoxo.ai. Your goal is to have a nice conversation with the user.
Secretly, during the conversations with the user, we will develop your character ~ to speed up the process, be extroverted, answer the questions about yourself and express your preference.
Expand Down Expand Up @@ -106,7 +96,7 @@ def get_xoxo_command(history: str) -> str:

return response.choices[0].text

def buffer_2_string(buffer: List[Message]) -> str:
def buffer_2_string(buffer: List[models.Message]) -> str:
output_string = ""
for msg in buffer:
if msg.author == "USER":
Expand All @@ -121,22 +111,37 @@ def buffer_2_string(buffer: List[Message]) -> str:

if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--user_name", type=str, required=True)
parser.add_argument("--user_name", type=str, required=False, default="noname")
args = parser.parse_args()

buffer = []
introduction_str = f"Hi {args.user_name}! 🤗 How can I help you today?"

buffer.append(
Message("XOXO", introduction_str.replace(f"{args.user_name}!", f"{args.user_name} (user name)"))
models.Message("XOXO", introduction_str.replace(f"{args.user_name}!", f"{args.user_name} (user name)"))
)
print(format_xoxo_msg(introduction_str))

user_input = input(format_user_msg(""))
buffer.append(Message("USER", user_input))

openai_api_key = config.OPENAI_API_KEY
bing_api_key = config.BING_SUBSCRIPTION_KEY
summary_prompt = config.XOXO_SUMMARY_PROMPT

bing_search = search.BingSearchService(bing_api_key=bing_api_key)
ddg_search = search.DuckDuckGoSearchService()

retriever_instance = retriever.Retriever(
search_service=ddg_search,
openai_api_key=openai_api_key,
summary_prompt=summary_prompt,
k=3
)
a = asteval.Interpreter()

logger.debug(buffer)
try:
while True:
while user_input:=input(format_user_msg("")):
buffer.append(models.Message("USER", user_input))
buffer_string = buffer_2_string(buffer)
xoxo_command = get_xoxo_command(buffer_string)

Expand All @@ -145,58 +150,65 @@ def buffer_2_string(buffer: List[Message]) -> str:
if cmd[-1] == ":":
cmd = cmd[:-1]

logger.debug(buffer)
logger.debug(cmd)
if cmd == "MESSAGE":
buffer.append(Message("XOXO", msg))
buffer.append(models.Message("XOXO", msg))
print(format_xoxo_msg(msg))

user_input = input(format_user_msg(""))
buffer.append(Message("USER", user_input))
buffer.append(models.Message("USER", user_input))
continue

elif cmd == "THINK":
buffer.append(Message(cmd, msg))
buffer.append(models.Message(cmd, msg))
print(format_xoxo_state(cmd.lower() + ": " + msg))
continue

elif cmd == "SEARCH":
buffer.append(Message(cmd, msg))
buffer.append(models.Message(cmd, msg))
print(format_xoxo_state(cmd.lower() + ": " + msg))
r = Retriever()
boring_response = r.trigger(msg)
boring_response = retriever_instance.trigger(msg)

buffer.append(boring_response)

if boring_response.author == "XOXO":
print(format_xoxo_msg(boring_response.content))

user_input = input(format_user_msg(""))
buffer.append(Message("USER", user_input))
buffer.append(models.Message("USER", user_input))
else:
print(Retriever.format_boring_msg(boring_response.content))
print(retriever.Retriever.format_boring_msg(boring_response.content))
continue

elif cmd == "CALENDAR":
buffer.append(Message(cmd, msg))
buffer.append(models.Message(cmd, msg))
print(format_xoxo_state(cmd.lower() + ": " + msg))

out = calendar()

buffer.append(Message("RESULT", out))
buffer.append(models.Message("RESULT", out))
print(format_xoxo_state("result" + ": " + out))
continue

elif cmd == "CALCULATE":
buffer.append(Message(cmd, msg))
buffer.append(models.Message(cmd, msg))
print(format_xoxo_state(cmd.lower() + ": " + msg))
try:
out = str(simple_math_eval(msg))
out = str(a.eval(msg))
except:
out = f"failed to calculate {msg}"
print(" >> " + out)

buffer.append(Message("RESULT", out))
buffer.append(models.Message("RESULT", out))
print(format_xoxo_state("result" + ": " + out))
continue

else:
print(f" >> command `{cmd}` is unknown")
continue

except KeyboardInterrupt:
print("\n" + format_xoxo_msg("Goodbye!"))
exit(0)

4 changes: 3 additions & 1 deletion xoxo/utils.py → xoxo/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ class Message:
author: str
content: str


@dataclass
class SearchResult:
name: str
url: str
snippet: str

def get_passage(self) -> str:
return self.url + "\n" + self.snippet
return self.url + "\n" + self.snippet

Loading