From 1750cecb54cb8691e26d6b26a9861e9236ddd7a7 Mon Sep 17 00:00:00 2001 From: markcheeky <10684818+markcheeky@users.noreply.github.com> Date: Sun, 1 Jan 2023 13:18:15 +0100 Subject: [PATCH 01/47] Create docs/supervised_datasets.md, suggested by yk in issue 186 --- docs/supervised_datasets.md | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/supervised_datasets.md diff --git a/docs/supervised_datasets.md b/docs/supervised_datasets.md new file mode 100644 index 00000000..38d4ba2c --- /dev/null +++ b/docs/supervised_datasets.md @@ -0,0 +1,59 @@ +# Supervised datasets + +For discussion about usage of supervised data see issue . + + +## Motivation + +An important part of making the assistant useful is to teach it to understand and follow instructions, and to perform large set of tasks well. + +While RLHF seems like the main ingredient, using existing supervised data might help. + +There are two large-scale projects in the area of instruction-following / multitask learning: Promptsource and Natural Instructions - +these projects crowdsourced templates and turned existing NLP datasets into instruction-following seq2seq form in natural langauge. +They include both long-output training examples like generating a sentence that is a likely consequence of sentence in the prompt, and +short-output, like rating prediction from review. (Pre-)training on such datasets should help model understand and follow instructions +and teach it many abilities neccessary to perform a large set of tasks correctly. However, these data are not dialog-like - they do not +look like a normal conversation. + +There are also supervised dialog datasets such as Blended Skill Talk or SODA. In constrast to instruction-following datasets, dialog data +is not as focused on "academic tasks" or correctness, but encourage the model to respond naturally like a person would. + +### Promptsource +- GitHub: +- paper: [Multitask Prompted Training Enables Zero-Shot Task Generalization](https://arxiv.org/abs/2110.08207) +- project for preparing templates and working with them +- they generated a dataset using the templates: + - + - (with multilingual data but English prompt) + - (with multilingual data and machine-translated prompt) +- they trained zero-shot models (= models for following instructions in the input) + - based on T5 architecture (encoder-decoder) called T0 family (and MT0 for multilingual) + - and based on GPT architecture (decoder-only) called BloomZ family + - Huggingface demo: [T0](https://huggingface.co/bigscience/T0pp), [MT0](https://huggingface.co/bigscience/mt0-large), [BloomZ](https://huggingface.co/bigscience/bloomz), + - GitHub repo for T0: + - GitHub repo for BloomZ and MT0: + + +### Natural instructions +- GitHub: +- paper: [Super-NaturalInstructions: Generalization via Declarative Instructions on 1600+ NLP Tasks](https://arxiv.org/abs/2204.07705) +- they crowdsource directly the data prepared for instruction following (and learning from a few examples) +- the GitHub repo = the dataset. It contains jsons +- they trained zero-shot and in-context few-shot models (in multiple sizes): + - mT5 architecture (encoder-decoder, multilingual pretraining) + - Huggingface demo few-shot: + - Huggingface demo zero-shot: + + +### Blended Skill Talk +- used by Facebook in Blenderbot project +- HuggingFace dataset: +- example model trained on it: + + +### SODA +- GitHub: +- paper: + + From 53ab8fb479d6b4773d0f5fe608451a98c3a5fa45 Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Mon, 2 Jan 2023 14:21:39 +0900 Subject: [PATCH 02/47] Adding a bare bones unstyled terms of service and privacy policy --- website/src/pages/privacy-policy.tsx | 414 +++++++++++++++++++++++++ website/src/pages/terms-of-service.tsx | 159 ++++++++++ 2 files changed, 573 insertions(+) create mode 100644 website/src/pages/privacy-policy.tsx create mode 100644 website/src/pages/terms-of-service.tsx diff --git a/website/src/pages/privacy-policy.tsx b/website/src/pages/privacy-policy.tsx new file mode 100644 index 00000000..42f439fc --- /dev/null +++ b/website/src/pages/privacy-policy.tsx @@ -0,0 +1,414 @@ +import { Container, Heading } from "@chakra-ui/react"; +import Head from "next/head"; +import { Footer } from "src/components/Footer"; +import { Header } from "src/components/Header"; + +const PrivacyPolicy = () => { + return ( + <> + + Open Assistant Privacy Policy + + +
+ + + Privacy Policy + + Overview + + + We are pleased that you are interested in our work and welcome you to our website laion.ai. In this Privacy + Policy you will learn which personal data we process when you visit our website and to what kind of purpose, + and also what rights you have regarding these data. Categorically, we only store data as long as we need + them. There is no legal obligation to provide us with personal data. Automated decision-making, as per + Article 22 of the EU-GDPR, will not happen. + + + 1. Definitions + + + We are required by law that personal data are processed lawfully, in good faith, and in a manner that can be + comprehended by the persons who are affected (“lawfulness, fair processing, transparency”). To this end, we + hereby inform you about the individual legal definitions of the European General Data Protection Regulation + (GDPR) and the new German Federal Data Protection Act, which are also used in these data privacy + regulations. + + + + 1.1 Personal data + + + + 'Personal data' means any information relating to an identified or identifiable natural person + (hereinafter the 'data subject'). A natural person is considered to be identifiable if he or she + can be identified directly or indirectly, in particular by association with an identifier such as a name, an + identification number, location data, an online identifier, or one or more special features which express + the physical, physiological, genetic, mental, economic, cultural or social identity of the natural person. + + + + 1.2 Restriction of processing + + + + 'Restriction of processing' means the marking of stored personal data with the aim of limiting its + processing in the future. + + + + 1.3 Profiling + + + + 'Profiling' means any form of automated processing of personal data consisting of the use of + personal data to evaluate certain personal aspects relating to a natural person, in particular to analyse or + predict aspects concerning that natural person's performance at work, economic situation, health, + personal preferences, interests, reliability, behaviour, location or movements. + + + + 1.4 Pseudonymization + + + + 'Pseudonymization' means the processing of personal data in such a manner that the personal data + can no longer be attributed to a specific data subject without the use of additional information, provided + that such additional information is kept separately and is subject to technical and organizational measures + to ensure that the personal data is not attributed to an identified or identifiable natural person + + + + 1.5 Filing system + + + + 'Filing system' means any structured set of personal data which is accessible according to + specific criteria, whether centralized, decentralized or dispersed on a functional or geographical basis. + + + + 1.6 Controller + + + + 'Controller' means the natural or legal person, public authority, agency or other body which, + alone or jointly with others, determines the purposes and means of the processing of personal data. Where + the purposes and means of such processing are determined by European Union or Member State law, the + controller or the specific criteria for its nomination may be provided for by European Union or Member State + law. + + + + 1.7 Processor + + + + 'Processor' means a natural or legal person, public authority, agency or other body which + processes personal data on behalf of the controller. + + + + 1.8 Recipient + + + + 'Recipient' means a natural or legal person, public authority, agency or another body, to which + the personal data is disclosed, whether a third party or not. However, public authorities which may receive + potentially personal data in the framework of a particular inquiry in accordance with European Union or + Member State law shall not be regarded as recipients. The processing of that data by those public + authorities shall be in compliance with the applicable data protection rules according to the purposes of + the processing. + + + + 1.9 Third party + + + + A 'third party' means a natural or legal person, public authority, agency or body other than the + data subject, controller, processor and persons who, under the direct authority of the controller or + processor, are authorized to process personal data. + + + 2. Responsible controller + + Responsible controller is: LAION e.V., Marie-Henning-Weg 143, 21035 Hamburg, Germany + + 3. Data we collect + + Open Assistant tracks data in the following conditions + + + 3.1 Using the Discord Bot + + + + When using the Open Assistant Discord bot, we privately track and store the unique Discord ID of the user + submitting responses. Each submitted response is associated with the user’s Discord ID. + + + + 3.1 Using the Website + + + + When a user registers an account with the website we privately track and store either the unique Discord ID + of the user or the unique Email of the registered user. When a user submits responses we store: +
    +
  1. When registered using Discord, we associate the unique Discord ID with each submitted response
  2. +
  3. When registered using Email, we associate a unique pseudonymous ID with each submitted response
  4. +
