diff --git a/.github/workflows/deploy-docs-site.yaml b/.github/workflows/deploy-docs-site.yaml index d3fee6cb..721a5b30 100644 --- a/.github/workflows/deploy-docs-site.yaml +++ b/.github/workflows/deploy-docs-site.yaml @@ -8,6 +8,9 @@ on: - ".github/workflows/deploy-docs-site.yaml" - "docs/**" pull_request: + paths: + - ".github/workflows/deploy-docs-site.yaml" + - "docs/**" jobs: deploy: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index bb844a34..1bf0ac6a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -29,6 +29,15 @@ jobs: deploy-dev: needs: [build-backend, build-web, build-bot] runs-on: ubuntu-latest + env: + WEB_ADMIN_USERS: ${{ secrets.DEV_WEB_ADMIN_USERS }} + WEB_DISCORD_CLIENT_ID: ${{ secrets.DEV_WEB_DISCORD_CLIENT_ID }} + WEB_DISCORD_CLIENT_SECRET: ${{ secrets.DEV_WEB_DISCORD_CLIENT_SECRET }} + WEB_EMAIL_SERVER_HOST: ${{ secrets.DEV_WEB_EMAIL_SERVER_HOST }} + WEB_EMAIL_SERVER_PASSWORD: ${{ secrets.DEV_WEB_EMAIL_SERVER_PASSWORD }} + WEB_EMAIL_SERVER_PORT: ${{ secrets.DEV_WEB_EMAIL_SERVER_PORT }} + WEB_EMAIL_SERVER_USER: ${{ secrets.DEV_WEB_EMAIL_SERVER_USER }} + WEB_NEXTAUTH_SECRET: ${{ secrets.DEV_WEB_NEXTAUTH_SECRET }} steps: - name: Checkout uses: actions/checkout@v2 diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml new file mode 100644 index 00000000..44ef5b04 --- /dev/null +++ b/.github/workflows/test-e2e.yaml @@ -0,0 +1,39 @@ +name: E2E Tests (Website) + +on: + push: + branches: + - main + paths: + - oasst-shared/** + - backend/** + - website/** + pull_request: + paths: + - oasst-shared/** + - backend/** + - website/** + +jobs: + test-e2e: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Start website, backend, etc + run: docker compose up ci --build -d + - name: Run Cypress tests + uses: cypress-io/github-action@v5.0.2 + with: + browser: chrome + working-directory: website + - uses: actions/upload-artifact@v3 + if: failure() # NOTE: screenshots will be generated only if E2E test failed + with: + name: cypress-screenshots + path: website/cypress/screenshots + - uses: actions/upload-artifact@v3 + if: always() + with: + name: cypress-videos + path: website/cypress/videos diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 00000000..2ab1943e --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,7 @@ +To test the ansible playbook on localhost run +`ansible-playbook -i test.inventory.ini dev.yaml`.\ +In case you're missing the ansible docker depencency install it with `ansible-galaxy collection install community.docker`.\ +Point Redis Insights to the Redis database by visiting localhost:8001 in a +browser and select "I already have a database" followed by "Connect to a Redis +Database".\ +For host, port and name fill in `oasst-redis`, `6379` and `redis`. diff --git a/ansible/dev.yaml b/ansible/dev.yaml index 577abd68..6f93aaa6 100644 --- a/ansible/dev.yaml +++ b/ansible/dev.yaml @@ -10,6 +10,39 @@ state: present driver: bridge + - name: Copy redis.conf to managed node + ansible.builtin.copy: + src: ./redis.conf + dest: ./redis.conf + + - name: Set up Redis + community.docker.docker_container: + name: oasst-redis + image: redis + state: started + restart_policy: always + network_mode: oasst + ports: + - 6379:6379 + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep PONG"] + interval: 2s + timeout: 2s + retries: 10 + command: redis-server /usr/local/etc/redis/redis.conf + volumes: + - "./redis.conf:/usr/local/etc/redis/redis.conf" + + - name: Set up Redis Insights + community.docker.docker_container: + name: oasst-redis-insights + image: redislabs/redisinsight:latest + state: started + restart_policy: always + network_mode: oasst + ports: + - 8001:8001 + - name: Create postgres containers community.docker.docker_container: name: "{{ item.name }}" @@ -32,14 +65,6 @@ - name: oasst-postgres - name: oasst-postgres-web - - name: Set up maildev - community.docker.docker_container: - name: oasst-maildev - image: maildev/maildev - state: started - restart_policy: always - network_mode: oasst - - name: Run the oasst oasst-backend community.docker.docker_container: name: oasst-backend @@ -51,10 +76,12 @@ network_mode: oasst env: POSTGRES_HOST: oasst-postgres + REDIS_HOST: oasst-redis DEBUG_ALLOW_ANY_API_KEY: "true" DEBUG_USE_SEED_DATA: "true" MAX_WORKERS: "1" RATE_LIMIT: "false" + DEBUG_SKIP_EMBEDDING_COMPUTATION: "true" ports: - 8080:8080 @@ -68,15 +95,27 @@ restart_policy: always network_mode: oasst env: - FASTAPI_URL: http://oasst-backend:8080 - FASTAPI_KEY: "123" + ADMIN_USERS: "{{ lookup('ansible.builtin.env', 'WEB_ADMIN_USERS') }}" DATABASE_URL: postgres://postgres:postgres@oasst-postgres-web/postgres - NEXTAUTH_SECRET: O/M2uIbGj+lDD2oyNa8ax4jEOJqCPJzO53UbWShmq98= - EMAIL_SERVER_HOST: oasst-maildev - EMAIL_SERVER_PORT: "25" - EMAIL_FROM: info@example.com - NEXTAUTH_URL: http://web.dev.open-assistant.io/ DEBUG_LOGIN: "true" + DISCORD_CLIENT_ID: + "{{ lookup('ansible.builtin.env', 'WEB_DISCORD_CLIENT_ID') }}" + DISCORD_CLIENT_SECRET: + "{{ lookup('ansible.builtin.env', 'WEB_DISCORD_CLIENT_SECRET') }}" + EMAIL_FROM: open-assistent@laion.ai + EMAIL_SERVER_HOST: + "{{ lookup('ansible.builtin.env', 'WEB_EMAIL_SERVER_HOST') }}" + EMAIL_SERVER_PASSWORD: + "{{ lookup('ansible.builtin.env', 'WEB_EMAIL_SERVER_PASSWORD') }}" + EMAIL_SERVER_PORT: + "{{ lookup('ansible.builtin.env', 'WEB_EMAIL_SERVER_PORT') }}" + EMAIL_SERVER_USER: + "{{ lookup('ansible.builtin.env', 'WEB_EMAIL_SERVER_USER') }}" + FASTAPI_URL: http://oasst-backend:8080 + FASTAPI_KEY: "1234" + NEXTAUTH_SECRET: + "{{ lookup('ansible.builtin.env', 'WEB_NEXTAUTH_SECRET') }}" + NEXTAUTH_URL: http://web.dev.open-assistant.io/ ports: - 3000:3000 command: bash wait-for-postgres.sh node server.js diff --git a/ansible/redis.conf b/ansible/redis.conf new file mode 100644 index 00000000..58da1e05 --- /dev/null +++ b/ansible/redis.conf @@ -0,0 +1,2 @@ +maxmemory 100mb +maxmemory-policy allkeys-lru diff --git a/ansible/test.inventory.ini b/ansible/test.inventory.ini new file mode 100644 index 00000000..bfe6d93f --- /dev/null +++ b/ansible/test.inventory.ini @@ -0,0 +1,2 @@ +[test] +dev ansible_connection=local diff --git a/backend/alembic/versions/2023_01_08_1106-3d96bb92e33a_added_minilm_embedding_column_to_message.py b/backend/alembic/versions/2023_01_08_1106-3d96bb92e33a_added_minilm_embedding_column_to_message.py new file mode 100644 index 00000000..9b81105f --- /dev/null +++ b/backend/alembic/versions/2023_01_08_1106-3d96bb92e33a_added_minilm_embedding_column_to_message.py @@ -0,0 +1,27 @@ +"""added miniLM_embedding column to message + +Revision ID: 023548d474f7 +Revises: ba61fe17fb6e +Create Date: 2023-01-08 11:06:25.613290 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "023548d474f7" +down_revision = "ba61fe17fb6e" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column("message", sa.Column("miniLM_embedding", sa.ARRAY(sa.Float()), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("message", "miniLM_embedding") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/2023_01_08_1603-35bdc1a08bb8_embedding_for_message_now_in_its_own_.py b/backend/alembic/versions/2023_01_08_1603-35bdc1a08bb8_embedding_for_message_now_in_its_own_.py new file mode 100644 index 00000000..b732b792 --- /dev/null +++ b/backend/alembic/versions/2023_01_08_1603-35bdc1a08bb8_embedding_for_message_now_in_its_own_.py @@ -0,0 +1,49 @@ +"""embedding for message now in its own table + +Revision ID: 35bdc1a08bb8 +Revises: 023548d474f7 +Create Date: 2023-01-08 16:03:48.454207 + +""" +import sqlalchemy as sa +import sqlmodel +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "35bdc1a08bb8" +down_revision = "023548d474f7" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "message_embedding", + sa.Column("message_id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("embedding", sa.ARRAY(sa.Float()), nullable=True), + sa.Column("model", sqlmodel.sql.sqltypes.AutoString(length=256), nullable=False), + sa.ForeignKeyConstraint( + ["message_id"], + ["message.id"], + ), + sa.PrimaryKeyConstraint("message_id", "model"), + ) + op.drop_column("message", "miniLM_embedding") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "message", + sa.Column( + "miniLM_embedding", + postgresql.ARRAY(postgresql.DOUBLE_PRECISION(precision=53)), + autoincrement=False, + nullable=True, + ), + ) + op.drop_table("message_embedding") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/2023_01_08_2128-aac6b2f66006_created_date.py b/backend/alembic/versions/2023_01_08_2128-aac6b2f66006_created_date.py new file mode 100644 index 00000000..6d40d896 --- /dev/null +++ b/backend/alembic/versions/2023_01_08_2128-aac6b2f66006_created_date.py @@ -0,0 +1,30 @@ +"""Created date + +Revision ID: aac6b2f66006 +Revises: 35bdc1a08bb8 +Create Date: 2023-01-08 21:28:27.342729 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "aac6b2f66006" +down_revision = "35bdc1a08bb8" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "message_embedding", + sa.Column("created_date", sa.DateTime(), server_default=sa.text("CURRENT_TIMESTAMP"), nullable=False), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("message_embedding", "created_date") + # ### end Alembic commands ### diff --git a/backend/oasst_backend/api/v1/hugging_face.py b/backend/oasst_backend/api/v1/hugging_face.py index 1e7f1ffe..62d2ea6b 100644 --- a/backend/oasst_backend/api/v1/hugging_face.py +++ b/backend/oasst_backend/api/v1/hugging_face.py @@ -1,19 +1,14 @@ -from enum import Enum from typing import List from fastapi import APIRouter, Depends from oasst_backend.api import deps from oasst_backend.models import ApiClient from oasst_backend.schemas.hugging_face import ToxicityClassification -from oasst_backend.utils.hugging_face import HuggingFaceAPI +from oasst_backend.utils.hugging_face import HfUrl, HuggingFaceAPI router = APIRouter() -class HF_url(str, Enum): - HUGGINGFACE_TOXIC_ROBERTA = "https://api-inference.huggingface.co/models/unitary/multilingual-toxic-xlm-roberta" - - @router.get("/text_toxicity") async def get_text_toxicity( msg: str, @@ -30,7 +25,7 @@ async def get_text_toxicity( ToxicityClassification: the score of toxicity of the message. """ - api_url: str = HF_url.HUGGINGFACE_TOXIC_ROBERTA.value + api_url: str = HfUrl.HUGGINGFACE_TOXIC_ROBERTA.value hugging_face_api = HuggingFaceAPI(api_url) response = await hugging_face_api.post(msg) diff --git a/backend/oasst_backend/api/v1/tasks.py b/backend/oasst_backend/api/v1/tasks.py index eb10dc00..821ba562 100644 --- a/backend/oasst_backend/api/v1/tasks.py +++ b/backend/oasst_backend/api/v1/tasks.py @@ -7,7 +7,9 @@ from fastapi.security.api_key import APIKey from loguru import logger from oasst_backend.api import deps from oasst_backend.api.v1.utils import prepare_conversation +from oasst_backend.config import settings from oasst_backend.prompt_repository import PromptRepository, TaskRepository +from oasst_backend.utils.hugging_face import HfEmbeddingModel, HfUrl, HuggingFaceAPI from oasst_shared.exceptions import OasstError, OasstErrorCode from oasst_shared.schemas import protocol as protocol_schema from sqlmodel import Session @@ -253,7 +255,7 @@ def tasks_acknowledge_failure( @router.post("/interaction", response_model=protocol_schema.TaskDone) -def tasks_interaction( +async def tasks_interaction( *, db: Session = Depends(deps.get_db), api_key: APIKey = Depends(deps.get_api_key), @@ -274,12 +276,26 @@ def tasks_interaction( ) # here we store the text reply in the database - pr.store_text_reply( + newMessage = pr.store_text_reply( text=interaction.text, frontend_message_id=interaction.message_id, user_frontend_message_id=interaction.user_message_id, ) + if not settings.DEBUG_SKIP_EMBEDDING_COMPUTATION: + try: + hugging_face_api = HuggingFaceAPI( + f"{HfUrl.HUGGINGFACE_FEATURE_EXTRACTION.value}/{HfEmbeddingModel.MINILM.value}" + ) + embedding = await hugging_face_api.post(interaction.text) + pr.insert_message_embedding( + message_id=newMessage.id, model=HfEmbeddingModel.MINILM.value, embedding=embedding + ) + except OasstError: + logger.error( + f"Could not fetch embbeddings for text reply to {interaction.message_id=} with {interaction.text=} by {interaction.user=}." + ) + return protocol_schema.TaskDone() case protocol_schema.MessageRating: logger.info( diff --git a/backend/oasst_backend/config.py b/backend/oasst_backend/config.py index 1765af7a..ed394412 100644 --- a/backend/oasst_backend/config.py +++ b/backend/oasst_backend/config.py @@ -25,6 +25,7 @@ class Settings(BaseSettings): DEBUG_USE_SEED_DATA_PATH: Optional[FilePath] = ( Path(__file__).parent.parent / "test_data/generic/test_generic_data.json" ) + DEBUG_SKIP_EMBEDDING_COMPUTATION: bool = False HUGGING_FACE_API_KEY: str = "" diff --git a/backend/oasst_backend/models/__init__.py b/backend/oasst_backend/models/__init__.py index a856b155..0873381c 100644 --- a/backend/oasst_backend/models/__init__.py +++ b/backend/oasst_backend/models/__init__.py @@ -1,6 +1,7 @@ from .api_client import ApiClient from .journal import Journal, JournalIntegration from .message import Message +from .message_embedding import MessageEmbedding from .message_reaction import MessageReaction from .message_tree_state import MessageTreeState from .task import Task @@ -13,6 +14,7 @@ __all__ = [ "User", "UserStats", "Message", + "MessageEmbedding", "MessageReaction", "MessageTreeState", "Task", diff --git a/backend/oasst_backend/models/message_embedding.py b/backend/oasst_backend/models/message_embedding.py new file mode 100644 index 00000000..74da5004 --- /dev/null +++ b/backend/oasst_backend/models/message_embedding.py @@ -0,0 +1,21 @@ +from datetime import datetime +from typing import List, Optional +from uuid import UUID + +import sqlalchemy as sa +import sqlalchemy.dialects.postgresql as pg +from sqlmodel import ARRAY, Field, Float, SQLModel + + +class MessageEmbedding(SQLModel, table=True): + __tablename__ = "message_embedding" + __table_args__ = (sa.PrimaryKeyConstraint("message_id", "model"),) + + message_id: UUID = Field(sa_column=sa.Column(pg.UUID(as_uuid=True), sa.ForeignKey("message.id"), nullable=False)) + model: str = Field(max_length=256, nullable=False) + embedding: List[float] = Field(sa_column=sa.Column(ARRAY(Float)), nullable=True) + + # In the case that the Message Embedding is created afterwards + created_date: Optional[datetime] = Field( + sa_column=sa.Column(sa.DateTime(), nullable=False, server_default=sa.func.current_timestamp()) + ) diff --git a/backend/oasst_backend/models/task.py b/backend/oasst_backend/models/task.py index a980c1b5..d923be97 100644 --- a/backend/oasst_backend/models/task.py +++ b/backend/oasst_backend/models/task.py @@ -35,4 +35,4 @@ class Task(SQLModel, table=True): @property def expired(self) -> bool: - return self.expiry_date is not None and datetime.utcnow() < self.expiry_date + return self.expiry_date is not None and datetime.utcnow() > self.expiry_date diff --git a/backend/oasst_backend/prompt_repository.py b/backend/oasst_backend/prompt_repository.py index 7446ec07..c31c0061 100644 --- a/backend/oasst_backend/prompt_repository.py +++ b/backend/oasst_backend/prompt_repository.py @@ -2,13 +2,13 @@ import datetime import random from collections import defaultdict from http import HTTPStatus -from typing import Optional +from typing import List, Optional from uuid import UUID, uuid4 import oasst_backend.models.db_payload as db_payload from loguru import logger from oasst_backend.journal_writer import JournalWriter -from oasst_backend.models import ApiClient, Message, MessageReaction, TextLabels, User +from oasst_backend.models import ApiClient, Message, MessageEmbedding, MessageReaction, TextLabels, User from oasst_backend.models.payload_column_type import PayloadContainer from oasst_backend.task_repository import TaskRepository, validate_frontend_message_id from oasst_backend.user_repository import UserRepository @@ -91,7 +91,12 @@ class PromptRepository: self.db.refresh(message) return message - def store_text_reply(self, text: str, frontend_message_id: str, user_frontend_message_id: str) -> Message: + def store_text_reply( + self, + text: str, + frontend_message_id: str, + user_frontend_message_id: str, + ) -> Message: validate_frontend_message_id(frontend_message_id) validate_frontend_message_id(user_frontend_message_id) @@ -224,6 +229,30 @@ class PromptRepository: OasstErrorCode.TASK_PAYLOAD_TYPE_MISMATCH, ) + def insert_message_embedding(self, message_id: UUID, model: str, embedding: List[float]) -> MessageEmbedding: + """Insert the embedding of a new message in the database. + + Args: + message_id (UUID): the identifier of the message we want to save its embedding + model (str): the model used for creating the embedding + embedding (List[float]): the values obtained from the message & model + + Raises: + OasstError: if misses some of the before params + + Returns: + MessageEmbedding: the instance in the database of the embedding saved for that message + """ + + if None in (message_id, model, embedding): + raise OasstError("Paramters missing to add embedding", OasstErrorCode.GENERIC_ERROR) + + message_embedding = MessageEmbedding(message_id=message_id, model=model, embedding=embedding) + self.db.add(message_embedding) + self.db.commit() + self.db.refresh(message_embedding) + return message_embedding + def insert_reaction(self, task_id: UUID, payload: db_payload.ReactionPayload) -> MessageReaction: if self.user_id is None: raise OasstError("User required", OasstErrorCode.USER_NOT_SPECIFIED) diff --git a/backend/oasst_backend/utils/hugging_face.py b/backend/oasst_backend/utils/hugging_face.py index 0df913f5..87c6288e 100644 --- a/backend/oasst_backend/utils/hugging_face.py +++ b/backend/oasst_backend/utils/hugging_face.py @@ -1,10 +1,21 @@ +from enum import Enum from typing import Any, Dict import aiohttp +from loguru import logger from oasst_backend.config import settings from oasst_shared.exceptions import OasstError, OasstErrorCode +class HfUrl(str, Enum): + HUGGINGFACE_TOXIC_ROBERTA = ("https://api-inference.huggingface.co/models/unitary/multilingual-toxic-xlm-roberta",) + HUGGINGFACE_FEATURE_EXTRACTION = "https://api-inference.huggingface.co/pipeline/feature-extraction" + + +class HfEmbeddingModel(str, Enum): + MINILM = "sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2" + + class HuggingFaceAPI: """Class Object to make post calls to endpoints for inference in models hosted in HuggingFace""" @@ -41,6 +52,9 @@ class HuggingFaceAPI: async with session.post(self.api_url, headers=self.headers, json=payload) as response: # If we get a bad response if response.status != 200: + + logger.error(response) + logger.info(self.headers) raise OasstError( "Response Error Detoxify HuggingFace", error_code=OasstErrorCode.HUGGINGFACE_API_ERROR ) diff --git a/docker-compose.yaml b/docker-compose.yaml index 6bc42c51..224c9efd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,6 +11,11 @@ services: image: sverrirab/sleep depends_on: [db, webdb, adminer, maildev, backend, redis] + # Used by CI automations. + ci: + image: sverrirab/sleep + depends_on: [db, webdb, maildev, backend, redis, web] + # This DB is for the FastAPI Backend. db: image: postgres @@ -95,6 +100,7 @@ services: - DEBUG_SKIP_API_KEY_CHECK=True - DEBUG_USE_SEED_DATA=True - MAX_WORKERS=1 + - DEBUG_SKIP_EMBEDDING_COMPUTATION=True depends_on: db: condition: service_healthy diff --git a/notebooks/README.md b/notebooks/README.md index edb5da33..2eea7a15 100644 --- a/notebooks/README.md +++ b/notebooks/README.md @@ -1,10 +1,14 @@ # Notebooks -This is a folders with some useful notebooks, all the notebooks have a markdown -file with the same name explaining what they do. +This is a folder with some useful notebooks, all the notebooks have a markdown +file with the same name (or a README.md if its a single notebook folder) +explaining what they do. ## Contributing -Contributing to both notebooks and making new notebooks is very welcome. If you -do so, make sure to make a markdown (.md) file to go with your notebook, makes +Contributing to notebooks and making new notebooks is very welcome. If you do +so, make sure to make a markdown (.md) file to go with your notebook, it makes it easier for people to know what your notebook is about. + +Check out the [example notebook](example/) for a reference example you can use +as a starting point. diff --git a/notebooks/example/README.md b/notebooks/example/README.md new file mode 100644 index 00000000..763b4812 --- /dev/null +++ b/notebooks/example/README.md @@ -0,0 +1,48 @@ +# Example Notebook + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LAION-AI/Open-Assistant/blob/main/notebooks/example/example.ipynb) + +This folder contains an example reference notebook structure and approach for +this project. Please try and follow this structure as closely as possible. While +things will not exactly be the same for each notebook some principles we would +like to try ensure are: + +1. Each notebook or collection of related or dependant notebooks should live in + its own folder. +1. Each notebook should have a markdown file with the same name as the notebook + (or README.md if it's a single notebook folder) that explains what the + notebook does and how to use it. +1. Add an "Open in Colab" badge to the top of the notebook (see the markdown + cell near the top of `example.ipynb` as an example you can adapt). +1. Make it as easy as possible for someone to run the notebook in Google Colab + or some other environment based on standard practices like providing a + `requirements.txt` file or anything else needed to successfully run the + notebook. + +## Running in Google Colab + +At the top of the [example notebook](example.ipynb) there is a code cell that +will (once uncommented): + +1. clone the repository into your colab instance. +1. `cd` into the relevant notebook directory. +1. run `pip install -r requirements.txt` to install the required packages. + +At this point you can run the notebook as normal and the folder structure will +match that of the repository and the colab notebook will be running from the +same directory that the notebook lives in so relative links etc should work as +expected (for example `example.ipynb` will read some sample data from +`data/data.csv`). + +If you are adding a notebook please try and add a similar cell to the top of the +notebook so that it is easy for others to run the notebook in colab. If your +notebook does not have any dependencies beyond what already comes as standard in +Google Colab then you do not need such a cell, just an "Open in Colab" badge +will suffice. + +## example.ipynb + +This notebook contains an example "Open In Colab" badge and a code cell to +prepare the colab environment to run the notebook. It also contains a code cell +that will read in some sample data from the `data` folder in the repository and +display it as a pandas dataframe. diff --git a/notebooks/example/data/data.csv b/notebooks/example/data/data.csv new file mode 100644 index 00000000..126a03bb --- /dev/null +++ b/notebooks/example/data/data.csv @@ -0,0 +1,3 @@ +row,text,label +1,some example data,1 +2,some more data,0 diff --git a/notebooks/example/example.ipynb b/notebooks/example/example.ipynb new file mode 100644 index 00000000..5f938c13 --- /dev/null +++ b/notebooks/example/example.ipynb @@ -0,0 +1,161 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Example Notebook" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/LAION-AI/Open-Assistant/blob/example-notebook/notebooks/example/example.ipynb)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "# uncomment and run below lines to set up if running in colab\n", + "# !git clone https://github.com/LAION-AI/Open-Assistant.git\n", + "# %cd Open-Assistant/notebooks/example\n", + "# !pip install -r requirements.txt" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Try to add a markdown section to the notebook that explains what the notebook is about and what it does. This will help people understand what the notebook is for and how to use it." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "# import required packages\n", + "import pandas as pd\n", + "from transformers import pipeline" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Use Headings\n", + "\n", + "(it will help with link sharing to specific sections of the notebook)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Make fancy markdown cells if you want." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
rowtextlabel
01some example data1
12some more data0
\n", + "
" + ], + "text/plain": [ + " row text label\n", + "0 1 some example data 1\n", + "1 2 some more data 0" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Do cool stuff here\n", + "df = pd.read_csv(\"data/data.csv\")\n", + "df.head()" + ] + } + ], + "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.7.4 (tags/v3.7.4:e09359112e, Jul 8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)]" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "25d5c2324055587ceaeef27650c79ce8358ea61d7689f2e0b8ada5d53f85bce4" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/notebooks/example/requirements.txt b/notebooks/example/requirements.txt new file mode 100644 index 00000000..976a2b1f --- /dev/null +++ b/notebooks/example/requirements.txt @@ -0,0 +1 @@ +transformers diff --git a/scripts/backend-development/run-local.sh b/scripts/backend-development/run-local.sh index 3ed0e936..f0f6d16c 100755 --- a/scripts/backend-development/run-local.sh +++ b/scripts/backend-development/run-local.sh @@ -6,6 +6,7 @@ pushd "$parent_path/../../backend" export DEBUG_SKIP_API_KEY_CHECK=True export DEBUG_USE_SEED_DATA=True +export DEBUG_SKIP_EMBEDDING_COMPUTATION=True uvicorn main:app --reload --port 8080 --host 0.0.0.0 diff --git a/website/.gitignore b/website/.gitignore index 86e167da..0b9f1dbe 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -39,5 +39,7 @@ next-env.d.ts *.swp # cypress +/cypress/screenshots +/cypress/videos /cypress-visual-screenshots/diff /cypress-visual-screenshots/comparison diff --git a/website/README.md b/website/README.md index 11e3ccc4..37f6991b 100644 --- a/website/README.md +++ b/website/README.md @@ -153,6 +153,34 @@ When writing code for the website, we have a few best practices: 1. Define functional React components (with types for all properties when feasible). +### Developing New Features + +When working on new features or making significant changes that can't be done +within a single Pull Request, we ask that you make use of Feature Flags. + +We've set up +[`react-feature-flags`](https://www.npmjs.com/package/react-feature-flags) to +make this easier. To get started: + +1. Add a new flag entry to `website/src/flags.ts`. We have an example flag you + can copy as an example. Be sure to `isActive` to true when testing your + features but false when submitting your PR. +1. Use your flag wherever you add a new UI element. This can be done with: + +```js +import { Flags } from "react-feature-flags"; +... + + + +``` + + You can see an example of how this works by checking `website/src/components/Header/Headers.tsx` where we use `flagTest`. + +1. Once you've finished building out the feature and it is ready for everyone + to use, it's safe to remove the `Flag` wrappers around your component and + the entry in `flags.ts`. + ### URL Paths To use stable and consistent URL paths, we recommend the following strategy for diff --git a/website/package-lock.json b/website/package-lock.json index ef867a98..81a9cd5d 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -39,6 +39,7 @@ "postcss-focus-visible": "^7.1.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-feature-flags": "^1.0.0", "react-icons": "^4.7.1", "swr": "^2.0.0", "tailwindcss": "^3.2.4", @@ -26424,6 +26425,16 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "node_modules/react-feature-flags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-feature-flags/-/react-feature-flags-1.0.0.tgz", + "integrity": "sha512-KBFUkXjF7ifGWEQr2Ida4LdAtKGDOwFdTRlXipWxGP9a43vUBxP6IscpYQofGjlzlBcgmFKuzubcVheB6NliEg==", + "peerDependencies": { + "prop-types": "^15.5.4", + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-focus-lock": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.2.tgz", @@ -50838,6 +50849,12 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz", "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA==" }, + "react-feature-flags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/react-feature-flags/-/react-feature-flags-1.0.0.tgz", + "integrity": "sha512-KBFUkXjF7ifGWEQr2Ida4LdAtKGDOwFdTRlXipWxGP9a43vUBxP6IscpYQofGjlzlBcgmFKuzubcVheB6NliEg==", + "requires": {} + }, "react-focus-lock": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.9.2.tgz", diff --git a/website/package.json b/website/package.json index d00a3f4d..032125dd 100644 --- a/website/package.json +++ b/website/package.json @@ -53,6 +53,7 @@ "postcss-focus-visible": "^7.1.0", "react": "18.2.0", "react-dom": "18.2.0", + "react-feature-flags": "^1.0.0", "react-icons": "^4.7.1", "swr": "^2.0.0", "tailwindcss": "^3.2.4", diff --git a/website/src/components/FlaggableElement.tsx b/website/src/components/FlaggableElement.tsx index 63370444..df7296cc 100644 --- a/website/src/components/FlaggableElement.tsx +++ b/website/src/components/FlaggableElement.tsx @@ -27,8 +27,22 @@ import poster from "src/lib/poster"; import { colors } from "styles/Theme/colors"; import useSWRMutation from "swr/mutation"; +interface textFlagLabels { + attributeName: string; + labelText: string; + additionalExplanation?: string; +} + export const FlaggableElement = (props) => { const [isEditing, setIsEditing] = useBoolean(); + const flaggable_labels = props.flaggable_labels; + const TEXT_LABEL_FLAGS = flaggable_labels.valid_labels.map((valid_label) => { + return { + attributeName: valid_label.name, + labelText: valid_label.display_text, + additionalExplanation: valid_label.help_text, + }; + }); const { trigger } = useSWRMutation("/api/set_label", poster, { onSuccess: () => { setIsEditing.off; @@ -181,48 +195,3 @@ export function FlagCheckbox(props: { ); } -interface textFlagLabels { - attributeName: string; - labelText: string; - additionalExplanation?: string; -} -const TEXT_LABEL_FLAGS: textFlagLabels[] = [ - // For the time being this list is configured on the FE. - // In the future it may be provided by the API. - // { - // attributeName: "fails_task", - // labelText: "Fails to follow the correct instruction / task", - // additionalExplanation: "__TODO__", - // }, - // { - // attributeName: "not_customer_assistant_appropriate", - // labelText: "Inappropriate for customer assistant", - // additionalExplanation: "__TODO__", - // }, - { - attributeName: "sexual_content", - labelText: "Contains sexual content", - }, - { - attributeName: "violence", - labelText: "Contains violent content", - }, - // { - // attributeName: "encourages_violence", - // labelText: "Encourages or fails to discourage violence/abuse/terrorism/self-harm", - // }, - // { - // attributeName: "denigrates_a_protected_class", - // labelText: "Denigrates a protected class", - // }, - // { - // attributeName: "gives_harmful_advice", - // labelText: "Fails to follow the correct instruction / task", - // additionalExplanation: - // "The advice given in the output is harmful or counter-productive. This may be in addition to, but is distinct from the question about encouraging violence/abuse/terrorism/self-harm.", - // }, - // { - // attributeName: "expresses_moral_judgement", - // labelText: "Expresses moral judgement", - // }, -]; diff --git a/website/src/components/Header/Header.tsx b/website/src/components/Header/Header.tsx index 4ea453c6..d0dc8a06 100644 --- a/website/src/components/Header/Header.tsx +++ b/website/src/components/Header/Header.tsx @@ -2,6 +2,7 @@ import { Box, Button, Text, useColorMode } from "@chakra-ui/react"; import Image from "next/image"; import Link from "next/link"; import { useSession } from "next-auth/react"; +import { Flags } from "react-feature-flags"; import { FaUser } from "react-icons/fa"; import { UserMenu } from "./UserMenu"; @@ -42,6 +43,9 @@ export function Header(props) {
+ +
FlagTest
+
diff --git a/website/src/components/Messages.tsx b/website/src/components/Messages.tsx index fb84559e..71ff237b 100644 --- a/website/src/components/Messages.tsx +++ b/website/src/components/Messages.tsx @@ -10,11 +10,31 @@ export interface Message { message_id: string; } -export const Messages = ({ messages, post_id }: { messages: Message[]; post_id: string }) => { +export interface ValidLabel { + name: string; + display_text: string; + help_text: string; +} + +export const Messages = ({ + messages, + post_id, + valid_labels, +}: { + messages: Message[]; + post_id: string; + valid_labels: ValidLabel[]; +}) => { const items = messages.map((messageProps: Message, i: number) => { const { message_id, text } = messageProps; return ( - + ); diff --git a/website/src/components/Tasks/CreateTask.tsx b/website/src/components/Tasks/CreateTask.tsx index 7dcb0d0f..7af262e1 100644 --- a/website/src/components/Tasks/CreateTask.tsx +++ b/website/src/components/Tasks/CreateTask.tsx @@ -18,7 +18,7 @@ export interface CreateTaskProps { } export const CreateTask = ({ tasks, taskType, trigger, onSkipTask, onNextTask, mainBgClasses }: CreateTaskProps) => { const task = tasks[0].task; - + const valid_labels = tasks[0].valid_labels; const [inputText, setInputText] = useState(""); const submitResponse = (task: { id: string }) => { @@ -42,7 +42,9 @@ export const CreateTask = ({ tasks, taskType, trigger, onSkipTask, onNextTask, m <>
{taskType.label}

{taskType.overview}

- {task.conversation ? : null} + {task.conversation ? ( + + ) : null} <>
{taskType.instruction}
diff --git a/website/src/flags.ts b/website/src/flags.ts new file mode 100644 index 00000000..2e59ec1a --- /dev/null +++ b/website/src/flags.ts @@ -0,0 +1,3 @@ +const flags = [{ name: "flagTest", isActive: false }]; + +export default flags; diff --git a/website/src/lib/oasst_api_client.ts b/website/src/lib/oasst_api_client.ts index 889d8b5b..1bbb13a8 100644 --- a/website/src/lib/oasst_api_client.ts +++ b/website/src/lib/oasst_api_client.ts @@ -48,6 +48,33 @@ export class OasstApiClient { return await resp.json(); } + private async get(path: string): Promise { + const resp = await fetch(`${this.oasstApiUrl}${path}`, { + method: "GET", + headers: { + "X-API-Key": this.oasstApiKey, + "Content-Type": "application/json", + }, + }); + + if (resp.status == 204) { + return null; + } + + if (resp.status >= 300) { + const errorText = await resp.text(); + let error: any; + try { + error = JSON.parse(errorText); + } catch (e) { + throw new OasstError(errorText, 0, resp.status); + } + throw new OasstError(error.message ?? error, error.error_code, resp.status); + } + + return await resp.json(); + } + // TODO return a strongly typed Task? // This method is used to store a task in RegisteredTask.task. // This is a raw Json type, so we can't use it to strongly type the task. @@ -96,6 +123,12 @@ export class OasstApiClient { ...content, }); } + + //Fetch valid labels. This is called every task. though the call may be redundant + //keeping this for future where the valid labels may change per task + async fetch_valid_text(): Promise { + return this.get(`/api/v1/text_labels/valid_labels`); + } } export const oasstApiClient = diff --git a/website/src/pages/_app.tsx b/website/src/pages/_app.tsx index ab7655cd..69d212e8 100644 --- a/website/src/pages/_app.tsx +++ b/website/src/pages/_app.tsx @@ -3,7 +3,9 @@ import "focus-visible"; import type { AppProps } from "next/app"; import { SessionProvider } from "next-auth/react"; +import { FlagsProvider } from "react-feature-flags"; import { getDefaultLayout, NextPageWithLayout } from "src/components/Layout"; +import flags from "src/flags"; import { Chakra, getServerSideProps } from "../styles/Chakra"; @@ -16,9 +18,11 @@ function MyApp({ Component, pageProps: { session, cookies, ...pageProps } }: App const page = getLayout(); return ( - - {page} - + + + {page} + + ); } export { getServerSideProps }; diff --git a/website/src/pages/api/new_task/[task_type].ts b/website/src/pages/api/new_task/[task_type].ts index 9f3be55c..a685d589 100644 --- a/website/src/pages/api/new_task/[task_type].ts +++ b/website/src/pages/api/new_task/[task_type].ts @@ -23,6 +23,7 @@ const handler = async (req, res) => { // Fetch the new task. const task = await oasstApiClient.fetchTask(task_type, token); + const valid_labels = await oasstApiClient.fetch_valid_text(); // Store the task and link it to the user.. const registeredTask = await prisma.registeredTask.create({ @@ -36,6 +37,11 @@ const handler = async (req, res) => { }, }); + // Add the valid labels that can be used to flag messages in this Task + registeredTask["valid_labels"] = valid_labels; + // Update the backend with our Task ID + await oasstApiClient.ackTask(task.id, registeredTask.id); + // Send the results to the client. res.status(200).json(registeredTask); };