Merge pull request #4094 from luismanez/react-azure-openai-connector

This commit is contained in:
Hugo Bernier 2023-10-23 11:36:30 -04:00 committed by GitHub
commit 4373a09b8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 110 additions and 47 deletions

View File

@ -0,0 +1 @@
v16.13.2

View File

@ -6,7 +6,7 @@ This (__experimental__) web part, shows how to use the new feature (_in preview
![./assets/react-azure-openai-connector.gif](./assets/react-azure-openai-connector.gif)
The web part is calling the Azure OpenAI API, with a specific Data source configured to an existing Azure Search service that contains the Hotels index provided by Microsoft. The following screenshots shows how you can configure the connector through the new Azure OpenAI Studio. This is **NOT required** for the sample, but helps to understand what the web part is doing behind the scenes.
The web part is calling the Azure OpenAI API, with a specific Data source configured to an existing Azure Search service that contains the Hotels index provided by Microsoft. The following screenshots shows how you can configure the connector through the new Azure OpenAI Studio. This is __NOT required__ for the sample, but helps to understand what the web part is doing behind the scenes.
First, you have configured your Search service (any tier except the free one) importing the Hotels data sample provided by Microsoft
@ -56,25 +56,23 @@ __Note__: this is an experimental sample, based on the preview of the Azure Open
- [Luis Mañez](https://github.com/luismanez)
## Version history
| Version | Date | Comments |
| ------- | ---------------- | --------------- |
| 1.0 | July 26, 2023 | Initial release |
| 1.1 | October 6, 2023 | Ctrl + enter to submit the query. Auto-scroll in the Messages list. |
## Minimal Path to Awesome
- Install the MS Graph Toolkit for SPFx package. [Follow this](https://learn.microsoft.com/graph/toolkit/get-started/mgt-spfx)
- Clone this repository
- Ensure that you are at the solution folder
- Edit the file __Constants.ts__ with your values (see _prerequisites_ section)
- Edit the file `Constants.ts` with your values (see _prerequisites_ section)
- in the command-line run:
- `npm install`
- `gulp serve`
- Add the web part in the SharePoint workbench or any SharePoint page (appending _?debug=true&noredir=true&debugManifestsFile=https://localhost:4321/temp/manifests.js_ to the page URL)
- Add the web part in the SharePoint workbench or any SharePoint page (appending _?debug=true&noredir=true&debugManifestsFile=<https://localhost:4321/temp/manifests.js>_ to the page URL)
## Features
@ -94,7 +92,9 @@ This extension illustrates the following concepts:
- [Publish SharePoint Framework applications to the Marketplace](https://docs.microsoft.com/sharepoint/dev/spfx/publish-to-marketplace-overview)
- [Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) - Guidance, tooling, samples and open-source controls for your Microsoft 365 development
## Help
## Video
[![Using Azure Open AI custom data sources with SPFx](./assets/video-thumbnail.png)](https://youtu.be/VVjNkwh2W3U?si=tYSpsBdcn5abiC-m&t=1750 "Using Azure Open AI custom data sources with SPFx")
We do not support samples, but this community is always willing to help, and we want to improve these samples. We use GitHub to track issues, which makes it easy for community members to volunteer their time and help resolve issues.

View File

@ -10,7 +10,7 @@
"Shows how to use the new feature of Azure OpenAI Connectors API in a SharePoint Framework web part."
],
"creationDateTime": "2023-07-26",
"updateDateTime": "2023-07-26",
"updateDateTime": "2023-10-06",
"products": [
"SharePoint"
],
@ -30,6 +30,12 @@
"order": 100,
"url": "https://github.com/pnp/sp-dev-fx-webparts/raw/main/samples/react-azure-openai-connector/assets/react-azure-openai-connector.gif",
"alt": "Web Part Preview"
},
{
"type": "video",
"order": 101,
"url": "https://youtu.be/VVjNkwh2W3U?si=tYSpsBdcn5abiC-m&t=1750",
"alt": "Community demo of the web part"
}
],
"authors": [

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

View File

@ -49,8 +49,6 @@ export default class OurHotelsFinderWebPart extends BaseClientSideWebPart<IOurHo
});
}
private _getEnvironmentMessage(): Promise<string> {
if (!!this.context.sdks.microsoftTeams) { // running in Teams, office.com or Outlook
return this.context.sdks.microsoftTeams.teamsJs.app.getContext()

View File

@ -6,7 +6,7 @@ export interface IAssistantResponseProps {
message: string;
}
export default class AssitantResponse extends React.Component<
export default class AssistantResponse extends React.Component<
IAssistantResponseProps,
{}
> {

View File

@ -3,4 +3,5 @@ import { IChatMessage } from "../models/IChatMessage";
export interface IOurHotelsFinderState {
userQuery: string;
sessionMessages: IChatMessage[];
findingHotels: boolean;
}

View File

@ -1,6 +1,6 @@
import * as React from "react";
import UserQuestion from "./UserQuestion";
import AssitantResponse from "./AssistantResponse";
import AssistantResponse from "./AssistantResponse";
import { IChatMessage } from "../models/IChatMessage";
import { ScrollablePane, ScrollbarVisibility } from '@fluentui/react';
@ -11,12 +11,22 @@ export interface IMessagesListProps {
export default class MessagesList extends React.Component<IMessagesListProps, {}> {
public componentDidUpdate(): void {
const scrollContainers = document.querySelectorAll(".ms-ScrollablePane--contentContainer");
const lastScrollContainer = scrollContainers[scrollContainers.length - 1] as HTMLElement;
if (lastScrollContainer) {
lastScrollContainer.scrollTop = lastScrollContainer.scrollHeight;
}
}
public render(): React.ReactElement<IMessagesListProps> {
const output = this.props.messages.map((m, i) => {
if (m.role === 'user') {
return <UserQuestion key={i} message={m.text} />
}
return <AssitantResponse key={i} message={m.text} />
return <AssistantResponse key={i} message={m.text} />
});
return (

View File

@ -1,7 +1,7 @@
import * as React from "react";
import { IOurHotelsFinderProps } from "./IOurHotelsFinderProps";
import MessagesList from "./MessagesList";
import { Stack } from "@fluentui/react";
import { Spinner, SpinnerSize, Stack } from "@fluentui/react";
import UserMessage from "./UserMessage";
import { IOurHotelsFinderState } from "./IOurHotelsFinderState";
import CompletionsService from "../services/CompletionsService";
@ -14,7 +14,8 @@ export default class OurHotelsFinder extends React.Component<IOurHotelsFinderPro
this.state = {
userQuery: '',
sessionMessages: []
sessionMessages: [],
findingHotels: false
};
}
@ -28,6 +29,10 @@ export default class OurHotelsFinder extends React.Component<IOurHotelsFinderPro
console.log(this.state.userQuery);
console.log(this.state.sessionMessages);
this.setState({
findingHotels: true
});
const completionsService: CompletionsService = new CompletionsService(this.props.httpClient);
const response: ICompletionsResponse =
@ -50,7 +55,9 @@ export default class OurHotelsFinder extends React.Component<IOurHotelsFinderPro
});
this.setState({
sessionMessages: tempMessages
sessionMessages: tempMessages,
userQuery: '',
findingHotels: false
});
}
@ -66,8 +73,17 @@ export default class OurHotelsFinder extends React.Component<IOurHotelsFinderPro
>
<MessagesList messages={this.state.sessionMessages} />
</Stack.Item>
{this.state.findingHotels && (
<Stack.Item>
<UserMessage onMessageChange={this._onUserQueryChange} sendQuery={this._onQuerySent} />
<Spinner size={SpinnerSize.large} label="Wait till our super cool AI system is finding you the best hotels..." ariaLive="assertive" labelPosition="right" />
</Stack.Item>
)}
<Stack.Item>
<UserMessage
textFieldValue={this.state.userQuery}
onMessageChange={this._onUserQueryChange}
sendQuery={this._onQuerySent}
/>
</Stack.Item>
</Stack>
);

View File

@ -4,26 +4,55 @@ import * as React from 'react';
export interface IUserMessageProps {
onMessageChange: (query: string) => void;
sendQuery: () => Promise<void>;
textFieldValue: string;
}
export default class UserMessage extends React.Component<IUserMessageProps, {}> {
private _onChange = (ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newText: string): void => {
private _onChange = (
ev: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>,
newText: string
): void => {
this.props.onMessageChange(newText);
}
};
private _handleClick = async (): Promise<void> => {
await this.props.sendQuery();
};
private _keyDownHandler = async (e: KeyboardEvent): Promise<void> => {
if (e.ctrlKey && e.code === "Enter") {
await this._handleClick();
}
};
public componentDidMount(): void {
window.addEventListener("keydown", this._keyDownHandler);
}
public componentWillUnmount(): void {
window.removeEventListener("keydown", this._keyDownHandler);
}
public render(): React.ReactElement<IUserMessageProps> {
return (
<Stack horizontal tokens={{ childrenGap: 5 }}>
<Stack.Item grow={1}>
<TextField multiline autoAdjustHeight onChange={this._onChange} label="User message" placeholder="Type user query here." />
<TextField
multiline
autoAdjustHeight
value={this.props.textFieldValue}
onChange={this._onChange}
label="User message"
placeholder="Type user query here."
/>
</Stack.Item>
<Stack.Item align="end">
<IconButton iconProps={{ iconName: 'Send' }} title="Send" ariaLabel="Send" onClick={this._handleClick} />
<IconButton
iconProps={{ iconName: "Send" }}
title="Send"
ariaLabel="Send"
onClick={this._handleClick}
/>
</Stack.Item>
</Stack>
);

View File

@ -13,17 +13,19 @@ export default class CompletionsRequestBuilder {
endpoint: Constants.AzureSearchEndpoint,
key: Constants.AzureSearchKey,
indexName: Constants.AzureSearchIndexName,
semanticConfiguration: "",
//semanticConfiguration: "hotels-index-semantic-config",
//queryType: "semantic",
queryType: "simple",
fieldsMapping: {
contentFieldsSeparator: "\n",
contentFields: ["Description", "HotelName", "Category"],
filepathField: "HotelName",
contentFields: ["Description", "HotelName", "Description_fr", "Category"],
filepathField: "HotelId",
titleField: "HotelName",
urlField: "HotelName"
urlField: undefined
},
inScope: true,
roleInformation: "You are an AI assistant that helps users of a travel agency to find Hotels in our internal company database for our customers."
// "filter": "Tags/any(g:search.in(g, 'pool, view'))",
}
}],
messages: [{

View File

@ -16,7 +16,7 @@ export interface ICompletionsDataSourceParameters {
endpoint: string;
key: string;
indexName: string;
semanticConfiguration: string;
semanticConfiguration?: string;
queryType: string;
fieldsMapping: ICompletionsDataSourceFieldsMapping;
inScope: boolean;
@ -28,7 +28,7 @@ export interface ICompletionsDataSourceFieldsMapping {
contentFields: string[];
filepathField: string;
titleField: string;
urlField: string;
urlField: string | undefined;
}
export interface ICompletionsMessage {