Skip to content

Commit

Permalink
Add timestamps to messages
Browse files Browse the repository at this point in the history
  • Loading branch information
paulovcmedeiros committed Nov 13, 2023
2 parents fc8138c + baec738 commit ecb51b1
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 31 deletions.
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,25 @@

A simple chatbot that uses the OpenAI API to get responses from [GPT LLMs](https://platform.openai.com/docs/models) via OpenAI API. Written in Python with a Web UI made with [Streamlit](https://streamlit.io). Can also be used directly from the terminal.

See also the [demo app on Streamlit](https://pyrobbot.streamlit.app).
**See and try the [demo app on Streamlit](https://pyrobbot.streamlit.app)!**

## Features
- [x] Web UI
- Add/remove conversations dynamically
- Automatic/editable conversation summary title
- [x] Fully configurable
- Support for multiple GPT LLMs
- Control over the parameters passed to the OpenAI API, with (hopefully) sensible defaults
- Ability o modify the chat parameters in the same conversation
- Each conversation has its own parameters
- [x] Autosave and retrieve chat history
- Ability to pass base directives to the LLM
- E.g., to make it adopt a persona, but you decide which directived to pass
- Dynamically modifiable AI parameters in each chat separately
- No need to restart the chat
- [x] Autosave & retrieve chat history
- [x] Chat context handling using [embeddings](https://platform.openai.com/docs/guides/embeddings)
- [x] Kepp track of estimated token usage and associated API call costs
- [x] Terminal UI
- [x] Estimated API token usage and associated costs
- [x] Terminal UI (for a more "Wake up, Neo" experience")
- [x] OpenAI API key is **never** stored on disk



## System Requirements
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[tool.poetry]
authors = ["Paulo V C Medeiros <[email protected]>"]
description = "Simple GPT chatbot using OpenAI API"
description = "GPT chatbot using OpenAI API"
license = "MIT"
name = "pyrobbot"
readme = "README.md"
version = "0.1.5"
version = "0.1.6"

[build-system]
build-backend = "poetry.core.masonry.api"
Expand Down
13 changes: 12 additions & 1 deletion pyrobbot/app/app_page_templates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Utilities for creating pages in a streamlit app."""
import contextlib
import datetime
import sys
import uuid
from abc import ABC, abstractmethod
Expand Down Expand Up @@ -171,6 +172,8 @@ def render_chat_history(self):
if role == "system":
continue
with st.chat_message(role, avatar=self.avatars.get(role)):
with contextlib.suppress(KeyError):
st.caption(message["timestamp"])
st.markdown(message["content"])

def render_cost_estimate_page(self):
Expand Down Expand Up @@ -217,11 +220,18 @@ def _render_chatbot_page(self):
placeholder=placeholder,
on_submit=lambda: self.state.update({"chat_started": True}),
):
time_now = datetime.datetime.now().replace(microsecond=0)
# Display user message in chat message container
with st.chat_message("user", avatar=self.avatars["user"]):
st.caption(time_now)
st.markdown(prompt)
self.chat_history.append(
{"role": "user", "name": self.chat_obj.username, "content": prompt}
{
"role": "user",
"name": self.chat_obj.username,
"content": prompt,
"timestamp": time_now,
}
)

# Display (stream) assistant response in chat message container
Expand All @@ -237,6 +247,7 @@ def _render_chatbot_page(self):
except CannotConnectToApiError:
full_response = self.chat_obj.api_connection_error_msg
finally:
st.caption(datetime.datetime.now().replace(microsecond=0))
st.markdown(full_response)

self.chat_history.append(
Expand Down
51 changes: 31 additions & 20 deletions pyrobbot/app/multipage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Code for the creation streamlit apps with dynamically created pages."""
import contextlib
import datetime
from abc import ABC, abstractmethod, abstractproperty

import openai
Expand Down Expand Up @@ -196,9 +197,7 @@ def handle_ui_page_selection(self):
self._build_sidebar_tabs()

with self.sidebar_tabs["settings"]:
caption = f"\u2699\uFE0F Settings for Chat #{self.selected_page.page_number}"
if self.selected_page.title != self.selected_page.fallback_page_title:
caption += f": {self.selected_page.title}"
caption = f"\u2699\uFE0F {self.selected_page.title}"
st.caption(caption)
current_chat_configs = self.selected_page.chat_obj.configs

Expand All @@ -224,8 +223,14 @@ def render(self, **kwargs):
_left_col, centre_col, _right_col = st.columns([0.33, 0.34, 0.33])
with centre_col:
st.title(GeneralConstants.APP_NAME)
st.image(_ASSISTANT_AVATAR_IMAGE, use_column_width=True)
st.subheader(GeneralConstants.PACKAGE_DESCRIPTION, divider="rainbow")
with contextlib.suppress(AttributeError, ValueError, OSError):
# st image raises some exceptions occasionally
st.image(_ASSISTANT_AVATAR_IMAGE, use_column_width=True)
st.subheader(
GeneralConstants.PACKAGE_DESCRIPTION,
divider="rainbow",
help="https://github.com/paulovcmedeiros/pyRobBot",
)
self.init_chat_credentials()
# Create a sidebar with tabs for chats and settings
tab1, tab2 = st.tabs(["Chats", "Settings for Current Chat"])
Expand Down Expand Up @@ -286,8 +291,18 @@ def set_page_title(page):

with self.sidebar_tabs["chats"]:
for page in self.pages.values():
col1, col2, col3 = st.columns([0.7, 0.15, 0.15])
col1, col2, col3 = st.columns([0.1, 0.8, 0.1])
with col1:
st.button(
":wastebasket:",
key=f"delete_{page.page_id}",
type="primary",
use_container_width=True,
on_click=self.remove_page,
kwargs={"page": page},
help="Delete this chat",
)
with col2:
if page.state.get("edit_chat_text"):
st.text_input(
"Edit Chat Title",
Expand All @@ -297,15 +312,21 @@ def set_page_title(page):
args=[page],
)
else:
mtime = None
with contextlib.suppress(FileNotFoundError):
mtime = page.chat_obj.context_file_path.stat().st_mtime
mtime = datetime.datetime.fromtimestamp(mtime)
mtime = mtime.replace(microsecond=0)
st.button(
label=page.sidebar_title,
key=f"select_{page.page_id}",
help=f"Latest backup: {mtime}" if mtime else None,
on_click=self.register_selected_page,
kwargs={"page": page},
use_container_width=True,
disabled=page.page_id == self.selected_page.page_id,
)
with col2:
with col3:
st.button(
":pencil:",
key=f"edit_{page.page_id}_button",
Expand All @@ -314,16 +335,6 @@ def set_page_title(page):
args=[page],
help="Edit chat title",
)
with col3:
st.button(
":wastebasket:",
key=f"delete_{page.page_id}",
type="primary",
use_container_width=True,
on_click=self.remove_page,
kwargs={"page": page},
help="Delete this chat.",
)

def _handle_chat_configs_value_selection(self, current_chat_configs, model_fields):
updates_to_chat_configs = {}
Expand Down Expand Up @@ -428,13 +439,13 @@ def _set_button_style():
<style>
.stButton button[kind="primary"] {
background-color: white;
border-color: #f63366;
border-width: 2px;
opacity: 0;
opacity: 0.5;
transition: opacity 0.3s;
}
.stButton button[kind="primary"]:hover {
opacity: 1;
border-color: #f63366;
border-width: 2px;
}
.stButton button[kind="secondary"]:disabled {
border-color: #2BB5E8;
Expand Down
23 changes: 21 additions & 2 deletions pyrobbot/chat_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import ast
import itertools
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from typing import TYPE_CHECKING

import numpy as np
Expand Down Expand Up @@ -46,8 +47,22 @@ def add_to_history(self, msg_list: list[dict]):

def load_history(self) -> list[dict]:
"""Load the chat history."""
messages_df = self.database.get_messages_dataframe()
selected_columns = ["timestamp", "message_exchange"]
messages_df = self.database.get_messages_dataframe()[selected_columns]

# Convert unix timestamps to datetime objs at the local timezone
messages_df["timestamp"] = messages_df["timestamp"].apply(
lambda ts: datetime.fromtimestamp(ts)
.replace(microsecond=0, tzinfo=timezone.utc)
.astimezone(tz=None)
.replace(tzinfo=None)
)

msg_exchanges = messages_df["message_exchange"].apply(ast.literal_eval).tolist()
# Add timestamps to messages
for i_msg_exchange, timestamp in enumerate(messages_df["timestamp"]):
msg_exchanges[i_msg_exchange][0]["timestamp"] = timestamp

return list(itertools.chain.from_iterable(msg_exchanges))

def get_context(self, msg: dict):
Expand Down Expand Up @@ -76,7 +91,11 @@ def request_embedding(self, msg_list: list[dict]): # noqa: ARG002

def select_relevant_history(self, msg: dict): # noqa: ARG002
"""Select chat history msgs to use as context for `msg`."""
return self.load_history()
history = []
for history_msg in self.load_history():
history_msg.pop("timestamp", None)
history.append(history_msg)
return history


class EmbeddingBasedChatContext(ChatContext):
Expand Down

0 comments on commit ecb51b1

Please sign in to comment.