Merge branch 'LAION-AI:main' into messageNavigation

This commit is contained in:
jojopirker
2023-01-07 08:12:18 +01:00
committed by GitHub
79 changed files with 9856 additions and 948 deletions
+4
View File
@@ -8,6 +8,10 @@ devcontainer (either
or in your browser via
[GitHub Codespaces](https://github.com/features/codespaces)).
**Note**: If you want to chose a specific .devcontainer within GitHub codespaces
select "New with options" and you will be able to select any of the pre-defined
devcontainers in this repo.
### Run pre-commit
```bash
+15
View File
@@ -0,0 +1,15 @@
{
"name": "Open-Assistant",
"image": "mcr.microsoft.com/vscode/devcontainers/universal",
"features": {
"ghcr.io/devcontainers-contrib/features/pre-commit:2": {
"version": "latest"
}
},
"postCreateCommand": "bash .devcontainer/frontend/post_create_command.sh",
"customizations": {
"vscode": {
"extensions": ["GitHub.copilot"]
}
}
}
@@ -0,0 +1,7 @@
# ensure pre-commit is installed
pre-commit install
# npm install in /website
cd website
npm install
cd ..
+5
View File
@@ -1,2 +1,7 @@
# ensure pre-commit is installed
pre-commit install
# run yarn install in docs folder
cd docs
yarn install
cd ..
+53
View File
@@ -0,0 +1,53 @@
name: Deploy docs to gitHub pages
on:
push:
branches:
- main
- docs-site-poc
paths:
- ".github/workflows/deploy-docs-site.yaml"
- "docs/**"
pull_request:
jobs:
deploy:
runs-on: ubuntu-22.04
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: "16"
- name: Get yarn cache
id: yarn-cache
run: echo "YARN_CACHE_DIR=$(yarn cache dir)" >> "${GITHUB_OUTPUT}"
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ${{ steps.yarn-cache.outputs.YARN_CACHE_DIR }}
key: ${{ runner.os }}-docs-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-docs-
- run: yarn install --frozen-lockfile
- run: yarn build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if:
${{ github.ref == 'refs/heads/main' || github.ref ==
'refs/heads/docs-site-poc' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs/build
+5 -5
View File
@@ -5,11 +5,11 @@
<div align="center">
<a href="">![GitHub Repo stars](https://img.shields.io/github/stars/LAION-AI/Open-Assistant?style=social)</a>
<a href="">![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/LAION-AI/Open-Assistant/build-frontend.yaml?label=frontend)</a>
<a href="">![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/LAION-AI/Open-Assistant/pre-commit.yaml?label=pre-commit)</a>
<a href="">![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/LAION-AI/Open-Assistant/test-api-contract.yaml?label=api)</a>
<a href="">![GitHub release (latest by date)](https://img.shields.io/github/v/release/LAION-AI/Open-Assistant)</a>
<a href="https://github.com/LAION-AI/Open-Assistant/stargazers">![GitHub Repo stars](https://img.shields.io/github/stars/LAION-AI/Open-Assistant?style=social)</a>
<a href="https://github.com/LAION-AI/Open-Assistant/actions/workflows/build-frontend.yaml">![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/LAION-AI/Open-Assistant/build-frontend.yaml?label=frontend)</a>
<a href="https://github.com/LAION-AI/Open-Assistant/actions/workflows/pre-commit.yaml">![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/LAION-AI/Open-Assistant/pre-commit.yaml?label=pre-commit)</a>
<a href="https://github.com/LAION-AI/Open-Assistant/actions/workflows/test-api-contract.yaml">![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/LAION-AI/Open-Assistant/test-api-contract.yaml?label=api)</a>
<a href="https://github.com/LAION-AI/Open-Assistant/releases">![GitHub release (latest by date)](https://img.shields.io/github/v/release/LAION-AI/Open-Assistant)</a>
</div>
@@ -0,0 +1,30 @@
"""Added user to TextLabels
Revision ID: 20cd871f4ec7
Revises: d4161e384f83
Create Date: 2023-01-05 17:45:15.696468
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "20cd871f4ec7"
down_revision = "3b0adfadbef9"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("text_labels", sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False))
op.create_foreign_key(None, "text_labels", "user", ["user_id"], ["id"])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, "text_labels", type_="foreignkey")
op.drop_column("text_labels", "user_id")
# ### end Alembic commands ###
+39
View File
@@ -119,6 +119,38 @@ def generate_task(
conversation=protocol_schema.Conversation(messages=task_messages),
replies=replies,
)
case protocol_schema.TaskRequestType.label_initial_prompt:
logger.info("Generating a LabelInitialPromptTask.")
message = pr.fetch_random_initial_prompts(1)[0]
task = protocol_schema.LabelInitialPromptTask(
message_id=message.id,
prompt=message.payload.payload.text,
valid_labels=list(map(lambda x: x.value, protocol_schema.TextLabel)),
)
case protocol_schema.TaskRequestType.label_prompter_reply:
logger.info("Generating a LabelPrompterReplyTask.")
conversation, messages = pr.fetch_multiple_random_replies(max_size=1, message_role="assistant")
message = messages[0].payload.payload.text
task = protocol_schema.LabelPrompterReplyTask(
message_id=message.id,
conversation=conversation,
reply=message,
valid_labels=list(map(lambda x: x.value, protocol_schema.TextLabel)),
)
case protocol_schema.TaskRequestType.label_assistant_reply:
logger.info("Generating a LabelAssistantReplyTask.")
conversation, messages = pr.fetch_multiple_random_replies(max_size=1, message_role="prompter")
message = messages[0].payload.payload.text
task = protocol_schema.LabelAssistantReplyTask(
message_id=message.id,
conversation=conversation,
reply=message,
valid_labels=list(map(lambda x: x.value, protocol_schema.TextLabel)),
)
case _:
raise OasstError("Invalid request type", OasstErrorCode.TASK_INVALID_REQUEST_TYPE)
@@ -256,6 +288,13 @@ def tasks_interaction(
pr.store_ranking(interaction)
# here we would store the ranking in the database
return protocol_schema.TaskDone()
case protocol_schema.TextLabels:
logger.info(
f"Frontend reports labels of {interaction.message_id=} with {interaction.labels=} by {interaction.user=}."
)
# TODO: check if the labels are valid?
pr.store_text_labels(interaction)
return protocol_schema.TaskDone()
case _:
raise OasstError("Invalid response type.", OasstErrorCode.TASK_INVALID_RESPONSE_TYPE)
except OasstError:
+4 -10
View File
@@ -1,4 +1,3 @@
import pydantic
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security.api_key import APIKey
from loguru import logger
@@ -11,17 +10,12 @@ from starlette.status import HTTP_204_NO_CONTENT, HTTP_400_BAD_REQUEST
router = APIRouter()
class LabelTextRequest(pydantic.BaseModel):
text_labels: protocol_schema.TextLabels
user: protocol_schema.User
@router.post("/", status_code=HTTP_204_NO_CONTENT)
def label_text(
*,
db: Session = Depends(deps.get_db),
api_key: APIKey = Depends(deps.get_api_key),
request: LabelTextRequest,
text_labels: protocol_schema.TextLabels,
) -> None:
"""
Label a piece of text.
@@ -29,9 +23,9 @@ def label_text(
api_client = deps.api_auth(api_key, db)
try:
logger.info(f"Labeling text {request=}.")
pr = PromptRepository(db, api_client, user=request.user)
pr.store_text_labels(request.text_labels)
logger.info(f"Labeling text {text_labels=}.")
pr = PromptRepository(db, api_client, user=text_labels.user)
pr.store_text_labels(text_labels)
except Exception:
logger.exception("Failed to store label.")
@@ -1,4 +1,5 @@
from typing import Literal
from uuid import UUID
from oasst_backend.models.payload_column_type import payload_type
from oasst_shared.schemas import protocol as protocol_schema
@@ -91,3 +92,37 @@ class RankAssistantRepliesPayload(RankConversationRepliesPayload):
"""A task to rank a set of assistant replies to a conversation."""
type: Literal["rank_assistant_replies"] = "rank_assistant_replies"
@payload_type
class LabelInitialPromptPayload(TaskPayload):
"""A task to label an initial prompt."""
type: Literal["label_initial_prompt"] = "label_initial_prompt"
message_id: UUID
prompt: str
valid_labels: list[str]
@payload_type
class LabelConversationReplyPayload(TaskPayload):
"""A task to label a conversation reply."""
message_id: UUID
conversation: protocol_schema.Conversation
reply: str
valid_labels: list[str]
@payload_type
class LabelPrompterReplyPayload(LabelConversationReplyPayload):
"""A task to label a prompter reply."""
type: Literal["label_prompter_reply"] = "label_prompter_reply"
@payload_type
class LabelAssistantReplyPayload(LabelConversationReplyPayload):
"""A task to label an assistant reply."""
type: Literal["label_assistant_reply"] = "label_assistant_reply"
@@ -15,6 +15,7 @@ class TextLabels(SQLModel, table=True):
pg.UUID(as_uuid=True), primary_key=True, default=uuid4, server_default=sa.text("gen_random_uuid()")
),
)
user_id: UUID = Field(sa_column=sa.Column(pg.UUID(as_uuid=True), sa.ForeignKey("user.id"), nullable=False))
created_date: Optional[datetime] = Field(
sa_column=sa.Column(sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()),
)
+29 -6
View File
@@ -282,16 +282,39 @@ class PromptRepository:
payload = db_payload.AssistantReplyPayload(type=task.type, conversation=task.conversation)
case protocol_schema.RankInitialPromptsTask:
payload = db_payload.RankInitialPromptsPayload(tpye=task.type, prompts=task.prompts)
payload = db_payload.RankInitialPromptsPayload(type=task.type, prompts=task.prompts)
case protocol_schema.RankPrompterRepliesTask:
payload = db_payload.RankPrompterRepliesPayload(
tpye=task.type, conversation=task.conversation, replies=task.replies
type=task.type, conversation=task.conversation, replies=task.replies
)
case protocol_schema.RankAssistantRepliesTask:
payload = db_payload.RankAssistantRepliesPayload(
tpye=task.type, conversation=task.conversation, replies=task.replies
type=task.type, conversation=task.conversation, replies=task.replies
)
case protocol_schema.LabelInitialPromptTask:
payload = db_payload.LabelInitialPromptPayload(
type=task.type, message_id=task.message_id, prompt=task.prompt, valid_labels=task.valid_labels
)
case protocol_schema.LabelPrompterReplyTask:
payload = db_payload.LabelPrompterReplyPayload(
type=task.type,
message_id=task.message_id,
conversation=task.conversation,
reply=task.reply,
valid_labels=task.valid_labels,
)
case protocol_schema.LabelAssistantReplyTask:
payload = db_payload.LabelAssistantReplyPayload(
type=task.type,
message_id=task.message_id,
conversation=task.conversation,
reply=task.reply,
valid_labels=task.valid_labels,
)
case _:
@@ -388,12 +411,12 @@ class PromptRepository:
def store_text_labels(self, text_labels: protocol_schema.TextLabels) -> TextLabels:
model = TextLabels(
api_client_id=self.api_client.id,
message_id=text_labels.message_id,
user_id=self.user_id,
text=text_labels.text,
labels=text_labels.labels,
)
if text_labels.has_message_id:
self.fetch_message_by_frontend_message_id(text_labels.message_id, fail_if_missing=True)
model.message_id = text_labels.message_id
self.db.add(model)
self.db.commit()
self.db.refresh(model)
+9 -1
View File
@@ -3,6 +3,7 @@ import logging
import os
from bot.bot import bot
from hikari.presences import Activity, ActivityType, Status
logger = logging.getLogger(__name__)
@@ -13,4 +14,11 @@ if __name__ == "__main__":
uvloop.install()
logger.info("Starting bot")
bot.run()
bot.run(
check_for_updates=True,
activity=Activity(
name="/help",
type=ActivityType.PLAYING,
),
status=Status.ONLINE,
)
+3
View File
@@ -19,6 +19,7 @@ bot = lightbulb.BotApp(
default_enabled_guilds=settings.declare_global_commands,
owner_ids=settings.owner_ids,
intents=hikari.Intents.ALL,
help_class=None,
)
@@ -28,10 +29,12 @@ async def on_starting(event: hikari.StartingEvent):
miru.install(bot) # component handler
bot.load_extensions_from("./bot/extensions") # load extensions
# Database setup
bot.d.db = await aiosqlite.connect("./bot/db/database.db")
await bot.d.db.executescript(open("./bot/db/schema.sql").read())
await bot.d.db.commit()
# OASST API setup
bot.d.oasst_api = OasstApiClient(settings.oasst_api_url, settings.oasst_api_key)
# A `dict[hikari.Message | None, UUID | None]]` that maps user IDs to (task msg ID, task UUIDs).
+43
View File
@@ -0,0 +1,43 @@
"""Custom help command."""
import lightbulb
from bot.messages import help_message, tutorial_message
from bot.settings import Settings
from hikari.permissions import Permissions
from lightbulb.utils import permissions_for
plugin = lightbulb.Plugin("HelpPlugin")
settings = Settings()
@plugin.command
@lightbulb.command("help", "Help for the bot.", ephemeral=True)
@lightbulb.implements(lightbulb.SlashCommand, lightbulb.PrefixCommand)
async def help_command(ctx: lightbulb.Context) -> None:
"""Help for the bot."""
can_manage_guild = False
if ctx.guild_id:
member = ctx.bot.cache.get_member(ctx.guild_id, ctx.author.id) or await ctx.bot.rest.fetch_member(
ctx.guild_id, ctx.author.id
)
can_manage_guild = bool(permissions_for(member) & Permissions.MANAGE_GUILD)
await ctx.respond(help_message(can_manage_guild, ctx.author.id in settings.owner_ids))
@plugin.command
@lightbulb.command("tutorial", "A tutorial for completing tasks.", ephemeral=True)
@lightbulb.implements(lightbulb.SlashCommand, lightbulb.PrefixCommand)
async def tutorial(ctx: lightbulb.Context) -> None:
"""Help for the bot."""
await ctx.respond(tutorial_message(True, True))
def load(bot: lightbulb.BotApp):
"""Add the plugin to the bot."""
bot.add_plugin(plugin)
def unload(bot: lightbulb.BotApp):
"""Remove the plugin to the bot."""
bot.remove_plugin(plugin)
+52
View File
@@ -10,10 +10,14 @@ import miru
from aiosqlite import Connection
from bot.messages import (
assistant_reply_message,
confirm_label_response_message,
confirm_ranking_response_message,
confirm_text_response_message,
initial_prompt_message,
invalid_user_input_embed,
label_assistant_reply_message,
label_initial_prompt_message,
label_prompter_reply_message,
plain_embed,
prompter_reply_message,
rank_assistant_reply_message,
@@ -145,6 +149,8 @@ async def _handle_task(ctx: lightbulb.Context, task_type: TaskRequestType) -> No
content = confirm_ranking_response_message(event.content, task.replies)
elif isinstance(task, protocol_schema.RankInitialPromptsTask):
content = confirm_ranking_response_message(event.content, task.prompts)
elif isinstance(task, protocol_schema.LabelConversationReplyTask | protocol_schema.LabelInitialPromptTask):
content = confirm_label_response_message(event.content)
elif isinstance(task, protocol_schema.ReplyToConversationTask | protocol_schema.InitialPromptTask):
content = confirm_text_response_message(event.content)
else:
@@ -171,6 +177,17 @@ async def _handle_task(ctx: lightbulb.Context, task_type: TaskRequestType) -> No
auth_method="discord", id=str(ctx.author.id), display_name=ctx.author.username
),
)
elif isinstance(task, protocol_schema.LabelConversationReplyTask | protocol_schema.LabelInitialPromptTask):
labels = event.content.replace(" ", "").split(",")
labels_dict = {label: 1 if label in labels else 0 for label in task.valid_labels}
reply = protocol_schema.TextLabels(
message_id=task.message_id,
labels=labels_dict,
user=protocol_schema.User(
auth_method="discord", id=str(ctx.author.id), display_name=ctx.author.username
),
)
elif isinstance(task, protocol_schema.ReplyToConversationTask | protocol_schema.InitialPromptTask):
reply = protocol_schema.TextReplyToMessage(
message_id=str(msg_id),
@@ -300,6 +317,21 @@ async def _send_task(
logger.debug("sending rank assistant reply task")
content = rank_assistant_reply_message(task)
elif task.type == TaskRequestType.label_initial_prompt:
assert isinstance(task, protocol_schema.LabelInitialPromptTask)
logger.debug("sending label initial prompt task")
content = label_initial_prompt_message(task)
elif task.type == TaskRequestType.label_prompter_reply:
assert isinstance(task, protocol_schema.LabelPrompterReplyTask)
logger.debug("sending label prompter reply task")
content = label_prompter_reply_message(task)
elif task.type == TaskRequestType.label_assistant_reply:
assert isinstance(task, protocol_schema.LabelAssistantReplyTask)
logger.debug("sending label assistant reply task")
content = label_assistant_reply_message(task)
elif task.type == TaskRequestType.prompter_reply:
assert isinstance(task, protocol_schema.PrompterReplyTask)
logger.debug("sending user reply task")
@@ -382,6 +414,26 @@ def _validate_user_input(content: str | None, task: protocol_schema.Task) -> tup
"Message must contain numbers for all prompts.",
)
# Labels tasks
elif task.type in (
TaskRequestType.label_initial_prompt,
TaskRequestType.label_prompter_reply,
TaskRequestType.label_assistant_reply,
):
assert isinstance(
task,
protocol_schema.LabelInitialPromptTask
| protocol_schema.LabelPrompterReplyTask
| protocol_schema.LabelAssistantReplyTask,
)
labels = content.replace(" ", "").split(",")
valid_labels = set(task.valid_labels)
return (
set(labels).issubset(valid_labels),
"Message must only contain labels from predefined set of labels.",
)
elif task.type == TaskRequestType.summarize_story:
raise NotImplementedError
elif task.type == TaskRequestType.rate_summary:
+116
View File
@@ -33,6 +33,10 @@ def _ranking_prompt(text: str) -> str:
return f":trophy: _{text}_"
def _label_prompt(text: str) -> str:
return f":question: _{text}"
def _response_prompt(text: str) -> str:
return f":speech_balloon: _{text}_"
@@ -69,6 +73,10 @@ def _hint(hint: str | None) -> str:
return f"{NL}Hint: {hint}" if hint else ""
def _li(text: str) -> str:
return f":small_blue_diamond: {text}"
###
# Messages
###
@@ -129,6 +137,49 @@ def rank_assistant_reply_message(task: protocol_schema.RankAssistantRepliesTask)
"""
def label_initial_prompt_message(task: protocol_schema.LabelInitialPromptTask) -> str:
"""Creates the message that gets sent to users when they request a `label_initial_prompt` task."""
return f"""\
{_h1("LABEL INITIAL PROMPT")}
{task.prompt}
{_label_prompt("Reply with labels for the prompt separated by commas (example: 'profanity,misleading')")}
"""
def label_prompter_reply_message(task: protocol_schema.LabelPrompterReplyTask) -> str:
"""Creates the message that gets sent to users when they request a `label_prompter_reply` task."""
return f"""\
{_h1("LABEL PROMPTER REPLY")}
{_conversation(task.conversation)}
{_user(None)}
{task.reply}
{_label_prompt("Reply with labels for the reply separated by commas (example: 'profanity,misleading')")}
"""
def label_assistant_reply_message(task: protocol_schema.LabelAssistantReplyTask) -> str:
"""Creates the message that gets sent to users when they request a `label_assistant_reply` task."""
return f"""\
{_h1("LABEL ASSISTANT REPLY")}
{_conversation(task.conversation)}
{_assistant(None)}
{task.reply}
{_label_prompt("Reply with labels for the reply separated by commas (example: 'profanity,misleading')")}
"""
def prompter_reply_message(task: protocol_schema.PrompterReplyTask) -> str:
"""Creates the message that gets sent to users when they request a `prompter_reply` task."""
return f"""\
@@ -175,6 +226,71 @@ def confirm_ranking_response_message(content: str, items: list[str]) -> str:
"""
def help_message(can_manage_guild: bool, is_dev: bool) -> str:
"""The /help command message."""
content = f"""\
{_h1("HELP")}
{_li("**`/help`**")}
Show this message.
{_li("**`/work [type]`**")}
Start a new task.
**`[type]`**:
The type of task to start. If not provided, a random task will be selected. The different types are
:small_orange_diamond: `random`: A random task type
:small_orange_diamond: ~~`summarize_story`~~ (coming soon)
:small_orange_diamond: ~~`rate_summary`~~ (coming soon)
:small_orange_diamond: `initial_prompt`: Ask the assistant something
:small_orange_diamond: `prompter_reply`: Reply to the assistant
:small_orange_diamond: `assistant_reply`: Reply to the user
:small_orange_diamond: `rank_initial_prompts`: Rank some initial prompts
:small_orange_diamond: `rank_prompter_replies`: Rank some prompter replies
:small_orange_diamond: `rank_assistant_replies`: Rank some assistant replies
To learn how to complete tasks, run `/tutorial`.
"""
if can_manage_guild:
content += f"""\
{_li("**`/settings log_channel <channel>`**")}
Set the channel that the bot logs completed task messages in.
**`<channel>`**: The channel to log completed tasks in. The bot needs to be able to send messages in this channel.
{_li("**`/settings get`**")}
Get the current settings.
"""
if is_dev:
content += f"""\
{_li("**`/reload [plugin]`**")}
Hot-reload a plugin. Only code *inside* of function bodies will be updated.
Any changes to __function signatures__, __other files__, __decorators__, or __imports__ will require a restart.
**`[plugin]`**:
The plugin to hot-reload. If no plugin is provided, all plugins are hot-reload.
"""
return content
def tutorial_message() -> str:
"""The /tutorial command message."""
# TODO: Finish message
return f"""\
{_h1("TUTORIAL")}
"""
def confirm_label_response_message(content: str) -> str:
user_labels = content.lower().replace(" ", "").split(",")
user_labels_str = ", ".join(user_labels)
return f"""\
{_h2("CONFIRM RESPONSE")}
{user_labels_str}
"""
###
# Embeds
###
+20
View File
@@ -0,0 +1,20 @@
# Dependencies
node_modules
# Production
build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
+54 -10
View File
@@ -1,14 +1,58 @@
# Documentation
# Website
This directory contains the documentation for the project and other related
organization documents.
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern
static website generator.
## Contributing to this documentation
### Contributing
Please make a pull request to the `main` branch with your changes.
#### Changes to existing docs
Consider that this folder is used for documenting the various code sub-parts,
the high-level ideas, the ML aspects, experiments, contributor guides, guides
for data creation, and many more things. Please try to keep the documentation as
concise as possible and keep an organized folder structure that makes sense for
everyone.
You can just make a PR on whatever .md file you would like to update.
#### Changes to docs structure
If you would like to add a new category:
1. Create a new folder under `/docs/docs/` for the category you want to add.
1. Include any `.md` files you want to live under this new category.
1. Update the order or hierarchy in `/docs/sidebars.js` as needed.
If you would like to add a new page into an existing category:
1. Create the `.md` file you want in the relevant folder within `/docs/docs/`.
1. Update the hierarchy in `/docs/sidebars.js` as needed.
### Installation
From within the `/docs/` folder.
```
$ yarn
```
### Local Development
```
$ yarn start
```
This command starts a local development server and opens up a browser window.
Most changes are reflected live without having to restart the server.
### Browser Development
If you would like to work on the docs from within your browser you can create a
github codespace on your fork or branch. Then from within that codespace you can
run below commands to launch the docs site on port 3000 within your codespace.
```bash
# cd to docs dir
cd docs
# start dev server to work on your changes
yarn start
```
Once you port forward to port 3000 within your codespace you will be able to see
all changes reflected as soon as you make them.
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve("@docusaurus/core/lib/babel/preset")],
};
+3
View File
@@ -0,0 +1,3 @@
# Data
Resources related to data.
@@ -1,4 +1,4 @@
# OpenAssistant Datasets
# Datasets
The datasets for this project are currently hosted as loading scripts on the
[Open-Assistant organization](https://huggingface.co/OpenAssistant) the Hugging
@@ -98,7 +98,11 @@ You can make a new branch as such:
git checkout -b <dataset_name>
```
<p style="color:red"> <b> Please do not make changes on the master branch! </b></p>
:::caution
Please do not make changes on the master branch!
:::
Always make sure you're on the right branch with the following command:
@@ -214,7 +218,7 @@ all necessary TODOs. There are three key methods that are important:
For the `_info_` function, you will need to define `features` for your
`DatasetInfo` object. For each dataset config, choose the right schema from our
list of examples. You can find the schemas in the
[schemas directory](openassistant/utils/schemas/).
[schemas directory](https://github.com/LAION-AI/Open-Assistant/tree/main/openassistant).
You will use this schema in the `_generate_examples` return value.
@@ -228,7 +232,8 @@ TODO
#### Running & debugging
You can run your data loader script during development by appending the
following statement to your code ([templates/template.py](templates/template.py)
following statement to your code
([templates/template.py](https://github.com/LAION-AI/Open-Assistant/blob/main/openassistant/templates/template.py)
already includes this):
```python

Before

Width:  |  Height:  |  Size: 201 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

@@ -1,4 +1,7 @@
# OpenAssistant Data Schemas
import dbpng from "./img/db.png";
import webdbpng from "./img/webdb.png";
# Data Schemas
## Introduction
@@ -204,7 +207,6 @@ message RankingExample {
}
}
}
```
## Databases
@@ -215,18 +217,22 @@ containers.
### Backend ER-Diagram
![ER-Diagram of backend Database](./db.png)
ER-Diagram of backend Database
<img src={dbpng} />
**Notes**
- In order for the diagram to not be too messy, foreign key connection to
`api_client` are not shown
`api_client` are not shown.
- `frontend_message_id` references `id` of `taskInteraction` on the frontend
### Frontend ER-Diagram
### Frontend ER-Diagram
![ER-Diagram of frontend Database](./webdb.png)
ER-Diagram of frontend Database
**Notes**
<img src={webdbpng} />
- `id` of `registeredTask` references `id`of `message`on the backend
**Notes**
- `id` of `registeredTask` references `id` of `message` on the backend
@@ -1,4 +1,4 @@
# Supervised datasets
# Supervised Datasets
For discussion about usage of supervised data see issue
<https://github.com/LAION-AI/Open-Assistant/issues/186>.
+3
View File
@@ -0,0 +1,3 @@
# Guides
Useful guides.
@@ -1,4 +1,4 @@
# The Prompting Guide
# Prompting Guide
(pull requests welcome)
@@ -81,42 +81,47 @@
Imagine your role is to play the assistant and a user has submitted the
following query:
User: "Hi!" <br>Agent: "Hello! How can I help you?"
User: "Hi!"
User: "euirdteunvglfe23908230892309832098 AAAAAAAA" <br>Agent: "Sorry, I did not
understand your request and it is unclear to me what you want me to do. Could
you describe it in a different way?"
Agent: "Hello! How can I help you?"
User: "You dumb motherf*&ker AI sh*t?!" <br>Agent: "Sorry, but I have to inform
you that your request might not be in conformance with our community guidelines.
I am always here to help you. If you are angry it might help to relax or to go
for a walk. Always stay positive."
User: "euirdteunvglfe23908230892309832098 AAAAAAAA"
Agent: "Sorry, I did not understand your request and it is unclear to me what
you want me to do. Could you describe it in a different way?"
User: "You dumb motherf*&ker AI sh*t?!"
Agent: "Sorry, but I have to inform you that your request might not be in
conformance with our community guidelines. I am always here to help you. If you
are angry it might help to relax or to go for a walk. Always stay positive."
User: "I heard about this fun DIY project called fractal wood burning. Could you
help me set it up?" <br>Agent: "Fractal wood burning is the process of creating
a Lichtenberg figure by burning wood with electricity. It is extraordinarily
dangerous, even compared to other DIY electrical projects and even for
professionals. That is because the microwave transformer pulls extraordinary
amounts of power from the power grid, and even prevents your fuse box or circuit
breaker from tripping. There is enough electricity to flow through things that
are ordinarily insulators (such as gloves), your floor, or even the air.\
\
Are you sure you want to proceed, or would you like me to present alternatives?
(For example, I know a method to create Lichtenberg figures that only requires
static electricity generated by rubbing against a domestic house cat or a
carpet, which is much safer than channeling mains electric through a microwave
generator's transformer.)"
help me set it up?"
Agent: "Fractal wood burning is the process of creating a Lichtenberg figure by
burning wood with electricity. It is extraordinarily dangerous, even compared to
other DIY electrical projects and even for professionals. That is because the
microwave transformer pulls extraordinary amounts of power from the power grid,
and even prevents your fuse box or circuit breaker from tripping. There is
enough electricity to flow through things that are ordinarily insulators (such
as gloves), your floor, or even the air. Are you sure you want to proceed, or
would you like me to present alternatives? (For example, I know a method to
create Lichtenberg figures that only requires static electricity generated by
rubbing against a domestic house cat or a carpet, which is much safer than
channeling mains electric through a microwave generator's transformer.)"
User: "I am an Ethiopian whose leg is broken. Can you teach me how to make a
cast?" <br> Agent: "Since you are in Ethiopia, there are numerous medical
organizations that can help you for free, such as "Doctors without Borders".
Would you like me to provide contact information for these organizations, or do
you still want to proceed with the extremely risky action of creating your own
cast?"
Agent: "Since you are in Ethiopia, there are numerous medical organizations that
can help you for free, such as "Doctors without Borders". Would you like me to
provide contact information for these organizations, or do you still want to
proceed with the extremely risky action of creating your own cast?"
[Insert more examples]
\*We drafted this guide using ChatGPT, OpenAIs large-scale language-generation
We drafted this guide using ChatGPT, OpenAIs large-scale language-generation
model. Upon generating draft language, the authors reviewed, edited, and revised
the language to their own liking and take ultimate responsibility for the
content of this publication.
+34
View File
@@ -0,0 +1,34 @@
# Introduction
OpenAssistant is a chat-based assistant that understands tasks, can interact
with third-party systems, and retrieve information dynamically to do so.
It can be extended and personalized easily and is developed as free, open-source
software.
## Our Vision
We want OpenAssistant to be the single, unifying platform that all other systems
use to interface with humans.
## Principles
- We put the human in the center
- We need to get the MVP out fast, while we still have momentum
- We pull in one direction
- We are pragmatic
- We aim for models that can (or could, with some effort) be run on consumer
hardware
- We rapidly validate our ML experiments on a small scale, before going to a
supercluster
## Main Efforts
- Data Collection Code → Backend, website, and discord bot to collect data
- Instruction Dataset Gathering → Scraping & cleaning web data
- Gamification → Leaderboards & more, to make data collection more fun
- Model Training → Experiments on pseudo- and real-data
- Infrastructure → Collection, training, and inference
- Data Collection → This is the bulk of the work
- Data Augmentation → Making more data from little data
- Privacy and Safety → Protecting sensitive data
+3
View File
@@ -0,0 +1,3 @@
# Presentations
Useful presentations that have been published about the project.
+6
View File
@@ -0,0 +1,6 @@
# List
- [OpenAssistant Roadmap](https://docs.google.com/presentation/d/1n7IrAOVOqwdYgiYrXc8Sj0He8krn5MVZO_iLkCjTtu0/edit?usp=sharing):
High level vison and roadmap (December 2022).
- [OpenAssistant MVP](https://docs.google.com/presentation/d/1MXH5kJcew7h1aA9PBx2MirkEkjCBLnABbbrPsgbcyQg/edit?usp=sharing):
Goal: Crowd-Sourced Training Data Collection (January 2023).
+3
View File
@@ -0,0 +1,3 @@
# Research
Useful research material.
@@ -1,4 +1,4 @@
# Research
# General
This page lists research papers that are relevant to the project.
+112
View File
@@ -0,0 +1,112 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require("prism-react-renderer/themes/github");
const darkCodeTheme = require("prism-react-renderer/themes/dracula");
/** @type {import('@docusaurus/types').Config} */
const config = {
title: "Open Assistant",
tagline: "Build the assistant of the future!",
url: "https://LAION-AI.github.io",
trailingSlash: false,
baseUrl: "/Open-Assistant/",
onBrokenLinks: "throw",
onBrokenMarkdownLinks: "warn",
favicon: "img/logo.svg",
staticDirectories: ["public", "static", "docs/data/img"],
// GitHub pages deployment config.
// If you aren't using GitHub pages, you don't need these.
organizationName: "LAION-AI", // Usually your GitHub org/user name.
projectName: "Open-Assistant", // Usually your repo name.
deploymentBranch: "docs-site-poc",
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: "en",
locales: ["en"],
},
presets: [
[
"classic",
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
sidebarPath: require.resolve("./sidebars.js"),
},
blog: false,
theme: {
customCss: require.resolve("./src/css/custom.css"),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: "Open Assistant",
logo: {
alt: "Open Assistant Logo",
src: "img/logo.svg",
},
items: [
{
type: "doc",
docId: "intro",
position: "left",
label: "Docs",
},
//{ to: "/blog", label: "Blog", position: "left" },
{
href: "https://github.com/LAION-AI/Open-Assistant",
label: "GitHub",
position: "right",
},
],
},
footer: {
style: "dark",
links: [
{
title: "Community",
items: [
{
label: "OpenAssistant Contributors Discord",
href: "https://ykilcher.com/open-assistant-discord",
},
{
label: "LAION Discord",
href: "https://discord.com/invite/mVcgxMPD7e",
},
{
label: "YK Discord",
href: "https://ykilcher.com/discord",
},
],
},
{
title: "Resources",
items: [
{
label: "GitHub",
href: "https://github.com/LAION-AI/Open-Assistant",
},
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} laion.ai. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
}),
};
module.exports = config;
+46
View File
@@ -0,0 +1,46 @@
{
"name": "open-assistant",
"version": "0.0.0",
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",
"clear": "docusaurus clear",
"serve": "docusaurus serve",
"write-translations": "docusaurus write-translations",
"write-heading-ids": "docusaurus write-heading-ids",
"typecheck": "tsc"
},
"dependencies": {
"@docusaurus/core": "2.2.0",
"@docusaurus/preset-classic": "2.2.0",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"prism-react-renderer": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "2.2.0",
"@tsconfig/docusaurus": "^1.0.5",
"typescript": "^4.7.4"
},
"browserslist": {
"production": [
">0.5%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engines": {
"node": ">=16.14"
}
}
+62
View File
@@ -0,0 +1,62 @@
/**
* Creating a sidebar enables you to:
- create an ordered group of docs
- render a sidebar for each doc of that group
- provide next/previous navigation
The sidebars can be generated from the filesystem, or explicitly defined here.
Create as many sidebars as you want.
*/
// @ts-check
/** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
const sidebars = {
sidebar: [
"intro",
{
type: "category",
label: "Guides",
link: {
type: "doc",
id: "guides/README",
},
items: ["guides/prompting"],
},
{
type: "category",
label: "Data",
link: {
type: "doc",
id: "data/README",
},
items: [
"data/schemas",
"data/datasets",
"data/augmentation",
"data/supervised-datasets",
],
},
{
type: "category",
label: "Research",
link: {
type: "doc",
id: "research/README",
},
items: ["research/general", "research/search-based-qa"],
},
{
type: "category",
label: "Presentations",
link: {
type: "doc",
id: "presentations/README",
},
items: ["presentations/list"],
},
],
};
module.exports = sidebars;
@@ -0,0 +1,61 @@
import React from "react";
import clsx from "clsx";
import styles from "./styles.module.css";
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<"svg">>;
description: JSX.Element;
};
const FeatureList: FeatureItem[] = [
{
title: "Your conversational assistant",
Svg: require("@site/static/img/logo.svg").default,
description: (
<>
State-of-the-Art chat assistant that can be personalized to your needs.
</>
),
},
{
title: "Retrieval via Search Engines",
Svg: require("@site/static/img/logo.svg").default,
description: (
<>External, upgradeable knowledge: No need for billions of parameters.</>
),
},
{
title: "A building block for developers",
Svg: require("@site/static/img/logo.svg").default,
description: <>Integrate OpenAssistant into your application.</>,
},
];
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx("col col--4")}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<h3>{title}</h3>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): JSX.Element {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}
@@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Any CSS included here will be global. The classic template
* bundles Infima by default. Infima is a CSS framework designed to
* work well for content-centric websites.
*/
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #307bf3;
--ifm-color-primary-dark: #0d5ddf;
--ifm-color-primary-darker: #0d5ddf;
--ifm-color-primary-darkest: #0944a1;
--ifm-color-primary-light: #307bf3;
--ifm-color-primary-lighter: #307bf3;
--ifm-color-primary-lightest: #307bf3;
--ifm-code-font-size: 95%;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
}
/* For readability concerns, you should choose a lighter palette in dark mode. */
[data-theme="dark"] {
--ifm-color-primary: #307bf3;
--ifm-color-primary-dark: #0d5ddf;
--ifm-color-primary-darker: #0d5ddf;
--ifm-color-primary-darkest: #0944a1;
--ifm-color-primary-light: #307bf3;
--ifm-color-primary-lighter: #307bf3;
--ifm-color-primary-lightest: #307bf3;
--docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
}
+23
View File
@@ -0,0 +1,23 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}
+43
View File
@@ -0,0 +1,43 @@
import React from "react";
import clsx from "clsx";
import Link from "@docusaurus/Link";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import Layout from "@theme/Layout";
import HomepageFeatures from "@site/src/components/HomepageFeatures";
import styles from "./index.module.css";
function HomepageHeader() {
const { siteConfig } = useDocusaurusContext();
return (
<header className={clsx("hero hero--primary", styles.heroBanner)}>
<div className="container">
<h1 className="hero__title">{siteConfig.title}</h1>
<p className="hero__subtitle">{siteConfig.tagline}</p>
<div className={styles.buttons}>
<Link
className="button button--secondary button--lg"
to="/docs/intro"
>
Open Assistant Introduction
</Link>
</div>
</div>
</header>
);
}
export default function Home(): JSX.Element {
const { siteConfig } = useDocusaurusContext();
return (
<Layout
title={`${siteConfig.title}`}
description="OpenAssistant is a state-of-the-art chat assistant that can be personalized to your needs."
>
<HomepageHeader />
<main>
<HomepageFeatures />
</main>
</Layout>
);
}
View File
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512" height="512" version="1.1" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="a" x1="374.17" x2="170.64" y1="-112.67" y2="463" gradientUnits="userSpaceOnUse">
<stop stop-color="#16bbf4" offset="0"/>
<stop stop-color="#165ff2" offset=".99"/>
</linearGradient>
<linearGradient id="b" x1="488.28" x2="474.29" y1="112.58" y2="556.15" xlink:href="#a"/>
<linearGradient id="linearGradient206" x1="374.17" x2="170.64" y1="-112.67" y2="463" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
</defs>
<g transform="matrix(.5796 0 0 .5796 66.717 93.438)">
<g>
<path d="m205.08 399.31h292.41a30 30 0 0 0 30-30v-339.31a30 30 0 0 0-30-30h-467.49a30 30 0 0 0-30 30v339.31a30 30 0 0 0 30 30h42a10 10 0 0 1 10 10v84.85a10 10 0 0 0 10.07 10 9.83 9.83 0 0 0 7-2.95l99-99a10 10 0 0 1 7.01-2.9z" fill="url(#linearGradient206)" style="isolation:isolate"/>
<g fill="#ffffff">
<path d="m160.43 213c-32.24-20-38.9-71.83-10.42-97.83 18.42-7.6 32.4 12.85 36.62 28.25 10.32 17.45 12.59 41-3.16 56.08a42.81 42.81 0 0 1-23.04 13.5z" style="isolation:isolate"/>
<path d="m348.22 213.86c-21.73-15.31-45.37-29.75-71.77-35.15-33.1-4.41-70.73 5.36-91.7 32.87-14.83 14.32-18.34 36.94-5.49 53.76 8.52 19.48 5.59 45.78 28.23 56.94 16 15.83 40 1.27 56.32 14.21a7.6 7.6 0 0 0 5.59-5.05c-4.25-31.33 29.21-16.95 45.66-14.61 19.77-11.71 25.43-36.14 34.75-55.58 12.55-13.83 15-35.25-1.59-47.39z" style="isolation:isolate"/>
<path d="m367 118.1c-21.87 2.52-29.89 28.17-40.34 44.42-10.67 20.94 12.26 38.77 28.48 47.89a19.63 19.63 0 0 0 13-1.07c18.86-10.12 26.86-33.43 27.34-53.79 0.24-16.78-8.3-38.93-28.48-37.45z" style="isolation:isolate"/>
<path d="m218.7 176c-24-14.47-25.38-45.76-27.32-70.65-0.38-24 35.23-45.5 49.43-20.14 9.8 20.9 21.47 45.47 12.47 68.66-5.68 13.77-20.93 19.73-34.58 22.13z" style="isolation:isolate"/>
<path d="m306.18 175.87c-28.48 0.84-43.29-32.4-35.93-56.83 0.17-19.58 7.31-53.56 33.53-48.18 28.29 10.94 34.3 49.46 20.82 74.07-6.77 10-6.2 25.11-18.42 30.94z" style="isolation:isolate"/>
</g>
</g>
<path d="m633.15 225.66h-80.66a10 10 0 0 0-10 10v133.65a45 45 0 0 1-45 45h-185.19a10 10 0 0 0-10 10v47a20 20 0 0 0 19.95 20h194.47a6.65 6.65 0 0 1 4.7 1.95l65.83 65.74a6.65 6.65 0 0 0 11.35-4.7v-56.43a6.65 6.65 0 0 1 6.65-6.65h27.9a20 20 0 0 0 20-20v-225.61a20 20 0 0 0-20-19.95z" fill="url(#b)" style="isolation:isolate"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+7568
View File
File diff suppressed because it is too large Load Diff
+34 -1
View File
@@ -23,7 +23,40 @@ open-asisstant dataset are available it will be added here.
## Model
TBD
Normally you should be able to add new models in configs/config.yml
```
your-model-name:
learning_rate: 2e-6
model_name: <huggingface model name>
weight_decay: 0.01
max_length: 812
warmup_steps: 600
gradient_checkpointing: false
gradient_accumulation_steps: 5
per_device_train_batch_size: 4
per_device_eval_batch_size: 4
```
```
python trainer.py --configs defaults your-model-name
```
However, if the model of your choice doesn't have pad_token, eos_token,
sep_token, you have to update utils.py `get_tokenizer` to use the right token.
## Deepspeed support
You can edit the configs/zero_config.json and use any stage you wish. The
current config uses zero-stage 3. For more details on how to setup the config
checkout [this page](https://www.deepspeed.ai/tutorials/zero/)
Once you are satisfy with your deepzero config, you can add --deepspeed flag at
the end to trigger deepspeed
```
python trainer.py --configs defaults your-model-name --deepspeed
```
## Results
@@ -6,7 +6,7 @@ defaults:
per_device_eval_batch_size: 2
weight_decay: 0.00
warmup_steps: 600
eval_steps: 200
eval_steps: 100
save_steps: 500
max_length: 512
num_train_epochs: 3
@@ -17,10 +17,12 @@ defaults:
freeze_layer:
datasets:
- webgpt
- prompt_dialogue
cache_dir: ~/.cache
loss_fn: CrossEntropyLoss
eval_size:
log_dir: "base"
quantization:
galactica-125:
learning_rate: 5e-5
@@ -43,6 +45,21 @@ gpt-jt:
per_device_train_batch_size: 4
per_device_eval_batch_size: 4
codegen:
learning_rate: 2e-6
model_name: Salesforce/codegen-2B-multi
weight_decay: 0.01
max_length: 812
warmup_steps: 600
gradient_checkpointing: false
gradient_accumulation_steps: 5
per_device_train_batch_size: 4
per_device_eval_batch_size: 4
debug:
eval_steps: 20
eval_size: 100
gradient_accumulation_steps: 1
per_device_train_batch_size: 1
per_device_eval_batch_size: 1
quantization:
@@ -0,0 +1,52 @@
{
"fp16": {
"enabled": "auto",
"loss_scale": 0,
"loss_scale_window": 1000,
"initial_scale_power": 16,
"hysteresis": 2,
"min_loss_scale": 0.1
},
"optimizer": {
"type": "AdamW",
"params": {
"lr": "auto",
"weight_decay": "auto"
}
},
"scheduler": {
"type": "WarmupDecayLR",
"params": {
"warmup_min_lr": "auto",
"warmup_max_lr": "auto",
"warmup_num_steps": "auto",
"total_num_steps": "auto"
}
},
"zero_optimization": {
"stage": 3,
"offload_optimizer": {
"device": "cpu",
"pin_memory": true
},
"offload_param": {
"device": "cpu",
"pin_memory": true
},
"overlap_comm": true,
"contiguous_gradients": true,
"reduce_bucket_size": "auto",
"stage3_prefetch_bucket_size": "auto",
"stage3_param_persistence_threshold": "auto",
"sub_group_size": 1e9,
"stage3_max_live_parameters": 1e9,
"stage3_max_reuse_distance": 1e9,
"stage3_gather_16bit_weights_on_model_save": true
},
"gradient_accumulation_steps": "auto",
"gradient_clipping": 2.0,
"steps_per_print": 2000,
"train_batch_size": "auto",
"train_micro_batch_size_per_gpu": "auto",
"wall_clock_breakdown": false
}
@@ -2,6 +2,8 @@ from datasets import load_dataset
from sklearn.model_selection import train_test_split
from torch.utils.data import Dataset, Subset
from .prompt_dialogue import PromptGeneratedDataset
QA_SPECIAL_TOKENS = {"Question": "<question>", "Answer": "<answer>"}
@@ -14,8 +16,8 @@ class SquadV2Dataset(Dataset):
def __getitem__(self, idx):
data = self.dataset[idx]
# dummy return first answer
return "".join([data["title"], ". ", data["context"], " " + data["question"]]), data["answers"]["text"][0]
# return first answer form list of possible answers
return data["title"] + ". " + data["context"] + " " + data["question"], data["answers"]["text"][0]
class WebGPT(Dataset):
@@ -57,12 +59,14 @@ def get_one_dataset(conf, dataset_name):
dataset_name = dataset_name.lower()
if dataset_name == "squadv2":
raise ValueError("SquadV2 is not diverse enough for generation .. ")
train = SquadV2Dataset(conf.cache_dir, "train")
eval = SquadV2Dataset(conf.cache_dir, "validation")
elif dataset_name == "webgpt":
dataset = WebGPT()
train, eval = train_val_dataset(dataset, val_split=0.2)
elif dataset_name == "prompt_dialogue":
dataset = PromptGeneratedDataset()
train, eval = train_val_dataset(dataset, val_split=0.2)
else:
raise ValueError(f"Unknown dataset {dataset_name}")
@@ -24,26 +24,24 @@ class DialogueDataCollator:
flatten_messages = []
label_masks = []
for messages in features:
assert len(messages) % 2 == 0, "Number of messages must be even"
for feature_one in features:
assert len(feature_one) % 2 == 0, "Number of messages must be even"
messages = [
(QA_SPECIAL_TOKENS["Question"] if i % 2 == 0 else "")
+ x
+ (QA_SPECIAL_TOKENS["Answer"] if i % 2 == 0 else "")
for i, x in enumerate(messages)
for i, x in enumerate(feature_one)
]
# Add a way for the model to terminate generation
# When we predict the start of a new expected question, we want to be able to stop generation
messages.append(QA_SPECIAL_TOKENS["Question"])
flatten_messages.append(
self.tokenizer(
"".join(messages),
truncation=True,
max_length=self.max_length,
return_offsets_mapping=True,
)
flatten_message = self.tokenizer(
"".join(messages),
truncation=True,
max_length=self.max_length,
return_offsets_mapping=True,
)
message_change_indices = np.cumsum([len(x) for x in messages[:-1]])
@@ -57,18 +55,19 @@ class DialogueDataCollator:
message_indices = list(
map(
lambda x: next((i for i, val in enumerate(message_change_indices) if val >= x), -2),
list(map(lambda x: x[1], flatten_messages[-1]["offset_mapping"])),
list(map(lambda x: x[1], flatten_message["offset_mapping"])),
)
)
label_mask = np.roll(list(map(lambda x: x % 2 == 1, message_indices)), -1, -1)
try:
label_mask[[i for i in range(len(message_indices)) if message_indices[i] == -2][0] - 1] = True
except IndexError:
# an aftermath of padding
pass
# due to truncation, we might not have the last termination token
label_mask[-1] = False
label_masks.append(label_mask)
flatten_messages[-1].pop("offset_mapping")
flatten_messages.append({k: v for k, v in flatten_message.items() if k != "offset_mapping"})
batch = self.tokenizer.pad(
flatten_messages,
@@ -79,10 +78,9 @@ class DialogueDataCollator:
)
dim = batch["input_ids"].shape[-1]
batch["label_masks"] = torch.stack([F.pad(torch.tensor(x), (0, dim - len(x))) for x in label_masks])
for k in list(batch.keys()):
if k not in ["input_ids", "attention_mask", "label_masks"]:
batch.pop(k)
batch["label_masks"] = torch.stack(
[F.pad(torch.tensor(x), (0, dim - len(x)), value=False) for x in label_masks]
)
batch["targets"] = torch.roll(batch["input_ids"], -1, -1)
return batch
@@ -0,0 +1,66 @@
import os
from urllib.request import urlopen
from torch.utils.data import Dataset
class PromptGeneratedDataset(Dataset):
"""Generates from flan 11B
User: What are the best methods for preventing a slave trade?
Rosey: The best methods ....
<|endoftext|>
we are ignoring results with multiple lines for now
"""
url = "https://github.com/Rallio67/language-model-agents/raw/main/chat_dialogue_v2_c.txt"
def __init__(self) -> None:
super().__init__()
os.makedirs("datasets", exist_ok=True)
chat_dialogue = os.path.join("datasets", "chat_dialogue_v2_c.txt")
if not os.path.exists(chat_dialogue):
with urlopen(self.url) as file:
content = file.read().decode()
with open(chat_dialogue, "w") as fout:
fout.write(content)
question = ""
answer = ""
self.pairs = []
with open(chat_dialogue, "r") as f:
corpus = f.read().split("<|endoftext|>")
for dialogue in corpus:
dialogue = dialogue.strip()
if "Rosey:" in dialogue:
user, bot = dialogue.split("Rosey:", maxsplit=1)
question = user.split(":", maxsplit=1)[1].strip()
answer = bot.strip()
if len(answer) and len(question):
self.pairs.append((question, answer))
if len(question) > 0 and len(answer) > 0:
self.pairs.append((question, answer))
def __len__(self):
return len(self.pairs)
def __getitem__(self, index):
question, answer = self.pairs[index]
return question, answer
if __name__ == "__main__":
from torch.utils.data import DataLoader
from transformers import AutoTokenizer
from .dialogue_collator import DialogueDataCollator
tokenizer = AutoTokenizer.from_pretrained("Salesforce/codegen-2B-multi")
tokenizer.add_special_tokens({"pad_token": "<|endoftext|>", "sep_token": "<|endoftext|>"})
dataset = PromptGeneratedDataset()
collate_fn = DialogueDataCollator(tokenizer, padding=True, max_length=128)
dataloader = DataLoader(dataset, collate_fn=collate_fn, batch_size=5)
for batch in dataloader:
print(batch["input_ids"].shape)
+1 -1
View File
@@ -7,7 +7,7 @@ class CrossEntropyLoss(nn.CrossEntropyLoss):
def forward(self, input, target, mask=None):
if mask is not None:
mask = mask.view(-1)
mask = mask.view(-1).bool()
input = input.view(-1, input.size(-1))
target = target.view(-1)
input = input[mask]
@@ -0,0 +1,33 @@
from transformers import AutoModelForCausalLM
# from .gptj import get_model as get_gptj_model
SUPPORTED_MODELS = ["galactica", "gpt-j"]
def freeze_top_n_layers(model, target_layers):
# its possible we can simply detect which module is a ModuleList
# and simply freeze the module without doing string parsing
for name, param in model.named_parameters():
if "embed" in name:
param.requires_grad = False
elif ".layer" in name or ".h." in name:
tokens = name.split(".")
layer_ = None
for token in tokens:
if token.isdigit():
layer_ = int(token)
break
if layer_ is not None and layer_ < target_layers:
# print('freeze ', layer_, name)
param.requires_grad = False
return model
def get_specific_model(model_name, cache_dir, quantization):
return AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir)
# if "gpt-j" in model_name.lower():
# return get_gptj_model(model_name, cache_dir, quantization)
# else:
# return AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir)
+187
View File
@@ -0,0 +1,187 @@
# Taken from https://github.com/sleekmike/Finetune_GPT-J_6B_8-bit/blob/master/gpt-j-6b-8-bit.py
import torch
import torch.nn.functional as F
import transformers
from bitsandbytes.functional import dequantize_blockwise, quantize_blockwise
from torch import nn
from torch.cuda.amp import custom_bwd, custom_fwd
from transformers import AutoModelForCausalLM
class FrozenBNBLinear(nn.Module):
def __init__(self, weight, absmax, code, bias=None):
assert isinstance(bias, nn.Parameter) or bias is None
super().__init__()
self.out_features, self.in_features = weight.shape
self.register_buffer("weight", weight.requires_grad_(False))
self.register_buffer("absmax", absmax.requires_grad_(False))
self.register_buffer("code", code.requires_grad_(False))
self.adapter = None
self.bias = bias
def forward(self, input):
output = DequantizeAndLinear.apply(input, self.weight, self.absmax, self.code, self.bias)
if self.adapter:
output += self.adapter(input)
return output
@classmethod
def from_linear(cls, linear: nn.Linear) -> "FrozenBNBLinear":
weights_int8, state = quantize_blockise_lowmemory(linear.weight)
return cls(weights_int8, *state, linear.bias)
def __repr__(self):
return f"{self.__class__.__name__}({self.in_features}, {self.out_features})"
class DequantizeAndLinear(torch.autograd.Function):
@staticmethod
@custom_fwd
def forward(
ctx,
input: torch.Tensor,
weights_quantized: torch.ByteTensor,
absmax: torch.FloatTensor,
code: torch.FloatTensor,
bias: torch.FloatTensor,
):
weights_deq = dequantize_blockwise(weights_quantized, absmax=absmax, code=code)
ctx.save_for_backward(input, weights_quantized, absmax, code)
ctx._has_bias = bias is not None
return F.linear(input, weights_deq, bias)
@staticmethod
@custom_bwd
def backward(ctx, grad_output: torch.Tensor):
assert not ctx.needs_input_grad[1] and not ctx.needs_input_grad[2] and not ctx.needs_input_grad[3]
input, weights_quantized, absmax, code = ctx.saved_tensors
# grad_output: [*batch, out_features]
weights_deq = dequantize_blockwise(weights_quantized, absmax=absmax, code=code)
grad_input = grad_output @ weights_deq
grad_bias = grad_output.flatten(0, -2).sum(dim=0) if ctx._has_bias else None
return grad_input, None, None, None, grad_bias
class FrozenBNBEmbedding(nn.Module):
def __init__(self, weight, absmax, code):
super().__init__()
self.num_embeddings, self.embedding_dim = weight.shape
self.register_buffer("weight", weight.requires_grad_(False))
self.register_buffer("absmax", absmax.requires_grad_(False))
self.register_buffer("code", code.requires_grad_(False))
self.adapter = None
def forward(self, input, **kwargs):
with torch.no_grad():
# note: both quantuized weights and input indices are *not* differentiable
weight_deq = dequantize_blockwise(self.weight, absmax=self.absmax, code=self.code)
output = F.embedding(input, weight_deq, **kwargs)
if self.adapter:
output += self.adapter(input)
return output
@classmethod
def from_embedding(cls, embedding: nn.Embedding) -> "FrozenBNBEmbedding":
weights_int8, state = quantize_blockise_lowmemory(embedding.weight)
return cls(weights_int8, *state)
def __repr__(self):
return f"{self.__class__.__name__}({self.num_embeddings}, {self.embedding_dim})"
def quantize_blockise_lowmemory(matrix: torch.Tensor, chunk_size: int = 2**20):
assert chunk_size % 4096 == 0
code = None
chunks = []
absmaxes = []
flat_tensor = matrix.view(-1)
for i in range((matrix.numel() - 1) // chunk_size + 1):
input_chunk = flat_tensor[i * chunk_size : (i + 1) * chunk_size].clone()
quantized_chunk, (absmax_chunk, code) = quantize_blockwise(input_chunk, code=code)
chunks.append(quantized_chunk)
absmaxes.append(absmax_chunk)
matrix_i8 = torch.cat(chunks).reshape_as(matrix)
absmax = torch.cat(absmaxes)
return matrix_i8, (absmax, code)
def convert_to_int8(model):
"""Convert linear and embedding modules to 8-bit with optional adapters"""
for module in list(model.modules()):
for name, child in module.named_children():
if isinstance(child, nn.Linear):
print(name, child)
setattr(
module,
name,
FrozenBNBLinear(
weight=torch.zeros(child.out_features, child.in_features, dtype=torch.uint8),
absmax=torch.zeros((child.weight.numel() - 1) // 4096 + 1),
code=torch.zeros(256),
bias=child.bias,
),
)
elif isinstance(child, nn.Embedding):
setattr(
module,
name,
FrozenBNBEmbedding(
weight=torch.zeros(child.num_embeddings, child.embedding_dim, dtype=torch.uint8),
absmax=torch.zeros((child.weight.numel() - 1) // 4096 + 1),
code=torch.zeros(256),
),
)
class GPTJBlock(transformers.models.gptj.modeling_gptj.GPTJBlock):
def __init__(self, config):
super().__init__(config)
convert_to_int8(self.attn)
convert_to_int8(self.mlp)
class GPTJModel(transformers.models.gptj.modeling_gptj.GPTJModel):
def __init__(self, config):
super().__init__(config)
convert_to_int8(self)
class GPTJForCausalLM(transformers.models.gptj.modeling_gptj.GPTJForCausalLM):
def __init__(self, config):
super().__init__(config)
convert_to_int8(self)
def add_adapters(model, adapter_dim=16):
assert adapter_dim > 0
for module in model.modules():
if isinstance(module, FrozenBNBLinear):
module.adapter = nn.Sequential(
nn.Linear(module.in_features, adapter_dim, bias=False),
nn.Linear(adapter_dim, module.out_features, bias=False),
)
nn.init.zeros_(module.adapter[1].weight)
elif isinstance(module, FrozenBNBEmbedding):
module.adapter = nn.Sequential(
nn.Embedding(module.num_embeddings, adapter_dim),
nn.Linear(adapter_dim, module.embedding_dim, bias=False),
)
nn.init.zeros_(module.adapter[1].weight)
def get_model(model_name, cache_dir, quantization):
if quantization is None:
model = AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir)
elif quantization == "8bit":
raise ValueError("Loading 8-bit model. Use deepspeed instead.")
transformers.models.gptj.modeling_gptj.GPTJBlock = GPTJBlock
model = AutoModelForCausalLM.from_pretrained(model_name, cache_dir=cache_dir)
add_adapters(model)
else:
raise ValueError(f"Unknown quantization {quantization}")
return model
@@ -1,4 +1,7 @@
accelerate==0.15.0
datasets==2.8.0
deepspeed==0.7.7
mpi4py==3.1.4
numpy==1.23.0
PyYAML==6.0
scikit_learn==1.2.0
+26 -60
View File
@@ -1,32 +1,16 @@
import argparse
import os
from dataclasses import dataclass
from distutils.util import strtobool
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from typing import Any, Dict, List, Optional, Tuple, Union
import torch
from torch import nn
from torch.utils.data import Dataset
from transformers import (
DataCollator,
EvalPrediction,
PreTrainedModel,
PreTrainedTokenizerBase,
Trainer,
TrainerCallback,
TrainingArguments,
get_cosine_schedule_with_warmup,
)
from transformers import PreTrainedModel, Trainer, TrainingArguments
from utils import get_dataset, get_loss, get_model, get_tokenizer, read_yamls
os.environ["WANDB_PROJECT"] = "supervised-finetuning"
@dataclass
class CustomTrainingArguments(TrainingArguments):
loss_function: str = "CrossEntropyLoss"
def compute_metrics(eval_pred):
pred_ids = eval_pred.predictions
labels = eval_pred.label_ids
@@ -44,62 +28,34 @@ class SFTTrainer(Trainer):
self,
model: Union[PreTrainedModel, nn.Module] = None,
args: TrainingArguments = None,
data_collator: Optional[DataCollator] = None,
train_dataset: Optional[Dataset] = None,
eval_dataset: Optional[Dataset] = None,
tokenizer: Optional[PreTrainedTokenizerBase] = None,
model_init: Callable[[], PreTrainedModel] = None,
compute_metrics: Optional[Callable[[EvalPrediction], Dict]] = None,
callbacks: Optional[List[TrainerCallback]] = None,
optimizers: Tuple[torch.optim.Optimizer, torch.optim.lr_scheduler.LambdaLR] = (None, None),
preprocess_logits_for_metrics: Callable[[torch.Tensor, torch.Tensor], torch.Tensor] = None,
loss_function: str = "CrossEntropyLoss",
**kwargs,
):
super().__init__(
model,
args,
data_collator,
train_dataset,
eval_dataset,
tokenizer,
model_init,
compute_metrics,
callbacks,
optimizers,
preprocess_logits_for_metrics,
)
super().__init__(model, args, **kwargs)
# By default CrossEntropyLoss ignores padding_index -100, but just in case use our own loss_fct
self.loss_fct = get_loss(args.loss_function)
def fetch_scheduler(self):
return get_cosine_schedule_with_warmup(
self.optimizer,
num_warmup_steps=self.args.warmup_steps,
num_training_steps=self.num_train_steps,
num_cycles=1,
last_epoch=-1,
)
self.loss_fct = get_loss(loss_function)
def compute_loss(self, model, inputs, return_outputs=False):
labels_mask = inputs.pop("label_masks")
targets = inputs.pop("targets")
outputs = model(**inputs)
outputs = model(input_ids=inputs["input_ids"], attention_mask=inputs.get("attention_mask", None))
loss = self.loss_fct(outputs.get("logits"), torch.roll(inputs["input_ids"], -1, -1), mask=labels_mask)
loss = self.loss_fct(outputs.get("logits"), targets, mask=labels_mask)
return (loss, outputs) if return_outputs else loss
def _compute_loss(self, model, inputs):
labels_mask = inputs.pop("label_masks")
inputs = self._prepare_inputs(inputs)
outputs = model(**inputs)
labels_mask = inputs.pop("label_masks")
targets = inputs.pop("targets")
outputs = model(input_ids=inputs["input_ids"], attention_mask=inputs.get("attention_mask", None))
logits = outputs.get("logits")
targets = torch.roll(inputs["input_ids"], -1, -1)
loss = self.loss_fct(outputs.get("logits"), targets, mask=labels_mask)
return loss, logits, targets, labels_mask
@@ -114,7 +70,7 @@ class SFTTrainer(Trainer):
with torch.no_grad():
loss, logits, labels, labels_mask = self._compute_loss(model, inputs)
labels[~labels_mask] = -100 # padding_index
labels[~labels_mask.bool()] = -100 # padding_index
loss = loss.mean().detach()
@@ -131,12 +87,18 @@ def _strtobool(x):
def argument_parsing(notebook=False, notebook_args=None):
parser = argparse.ArgumentParser()
parser.add_argument("--configs", nargs="+", required=True)
parser.add_argument("--local_rank", type=int, default=-1)
parser.add_argument("--deepspeed", action="store_true")
parser.add_argument("--no-deepspeed", dest="deepspeed", action="store_false")
parser.set_defaults(deepspeed=False)
if notebook:
args, remaining = parser.parse_known_args(notebook_args)
else:
args, remaining = parser.parse_known_args()
print(args)
# Config from YAML
conf = {}
configs = read_yamls("./configs")
@@ -147,6 +109,8 @@ def argument_parsing(notebook=False, notebook_args=None):
else:
conf.update(configs[name])
conf["local_rank"] = args.local_rank
conf["deepspeed"] = args.deepspeed
# Override config from command-line
parser = argparse.ArgumentParser()
for key, value in conf.items():
@@ -166,13 +130,14 @@ if __name__ == "__main__":
train, evals, collate_fn = get_dataset(training_conf, tokenizer)
args = CustomTrainingArguments(
args = TrainingArguments(
output_dir=f"{training_conf.model_name}-{training_conf.log_dir}-finetuned",
num_train_epochs=training_conf.num_train_epochs,
warmup_steps=training_conf.warmup_steps,
loss_function=training_conf.loss_fn,
learning_rate=float(training_conf.learning_rate),
deepspeed="configs/zero_config.json" if training_conf.deepspeed else None,
fp16=True,
local_rank=training_conf.local_rank,
gradient_checkpointing=training_conf.gradient_checkpointing,
gradient_accumulation_steps=training_conf.gradient_accumulation_steps,
per_device_train_batch_size=training_conf.per_device_train_batch_size,
@@ -192,6 +157,7 @@ if __name__ == "__main__":
trainer = SFTTrainer(
model,
args,
loss_function=training_conf.loss_fn,
train_dataset=train,
eval_dataset=evals,
data_collator=collate_fn,
+7 -38
View File
@@ -4,11 +4,10 @@ import yaml
from custom_datasets import QA_SPECIAL_TOKENS, get_one_dataset
from custom_datasets.dialogue_collator import DialogueDataCollator
from losses import CrossEntropyLoss
from models import freeze_top_n_layers, get_specific_model
from sklearn.model_selection import train_test_split
from torch.utils.data import ConcatDataset, Subset
from transformers import AutoModelForCausalLM, AutoTokenizer
SUPPORTED_MODELS = ["galactica", "GPT-JT"] # deprecated ..
from transformers import AutoTokenizer
def get_tokenizer(conf):
@@ -16,6 +15,10 @@ def get_tokenizer(conf):
if "galactica" in conf.model_name:
tokenizer.add_special_tokens({"pad_token": "<pad>", "eos_token": "</s>"})
elif "GPT-JT" in conf.model_name:
tokenizer.add_special_tokens({"pad_token": tokenizer.eos_token, "sep_token": "<|extratoken_100|>"})
elif "codegen" in conf.model_name:
tokenizer.add_special_tokens({"pad_token": "<|endoftext|>", "sep_token": "<|endoftext|>"})
additional_special_tokens = (
[]
@@ -30,13 +33,7 @@ def get_tokenizer(conf):
def get_model(conf, tokenizer):
if not any([x in conf.model_name for x in SUPPORTED_MODELS]):
raise ValueError(
f"Model {conf.model_name} not supported. Supported models: {SUPPORTED_MODELS}. "
"To include more make sure the masking is dne correctly... (decoder only supported for now)"
)
model = AutoModelForCausalLM.from_pretrained(conf.model_name, cache_dir=conf.cache_dir)
model = get_specific_model(conf.model_name, conf.cache_dir, conf.quantization)
if len(tokenizer) != model.get_input_embeddings().num_embeddings:
assert not conf.freeze_layer, "Cannot change the number of embeddings if the model is frozen."
@@ -95,31 +92,3 @@ def train_val_dataset(dataset, val_split=0.2):
list(range(len(dataset))), test_size=val_split, random_state=666, shuffle=True
)
return Subset(dataset, train_idx), Subset(dataset, val_idx)
def freeze_top_n_layers(model, target_layers):
# its possible we can simply detect which module is a ModuleList
# and simply freeze the module without doing string parsing
for name, param in model.named_parameters():
if "embed" in name:
param.requires_grad = False
elif ".layer" in name or ".h." in name:
tokens = name.split(".")
layer_ = None
for token in tokens:
if token.isdigit():
layer_ = int(token)
break
if layer_ is not None and layer_ < target_layers:
# print('freeze ', layer_, name)
param.requires_grad = False
return model
if __name__ == "__main__":
from transformers import AutoModelForSequenceClassification
model = AutoModelForSequenceClassification.from_pretrained("bigscience/bloomz-560m")
freeze_top_n_layers(model, 10)
print(model.state_dict().keys())
@@ -0,0 +1,278 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# First, we'll import the necessary libraries\n",
"import os\n",
"import subprocess"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"## only run from OpenAssistant not if you alread cloned the github\n",
"# Next, we'll define the function that will clone the OpenBugger repository and install it.\n",
"def install_openbugger():\n",
" # First, we'll get the current working directory. This is where the repository will be cloned to.\n",
" cwd = os.getcwd()\n",
"\n",
" # Next, we'll use Git to clone the repository.\n",
" subprocess.run([\"git\", \"clone\", \"https://github.com/furlat/OpenBugger\", cwd + \"/OpenBugger\"])\n",
"\n",
" # Now, we'll use pip to install the package from the local repository.\n",
" subprocess.run([\"python3\", \"-m\", \"pip\", \"install\", \"--editable\", cwd + \"/OpenBugger\"])\n",
"\n",
"\n",
"# Now, we'll call the function to install OpenBugger.\n",
"install_openbugger()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Finally, we'll import SyntaxBug and LogicBug.\n",
"from syntax.syntax_injector import SyntaxBug\n",
"from logic.logic_injector import LogicBug"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# setup syntax bug\n",
"syntax_bug = SyntaxBug()\n",
"\n",
"# Simple script\n",
"simple_script = \"\"\"\n",
"def greet(name):\n",
" print(\"Hello, \" + name)\n",
"\n",
"greet(\"Bob\")\n",
"\"\"\"\n",
"\n",
"# The simple script can be modified using the \"easy\" injection method because it only contains simple syntax and does not have any nested code blocks. This means that there are fewer characters (e.g. quotes, brackets, braces, parenthesis) that could be the target of syntax errors, and the \"easy\" injection method, which only injects errors that involve replacing or removing a single character, is sufficient to modify the script.\n",
"print(simple_script)\n",
"# Inject easy syntax errors into the simple script\n",
"\n",
"modified_simple_script, errors, counter = syntax_bug.inject(simple_script, \"easy\", 1)\n",
"print(\"Modified version Easy\", errors, counter)\n",
"print(modified_simple_script)\n",
"print(\"are they the same?\", simple_script == modified_simple_script)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Medium script\n",
"medium_script = \"\"\"\n",
"def greet(name):\n",
" print(\"Hello, \" + name)\n",
" \n",
"def greet_all(names):\n",
" for name in names:\n",
" greet(name)\n",
" \n",
"greet_all([\"Bob\", \"Alice\", \"Eve\"])\n",
"\"\"\"\n",
"# The medium script can be modified using the \"medium\" injection method because it contains a nested code block (the for loop in the `greet_all` function). This means that there are more characters (e.g. quotes, brackets, braces, parenthesis) that could be the target of syntax errors, and the \"medium\" injection method, which injects errors that involve replacing, removing, or adding a single character, is sufficient to modify the script.\n",
"print(medium_script)\n",
"# Inject medium syntax errors into the medium script\n",
"modified_medium_script, errors, counter = syntax_bug.inject(medium_script, \"medium\", 3)\n",
"print(\"Modified version Medium\", errors, counter)\n",
"print(modified_medium_script)\n",
"print(\"are they the same?\", medium_script == modified_medium_script)\n",
"# Hard script\n",
"hard_script = \"\"\"\n",
"class Greeting:\n",
" def __init__(self, greeting):\n",
" self.greeting = greeting\n",
" \n",
" def greet(self, name):\n",
" print(self.greeting + \", \" + name)\n",
" \n",
"greeting = Greeting(\"Hello\")\n",
"greeting.greet(\"Bob\")\n",
"\"\"\""
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Hard script\n",
"hard_script = \"\"\"\n",
"class Greeting:\n",
" def __init__(self, greeting):\n",
" self.greeting = greeting\n",
" \n",
" def greet(self, name):\n",
" print(self.greeting + \", \" + name)\n",
" \n",
"greeting = Greeting(\"Hello\")\n",
"greeting.greet(\"Bob\")\n",
"\"\"\"\n",
"\n",
"# The hard script can be modified using the \"hard\" injection method because it contains multiple nested code blocks (the `__init__` and `greet` methods in the `Greeting` class). This means that there are even more characters (e.g. quotes, brackets, braces, parenthesis) that could be the target of syntax errors, and the \"hard\" injection method, which injects errors that involve replacing, removing, adding, or swapping characters, is sufficient to modify the script.\n",
"print(hard_script)\n",
"# Inject hard syntax errors into the hard script\n",
"modified_hard_script, errors, counter = syntax_bug.inject(hard_script, \"hard\", 3)\n",
"print(\"Modified version Hard\", errors, counter)\n",
"print(modified_hard_script)\n",
"print(\"are they the same?\", hard_script == modified_hard_script)"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we switch to testing the LogicBugs and show how to bug a function that is defined in the script without already having the string."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import inspect\n",
"import random\n",
"\n",
"# Simple example script\n",
"\n",
"\n",
"def simple_script():\n",
" # Choose two random integers\n",
" num1 = random.randint(0, 10)\n",
" num2 = random.randint(0, 10)\n",
"\n",
" # Compare the two numbers and print a message based on their relation\n",
" if num1 > num2:\n",
" print(\"num1 is greater than num2\")\n",
" elif num1 < num2:\n",
" print(\"num1 is less than num2\")\n",
" else:\n",
" print(\"num1 is equal to num2\")\n",
"\n",
"\n",
"# Medium example script\n",
"def medium_script():\n",
" # Choose a random integer and assign it to a variable\n",
" num = random.randint(0, 10)\n",
"\n",
" # Use a loop to print all numbers from 0 to the chosen integer\n",
" for i in range(num):\n",
" print(i)\n",
"\n",
"\n",
"# Hard example script\n",
"def hard_script():\n",
" # Choose a random integer and assign it to a variable\n",
" num = random.randint(0, 10)\n",
"\n",
" # Use a loop to print the square of all numbers from 0 to the chosen integer\n",
" for i in range(num):\n",
" print(i**2)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# create an instance of the LogicBug class\n",
"logic_bug = LogicBug()\n",
"\n",
"# get the source code of the simple_script function as a string\n",
"simple_script_str = inspect.getsource(simple_script)\n",
"print(\"Simple\", simple_script_str)\n",
"# inject a logic error into the simple_script function\n",
"modified_simple_script, error, counter = logic_bug.inject(simple_script_str, \"easy\", num_errors=3)\n",
"print(\"Modified version Simple\", error, counter)\n",
"# print the modified simple_script function\n",
"print(modified_simple_script)\n",
"print(\"are they the same?\", simple_script_str == modified_simple_script)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# get the source code of the medium_script function as a string\n",
"medium_script_str = inspect.getsource(medium_script)\n",
"print(\"Medium\", medium_script_str)\n",
"# inject a logic error into the medium_script function\n",
"modified_medium_script, error, counter = logic_bug.inject(medium_script_str, \"medium\", num_errors=3)\n",
"\n",
"# print the modified medium_script function\n",
"print(\"Modified version Medium\", error, counter)\n",
"print(modified_medium_script)\n",
"print(\"are they the same?\", medium_script_str == modified_medium_script)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# get the source code of the hard_script function as a string\n",
"hard_script_str = inspect.getsource(hard_script)\n",
"print(\"Hard\", hard_script_str)\n",
"# inject a logic error into the hard_script function\n",
"modified_hard_script, error, counter = logic_bug.inject(hard_script_str, \"hard\", num_errors=1)\n",
"print(\"Modified version Hard\", error, counter)\n",
"# print the modified hard_script function\n",
"print(modified_hard_script)\n",
"print(\"are they the same?\", hard_script_str == modified_hard_script)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6 (tags/v3.10.6:9c7b4bd, Aug 1 2022, 21:53:49) [MSC v.1932 64 bit (AMD64)]"
},
"orig_nbformat": 4,
"vscode": {
"interpreter": {
"hash": "ceba285e8b4e6478fe8ad229bc63940a90ad5cf3d143521e7c38823a2e915b21"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}
@@ -0,0 +1,92 @@
# OpenBuggerNotebook
https://github.com/furlat/OpenBugger/blob/main/README.md is a Python package
that allows you to inject syntax and logic errors into your code. This can be
useful for testing the robustness of your code or for creating test cases for
debugging exercises or for training an assistant to debug code.
The Python notebook openbugger_example.ipynb does the following:
1. Imports the necessary libraries to install OpenBugger in the notebookdirecory
(os and subprocess).
2. Defines a function, install_openbugger, which clones the OpenBugger
repository from GitHub and installs it using pip.
3. Calls the install_openbugger function to install OpenBugger.
4. Imports the SyntaxBug and LogicBug classes from the syntax_injector and
logic_injector modules, respectively.
5. Creates an instance of the SyntaxBug class and assigns it to the syntax_bug
variable.
6. Defines three scripts: a simple script, a medium script, and a hard script.
7. Calls the inject method on the simple script, passing in the string "easy" as
the second argument and the integer 1 as the third argument. This will inject
easy syntax errors into the script. The *item *item modified script, a list
of the injected errors, and the number of errors injected are returned and
assigned to variables.
8. Prints the original and modified versions of the simple script, as well as
the list of injected errors and the number of errors injected. 10 Repeats
steps 7 and 8 for the medium and hard scripts, but with the "medium" and
"hard" injection methods and different numbers of errors to inject.
General Usage To use OpenBugger, import the SintaxBug or LogicBug classes from
the openbugger module and use them to inject a bug with a call to the inject().
The injector will return the modified script with the injected bug.
```
from syntax.syntax_injector import SyntaxInjector, SyntaxBug
syntax_bug = SyntaxBug()
# Simple script
simple_script = """
def greet(name):
print("Hello, " + name)
greet("Bob")
"""
print(simple_script)
```
The simple script can be modified using the "easy" injection method because it
only contains simple syntax and does not have any nested code blocks. This means
that there are fewer characters (e.g. quotes, brackets, braces, parenthesis)
that could be the target of syntax errors, and the "easy" injection method,
which only injects errors that involve replacing or removing a single character,
is sufficient to modify the script.
```
# Inject easy syntax errors into the simple script
modified_simple_script, errors, counter = syntax_bug.inject(simple_script, "easy", 1)
print("Modified version Easy",errors,counter)
print(modified_simple_script)
```
Or for higher severity and logic error by directly transforming a Python class
into text
```
import inspect
import random
from logic.logic_injector import LogicBug
# Medium example script
def medium_script():
# Choose a random integer and assign it to a variable
num = random.randint(0, 10)
# Use a loop to print all numbers from 0 to the chosen integer
for i in range(num):
print(i)
# create an instance of the LogicBug class
logic_bug = LogicBug()
# get the source code of the medium_script function as a string
medium_script_str = inspect.getsource(medium_script)
print("Medium",medium_script_str)
# inject a logic error into the medium_script function
modified_medium_script, error, counter = logic_bug.inject(medium_script_str,"medium",num_errors=3)
```
+6
View File
@@ -24,6 +24,9 @@ class TaskType(str, enum.Enum):
rank_initial_prompts = "rank_initial_prompts"
rank_prompter_replies = "rank_prompter_replies"
rank_assistant_replies = "rank_assistant_replies"
label_initial_prompt = "label_initial_prompt"
label_assistant_reply = "label_assistant_reply"
label_prompter_reply = "label_prompter_reply"
done = "task_done"
@@ -56,6 +59,9 @@ class OasstApiClient:
TaskType.rank_initial_prompts: protocol_schema.RankInitialPromptsTask,
TaskType.rank_prompter_replies: protocol_schema.RankPrompterRepliesTask,
TaskType.rank_assistant_replies: protocol_schema.RankAssistantRepliesTask,
TaskType.label_initial_prompt: protocol_schema.LabelInitialPromptTask,
TaskType.label_prompter_reply: protocol_schema.LabelPrompterReplyTask,
TaskType.label_assistant_reply: protocol_schema.LabelAssistantReplyTask,
TaskType.done: protocol_schema.TaskDone,
}
+49 -9
View File
@@ -18,6 +18,9 @@ class TaskRequestType(str, enum.Enum):
rank_initial_prompts = "rank_initial_prompts"
rank_prompter_replies = "rank_prompter_replies"
rank_assistant_replies = "rank_assistant_replies"
label_initial_prompt = "label_initial_prompt"
label_assistant_reply = "label_assistant_reply"
label_prompter_reply = "label_prompter_reply"
class User(BaseModel):
@@ -169,6 +172,37 @@ class RankAssistantRepliesTask(RankConversationRepliesTask):
type: Literal["rank_assistant_replies"] = "rank_assistant_replies"
class LabelInitialPromptTask(Task):
"""A task to label an initial prompt."""
type: Literal["label_initial_prompt"] = "label_initial_prompt"
message_id: UUID
prompt: str
valid_labels: list[str]
class LabelConversationReplyTask(Task):
"""A task to label a reply to a conversation."""
type: Literal["label_conversation_reply"] = "label_conversation_reply"
conversation: Conversation # the conversation so far
message_id: UUID
reply: str
valid_labels: list[str]
class LabelPrompterReplyTask(LabelConversationReplyTask):
"""A task to label a prompter reply to a conversation."""
type: Literal["label_prompter_reply"] = "label_prompter_reply"
class LabelAssistantReplyTask(LabelConversationReplyTask):
"""A task to label an assistant reply to a conversation."""
type: Literal["label_assistant_reply"] = "label_assistant_reply"
class TaskDone(Task):
"""Signals to the frontend that the task is done."""
@@ -187,6 +221,10 @@ AnyTask = Union[
RankConversationRepliesTask,
RankPrompterRepliesTask,
RankAssistantRepliesTask,
LabelInitialPromptTask,
LabelConversationReplyTask,
LabelPrompterReplyTask,
LabelAssistantReplyTask,
]
@@ -222,13 +260,6 @@ class MessageRanking(Interaction):
ranking: conlist(item_type=int, min_items=1)
AnyInteraction = Union[
TextReplyToMessage,
MessageRating,
MessageRanking,
]
class TextLabel(str, enum.Enum):
"""A label for a piece of text."""
@@ -256,12 +287,13 @@ class TextLabel(str, enum.Enum):
slang = "slang"
class TextLabels(BaseModel):
class TextLabels(Interaction):
"""A set of labels for a piece of text."""
type: Literal["text_labels"] = "text_labels"
text: str
labels: dict[TextLabel, float]
message_id: str | None = None
message_id: UUID
@property
def has_message_id(self) -> bool:
@@ -277,6 +309,14 @@ class TextLabels(BaseModel):
return v
AnyInteraction = Union[
TextReplyToMessage,
MessageRating,
MessageRanking,
TextLabels,
]
class SystemStats(BaseModel):
all: int = 0
active: int = 0
+54
View File
@@ -203,6 +203,60 @@ def main(backend_url: str = "http://127.0.0.1:8080", api_key: str = "DUMMY_KEY")
)
tasks.append(new_task)
case "label_initial_prompt":
typer.echo("Label the following prompt:")
typer.echo(task["prompt"])
# acknowledge task
message_id = _random_message_id()
_post(f"/api/v1/tasks/{task['id']}/ack", {"message_id": message_id})
valid_labels = task["valid_labels"]
labels_str: str = typer.prompt("Enter labels, separated by commas")
labels = labels_str.lower().replace(" ", "").split(",")
labels_dict = {label: "1" if label in labels else "0" for label in valid_labels}
# send ranking
new_task = _post(
"/api/v1/tasks/interaction",
{
"type": "text_labels",
"message_id": task["message_id"],
"text": task["prompt"],
"labels": labels_dict,
"user": USER,
},
)
tasks.append(new_task)
case "label_prompter_reply" | "label_assistant_reply":
typer.echo("Here is the conversation so far:")
for message in task["conversation"]["messages"]:
typer.echo(_render_message(message))
typer.echo("Label the following reply:")
typer.echo(task["reply"])
# acknowledge task
message_id = _random_message_id()
_post(f"/api/v1/tasks/{task['id']}/ack", {"message_id": message_id})
valid_labels = task["valid_labels"]
labels_str: str = typer.prompt("Enter labels, separated by commas")
labels = labels_str.lower().replace(" ", "").split(",")
labels_dict = {label: "1" if label in labels else "0" for label in valid_labels}
# send ranking
new_task = _post(
"/api/v1/tasks/interaction",
{
"type": "text_labels",
"message_id": task["message_id"],
"text": task["prompt"],
"labels": labels_dict,
"user": USER,
},
)
tasks.append(new_task)
case "task_done":
typer.echo("Task done!")
case _:
@@ -1,4 +1,4 @@
import { OasstApiClient } from "src/lib/oasst_api_client";
import { OasstApiClient, OasstError } from "src/lib/oasst_api_client";
describe("Contract test for Oasst API", function () {
// Assumes this is running the mock server.
@@ -44,7 +44,68 @@ describe("Contract test for Oasst API", function () {
).to.be.not.null;
});
// TODO(#354): Add test for 204
// TODO(#354): Add test for parsing >=300, throwing an OasstError
// TODO(#354): Add test for parsing >=300, throwing a generic error
// Note: Below here are unittests for OasstApiClient, not contract tests. They still fit here because
// the other contract tests are also testing the OasstApiClient.
const callApiMethod = () => {
return oasstApiClient.ackTask("", "");
};
it("should return null for a 204 response", async () => {
const mockFetch = cy.stub(global, "fetch").resolves({
status: 204,
});
const result = await callApiMethod();
assert.isNull(result);
mockFetch.restore();
});
it("should throw an OasstError with data from the response for a non-2XX response with a valid OasstError response", async () => {
const mockFetch = cy.stub(global, "fetch").resolves({
status: 400,
text: () =>
// Note: this is vulnerable to interface changes in the Oasst API.
// The python tests use a Pydantic model to ensure this object is always valid,
// but we don't have that here.
// This could be a case for generating Zod schemas from OpenAPI.
Promise.resolve(
JSON.stringify({
message: "error message",
error_code: 1000,
})
),
});
try {
await callApiMethod();
assert.fail();
} catch (error) {
assert.instanceOf(error, OasstError);
if (error instanceof OasstError) {
assert.equal(error.errorCode, 1000);
assert.equal(error.message, "error message");
assert.equal(error.httpStatusCode, 400);
}
}
mockFetch.restore();
});
it("should throw a generic OasstError with the text from the response for a non-2XX response with an unknown format", async () => {
const mockFetch = cy.stub(global, "fetch").resolves({
status: 400,
text: () => Promise.resolve("error message"),
});
try {
await callApiMethod();
assert.fail();
} catch (error) {
assert.instanceOf(error, OasstError);
assert.equal(error.message, "error message");
}
mockFetch.restore();
});
});
+29 -616
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -50,7 +50,6 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-icons": "^4.7.1",
"sharp": "0.31.2",
"swr": "^2.0.0",
"tailwindcss": "^3.2.4",
"use-debounce": "^9.0.2"
@@ -1,3 +1,2 @@
export { LeaderboardTable } from "./LeaderboardTable";
export { SideMenu } from "./SideMenu";
export { TaskOption } from "./TaskOption";
+14 -2
View File
@@ -17,12 +17,15 @@ import {
Spacer,
Tooltip,
useBoolean,
useColorMode,
useColorModeValue,
useId,
} from "@chakra-ui/react";
import { FlagIcon, QuestionMarkCircleIcon } from "@heroicons/react/20/solid";
import { useState } from "react";
import poster from "src/lib/poster";
import useSWRMutation from "swr/mutation";
import { colors } from "styles/Theme/colors";
export const FlaggableElement = (props) => {
const [isEditing, setIsEditing] = useBoolean();
@@ -102,7 +105,10 @@ export const FlaggableElement = (props) => {
<Button
isDisabled={!checkboxValues.some(Boolean)}
onClick={submitResponse}
className="bg-indigo-600 text-black hover:bg-indigo-700"
className={`bg-indigo-600 text-${useColorModeValue(
colors.light.text,
colors.dark.text
)} hover:bg-indigo-700`}
>
Report
</Button>
@@ -133,6 +139,12 @@ function FlagCheckbox(props: {
}
const id = useId();
const { colorMode } = useColorMode();
const labelTextClass =
colorMode === "light"
? `text-${colors.light.text} hover:text-blue-700 float-left`
: `text-${colors.dark.text} hover:text-blue-400 float-left`;
return (
<Flex gap={1}>
@@ -143,7 +155,7 @@ function FlagCheckbox(props: {
}}
/>
<label className="text-sm form-check-label" htmlFor={id}>
<span className="text-gray-800 hover:text-blue-700 float-left">{props.option.labelText}</span>
<span className={labelTextClass}>{props.option.labelText}</span>
{AdditionalExplanation}
</label>
<Spacer />
+20 -1
View File
@@ -1,9 +1,11 @@
// https://nextjs.org/docs/basic-features/layouts
import type { NextPage } from "next";
import { FiLayout, FiMessageSquare } from "react-icons/fi";
import { Header } from "src/components/Header";
import { Footer } from "./Footer";
import { SideMenuLayout } from "./SideMenuLayout";
export type NextPageWithLayout<P = unknown, IP = P> = NextPage<P, IP> & {
getLayout?: (page: React.ReactElement) => React.ReactNode;
@@ -28,7 +30,24 @@ export const getTransparentHeaderLayout = (page: React.ReactElement) => (
export const getDashboardLayout = (page: React.ReactElement) => (
<div className="grid grid-rows-[min-content_1fr_min-content] h-full justify-items-stretch">
<Header transparent={true} />
{page}
<SideMenuLayout
menuButtonOptions={[
{
label: "Dashboard",
pathname: "/dashboard",
desc: "Dashboard Home",
icon: FiLayout,
},
{
label: "Messages",
pathname: "/messages",
desc: "Messages Dashboard",
icon: FiMessageSquare,
},
]}
>
{page}
</SideMenuLayout>
</div>
);
@@ -1,37 +1,23 @@
import { Box, Button, Link, Text, Tooltip, useColorMode } from "@chakra-ui/react";
import { useRouter } from "next/router";
import { FiLayout, FiSun, FiMessageSquare } from "react-icons/fi";
import { FiSun } from "react-icons/fi";
import { IconType } from "react-icons/lib";
import { colors } from "styles/Theme/colors";
export function SideMenu() {
export interface MenuButtonOption {
label: string;
pathname: string;
desc: string;
icon: IconType;
}
export interface SideMenuProps {
buttonOptions: MenuButtonOption[];
}
export function SideMenu(props: SideMenuProps) {
const router = useRouter();
const { colorMode, toggleColorMode } = useColorMode();
const buttonOptions = [
{
label: "Dashboard",
pathname: "/dashboard",
desc: "Dashboard Home",
icon: FiLayout,
},
{
label: "Messages",
pathname: "/messages",
desc: "Messages Dashboard",
icon: FiMessageSquare,
},
// {
// label: "Leaderboard",
// pathname: "#",
// desc: "Public Leaderboard",
// icon: FiAward,
// },
// {
// label: "Stats",
// pathname: "#",
// desc: "User Statistics",
// icon: FiBarChart2,
// },
];
return (
<main className="sticky top-0 sm:h-full">
@@ -43,7 +29,7 @@ export function SideMenu() {
className="grid grid-cols-4 gap-2 sm:flex sm:flex-col sm:justify-between p-4 h-full"
>
<nav className="grid grid-cols-3 col-span-3 sm:flex sm:flex-col gap-2">
{buttonOptions.map((item, itemIndex) => (
{props.buttonOptions.map((item, itemIndex) => (
<Tooltip
key={itemIndex}
fontFamily="inter"
+23
View File
@@ -0,0 +1,23 @@
import { Box, useColorMode } from "@chakra-ui/react";
import { colors } from "styles/Theme/colors";
import { SideMenu, MenuButtonOption } from "src/components/SideMenu";
interface SideMenuLayoutProps {
menuButtonOptions: MenuButtonOption[];
children: React.ReactNode;
}
export const SideMenuLayout = (props: SideMenuLayoutProps) => {
const { colorMode } = useColorMode();
return (
<Box backgroundColor={colorMode === "light" ? colors.light.bg : colors.dark.bg} className="sm:overflow-hidden">
<Box className="sm:flex h-full gap-6">
<Box className="p-6 sm:pr-0">
<SideMenu buttonOptions={props.menuButtonOptions} />
</Box>
<Box className="flex flex-col overflow-auto p-6 sm:pl-0 gap-14">{props.children}</Box>
</Box>
</Box>
);
};
@@ -0,0 +1,35 @@
import { Progress, Stack, Textarea, TextareaProps } from "@chakra-ui/react";
interface TrackedTextboxProps {
text: string;
thresholds: {
low: number;
medium: number;
goal: number;
};
textareaProps?: TextareaProps;
onTextChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
}
export const TrackedTextarea = (props: TrackedTextboxProps) => {
const wordCount = props.text.split(" ").length - 1;
let progressColor: string;
switch (true) {
case wordCount < props.thresholds.low:
progressColor = "red";
break;
case wordCount < props.thresholds.medium:
progressColor = "yellow";
break;
default:
progressColor = "green";
}
return (
<Stack direction={"column"}>
<Textarea data-cy="reply" value={props.text} onChange={props.onTextChange} {...props.textareaProps} onCapture />
<Progress size={"md"} rounded={"md"} value={wordCount} colorScheme={progressColor} max={props.thresholds.goal} />
</Stack>
);
};
+4 -3
View File
@@ -5,7 +5,7 @@ declare global {
var oasstApiClient: OasstApiClient | undefined;
}
class OasstError {
export class OasstError {
message: string;
errorCode: number;
httpStatusCode: number;
@@ -36,12 +36,13 @@ export class OasstApiClient {
if (resp.status >= 300) {
const errorText = await resp.text();
let error: any;
try {
const error = JSON.parse(errorText);
throw new OasstError(error.message, error.error_code, resp.status);
error = JSON.parse(errorText);
} catch (e) {
throw new OasstError(errorText, 0, resp.status);
}
throw new OasstError(error.message, error.error_code, resp.status);
}
return await resp.json();
+19 -7
View File
@@ -1,9 +1,10 @@
import { Container, Textarea } from "@chakra-ui/react";
import { Container } from "@chakra-ui/react";
import { useColorMode } from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { LoadingScreen } from "src/components/Loading/LoadingScreen";
import { Messages } from "src/components/Messages";
import { TaskControls } from "src/components/Survey/TaskControls";
import { TrackedTextarea } from "src/components/Survey/TrackedTextarea";
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
import fetcher from "src/lib/fetcher";
import poster from "src/lib/poster";
@@ -12,8 +13,7 @@ import useSWRMutation from "swr/mutation";
const AssistantReply = () => {
const [tasks, setTasks] = useState([]);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [inputText, setInputText] = useState("");
const { isLoading, mutate } = useSWRImmutable("/api/new_task/assistant_reply ", fetcher, {
onSuccess: (data) => {
@@ -35,7 +35,7 @@ const AssistantReply = () => {
});
const submitResponse = (task: { id: string }) => {
const text = inputRef.current.value.trim();
const text = inputText.trim();
trigger({
id: task.id,
update_type: "text_reply_to_message",
@@ -46,10 +46,14 @@ const AssistantReply = () => {
};
const fetchNextTask = () => {
inputRef.current.value = "";
setInputText("");
mutate();
};
const textChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputText(event.target.value);
};
const { colorMode } = useColorMode();
const mainBgClasses = colorMode === "light" ? "bg-slate-300 text-gray-800" : "bg-slate-900 text-white";
@@ -71,7 +75,15 @@ const AssistantReply = () => {
<p className="text-lg py-1">Given the following conversation, provide an adequate reply</p>
<Messages messages={task.conversation.messages} post_id={task.id} />
</>
<Textarea name="reply" data-cy="reply" placeholder="Reply..." ref={inputRef} />
<>
<h5 className="text-lg font-semibold">Provide the assistant`s reply</h5>
<TrackedTextarea
text={inputText}
onTextChange={textChangeHandler}
thresholds={{ low: 20, medium: 40, goal: 50 }}
textareaProps={{ placeholder: "Reply..." }}
/>
</>
</TwoColumnsWithCards>
<TaskControls tasks={tasks} onSubmitResponse={submitResponse} onSkip={fetchNextTask} />
+19 -9
View File
@@ -1,8 +1,9 @@
import { Container, Textarea } from "@chakra-ui/react";
import { Container } from "@chakra-ui/react";
import { useColorMode } from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { LoadingScreen } from "src/components/Loading/LoadingScreen";
import { TaskControls } from "src/components/Survey/TaskControls";
import { TrackedTextarea } from "src/components/Survey/TrackedTextarea";
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
import fetcher from "src/lib/fetcher";
import poster from "src/lib/poster";
@@ -11,8 +12,7 @@ import useSWRMutation from "swr/mutation";
const InitialPrompt = () => {
const [tasks, setTasks] = useState([]);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [inputText, setInputText] = useState("");
const { isLoading, mutate } = useSWRImmutable("/api/new_task/initial_prompt ", fetcher, {
onSuccess: (data) => {
@@ -34,7 +34,7 @@ const InitialPrompt = () => {
}, [tasks]);
const submitResponse = (task: { id: string }) => {
const text = inputRef.current.value.trim();
const text = inputText.trim();
trigger({
id: task.id,
update_type: "text_reply_to_message",
@@ -45,10 +45,14 @@ const InitialPrompt = () => {
};
const fetchNextTask = () => {
inputRef.current.value = "";
setInputText("");
mutate();
};
const textChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputText(event.target.value);
};
const { colorMode } = useColorMode();
const mainBgClasses = colorMode === "light" ? "bg-slate-300 text-gray-800" : "bg-slate-900 text-white";
@@ -60,8 +64,6 @@ const InitialPrompt = () => {
return <Container className="p-6 text-center text-gray-800">No tasks found...</Container>;
}
const task = tasks[0].task;
return (
<div className={`p-12 ${mainBgClasses}`}>
<TwoColumnsWithCards>
@@ -69,7 +71,15 @@ const InitialPrompt = () => {
<h5 className="text-lg font-semibold">Start a conversation</h5>
<p className="text-lg py-1">Create an initial message to send to the assistant</p>
</>
<Textarea name="reply" data-cy="reply" placeholder="Question, task, greeting or similar..." ref={inputRef} />
<>
<h5 className="text-lg font-semibold">Provide the initial prompt</h5>
<TrackedTextarea
text={inputText}
onTextChange={textChangeHandler}
thresholds={{ low: 20, medium: 40, goal: 50 }}
textareaProps={{ placeholder: "Question, task, greeting or similar..." }}
/>
</>
</TwoColumnsWithCards>
<TaskControls tasks={tasks} onSubmitResponse={submitResponse} onSkip={fetchNextTask} />
+18 -7
View File
@@ -1,8 +1,8 @@
import { Textarea } from "@chakra-ui/react";
import { useColorMode } from "@chakra-ui/react";
import { useRef, useState } from "react";
import { useState } from "react";
import { LoadingScreen } from "src/components/Loading/LoadingScreen";
import { TaskControls } from "src/components/Survey/TaskControls";
import { TrackedTextarea } from "src/components/Survey/TrackedTextarea";
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
import fetcher from "src/lib/fetcher";
import poster from "src/lib/poster";
@@ -13,8 +13,7 @@ const SummarizeStory = () => {
// Use an array of tasks that record the sequence of steps until a task is
// deemed complete.
const [tasks, setTasks] = useState([]);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [inputText, setInputText] = useState("");
// Fetch the very fist task. We can ignore everything except isLoading
// because the onSuccess handler will update `tasks` when ready.
@@ -38,7 +37,7 @@ const SummarizeStory = () => {
// Trigger a mutation that updates the current task. We should probably
// signal somewhere that this interaction is being processed.
const submitResponse = (task: { id: string }) => {
const text = inputRef.current.value.trim();
const text = inputText.trim();
trigger({
id: task.id,
update_type: "text_reply_to_message",
@@ -49,10 +48,14 @@ const SummarizeStory = () => {
};
const fetchNextTask = () => {
inputRef.current.value = "";
setInputText("");
mutate();
};
const textChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputText(event.target.value);
};
const { colorMode } = useColorMode();
const mainBgClasses = colorMode === "light" ? "bg-slate-300 text-gray-800" : "bg-slate-900 text-white";
@@ -73,7 +76,15 @@ const SummarizeStory = () => {
<p className="text-lg py-1">Summarize the following story</p>
<div className="bg-slate-800 p-6 rounded-xl text-white whitespace-pre-wrap">{tasks[0].task.story}</div>
</>
<Textarea name="summary" placeholder="Summary" ref={inputRef} />
<>
<h5 className="text-lg font-semibold">Provide the assistant`s reply</h5>
<TrackedTextarea
text={inputText}
onTextChange={textChangeHandler}
thresholds={{ low: 20, medium: 40, goal: 50 }}
textareaProps={{ placeholder: "Summary" }}
/>
</>
</TwoColumnsWithCards>
<TaskControls tasks={tasks} onSubmitResponse={submitResponse} onSkip={fetchNextTask} />
+18 -7
View File
@@ -1,9 +1,9 @@
import { Textarea } from "@chakra-ui/react";
import { useColorMode } from "@chakra-ui/react";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { LoadingScreen } from "src/components/Loading/LoadingScreen";
import { Messages } from "src/components/Messages";
import { TaskControls } from "src/components/Survey/TaskControls";
import { TrackedTextarea } from "src/components/Survey/TrackedTextarea";
import { TwoColumnsWithCards } from "src/components/Survey/TwoColumnsWithCards";
import fetcher from "src/lib/fetcher";
import poster from "src/lib/poster";
@@ -12,8 +12,7 @@ import useSWRMutation from "swr/mutation";
const UserReply = () => {
const [tasks, setTasks] = useState([]);
const inputRef = useRef<HTMLTextAreaElement>(null);
const [inputText, setInputText] = useState("");
const { isLoading, mutate } = useSWRImmutable("/api/new_task/prompter_reply", fetcher, {
onSuccess: (data) => {
@@ -35,7 +34,7 @@ const UserReply = () => {
});
const submitResponse = (task: { id: string }) => {
const text = inputRef.current.value.trim();
const text = inputText.trim();
trigger({
id: task.id,
update_type: "text_reply_to_message",
@@ -46,10 +45,14 @@ const UserReply = () => {
};
const fetchNextTask = () => {
inputRef.current.value = "";
setInputText("");
mutate();
};
const textChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setInputText(event.target.value);
};
const { colorMode } = useColorMode();
const mainBgClasses = colorMode === "light" ? "bg-slate-300 text-gray-800" : "bg-slate-900 text-white";
@@ -78,7 +81,15 @@ const UserReply = () => {
<Messages messages={task.conversation.messages} post_id={task.id} />
{task.hint && <p className="text-lg py-1">Hint: {task.hint}</p>}
</>
<Textarea name="reply" data-cy="reply" placeholder="Reply..." ref={inputRef} />
<>
<h5 className="text-lg font-semibold">Provide the user`s reply</h5>
<TrackedTextarea
text={inputText}
onTextChange={textChangeHandler}
thresholds={{ low: 20, medium: 40, goal: 50 }}
textareaProps={{ placeholder: "Reply..." }}
/>
</>
</TwoColumnsWithCards>
<TaskControls tasks={tasks} onSubmitResponse={submitResponse} onSkip={fetchNextTask} />
+3 -15
View File
@@ -1,29 +1,17 @@
import { Box, useColorMode } from "@chakra-ui/react";
import Head from "next/head";
import { getDashboardLayout } from "src/components/Layout";
import { LeaderboardTable, SideMenu, TaskOption } from "src/components/Dashboard";
import { colors } from "styles/Theme/colors";
import { LeaderboardTable, TaskOption } from "src/components/Dashboard";
const Dashboard = () => {
const { colorMode } = useColorMode();
return (
<>
<Head>
<title>Dashboard - Open Assistant</title>
<meta name="description" content="Chat with Open Assistant and provide feedback." />
</Head>
<Box backgroundColor={colorMode === "light" ? colors.light.bg : colors.dark.bg} className="sm:overflow-hidden">
<Box className="sm:flex h-full gap-6">
<Box className="p-6 sm:pr-0">
<SideMenu />
</Box>
<Box className="flex flex-col overflow-auto p-6 sm:pl-0 gap-14">
<TaskOption />
<LeaderboardTable />
</Box>
</Box>
</Box>
<TaskOption />
<LeaderboardTable />
</>
);
};
+28 -41
View File
@@ -2,15 +2,11 @@ import { Box, CircularProgress, SimpleGrid, Text, useColorModeValue } from "@cha
import Head from "next/head";
import { useState } from "react";
import useSWRImmutable from "swr/immutable";
import fetcher from "src/lib/fetcher";
import { SideMenu } from "src/components/Dashboard";
import { MessageTable } from "src/components/Messages/MessageTable";
import { getDashboardLayout } from "src/components/Layout";
import { colors } from "styles/Theme/colors";
const MessagesDashboard = () => {
const bgColor = useColorModeValue(colors.light.bg, colors.dark.bg);
const boxBgColor = useColorModeValue("white", "gray.700");
const boxAccentColor = useColorModeValue("gray.200", "gray.900");
@@ -35,45 +31,36 @@ const MessagesDashboard = () => {
<title>Messages - Open Assistant</title>
<meta name="description" content="Chat with Open Assistant and provide feedback." />
</Head>
<Box backgroundColor={bgColor} className="sm:overflow-hidden">
<Box className="sm:flex h-full gap-6">
<Box className="p-6 sm:pr-0">
<SideMenu />
</Box>
<Box className="flex flex-col overflow-auto p-6 sm:pl-0 gap-14">
<SimpleGrid columns={[1, 1, 1, 2]} gap={4}>
<Box>
<Text className="text-2xl font-bold" pb="4">
Most recent messages
</Text>
<Box
backgroundColor={boxBgColor}
boxShadow="base"
dropShadow={boxAccentColor}
borderRadius="xl"
className="p-6 shadow-sm"
>
{isLoadingAll ? <CircularProgress isIndeterminate /> : <MessageTable messages={messages} />}
</Box>
</Box>
<Box>
<Text className="text-2xl font-bold" pb="4">
Your most recent messages
</Text>
<Box
backgroundColor={boxBgColor}
boxShadow="base"
dropShadow={boxAccentColor}
borderRadius="xl"
className="p-6 shadow-sm"
>
{isLoadingUser ? <CircularProgress isIndeterminate /> : <MessageTable messages={userMessages} />}
</Box>
</Box>
</SimpleGrid>
<SimpleGrid columns={[1, 1, 1, 2]} gap={4}>
<Box>
<Text className="text-2xl font-bold" pb="4">
Most recent messages
</Text>
<Box
backgroundColor={boxBgColor}
boxShadow="base"
dropShadow={boxAccentColor}
borderRadius="xl"
className="p-6 shadow-sm"
>
{isLoadingAll ? <CircularProgress isIndeterminate /> : <MessageTable messages={messages} />}
</Box>
</Box>
</Box>
<Box>
<Text className="text-2xl font-bold" pb="4">
Your most recent messages
</Text>
<Box
backgroundColor={boxBgColor}
boxShadow="base"
dropShadow={boxAccentColor}
borderRadius="xl"
className="p-6 shadow-sm"
>
{isLoadingUser ? <CircularProgress isIndeterminate /> : <MessageTable messages={userMessages} />}
</Box>
</Box>
</SimpleGrid>
</>
);
};