+
+ 4. Inquiries + + + When you contact us via e-mail, telephone or telefax, your inquiry, including all personal data arising + thereof will be stored by us for the purpose of processing your request. We will not pass on these data + without your consent. The processing of these data is based on Article 6 (1) (1) (b) GDPR, if your inquiry + is related to the fulfilment of a contract concluded with us or required for the implementation of + pre-contractual measures. Furthermore, the processing is based on Article 6 (1) (1) (f) GDPR, because we + have a legitimate interest in the effective handling of requests sent to us. In addition, according to + Article 6 (1) (1) (c) GDPR we are also entitled to the processing of the above-mentioned data, because we + are legally bound to enable fast electronic contact and immediate communication. Of course, your data will + only be used strictly according to purpose and only for processing and responding to your request. After + final processing, your data will immediately be anonymized or deleted, unless we are bound by a legally + prescribed storage period. + + + 5. Processors + + + In principle, we will never pass on your personal data to third parties without your explicit consent. + However, just as every modern business we cooperate with data processors in order to be able to offer you + the best possible uninterrupted service. When we cooperate with external service providers, regular order + processing is performed, based on Article 28 GDPR. For this purpose, we enter into respective agreements + with our partners, in order to safeguard the protection of your data. For processing your data, we only use + carefully selected processors. They are bound by our instructions, and regularly controlled by us. We only + commission external service provider who have guaranteed that all data processing procedures are performed + in unison with data protection regulations. Receivers of personal data may be: Hosting companies and Hosting + service providers + + + 6. Children and young people + + + In principle, our offer is directed towards adults. Children and young people under the age of 16 are not + allowed to transmit personal data to us without the consent of their parents or legal guardians. + + + 7. Your rights + + + If your personal data is processed on the basis of consent which you have given us, you have the right to + revoke your consent at any time. The revocation of consent does not affect the legality of the processing + performed on the basis of the consent until the time of revocation. You can contact us at any time to + exercise your right to revoke consent. + + + + 7.2 Right to confirmation + + + + You have the right to request confirmation from the controller that we are processing personal data + concerning you. You can request this confirmation at any time using the contact details above. + + + + 7.3 Right to information + + + + In the event that personal data is processed, you can request information about this personal data and the + following information at any time: the purposes of the processing, the categories of personal data being + processed, the recipients or categories of recipients to whom the personal data has been or is being + disclosed, in particular in the case of recipients in third countries or international organizations, if + possible, the planned duration for which the personal data is stored or, if this is not, possible, the + criteria for determining this duration, the existence of a right to rectification or erasure of the personal + data concerning you, or to a restriction of processing by the controller or a right to object to such + processing, the existence of a right to lodge a complaint with a supervisory authority, if the personal data + is not collected from the data subject, all available information on the source of the data, the existence + of automated decision-making, including profiling, in accordance with Article 22 (1) and (4) GDPR and, at + least in these cases, meaningful information about the logic involved and the scope and intended impact of + such processing on the data subject. If personal data is transferred to a third country or to an + international organization, you have the right to be informed of the appropriate safeguards under Article 46 + of the GDPR in connection with the transfer. We provide a copy of the personal data that is the subject of + the processing. For any additional copies you request of a person, we may charge a reasonable fee based on + our administrative costs. If your request is submitted electronically, the information must be provided in a + standard electronic format, unless otherwise stated. The right to receive a copy under paragraph 3 shall not + affect the rights and freedoms of others. + + + + 7.4 Right to rectification + + + + You have the right to demand the immediate correction of incorrect personal data concerning you. Taking into + account the purposes of processing, you have the right to request the completion of incomplete personal + data, including by means of a supplementary statement.{" "} + + + + 7.4 Right to rectification + + + + You have the right to demand the immediate correction of incorrect personal data concerning you. Taking into + account the purposes of processing, you have the right to request the completion of incomplete personal + data, including by means of a supplementary statement. + + + + 7.5 Right to erasure (“right to be forgotten“) + + + + You have the right to demand that the controller erase personal data concerning you without undue delay, and + we are obligated to erase personal data without undue delay where one of the following grounds applies: the + personal data are no longer necessary in relation to the purposes for which they were collected or otherwise + processed, the data subject withdraws the consent on which the processing is based according to point (a) of + Article 6(1), or point (a) of Article 9(2), and there is no other legal ground for the processing, the data + subject objects to the processing pursuant to Article 21(1) GDPR and there are no overriding legitimate + grounds for the processing, or the data subject objects to the processing pursuant to Article 21(2) GDPR, + the personal data have been unlawfully processed, personal data must be erased for compliance with a legal + obligation in Union or Member State law to which the controller is subject, the personal data was collected + in relation to the offer of information society services referred to in Article 8(1) GDPR. If the controller + has made the personal data public and is obliged pursuant to paragraph 1 to erase the personal data, the + controller, taking account of available technology and the cost of implementation, shall take reasonable + steps, including technical measures, to inform controllers which are processing the personal data that the + data subject has requested the erasure by such controllers of any links to, or copy or replication of, that + personal data. The right to erasure (“right to be forgotten“) does not apply to the extent that the + processing is necessary: to exercise the right of freedom of expression and information, for compliance with + a legal obligation which requires processing by Union or Member, State law to which the controller is + subject or for the performance of a task carried out in the public interest or in the exercise of official + authority vested in the controller, for reasons of public interest in the area of public health in + accordance with points (h) and (i) of Article 9(2) as well as Article 9(3) GDPR, for archiving purposes in + the public interest, scientific or historical research purposes or statistical purposes in accordance with + Article 89(1) GDPR in so far as the right referred to in paragraph 1 is likely to render impossible or + seriously impair the achievement of the objectives of that processing; or for the establishment, exercise or + defense of legal claims + + + + 7.6 Right to restriction of processing + + + + You have the right to request that we restrict the processing of your personal data if any of the following + conditions apply: the accuracy of the personal data is contested by the data subject, for a period enabling + the controller to verify the accuracy of the personal data, the processing is unlawful and the data subject + opposes the erasure of the personal data and requests the restriction of their use instead, the controller + no longer needs the personal data for the purposes of the processing, but the data is required by the data + subject for the establishment, exercise or defense of legal claims, or the data subject has objected to + processing pursuant to Article 21(1) GDPR pending the verification whether the legitimate grounds of the + controller override those of the data subject In the event that processing has been restricted under the + aforementioned conditions, this personal data shall – with the exception of storage – only be processed with + the data subject’s consent or for the establishment, exercise or defense of legal claims or for the + protection of the rights of another natural or legal person or for reasons of important public interest of + the Union or of a Member State. In order to exercise the right to restrict processing, the data subject may + contact us at any time using the contact details provided above. + + + + 7.7 Right to data portability + + + + You have the right to receive the personal data concerning you which you have provided to us in a + structured, commonly used and machine-readable format and have the right to transmit that data to another + controller without hindrance from the controller to which the personal data have been provided, to the + extent that: the processing is based on consent pursuant to point (a) of Article 6 (1) or point (a) of + Article 9 (2) or on a contract pursuant to point (b) of Article 6 (1) GDPR and the processing is carried out + by automated means. In exercising your right to data portability pursuant to paragraph 1, you have the right + to have the personal data transmitted directly from one controller to another, to the extent that this is + technically feasible. The exercise of the right to data portability does not affect your right to erasure + (“right to be forgotten”). That right shall not apply to processing necessary for the performance of a task + carried out in the public interest or in the exercise of official authority vested in the controller. + + + + 7.8 Right to object + + + + You have the right to object, on grounds relating to your particular situation, at any time to processing of + personal data which concerns you which is based on point (e) or (f) of Article 6 (1) GDPR, including + profiling based on those provisions. If objection is made, the controller will no longer process the + personal data unless the controller demonstrates compelling legitimate grounds for the processing which + override the interests, rights and freedoms of the data subject or for the establishment, exercise or + defense of legal claims. In the event that personal data is processed for direct marketing purposes, you + have the right to object at any time to processing of personal data concerning you for such marketing. This + also applies to profiling to the extent that it is related to such direct marketing. If you object to + processing for direct marketing purposes, your personal data shall no longer be processed for such purposes. + Regarding the use of information society services, and notwithstanding Directive 2002/58/EC, you can + exercise your right to object by automated means using technical specifications. Where personal data are + processed for scientific or historical research purposes or statistical purposes pursuant to Article 89 (1), + you, on grounds relating to your particular situation, have the right to object to processing of personal + data concerning you, unless the processing is necessary for the performance of a task carried out for + reasons of public interest. The right of objection can be exercised at any time by contacting the respective + controller. + + + + 7.9 Automated individual decision-making, including profiling + + + + You have the right not to be subject to a decision based solely on automated processing, including + profiling, which produces legal effects for you or similarly significantly affects you. This does not apply + if the decision: is necessary for entering into, or performance of, a contract between the data subject and + a data controller, is authorized by Union or Member State law to which the controller is subject and which + also lays down suitable measures to safeguard the data subject’s rights and freedoms and legitimate + interests, or is based on the data subject’s explicit consent. The controller shall implement suitable + measures to safeguard the data subject’s rights and freedoms and legitimate interests, at least the right to + obtain human intervention on the part of the controller, to express his or her point of view and to contest + the decision. This right can be exercised by the data subject at any time by contacting the respective + controller. + + + + 7.10 Right to lodge a complaint with a supervisory authority + + + + You also have the right, without prejudice to any other administrative or judicial remedy, to lodge a + complaint with a supervisory authority, in particular in the Member State of your habitual residence, place + of work or place of the alleged infringement if you as data subject consider that the processing of personal + data relating to you infringes this Regulation. + + + + 7.11 Right to effective judicial remedy + + + + Without prejudice to any other available administrative or judicial remedy, including the right to lodge a + complaint with a supervisory authority pursuant to Article 77 GDPR, you have the right to an effective + judicial remedy if you consider that your rights under this Regulation have been infringed as a result of + the processing of your personal data in breach of this Regulation. + + + Submitting requests + + Email privacy@open-assistant.io + +
+
+ + ); +}; + +PrivacyPolicy.getLayout = (page) => ( +
+
+ {page} +
+
+); + +export default PrivacyPolicy; diff --git a/website/src/pages/terms-of-service.tsx b/website/src/pages/terms-of-service.tsx new file mode 100644 index 00000000..b2d668a5 --- /dev/null +++ b/website/src/pages/terms-of-service.tsx @@ -0,0 +1,159 @@ +import { Container, Heading } from "@chakra-ui/react"; +import Head from "next/head"; +import { Footer } from "src/components/Footer"; +import { Header } from "src/components/Header"; + +const TermsOfService = () => { + return ( + <> + + Open Assistant Terms of Service + + +
+ + + Terms Of Service + + 1. Scope of application, amendments + + 1.1. LAION (association in formation), Marie-Henning-Weg 143, 21035 Hamburg (hereinafter referred to as: + "LAION") operates an online portal for the producing a machine learning model called Open + Assistant using crowdsourced data. + + + 1.2. The present terms of use regulate the user relationship between the users of the portal and LAION. + + + 1.3. LAION reserves the right to amend these Terms of Use at any time, also with regard to persons already + registered, if this becomes necessary due to changes in the law, changes in jurisdiction, changes in + economic circumstances or gaps in these Terms of Use that subsequently become apparent. The user will be + informed of such changes in good time by e-mail The user has the opportunity to object to the changes within + 14 days of receipt of this e-mail. If the user does not object to the changes and continues to use the + portal after expiry of the objection period, the changes shall be deemed to have been agreed effectively + from the expiry of the period. If the user objects to the changes within the two-week period, LAION shall be + entitled to exclude the user from using the portal. The user shall be informed of these effects once again + in the e-mail. + + 2. Subject of use, availability of the service + + 2.1. The portal serves as a platform for creating data to train an interactive agent for scientific + purposes. All text and prompt generated through the service are used for scientific purposes, in particular + for the optimization of the AI. + + + 2.2. The input of texts on the portal and the subsequent generation of text by the artificial intelligence + provided by the portal do not give rise to any works protected by copyright. The user who has entered the + text for the generation of the text shall have neither the exclusive rights of use nor any rights of an + author to the generated text. + + + 2.3. LAION shall endeavour to ensure that the portal can be used as uninterruptedly as possible. However, + there shall be no legal claim to the use of the portal. LAION reserves the right, at its own discretion, to + change the portal at any time and without notice, to discontinue its operation or to exclude individual + users from using it. Furthermore, it cannot be ruled out that temporary restrictions or interruptions may + occur due to technical faults (such as interruption of the power supply, hardware and software errors, + technical problems in the data lines). + + 3. User obligations + + 3.1. The user may only use the portal for the intended purposes. In particular, he/she may not misuse the + portal. The user undertakes to refrain from generating text that violate criminal law, youth protection + regulations or the applicable laws of the following countries: Federal Republic of Germany, United States of + America (USA), Great Britain, user's place of residence. In particular it is prohibited to enter texts + that lead to the creation of pornographic, violence-glorifying or paedosexual content and/or content that + violates the personal rights of third parties. LAION reserves the right to file a criminal complaint with + the competent authorities in the event of violations. + + + 3.2. The user undertakes not to use any programs, algorithms or other software in connection with the use of + the portal which could interfere with the functioning of the portal. Furthermore, the user shall not take + any measures that may result in an unreasonable or excessive load on the infrastructure of the portal or may + interfere with it in a disruptive manner. + + + 3.3. If a user notices obvious errors in the portal which could lead to misuse of the portal or the contents + contained therein, the user shall be obliged to report the error to LAION without delay. + + + 3.4. The use, distribution, storage, forwarding, editing and/or other use of images that violate these terms + of use is prohibited. + + 4. Liability + + 4.1. LAION accepts no liability for the accuracy, completeness, reliability, up-to-dateness and usability of + the content. + + + 4.2. LAION shall be liable without limitation for intent and gross negligence. In the case of simple + negligence, LAION shall only be liable for damage resulting from injury to life, limb or health or an + essential contractual obligation (obligation the fulfillment of which makes the proper performance of the + contract possible in the first place and on the observance of which the contractual partner regularly trusts + and may trust). + + + 4.3. In the event of a breach of material contractual obligations due to simple negligence, the liability of + LAION shall be limited to the amount of the foreseeable, typically occurring damage. In all other respects + liability shall be excluded. + + + 4.4. The above limitations of liability shall also apply in favour of the legal representatives and + vicarious agents of LAION. + + + 4.5. LAION shall not be liable for the loss of data of the user. The user shall be solely responsible for + the secure storage of his/her data. + + + 4.6 LAION shall not be liable for any damages incurred by the user as a result of the violation of these + terms of use. + + + 4.7 LAION shall not be liable for the use of content generated on the portal by text input outside the + portal. In particular, LAION shall not be liable for any damages incurred by the user due to the assumption + of copyrights or exclusive rights of use. + + 5. Data protection + + 5.1. LAION processes the personal data of users in accordance with the provisions of data protection law. + Detailed information can be found in the privacy policy, available at: /privacy-policy. + + + 5.2 The user expressly agrees that communication within the scope of and for the purpose of the user + relationship between him/her and LAION may also take place via unencrypted e-mails. The user is aware that + unencrypted e-mails only offer limited security and confidentiality. + + 6. Final provisions + + 6.1 The contractual relationship shall be governed exclusively by the law of the Federal Republic of Germany + to the exclusion of the UN Convention on Contracts for the International Sale of Goods. + + + 6.2 Should individual provisions of these GTC including this provision be or become invalid in whole or in + part, the validity of the remaining provisions shall remain unaffected. The invalid or missing provisions + shall be replaced by the respective statutory provisions. + + + 6.3 If the customer is a merchant, a legal entity under public law or a special fund under public law, the + place of jurisdiction for all disputes arising from and in connection with contracts concluded under these + terms of use shall be the registered office of LAION. + + Status: 1st January 2023 + +
+ + ); +}; + +TermsOfService.getLayout = (page) => ( +
+
+ {page} +
+
+); + +export default TermsOfService; From 7f1644e38d301ee2bea0178062e7f0f8177a911c Mon Sep 17 00:00:00 2001 From: Keith Stevens Date: Tue, 3 Jan 2023 08:40:59 +0900 Subject: [PATCH 03/47] Factoring out the re-used layout --- website/src/components/Layout.tsx | 8 ++++++++ website/src/pages/index.tsx | 9 ++------- website/src/pages/privacy-policy.tsx | 9 ++------- website/src/pages/terms-of-service.tsx | 9 ++------- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/website/src/components/Layout.tsx b/website/src/components/Layout.tsx index 5f6f66b4..3564d765 100644 --- a/website/src/components/Layout.tsx +++ b/website/src/components/Layout.tsx @@ -17,4 +17,12 @@ export const getDefaultLayout = (page: React.ReactElement) => ( ); +export const getTransparentHeaderLayout = (page: React.ReactElement) => ( +
+
+ {page} +
+
+); + export const noLayout = (page: React.ReactElement) => page; diff --git a/website/src/pages/index.tsx b/website/src/pages/index.tsx index 8c2c34b5..20a4068c 100644 --- a/website/src/pages/index.tsx +++ b/website/src/pages/index.tsx @@ -5,6 +5,7 @@ import { Faq } from "src/components/Faq"; import { Footer } from "src/components/Footer"; import { Header } from "src/components/Header"; import { Hero } from "src/components/Hero"; +import { getTransparentHeaderLayout } from "src/components/Layout"; import { TaskSelection } from "src/components/TaskSelection"; const Home = () => { @@ -34,12 +35,6 @@ const Home = () => { ); }; -Home.getLayout = (page) => ( -
-
- {page} -
-
-); +Home.getLayout = getTransparentHeaderLayout; export default Home; diff --git a/website/src/pages/privacy-policy.tsx b/website/src/pages/privacy-policy.tsx index 42f439fc..dcb3bc19 100644 --- a/website/src/pages/privacy-policy.tsx +++ b/website/src/pages/privacy-policy.tsx @@ -2,6 +2,7 @@ import { Container, Heading } from "@chakra-ui/react"; import Head from "next/head"; import { Footer } from "src/components/Footer"; import { Header } from "src/components/Header"; +import { getTransparentHeaderLayout } from "src/components/Layout"; const PrivacyPolicy = () => { return ( @@ -403,12 +404,6 @@ const PrivacyPolicy = () => { ); }; -PrivacyPolicy.getLayout = (page) => ( -
-
- {page} -
-
-); +PrivacyPolicy.getLayout = getTransparentHeaderLayout; export default PrivacyPolicy; diff --git a/website/src/pages/terms-of-service.tsx b/website/src/pages/terms-of-service.tsx index b2d668a5..d97c8d34 100644 --- a/website/src/pages/terms-of-service.tsx +++ b/website/src/pages/terms-of-service.tsx @@ -2,6 +2,7 @@ import { Container, Heading } from "@chakra-ui/react"; import Head from "next/head"; import { Footer } from "src/components/Footer"; import { Header } from "src/components/Header"; +import { getTransparentHeaderLayout } from "src/components/Layout"; const TermsOfService = () => { return ( @@ -148,12 +149,6 @@ const TermsOfService = () => { ); }; -TermsOfService.getLayout = (page) => ( -
-
- {page} -
-
-); +TermsOfService.getLayout = getTransparentHeaderLayout; export default TermsOfService; From 1fdb3c48763803a0b85a70eb6bef98bce38e959d Mon Sep 17 00:00:00 2001 From: Kostia Date: Tue, 3 Jan 2023 03:11:50 +0200 Subject: [PATCH 04/47] Added collapsable text for text that's too long --- website/src/components/CollapsableText.tsx | 37 +++++++++++++++++++ website/src/components/Sortable/Sortable.tsx | 3 +- .../pages/evaluate/rank_assistant_replies.tsx | 2 + 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 website/src/components/CollapsableText.tsx diff --git a/website/src/components/CollapsableText.tsx b/website/src/components/CollapsableText.tsx new file mode 100644 index 00000000..77792ede --- /dev/null +++ b/website/src/components/CollapsableText.tsx @@ -0,0 +1,37 @@ +import { Button, Container, useDisclosure } from "@chakra-ui/react" +import { + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalBody, + ModalCloseButton, + } from '@chakra-ui/react' +import React from "react"; + +export const CollapsableText = ({text, maxLength=220}) => { + const { isOpen, onOpen, onClose } = useDisclosure() + if (typeof(text) != 'string' || text.length <= maxLength) { + return text; + } else { + return ( + <> + {text.substring(0, maxLength-3)} + + + + + + Full Text + + + {text} + + + + + + + ); + } + } diff --git a/website/src/components/Sortable/Sortable.tsx b/website/src/components/Sortable/Sortable.tsx index 615b0853..b8c38932 100644 --- a/website/src/components/Sortable/Sortable.tsx +++ b/website/src/components/Sortable/Sortable.tsx @@ -17,6 +17,7 @@ import { verticalListSortingStrategy, } from "@dnd-kit/sortable"; import { ReactNode, useEffect, useState } from "react"; +import { CollapsableText } from "../CollapsableText"; import { SortableItem } from "./SortableItem"; @@ -61,7 +62,7 @@ export const Sortable = ({ items, onChange }: SortableProps) => { {itemsWithIds.map(({ id, item }) => ( - {item} + ))} diff --git a/website/src/pages/evaluate/rank_assistant_replies.tsx b/website/src/pages/evaluate/rank_assistant_replies.tsx index 017deb3f..109240a0 100644 --- a/website/src/pages/evaluate/rank_assistant_replies.tsx +++ b/website/src/pages/evaluate/rank_assistant_replies.tsx @@ -57,6 +57,8 @@ const RankAssistantReplies = () => { const replies = tasks[0].task.replies as string[]; const endTask = tasks[tasks.length - 1]; + // Added for testing purposes, will be removed: + replies.push("My test text that is very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long."); return ( <> From 57d3b92fa4fdd83d29ec194bd00a5ca3b3015a87 Mon Sep 17 00:00:00 2001 From: Janosh Riebesell Date: Mon, 2 Jan 2023 17:31:36 -0800 Subject: [PATCH 05/47] pre-commit hook black->black-jupyter --- .pre-commit-config.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6d885cdc..329918ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,10 +26,7 @@ # # /WARNING! -exclude: "build|stubs|^bot/templates/|^notebooks/.*\\.ipynb$" - -default_language_version: - python: python3 +exclude: build|stubs|^bot/templates/|^notebooks/.*\\.ipynb$ repos: - repo: https://github.com/pre-commit/pre-commit-hooks @@ -42,12 +39,12 @@ repos: # and which break the standard YAML check. The alternative would be to # skip any unsafe errors (and thus break YAML compatibility) or use # some other checker that may not work in general. - exclude: "^copilot/web/addons/.*$" + exclude: ^copilot/web/addons/.*$ - id: check-json - id: check-case-conflict - id: detect-private-key - id: fix-encoding-pragma - args: ["--remove"] + args: [--remove] - id: forbid-submodules - id: mixed-line-ending - id: requirements-txt-fixer @@ -57,13 +54,13 @@ repos: - id: check-symlinks - id: check-merge-conflict - id: check-added-large-files - args: ["--maxkb=1024"] + args: [--maxkb=1024] - id: end-of-file-fixer - repo: https://github.com/psf/black rev: 22.12.0 hooks: - - id: black + - id: black-jupyter - repo: https://github.com/pycqa/flake8 rev: 6.0.0 @@ -79,7 +76,7 @@ repos: rev: v2.7.1 hooks: - id: prettier - args: ["--prose-wrap=always", "--write"] + args: [--prose-wrap=always, --write] - repo: local hooks: From c68bbe75d09368d6f1a156769c1092a8da8872ac Mon Sep 17 00:00:00 2001 From: Kostia Date: Tue, 3 Jan 2023 03:42:36 +0200 Subject: [PATCH 06/47] Ran pre-commit to make prettier happy. --- website/src/components/CollapsableText.tsx | 63 +++++++++---------- .../pages/evaluate/rank_assistant_replies.tsx | 4 +- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/website/src/components/CollapsableText.tsx b/website/src/components/CollapsableText.tsx index 77792ede..9fd28b93 100644 --- a/website/src/components/CollapsableText.tsx +++ b/website/src/components/CollapsableText.tsx @@ -1,37 +1,30 @@ -import { Button, Container, useDisclosure } from "@chakra-ui/react" -import { - Modal, - ModalOverlay, - ModalContent, - ModalHeader, - ModalBody, - ModalCloseButton, - } from '@chakra-ui/react' +import { Button, Container, useDisclosure } from "@chakra-ui/react"; +import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton } from "@chakra-ui/react"; import React from "react"; -export const CollapsableText = ({text, maxLength=220}) => { - const { isOpen, onOpen, onClose } = useDisclosure() - if (typeof(text) != 'string' || text.length <= maxLength) { - return text; - } else { - return ( - <> - {text.substring(0, maxLength-3)} - - - - - - Full Text - - - {text} - - - - - - - ); - } - } +export const CollapsableText = ({ text, maxLength = 220 }) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + if (typeof text != "string" || text.length <= maxLength) { + return text; + } else { + return ( + <> + {text.substring(0, maxLength - 3)} + + + + + + Full Text + + {text} + + + + + + ); + } +}; diff --git a/website/src/pages/evaluate/rank_assistant_replies.tsx b/website/src/pages/evaluate/rank_assistant_replies.tsx index 109240a0..8e015976 100644 --- a/website/src/pages/evaluate/rank_assistant_replies.tsx +++ b/website/src/pages/evaluate/rank_assistant_replies.tsx @@ -58,7 +58,9 @@ const RankAssistantReplies = () => { const replies = tasks[0].task.replies as string[]; const endTask = tasks[tasks.length - 1]; // Added for testing purposes, will be removed: - replies.push("My test text that is very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long."); + replies.push( + "My test text that is very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very very long." + ); return ( <> From e7515fd08e2f48b403d7be3a13508d35c2d67b8b Mon Sep 17 00:00:00 2001 From: Alex Ott <66271487+AlexanderHOtt@users.noreply.github.com> Date: Tue, 3 Jan 2023 04:13:44 -0800 Subject: [PATCH 07/47] Feat/better messages (#318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve messages and UX * move log statement Co-authored-by: Andreas Köpf --- discord-bot/.env.example | 2 +- discord-bot/bot/bot.py | 19 +- discord-bot/bot/extensions/guild_settings.py | 1 - discord-bot/bot/extensions/text_labels.py | 5 +- discord-bot/bot/extensions/work.py | 346 +++++++++---------- discord-bot/bot/messages.py | 207 +++++++++++ discord-bot/bot/utils.py | 7 - 7 files changed, 394 insertions(+), 193 deletions(-) create mode 100644 discord-bot/bot/messages.py diff --git a/discord-bot/.env.example b/discord-bot/.env.example index ec114c8f..8474ee90 100644 --- a/discord-bot/.env.example +++ b/discord-bot/.env.example @@ -1,7 +1,7 @@ BOT_TOKEN= DECLARE_GLOBAL_COMMANDS= OWNER_IDS=[, ] -PREFIX="/" # Don't change, this allows for slash commands in DMs +PREFIX="/" # DO NOT LEAVE EMPTY, slash command prefix in DMs OASST_API_URL="http://localhost:8080" # No trailing '/' OASST_API_KEY="" diff --git a/discord-bot/bot/bot.py b/discord-bot/bot/bot.py index df3c5f2f..8c604e1a 100644 --- a/discord-bot/bot/bot.py +++ b/discord-bot/bot/bot.py @@ -6,7 +6,7 @@ import hikari import lightbulb import miru from bot.settings import Settings -from bot.utils import EMPTY, mention +from bot.utils import mention from oasst_shared.api_client import OasstApiClient settings = Settings() @@ -34,8 +34,11 @@ async def on_starting(event: hikari.StartingEvent): bot.d.oasst_api = OasstApiClient(settings.oasst_api_url, settings.oasst_api_key) - # A set of user id's that are currently doing work. - bot.d.currently_working = set() + # A `dict[hikari.Message | None, UUID | None]]` that maps user IDs to (task msg ID, task UUIDs). + # Either both are `None` or both are not `None`. + # If both are `None`, the user is not currently selecting a task. + # TODO: Grow this on startup so we don't have to re-allocate memory every time it needs to grow + bot.d.currently_working = {} @bot.listen() @@ -50,13 +53,13 @@ async def _send_error_embed( ) -> None: ctx.command embed = hikari.Embed( - title=f"`{exception.__class__.__name__}` Error{f' in `{ctx.command.name}`' if ctx.command else '' }", + title=f"`{exception.__class__.__name__}` Error{f' in `/{ctx.command.name}`' if ctx.command else '' }", description=content, color=0xFF0000, timestamp=datetime.now().astimezone(), ).set_author(name=ctx.author.username, url=str(ctx.author.avatar_url)) - await ctx.respond(EMPTY, embed=embed) + await ctx.respond(embed=embed) @bot.listen(lightbulb.CommandErrorEvent) @@ -65,6 +68,8 @@ async def on_error(event: lightbulb.CommandErrorEvent) -> None: # Unwrap the exception to get the original cause exc = event.exception.__cause__ or event.exception ctx = event.context + if not ctx.bot.rest.is_alive: + return if isinstance(event.exception, lightbulb.CommandInvocationError): if not event.context.command: @@ -114,6 +119,8 @@ async def on_error(event: lightbulb.CommandErrorEvent) -> None: ctx, ) elif isinstance(exc, lightbulb.errors.MissingRequiredAttachment): - await _send_error_embed("Not enough attachemnts were supplied to this command.", exc, ctx) + await _send_error_embed("Not enough attachments were supplied to this command.", exc, ctx) + elif isinstance(exc, lightbulb.errors.CommandNotFound): + await ctx.respond(f"`/{exc.invoked_with}` is not a valid command. Use `/help` to see a list of commands.") else: raise exc diff --git a/discord-bot/bot/extensions/guild_settings.py b/discord-bot/bot/extensions/guild_settings.py index 62f21305..5940f33a 100644 --- a/discord-bot/bot/extensions/guild_settings.py +++ b/discord-bot/bot/extensions/guild_settings.py @@ -78,7 +78,6 @@ async def log_channel(ctx: lightbulb.SlashContext) -> None: # if the bot's permissions for this channel don't contain SEND_MESSAGE # This will also filter out categories and voice channels - print(permissions_in(ch, own_member) & hikari.Permissions.SEND_MESSAGES) if not permissions_in(ch, own_member) & hikari.Permissions.SEND_MESSAGES: await ctx.respond(f"I don't have permission to send messages in {ch.mention}.") return diff --git a/discord-bot/bot/extensions/text_labels.py b/discord-bot/bot/extensions/text_labels.py index 388a93f0..a2607aec 100644 --- a/discord-bot/bot/extensions/text_labels.py +++ b/discord-bot/bot/extensions/text_labels.py @@ -7,7 +7,6 @@ import lightbulb import miru from aiosqlite import Connection from bot.db.schemas import GuildSettings -from bot.utils import EMPTY from loguru import logger plugin = lightbulb.Plugin( @@ -74,7 +73,7 @@ class LabelModal(miru.Modal): ) channel = await context.bot.rest.fetch_channel(guild_settings.log_channel_id) assert isinstance(channel, hikari.TextableChannel) - await channel.send(EMPTY, embed=embed) + await channel.send(embed=embed) class LabelSelect(miru.View): @@ -164,7 +163,7 @@ async def label_message_text(ctx: lightbulb.MessageContext): msg.content, timeout=60, ) - resp = await ctx.respond(EMPTY, embed=embed, components=label_select_view, flags=hikari.MessageFlag.EPHEMERAL) + resp = await ctx.respond(embed=embed, components=label_select_view, flags=hikari.MessageFlag.EPHEMERAL) await label_select_view.start(await resp.message()) await label_select_view.wait() diff --git a/discord-bot/bot/extensions/work.py b/discord-bot/bot/extensions/work.py index c905e7a0..6b7f8ea4 100644 --- a/discord-bot/bot/extensions/work.py +++ b/discord-bot/bot/extensions/work.py @@ -1,14 +1,27 @@ """Work plugin for collecting user data.""" import asyncio import typing as t -from datetime import datetime +from uuid import UUID import hikari import lightbulb import lightbulb.decorators import miru from aiosqlite import Connection -from bot.utils import EMPTY +from bot.messages import ( + assistant_reply_message, + confirm_ranking_response_message, + confirm_text_response_message, + initial_prompt_message, + invalid_user_input_embed, + plain_embed, + prompter_reply_message, + rank_assistant_reply_message, + rank_initial_prompts_message, + rank_prompter_reply_message, + task_complete_embed, +) +from bot.settings import Settings from loguru import logger from oasst_shared.api_client import OasstApiClient, TaskType from oasst_shared.schemas import protocol as protocol_schema @@ -19,6 +32,8 @@ plugin = lightbulb.Plugin("WorkPlugin") MAX_TASK_TIME = 60 * 60 # 1 hour MAX_TASK_ACCEPT_TIME = 60 # 1 minute +settings = Settings() + @plugin.command @lightbulb.option( @@ -33,25 +48,50 @@ MAX_TASK_ACCEPT_TIME = 60 # 1 minute @lightbulb.implements(lightbulb.SlashCommand, lightbulb.PrefixCommand) async def work(ctx: lightbulb.Context): """Create and handle a task.""" - # make sure the user isn't currently doing a task - currently_working: set[hikari.Snowflakeish] = ctx.bot.d.currently_working + # Only send this message if started from a server + if ctx.guild_id is not None: + await ctx.respond(embed=plain_embed("Sending you a task, check your DMs"), flags=hikari.MessageFlag.EPHEMERAL) + + # make sure the user isn't currently doing a task, and if they are, ask if they want to cancel it + currently_working: dict[ + hikari.Snowflakeish, tuple[hikari.Message | None, UUID | None] + ] = ctx.bot.d.currently_working + + oasst_api: OasstApiClient = ctx.bot.d.oasst_api if ctx.author.id in currently_working: - await ctx.respond( - "You are already performing a task. Please complete that one first.", flags=hikari.MessageFlag.EPHEMERAL + yn_view = YesNoView(timeout=MAX_TASK_ACCEPT_TIME) + msg = await ctx.author.send( + embed=plain_embed("You are already working. Would you like to cancel your old task start a new one?"), + flags=hikari.MessageFlag.EPHEMERAL, + components=yn_view, ) - return + await yn_view.start(msg) + await yn_view.wait() - currently_working.add(ctx.author.id) + match yn_view.choice: + case False | None: + return + case True: + old_msg, task_id = currently_working[ctx.author.id] + if old_msg is not None: + logger.info(f"User {ctx.author.id} cancelled task {task_id}, deleting message {old_msg.id}") + map(lambda c: c, old_msg.components) + await old_msg.delete() + if task_id is not None: + await oasst_api.nack_task(task_id, reason="user cancelled") + await msg.delete() + + currently_working[ctx.author.id] = (None, None) + + # Create a TaskRequestType from the stringified enum value task_type: TaskRequestType = TaskRequestType(ctx.options.type.split(".")[-1]) - await ctx.respond("Sending you a task, check your DMs", flags=hikari.MessageFlag.EPHEMERAL) logger.debug(f"Starting task_type: {task_type!r}") - try: await _handle_task(ctx, task_type) finally: - currently_working.remove(ctx.author.id) + del currently_working[ctx.author.id] async def _handle_task(ctx: lightbulb.Context, task_type: TaskRequestType) -> None: @@ -71,38 +111,79 @@ async def _handle_task(ctx: lightbulb.Context, task_type: TaskRequestType) -> No task, msg_id = await _select_task(ctx, task_type) if task is None: + # User cancelled return # Task action loop completed = False while not completed: - await ctx.author.send("Please type your response here:") + await ctx.author.send(embed=plain_embed("Please type your response here")) try: event = await ctx.bot.wait_for( - hikari.DMMessageCreateEvent, timeout=MAX_TASK_TIME, predicate=lambda e: e.author.id == ctx.author.id + hikari.DMMessageCreateEvent, + timeout=MAX_TASK_TIME, + predicate=lambda e: e.author.id == ctx.author.id + and not (e.message.content or "").startswith(settings.prefix), ) except asyncio.TimeoutError: - await ctx.author.send("Task timed out. Exiting") + await ctx.author.send(embed=plain_embed("Task timed out. Exiting")) await oasst_api.nack_task(task.id, reason="timed out") logger.info(f"Task {task.id} timed out") return # Invalid response - if event.content is None or not _validate_user_input(event.content, task): - await ctx.author.send("Invalid response") + valid, err_msg = _validate_user_input(event.content, task) + if not valid or event.content is None: + + await ctx.author.send(embed=invalid_user_input_embed(err_msg)) continue logger.debug(f"Successful user input received: {event.content}") + # Confirm user input + if isinstance(task, protocol_schema.RankConversationRepliesTask): + 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.ReplyToConversationTask | protocol_schema.InitialPromptTask): + content = confirm_text_response_message(event.content) + else: + logger.critical(f"Unknown task type: {task.type}") + raise ValueError(f"Unknown task type: {task.type}") + + confirm_resp_view = YesNoView(timeout=MAX_TASK_TIME) + msg = await ctx.author.send(content, components=confirm_resp_view) + await confirm_resp_view.start(msg) + await confirm_resp_view.wait() + + match confirm_resp_view.choice: + case False | None: + continue + case True: + await msg.delete() # buttons are already gone + # Send the response to the backend - reply = protocol_schema.TextReplyToMessage( - message_id=str(msg_id), - user_message_id=str(event.message_id), - user=protocol_schema.User( - auth_method="discord", id=str(ctx.author.id), display_name=ctx.author.username - ), - text=event.content, - ) + if isinstance(task, protocol_schema.RankConversationRepliesTask | protocol_schema.RankInitialPromptsTask): + reply = protocol_schema.MessageRanking( + message_id=str(msg_id), + ranking=[int(r) - 1 for r in event.content.replace(" ", "").split(",")], + 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), + user_message_id=str(event.message_id), + user=protocol_schema.User( + auth_method="discord", id=str(ctx.author.id), display_name=ctx.author.username + ), + text=event.content, + ) + else: + logger.critical(f"Unexpected task type received: {task.type}") + raise ValueError(f"Unexpected task type received: {task.type}") + logger.debug(f"Sending reply to backend: {reply!r}") # Get next task @@ -110,7 +191,7 @@ async def _handle_task(ctx: lightbulb.Context, task_type: TaskRequestType) -> No logger.info(f"New task {new_task}") if new_task.type == TaskType.done: - await ctx.author.send("Task completed") + await ctx.author.send(embed=plain_embed("Task completed")) completed = True continue else: @@ -127,33 +208,20 @@ async def _handle_task(ctx: lightbulb.Context, task_type: TaskRequestType) -> No for id in log_channel_ids ] - done_embed = ( - hikari.Embed( - title="Task Completion", - description=f"`{task.type}` completed by {ctx.author.mention}", - color=hikari.Color(0x00FF00), - timestamp=datetime.now().astimezone(), - ) - .add_field("Total Tasks", "0", inline=True) - .add_field("Server Ranking", "0/0", inline=True) - .add_field("Global Ranking", "0/0", inline=True) - .set_footer(f"Task ID: {task.id}") - ) + done_embed = task_complete_embed(task, ctx.author.mention) # This will definitely get the bot rate limited, but that's a future problem - asyncio.gather( - *(ch.send(EMPTY, embed=done_embed) for ch in channels if isinstance(ch, hikari.TextableChannel)) - ) + asyncio.gather(*(ch.send(embed=done_embed) for ch in channels if isinstance(ch, hikari.TextableChannel))) # ask the user if they want to do another task - choice_view = ChoiceView(timeout=MAX_TASK_ACCEPT_TIME) - msg = await ctx.author.send("Would you like another task?", components=choice_view) - await choice_view.start(msg) - await choice_view.wait() + another_task_view = YesNoView(timeout=MAX_TASK_ACCEPT_TIME) + msg = await ctx.author.send(embed=plain_embed("Would you like another task?"), components=another_task_view) + await another_task_view.start(msg) + await another_task_view.wait() - match choice_view.choice: + match another_task_view.choice: case False | None: done = True - await ctx.author.send("Exiting, goodbye!") + await msg.edit(embed=plain_embed("Exiting, goodbye!")) case True: pass @@ -166,10 +234,12 @@ async def _select_task( logger.debug(f"Starting task selection for {task_type}") # Loop until the user accepts a task, cancels, or times out + msg: hikari.UndefinedOr[hikari.Message] = hikari.UNDEFINED while True: logger.debug(f"Requesting task of type {task_type}") task = await oasst_api.fetch_task(task_type, user) - resp, msg_id = await _send_task(ctx, task) + resp, msg = await _send_task(ctx, task, msg) + msg_id = str(msg.id) logger.debug(f"User choice: {resp}") match resp: @@ -181,25 +251,24 @@ async def _select_task( case "next": logger.info(f"Task {task.id} rejected, sending NACK") await oasst_api.nack_task(task.id, "rejected") - await ctx.author.send("Sending next task...") continue case "cancel": logger.info(f"Task {task.id} canceled, sending NACK") await oasst_api.nack_task(task.id, "canceled") - await ctx.author.send("Task canceled. Exiting") + await ctx.author.send(embed=plain_embed("Task canceled. Exiting")) return None, msg_id case None: logger.info(f"Task {task.id} timed out, sending NACK") await oasst_api.nack_task(task.id, "timed out") - await ctx.author.send("Task timed out. Exiting") + await ctx.author.send(embed=plain_embed("Task timed out. Exiting")) return None, msg_id async def _send_task( - ctx: lightbulb.Context, task: protocol_schema.Task -) -> tuple[t.Literal["accept", "next", "cancel"] | None, str]: + ctx: lightbulb.Context, task: protocol_schema.Task, msg: hikari.UndefinedOr[hikari.Message] +) -> tuple[t.Literal["accept", "next", "cancel"] | None, hikari.Message]: """Send a task to the user. Returns the user's choice and the message ID of the task message. @@ -208,37 +277,38 @@ async def _send_task( # but the tasks aren't discord specific so that doesn't really make sense. embed: hikari.UndefinedOr[hikari.Embed] = hikari.UNDEFINED + content: hikari.UndefinedOr[str] = hikari.UNDEFINED # Create an embed based on the task's type if task.type == TaskRequestType.initial_prompt: assert isinstance(task, protocol_schema.InitialPromptTask) logger.debug("sending initial prompt task") - embed = _initial_prompt_embed(task) + content = initial_prompt_message(task) elif task.type == TaskRequestType.rank_initial_prompts: assert isinstance(task, protocol_schema.RankInitialPromptsTask) logger.debug("sending rank initial prompt task") - embed = _rank_initial_prompt_embed(task) + content = rank_initial_prompts_message(task) elif task.type == TaskRequestType.rank_prompter_replies: assert isinstance(task, protocol_schema.RankPrompterRepliesTask) logger.debug("sending rank user reply task") - embed = _rank_prompter_reply_embed(task) + content = rank_prompter_reply_message(task) elif task.type == TaskRequestType.rank_assistant_replies: assert isinstance(task, protocol_schema.RankAssistantRepliesTask) logger.debug("sending rank assistant reply task") - embed = _rank_assistant_reply_embed(task) + content = rank_assistant_reply_message(task) elif task.type == TaskRequestType.prompter_reply: assert isinstance(task, protocol_schema.PrompterReplyTask) logger.debug("sending user reply task") - embed = _prompter_reply_embed(task) + content = prompter_reply_message(task) elif task.type == TaskRequestType.assistant_reply: assert isinstance(task, protocol_schema.AssistantReplyTask) logger.debug("sending assistant reply task") - embed = _assistant_reply_embed(task) + content = assistant_reply_message(task) elif task.type == TaskRequestType.summarize_story: raise NotImplementedError @@ -250,24 +320,34 @@ async def _send_task( raise ValueError(f"unknown task type {task.type}") view = TaskAcceptView(timeout=MAX_TASK_ACCEPT_TIME) - msg = await ctx.author.send( - EMPTY, - embed=embed, - components=view, - ) + if not msg: + msg = await ctx.author.send( + content, + embed=embed, + components=view, + ) + else: + await msg.edit( + content, + embed=embed, + components=view, + ) assert msg is not None + # Set the choice id as the current msg id + ctx.bot.d.currently_working[ctx.author.id] = (msg, task.id) + await view.start(msg) await view.wait() - return view.choice, str(msg.id) + return view.choice, msg -def _validate_user_input(content: str | None, task: protocol_schema.Task) -> bool: - """Returns whether the user's input is valid for the task type.""" +def _validate_user_input(content: str | None, task: protocol_schema.Task) -> tuple[bool, str]: + """Returns whether the user's input is valid for the task type and an error message.""" if content is None: - return False + return False, "No input provided" # User message input if ( @@ -279,22 +359,28 @@ def _validate_user_input(content: str | None, task: protocol_schema.Task) -> boo task, protocol_schema.InitialPromptTask | protocol_schema.PrompterReplyTask | protocol_schema.AssistantReplyTask, ) - return len(content) > 0 + return len(content) > 0, "Message must be at least one character long." # Ranking tasks elif task.type == TaskRequestType.rank_prompter_replies or task.type == TaskRequestType.rank_assistant_replies: assert isinstance(task, protocol_schema.RankPrompterRepliesTask | protocol_schema.RankAssistantRepliesTask) num_replies = len(task.replies) - rankings = content.split(",") - return set(rankings) == {str(i) for i in range(1, num_replies + 1)} and len(rankings) == num_replies + rankings = content.replace(" ", "").split(",") + return ( + set(rankings) == {str(i) for i in range(1, num_replies + 1)} and len(rankings) == num_replies, + "Message must contain numbers for all replies.", + ) elif task.type == TaskRequestType.rank_initial_prompts: assert isinstance(task, protocol_schema.RankInitialPromptsTask) num_prompts = len(task.prompts) - rankings = content.split(",") - return set(rankings) == {str(i) for i in range(1, num_prompts + 1)} and len(rankings) == num_prompts + rankings = content.replace(" ", "").split(",") + return ( + set(rankings) == {str(i) for i in range(1, num_prompts + 1)} and len(rankings) == num_prompts, + "Message must contain numbers for all prompts.", + ) elif task.type == TaskRequestType.summarize_story: raise NotImplementedError @@ -318,22 +404,29 @@ class TaskAcceptView(miru.View): async def accept_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: logger.info("Accept button pressed") self.choice = "accept" + await ctx.message.edit(component=None) self.stop() @miru.button(label="Next Task", custom_id="next_task", row=0, style=hikari.ButtonStyle.SECONDARY) async def next_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: logger.info("Next button pressed") self.choice = "next" + await ctx.message.edit(component=None) self.stop() @miru.button(label="Cancel", custom_id="cancel", row=0, style=hikari.ButtonStyle.DANGER) async def cancel_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: logger.info("Cancel button pressed") self.choice = "cancel" + await ctx.message.edit(component=None) self.stop() + async def on_timeout(self) -> None: + if self.message is not None: + await self.message.edit(component=None) -class ChoiceView(miru.View): + +class YesNoView(miru.View): """View with two buttons: yes and no. The view stops once one of the buttons is pressed and the choice is stored in the `choice` attribute. @@ -344,115 +437,18 @@ class ChoiceView(miru.View): @miru.button(label="Yes", custom_id="yes", style=hikari.ButtonStyle.SUCCESS) async def yes_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: self.choice = True + await ctx.message.edit(component=None) self.stop() @miru.button(label="No", custom_id="no", style=hikari.ButtonStyle.DANGER) async def no_button(self, button: miru.Button, ctx: miru.ViewContext) -> None: self.choice = False + await ctx.message.edit(component=None) self.stop() - -################################################################ -# Template Embeds # -################################################################ - -# TODO: Maybe implement a better way of creating embeds, like `from_json` or something - - -def _initial_prompt_embed(task: protocol_schema.InitialPromptTask) -> hikari.Embed: - return ( - hikari.Embed(title="Initial Prompt", description=f"Hint: {task.hint}", timestamp=datetime.now().astimezone()) - .set_image("https://images.unsplash.com/photo-1455390582262-044cdead277a?w=512") - .set_footer(text=f"OASST Assistant | {task.id}") - ) - - -def _rank_initial_prompt_embed(task: protocol_schema.RankInitialPromptsTask) -> hikari.Embed: - embed = ( - hikari.Embed( - title="Rank Initial Prompt", - description="Rank the following tasks from best to worst (1,2,3,4,5)", - timestamp=datetime.now().astimezone(), - ) - .set_image("https://images.unsplash.com/photo-1455390582262-044cdead277a?w=512") - .set_footer(text=f"OASST Assistant | {task.id}") - ) - - for i, prompt in enumerate(task.prompts): - embed.add_field(name=f"Prompt {i + 1}", value=prompt, inline=False) - - return embed - - -def _rank_prompter_reply_embed(task: protocol_schema.RankPrompterRepliesTask) -> hikari.Embed: - embed = ( - hikari.Embed( - title="Rank User Reply", - description="Rank the following user replies from best to worst. e.g. 1,2,5,3,4", - timestamp=datetime.now().astimezone(), - ) - .set_image("https://images.unsplash.com/photo-1455390582262-044cdead277a?w=512") # TODO: update image - .set_footer(text=f"OASST Assistant | {task.id}") - ) - - for i, reply in enumerate(task.replies): - embed.add_field(name=f"Reply {i + 1}", value=reply, inline=False) - - return embed - - -def _rank_assistant_reply_embed(task: protocol_schema.RankAssistantRepliesTask) -> hikari.Embed: - embed = ( - hikari.Embed( - title="Rank Assistant Reply", - description="Rank the following assistant replies from best to worst. e.g. 1,2,5,3,4", - timestamp=datetime.now().astimezone(), - ) - .set_image("https://images.unsplash.com/photo-1455390582262-044cdead277a?w=512") # TODO: update image - .set_footer(text=f"OASST Assistant | {task.id}") - ) - - for i, reply in enumerate(task.replies): - embed.add_field(name=f"Reply {i + 1}", value=reply, inline=False) - - return embed - - -def _prompter_reply_embed(task: protocol_schema.PrompterReplyTask) -> hikari.Embed: - embed = ( - hikari.Embed( - title="User Reply", - description=f"""\ - Send the next message in the conversation as if you were the user. - {'Hint: ' if task.hint else ''} - """, - timestamp=datetime.now().astimezone(), - ) - # .set_image("https://images.unsplash.com/photo-1455390582262-044cdead277a?w=512") # TODO: change image - .set_footer(text=f"OASST Assistant | {task.id}") - ) - - for message in task.conversation.messages: - embed.add_field(name="Assistant" if message.is_assistant else "User", value=message.text, inline=False) - - return embed - - -def _assistant_reply_embed(task: protocol_schema.AssistantReplyTask) -> hikari.Embed: - embed = ( - hikari.Embed( - title="User Reply", - description="Send the next message in the conversation as if you were the user.", - timestamp=datetime.now().astimezone(), - ) - # .set_image("https://images.unsplash.com/photo-1455390582262-044cdead277a?w=512") # TODO: change image - .set_footer(text=f"OASST Assistant | {task.id}") - ) - - for message in task.conversation.messages: - embed.add_field(name="Assistant" if message.is_assistant else "User", value=message.text, inline=False) - - return embed + async def on_timeout(self) -> None: + if self.message is not None: + await self.message.edit(component=None) def load(bot: lightbulb.BotApp): diff --git a/discord-bot/bot/messages.py b/discord-bot/bot/messages.py new file mode 100644 index 00000000..0f29511a --- /dev/null +++ b/discord-bot/bot/messages.py @@ -0,0 +1,207 @@ +"""All user-facing messages and embeds.""" + +from datetime import datetime + +import hikari +from oasst_shared.schemas import protocol as protocol_schema + +NUMBER_EMOJIS = [":one:", ":two:", ":three:", ":four:", ":five:", ":six:", ":seven:", ":eight:", ":nine:", ":ten:"] +NL = "\n" + +### +# Reusable 'components' +### + + +def _h1(text: str) -> str: + return f"\n:small_blue_diamond: __**{text}**__ :small_blue_diamond:" + + +def _h2(text: str) -> str: + return f"__**{text}**__" + + +def _h3(text: str) -> str: + return f"__{text}__" + + +def _writing_prompt(text: str) -> str: + return f":pencil: _{text}_" + + +def _ranking_prompt(text: str) -> str: + return f":trophy: _{text}_" + + +def _response_prompt(text: str) -> str: + return f":speech_balloon: _{text}_" + + +def _summarize_prompt(text: str) -> str: + return f":notepad_spiral: _{text}_" + + +def _user(text: str | None) -> str: + return f"""\ +:person_red_hair: {_h3("User")}:{f"{NL}> **{text}**" if text is not None else ""} +""" + + +def _assistant(text: str | None) -> str: + return f"""\ +:robot: {_h3("Assistant")}:{f"{NL}> {text}" if text is not None else ""} +""" + + +def _make_ordered_list(items: list[str]) -> list[str]: + return [f"{num} {item}" for num, item in zip(NUMBER_EMOJIS, items)] + + +def _ordered_list(items: list[str]) -> str: + return "\n\n".join(_make_ordered_list(items)) + + +def _conversation(conv: protocol_schema.Conversation) -> str: + return "\n".join([_assistant(msg.text) if msg.is_assistant else _user(msg.text) for msg in conv.messages]) + + +def _hint(hint: str | None) -> str: + return f"{NL}Hint: {hint}" if hint else "" + + +### +# Messages +### + + +def initial_prompt_message(task: protocol_schema.InitialPromptTask) -> str: + """Creates the message that gets sent to users when they request an `initial_prompt` task.""" + return f"""\ + +{_h1("INITIAL PROMPT")} + +{_writing_prompt("Please provide an initial prompt to the assistant.")} +{_hint(task.hint)} +""" + + +def rank_initial_prompts_message(task: protocol_schema.RankInitialPromptsTask) -> str: + """Creates the message that gets sent to users when they request a `rank_initial_prompts` task.""" + return f"""\ + +{_h1("RANK INITIAL PROMPTS")} + +{_ranking_prompt("Reply with the numbers of best to worst prompts separated by commas (example: '4,1,3,2')")} + + +{_ordered_list(task.prompts)} +""" + + +def rank_prompter_reply_message(task: protocol_schema.RankPrompterRepliesTask) -> str: + """Creates the message that gets sent to users when they request a `rank_prompter_replies` task.""" + return f"""\ + +{_h1("RANK PROMPTER REPLIES")} + +{_ranking_prompt("Reply with the numbers of best to worst replies separated by commas (example: '4,1,3,2')")} + + +{_conversation(task.conversation)} +{_user(None)} +{_ordered_list(task.replies)} +""" + + +def rank_assistant_reply_message(task: protocol_schema.RankAssistantRepliesTask) -> str: + """Creates the message that gets sent to users when they request a `rank_assistant_replies` task.""" + return f"""\ + +{_h1("RANK ASSISTANT REPLIES")} + +{_ranking_prompt("Reply with the numbers of best to worst replies separated by commas (example: '4,1,3,2')")} + + +{_conversation(task.conversation)} +{_assistant(None)} +{_ordered_list(task.replies)} +""" + + +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"""\ + +{_h1("PROMPTER REPLY")} + +{_response_prompt("Please provide a reply to the assistant.")} + + +{_conversation(task.conversation)} +{_hint(task.hint)} +""" + + +def assistant_reply_message(task: protocol_schema.AssistantReplyTask) -> str: + """Creates the message that gets sent to users when they request a `assistant_reply` task.""" + return f"""\ +{_h1("ASSISTANT REPLY")} + +{_response_prompt("Please provide a reply to the assistant.")} + + +{_conversation(task.conversation)} +""" + + +def confirm_text_response_message(content: str) -> str: + return f"""\ +{_h2("CONFIRM RESPONSE")} + +> {content} +""" + + +def confirm_ranking_response_message(content: str, items: list[str]) -> str: + user_rankings = [int(r) for r in content.replace(" ", "").split(",")] + original_list = _make_ordered_list(items) + user_ranked_list = "\n\n".join([original_list[r - 1] for r in user_rankings]) + + return f"""\ +{_h2("CONFIRM RESPONSE")} + +{user_ranked_list} +""" + + +### +# Embeds +### + + +def task_complete_embed(task: protocol_schema.Task, mention: str) -> hikari.Embed: + return ( + hikari.Embed( + title="Task Completion", + description=f"`{task.type}` completed by {mention}", + color=hikari.Color(0x00FF00), + timestamp=datetime.now().astimezone(), + ) + .add_field("Total Tasks", "0", inline=True) + .add_field("Server Ranking", "0/0", inline=True) + .add_field("Global Ranking", "0/0", inline=True) + .set_footer(f"Task ID: {task.id}") + ) + + +def invalid_user_input_embed(error_message: str) -> hikari.Embed: + return hikari.Embed( + title="Invalid User Input", + description=error_message, + color=hikari.Color(0xFF0000), + timestamp=datetime.now().astimezone(), + ) + + +def plain_embed(text: str) -> hikari.Embed: + return hikari.Embed(color=0x36393F, description=text) diff --git a/discord-bot/bot/utils.py b/discord-bot/bot/utils.py index 2d968c93..530f402a 100644 --- a/discord-bot/bot/utils.py +++ b/discord-bot/bot/utils.py @@ -24,13 +24,6 @@ def format_time(dt: datetime, fmt: t.Literal["t", "T", "D", "f", "F", "R"]) -> s raise ValueError(f"`fmt` must be 't', 'T', 'D', 'f', 'F' or 'R', not {fmt}") -EMPTY = "\u200d" -"""Zero-width joiner. - -This appears as an empty message in Discord. -""" - - def mention( id: hikari.Snowflakeish, type: t.Literal["channel", "role", "user"], From ff5e88916ac271266094b190802b15af4ae84d39 Mon Sep 17 00:00:00 2001 From: chs20 Date: Tue, 3 Jan 2023 13:27:51 +0100 Subject: [PATCH 08/47] Make menu icon visible in dark mode --- website/src/components/Header/Header.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/src/components/Header/Header.tsx b/website/src/components/Header/Header.tsx index 8b8c4663..ddc369ea 100644 --- a/website/src/components/Header/Header.tsx +++ b/website/src/components/Header/Header.tsx @@ -10,9 +10,11 @@ import { ColorModeIconToggle } from "../UI/ColorModeIconToggle"; import { UserMenu } from "./UserMenu"; function MenuIcon(props) { + const { colorMode } = useColorMode(); + const stroke = colorMode === "light" ? "black" : "white"; return ( ); } From 413e21176148e3c66bb5d635f667810324ee9258 Mon Sep 17 00:00:00 2001 From: Adrian Cowan Date: Tue, 3 Jan 2023 23:46:34 +1100 Subject: [PATCH 09/47] website: Fix broken e2e tests --- website/src/components/Survey/TaskControls.tsx | 8 ++++++-- website/src/pages/auth/signin.tsx | 3 ++- website/src/pages/create/assistant_reply.tsx | 2 +- website/src/pages/create/user_reply.tsx | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/website/src/components/Survey/TaskControls.tsx b/website/src/components/Survey/TaskControls.tsx index 7847c452..a93889ea 100644 --- a/website/src/components/Survey/TaskControls.tsx +++ b/website/src/components/Survey/TaskControls.tsx @@ -30,9 +30,13 @@ export const TaskControls = (props: TaskControlsProps) => { Skip {endTask.task.type !== "task_done" ? ( - props.onSubmitResponse(props.tasks[0])}>Submit + props.onSubmitResponse(props.tasks[0])}> + Submit + ) : ( - Next Task + + Next Task + )} diff --git a/website/src/pages/auth/signin.tsx b/website/src/pages/auth/signin.tsx index 936a3dbf..221eb1f0 100644 --- a/website/src/pages/auth/signin.tsx +++ b/website/src/pages/auth/signin.tsx @@ -52,8 +52,9 @@ function Signin({ csrfToken, providers }) { {email && (
- +