Merge pull request #3494 from joaojmendes/OpenAIChatGPT

ChatGPT-APP, Update
This commit is contained in:
Hugo Bernier 2023-03-11 20:24:41 -05:00 committed by GitHub
commit 7f0b96dd66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 228 additions and 113 deletions

View File

@ -101,6 +101,7 @@ This assume you have a Azure App called "OpenAIFunctionsApp", you can change thi
Version|Date|Comments
-------|----|--------
1.0.0|Feb 19, 2023|Initial release
1.1.0|March 2, 2023|Update ChatGPT-APP to use the latest API and model ChatGPT-3.5-turbo
## Minimal Path to Awesome

View File

@ -10,7 +10,7 @@
"This App is a implementation of OpenAI ChatGPT-3. It runs on SharePoint, Teams as Personal or Teams App and Message Extension."
],
"creationDateTime": "2023-02-19",
"updateDateTime": "2023-02-19",
"updateDateTime": "2023-03-02",
"products": [
"SharePoint"
],

View File

@ -1,7 +1,7 @@
export const CARD = {
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.2",
"version": "1.3",
"body": [
{
"type": "ColumnSet",
@ -56,6 +56,21 @@ export const CARD = {
}
]
},
{
"type": "Container",
"spacing": "medium",
"style": "emphasis",
"items": [
{
"type": "TextBlock",
"text": "${$root.question}",
"wrap": true,
"horizontalAlignment": "Right",
"color": "Accent"
}
],
"$when": "${length($root.question)>0}"
},
{
"type": "Container",
"spacing": "Padding",

View File

@ -26,14 +26,19 @@ export const ChatGptControl: React.FunctionComponent<IChatGptProps> = (
const { context } = props;
const [appGlobalState, setAppGlobalState] = useAtom(globalState);
const { containerStyles } = useChatGptStyles();
const { hasTeamsContext, chatId } = appGlobalState;
const { hasTeamsContext, chatId, channelId } = appGlobalState;
const { getTenantProperty } = useSpAPI(context);
const [isLoading, setIsLoading] = React.useState(true);
const [error, setError] = React.useState<Error | undefined>(undefined);
const isInChat = React.useMemo((): boolean => {
return hasTeamsContext && !!chatId;
}, [chatId, hasTeamsContext]);
if (hasTeamsContext && (chatId )) {
return true;
}
return false;
}, [chatId, channelId, hasTeamsContext]);
const isInChannel = React.useMemo(() => !!channelId, [ channelId]);
const isPreviewChatId = React.useMemo((): boolean => {
if (isInChat) {
@ -91,7 +96,7 @@ export const ChatGptControl: React.FunctionComponent<IChatGptProps> = (
return (
<>
<Stack tokens={{ childrenGap: 20 }} styles={containerStyles}>
<Header isInChat={isInChat} />
<Header isInChat={isInChat || isInChannel} />
<RenderPreviewChatInfo isPreviewChatId={isPreviewChatId} />
<RenderMessages isShowMessages={!isPreviewChatId} />
</Stack>

View File

@ -14,7 +14,6 @@ import { showNotification } from '@mantine/notifications';
import { CARD } from '../../adaptiveCards/chatGPTAnswerCard';
import { globalState } from '../../atoms';
import { useAdaptiveCardsUtils } from '../../hooks/useAdaptiveCardsUtils';
import { useGraphAPI } from '../../hooks/useGraphAPI';
import { useSendMessageToTeams } from '../../hooks/useSendMessageToTeams';
import { IAdaptativeCardData } from '../../models/IAdaptivecardData';
import { IRenderAnswerProps } from '../../models/IRenderAnswerProps';
@ -26,24 +25,25 @@ import { SendMessageToChat } from '../SendMessageToChat/SendMessageToChat';
export const RenderAnswer: React.FunctionComponent<IRenderAnswerProps> = (
props: React.PropsWithChildren<IRenderAnswerProps>
) => {
const { answer } = props;
const { answer, question } = props;
const { answerStyles, nameStyles, answerContainerStyles, controlStyles } = useChatGptStyles();
const [appGlobalState] = useAtom(globalState);
const { lastConversation, context, chatId } = appGlobalState;
const { lastConversation, context, chatId, teamsId, channelId, parentMessageId, hasTeamsContext } = appGlobalState;
const [error, setError] = React.useState<Error | undefined>(undefined);
const { sendMessage } = useGraphAPI(context);
const { createAdaptiveCard } = useAdaptiveCardsUtils();
const { sendAdativeCardToUsers } = useSendMessageToTeams(context);
const hasError = React.useMemo(() => error !== undefined, [error]);
const onSendMessageToChat = React.useCallback(async () => {
if (answer && chatId) {
if (answer && hasTeamsContext ) {
try {
const cardData: IAdaptativeCardData = { date: format(new Date(), "PPpp"), answer: answer };
const cardData: IAdaptativeCardData = { date: format(new Date(), "PPpp"), answer: answer, question: question ?? ""};
const card = createAdaptiveCard(cardData, CARD);
console.log("carddata", cardData);
console.log("card", card);
await sendAdativeCardToUsers(card, cardData, chatId);
await sendAdativeCardToUsers(card, cardData, chatId, teamsId, channelId, parentMessageId);
showNotification({
title: strings.ChatGPTAppNotificationTitle,
@ -57,7 +57,7 @@ export const RenderAnswer: React.FunctionComponent<IRenderAnswerProps> = (
setError(error);
}
}
}, [answer, chatId, sendMessage, sendAdativeCardToUsers, createAdaptiveCard]);
}, [answer,teamsId,channelId, hasTeamsContext, chatId, sendAdativeCardToUsers, createAdaptiveCard, question, parentMessageId]);
const islastConversation = React.useMemo(() => lastConversation === "answer", [lastConversation]);
return (
@ -86,7 +86,6 @@ export const RenderAnswer: React.FunctionComponent<IRenderAnswerProps> = (
<SendMessageToChat onSendMessage={onSendMessageToChat} />
</Stack>
</Stack>
<Stack horizontalAlign="start" tokens={{ childrenGap: 10 }}>
<div
dangerouslySetInnerHTML={{ __html: answer?.replace("\n\n", " ") }}

View File

@ -1,3 +1,5 @@
/* eslint-disable require-atomic-updates */
/* eslint-disable @typescript-eslint/no-floating-promises */
import * as React from 'react';
import { useAtom } from 'jotai';
@ -6,8 +8,12 @@ import { IIconProps } from 'office-ui-fabric-react/lib/Icon';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { ChatMessage } from '@microsoft/microsoft-graph-types';
import { globalState } from '../../atoms/globalState';
import { useChatGpt } from '../../hooks';
import { useGraphAPI } from '../../hooks/useGraphAPI';
import { useHtmlUtils } from '../../hooks/useHtmlUtils';
import { useChatGptStyles } from '../ChatGpt/useChatGptStyles';
import { ErrorMessage } from '../ErrorMessage/ErrorMessage';
import { Loading } from '../LoadingAnswer/Loading';
@ -23,7 +29,7 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
) => {
const { isShowMessages } = props;
const [appGlobalState] = useAtom(globalState);
const { context, appId, AzureFunctionUrl } = appGlobalState;
const { context, appId, AzureFunctionUrl, parentMessageId, chatId, teamsId, channelId } = appGlobalState;
const { textFieldStyles, controlStyles, buttonIconStyles } = useChatGptStyles();
const [conversation, setConversation] = React.useState<React.ReactNode[]>([]);
const [textToAsk, setTextToAsk] = React.useState<string>("");
@ -31,6 +37,12 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
const { getCompletion } = useChatGpt(context, appId, AzureFunctionUrl);
const scrollRef = React.useRef<HTMLDivElement>(null);
const [error, setError] = React.useState<Error | undefined>(undefined);
const { getChatParentMessage, getChannelParentMessage } = useGraphAPI(context);
const executeAutoGetComplete = React.useRef<boolean>(false);
const { getTextFromHtml } = useHtmlUtils();
const hasParentMessage = React.useMemo(() => !!parentMessageId , [parentMessageId]);
const isInChannel = React.useMemo(() => !!teamsId && !!channelId, [teamsId, channelId]);
const hasError = React.useMemo(() => error !== undefined, [error]);
@ -54,8 +66,8 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
);
const addAnswer = React.useCallback(
(answer: string) => {
const newAnswer = <RenderAnswer answer={answer} key={conversation.length + 1} />;
(answer: string, question?: string) => {
const newAnswer = <RenderAnswer answer={answer} question={question} key={conversation.length + 1} />;
setConversation((prev) => {
return [...prev, newAnswer];
});
@ -92,6 +104,44 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
[onSubmit]
);
const runAutoGetComplete = React.useCallback(async () => {
try {
let messageDetails: ChatMessage = undefined;
if (!isInChannel ) {
messageDetails = await getChatParentMessage(chatId, parentMessageId);
} else {
messageDetails = await getChannelParentMessage(teamsId, channelId, parentMessageId);
}
const { body } = messageDetails;
if (body) {
setError(undefined);
const { content, } = body;
console.log(body);
const text = getTextFromHtml(content);
if ( text ) {
addQuestion(text);
setIsLoading(true);
const response = await getCompletion(text);
addAnswer(response, text);
}
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
}, []);
React.useEffect(() => {
(async () => {
if (hasParentMessage && !executeAutoGetComplete.current) {
executeAutoGetComplete.current = true;
await runAutoGetComplete();
}
})();
}, [hasParentMessage]);
if (!isShowMessages) {
return null;
}
@ -104,7 +154,7 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
<Stack tokens={{ padding: 20, childrenGap: 10 }}>
<Loading isLoading={isLoading} />
<Stack horizontal tokens={{ childrenGap: 5 }} >
<Stack horizontal tokens={{ childrenGap: 5 }}>
{hasError ? (
<ErrorMessage errorMessage={error?.message} showError={hasError} />
) : (

View File

@ -18,7 +18,7 @@ export const SendMessageToChat: React.FunctionComponent<ISendMessageProps> = (
props: React.PropsWithChildren<ISendMessageProps>
) => {
const [appGlobalState] = useAtom(globalState);
const { hasTeamsContext, chatId, } = appGlobalState;
const { hasTeamsContext, chatId,teamsId,channelId, } = appGlobalState;
const { onSendMessage } = props;
const shareIcon: IIconProps = React.useMemo(() => {return { iconName: "Share" }}, []);
@ -26,8 +26,8 @@ export const SendMessageToChat: React.FunctionComponent<ISendMessageProps> = (
const tooltipId = useId("tooltip");
const isInChat = React.useMemo(() => {
return hasTeamsContext && chatId;
}, [chatId, hasTeamsContext]);
return hasTeamsContext && (chatId || teamsId || channelId);
}, [chatId,teamsId,channelId, hasTeamsContext]);
if (!isInChat) {
return null;

View File

@ -19,7 +19,7 @@ const onProcessMarkdownHandler = (md:any, result: { outputHtml: string; didProc
result.didProcess = false;
}
};
export const useAdaptiveCardsUtils = function () {
export const useAdaptiveCardsUtils = () => {
const createAdaptiveCard = React.useCallback((adaptiveCardData, card) => {
const adaptiveCardToRender = new adaptiveCards.AdaptiveCard();

View File

@ -8,10 +8,18 @@ import {
IHttpClientOptions,
} from '@microsoft/sp-http';
/* const APPID = "6b4a20b2-bf2f-4cbb-a162-af960a40c2dc";
const AZURE_FUNCTION_URL = "https://openaifunctionsapp.azurewebsites.net/api/OpenAICompletion"; */
enum ERole {
user = "user",
assistant = "assistant",
system = "system",
}
interface IMessages {
role:ERole;
content: string;
}
export const useChatGpt = (context: BaseComponentContext, appId: string, AzureFunctionUrl: string) => {
export const useChatGpt = (context: BaseComponentContext, appId:string, AzureFunctionUrl:string) => {
const messages = React.useRef<IMessages[]>([]);
const client = React.useMemo(() => {
if (context) {
return async () => {
@ -25,27 +33,30 @@ export const useChatGpt = (context: BaseComponentContext, appId: string, AzureFu
const getCompletion = React.useCallback(
async (query: string): Promise<string> => {
try {
messages.current.push({ role: ERole.user, content: query });
if (!client) return;
const options: IHttpClientOptions = {
headers: { "Content-Type": "application/json;odata=verbose", Accept: "application/json;odata=verbose" },
mode: "cors",
body: JSON.stringify({ prompt: query }),
body: JSON.stringify({ messages: messages.current }),
method: "POST",
};
const response = await (await client()).post(AzureFunctionUrl, AadHttpClient.configurations.v1, options);
const answer = await response.json();
if (response.status === 200) {
return answer?.choices[0].text;
} else {
console.log("[getCompletion] error:", answer);
throw new Error("Error on executing the request, please try again later.");
}
} catch (error) {
if (!DEBUG) {
console.log("[getCompletion] error:", error);
}
throw error;
}
messages.current.push(answer.choices[0].message)
return answer.choices[0].message.content;
} else {
console.log("[getCompletion] error:", answer);
throw new Error("Error on executing the request, please try again later.");
}
} catch (error) {
if (!DEBUG) {
console.log("[getCompletion] error:", error);
}
throw error;
}
},
[client]
);

View File

@ -8,27 +8,52 @@ import {
} from '@microsoft/microsoft-graph-types';
import { BaseComponentContext } from '@microsoft/sp-component-base';
export const useGraphAPI = (context: BaseComponentContext) => {
export const useGraphAPI = (context: BaseComponentContext) => {
const graphClient = React.useMemo(() => {
return async () => {
const client = await context.msGraphClientFactory.getClient("3");
return client;
};
}, [context]);
const sendMessageToChat = React.useCallback(
async (chatId: string, chatMessagePayload: object): Promise<ChatMessage> => {
const chatMessage = await (await graphClient()).api(`/chats/${chatId}/messages`).post(chatMessagePayload);
return chatMessage;
},
[graphClient]
);
const sendMessage = React.useCallback(async (chatId: string, message: string):Promise<ChatMessage> => {
const client = await context.msGraphClientFactory.getClient("3");
const response:ChatMessage = await client.api(`/chats/${chatId}/messages`)
.post({
body: {
content: `${message} (source: ChatGPT)` ,
},
});
return response;
},[context]);
const sendMessageToChannel = React.useCallback(
async (teamsId: string, channelId: string, chatMessagePayload: object): Promise<ChatMessage> => {
const channelMessage = await (await graphClient())
.api(`/teams/${teamsId}/channels/${channelId}/messages`)
.post(chatMessagePayload);
return channelMessage;
},
[graphClient]
);
const replyToMessage = React.useCallback( async (teamsId: string, channelId: string, parentMessageId: string, chatMessagePayload: object) => {
return (await graphClient())
.api(`/teams/${teamsId}/channels/${channelId}/messages/${parentMessageId}/replies`)
.post(chatMessagePayload);
}, []);
const getChatInfo = React.useCallback(async (chatId:string):Promise<Chat> => {
const client = await context.msGraphClientFactory.getClient("3");
const response:Chat = await client.api(`/chats/${chatId}?$expand=members`)
.get();
return response;
},[context]);
const getChatInfo = React.useCallback(
async (chatId: string): Promise<Chat> => {
const response: Chat = await (await graphClient()).api(`/chats/${chatId}`).get();
return response;
},
[context]
);
return {sendMessage, getChatInfo}
const getChatParentMessage = React.useCallback(async (chatId: string, parentMessageId: string):Promise<ChatMessage> => {
return (await graphClient()).api(`/chats/${chatId}/messages/${parentMessageId}`).get();
}, []);
const getChannelParentMessage = React.useCallback(async (teamId: string,channelId:string, parentMessageId: string):Promise<ChatMessage> => {
return (await graphClient()).api(`/teams/${teamId}/channels/${channelId}/messages/${parentMessageId}`).get();
}, []);
return { sendMessageToChat, sendMessageToChannel, replyToMessage, getChatInfo, getChatParentMessage, getChannelParentMessage };
};

View File

@ -0,0 +1,13 @@
interface IHtmlUtils {
getTextFromHtml: (html: string) => string;
}
export const useHtmlUtils = ():IHtmlUtils => {
const getTextFromHtml = (html: string):string => {
const tmp = document.createElement('DIV');
tmp.innerHTML = html;
return tmp.textContent || tmp.innerText || undefined;
};
return {getTextFromHtml}
};

View File

@ -4,17 +4,15 @@ import { BaseComponentContext } from '@microsoft/sp-component-base';
import { Guid } from '@microsoft/sp-core-library';
import { isEmpty } from '@microsoft/sp-lodash-subset';
import { useGraphAPI } from '../hooks/useGraphAPI';
import { IAdaptativeCardData } from '../models/IAdaptivecardData';
import { HostedContents } from '../models/IChatMessage';
/* eslint-disable @typescript-eslint/explicit-function-return-type */
export const useSendMessageToTeams = (context: BaseComponentContext) => {
const graphClient = React.useMemo(() => {
return async () => {
const client = await context.msGraphClientFactory.getClient("3");
return client;
};
}, [context]);
const { sendMessageToChat, sendMessageToChannel, replyToMessage } = useGraphAPI(context);
const getHostedContent = React.useCallback(async (adaptiveCard: object, adaptiveCardData: IAdaptativeCardData) => {
try {
@ -55,50 +53,17 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => {
[]
);
/* const createChatMembers = React.useCallback((receiverEmail: string) => {
try {
const currentUser = context.pageContext.user.email;
const chatMembers = [
{
"@odata.type": "#microsoft.graph.aadUserConversationMember",
roles: ["owner"],
"user@odata.bind": `https://graph.microsoft.com/v1.0/users('${currentUser}')`,
},
{
"@odata.type": "#microsoft.graph.aadUserConversationMember",
roles: ["owner"],
"user@odata.bind": `https://graph.microsoft.com/v1.0/users('${receiverEmail}')`,
},
];
return chatMembers;
} catch (error) {
if (DEBUG) {
console.error(`[SendMessage.createChatMembers]: error=${error}`);
throw error;
}
}
}, []); */
/*
const createChat = React.useCallback(
async (receiverEmail) => {
try {
const members = createChatMembers(receiverEmail);
const chat = await (await graphClient()).api("/chats").post({ chatType: "oneOnOne", members: members });
return chat;
} catch (error) {
if (DEBUG) {
console.error("[SendMessage.createChat]: error=", error);
throw error;
}
}
},
[graphClient]
);
*/
const sendMessage = React.useCallback(
async (adaptiveCard: object, adaptiveCardData: IAdaptativeCardData, chatId: string) => {
try {
const sendMessage = React.useCallback(
async (
adaptiveCard: object,
adaptiveCardData: IAdaptativeCardData,
chatId: string,
teamsId: string,
channelId: string,
parentMessageId: string
) => {
try {
const { body, attachments, hostedContents } = await getSendMessagePayload(adaptiveCard, adaptiveCardData);
const chatMessagePayload = {
subject: "OpenAI Answer",
@ -106,8 +71,20 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => {
attachments: attachments,
hostedContents: hostedContents,
};
const chatMessage = await (await graphClient()).api(`/chats/${chatId}/messages`).post(chatMessagePayload);
return chatMessage;
if (chatId && !teamsId && !channelId) {
console.log('channelId', channelId);
console.log('teamsId', teamsId);
const chatMessage = await sendMessageToChat(chatId, chatMessagePayload, );
return chatMessage;
}
if (teamsId && channelId && !parentMessageId) {
const channelMessage = sendMessageToChannel(teamsId, channelId, chatMessagePayload);
return channelMessage;
}
if (teamsId && channelId && parentMessageId) {
const replyMessage = await replyToMessage(teamsId, channelId, parentMessageId, chatMessagePayload);
return replyMessage;
}
} catch (error) {
if (DEBUG) {
console.error("[SendMessage]: error=", error);
@ -115,13 +92,20 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => {
}
}
},
[graphClient]
[getSendMessagePayload, sendMessageToChannel, sendMessageToChat, replyToMessage, ]
);
const sendAdativeCardToUsers = React.useCallback(
async (adaptiveCard: object, adaptiveCardData: IAdaptativeCardData, chatId: string) => {
async (
adaptiveCard: object,
adaptiveCardData: IAdaptativeCardData,
chatId: string,
teamsId: string,
channelId: string,
parentMessageId: string
) => {
try {
await sendMessage(adaptiveCard, adaptiveCardData, chatId);
await sendMessage(adaptiveCard, adaptiveCardData, chatId, teamsId, channelId, parentMessageId);
} catch (error) {
if (DEBUG) {
console.error(`[SendMessage.sendAdativeCardToUsers]: error=${error.message}`);
@ -129,7 +113,7 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => {
}
}
},
[graphClient]
[sendMessage]
);
return { sendAdativeCardToUsers };

View File

@ -1,4 +1,5 @@
export interface IAdaptativeCardData {
date: string;
answer: string;
question: string;
}

View File

@ -12,4 +12,7 @@ export interface IChatGptProps {
context: BaseComponentContext;
theme: ITheme | IReadonlyTheme ;
chatId: string;
teamsId: string;
channelId: string;
parentMessageId: string;
}

View File

@ -11,6 +11,9 @@ export interface IGlobalState {
isDarkTheme: boolean;
hasTeamsContext: boolean;
chatId: string;
teamsId: string;
channelId: string;
parentMessageId: string;
appId: string;
AzureFunctionUrl: string;
}

View File

@ -1,3 +1,4 @@
export interface IRenderAnswerProps {
answer: string;
question?:string
}

View File

@ -66,6 +66,9 @@ export default class ChatGptWebPart extends BaseClientSideWebPart<IChatGptProps>
theme: this._currentTheme,
context: this.context,
chatId: this._chatId,
teamsId: this._teamId,
channelId: this._channelId,
parentMessageId: this._parentMessageId,
});
ReactDom.render(element, this.domElement);
@ -84,6 +87,7 @@ export default class ChatGptWebPart extends BaseClientSideWebPart<IChatGptProps>
this._channelId = teamsContext.channel?.id;
this._parentMessageId = teamsContext.app.parentMessageId;
console.log("chatId", this._chatId);
console.log("teamId", this._teamId);
console.log("channelId", this._channelId);

View File

@ -3,7 +3,7 @@ define([], function() {
CgatGPTAppOpenAILabel: " Open AI",
ChatGPTAppNotificationMessage: "Answer was sent to chat",
ChatGPTAppNotificationTitle: "Sent to chat",
ChatGPTAppPoweredByLabel: "Powered by OpenAI, GPT-3",
ChatGPTAppPoweredByLabel: "Powered by OpenAI, ChatGPT-3.5-turbo",
ChatGPTAppPreviewChatInfoMessage: " Please create a mesage first and after return here",
PropertyPaneDescription: "ChatGPT App",
BasicGroupName: "Properties",