From 1e5e08a495416b5fabb938c191830b04fc4a81c4 Mon Sep 17 00:00:00 2001 From: "MTG\\mtg09" Date: Mon, 24 Jul 2023 17:10:36 +0400 Subject: [PATCH 1/6] feat (chatbot): create core functionality --- package-lock.json | 19 ++ package.json | 1 + src/features/Dashboard/Chatbot/Chatbot.tsx | 36 +++ .../Dashboard/Chatbot/components/Chat.tsx | 11 + .../Chatbot/components/MessagesContainer.tsx | 123 ++++++++ .../Chatbot/components/useSubmitMessage.tsx | 31 +++ .../Chatbot/contexts/chat.context.tsx | 85 ++++++ src/features/Dashboard/Chatbot/index.ts | 1 + src/features/Dashboard/Chatbot/lib/index.ts | 100 +++++++ .../Chatbot/lib/updateTournament.graphql | 92 ++++++ .../tournamentDetails.graphql | 1 + src/graphql/index.tsx | 262 +++++++++++++++++- src/mocks/data/tournament.ts | 7 + src/utils/consts/consts.ts | 1 + src/utils/routing/rootRouter.tsx | 11 + 15 files changed, 779 insertions(+), 2 deletions(-) create mode 100644 src/features/Dashboard/Chatbot/Chatbot.tsx create mode 100644 src/features/Dashboard/Chatbot/components/Chat.tsx create mode 100644 src/features/Dashboard/Chatbot/components/MessagesContainer.tsx create mode 100644 src/features/Dashboard/Chatbot/components/useSubmitMessage.tsx create mode 100644 src/features/Dashboard/Chatbot/contexts/chat.context.tsx create mode 100644 src/features/Dashboard/Chatbot/index.ts create mode 100644 src/features/Dashboard/Chatbot/lib/index.ts create mode 100644 src/features/Dashboard/Chatbot/lib/updateTournament.graphql diff --git a/package-lock.json b/package-lock.json index 15cd6102..446cc38f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "nexus": "^1.3.0", "nostr-relaypool": "^0.6.16", "nostr-tools": "^1.2.0", + "openai": "^3.3.0", "qrcode.react": "^3.0.2", "react": "^18.0.0", "react-accessible-accordion": "^5.0.0", @@ -58355,6 +58356,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", + "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "dependencies": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, "node_modules/optimism": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", @@ -114549,6 +114559,15 @@ "is-wsl": "^2.2.0" } }, + "openai": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-3.3.0.tgz", + "integrity": "sha512-uqxI/Au+aPRnsaQRe8CojU0eCR7I0mBiKjD3sNMzY6DaC1ZVrc85u98mtJW6voDug8fgGN+DIZmTDxTthxb7dQ==", + "requires": { + "axios": "^0.26.0", + "form-data": "^4.0.0" + } + }, "optimism": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz", diff --git a/package.json b/package.json index fbff8571..45dbfd17 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "nexus": "^1.3.0", "nostr-relaypool": "^0.6.16", "nostr-tools": "^1.2.0", + "openai": "^3.3.0", "qrcode.react": "^3.0.2", "react": "^18.0.0", "react-accessible-accordion": "^5.0.0", diff --git a/src/features/Dashboard/Chatbot/Chatbot.tsx b/src/features/Dashboard/Chatbot/Chatbot.tsx new file mode 100644 index 00000000..27efc722 --- /dev/null +++ b/src/features/Dashboard/Chatbot/Chatbot.tsx @@ -0,0 +1,36 @@ +import OgTags from "src/Components/OgTags/OgTags"; +import Chat from "./components/Chat"; +import { useGetTournamentByIdQuery } from "src/graphql"; +import { ChatContextProvider } from "./contexts/chat.context"; + +export default function HangoutPage() { + const { data, loading } = useGetTournamentByIdQuery({ + variables: { + idOrSlug: "nostr-hack", + }, + }); + + return ( + <> + +
+
+ + + +
+ {loading &&

Loading...

} + {data && ( +
+                {JSON.stringify(data.getTournamentById, null, "\t")}
+              
+ )} +
+
+
+ + ); +} diff --git a/src/features/Dashboard/Chatbot/components/Chat.tsx b/src/features/Dashboard/Chatbot/components/Chat.tsx new file mode 100644 index 00000000..13ea1d32 --- /dev/null +++ b/src/features/Dashboard/Chatbot/components/Chat.tsx @@ -0,0 +1,11 @@ +import MessagesContainer from "./MessagesContainer"; + +export default function Chat() { + return ( +
+
+ +
+
+ ); +} diff --git a/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx b/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx new file mode 100644 index 00000000..8b55f190 --- /dev/null +++ b/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx @@ -0,0 +1,123 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Message, useChat } from "../contexts/chat.context"; +import { useSubmitMessage } from "./useSubmitMessage"; + +interface Props {} + +export default function MessagesContainer({}: Props) { + const inputRef = React.useRef(null!); + + const [msgInput, setMessageInput] = useState(""); + const [inputDisabled, setInputDisabled] = useState(false); + const inputWasDisabled = React.useRef(false); + + const messagesContainerRef = React.useRef(null!); + + const { messages: newMessages } = useChat(); + + const submitMessageMutation = useSubmitMessage(); + + const [messages, setMessages] = useState(newMessages); + const [shouldScroll, setShouldScroll] = useState(true); + + if (messages !== newMessages) { + const scrolledToBottom = + messagesContainerRef.current.scrollTop + + messagesContainerRef.current.clientHeight === + messagesContainerRef.current.scrollHeight; + setShouldScroll(shouldScroll || scrolledToBottom); + setMessages(newMessages); + } + + const scrollToBottom = useCallback(() => { + messagesContainerRef.current.scrollTop = + messagesContainerRef.current.scrollHeight; + }, []); + + useEffect(() => { + if (shouldScroll) { + scrollToBottom(); + setShouldScroll(false); + } + }, [scrollToBottom, shouldScroll]); + + useEffect(() => { + if (!inputDisabled && inputWasDisabled.current) { + inputRef.current.focus(); + inputWasDisabled.current = false; + } + }, [inputDisabled]); + + const onSubmitMessage = async (e: React.FormEvent) => { + e.preventDefault(); + + if (msgInput.trim() === "") return; + + try { + setInputDisabled(true); + await submitMessageMutation.submit(msgInput); + + setMessageInput(""); + setShouldScroll(true); + } catch (error) { + alert("Failed to submit message"); + } finally { + inputWasDisabled.current = true; + setInputDisabled(false); + } + }; + + return ( + <> +
+
+ {messages.map((message) => ( +
+

{message.content}

+
+ ))} +
+
+
+
+ setMessageInput(e.target.value)} + disabled={inputDisabled} + /> + +
+
+ + ); +} diff --git a/src/features/Dashboard/Chatbot/components/useSubmitMessage.tsx b/src/features/Dashboard/Chatbot/components/useSubmitMessage.tsx new file mode 100644 index 00000000..7f45b320 --- /dev/null +++ b/src/features/Dashboard/Chatbot/components/useSubmitMessage.tsx @@ -0,0 +1,31 @@ +import { useState } from "react"; +import { useChat } from "../contexts/chat.context"; + +export const useSubmitMessage = () => { + const { submitMessage } = useChat(); + const [currentState, setCurrentState] = + useState<"idle" | "fetching-invoice" | "getting-response" | "error">( + "idle" + ); + const [error, setError] = useState(null); + + const submit = async (prompt: string) => { + try { + await submitMessage(prompt, { + onStatusUpdate: (status) => { + if (status === "fetching-invoice") + setCurrentState("fetching-invoice"); + if (status === "fetching-response") + setCurrentState("getting-response"); + }, + }); + setCurrentState("idle"); + } catch (error) { + setCurrentState("error"); + setError(error); + } finally { + } + }; + + return { submit, currentState, error }; +}; diff --git a/src/features/Dashboard/Chatbot/contexts/chat.context.tsx b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx new file mode 100644 index 00000000..cf58a793 --- /dev/null +++ b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx @@ -0,0 +1,85 @@ +import { createContext, useState, useContext, useCallback } from "react"; +import { Tournament } from "src/graphql"; +import { sendCommand } from "../lib"; + +export type Message = { + id: string; + content: string; + role: "user" | "assistant"; +}; + +interface ChatContext { + messages: Message[]; + submitMessage: ( + message: string, + options?: Partial<{ + onStatusUpdate: ( + status: + | "fetching-invoice" + | "invoice-paid" + | "fetching-response" + | "response-fetched" + ) => void; + }> + ) => Promise; +} + +const context = createContext(null!); + +export const ChatContextProvider = ({ + children, + currentTournament, +}: { + children: React.ReactNode; + currentTournament?: Partial; +}) => { + const [messages, setMessages] = useState([]); + + const submitMessage = useCallback( + async (message: string, options) => { + if (!currentTournament) throw new Error("No current tournament"); + + const onStatusUpdate = options?.onStatusUpdate || (() => {}); + + onStatusUpdate("fetching-response"); + const chatbotResponses = await sendCommand( + message, + [], + currentTournament + ); + + console.log(chatbotResponses); + + // go over the functions calls and execute them + + onStatusUpdate("response-fetched"); + + setMessages((prev) => [ + ...prev, + { id: Math.random().toString(), content: message, role: "user" }, + ...chatbotResponses.responses + .filter((r) => !r.function_call) + .map((r) => ({ + id: Math.random().toString(), + content: r.content ?? "", + role: "assistant" as const, + })), + ]); + }, + [currentTournament] + ); + + return ( + + {children} + + ); +}; + +export const useChat = () => { + const ctx = useContext(context); + if (!ctx) { + throw new Error("useChat must be used within a ChatContextProvider"); + } + return ctx; +}; diff --git a/src/features/Dashboard/Chatbot/index.ts b/src/features/Dashboard/Chatbot/index.ts new file mode 100644 index 00000000..6c926be2 --- /dev/null +++ b/src/features/Dashboard/Chatbot/index.ts @@ -0,0 +1 @@ +export * from "./Chatbot"; diff --git a/src/features/Dashboard/Chatbot/lib/index.ts b/src/features/Dashboard/Chatbot/lib/index.ts new file mode 100644 index 00000000..39f3453b --- /dev/null +++ b/src/features/Dashboard/Chatbot/lib/index.ts @@ -0,0 +1,100 @@ +import { + ChatCompletionFunctions, + ChatCompletionResponseMessage, + Configuration, + OpenAIApi, +} from "openai"; +import { Tournament } from "src/graphql"; +import { CONSTS } from "src/utils"; + +const configuration = new Configuration({ + apiKey: CONSTS.OPENAI_API_KEY, +}); +const openai = new OpenAIApi(configuration); + +const SYSTEM_MESSAGE = ` +You are an assisstant chatbot whose job is to help the user in updating his tournament data. + +The user will provide you with a prompt that can contain one or more commands from the user. + +& you will have to decide which functions to use & what parameters to pass to them to update the tournament data. + +RULES: +- Only use the functions provided to you & with their parameters. Don't invent new functions. +- Don't make assumptions about what values to plug into functions if not clear. Ask for clarification. +`; + +const availableFunctions: ChatCompletionFunctions[] = [ + { + name: "update_tournament", + description: "Update the tournament data", + parameters: { + type: "object", + properties: { + title: { + type: "string", + description: "The new title of the tournament", + }, + description: { + type: "string", + description: "The new description of the tournament in markdown", + }, + start_date: { + type: "string", + description: "The new start date of the tournament in ISO format", + }, + end_date: { + type: "string", + description: "The new end date of the tournament in ISO format", + }, + }, + }, + }, +]; + +export async function sendCommand( + prompt: string, + context: ChatCompletionResponseMessage[], + current_tournament_data: Partial +) { + let finishedCallingFunctions = false; + + let responses: ChatCompletionResponseMessage[] = []; + + while (!finishedCallingFunctions) { + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: [ + { + role: "system", + content: SYSTEM_MESSAGE, + }, + ...responses, + { + role: "user", + content: prompt, + }, + ...responses, + ], + functions: availableFunctions, + }); + + if (response.data.choices[0].finish_reason === "stop") { + finishedCallingFunctions = true; + + responses.push(response.data.choices[0].message!); + } else if (response.data.choices[0].finish_reason === "function_call") { + responses.push(response.data.choices[0].message!); + } else { + throw new Error( + `Unexpected finish reason: ${response.data.choices[0].finish_reason}` + ); + } + } + + console.log(responses); + + return { + responses, + }; +} diff --git a/src/features/Dashboard/Chatbot/lib/updateTournament.graphql b/src/features/Dashboard/Chatbot/lib/updateTournament.graphql new file mode 100644 index 00000000..abe4fc56 --- /dev/null +++ b/src/features/Dashboard/Chatbot/lib/updateTournament.graphql @@ -0,0 +1,92 @@ +mutation UpdateTournament($data: UpdateTournamentInput) { + updateTournament(data: $data) { + id + title + description + thumbnail_image + cover_image + start_date + end_date + location + website + events_count + makers_count + projects_count + tracks { + id + title + icon + } + prizes { + title + description + image + positions { + position + reward + project + } + additional_prizes { + text + url + } + } + judges { + name + company + avatar + } + faqs { + question + answer + } + events { + id + title + image + description + starts_at + ends_at + location + website + type + links + } + contacts { + type + url + } + partners { + title + items { + image + url + isBigImage + } + } + schedule { + date + events { + title + time + timezone + url + type + location + } + } + makers_deals { + title + description + url + } + config { + registerationOpen + projectsSubmissionOpen + ideasRootNostrEventId + showFeed + mainFeedHashtag + feedFilters + } + } +} diff --git a/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql b/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql index 0949cd2e..9f713b82 100644 --- a/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql +++ b/src/features/Tournaments/pages/TournamentDetailsPage/tournamentDetails.graphql @@ -51,6 +51,7 @@ query GetTournamentById($idOrSlug: String!) { links } faqs { + id question answer } diff --git a/src/graphql/index.tsx b/src/graphql/index.tsx index ad489001..2b285aeb 100644 --- a/src/graphql/index.tsx +++ b/src/graphql/index.tsx @@ -150,6 +150,43 @@ export type CreateProjectResponse = { project: Project; }; +export type CreateTournamentFaqInput = { + answer: Scalars['String']; + question: Scalars['String']; +}; + +export type CreateTournamentInput = { + config: TournamentConfigInput; + contacts: Array; + cover_image: ImageInput; + description: Scalars['String']; + end_date: Scalars['Date']; + faqs: Array; + judges: Array; + location: Scalars['String']; + makers_deals: Array; + partners: Array; + prizes: Array; + schedule: Array; + slug: Scalars['String']; + start_date: Scalars['Date']; + thumbnail_image: ImageInput; + title: Scalars['String']; + tracks: Array; + website: Scalars['String']; +}; + +export type CreateTournamentJudgeInput = { + avatar: ImageInput; + company: Scalars['String']; + name: Scalars['String']; +}; + +export type CreateTournamentTrackInput = { + icon: Scalars['String']; + title: Scalars['String']; +}; + export type Donation = { __typename?: 'Donation'; amount: Scalars['Int']; @@ -233,6 +270,7 @@ export type Mutation = { confirmVote: Vote; createProject: Maybe; createStory: Maybe; + createTournament: Maybe; deleteProject: Maybe; deleteStory: Maybe; donate: Donation; @@ -243,6 +281,7 @@ export type Mutation = { updateProfileDetails: Maybe; updateProfileRoles: Maybe; updateProject: Maybe; + updateTournament: Maybe; updateTournamentRegistration: Maybe; updateUserPreferences: User; vote: Vote; @@ -276,6 +315,11 @@ export type MutationCreateStoryArgs = { }; +export type MutationCreateTournamentArgs = { + data: InputMaybe; +}; + + export type MutationDeleteProjectArgs = { id: Scalars['Int']; }; @@ -327,6 +371,11 @@ export type MutationUpdateProjectArgs = { }; +export type MutationUpdateTournamentArgs = { + data: InputMaybe; +}; + + export type MutationUpdateTournamentRegistrationArgs = { data: InputMaybe; tournament_id: Scalars['Int']; @@ -821,7 +870,16 @@ export type TournamentConfig = { mainFeedHashtag: Maybe; projectsSubmissionOpen: Scalars['Boolean']; registerationOpen: Scalars['Boolean']; - showFeed: Scalars['Boolean']; + showFeed: Maybe; +}; + +export type TournamentConfigInput = { + feedFilters?: InputMaybe>; + ideasRootNostrEventId?: InputMaybe; + mainFeedHashtag?: InputMaybe; + projectsSubmissionOpen: Scalars['Boolean']; + registerationOpen: Scalars['Boolean']; + showFeed?: InputMaybe; }; export type TournamentContact = { @@ -830,6 +888,11 @@ export type TournamentContact = { url: Scalars['String']; }; +export type TournamentContactInput = { + type: Scalars['String']; + url: Scalars['String']; +}; + export type TournamentEvent = { __typename?: 'TournamentEvent'; description: Scalars['String']; @@ -854,6 +917,7 @@ export enum TournamentEventTypeEnum { export type TournamentFaq = { __typename?: 'TournamentFAQ'; answer: Scalars['String']; + id: Scalars['Int']; question: Scalars['String']; }; @@ -871,6 +935,12 @@ export type TournamentMakerDeal = { url: Maybe; }; +export type TournamentMakerDealInput = { + description: Scalars['String']; + title: Scalars['String']; + url?: InputMaybe; +}; + export enum TournamentMakerHackingStatusEnum { OpenToConnect = 'OpenToConnect', Solo = 'Solo' @@ -896,6 +966,11 @@ export type TournamentPartner = { title: Scalars['String']; }; +export type TournamentPartnerInput = { + items: Array; + title: Scalars['String']; +}; + export type TournamentPartnerItem = { __typename?: 'TournamentPartnerItem'; image: Scalars['String']; @@ -903,6 +978,12 @@ export type TournamentPartnerItem = { url: Scalars['String']; }; +export type TournamentPartnerItemInput = { + image: Scalars['String']; + isBigImage?: InputMaybe; + url: Scalars['String']; +}; + export type TournamentPrize = { __typename?: 'TournamentPrize'; additional_prizes: Maybe>; @@ -918,6 +999,19 @@ export type TournamentPrizeAdditionalPrize = { url: Maybe; }; +export type TournamentPrizeAdditionalPrizeInput = { + text: Scalars['String']; + url?: InputMaybe; +}; + +export type TournamentPrizeInput = { + additional_prizes?: InputMaybe>; + description: Scalars['String']; + image: Scalars['String']; + positions: Array; + title: Scalars['String']; +}; + export type TournamentPrizePosition = { __typename?: 'TournamentPrizePosition'; position: Scalars['String']; @@ -925,6 +1019,12 @@ export type TournamentPrizePosition = { reward: Scalars['String']; }; +export type TournamentPrizePositionInput = { + position: Scalars['String']; + project?: InputMaybe; + reward: Scalars['String']; +}; + export type TournamentProjectsResponse = { __typename?: 'TournamentProjectsResponse'; allItemsCount: Maybe; @@ -949,6 +1049,20 @@ export type TournamentScheduleEvent = { url: Maybe; }; +export type TournamentScheduleEventInput = { + location?: InputMaybe; + time?: InputMaybe; + timezone?: InputMaybe; + title: Scalars['String']; + type?: InputMaybe; + url?: InputMaybe; +}; + +export type TournamentScheduleInput = { + date: Scalars['String']; + events: Array; +}; + export type TournamentTrack = { __typename?: 'TournamentTrack'; icon: Scalars['String']; @@ -983,6 +1097,22 @@ export type UpdateProjectInput = { website: Scalars['String']; }; +export type UpdateTournamentInput = { + config: TournamentConfigInput; + contacts: Array; + description: Scalars['String']; + end_date: Scalars['Date']; + id?: InputMaybe; + location: Scalars['String']; + makers_deals: Array; + partners: Array; + prizes: Array; + schedule: Array; + start_date: Scalars['Date']; + title: Scalars['String']; + website: Scalars['String']; +}; + export type UpdateTournamentRegistrationInput = { email?: InputMaybe; hacking_status?: InputMaybe; @@ -1092,6 +1222,13 @@ export type MeQueryVariables = Exact<{ [key: string]: never; }>; export type MeQuery = { __typename?: 'Query', me: { __typename?: 'User', id: number, name: string, avatar: string, join_date: any, jobTitle: string | null, bio: string | null, primary_nostr_key: string | null } | null }; +export type UpdateTournamentMutationVariables = Exact<{ + data: InputMaybe; +}>; + + +export type UpdateTournamentMutation = { __typename?: 'Mutation', updateTournament: { __typename?: 'Tournament', id: number, title: string, description: string, thumbnail_image: string, cover_image: string, start_date: any, end_date: any, location: string, website: string, events_count: number, makers_count: number, projects_count: number, tracks: Array<{ __typename?: 'TournamentTrack', id: number, title: string, icon: string }>, prizes: Array<{ __typename?: 'TournamentPrize', title: string, description: string, image: string, positions: Array<{ __typename?: 'TournamentPrizePosition', position: string, reward: string, project: string | null }>, additional_prizes: Array<{ __typename?: 'TournamentPrizeAdditionalPrize', text: string, url: string | null }> | null }>, judges: Array<{ __typename?: 'TournamentJudge', name: string, company: string, avatar: string }>, faqs: Array<{ __typename?: 'TournamentFAQ', question: string, answer: string }>, events: Array<{ __typename?: 'TournamentEvent', id: number, title: string, image: string, description: string, starts_at: any, ends_at: any, location: string, website: string, type: TournamentEventTypeEnum, links: Array }>, contacts: Array<{ __typename?: 'TournamentContact', type: string, url: string }>, partners: Array<{ __typename?: 'TournamentPartner', title: string, items: Array<{ __typename?: 'TournamentPartnerItem', image: string, url: string, isBigImage: boolean | null }> }>, schedule: Array<{ __typename?: 'TournamentSchedule', date: string, events: Array<{ __typename?: 'TournamentScheduleEvent', title: string, time: string | null, timezone: string | null, url: string | null, type: string | null, location: string | null }> }>, makers_deals: Array<{ __typename?: 'TournamentMakerDeal', title: string, description: string, url: string | null }>, config: { __typename?: 'TournamentConfig', registerationOpen: boolean, projectsSubmissionOpen: boolean, ideasRootNostrEventId: string | null, showFeed: boolean | null, mainFeedHashtag: string | null, feedFilters: Array | null } } | null }; + export type DonationsStatsQueryVariables = Exact<{ [key: string]: never; }>; @@ -1439,7 +1576,7 @@ export type GetTournamentByIdQueryVariables = Exact<{ }>; -export type GetTournamentByIdQuery = { __typename?: 'Query', pubkeysOfMakersInTournament: Array, pubkeysOfProjectsInTournament: Array, getTournamentById: { __typename?: 'Tournament', id: number, title: string, description: string, thumbnail_image: string, cover_image: string, start_date: any, end_date: any, location: string, website: string, events_count: number, makers_count: number, projects_count: number, prizes: Array<{ __typename?: 'TournamentPrize', title: string, description: string, image: string, positions: Array<{ __typename?: 'TournamentPrizePosition', position: string, reward: string, project: string | null }>, additional_prizes: Array<{ __typename?: 'TournamentPrizeAdditionalPrize', text: string, url: string | null }> | null }>, tracks: Array<{ __typename?: 'TournamentTrack', id: number, title: string, icon: string }>, judges: Array<{ __typename?: 'TournamentJudge', name: string, company: string, avatar: string }>, events: Array<{ __typename?: 'TournamentEvent', id: number, title: string, image: string, description: string, starts_at: any, ends_at: any, location: string, website: string, type: TournamentEventTypeEnum, links: Array }>, faqs: Array<{ __typename?: 'TournamentFAQ', question: string, answer: string }>, contacts: Array<{ __typename?: 'TournamentContact', type: string, url: string }>, partners: Array<{ __typename?: 'TournamentPartner', title: string, items: Array<{ __typename?: 'TournamentPartnerItem', image: string, url: string, isBigImage: boolean | null }> }>, schedule: Array<{ __typename?: 'TournamentSchedule', date: string, events: Array<{ __typename?: 'TournamentScheduleEvent', title: string, time: string | null, timezone: string | null, url: string | null, type: string | null, location: string | null }> }>, makers_deals: Array<{ __typename?: 'TournamentMakerDeal', title: string, description: string, url: string | null }>, config: { __typename?: 'TournamentConfig', registerationOpen: boolean, projectsSubmissionOpen: boolean, ideasRootNostrEventId: string | null, showFeed: boolean, mainFeedHashtag: string | null, feedFilters: Array | null } }, getMakersInTournament: { __typename?: 'TournamentMakersResponse', makers: Array<{ __typename?: 'TournamentParticipant', user: { __typename?: 'User', id: number, avatar: string } }> } }; +export type GetTournamentByIdQuery = { __typename?: 'Query', pubkeysOfMakersInTournament: Array, pubkeysOfProjectsInTournament: Array, getTournamentById: { __typename?: 'Tournament', id: number, title: string, description: string, thumbnail_image: string, cover_image: string, start_date: any, end_date: any, location: string, website: string, events_count: number, makers_count: number, projects_count: number, prizes: Array<{ __typename?: 'TournamentPrize', title: string, description: string, image: string, positions: Array<{ __typename?: 'TournamentPrizePosition', position: string, reward: string, project: string | null }>, additional_prizes: Array<{ __typename?: 'TournamentPrizeAdditionalPrize', text: string, url: string | null }> | null }>, tracks: Array<{ __typename?: 'TournamentTrack', id: number, title: string, icon: string }>, judges: Array<{ __typename?: 'TournamentJudge', name: string, company: string, avatar: string }>, events: Array<{ __typename?: 'TournamentEvent', id: number, title: string, image: string, description: string, starts_at: any, ends_at: any, location: string, website: string, type: TournamentEventTypeEnum, links: Array }>, faqs: Array<{ __typename?: 'TournamentFAQ', id: number, question: string, answer: string }>, contacts: Array<{ __typename?: 'TournamentContact', type: string, url: string }>, partners: Array<{ __typename?: 'TournamentPartner', title: string, items: Array<{ __typename?: 'TournamentPartnerItem', image: string, url: string, isBigImage: boolean | null }> }>, schedule: Array<{ __typename?: 'TournamentSchedule', date: string, events: Array<{ __typename?: 'TournamentScheduleEvent', title: string, time: string | null, timezone: string | null, url: string | null, type: string | null, location: string | null }> }>, makers_deals: Array<{ __typename?: 'TournamentMakerDeal', title: string, description: string, url: string | null }>, config: { __typename?: 'TournamentConfig', registerationOpen: boolean, projectsSubmissionOpen: boolean, ideasRootNostrEventId: string | null, showFeed: boolean | null, mainFeedHashtag: string | null, feedFilters: Array | null } }, getMakersInTournament: { __typename?: 'TournamentMakersResponse', makers: Array<{ __typename?: 'TournamentParticipant', user: { __typename?: 'User', id: number, avatar: string } }> } }; export type NostrKeysMetadataQueryVariables = Exact<{ keys: Array | Scalars['String']; @@ -1751,6 +1888,126 @@ export function useMeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type MeLazyQueryHookResult = ReturnType; export type MeQueryResult = Apollo.QueryResult; +export const UpdateTournamentDocument = gql` + mutation UpdateTournament($data: UpdateTournamentInput) { + updateTournament(data: $data) { + id + title + description + thumbnail_image + cover_image + start_date + end_date + location + website + events_count + makers_count + projects_count + tracks { + id + title + icon + } + prizes { + title + description + image + positions { + position + reward + project + } + additional_prizes { + text + url + } + } + judges { + name + company + avatar + } + faqs { + question + answer + } + events { + id + title + image + description + starts_at + ends_at + location + website + type + links + } + contacts { + type + url + } + partners { + title + items { + image + url + isBigImage + } + } + schedule { + date + events { + title + time + timezone + url + type + location + } + } + makers_deals { + title + description + url + } + config { + registerationOpen + projectsSubmissionOpen + ideasRootNostrEventId + showFeed + mainFeedHashtag + feedFilters + } + } +} + `; +export type UpdateTournamentMutationFn = Apollo.MutationFunction; + +/** + * __useUpdateTournamentMutation__ + * + * To run a mutation, you first call `useUpdateTournamentMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateTournamentMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [updateTournamentMutation, { data, loading, error }] = useUpdateTournamentMutation({ + * variables: { + * data: // value for 'data' + * }, + * }); + */ +export function useUpdateTournamentMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(UpdateTournamentDocument, options); + } +export type UpdateTournamentMutationHookResult = ReturnType; +export type UpdateTournamentMutationResult = Apollo.MutationResult; +export type UpdateTournamentMutationOptions = Apollo.BaseMutationOptions; export const DonationsStatsDocument = gql` query DonationsStats { getDonationsStats { @@ -4267,6 +4524,7 @@ export const GetTournamentByIdDocument = gql` links } faqs { + id question answer } diff --git a/src/mocks/data/tournament.ts b/src/mocks/data/tournament.ts index e0d86ef5..b0d15d0a 100644 --- a/src/mocks/data/tournament.ts +++ b/src/mocks/data/tournament.ts @@ -240,24 +240,28 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Semper turpis est, ac e faqs: [ { + id: 1, question: "What is Shock the Web?", answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there. Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`, }, { + id: 2, question: "When and where will it take place?", answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there. Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`, }, { + id: 3, question: "What will we be doing?", answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there. Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`, }, { + id: 4, question: "This is my first time hacking on lightning, will there be help?", answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there. @@ -265,6 +269,7 @@ Bitcoin development can seem scary for new developers coming in, but it doesn't Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`, }, { + id: 5, question: "This is my first time hacking on lightning, will there be help?", answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there. @@ -272,12 +277,14 @@ Bitcoin development can seem scary for new developers coming in, but it doesn't Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`, }, { + id: 6, question: "How many members can I have on my team?", answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there. Bitcoin development can seem scary for new developers coming in, but it doesn't have to be. With the lightning network's toolkit and libraries a bunch of new opportunities are waiting to be explored. We hope these hackathons can be a chance for you to preview what is possible on bitcoin and the lightning network by fostering collaboration, hopefully shortening (or easing) any developer onboarding time, and helping you connect with other bitcoiners in a fun and friendly space.`, }, { + id: 7, question: "Who will choose the winners?", answer: `Shock the Web is a virtual hackathon to promote, explore, build and design web applications that can interact with WebLN enabled wallets and browsers. We want to make building on bitcoin more accessible to the masses of web developers out there. diff --git a/src/utils/consts/consts.ts b/src/utils/consts/consts.ts index ff91f12e..19c994e1 100644 --- a/src/utils/consts/consts.ts +++ b/src/utils/consts/consts.ts @@ -11,6 +11,7 @@ const CONSTS = { DEFAULT_RELAYS, BF_NOSTR_PUBKEY: "4f260791d78a93d13e09f1965f4ba1e1f96d1fcb812123a26d95737c9d54802b", + OPENAI_API_KEY: process.env.REACT_APP_OPENAI_API_KEY ?? "", }; export default CONSTS; diff --git a/src/utils/routing/rootRouter.tsx b/src/utils/routing/rootRouter.tsx index e98937b3..bbdf88fe 100644 --- a/src/utils/routing/rootRouter.tsx +++ b/src/utils/routing/rootRouter.tsx @@ -206,6 +206,15 @@ const TermsAndConditionsPage = Loadable( ) ); +const ChatbotPage = Loadable( + React.lazy( + () => + import( + /* webpackChunkName: "terms_conditions_page" */ "../../features/Dashboard/Chatbot/Chatbot" + ) + ) +); + const createRoutes = (queryClient: ApolloClient) => createRoutesFromElements( } errorElement={}> @@ -318,6 +327,8 @@ const createRoutes = (queryClient: ApolloClient) => element={} /> + } /> + Date: Tue, 25 Jul 2023 15:51:33 +0400 Subject: [PATCH 2/6] feat: finish core functions of chatbot --- api/functions/graphql/types/tournament.js | 2 +- src/features/Dashboard/Chatbot/Chatbot.tsx | 36 +++-- .../Dashboard/Chatbot/components/Chat.tsx | 4 +- .../Chatbot/components/MessagesContainer.tsx | 4 +- .../Chatbot/components/TournamentPreview.tsx | 141 ++++++++++++++++++ .../Chatbot/contexts/chat.context.tsx | 61 ++++++-- .../Chatbot/contexts/tournament.context.tsx | 48 ++++++ src/features/Dashboard/Chatbot/lib/index.ts | 82 ++++------ .../Dashboard/Chatbot/lib/open-ai.service.ts | 22 +++ .../AI4ALLOverviewPage/AI4ALLOverviewPage.tsx | 1 + .../LegendsOfLightningOverviewPage.tsx | 1 + .../NostrHackWeekOverviewPage.tsx | 1 + .../pages/OverviewPage/OverviewPage.tsx | 1 + .../RegisterCard/RegisterCard.tsx | 17 +-- 14 files changed, 320 insertions(+), 101 deletions(-) create mode 100644 src/features/Dashboard/Chatbot/components/TournamentPreview.tsx create mode 100644 src/features/Dashboard/Chatbot/contexts/tournament.context.tsx create mode 100644 src/features/Dashboard/Chatbot/lib/open-ai.service.ts diff --git a/api/functions/graphql/types/tournament.js b/api/functions/graphql/types/tournament.js index 7d183c53..bb9c14cb 100644 --- a/api/functions/graphql/types/tournament.js +++ b/api/functions/graphql/types/tournament.js @@ -1013,7 +1013,7 @@ const CreateTournamentInput = inputObjectType({ }); const isAdminUser = (userId) => { - return userId === 3 || userId === 37; + return userId === 3 || userId === 37 || userId === 9; }; const createTournament = extendType({ diff --git a/src/features/Dashboard/Chatbot/Chatbot.tsx b/src/features/Dashboard/Chatbot/Chatbot.tsx index 27efc722..fbc252dc 100644 --- a/src/features/Dashboard/Chatbot/Chatbot.tsx +++ b/src/features/Dashboard/Chatbot/Chatbot.tsx @@ -1,14 +1,17 @@ +import { useParams } from "react-router"; +import { useSearchParams } from "react-router-dom"; import OgTags from "src/Components/OgTags/OgTags"; import Chat from "./components/Chat"; -import { useGetTournamentByIdQuery } from "src/graphql"; +import TournamentPreview from "./components/TournamentPreview"; import { ChatContextProvider } from "./contexts/chat.context"; +import { TournamentContextProvider } from "./contexts/tournament.context"; -export default function HangoutPage() { - const { data, loading } = useGetTournamentByIdQuery({ - variables: { - idOrSlug: "nostr-hack", - }, - }); +export default function ChatbotPage() { + const [searchParams] = useSearchParams(); + + const tournamentIdOrSlug = searchParams.get("tournament"); + + if (!tournamentIdOrSlug) return

No tournament selected

; return ( <> @@ -16,19 +19,14 @@ export default function HangoutPage() { title="Chatbot Commander" description="Update stuff using chatbot" /> -
+
- - - -
- {loading &&

Loading...

} - {data && ( -
-                {JSON.stringify(data.getTournamentById, null, "\t")}
-              
- )} -
+ + + + + +
diff --git a/src/features/Dashboard/Chatbot/components/Chat.tsx b/src/features/Dashboard/Chatbot/components/Chat.tsx index 13ea1d32..5d741ef6 100644 --- a/src/features/Dashboard/Chatbot/components/Chat.tsx +++ b/src/features/Dashboard/Chatbot/components/Chat.tsx @@ -2,8 +2,8 @@ import MessagesContainer from "./MessagesContainer"; export default function Chat() { return ( -
-
+
+
diff --git a/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx b/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx index 8b55f190..ebddd5b5 100644 --- a/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx +++ b/src/features/Dashboard/Chatbot/components/MessagesContainer.tsx @@ -5,7 +5,7 @@ import { useSubmitMessage } from "./useSubmitMessage"; interface Props {} export default function MessagesContainer({}: Props) { - const inputRef = React.useRef(null!); + const inputRef = React.useRef(null!); const [msgInput, setMessageInput] = useState(""); const [inputDisabled, setInputDisabled] = useState(false); @@ -94,7 +94,7 @@ export default function MessagesContainer({}: Props) { inputDisabled && "opacity-70" }`} > - + {loading &&

Loading...

} + {tournament && ( + <> +
+ +
+
+
+ +
+

TOURNAMENT 🏆

+

+ {tournament.title} +

+

+ {new Date(tournament.start_date).toDateString()} -{" "} + {new Date(tournament.end_date).toDateString()} +

+

+ {tournament.location} +

+
+
+
+
+ +
+
+
+ {true && + tournament.makers_deals && + tournament.makers_deals?.length > 0 && ( +
+

+ Hacker perks from our partners 🎁 +

+ +
+ )} +
+ +
+ + + {tournament.judges && tournament.judges?.length > 0 && ( + + )} + {tournament.faqs && tournament.faqs.length > 0 && ( + + )} +
+ + )} +
+ ); +} diff --git a/src/features/Dashboard/Chatbot/contexts/chat.context.tsx b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx index cf58a793..5f453354 100644 --- a/src/features/Dashboard/Chatbot/contexts/chat.context.tsx +++ b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx @@ -1,6 +1,11 @@ +import { + ChatCompletionRequestMessageFunctionCall, + ChatCompletionResponseMessage, +} from "openai"; import { createContext, useState, useContext, useCallback } from "react"; import { Tournament } from "src/graphql"; -import { sendCommand } from "../lib"; +import { Functions, sendCommand } from "../lib"; +import { useTournament } from "./tournament.context"; export type Message = { id: string; @@ -28,37 +33,65 @@ const context = createContext(null!); export const ChatContextProvider = ({ children, - currentTournament, }: { children: React.ReactNode; - currentTournament?: Partial; }) => { const [messages, setMessages] = useState([]); + const { tournament: currentTournament, updateTournament } = useTournament(); + const submitMessage = useCallback( async (message: string, options) => { - if (!currentTournament) throw new Error("No current tournament"); + if (!currentTournament) throw new Error("No tournament selected"); const onStatusUpdate = options?.onStatusUpdate || (() => {}); + const functions: Functions = { + update_tournament: (tournament) => { + updateTournament(tournament); + }, + }; + + const oldMessages = messages; + + const newResponses = [ + { content: message, role: "user" }, + ] as ChatCompletionResponseMessage[]; + onStatusUpdate("fetching-response"); - const chatbotResponses = await sendCommand( - message, - [], - currentTournament - ); - console.log(chatbotResponses); + let finishedCallingFunctions = false; + + while (!finishedCallingFunctions) { + const response = await sendCommand([...oldMessages, ...newResponses]); + + if (response?.function_call) { + execFunction(response.function_call); + newResponses.push(response); + } else if (response?.content) { + newResponses.push(response); + finishedCallingFunctions = true; + } + } - // go over the functions calls and execute them + function execFunction(f: ChatCompletionRequestMessageFunctionCall) { + const fn = functions[f.name as keyof Functions]; + if (!fn) throw new Error(`Function ${f.name} not found`); + return fn(JSON.parse(f?.arguments as any)); + } onStatusUpdate("response-fetched"); setMessages((prev) => [ ...prev, - { id: Math.random().toString(), content: message, role: "user" }, - ...chatbotResponses.responses + { + id: Math.random().toString(), + content: message, + role: "user", + }, + ...newResponses .filter((r) => !r.function_call) + .filter((r) => r.role === "assistant") .map((r) => ({ id: Math.random().toString(), content: r.content ?? "", @@ -66,7 +99,7 @@ export const ChatContextProvider = ({ })), ]); }, - [currentTournament] + [currentTournament, messages, updateTournament] ); return ( diff --git a/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx b/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx new file mode 100644 index 00000000..ac26dc10 --- /dev/null +++ b/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx @@ -0,0 +1,48 @@ +import { createContext, useState, useContext, useCallback } from "react"; +import { Tournament, useGetTournamentByIdQuery } from "src/graphql"; + +interface TournamentContext { + tournament?: Tournament; + updateTournament: (newTournament: Partial) => void; +} + +const context = createContext(null!); + +export const TournamentContextProvider = ({ + children, + idOrSlug, +}: { + children: React.ReactNode; + idOrSlug: string; +}) => { + const [tournament, setTournament] = useState(); + + useGetTournamentByIdQuery({ + variables: { + idOrSlug, + }, + onCompleted: (data) => { + setTournament(data.getTournamentById); + }, + }); + + const updateTournament = useCallback((newTournament: Partial) => { + setTournament((prev) => ({ ...prev, ...(newTournament as any) })); + }, []); + + return ( + + {children} + + ); +}; + +export const useTournament = () => { + const ctx = useContext(context); + if (!ctx) { + throw new Error( + "useTournament must be used within a TournamentContextProvider" + ); + } + return ctx; +}; diff --git a/src/features/Dashboard/Chatbot/lib/index.ts b/src/features/Dashboard/Chatbot/lib/index.ts index 39f3453b..e727b8a6 100644 --- a/src/features/Dashboard/Chatbot/lib/index.ts +++ b/src/features/Dashboard/Chatbot/lib/index.ts @@ -1,16 +1,6 @@ -import { - ChatCompletionFunctions, - ChatCompletionResponseMessage, - Configuration, - OpenAIApi, -} from "openai"; +import { ChatCompletionFunctions, ChatCompletionResponseMessage } from "openai"; import { Tournament } from "src/graphql"; -import { CONSTS } from "src/utils"; - -const configuration = new Configuration({ - apiKey: CONSTS.OPENAI_API_KEY, -}); -const openai = new OpenAIApi(configuration); +import { getOpenAIApi } from "./open-ai.service"; const SYSTEM_MESSAGE = ` You are an assisstant chatbot whose job is to help the user in updating his tournament data. @@ -22,8 +12,14 @@ The user will provide you with a prompt that can contain one or more commands fr RULES: - Only use the functions provided to you & with their parameters. Don't invent new functions. - Don't make assumptions about what values to plug into functions if not clear. Ask for clarification. +- Your sole purpose is to update the tournament data. Don't do anything else. +- If you don't know how to do something, tell the user that you can't & suggest to him contacting the admins. `; +export type Functions = { + update_tournament: (parameters: Partial) => void; +}; + const availableFunctions: ChatCompletionFunctions[] = [ { name: "update_tournament", @@ -52,49 +48,29 @@ const availableFunctions: ChatCompletionFunctions[] = [ }, ]; -export async function sendCommand( - prompt: string, - context: ChatCompletionResponseMessage[], - current_tournament_data: Partial -) { - let finishedCallingFunctions = false; +export async function sendCommand(context: ChatCompletionResponseMessage[]) { + const openai = getOpenAIApi(); - let responses: ChatCompletionResponseMessage[] = []; - - while (!finishedCallingFunctions) { - const response = await openai.createChatCompletion({ - model: "gpt-3.5-turbo", - messages: [ - { - role: "system", - content: SYSTEM_MESSAGE, - }, - ...responses, - { - role: "user", - content: prompt, - }, - ...responses, - ], - functions: availableFunctions, - }); - - if (response.data.choices[0].finish_reason === "stop") { - finishedCallingFunctions = true; + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages: [ + { + role: "system", + content: SYSTEM_MESSAGE, + }, + ...context.map((m) => ({ + role: m.role, + content: m.content, + function_call: m.function_call, + })), + ], + functions: availableFunctions, + }); - responses.push(response.data.choices[0].message!); - } else if (response.data.choices[0].finish_reason === "function_call") { - responses.push(response.data.choices[0].message!); - } else { - throw new Error( - `Unexpected finish reason: ${response.data.choices[0].finish_reason}` - ); - } - } + const finish_reason = response.data.choices[0].finish_reason; - console.log(responses); + if (finish_reason !== "function_call" && finish_reason !== "stop") + throw new Error(`Unexpected finish reason: ${finish_reason}`); - return { - responses, - }; + return response.data.choices[0].message; } diff --git a/src/features/Dashboard/Chatbot/lib/open-ai.service.ts b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts new file mode 100644 index 00000000..4b4fd2a4 --- /dev/null +++ b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts @@ -0,0 +1,22 @@ +import { Configuration, OpenAIApi } from "openai"; +import { CONSTS } from "src/utils"; + +let openai: OpenAIApi; + +export function getOpenAIApi() { + if (openai) return openai; + + let apiKey: string | null = CONSTS.OPENAI_API_KEY; + + if (!apiKey) apiKey = prompt("Please enter an OpenAI API key"); + + if (!apiKey) throw new Error("No OpenAI API key provided"); + + const configuration = new Configuration({ + apiKey, + }); + + openai = new OpenAIApi(configuration); + + return openai; +} diff --git a/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx index dafbfb7a..2bbaf554 100644 --- a/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx +++ b/src/features/Tournaments/pages/OverviewPage/AI4ALLOverviewPage/AI4ALLOverviewPage.tsx @@ -79,6 +79,7 @@ export default function LegendsOfLightningOverviewPage() { )}
m.user.avatar)} diff --git a/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx index 7f757d12..898db147 100644 --- a/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx +++ b/src/features/Tournaments/pages/OverviewPage/LegendsOfLightningOverviewPage/LegendsOfLightningOverviewPage.tsx @@ -79,6 +79,7 @@ export default function LegendsOfLightningOverviewPage() { )}
m.user.avatar)} diff --git a/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx index c80bcd65..41633b0e 100644 --- a/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx +++ b/src/features/Tournaments/pages/OverviewPage/NostrHackWeekOverviewPage/NostrHackWeekOverviewPage.tsx @@ -79,6 +79,7 @@ export default function NostrHackWeekOverviewPage() { )} m.user.avatar)} diff --git a/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx b/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx index 83749b17..29945c25 100644 --- a/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx +++ b/src/features/Tournaments/pages/OverviewPage/OverviewPage.tsx @@ -85,6 +85,7 @@ export default function OverviewPage() { isRegistrationOpen={tournamentDetails.config.registerationOpen} partnersList={tournamentDetails.partners} contacts={tournamentDetails.contacts} + tournament={tournamentDetails} /> diff --git a/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx b/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx index dfe41627..72c0bd67 100644 --- a/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx +++ b/src/features/Tournaments/pages/OverviewPage/RegisterCard/RegisterCard.tsx @@ -2,15 +2,14 @@ import dayjs from "dayjs"; import Button from "src/Components/Button/Button"; import Card from "src/Components/Card/Card"; import Avatar from "src/features/Profiles/Components/Avatar/Avatar"; -import { TournamentContact, TournamentPartner } from "src/graphql"; +import { Tournament, TournamentContact, TournamentPartner } from "src/graphql"; import { openModal } from "src/redux/features/modals.slice"; import { useCountdown } from "src/utils/hooks"; import { useAppDispatch, useAppSelector } from "src/utils/hooks"; import { twMerge } from "tailwind-merge"; -import { useTournament } from "../../TournamentDetailsPage/TournamentDetailsContext"; -import { TournamentStaticData } from "../../types"; interface Props { + tournament: Pick; start_date: string; makers_count: number; avatars: string[]; @@ -28,24 +27,22 @@ export default function RegisterCard({ isRegistrationOpen, partnersList, contacts, + tournament, }: Props) { const counter = useCountdown(start_date); - const { - tournamentDetails: { id: tournamentId, end_date }, - } = useTournament(); const isLoggedIn = useAppSelector((state) => !!state.user.me); const dispatch = useAppDispatch(); const onRegister = () => { - if (!tournamentId) return; + if (!tournament.id) return; if (isLoggedIn) dispatch( openModal({ Modal: "RegisterTournamet_ConfrimAccount", props: { - tournamentId: Number(tournamentId), + tournamentId: Number(tournament.id), }, }) ); @@ -54,7 +51,7 @@ export default function RegisterCard({ openModal({ Modal: "RegisterTournamet_Login", props: { - tournamentId: Number(tournamentId), + tournamentId: Number(tournament.id), }, }) ); @@ -114,7 +111,7 @@ export default function RegisterCard({ Live - entries close {dayjs(end_date).format("Do MMM")} + entries close {dayjs(tournament.end_date).format("Do MMM")} ) : ( From 62b8b415d69c60e3adc517e1388467197bbbf270 Mon Sep 17 00:00:00 2001 From: "MTG\\mtg09" Date: Tue, 25 Jul 2023 17:49:39 +0400 Subject: [PATCH 3/6] fix (chatbot): fix overflow messages container --- src/features/Dashboard/Chatbot/components/Chat.tsx | 4 ++-- src/features/Dashboard/Chatbot/lib/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/Dashboard/Chatbot/components/Chat.tsx b/src/features/Dashboard/Chatbot/components/Chat.tsx index 5d741ef6..a2b3760f 100644 --- a/src/features/Dashboard/Chatbot/components/Chat.tsx +++ b/src/features/Dashboard/Chatbot/components/Chat.tsx @@ -2,8 +2,8 @@ import MessagesContainer from "./MessagesContainer"; export default function Chat() { return ( -
-
+
+
diff --git a/src/features/Dashboard/Chatbot/lib/index.ts b/src/features/Dashboard/Chatbot/lib/index.ts index e727b8a6..a3dc819d 100644 --- a/src/features/Dashboard/Chatbot/lib/index.ts +++ b/src/features/Dashboard/Chatbot/lib/index.ts @@ -3,7 +3,7 @@ import { Tournament } from "src/graphql"; import { getOpenAIApi } from "./open-ai.service"; const SYSTEM_MESSAGE = ` -You are an assisstant chatbot whose job is to help the user in updating his tournament data. +You are an assisstant chatbot whose sole job is to help the user in updating his tournament data. The user will provide you with a prompt that can contain one or more commands from the user. @@ -12,8 +12,8 @@ The user will provide you with a prompt that can contain one or more commands fr RULES: - Only use the functions provided to you & with their parameters. Don't invent new functions. - Don't make assumptions about what values to plug into functions if not clear. Ask for clarification. -- Your sole purpose is to update the tournament data. Don't do anything else. - If you don't know how to do something, tell the user that you can't & suggest to him contacting the admins. +- Don't answer questions or queries not related to updating the tournament data. `; export type Functions = { From b332062f8fc334034a0f97a99983ac2c46df3571 Mon Sep 17 00:00:00 2001 From: "MTG\\mtg09" Date: Wed, 26 Jul 2023 15:26:21 +0400 Subject: [PATCH 4/6] update (chatbot): improve the functions & clean the code --- package-lock.json | 118 ++++++++++++- package.json | 1 + src/features/Dashboard/Chatbot/Chatbot.tsx | 7 +- .../Chatbot/components/TournamentPreview.tsx | 3 +- .../Chatbot/contexts/chat.context.tsx | 48 +++--- .../contexts/tournamentChatbot.context.tsx | 155 ++++++++++++++++++ src/features/Dashboard/Chatbot/lib/index.ts | 77 +-------- .../Dashboard/Chatbot/lib/open-ai.service.ts | 51 +++++- 8 files changed, 351 insertions(+), 109 deletions(-) create mode 100644 src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx diff --git a/package-lock.json b/package-lock.json index 446cc38f..a90f54b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,6 +102,7 @@ "web-vitals": "^2.1.4", "webln": "^0.3.0", "websocket-polyfill": "^0.0.3", + "yaml": "^2.3.1", "yup": "^0.32.11" }, "devDependencies": { @@ -2996,6 +2997,15 @@ "node": ">=8" } }, + "node_modules/@graphql-codegen/cli/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@graphql-codegen/cli/node_modules/yargs": { "version": "17.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", @@ -10296,6 +10306,15 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, + "node_modules/@storybook/builder-webpack4/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/@storybook/builder-webpack5": { "version": "6.4.22", "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-6.4.22.tgz", @@ -18938,6 +18957,15 @@ "node": ">=8" } }, + "node_modules/babel-plugin-emotion/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-extract-import-names": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/babel-plugin-extract-import-names/-/babel-plugin-extract-import-names-1.6.22.tgz", @@ -21506,6 +21534,14 @@ "@iarna/toml": "^2.2.5" } }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/cp-file": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-7.0.0.tgz", @@ -22354,6 +22390,14 @@ "postcss": "^8.2.15" } }, + "node_modules/cssnano/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/csso": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", @@ -25928,6 +25972,14 @@ "node": ">=6" } }, + "node_modules/fork-ts-checker-webpack-plugin/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -59762,6 +59814,14 @@ } } }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "engines": { + "node": ">= 6" + } + }, "node_modules/postcss-loader": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", @@ -69769,11 +69829,11 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==", "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yaml-ast-parser": { @@ -71927,6 +71987,12 @@ "has-flag": "^4.0.0" } }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, "yargs": { "version": "17.4.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", @@ -77407,6 +77473,12 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true } } }, @@ -84367,6 +84439,12 @@ "path-type": "^4.0.0", "yaml": "^1.7.2" } + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true } } }, @@ -86412,6 +86490,13 @@ "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } } }, "cosmiconfig-toml-loader": { @@ -87018,6 +87103,13 @@ "cssnano-preset-default": "^5.2.7", "lilconfig": "^2.0.3", "yaml": "^1.10.2" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } } }, "cssnano-preset-default": { @@ -89869,6 +89961,11 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" } } }, @@ -115537,6 +115634,13 @@ "requires": { "lilconfig": "^2.0.5", "yaml": "^1.10.2" + }, + "dependencies": { + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + } } }, "postcss-loader": { @@ -123233,9 +123337,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz", + "integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==" }, "yaml-ast-parser": { "version": "0.0.43", diff --git a/package.json b/package.json index 45dbfd17..2f1862b4 100644 --- a/package.json +++ b/package.json @@ -97,6 +97,7 @@ "web-vitals": "^2.1.4", "webln": "^0.3.0", "websocket-polyfill": "^0.0.3", + "yaml": "^2.3.1", "yup": "^0.32.11" }, "scripts": { diff --git a/src/features/Dashboard/Chatbot/Chatbot.tsx b/src/features/Dashboard/Chatbot/Chatbot.tsx index fbc252dc..82498842 100644 --- a/src/features/Dashboard/Chatbot/Chatbot.tsx +++ b/src/features/Dashboard/Chatbot/Chatbot.tsx @@ -1,10 +1,9 @@ -import { useParams } from "react-router"; import { useSearchParams } from "react-router-dom"; import OgTags from "src/Components/OgTags/OgTags"; import Chat from "./components/Chat"; import TournamentPreview from "./components/TournamentPreview"; -import { ChatContextProvider } from "./contexts/chat.context"; import { TournamentContextProvider } from "./contexts/tournament.context"; +import { TournamentChatbotContextProvider } from "./contexts/tournamentChatbot.context"; export default function ChatbotPage() { const [searchParams] = useSearchParams(); @@ -22,9 +21,9 @@ export default function ChatbotPage() {
- + - +
diff --git a/src/features/Dashboard/Chatbot/components/TournamentPreview.tsx b/src/features/Dashboard/Chatbot/components/TournamentPreview.tsx index 8072abf4..72906e52 100644 --- a/src/features/Dashboard/Chatbot/components/TournamentPreview.tsx +++ b/src/features/Dashboard/Chatbot/components/TournamentPreview.tsx @@ -76,8 +76,9 @@ export default function TournamentPreview() { Hacker perks from our partners 🎁

    - {tournament.makers_deals.map((deal) => ( + {tournament.makers_deals.map((deal, idx) => (
  • (null!); export const ChatContextProvider = ({ children, + systemMessage, + functionsTemplates, + functions, }: { children: React.ReactNode; + systemMessage: string; + functionsTemplates: ChatCompletionFunctions[]; + functions: Record; }) => { const [messages, setMessages] = useState([]); - const { tournament: currentTournament, updateTournament } = useTournament(); - const submitMessage = useCallback( async (message: string, options) => { - if (!currentTournament) throw new Error("No tournament selected"); - const onStatusUpdate = options?.onStatusUpdate || (() => {}); - const functions: Functions = { - update_tournament: (tournament) => { - updateTournament(tournament); - }, - }; - const oldMessages = messages; const newResponses = [ { content: message, role: "user" }, - ] as ChatCompletionResponseMessage[]; + ] as (ChatCompletionRequestMessage & { internal?: boolean })[]; onStatusUpdate("fetching-response"); let finishedCallingFunctions = false; while (!finishedCallingFunctions) { - const response = await sendCommand([...oldMessages, ...newResponses]); + const response = await sendCommand({ + messages: [...oldMessages, ...newResponses], + availableFunctions: functionsTemplates, + systemMessage: systemMessage, + }); if (response?.function_call) { - execFunction(response.function_call); - newResponses.push(response); + const fnResponse = execFunction(response.function_call); + newResponses.push({ ...response, internal: true }); + + newResponses.push({ + content: YAML.stringify(fnResponse ?? "Success"), + role: "function", + name: response.function_call.name, + internal: true, + }); } else if (response?.content) { newResponses.push(response); finishedCallingFunctions = true; @@ -75,7 +83,7 @@ export const ChatContextProvider = ({ } function execFunction(f: ChatCompletionRequestMessageFunctionCall) { - const fn = functions[f.name as keyof Functions]; + const fn = functions[f.name as string]; if (!fn) throw new Error(`Function ${f.name} not found`); return fn(JSON.parse(f?.arguments as any)); } @@ -90,7 +98,7 @@ export const ChatContextProvider = ({ role: "user", }, ...newResponses - .filter((r) => !r.function_call) + .filter((r) => !r.internal) .filter((r) => r.role === "assistant") .map((r) => ({ id: Math.random().toString(), @@ -99,7 +107,7 @@ export const ChatContextProvider = ({ })), ]); }, - [currentTournament, messages, updateTournament] + [functions, functionsTemplates, messages, systemMessage] ); return ( diff --git a/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx new file mode 100644 index 00000000..fff4709b --- /dev/null +++ b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx @@ -0,0 +1,155 @@ +import { ChatCompletionFunctions } from "openai"; +import { createContext, useContext, useEffect, useMemo, useRef } from "react"; +import { Tournament } from "src/graphql"; +import { ChatContextProvider } from "./chat.context"; +import { useTournament } from "./tournament.context"; + +interface TournamentChatbotContext {} + +const context = createContext(null!); + +export const TournamentChatbotContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const { tournament, updateTournament } = useTournament(); + + const tournamentRef = useRef(tournament); + + useEffect(() => { + tournamentRef.current = tournament; + }, [tournament]); + + const functions: Functions = useMemo(() => { + return { + update_tournament: (updated_data) => { + const newData = { + ...tournamentRef.current, + ...updated_data, + } as Tournament; + + tournamentRef.current = newData; + updateTournament(updated_data); + }, + get_tournament_data: (select) => { + const tournament = tournamentRef.current; + + if (!tournament) throw new Error("No tournament selected"); + + let result: Partial = {}; + + for (const [key, include] of Object.entries(select)) { + if (include) + result[key as keyof Tournament] = + tournament[key as keyof Tournament]; + } + + return { + tournament_data: result, + }; + }, + }; + }, [updateTournament]); + + return ( + + + {children} + + + ); +}; + +export const useTournamentChatbot = () => { + const ctx = useContext(context); + if (!ctx) { + throw new Error( + "useTournament must be used within a TournamentChatbotContextProvider" + ); + } + return ctx; +}; + +const SYSTEM_MESSAGE = ` +You are an assisstant chatbot whose sole job is to help the user in updating his tournament data. + +The user will provide you with a prompt that can contain one or more commands from the user. + +& you will have to decide which functions to use & what parameters to pass to them to update the tournament data. + +RULES: +- Never invent new functions. Only use the functions provided to you. +- Never make assumptions about the values to plug into functions. If not clear, try to check the current tournament data, then ask for clarification. +- If you don't know how to do something, tell the user that you can't & suggest to him contacting the admins. +- Don't answer questions or queries not related to updating the tournament data. +- Don't call the same function twice with the same parameters. +`; + +type SelectTournamentFields = { + [key in keyof Tournament]?: boolean; +}; + +type Functions = { + update_tournament: (parameters: Partial) => void; + get_tournament_data: (parameters: SelectTournamentFields) => { + tournament_data: Partial; + }; +}; + +const availableFunctions: ChatCompletionFunctions[] = [ + { + name: "update_tournament", + description: "Update the tournament data", + parameters: { + type: "object", + properties: { + title: { + type: "string", + description: "The new title of the tournament", + }, + description: { + type: "string", + description: "The new description of the tournament in markdown", + }, + start_date: { + type: "string", + description: "The new start date of the tournament in ISO format", + }, + end_date: { + type: "string", + description: "The new end date of the tournament in ISO format", + }, + }, + }, + }, + { + name: "get_tournament_data", + description: "Select specific fields to get from the tournament data", + parameters: { + type: "object", + properties: { + title: { + type: "boolean", + description: "Get title or not", + }, + description: { + type: "boolean", + description: "Get description or not", + }, + start_date: { + type: "boolean", + description: "Get start date or not", + }, + end_date: { + type: "boolean", + description: "Get end date or not", + }, + }, + }, + }, +]; diff --git a/src/features/Dashboard/Chatbot/lib/index.ts b/src/features/Dashboard/Chatbot/lib/index.ts index a3dc819d..6b6c9a85 100644 --- a/src/features/Dashboard/Chatbot/lib/index.ts +++ b/src/features/Dashboard/Chatbot/lib/index.ts @@ -1,76 +1 @@ -import { ChatCompletionFunctions, ChatCompletionResponseMessage } from "openai"; -import { Tournament } from "src/graphql"; -import { getOpenAIApi } from "./open-ai.service"; - -const SYSTEM_MESSAGE = ` -You are an assisstant chatbot whose sole job is to help the user in updating his tournament data. - -The user will provide you with a prompt that can contain one or more commands from the user. - -& you will have to decide which functions to use & what parameters to pass to them to update the tournament data. - -RULES: -- Only use the functions provided to you & with their parameters. Don't invent new functions. -- Don't make assumptions about what values to plug into functions if not clear. Ask for clarification. -- If you don't know how to do something, tell the user that you can't & suggest to him contacting the admins. -- Don't answer questions or queries not related to updating the tournament data. -`; - -export type Functions = { - update_tournament: (parameters: Partial) => void; -}; - -const availableFunctions: ChatCompletionFunctions[] = [ - { - name: "update_tournament", - description: "Update the tournament data", - parameters: { - type: "object", - properties: { - title: { - type: "string", - description: "The new title of the tournament", - }, - description: { - type: "string", - description: "The new description of the tournament in markdown", - }, - start_date: { - type: "string", - description: "The new start date of the tournament in ISO format", - }, - end_date: { - type: "string", - description: "The new end date of the tournament in ISO format", - }, - }, - }, - }, -]; - -export async function sendCommand(context: ChatCompletionResponseMessage[]) { - const openai = getOpenAIApi(); - - const response = await openai.createChatCompletion({ - model: "gpt-3.5-turbo", - messages: [ - { - role: "system", - content: SYSTEM_MESSAGE, - }, - ...context.map((m) => ({ - role: m.role, - content: m.content, - function_call: m.function_call, - })), - ], - functions: availableFunctions, - }); - - const finish_reason = response.data.choices[0].finish_reason; - - if (finish_reason !== "function_call" && finish_reason !== "stop") - throw new Error(`Unexpected finish reason: ${finish_reason}`); - - return response.data.choices[0].message; -} +export * from "./open-ai.service"; diff --git a/src/features/Dashboard/Chatbot/lib/open-ai.service.ts b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts index 4b4fd2a4..5d4fa835 100644 --- a/src/features/Dashboard/Chatbot/lib/open-ai.service.ts +++ b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts @@ -1,4 +1,10 @@ -import { Configuration, OpenAIApi } from "openai"; +import { + ChatCompletionFunctions, + ChatCompletionRequestMessage, + ChatCompletionResponseMessage, + Configuration, + OpenAIApi, +} from "openai"; import { CONSTS } from "src/utils"; let openai: OpenAIApi; @@ -20,3 +26,46 @@ export function getOpenAIApi() { return openai; } + +export async function sendCommand({ + messages: _messages, + systemMessage, + availableFunctions = [], +}: { + systemMessage?: string; + availableFunctions?: ChatCompletionFunctions[]; + messages: ChatCompletionRequestMessage[]; +}) { + const openai = getOpenAIApi(); + + let messages: ChatCompletionRequestMessage[] = []; + + if (systemMessage) { + messages.push({ + role: "system", + content: systemMessage, + }); + + messages.push( + ..._messages.map((m) => ({ + role: m.role, + name: m.name, + content: m.content, + function_call: m.function_call, + })) + ); + + const response = await openai.createChatCompletion({ + model: "gpt-3.5-turbo", + messages, + ...(availableFunctions.length > 0 && { functions: availableFunctions }), + }); + + const finish_reason = response.data.choices[0].finish_reason; + + if (finish_reason !== "function_call" && finish_reason !== "stop") + throw new Error(`Unexpected finish reason: ${finish_reason}`); + + return response.data.choices[0].message; + } +} From eb1850b3adcd11047328cb7308753064a5083971 Mon Sep 17 00:00:00 2001 From: "MTG\\mtg09" Date: Wed, 26 Jul 2023 16:46:26 +0400 Subject: [PATCH 5/6] feat (chatbot): add 'set-makers-deals' function --- .../Chatbot/contexts/tournament.context.tsx | 6 +- .../contexts/tournamentChatbot.context.tsx | 94 +++++++++++++++---- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx b/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx index ac26dc10..047adabf 100644 --- a/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx +++ b/src/features/Dashboard/Chatbot/contexts/tournament.context.tsx @@ -1,5 +1,9 @@ import { createContext, useState, useContext, useCallback } from "react"; -import { Tournament, useGetTournamentByIdQuery } from "src/graphql"; +import { + Tournament, + TournamentMakerDeal, + useGetTournamentByIdQuery, +} from "src/graphql"; interface TournamentContext { tournament?: Tournament; diff --git a/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx index fff4709b..6b62ee35 100644 --- a/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx +++ b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx @@ -1,6 +1,6 @@ import { ChatCompletionFunctions } from "openai"; import { createContext, useContext, useEffect, useMemo, useRef } from "react"; -import { Tournament } from "src/graphql"; +import { Tournament, TournamentMakerDeal } from "src/graphql"; import { ChatContextProvider } from "./chat.context"; import { useTournament } from "./tournament.context"; @@ -22,17 +22,33 @@ export const TournamentChatbotContextProvider = ({ }, [tournament]); const functions: Functions = useMemo(() => { + const set_tournament_info: Functions["set_tournament_info"] = ( + new_data + ) => { + const newData = { + ...tournamentRef.current, + ...new_data, + } as Tournament; + tournamentRef.current = newData; + updateTournament(new_data); + }; + + const set_tournament_deals: Functions["set_tournament_deals"] = ({ + deals, + }) => { + set_tournament_info({ + makers_deals: deals.map((d) => ({ + ...d, + __typename: "TournamentMakerDeal", + })), + }); + }; + return { - update_tournament: (updated_data) => { - const newData = { - ...tournamentRef.current, - ...updated_data, - } as Tournament; - - tournamentRef.current = newData; - updateTournament(updated_data); - }, - get_tournament_data: (select) => { + set_tournament_info, + set_tournament_deals, + + get_current_tournament_state: (select) => { const tournament = tournamentRef.current; if (!tournament) throw new Error("No tournament selected"); @@ -45,6 +61,11 @@ export const TournamentChatbotContextProvider = ({ tournament[key as keyof Tournament]; } + if (result.makers_deals) + result.makers_deals = result.makers_deals?.map( + ({ __typename, ...data }) => data + ); + return { tournament_data: result, }; @@ -85,9 +106,11 @@ The user will provide you with a prompt that can contain one or more commands fr RULES: - Never invent new functions. Only use the functions provided to you. - Never make assumptions about the values to plug into functions. If not clear, try to check the current tournament data, then ask for clarification. +- ALWAYS get the current state of the tournament before making updates. - If you don't know how to do something, tell the user that you can't & suggest to him contacting the admins. - Don't answer questions or queries not related to updating the tournament data. - Don't call the same function twice with the same parameters. +- functions that start with "set" will replace old data with new data. So make sure to include all the data you want to keep. `; type SelectTournamentFields = { @@ -95,16 +118,17 @@ type SelectTournamentFields = { }; type Functions = { - update_tournament: (parameters: Partial) => void; - get_tournament_data: (parameters: SelectTournamentFields) => { + set_tournament_info: (parameters: Partial) => void; + set_tournament_deals: (parameters: { deals: TournamentMakerDeal[] }) => void; + get_current_tournament_state: (parameters: SelectTournamentFields) => { tournament_data: Partial; }; }; const availableFunctions: ChatCompletionFunctions[] = [ { - name: "update_tournament", - description: "Update the tournament data", + name: "set_tournament_info", + description: "set the new tournament info", parameters: { type: "object", properties: { @@ -128,8 +152,39 @@ const availableFunctions: ChatCompletionFunctions[] = [ }, }, { - name: "get_tournament_data", - description: "Select specific fields to get from the tournament data", + name: "set_tournament_deals", + description: "set the tournament makers deals", + parameters: { + type: "object", + properties: { + deals: { + type: "array", + description: "the list of deals to set", + items: { + type: "object", + properties: { + title: { + type: "string", + description: "The new title of the deal", + }, + description: { + type: "string", + description: "The new description of the deal in markdown", + }, + url: { + type: "string", + description: "The new url of the deal", + }, + }, + }, + }, + }, + }, + }, + { + name: "get_current_tournament_state", + description: + "Get the current state of the tournament selectivly. You will only get the fields you ask for.", parameters: { type: "object", properties: { @@ -149,6 +204,11 @@ const availableFunctions: ChatCompletionFunctions[] = [ type: "boolean", description: "Get end date or not", }, + + makers_deals: { + type: "boolean", + description: "Get makers deals or not", + }, }, }, }, From b69b1fd35d6765b79ec65ae0ccd1fb6cf5d39179 Mon Sep 17 00:00:00 2001 From: "MTG\\mtg09" Date: Wed, 9 Aug 2023 16:27:34 +0400 Subject: [PATCH 6/6] update (chatbot): add embeddings functions --- .../Chatbot/contexts/chat.context.tsx | 10 +- .../contexts/tournamentChatbot.context.tsx | 236 ++++++++++++++---- .../Dashboard/Chatbot/lib/open-ai.service.ts | 12 + 3 files changed, 204 insertions(+), 54 deletions(-) diff --git a/src/features/Dashboard/Chatbot/contexts/chat.context.tsx b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx index b3fd4846..44ce6486 100644 --- a/src/features/Dashboard/Chatbot/contexts/chat.context.tsx +++ b/src/features/Dashboard/Chatbot/contexts/chat.context.tsx @@ -35,11 +35,13 @@ const context = createContext(null!); export const ChatContextProvider = ({ children, systemMessage, + getContextMessage, functionsTemplates, functions, }: { children: React.ReactNode; systemMessage: string; + getContextMessage: (options: { input: string }) => Promise; functionsTemplates: ChatCompletionFunctions[]; functions: Record; }) => { @@ -59,11 +61,17 @@ export const ChatContextProvider = ({ let finishedCallingFunctions = false; + const contextMessage = await getContextMessage({ + input: message, + }); + while (!finishedCallingFunctions) { const response = await sendCommand({ messages: [...oldMessages, ...newResponses], availableFunctions: functionsTemplates, - systemMessage: systemMessage, + systemMessage: + systemMessage + + (contextMessage ? `\nContext: ${contextMessage}` : ""), }); if (response?.function_call) { diff --git a/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx index 6b62ee35..b8b95969 100644 --- a/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx +++ b/src/features/Dashboard/Chatbot/contexts/tournamentChatbot.context.tsx @@ -1,6 +1,15 @@ import { ChatCompletionFunctions } from "openai"; -import { createContext, useContext, useEffect, useMemo, useRef } from "react"; +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useRef, +} from "react"; import { Tournament, TournamentMakerDeal } from "src/graphql"; +import YAML from "yaml"; +import { getEmbeddings } from "../lib"; import { ChatContextProvider } from "./chat.context"; import { useTournament } from "./tournament.context"; @@ -17,6 +26,37 @@ export const TournamentChatbotContextProvider = ({ const tournamentRef = useRef(tournament); + const getContextMessage = useCallback( + async ({ input }: { input: string }) => { + if (!tournamentRef.current) return null; + + const relatedData = await getRelatedData(input); + const dataToInclude = {} as any; + console.log(relatedData); + relatedData.slice(0, 2).forEach(({ field }) => { + if (field === "title") + dataToInclude["Tournament Title"] = tournamentRef.current?.title; + if (field === "description") + dataToInclude["TournamentDescription"] = + tournamentRef.current?.description; + if (field === "start_date") + dataToInclude["Start Date"] = tournamentRef.current?.start_date; + if (field === "end_date") + dataToInclude["End Date"] = tournamentRef.current?.end_date; + if (field === "makers_deals") + dataToInclude["Makers Deals"] = + tournamentRef.current?.makers_deals.map((d) => ({ + title: d.title, + description: d.description, + url: d.url, + })); + }); + + return YAML.stringify(dataToInclude); + }, + [] + ); + useEffect(() => { tournamentRef.current = tournament; }, [tournament]); @@ -48,28 +88,28 @@ export const TournamentChatbotContextProvider = ({ set_tournament_info, set_tournament_deals, - get_current_tournament_state: (select) => { - const tournament = tournamentRef.current; + // get_current_tournament_state: (select) => { + // const tournament = tournamentRef.current; - if (!tournament) throw new Error("No tournament selected"); + // if (!tournament) throw new Error("No tournament selected"); - let result: Partial = {}; + // let result: Partial = {}; - for (const [key, include] of Object.entries(select)) { - if (include) - result[key as keyof Tournament] = - tournament[key as keyof Tournament]; - } + // for (const [key, include] of Object.entries(select)) { + // if (include) + // result[key as keyof Tournament] = + // tournament[key as keyof Tournament]; + // } - if (result.makers_deals) - result.makers_deals = result.makers_deals?.map( - ({ __typename, ...data }) => data - ); + // if (result.makers_deals) + // result.makers_deals = result.makers_deals?.map( + // ({ __typename, ...data }) => data + // ); - return { - tournament_data: result, - }; - }, + // return { + // tournament_data: result, + // }; + // }, }; }, [updateTournament]); @@ -78,6 +118,7 @@ export const TournamentChatbotContextProvider = ({ {children} @@ -105,8 +146,8 @@ The user will provide you with a prompt that can contain one or more commands fr RULES: - Never invent new functions. Only use the functions provided to you. -- Never make assumptions about the values to plug into functions. If not clear, try to check the current tournament data, then ask for clarification. -- ALWAYS get the current state of the tournament before making updates. +- Never make assumptions about the values to plug into functions. If not clear, ask user for clarification. +- Always Use the existing data in the tournament to make decisions. - If you don't know how to do something, tell the user that you can't & suggest to him contacting the admins. - Don't answer questions or queries not related to updating the tournament data. - Don't call the same function twice with the same parameters. @@ -120,9 +161,9 @@ type SelectTournamentFields = { type Functions = { set_tournament_info: (parameters: Partial) => void; set_tournament_deals: (parameters: { deals: TournamentMakerDeal[] }) => void; - get_current_tournament_state: (parameters: SelectTournamentFields) => { - tournament_data: Partial; - }; + // get_current_tournament_state: (parameters: SelectTournamentFields) => { + // tournament_data: Partial; + // }; }; const availableFunctions: ChatCompletionFunctions[] = [ @@ -181,35 +222,124 @@ const availableFunctions: ChatCompletionFunctions[] = [ }, }, }, - { - name: "get_current_tournament_state", - description: - "Get the current state of the tournament selectivly. You will only get the fields you ask for.", - parameters: { - type: "object", - properties: { - title: { - type: "boolean", - description: "Get title or not", - }, - description: { - type: "boolean", - description: "Get description or not", - }, - start_date: { - type: "boolean", - description: "Get start date or not", - }, - end_date: { - type: "boolean", - description: "Get end date or not", - }, + // { + // name: "get_current_tournament_state", + // description: + // "Get the current state of the tournament selectivly. You will only get the fields you ask for.", + // parameters: { + // type: "object", + // properties: { + // title: { + // type: "boolean", + // description: "Get title or not", + // }, + // description: { + // type: "boolean", + // description: "Get description or not", + // }, + // start_date: { + // type: "boolean", + // description: "Get start date or not", + // }, + // end_date: { + // type: "boolean", + // description: "Get end date or not", + // }, - makers_deals: { - type: "boolean", - description: "Get makers deals or not", - }, - }, - }, - }, + // makers_deals: { + // type: "boolean", + // description: "Get makers deals or not", + // }, + // }, + // }, + // }, ]; + +async function getRelatedData(input: string) { + // const input1 = [ + // "add or update title", + // "add or update description", + // "add or update start and end dates", + // "add or update or delete makers deals", + // // "add or update or delete organizers", + // // "add or update or delete prizes", + // // "add or update or delete schedule", + // ]; + + const fieldsEmbeddings = await getFieldsEmbeddings(); + + const inputEmbedding = await getEmbeddings([input]).then( + (res) => res.data[0].embedding + ); + + const similarities = getSimilarities( + inputEmbedding, + fieldsEmbeddings.map((f) => f.embeddings) + ); + + const fieldsSimilarities = fieldsEmbeddings.map((f, i) => ({ + field: f.field, + similarity: similarities[i], + })); + + const sortedFields = fieldsSimilarities.sort( + (a, b) => b.similarity - a.similarity + ); + + return sortedFields; +} + +const fields = { + title: "add or update title", + description: "add or update description", + start_date: "add or update start date", + end_date: "add or update end date", + makers_deals: "add or update makers deals", +} as const; + +let cachedFieldsEmbeddings: + | { field: keyof typeof fields; embeddings: number[] }[] + | null = null; + +async function getFieldsEmbeddings() { + if (cachedFieldsEmbeddings === null) { + const embeddings = await getEmbeddings(Object.values(fields)).then((res) => + res.data.map((d) => d.embedding) + ); + cachedFieldsEmbeddings = embeddings.map((embedding, i) => ({ + field: Object.keys(fields)[i] as keyof typeof fields, + embeddings: embedding, + })); + } + + return cachedFieldsEmbeddings; +} + +type Vector = number[]; + +function getSimilarities(test: Vector, options: Vector[]) { + function dotproduct(a: Vector, b: Vector) { + let n = 0, + lim = Math.min(a.length, b.length); + for (let i = 0; i < lim; i++) n += a[i] * b[i]; + return n; + } + function norm2(a: Vector) { + let sumsqr = 0; + for (let i = 0; i < a.length; i++) sumsqr += a[i] * a[i]; + return Math.sqrt(sumsqr); + } + function similarity(a: Vector, b: Vector) { + return dotproduct(a, b) / norm2(a) / norm2(b); + } + + let similarities = []; + for (let i = 0; i < options.length; i++) { + similarities.push(similarity(test, options[i])); + } + return similarities; +} + +// getRelatedData(` +// Make the end date month september +// `).then((res) => console.log(res)); diff --git a/src/features/Dashboard/Chatbot/lib/open-ai.service.ts b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts index 5d4fa835..9157272c 100644 --- a/src/features/Dashboard/Chatbot/lib/open-ai.service.ts +++ b/src/features/Dashboard/Chatbot/lib/open-ai.service.ts @@ -3,6 +3,7 @@ import { ChatCompletionRequestMessage, ChatCompletionResponseMessage, Configuration, + CreateEmbeddingRequest, OpenAIApi, } from "openai"; import { CONSTS } from "src/utils"; @@ -69,3 +70,14 @@ export async function sendCommand({ return response.data.choices[0].message; } } + +export async function getEmbeddings(input: string | string[]) { + const openai = getOpenAIApi(); + + const response = await openai.createEmbedding({ + model: "text-embedding-ada-002", + input, + }); + + return response.data; +}