├── .nvmrc ├── .gitattributes ├── .npmrc ├── packages ├── indexer │ ├── src │ │ ├── lib │ │ │ ├── util │ │ │ │ ├── index.ts │ │ │ │ └── wait.ts │ │ │ ├── formats │ │ │ │ ├── index.ts │ │ │ │ ├── text.ts │ │ │ │ └── pdf.ts │ │ │ ├── index.ts │ │ │ ├── model-limits.ts │ │ │ ├── document.ts │ │ │ └── blob-storage.ts │ │ ├── plugins │ │ │ ├── multipart.ts │ │ │ ├── sensible.ts │ │ │ ├── indexer.ts │ │ │ ├── README.md │ │ │ ├── openapi.ts │ │ │ ├── azure.ts │ │ │ ├── openai.ts │ │ │ └── config.ts │ │ ├── routes │ │ │ ├── root.ts │ │ │ └── README.md │ │ └── app.ts │ ├── bin │ │ └── index-files.js │ ├── test │ │ ├── tsconfig.json │ │ ├── routes │ │ │ └── root.test.ts │ │ └── helper.ts │ ├── tsconfig.json │ ├── .env.example │ ├── README.md │ ├── Dockerfile │ ├── test.http │ └── package.json ├── search │ ├── src │ │ ├── lib │ │ │ ├── util │ │ │ │ ├── index.ts │ │ │ │ └── string.ts │ │ │ ├── langchain │ │ │ │ ├── index.ts │ │ │ │ └── csv-lookup-tool.ts │ │ │ ├── approaches │ │ │ │ ├── index.ts │ │ │ │ └── approach.ts │ │ │ ├── index.ts │ │ │ ├── message.ts │ │ │ ├── message-builder.ts │ │ │ └── tokens.ts │ │ ├── plugins │ │ │ ├── cors.ts │ │ │ ├── sensible.ts │ │ │ ├── README.md │ │ │ ├── openapi.ts │ │ │ ├── approaches.ts │ │ │ ├── azure.ts │ │ │ ├── langchain.ts │ │ │ └── openai.ts │ │ ├── routes │ │ │ └── README.md │ │ └── app.ts │ ├── test │ │ ├── tsconfig.json │ │ ├── routes │ │ │ └── root.test.ts │ │ ├── lib │ │ │ ├── util │ │ │ │ └── string.test.ts │ │ │ ├── langchain │ │ │ │ └── csv-lookup-tool.test.ts │ │ │ └── message-builder.test.ts │ │ └── helper.ts │ ├── data │ │ └── employee-info.csv │ ├── tsconfig.json │ ├── .env.example │ ├── Dockerfile │ ├── README.md │ ├── test.http │ └── package.json ├── chat-component │ ├── src │ │ ├── vite-env.d.ts │ │ ├── core │ │ │ ├── index.ts │ │ │ ├── stream │ │ │ │ ├── data-format │ │ │ │ │ └── ndjson.ts │ │ │ │ └── index.ts │ │ │ └── http │ │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── features.ts │ │ │ ├── citation-previewer.ts │ │ │ ├── voice-input.ts │ │ │ ├── loading-indicator.ts │ │ │ ├── link-icon.ts │ │ │ ├── chat-stage.ts │ │ │ ├── chat-action-button.ts │ │ │ ├── debug-chat-entry.ts │ │ │ ├── follow-up-questions.ts │ │ │ ├── document-previewer.ts │ │ │ ├── copy-entry-to-clipboard.ts │ │ │ ├── default-questions.ts │ │ │ ├── citation-list.ts │ │ │ ├── teaser-list-component.ts │ │ │ ├── chat-context.ts │ │ │ └── tab-component.ts │ │ ├── styles │ │ │ ├── loading-indicator.ts │ │ │ ├── voice-input-button.ts │ │ │ ├── citation-list.ts │ │ │ ├── link-icon.ts │ │ │ ├── tab-component.ts │ │ │ ├── chat-stage.ts │ │ │ ├── teaser-list-component.ts │ │ │ └── chat-action-button.ts │ │ └── types.d.ts │ ├── public │ │ └── svg │ │ │ ├── readme.md │ │ │ ├── send-icon.svg │ │ │ ├── close-icon.svg │ │ │ ├── chevron-up-icon.svg │ │ │ ├── copy-icon.svg │ │ │ ├── cancel-icon.svg │ │ │ ├── spinner-icon.svg │ │ │ ├── delete-icon.svg │ │ │ ├── success-icon.svg │ │ │ ├── mic-icon.svg │ │ │ ├── question-icon.svg │ │ │ ├── lightbulb-icon.svg │ │ │ ├── history-icon.svg │ │ │ ├── mic-record-on-icon.svg │ │ │ ├── history-dismiss-icon.svg │ │ │ └── bubblequestion-icon.svg │ ├── .gitignore │ ├── vite.config.js │ ├── swa-cli.config.json │ ├── tsconfig.json │ ├── package.json │ ├── index.html │ └── icon.svg ├── webapp │ ├── src │ │ ├── components │ │ │ ├── ThemeSwitch │ │ │ │ ├── index.tsx │ │ │ │ ├── ThemeSwitch.css │ │ │ │ └── ThemeSwitch.tsx │ │ │ ├── SettingsButton │ │ │ │ ├── index.tsx │ │ │ │ ├── SettingsButton.module.css │ │ │ │ └── SettingsButton.tsx │ │ │ ├── SettingsStyles │ │ │ │ ├── index.tsx │ │ │ │ └── SettingsStyles.css │ │ │ └── SupportingContent │ │ │ │ ├── index.ts │ │ │ │ ├── supporting-content-parser.ts │ │ │ │ ├── SupportingContent.module.css │ │ │ │ └── SupportingContent.tsx │ │ ├── api │ │ │ ├── index.ts │ │ │ └── models.ts │ │ ├── pages │ │ │ ├── NoPage.tsx │ │ │ ├── oneshot │ │ │ │ └── OneShot.module.css │ │ │ ├── layout │ │ │ │ ├── Layout.module.css │ │ │ │ └── Layout.tsx │ │ │ └── chat │ │ │ │ └── Chat.module.css │ │ ├── vite-env.d.ts │ │ ├── index.css │ │ ├── assets │ │ │ ├── search.svg │ │ │ └── github.svg │ │ ├── index.tsx │ │ └── i18n │ │ │ └── tooltips.ts │ ├── public │ │ └── favicon.ico │ ├── tsconfig.json │ ├── index.html │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── package.json │ └── vite.config.ts └── eslint-config │ ├── .eslintrc.json │ ├── package.json │ └── index.js ├── data └── support.pdf ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── docs ├── deployment.png ├── chat-screenshot.png ├── rag-architecture.png ├── app-architecture.drawio.png └── low-cost.md ├── .lintstagedrc ├── .dockerignore ├── .devcontainer ├── postCreateCommand.sh └── devcontainer.json ├── .gitignore ├── infra ├── core │ ├── security │ │ ├── managed-identity.bicep │ │ ├── role.bicep │ │ └── registry-access.bicep │ ├── host │ │ ├── staticwebapp.bicep │ │ ├── container-apps-environment.bicep │ │ ├── container-apps.bicep │ │ └── container-registry.bicep │ ├── monitor │ │ ├── loganalytics.bicep │ │ ├── applicationinsights.bicep │ │ └── monitoring.bicep │ ├── ai │ │ └── cognitiveservices.bicep │ ├── search │ │ └── search-services.bicep │ └── storage │ │ └── storage-account.bicep └── main.parameters.json ├── scripts ├── index-data.sh ├── index-data.ps1 ├── roles.ps1 └── roles.sh ├── .github ├── CODE_OF_CONDUCT.md ├── workflows │ ├── playwright.yml │ ├── stale-bot.yml │ ├── azure-dev-validation.yaml │ ├── build-test.yaml │ └── azure-dev.yaml ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── tests └── load │ ├── index.js │ ├── mainpage.js │ ├── README.md │ ├── config.js │ └── chat.js ├── LICENSE ├── azure.yaml ├── package.json └── playwright.config.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './wait.js'; 2 | -------------------------------------------------------------------------------- /packages/search/src/lib/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './string.js'; 2 | -------------------------------------------------------------------------------- /packages/chat-component/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ThemeSwitch/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './ThemeSwitch.jsx'; 2 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SettingsButton/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SettingsButton.jsx'; 2 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SettingsStyles/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './SettingsStyles.jsx'; 2 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/formats/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pdf.js'; 2 | export * from './text.js'; 3 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SupportingContent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './SupportingContent.jsx'; 2 | -------------------------------------------------------------------------------- /data/support.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-javascript/HEAD/data/support.pdf -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "ms-azuretools.azure-dev"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-javascript/HEAD/docs/deployment.png -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,jsx,ts,tsx}": ["eslint --fix"], 3 | "*": ["prettier --ignore-unknown --write"] 4 | } 5 | -------------------------------------------------------------------------------- /docs/chat-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-javascript/HEAD/docs/chat-screenshot.png -------------------------------------------------------------------------------- /docs/rag-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-javascript/HEAD/docs/rag-architecture.png -------------------------------------------------------------------------------- /packages/search/src/lib/langchain/index.ts: -------------------------------------------------------------------------------- 1 | export * from './csv-lookup-tool.js'; 2 | export * from './html-callback-handler.js'; 3 | -------------------------------------------------------------------------------- /packages/webapp/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './models.js'; 2 | export const apiBaseUrl = import.meta.env.VITE_SEARCH_API_URI ?? ''; 3 | -------------------------------------------------------------------------------- /docs/app-architecture.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-javascript/HEAD/docs/app-architecture.drawio.png -------------------------------------------------------------------------------- /packages/webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-javascript/HEAD/packages/webapp/public/favicon.ico -------------------------------------------------------------------------------- /packages/webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/readme.md: -------------------------------------------------------------------------------- 1 | ## Icon sources 2 | 3 | All icons in this sample are part of the 4 | https://iconcloud.design/ icon set. 5 | -------------------------------------------------------------------------------- /packages/chat-component/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http/index.js'; 2 | export * from './parser/index.js'; 3 | export * from './stream/index.js'; 4 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/util/wait.ts: -------------------------------------------------------------------------------- 1 | export function wait(ms: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/NoPage.tsx: -------------------------------------------------------------------------------- 1 | export function Component(): JSX.Element { 2 | return

404

; 3 | } 4 | 5 | Component.displayName = 'NoPage'; 6 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ThemeSwitch/ThemeSwitch.css: -------------------------------------------------------------------------------- 1 | .ms-toggle-wrapper { 2 | display: flex; 3 | align-items: flex-start; 4 | margin-top: 20px; 5 | } 6 | -------------------------------------------------------------------------------- /packages/indexer/bin/index-files.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import process from 'node:process'; 3 | import { run } from '../dist/lib/cli.js'; 4 | 5 | run(process.argv); 6 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .azure 4 | .env* 5 | static/ 6 | dist/ 7 | /test-results/ 8 | /playwright-report/ 9 | /playwright/.cache/ 10 | coverage/ 11 | -------------------------------------------------------------------------------- /packages/search/src/lib/approaches/index.ts: -------------------------------------------------------------------------------- 1 | export * from './approach.js'; 2 | export * from './ask-retrieve-then-read.js'; 3 | export * from './chat-read-retrieve-read.js'; 4 | -------------------------------------------------------------------------------- /packages/chat-component/.gitignore: -------------------------------------------------------------------------------- 1 | # node modules 2 | node_modules 3 | 4 | # env files 5 | *.env* 6 | 7 | # distribution 8 | dist/ 9 | 10 | # hint confif 11 | .hintrc 12 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SettingsButton/SettingsButton.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | align-items: center; 4 | gap: 6px; 5 | cursor: pointer; 6 | } 7 | -------------------------------------------------------------------------------- /packages/chat-component/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | export * from './components/index.js'; 4 | export * from './core/index.js'; 5 | export * from './utils/index.js'; 6 | -------------------------------------------------------------------------------- /.devcontainer/postCreateCommand.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Installing dependencies..." 4 | npm install 5 | 6 | echo "Installing playwright browsers..." 7 | npx playwright install --with-deps 8 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/send-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/indexer/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "noEmit": true 6 | }, 7 | "include": ["../src/**/*.ts", "**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/search/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './approaches/index.js'; 2 | export * from './util/index.js'; 3 | export * from './message-builder.js'; 4 | export * from './message.js'; 5 | export * from './tokens.js'; 6 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/close-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/chevron-up-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/search/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "noEmit": true, 6 | "noImplicitAny": false 7 | }, 8 | "include": ["../src/**/*.ts", "**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/search/data/employee-info.csv: -------------------------------------------------------------------------------- 1 | name,title,insurance,insurancegroup 2 | Employee1,Program Manager,Northwind Health Plus,Family 3 | Employee2,Software Engineer,Northwind Health Plus,Single 4 | Employee3,Software Engineer,Northwind Health Standard,Family 5 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/formats/text.ts: -------------------------------------------------------------------------------- 1 | import { type ContentPage } from '../document.js'; 2 | 3 | export async function extractText(data: Buffer): Promise { 4 | const text = data.toString('utf8'); 5 | return [{ content: text, offset: 0, page: 0 }]; 6 | } 7 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './util/index.js'; 2 | export * from './cli.js'; 3 | export * from './blob-storage.js'; 4 | export * from './document-processor.js'; 5 | export * from './document.js'; 6 | export * from './indexer.js'; 7 | export * from './model-limits.js'; 8 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/copy-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/eslint-config/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "extends": "./index.js", 5 | "files": ["*.js", "*.ts"], 6 | "rules": { 7 | "unicorn/prefer-module": "off", 8 | "no-prototype-builtins": "off" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/model-limits.ts: -------------------------------------------------------------------------------- 1 | export interface ModelLimit { 2 | tokenLimit: number; 3 | maxBatchSize: number; 4 | } 5 | 6 | export const MODELS_SUPPORTED_BATCH_SIZE: Record = { 7 | 'text-embedding-ada-002': { 8 | tokenLimit: 8100, 9 | maxBatchSize: 16, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/indexer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "fastify-tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext"] 10 | }, 11 | "include": ["src/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/webapp/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import type * as React from 'react'; 4 | declare global { 5 | namespace JSX { 6 | interface IntrinsicElements { 7 | ['chat-component']: React.DetailedHTMLProps, HTMLElement>; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/search/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "fastify-tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "module": "esnext", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "noStrictGenericChecks": false, 10 | "lib": ["esnext"] 11 | }, 12 | "include": ["src/**/*.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/search/.env.example: -------------------------------------------------------------------------------- 1 | # Azure OpenAI 2 | AZURE_OPENAI_CHATGPT_DEPLOYMENT= 3 | AZURE_OPENAI_CHATGPT_MODEL= 4 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT= 5 | AZURE_OPENAI_EMBEDDING_MODEL= 6 | AZURE_OPENAI_SERVICE= 7 | 8 | # Azure AI Search 9 | AZURE_SEARCH_INDEX= 10 | AZURE_SEARCH_SERVICE= 11 | 12 | # Azure Storage 13 | AZURE_STORAGE_ACCOUNT= 14 | AZURE_STORAGE_CONTAINER= 15 | -------------------------------------------------------------------------------- /packages/indexer/.env.example: -------------------------------------------------------------------------------- 1 | # Azure OpenAI 2 | AZURE_OPENAI_CHATGPT_DEPLOYMENT= 3 | AZURE_OPENAI_CHATGPT_MODEL= 4 | AZURE_OPENAI_EMBEDDING_DEPLOYMENT= 5 | AZURE_OPENAI_EMBEDDING_MODEL= 6 | AZURE_OPENAI_SERVICE= 7 | 8 | # Azure AI Search 9 | AZURE_SEARCH_INDEX= 10 | AZURE_SEARCH_SERVICE= 11 | 12 | # Azure Storage 13 | AZURE_STORAGE_ACCOUNT= 14 | AZURE_STORAGE_CONTAINER= 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Azure az webapp deployment details 2 | .azure 3 | *_env 4 | 5 | # Unit test / coverage reports 6 | .tap/ 7 | packages/*/test-dist/ 8 | 9 | # Environments 10 | .env 11 | 12 | # NPM 13 | npm-debug.log* 14 | node_modules 15 | static/ 16 | dist/ 17 | 18 | # macOS 19 | *.DS_Store* 20 | 21 | # misc 22 | TODO 23 | /test-results/ 24 | /playwright-report/ 25 | /playwright/.cache/ 26 | coverage/ 27 | -------------------------------------------------------------------------------- /packages/indexer/src/plugins/multipart.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import multipart from '@fastify/multipart'; 3 | 4 | const FILE_UPLOAD_LIMIT = 20 * 1024 * 1024; // 20 MB 5 | 6 | export default fp(async (fastify) => { 7 | fastify.register(multipart, { 8 | attachFieldsToBody: true, 9 | sharedSchemaId: 'multipartField', 10 | limits: { fileSize: FILE_UPLOAD_LIMIT, files: 1 }, 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/cancel-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/search/test/routes/root.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { build } from '../helper.js'; 3 | 4 | test('default root route', async (t) => { 5 | const app = await build(t); 6 | const response = await app.inject({ url: '/' }); 7 | 8 | const result = JSON.parse(response.payload); 9 | t.assert.partialDeepStrictEqual(result, { service: 'search', description: 'AI search service', version: '1.0.0' }); 10 | }); 11 | -------------------------------------------------------------------------------- /infra/core/security/managed-identity.bicep: -------------------------------------------------------------------------------- 1 | param name string 2 | param location string = resourceGroup().location 3 | 4 | resource apiIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { 5 | name: name 6 | location: location 7 | } 8 | 9 | output tenantId string = apiIdentity.properties.tenantId 10 | output principalId string = apiIdentity.properties.principalId 11 | output clientId string = apiIdentity.properties.clientId 12 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/spinner-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/search/src/lib/message.ts: -------------------------------------------------------------------------------- 1 | export type MessageRole = 'system' | 'user' | 'assistant'; 2 | 3 | export interface Message { 4 | role: MessageRole; 5 | content: string; 6 | } 7 | 8 | export function messageToString(message: Message): string { 9 | return `${message.role}: ${message.content}`; 10 | } 11 | 12 | export function messagesToString(messages: Message[]): string { 13 | return messages.map((m) => messageToString(m)).join('\n\n'); 14 | } 15 | -------------------------------------------------------------------------------- /packages/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | GPT + Enterprise data | Sample 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/delete-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/index.ts: -------------------------------------------------------------------------------- 1 | // shared components 2 | import './link-icon.js'; 3 | import './chat-stage.js'; 4 | import './loading-indicator.js'; 5 | import './citation-list.js'; 6 | import './chat-thread-component.js'; 7 | import './chat-action-button.js'; 8 | 9 | import './chat-context.js'; 10 | 11 | export * from './composable.js'; 12 | import './features.js'; 13 | import './copy-entry-to-clipboard.js'; 14 | 15 | export * from './chat-component.js'; 16 | -------------------------------------------------------------------------------- /packages/indexer/test/routes/root.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { build } from '../helper.js'; 3 | 4 | test('default root route', async (t) => { 5 | const app = await build(t); 6 | 7 | const response = await app.inject({ url: '/' }); 8 | 9 | const result = JSON.parse(response.payload); 10 | t.assert.partialDeepStrictEqual(result, { 11 | service: 'indexer', 12 | description: 'Document indexer service', 13 | version: '1.0.0', 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /scripts/index-data.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd "$(dirname "${BASH_SOURCE[0]}")/.." 3 | 4 | echo "Loading azd .env file from current environment" 5 | export $(azd env get-values | xargs) 6 | 7 | echo 'Installing dependencies and building CLI' 8 | npm ci 9 | npm run build --workspace=indexer 10 | 11 | echo 'Running "index-files" CLI tool' 12 | npx index-files \ 13 | --wait \ 14 | --indexer-url "${INDEXER_API_URI}" \ 15 | --index-name "${AZURE_SEARCH_INDEX}" \ 16 | ./data/*.* 17 | -------------------------------------------------------------------------------- /packages/chat-component/vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | // eslint-disable-next-line n/no-unpublished-import 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | emptyOutDir: true, 8 | lib: { 9 | // eslint-disable-next-line unicorn/prefer-module, no-undef 10 | entry: resolve(__dirname, 'src/index.ts'), 11 | name: 'chat-component', 12 | fileName: 'chat-component', 13 | }, 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /packages/search/src/plugins/cors.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import cors from '@fastify/cors'; 3 | 4 | export default fp( 5 | async (fastify) => { 6 | const allowedOrigins = fastify.config.allowedOrigins.split(',').map((origin) => origin.trim()); 7 | fastify.log.info(`CORS allowed origins: ${allowedOrigins.join(', ')}`); 8 | fastify.register(cors, { 9 | origin: allowedOrigins, 10 | }); 11 | }, 12 | { 13 | name: 'cors', 14 | dependencies: ['config'], 15 | }, 16 | ); 17 | -------------------------------------------------------------------------------- /tests/load/index.js: -------------------------------------------------------------------------------- 1 | import { mainpage } from './mainpage.js'; 2 | import { chat } from './chat.js'; 3 | import { thresholdsSettings, standardWorkload } from './config.js'; 4 | 5 | export const options = { 6 | scenarios: { 7 | staged: standardWorkload, 8 | }, 9 | thresholds: thresholdsSettings, 10 | }; 11 | 12 | const webappUrl = __ENV.WEBAPP_URI; 13 | const searchUrl = __ENV.SEARCH_API_URI; 14 | 15 | export default function () { 16 | mainpage(webappUrl); 17 | chat(searchUrl, true); 18 | //chat(searchUrl, false); 19 | } 20 | -------------------------------------------------------------------------------- /tests/load/mainpage.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { Trend } from 'k6/metrics'; 3 | import { group, sleep } from 'k6'; 4 | 5 | const mainpageLatency = new Trend('mainpage_duration'); 6 | 7 | export function mainpage(baseUrl) { 8 | group('Mainpage', function () { 9 | // save response as variable 10 | const response = http.get(`${baseUrl}`, { tags: { type: 'content' } }); 11 | // add duration property to metric 12 | mainpageLatency.add(response.timings.duration, { type: 'content' }); 13 | sleep(1); 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /packages/chat-component/swa-cli.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", 3 | "configurations": { 4 | "frontend": { 5 | "appLocation": "./src", 6 | "apiLocation": "", 7 | "outputLocation": "./dist", 8 | "apiLanguage": "node", 9 | "apiVersion": "18", 10 | "appBuildCommand": "npm run build --if-present", 11 | "apiBuildCommand": "npm run build --if-present", 12 | "run": "npm start", 13 | "appDevserverUrl": "http://localhost:8000" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/loading-indicator.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | @keyframes spinneranimation { 5 | 0% { 6 | transform: rotate(0deg); 7 | } 8 | 100% { 9 | transform: rotate(360deg); 10 | } 11 | } 12 | p { 13 | display: flex; 14 | align-items: center; 15 | } 16 | svg { 17 | width: var(--d-large); 18 | height: 30px; 19 | fill: var(--c-accent-light); 20 | animation: spinneranimation 1s linear infinite; 21 | margin-right: 10px; 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/features.ts: -------------------------------------------------------------------------------- 1 | // [COMPOSE COMPONENTS START] 2 | 3 | import './chat-debug-thought-process.js'; 4 | import './debug-chat-entry.js'; 5 | import './tab-component.js'; 6 | 7 | import './voice-input.js'; 8 | import './voice-input-button.js'; 9 | 10 | import './default-questions.js'; 11 | import './teaser-list-component.js'; 12 | 13 | import './document-previewer.js'; 14 | import './citation-previewer.js'; 15 | 16 | import './follow-up-questions.js'; 17 | 18 | import './chat-history-controller.js'; 19 | // [COMPOSE COMPONENTS END] 20 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/document.ts: -------------------------------------------------------------------------------- 1 | export interface Document { 2 | filename: string; 3 | type: string; 4 | category: string; 5 | sections: Section[]; 6 | } 7 | 8 | export interface Section { 9 | id: string; 10 | content: string; 11 | category: string; 12 | sourcepage: string; 13 | sourcefile: string; 14 | embedding?: number[]; 15 | } 16 | 17 | export interface ContentPage { 18 | content: string; 19 | offset: number; 20 | page: number; 21 | } 22 | 23 | export interface ContentSection { 24 | content: string; 25 | page: number; 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "Start app", 6 | "type": "shell", 7 | "command": "npm", 8 | "args": ["start"], 9 | "presentation": { 10 | "reveal": "always" 11 | }, 12 | "options": { 13 | "cwd": "${workspaceFolder}" 14 | }, 15 | "problemMatcher": [] 16 | } 17 | ], 18 | "inputs": [ 19 | { 20 | "id": "dotEnvFilePath", 21 | "type": "command", 22 | "command": "azure-dev.commands.getDotEnvFilePath" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /infra/core/host/staticwebapp.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Static Web Apps instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param sku object = { 7 | name: 'Free' 8 | tier: 'Free' 9 | } 10 | 11 | resource web 'Microsoft.Web/staticSites@2022-03-01' = { 12 | name: name 13 | location: location 14 | tags: tags 15 | sku: sku 16 | properties: { 17 | provider: 'Custom' 18 | } 19 | } 20 | 21 | output name string = web.name 22 | output uri string = 'https://${web.properties.defaultHostname}' 23 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SupportingContent/supporting-content-parser.ts: -------------------------------------------------------------------------------- 1 | type ParsedSupportingContentItem = { 2 | title: string; 3 | content: string; 4 | }; 5 | 6 | export function parseSupportingContentItem(item: string): ParsedSupportingContentItem { 7 | // Assumes the item starts with the file name followed by : and the content. 8 | // Example: "sdp_corporate.pdf: this is the content that follows". 9 | const parts = item.split(': '); 10 | const title = parts[0]; 11 | const content = parts.slice(1).join(': '); 12 | 13 | return { 14 | title, 15 | content, 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/success-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/citation-previewer.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { container, type CitationController, ControllerType, ComposableReactiveControllerBase } from './composable.js'; 3 | import { html } from 'lit'; 4 | 5 | @injectable() 6 | export class CitationPreviewer extends ComposableReactiveControllerBase implements CitationController { 7 | render(citation: Citation, url: string) { 8 | return html``; 9 | } 10 | } 11 | 12 | container.bind(ControllerType.Citation).to(CitationPreviewer); 13 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SettingsStyles/SettingsStyles.css: -------------------------------------------------------------------------------- 1 | .ms-style-picker.colors, 2 | .ms-style-picker.sliders { 3 | display: grid; 4 | margin-top: 10px; 5 | padding-bottom: 20px; 6 | } 7 | 8 | .ms-style-picker.colors { 9 | grid-template-columns: repeat(4, 1fr); 10 | } 11 | 12 | .ms-style-picker > input, 13 | .ms-style-picker > label { 14 | margin-bottom: 3px; 15 | margin-right: 10px; 16 | font-size: small; 17 | font-weight: bold; 18 | } 19 | 20 | .ms-settings-input-slider { 21 | display: flex; 22 | align-items: center; 23 | justify-content: space-around; 24 | margin-bottom: 10px; 25 | } 26 | -------------------------------------------------------------------------------- /infra/core/monitor/loganalytics.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a Log Analytics workspace.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { 7 | name: name 8 | location: location 9 | tags: tags 10 | properties: any({ 11 | retentionInDays: 30 12 | features: { 13 | searchVersion: 1 14 | } 15 | sku: { 16 | name: 'PerGB2018' 17 | } 18 | }) 19 | } 20 | 21 | output id string = logAnalytics.id 22 | output name string = logAnalytics.name 23 | -------------------------------------------------------------------------------- /packages/indexer/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with [Fastify-CLI](https://www.npmjs.com/package/fastify-cli) 2 | 3 | This project was bootstrapped with Fastify-CLI. 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm run dev` 10 | 11 | To start the app in dev mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | ### `npm start` 15 | 16 | For production mode 17 | 18 | ### `npm run test` 19 | 20 | Run the test cases. 21 | 22 | ## Learn More 23 | 24 | To learn Fastify, check out the [Fastify documentation](https://www.fastify.io/docs/latest/). 25 | -------------------------------------------------------------------------------- /tests/load/README.md: -------------------------------------------------------------------------------- 1 | The tests use [k6](https://k6.io/) to perform load testing. 2 | 3 | # Install k6 4 | 5 | k6 is already included in the dev container, so no further installation is required. 6 | 7 | For manual installation, refer to [k6 installation docs](https://k6.io/docs/get-started/installation/). 8 | 9 | # To run the test 10 | 11 | Set the following environment variables to point to the deployment. 12 | 13 | ``` 14 | export WEBAPP_URI= 15 | export SEARCH_API_URI= 16 | ``` 17 | 18 | Once set, you can now run load tests using the following command: 19 | 20 | ``` 21 | npm run test:load 22 | ``` 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "editor.formatOnSave": true 9 | }, 10 | "[typescriptreact]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true 13 | }, 14 | "[css]": { 15 | "editor.defaultFormatter": "esbenp.prettier-vscode", 16 | "editor.formatOnSave": true 17 | }, 18 | "search.exclude": { 19 | "**/node_modules": true, 20 | "static": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/search/src/plugins/sensible.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import sensible, { type FastifySensibleOptions } from '@fastify/sensible'; 3 | 4 | /** 5 | * This plugins adds some utilities to handle http errors 6 | * @see https://github.com/fastify/fastify-sensible 7 | */ 8 | export default fp(async (fastify) => { 9 | fastify.register(sensible); 10 | 11 | fastify.addSchema({ 12 | $id: 'httpError', 13 | type: 'object', 14 | properties: { 15 | statusCode: { type: 'number' }, 16 | code: { type: 'string' }, 17 | error: { type: 'string' }, 18 | message: { type: 'string' }, 19 | }, 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/indexer/src/plugins/sensible.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import sensible, { type FastifySensibleOptions } from '@fastify/sensible'; 3 | 4 | /** 5 | * This plugins adds some utilities to handle http errors 6 | * @see https://github.com/fastify/fastify-sensible 7 | */ 8 | export default fp(async (fastify) => { 9 | fastify.register(sensible); 10 | 11 | fastify.addSchema({ 12 | $id: 'httpError', 13 | type: 'object', 14 | properties: { 15 | statusCode: { type: 'number' }, 16 | code: { type: 'string' }, 17 | error: { type: 'string' }, 18 | message: { type: 'string' }, 19 | }, 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/voice-input.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { container, type ChatInputController, ControllerType, ComposableReactiveControllerBase } from './composable.js'; 3 | import { html } from 'lit'; 4 | 5 | @injectable() 6 | export class VoiceInputController extends ComposableReactiveControllerBase implements ChatInputController { 7 | position = 'right'; 8 | 9 | render(handleInput: (input: string) => void) { 10 | return html``; 11 | } 12 | } 13 | 14 | container.bind(ControllerType.ChatInput).to(VoiceInputController); 15 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SettingsButton/SettingsButton.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from '@fluentui/react'; 2 | import { Settings24Regular } from '@fluentui/react-icons'; 3 | 4 | import styles from './SettingsButton.module.css'; 5 | 6 | interface Props { 7 | className?: string; 8 | onClick: () => void; 9 | } 10 | 11 | export const SettingsButton = ({ className, onClick }: Props) => { 12 | return ( 13 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/indexer/src/plugins/indexer.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { Indexer } from '../lib/index.js'; 3 | 4 | export default fp( 5 | async (fastify, _options) => { 6 | const config = fastify.config; 7 | 8 | fastify.decorate( 9 | 'indexer', 10 | new Indexer(fastify.log, fastify.azure, fastify.openai, config.azureOpenAiEmbeddingModel), 11 | ); 12 | }, 13 | { 14 | name: 'indexer', 15 | dependencies: ['config', 'azure', 'openai'], 16 | }, 17 | ); 18 | 19 | // When using .decorate you have to specify added properties for Typescript 20 | declare module 'fastify' { 21 | export interface FastifyInstance { 22 | indexer: Indexer; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /infra/core/security/role.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates a role assignment for a service principal.' 2 | param principalId string 3 | 4 | @allowed([ 5 | 'Device' 6 | 'ForeignGroup' 7 | 'Group' 8 | 'ServicePrincipal' 9 | 'User' 10 | ]) 11 | param principalType string = 'ServicePrincipal' 12 | param roleDefinitionId string 13 | 14 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 15 | name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) 16 | properties: { 17 | principalId: principalId 18 | principalType: principalType 19 | roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-shared", 3 | "version": "1.0.0", 4 | "description": "ESLint shared config for this project", 5 | "private": true, 6 | "main": "./index.js", 7 | "peerDependencies": { 8 | "@typescript-eslint/eslint-plugin": "^6.7.0", 9 | "eslint": "^8.49.0", 10 | "eslint-plugin-import": "^2.28.1", 11 | "eslint-plugin-jsx-a11y": "^6.7.1", 12 | "eslint-plugin-n": "^16.1.0", 13 | "eslint-plugin-react": "^7.33.2", 14 | "eslint-plugin-react-hooks": "^4.6.0", 15 | "eslint-plugin-unicorn": "^48.0.1", 16 | "typescript": "^5.7.3" 17 | }, 18 | "dependencies": { 19 | "@typescript-eslint/parser": "^6.7.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/webapp/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /packages/indexer/src/routes/root.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { type FastifyPluginAsync } from 'fastify'; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | const root: FastifyPluginAsync = async (fastify, _options): Promise => { 9 | fastify.get('/', async function (_request, _reply) { 10 | const packageJson = JSON.parse(await fs.readFile(path.join(__dirname, '../../package.json'), 'utf8')); 11 | return { 12 | service: packageJson.name, 13 | description: packageJson.description, 14 | version: packageJson.version, 15 | }; 16 | }); 17 | }; 18 | 19 | export default root; 20 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SupportingContent/SupportingContent.module.css: -------------------------------------------------------------------------------- 1 | .supportingContentNavList { 2 | list-style: none; 3 | padding-left: 5px; 4 | display: flex; 5 | flex-direction: column; 6 | gap: 10px; 7 | } 8 | 9 | .supportingContentItem { 10 | word-break: break-word; 11 | background: rgb(249, 249, 249); 12 | border-radius: 8px; 13 | box-shadow: 14 | rgb(0 0 0 / 5%) 0px 0px 0px 1px, 15 | rgb(0 0 0 / 10%) 0px 2px 3px 0px; 16 | outline: transparent solid 1px; 17 | 18 | display: flex; 19 | flex-direction: column; 20 | padding: 20px; 21 | } 22 | 23 | .supportingContentItemHeader { 24 | margin: 0; 25 | } 26 | 27 | .supportingContentItemText { 28 | margin-bottom: 0; 29 | font-weight: 300; 30 | } 31 | -------------------------------------------------------------------------------- /tests/load/config.js: -------------------------------------------------------------------------------- 1 | export const thresholdsSettings = { 2 | 'http_req_failed{type:API}': [{ threshold: 'rate<0.01' }], // less than 1% failed requests 3 | 'http_req_failed{type:content}': [{ threshold: 'rate<0.01' }], // less than 1% failed requests 4 | 'http_req_duration{type:API}': ['p(90)<40000'], // 90% of the API requests must complete below 40s 5 | 'http_req_duration{type:content}': ['p(99)<200'], // 99% of the content requests must complete below 200ms 6 | }; 7 | 8 | // 5.00 iterations/s for 1m0s (maxVUs: 100-200, gracefulStop: 30s) 9 | export const standardWorkload = { 10 | executor: 'constant-arrival-rate', 11 | rate: 5, 12 | timeUnit: '1s', 13 | duration: '1m', 14 | preAllocatedVUs: 100, 15 | maxVUs: 200, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/voice-input-button.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | button { 5 | color: var(--text-color); 6 | font-weight: bold; 7 | margin-left: 8px; 8 | background: transparent; 9 | transition: background 0.3s ease-in-out; 10 | box-shadow: none; 11 | border: none; 12 | cursor: pointer; 13 | width: var(--d-xlarge); 14 | height: 100%; 15 | } 16 | button:hover, 17 | button:focus { 18 | background: var(--c-secondary); 19 | } 20 | button:hover svg, 21 | button:focus svg { 22 | opacity: 0.8; 23 | } 24 | .not-recording svg { 25 | fill: var(--c-black); 26 | } 27 | .recording svg { 28 | fill: var(--red); 29 | } 30 | `; 31 | -------------------------------------------------------------------------------- /scripts/index-data.ps1: -------------------------------------------------------------------------------- 1 | $scriptPath = $MyInvocation.MyCommand.Path 2 | cd $scriptPath/../.. 3 | 4 | Write-Host "Loading azd .env file from current environment" 5 | $output = azd env get-values 6 | 7 | foreach ($line in $output) { 8 | if (!$line.Contains('=')) { 9 | continue 10 | } 11 | 12 | $name, $value = $line.Split("=") 13 | $value = $value -replace '^\"|\"$' 14 | [Environment]::SetEnvironmentVariable($name, $value) 15 | } 16 | 17 | Write-Host 'Installing dependencies and building CLI' 18 | npm ci 19 | npm run build --workspace=indexer 20 | 21 | Write-Host 'Running "index-files" CLI tool' 22 | $files = Get-Item "data/*.*" 23 | npx index-files --wait --indexer-url "$env:INDEXER_API_URI" --index-name "$env:AZURE_SEARCH_INDEX" $files 24 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/mic-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/indexer/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build Node.js app 4 | # ------------------------------------ 5 | FROM node:22-alpine as build 6 | WORKDIR /app 7 | COPY ./package*.json ./ 8 | COPY ./packages/indexer ./packages/indexer 9 | RUN npm ci --cache /tmp/empty-cache 10 | RUN npm run build --workspace=indexer 11 | 12 | # Run Node.js app 13 | # ------------------------------------ 14 | FROM node:22-alpine 15 | ENV NODE_ENV=production 16 | 17 | WORKDIR /app 18 | COPY ./package*.json ./ 19 | COPY ./packages/indexer/package.json ./packages/indexer/ 20 | RUN npm ci --omit=dev --workspace=indexer --cache /tmp/empty-cache 21 | COPY --from=build app/packages/indexer/dist packages/indexer/dist 22 | EXPOSE 3001 23 | CMD [ "npm", "start", "--workspace=indexer" ] 24 | -------------------------------------------------------------------------------- /packages/webapp/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | margin: 0; 9 | padding: 0; 10 | } 11 | 12 | body.dark { 13 | background: #1e1e1e; 14 | color: #fff; 15 | } 16 | 17 | html { 18 | background: #f2f2f2; 19 | 20 | font-family: 21 | 'Segoe UI', 22 | -apple-system, 23 | BlinkMacSystemFont, 24 | 'Roboto', 25 | 'Oxygen', 26 | 'Ubuntu', 27 | 'Cantarell', 28 | 'Fira Sans', 29 | 'Droid Sans', 30 | 'Helvetica Neue', 31 | sans-serif; 32 | -webkit-font-smoothing: antialiased; 33 | -moz-osx-font-smoothing: grayscale; 34 | } 35 | 36 | button { 37 | all: unset; 38 | } 39 | 40 | button:focus { 41 | outline: revert; 42 | } 43 | 44 | #root { 45 | height: 100%; 46 | } 47 | -------------------------------------------------------------------------------- /packages/search/test/lib/util/string.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { removeNewlines, parseBoolean } from '../../../src/lib/util/string.js'; 3 | 4 | test('removeNewlines', (t) => { 5 | t.assert.equal(removeNewlines('Hello\nworld'), 'Hello world'); 6 | t.assert.equal(removeNewlines('Hello\r\nworld'), 'Hello world'); 7 | t.assert.equal(removeNewlines(''), ''); 8 | t.assert.equal(removeNewlines(), ''); 9 | }); 10 | 11 | test('parseBoolean', (t) => { 12 | t.assert.equal(parseBoolean('true'), true); 13 | t.assert.equal(parseBoolean('false'), false); 14 | // eslint-disable-next-line unicorn/no-useless-undefined 15 | t.assert.equal(parseBoolean(undefined), false); 16 | t.assert.equal(parseBoolean(true), true); 17 | t.assert.equal(parseBoolean(false), false); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/question-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/webapp/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "noUncheckedSideEffectImports": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/chat-component/src/core/stream/data-format/ndjson.ts: -------------------------------------------------------------------------------- 1 | export class NdJsonParserStream extends TransformStream { 2 | private buffer: string = ''; 3 | constructor() { 4 | let controller: TransformStreamDefaultController; 5 | super({ 6 | start: (_controller) => { 7 | controller = _controller; 8 | }, 9 | transform: (chunk) => { 10 | const jsonChunks = chunk.split('\n').filter(Boolean); 11 | for (const jsonChunk of jsonChunks) { 12 | try { 13 | this.buffer += jsonChunk; 14 | controller.enqueue(JSON.parse(this.buffer)); 15 | this.buffer = ''; 16 | } catch { 17 | // Invalid JSON, wait for next chunk 18 | } 19 | } 20 | }, 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [main, develop] 5 | pull_request: 6 | branches: [main, develop] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 22 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Install Playwright Browsers 19 | run: npm run install:playwright 20 | - name: Run Playwright tests 21 | run: npm run test:playwright 22 | - uses: actions/upload-artifact@v4 23 | if: failure() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/loading-indicator.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { styles } from '../styles/loading-indicator.js'; 4 | import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; 5 | import iconSpinner from '../../public/svg/spinner-icon.svg?raw'; 6 | 7 | @customElement('loading-indicator') 8 | export class LoadingIndicatorComponent extends LitElement { 9 | static override styles = [styles]; 10 | 11 | @property({ type: String }) 12 | label: string = ''; 13 | 14 | override render() { 15 | return html` 16 |

17 | ${unsafeSVG(iconSpinner)} 18 | ${this.label} 19 |

20 | `; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/indexer/src/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins Folder 2 | 3 | Plugins define behavior that is common to all the routes in your 4 | application. Authentication, caching, templates, and all the other cross 5 | cutting concerns should be handled by plugins placed in this folder. 6 | 7 | Files in this folder are typically defined through the 8 | [`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, 9 | making them non-encapsulated. They can define decorators and set hooks 10 | that will then be used in the rest of your application. 11 | 12 | Check out: 13 | 14 | - [The hitchhiker's guide to plugins](https://www.fastify.io/docs/latest/Guides/Plugins-Guide/) 15 | - [Fastify decorators](https://www.fastify.io/docs/latest/Reference/Decorators/). 16 | - [Fastify lifecycle](https://www.fastify.io/docs/latest/Reference/Lifecycle/). 17 | -------------------------------------------------------------------------------- /packages/search/src/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Plugins Folder 2 | 3 | Plugins define behavior that is common to all the routes in your 4 | application. Authentication, caching, templates, and all the other cross 5 | cutting concerns should be handled by plugins placed in this folder. 6 | 7 | Files in this folder are typically defined through the 8 | [`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, 9 | making them non-encapsulated. They can define decorators and set hooks 10 | that will then be used in the rest of your application. 11 | 12 | Check out: 13 | 14 | - [The hitchhiker's guide to plugins](https://www.fastify.io/docs/latest/Guides/Plugins-Guide/) 15 | - [Fastify decorators](https://www.fastify.io/docs/latest/Reference/Decorators/). 16 | - [Fastify lifecycle](https://www.fastify.io/docs/latest/Reference/Lifecycle/). 17 | -------------------------------------------------------------------------------- /packages/search/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | # Build Node.js app 4 | # ------------------------------------ 5 | FROM node:22-alpine as build 6 | WORKDIR /app 7 | COPY ./package*.json ./ 8 | COPY ./packages/search ./packages/search 9 | RUN npm ci --cache /tmp/empty-cache 10 | RUN npm run build --workspace=search 11 | 12 | # Run Node.js app 13 | # ------------------------------------ 14 | FROM node:22-alpine 15 | ENV NODE_ENV=production 16 | 17 | WORKDIR /app 18 | COPY ./package*.json ./ 19 | COPY ./packages/search/package.json ./packages/search/ 20 | COPY ./packages/search/data/employee-info.csv ./packages/search/data/employee-info.csv 21 | RUN npm ci --omit=dev --workspace=search --cache /tmp/empty-cache 22 | COPY --from=build app/packages/search/dist packages/search/dist 23 | EXPOSE 3000 24 | CMD [ "npm", "start", "--workspace=search" ] 25 | -------------------------------------------------------------------------------- /packages/webapp/src/components/SupportingContent/SupportingContent.tsx: -------------------------------------------------------------------------------- 1 | import { parseSupportingContentItem } from './supporting-content-parser.js'; 2 | 3 | import styles from './SupportingContent.module.css'; 4 | 5 | interface Props { 6 | supportingContent: string[]; 7 | } 8 | 9 | export const SupportingContent = ({ supportingContent }: Props) => { 10 | return ( 11 |
    12 | {supportingContent.map((x, i) => { 13 | const parsed = parseSupportingContentItem(x); 14 | 15 | return ( 16 |
  • 17 |

    {parsed.title}

    18 |

    {parsed.content}

    19 |
  • 20 | ); 21 | })} 22 |
23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /infra/core/security/registry-access.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Assigns ACR Pull permissions to access an Azure Container Registry.' 2 | param containerRegistryName string 3 | param principalId string 4 | 5 | var acrPullRole = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '7f951dda-4ed3-4680-a7ca-43fe172d538d') 6 | 7 | resource aksAcrPull 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 8 | scope: containerRegistry // Use when specifying a scope that is different than the deployment scope 9 | name: guid(subscription().id, resourceGroup().id, principalId, acrPullRole) 10 | properties: { 11 | roleDefinitionId: acrPullRole 12 | principalType: 'ServicePrincipal' 13 | principalId: principalId 14 | } 15 | } 16 | 17 | resource containerRegistry 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' existing = { 18 | name: containerRegistryName 19 | } 20 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/link-icon.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | 4 | import { styles } from '../styles/link-icon.js'; 5 | import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; 6 | 7 | export interface LinkIcon { 8 | label: string; 9 | svgIcon: string; 10 | url: string; 11 | } 12 | 13 | @customElement('link-icon') 14 | export class LinkIconComponent extends LitElement { 15 | static override styles = [styles]; 16 | 17 | @property({ type: String }) 18 | label = ''; 19 | 20 | @property({ type: String }) 21 | svgIcon = ''; 22 | 23 | @property({ type: String }) 24 | url = ''; 25 | 26 | override render() { 27 | return html` 28 | 29 | ${unsafeSVG(this.svgIcon)} 30 | 31 | `; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/search/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with [Fastify-CLI](https://www.npmjs.com/package/fastify-cli) 2 | 3 | This project was bootstrapped with Fastify-CLI. 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm run dev` 10 | 11 | To start the app in dev mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | To target the deployed resources you need to create an `.env` file in the package. 15 | You can create and populate the .env file using 16 | 17 | ``` 18 | azd env get-values > .env 19 | ``` 20 | 21 | You will also need to authenticate to Azure using `az login` before calling the APIs. 22 | 23 | ### `npm start` 24 | 25 | For production mode 26 | 27 | ### `npm run test` 28 | 29 | Run the test cases. 30 | 31 | ## Learn More 32 | 33 | To learn Fastify, check out the [Fastify documentation](https://www.fastify.io/docs/latest/). 34 | -------------------------------------------------------------------------------- /packages/webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "watch": "tsc -b && vite build --watch", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@fluentui/react": "^8.110.7", 14 | "@fluentui/react-icons": "^2.0.206", 15 | "@react-spring/web": "^9.7.3", 16 | "chat-component": "^0.0.1", 17 | "dompurify": "^3.0.4", 18 | "react": "^18", 19 | "react-dom": "^18", 20 | "react-router-dom": "^6.29.0", 21 | "rimraf": "^6.0.1" 22 | }, 23 | "devDependencies": { 24 | "@types/dompurify": "^3.2.0", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "@vitejs/plugin-react": "^4.3.4", 28 | "prettier": "^3.0.0", 29 | "typescript": "^5.7.3", 30 | "vite": "^6.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/webapp/src/assets/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/lightbulb-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/webapp/src/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/roles.ps1: -------------------------------------------------------------------------------- 1 | $output = azd env get-values 2 | 3 | foreach ($line in $output) { 4 | $name, $value = $line.Split("=") 5 | $value = $value -replace '^\"|\"$' 6 | [Environment]::SetEnvironmentVariable($name, $value) 7 | } 8 | 9 | Write-Host "Environment variables set." 10 | 11 | $roles = @( 12 | "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd", 13 | "ba92f5b4-2d11-453d-a403-e96b0029c9fe", 14 | "8ebe5a00-799e-43f5-93ac-243d3dce84a7" 15 | ) 16 | 17 | if ([string]::IsNullOrEmpty($env:AZURE_RESOURCE_GROUP)) { 18 | $env:AZURE_RESOURCE_GROUP = "rg-$env:AZURE_ENV_NAME" 19 | azd env set AZURE_RESOURCE_GROUP $env:AZURE_RESOURCE_GROUP 20 | } 21 | 22 | foreach ($role in $roles) { 23 | az role assignment create ` 24 | --role $role ` 25 | --assignee-object-id $env:AZURE_PRINCIPAL_ID ` 26 | --scope /subscriptions/$env:AZURE_SUBSCRIPTION_ID/resourceGroups/$env:AZURE_RESOURCE_GROUP ` 27 | --assignee-principal-type User 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/stale-bot.yml: -------------------------------------------------------------------------------- 1 | name: Close stale issues and PRs 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: write 11 | issues: write 12 | pull-requests: write 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this issue will be closed.' 17 | stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed.' 18 | close-issue-message: 'This issue was closed because it has been stalled for 7 days with no activity.' 19 | close-pr-message: 'This PR was closed because it has been stalled for 7 days with no activity.' 20 | days-before-issue-stale: 60 21 | days-before-pr-stale: 60 22 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/chat-stage.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | import { styles } from '../styles/chat-stage.js'; 4 | import './link-icon.js'; 5 | export interface ChatStage { 6 | pagetitle: string; 7 | url: string; 8 | svgIcon: string; 9 | } 10 | 11 | @customElement('chat-stage') 12 | export class ChatStageComponent extends LitElement { 13 | static override styles = [styles]; 14 | 15 | @property({ type: String }) 16 | pagetitle = ''; 17 | 18 | @property({ type: String }) 19 | url = ''; 20 | 21 | @property({ type: String }) 22 | svgIcon = ''; 23 | 24 | override render() { 25 | return html` 26 |
27 | 28 |

${this.pagetitle}

29 |
30 | `; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/webapp/vite.config.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { defineConfig } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | // Expose environment variables to the client 6 | process.env.VITE_SEARCH_API_URI = process.env.BACKEND_URI ?? ''; 7 | console.log(`Using search API base URL: "${process.env.VITE_SEARCH_API_URI}"`); 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [react()], 12 | build: { 13 | outDir: './dist', 14 | emptyOutDir: true, 15 | sourcemap: true, 16 | rollupOptions: { 17 | output: { 18 | manualChunks: (id) => { 19 | if (id.includes('node_modules')) { 20 | return 'vendor'; 21 | } 22 | }, 23 | }, 24 | }, 25 | chunkSizeWarningLimit: 1024, 26 | }, 27 | server: { 28 | proxy: { '/ask': 'http://127.0.0.1:3000', '/chat': 'http://127.0.0.1:3000', '/content': 'http://127.0.0.1:3000' }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /scripts/roles.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | output=$(azd env get-values) 4 | 5 | while IFS= read -r line; do 6 | name=$(echo "$line" | cut -d '=' -f 1) 7 | value=$(echo "$line" | cut -d '=' -f 2 | sed 's/^\"//;s/\"$//') 8 | export "$name"="$value" 9 | done <<< "$output" 10 | 11 | echo "Environment variables set." 12 | 13 | roles=( 14 | "5e0bd9bd-7b93-4f28-af87-19fc36ad61bd" 15 | "ba92f5b4-2d11-453d-a403-e96b0029c9fe" 16 | "8ebe5a00-799e-43f5-93ac-243d3dce84a7" 17 | ) 18 | 19 | if [ -z "$AZURE_RESOURCE_GROUP" ]; then 20 | export AZURE_RESOURCE_GROUP="rg-$AZURE_ENV_NAME" 21 | azd env set AZURE_RESOURCE_GROUP "$AZURE_RESOURCE_GROUP" 22 | fi 23 | 24 | for role in "${roles[@]}"; do 25 | az role assignment create \ 26 | --role "$role" \ 27 | --assignee-object-id "$AZURE_PRINCIPAL_ID" \ 28 | --scope /subscriptions/"$AZURE_SUBSCRIPTION_ID"/resourceGroups/"$AZURE_RESOURCE_GROUP" \ 29 | --assignee-principal-type User 30 | done 31 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/citation-list.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | .subheadline--small { 5 | font-size: 12px; 6 | display: inline-block; 7 | } 8 | .items__list { 9 | border-top: none; 10 | padding: 0 var(--d-base); 11 | margin: var(--d-small) 0; 12 | display: block; 13 | } 14 | .items__listItem { 15 | display: inline-block; 16 | background-color: var(--c-accent-light); 17 | border-radius: var(--radius-small); 18 | text-decoration: none; 19 | padding: var(--d-xsmall); 20 | margin-top: 5px; 21 | font-size: var(--font-small); 22 | } 23 | .items__listItem.active { 24 | background-color: var(--c-accent-high); 25 | } 26 | .items__listItem:not(first-child) { 27 | margin-left: 5px; 28 | } 29 | .items__link { 30 | text-decoration: none; 31 | color: var(--text-color); 32 | } 33 | .items__listItem.active .items__link { 34 | color: var(--c-white); 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/history-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/webapp/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { createHashRouter, RouterProvider } from 'react-router-dom'; 4 | import { initializeIcons } from '@fluentui/react'; 5 | 6 | import './index.css'; 7 | 8 | import Layout from './pages/layout/Layout.jsx'; 9 | import Chat from './pages/chat/Chat.jsx'; 10 | 11 | initializeIcons(); 12 | 13 | const router = createHashRouter([ 14 | { 15 | path: '/', 16 | element: , 17 | children: [ 18 | { 19 | index: true, 20 | element: , 21 | }, 22 | { 23 | path: 'qa', 24 | lazy: () => import('./pages/oneshot/OneShot.jsx'), 25 | }, 26 | { 27 | path: '*', 28 | lazy: () => import('./pages/NoPage.jsx'), 29 | }, 30 | ], 31 | }, 32 | ]); 33 | 34 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 35 | 36 | 37 | , 38 | ); 39 | -------------------------------------------------------------------------------- /packages/search/src/lib/util/string.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Replace newline and carriage return characters with spaces. 3 | * @param {string} s The input string. 4 | * @returns {string} The input string with newline and carriage return characters replaced with spaces. 5 | * @example 6 | * removeNewlines('Hello\nworld\r\n!'); 7 | * // output: 'Hello world !' 8 | */ 9 | export function removeNewlines(s: string = ''): string { 10 | return s.replaceAll(/[\n\r]+/g, ' '); 11 | } 12 | 13 | /** 14 | * Parse a boolean value from a string. 15 | * @param {string | boolean | undefined} value The value to parse. 16 | * @returns {boolean} The parsed boolean value. 17 | * @example 18 | * parseBoolean('true'); 19 | * // output: true 20 | * parseBoolean('false'); 21 | * // output: false 22 | */ 23 | export function parseBoolean(value: string | boolean | undefined): boolean { 24 | if (typeof value === 'boolean') { 25 | return value; 26 | } 27 | if (value === undefined) { 28 | return false; 29 | } 30 | return value === 'true'; 31 | } 32 | -------------------------------------------------------------------------------- /packages/chat-component/src/core/stream/index.ts: -------------------------------------------------------------------------------- 1 | import { NdJsonParserStream } from './data-format/ndjson.js'; 2 | import { globalConfig } from '../../config/global-config.js'; 3 | 4 | export function createReader(responseBody: ReadableStream | null) { 5 | return responseBody?.pipeThrough(new TextDecoderStream()).pipeThrough(new NdJsonParserStream()).getReader(); 6 | } 7 | 8 | export async function* readStream(reader: any): AsyncGenerator { 9 | if (!reader) { 10 | throw new Error('No response body or body is not readable'); 11 | } 12 | 13 | let value: JSON | undefined; 14 | let done: boolean; 15 | while ((({ value, done } = await reader.read()), !done)) { 16 | yield new Promise((resolve) => { 17 | setTimeout(() => { 18 | resolve(value as T); 19 | }, globalConfig.BOT_TYPING_EFFECT_INTERVAL); 20 | }); 21 | } 22 | } 23 | 24 | // Stop stream 25 | export function cancelStream(stream: ReadableStream | null): void { 26 | if (stream) { 27 | stream.cancel(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | > ## Please provide us with the following information: 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | 9 | ``` 10 | - [ ] bug report -> please search issues before submitting 11 | - [ ] feature request 12 | - [ ] documentation issue or request 13 | - [ ] regression (a behavior that used to work and stopped in a new release) 14 | ``` 15 | 16 | ### Minimal steps to reproduce 17 | 18 | > 19 | 20 | ### Any log messages given by the failure 21 | 22 | > 23 | 24 | ### Expected/desired behavior 25 | 26 | > 27 | 28 | ### OS and Version? 29 | 30 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 31 | 32 | ### azd version? 33 | 34 | > run `azd version` and copy paste here. 35 | 36 | ### Versions 37 | 38 | > 39 | 40 | ### Mention any other details that might be useful 41 | 42 | > --- 43 | > 44 | > Thanks! We'll be in touch soon. 45 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/mic-record-on-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/chat-component/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "module": "es2020", 5 | "lib": ["esnext", "DOM", "DOM.Iterable", "es6"], 6 | "types": ["reflect-metadata"], 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true, 10 | "inlineSources": true, 11 | "outDir": "./dist", 12 | "rootDir": "./src", 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": false, 19 | "noImplicitThis": true, 20 | "moduleResolution": "node", 21 | "allowSyntheticDefaultImports": true, 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | "forceConsistentCasingInFileNames": true, 25 | "noImplicitOverride": true, 26 | "emitDeclarationOnly": true, 27 | "plugins": [ 28 | { 29 | "name": "ts-lit-plugin", 30 | "strict": true 31 | } 32 | ] 33 | }, 34 | "include": ["**/*.ts"], 35 | "exclude": [] 36 | } 37 | -------------------------------------------------------------------------------- /packages/webapp/src/api/models.ts: -------------------------------------------------------------------------------- 1 | export const enum Approaches { 2 | RetrieveThenRead = 'rtr', 3 | ReadRetrieveRead = 'rrr', 4 | ReadDecomposeAsk = 'rda', 5 | } 6 | 7 | export const enum RetrievalMode { 8 | Hybrid = 'hybrid', 9 | Vectors = 'vectors', 10 | Text = 'text', 11 | } 12 | 13 | export const enum CustomStyles { 14 | AccentHigh = 'AccentHigh', 15 | AccentLight = 'AccentLighter', 16 | AccentDark = 'AccentContrast', 17 | TextColor = 'TextColor', 18 | BackgroundColor = 'BackgroundColor', 19 | FormBackgroundColor = 'FormBackgroundColor', 20 | ForegroundColor = 'ForegroundColor', 21 | BorderRadius = 'BorderRadius', 22 | BorderWidth = 'BorderWidth', 23 | FontBaseSize = 'FontBaseSize', 24 | } 25 | 26 | export type RequestOverrides = { 27 | retrieval_mode?: RetrievalMode; 28 | semantic_ranker?: boolean; 29 | semantic_captions?: boolean; 30 | exclude_category?: string; 31 | top?: number; 32 | temperature?: number; 33 | prompt_template?: string; 34 | prompt_template_prefix?: string; 35 | prompt_template_suffix?: string; 36 | suggest_followup_questions?: boolean; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/indexer/src/plugins/openapi.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { join, dirname } from 'node:path'; 4 | import fp from 'fastify-plugin'; 5 | import swagger from '@fastify/swagger'; 6 | import swaggerUi from '@fastify/swagger-ui'; 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | 10 | export default fp(async (fastify) => { 11 | const file = await fs.readFile(join(__dirname, '../../package.json'), 'utf8'); 12 | const packageJson = JSON.parse(file) as Record; 13 | 14 | fastify.register(swagger, { 15 | openapi: { 16 | info: { 17 | title: packageJson.name, 18 | description: packageJson.description, 19 | version: packageJson.version, 20 | }, 21 | }, 22 | hideUntagged: true, 23 | refResolver: { 24 | buildLocalReference(json) { 25 | // Keep the same name as the JSON schema 26 | return String(json.$id); 27 | }, 28 | }, 29 | }); 30 | 31 | fastify.register(swaggerUi, { 32 | routePrefix: '/openapi', 33 | staticCSP: true, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/search/src/plugins/openapi.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { join, dirname } from 'node:path'; 4 | import fp from 'fastify-plugin'; 5 | import swagger from '@fastify/swagger'; 6 | import swaggerUi from '@fastify/swagger-ui'; 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | 10 | export default fp(async (fastify) => { 11 | const file = await fs.readFile(join(__dirname, '../../package.json'), 'utf8'); 12 | const packageJson = JSON.parse(file) as Record; 13 | 14 | fastify.register(swagger, { 15 | openapi: { 16 | info: { 17 | title: packageJson.name, 18 | description: packageJson.description, 19 | version: packageJson.version, 20 | }, 21 | }, 22 | hideUntagged: true, 23 | refResolver: { 24 | buildLocalReference(json) { 25 | // Keep the same name as the JSON schema 26 | return String(json.$id); 27 | }, 28 | }, 29 | }); 30 | 31 | fastify.register(swaggerUi, { 32 | routePrefix: '/openapi', 33 | staticCSP: true, 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /infra/core/monitor/applicationinsights.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' 2 | param name string 3 | param dashboardName string = '' 4 | param location string = resourceGroup().location 5 | param tags object = {} 6 | param logAnalyticsWorkspaceId string 7 | 8 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { 9 | name: name 10 | location: location 11 | tags: tags 12 | kind: 'web' 13 | properties: { 14 | Application_Type: 'web' 15 | WorkspaceResourceId: logAnalyticsWorkspaceId 16 | } 17 | } 18 | 19 | module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { 20 | name: 'application-insights-dashboard' 21 | params: { 22 | name: dashboardName 23 | location: location 24 | applicationInsightsName: applicationInsights.name 25 | } 26 | } 27 | 28 | output connectionString string = applicationInsights.properties.ConnectionString 29 | output instrumentationKey string = applicationInsights.properties.InstrumentationKey 30 | output name string = applicationInsights.name 31 | -------------------------------------------------------------------------------- /.github/workflows/azure-dev-validation.yaml: -------------------------------------------------------------------------------- 1 | name: Validate AZD template 2 | on: 3 | push: 4 | branches: [main, develop] 5 | paths: 6 | - 'infra/**' 7 | pull_request: 8 | branches: [main, develop] 9 | paths: 10 | - 'infra/**' 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | 21 | - name: Build Bicep for linting 22 | uses: azure/CLI@v2 23 | with: 24 | inlineScript: az config set bicep.use_binary_from_path=false && az bicep build -f infra/main.bicep --stdout 25 | 26 | - name: Run Microsoft Security DevOps Analysis 27 | uses: microsoft/security-devops-action@preview 28 | id: msdo 29 | continue-on-error: true 30 | with: 31 | tools: templateanalyzer 32 | 33 | - name: Upload alerts to Security tab 34 | if: github.repository_owner == 'Azure-Samples' 35 | uses: github/codeql-action/upload-sarif@v3 36 | with: 37 | sarif_file: ${{ steps.msdo.outputs.sarifFile }} 38 | -------------------------------------------------------------------------------- /packages/search/test/lib/langchain/csv-lookup-tool.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import fs from 'node:fs/promises'; 3 | import { CsvLookupTool } from '../../../src/lib/langchain/csv-lookup-tool.js'; 4 | 5 | test('CsvLookupTool', async (t) => { 6 | const filename = 'test.csv'; 7 | const keyField = 'id'; 8 | const csv = `id,name,age 9 | 1,John Doe,30 10 | 2,Jane Doe,25 11 | 3,Bob Smith,40`; 12 | 13 | // Mock readFile function 14 | t.mock.method(fs, 'readFile', async (_filename: string) => csv); 15 | 16 | const tool = new CsvLookupTool(filename, keyField); 17 | 18 | await t.test('_call()', async (t) => { 19 | await t.test('should return the correct data', async (t) => { 20 | const input = '1'; 21 | const expected = 'id:1\nname:John Doe\nage:30'; 22 | const actual = await tool._call(input); 23 | t.assert.equal(actual, expected); 24 | }); 25 | 26 | await t.test('should return an empty string if no data is found', async (t) => { 27 | const input = '4'; 28 | const expected = ''; 29 | const actual = await tool._call(input); 30 | t.assert.equal(actual, expected); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/chat-action-button.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, html } from 'lit'; 2 | import { customElement, property } from 'lit/decorators.js'; 3 | 4 | import { styles } from '../styles/chat-action-button.js'; 5 | import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; 6 | 7 | export interface ChatActionButton { 8 | label: string; 9 | svgIcon: string; 10 | isDisabled: boolean; 11 | id: string; 12 | } 13 | 14 | @customElement('chat-action-button') 15 | export class ChatActionButtonComponent extends LitElement { 16 | static override styles = [styles]; 17 | 18 | @property({ type: String }) 19 | label = ''; 20 | 21 | @property({ type: String }) 22 | svgIcon = ''; 23 | 24 | @property({ type: Boolean }) 25 | isDisabled = false; 26 | 27 | @property({ type: String }) 28 | actionId = ''; 29 | 30 | @property({ type: String }) 31 | tooltip: string | undefined = undefined; 32 | 33 | override render() { 34 | return html` 35 | 39 | `; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | 4 | 5 | - ... 6 | 7 | ## Does this introduce a breaking change? 8 | 9 | 10 | 11 | ``` 12 | [ ] Yes 13 | [ ] No 14 | ``` 15 | 16 | ## Pull Request Type 17 | 18 | What kind of change does this Pull Request introduce? 19 | 20 | 21 | 22 | ``` 23 | [ ] Bugfix 24 | [ ] Feature 25 | [ ] Code style update (formatting, local variables) 26 | [ ] Refactoring (no functional changes, no api changes) 27 | [ ] Documentation content changes 28 | [ ] Other... Please describe: 29 | ``` 30 | 31 | ## How to Test 32 | 33 | - Get the code 34 | 35 | ``` 36 | git clone [repo-address] 37 | cd [repo-name] 38 | git checkout [branch-name] 39 | npm install 40 | ``` 41 | 42 | - Test the code 43 | 44 | 45 | ``` 46 | 47 | ``` 48 | 49 | ## What to Check 50 | 51 | Verify that the following are valid 52 | 53 | - ... 54 | 55 | ## Other Information 56 | 57 | 58 | -------------------------------------------------------------------------------- /packages/chat-component/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-component", 3 | "version": "0.0.1", 4 | "description": "A minimal OpenAI chat web component to hook as a client to any backend implementation", 5 | "main": "./dist/chat-component.umd.cjs", 6 | "module": "./dist/chat-component.js", 7 | "exports": { 8 | ".": { 9 | "import": "./dist/chat-component.js", 10 | "require": "./dist/chat-component.udm.cjs" 11 | } 12 | }, 13 | "type": "module", 14 | "scripts": { 15 | "clean": "rimraf dist *.tgz", 16 | "start": "vite --port 8000 --host 0.0.0.0", 17 | "build": "vite build", 18 | "watch": "vite build --watch --minify false", 19 | "preview": "vite preview", 20 | "prepare": "npm run build -s" 21 | }, 22 | "author": "", 23 | "license": "MIT", 24 | "dependencies": { 25 | "dompurify": "^3.0.6", 26 | "inversify": "^6.0.2", 27 | "inversify-inject-decorators": "^3.1.0", 28 | "lit": "^2.8.0", 29 | "marked": "^9.1.5", 30 | "reflect-metadata": "^0.2.1" 31 | }, 32 | "devDependencies": { 33 | "@types/dom-speech-recognition": "^0.0.4", 34 | "@types/dompurify": "^3.0.3", 35 | "rimraf": "^6.0.1", 36 | "typescript": "^5.7.3", 37 | "vite": "^6.1.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/link-icon.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | a svg { 5 | width: calc(var(--width-base) - var(--d-small)); 6 | height: calc(var(--width-base) - var(--d-small)); 7 | position: relative; 8 | z-index: 1; 9 | } 10 | a { 11 | flex-shrink: 0; 12 | border-radius: calc(var(--radius-large) * 3); 13 | border: var(--border-thicker) solid transparent; 14 | background-origin: border-box; 15 | background-clip: content-box, border-box; 16 | background-size: cover; 17 | background-image: linear-gradient(to right, var(--c-accent-light), var(--c-accent-high)); 18 | width: calc(var(--d-xlarge) * 2); 19 | height: calc(var(--d-xlarge) * 2); 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | margin-right: var(--d-large); 24 | overflow: hidden; 25 | padding: var(--d-small); 26 | position: relative; 27 | } 28 | a::after { 29 | content: ''; 30 | border-radius: calc(var(--radius-large) * 3); 31 | width: calc(var(--width-base) - var(--d-small)); 32 | height: calc(var(--width-base) - var(--d-small)); 33 | position: absolute; 34 | background-color: var(--c-secondary); 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /packages/indexer/src/lib/formats/pdf.ts: -------------------------------------------------------------------------------- 1 | import * as pdfjs from 'pdfjs-dist/legacy/build/pdf.mjs'; 2 | import { type TextItem } from 'pdfjs-dist/types/src/display/api.js'; 3 | import { type ContentPage } from '../document.js'; 4 | 5 | export async function extractTextFromPdf(data: Buffer): Promise { 6 | const pages: ContentPage[] = []; 7 | const pdf = await pdfjs.getDocument(new Uint8Array(data)).promise; 8 | let offset = 0; 9 | 10 | for (let i = 1; i <= pdf.numPages; i++) { 11 | const page = await pdf.getPage(i); 12 | const textContent = await page.getTextContent(); 13 | let previousY = 0; 14 | const text = textContent.items 15 | .filter((item) => 'str' in item) 16 | .map((item) => { 17 | const textItem = item as TextItem; 18 | const y = textItem.transform[5]; 19 | let textContent = textItem.str; 20 | if (y !== previousY && previousY !== 0) { 21 | // If the Y coordinate changes, we're on a new line 22 | textContent = '\n' + textContent; 23 | } 24 | previousY = y; 25 | return textContent; 26 | }) 27 | .join(''); 28 | 29 | pages.push({ content: text + '\n', offset, page: i }); 30 | offset += text.length; 31 | } 32 | return pages; 33 | } 34 | -------------------------------------------------------------------------------- /infra/core/monitor/monitoring.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' 2 | param logAnalyticsName string 3 | param applicationInsightsName string 4 | param applicationInsightsDashboardName string = '' 5 | param location string = resourceGroup().location 6 | param tags object = {} 7 | 8 | module logAnalytics 'loganalytics.bicep' = { 9 | name: 'loganalytics' 10 | params: { 11 | name: logAnalyticsName 12 | location: location 13 | tags: tags 14 | } 15 | } 16 | 17 | module applicationInsights 'applicationinsights.bicep' = { 18 | name: 'applicationinsights' 19 | params: { 20 | name: applicationInsightsName 21 | location: location 22 | tags: tags 23 | dashboardName: applicationInsightsDashboardName 24 | logAnalyticsWorkspaceId: logAnalytics.outputs.id 25 | } 26 | } 27 | 28 | output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString 29 | output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey 30 | output applicationInsightsName string = applicationInsights.outputs.name 31 | output logAnalyticsWorkspaceId string = logAnalytics.outputs.id 32 | output logAnalyticsWorkspaceName string = logAnalytics.outputs.name 33 | -------------------------------------------------------------------------------- /packages/indexer/src/routes/README.md: -------------------------------------------------------------------------------- 1 | # Routes Folder 2 | 3 | Routes define endpoints within your application. Fastify provides an 4 | easy path to a microservice architecture, in the future you might want 5 | to independently deploy some of those. 6 | 7 | In this folder you should define all the routes that define the endpoints 8 | of your web application. 9 | Each service is a [Fastify 10 | plugin](https://www.fastify.io/docs/latest/Reference/Plugins/), it is 11 | encapsulated (it can have its own independent plugins) and it is 12 | typically stored in a file; be careful to group your routes logically, 13 | e.g. all `/users` routes in a `users.js` file. We have added 14 | a `root.js` file for you with a '/' root added. 15 | 16 | If a single file become too large, create a folder and add a `index.js` file there: 17 | this file must be a Fastify plugin, and it will be loaded automatically 18 | by the application. You can now add as many files as you want inside that folder. 19 | In this way you can create complex routes within a single monolith, 20 | and eventually extract them. 21 | 22 | If you need to share functionality between routes, place that 23 | functionality into the `plugins` folder, and share it via 24 | [decorators](https://www.fastify.io/docs/latest/Reference/Decorators/). 25 | -------------------------------------------------------------------------------- /packages/search/src/routes/README.md: -------------------------------------------------------------------------------- 1 | # Routes Folder 2 | 3 | Routes define endpoints within your application. Fastify provides an 4 | easy path to a microservice architecture, in the future you might want 5 | to independently deploy some of those. 6 | 7 | In this folder you should define all the routes that define the endpoints 8 | of your web application. 9 | Each service is a [Fastify 10 | plugin](https://www.fastify.io/docs/latest/Reference/Plugins/), it is 11 | encapsulated (it can have its own independent plugins) and it is 12 | typically stored in a file; be careful to group your routes logically, 13 | e.g. all `/users` routes in a `users.js` file. We have added 14 | a `root.js` file for you with a '/' root added. 15 | 16 | If a single file become too large, create a folder and add a `index.js` file there: 17 | this file must be a Fastify plugin, and it will be loaded automatically 18 | by the application. You can now add as many files as you want inside that folder. 19 | In this way you can create complex routes within a single monolith, 20 | and eventually extract them. 21 | 22 | If you need to share functionality between routes, place that 23 | functionality into the `plugins` folder, and share it via 24 | [decorators](https://www.fastify.io/docs/latest/Reference/Decorators/). 25 | -------------------------------------------------------------------------------- /packages/search/src/lib/message-builder.ts: -------------------------------------------------------------------------------- 1 | import { getTokenCountFromMessages } from './tokens.js'; 2 | import { type Message, type MessageRole } from './message.js'; 3 | 4 | export class MessageBuilder { 5 | messages: Message[]; 6 | model: string; 7 | tokens: number; 8 | 9 | /** 10 | * A class for building and managing messages in a chat conversation. 11 | * @param {string} systemContent The initial system message content. 12 | * @param {string} chatgptModel The name of the ChatGPT model. 13 | */ 14 | constructor(systemContent: string, chatgptModel: string) { 15 | this.messages = [{ role: 'system', content: systemContent }]; 16 | this.model = chatgptModel; 17 | this.tokens = getTokenCountFromMessages(this.messages[this.messages.length - 1], this.model); 18 | } 19 | 20 | /** 21 | * Append a new message to the conversation. 22 | * @param {MessageRole} role The role of the message sender. 23 | * @param {string} content The content of the message. 24 | * @param {number} index The index at which to insert the message. 25 | */ 26 | appendMessage(role: MessageRole, content: string, index = 1) { 27 | this.messages.splice(index, 0, { role, content }); 28 | this.tokens += getTokenCountFromMessages(this.messages[index], this.model); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/webapp/src/components/ThemeSwitch/ThemeSwitch.tsx: -------------------------------------------------------------------------------- 1 | // ThemeSwitch.tsx 2 | import React, { useEffect } from 'react'; 3 | import { Toggle } from '@fluentui/react'; 4 | import './ThemeSwitch.css'; 5 | 6 | interface ThemeSwitchProps { 7 | onToggle: (isDarkTheme: boolean) => void; 8 | isDarkTheme: boolean; 9 | isConfigPanelOpen: boolean; 10 | } 11 | 12 | export const ThemeSwitch: React.FC = ({ onToggle, isDarkTheme, isConfigPanelOpen }) => { 13 | const handleToggleChange = () => { 14 | onToggle(!isDarkTheme); // Pass the new theme state to the parent component 15 | }; 16 | 17 | useEffect(() => { 18 | // Toggle 'dark' class on the shell app body element based on the isDarkTheme prop and isConfigPanelOpen 19 | document.body.classList.toggle('dark', isDarkTheme); 20 | document.documentElement.dataset.theme = isDarkTheme && isConfigPanelOpen ? 'dark' : ''; 21 | localStorage.removeItem('ms-azoaicc:isDarkTheme'); 22 | }, [isDarkTheme, isConfigPanelOpen]); 23 | 24 | return ( 25 |
26 | 33 |
34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/search/src/app.ts: -------------------------------------------------------------------------------- 1 | import path, { join } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { type FastifyPluginAsync } from 'fastify'; 4 | import AutoLoad, { type AutoloadPluginOptions } from '@fastify/autoload'; 5 | 6 | export type AppOptions = { 7 | // Place your custom options for app below here. 8 | } & Partial; 9 | 10 | // Pass --options via CLI arguments in command to enable these options. 11 | const options: AppOptions = {}; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | 16 | const app: FastifyPluginAsync = async (fastify, options_): Promise => { 17 | // Place here your custom code! 18 | 19 | // Do not touch the following lines 20 | 21 | // This loads all plugins defined in plugins 22 | // those should be support plugins that are reused 23 | // through your application 24 | fastify.register(AutoLoad, { 25 | dir: join(__dirname, 'plugins'), 26 | options: options_, 27 | }); 28 | 29 | // This loads all plugins defined in routes 30 | // define your routes in one of these 31 | fastify.register(AutoLoad, { 32 | dir: join(__dirname, 'routes'), 33 | options: options_, 34 | }); 35 | }; 36 | 37 | export default app; 38 | export { app, options }; 39 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/oneshot/OneShot.module.css: -------------------------------------------------------------------------------- 1 | .oneshotContainer { 2 | display: flex; 3 | flex: 1; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .oneshotTopSection { 9 | display: flex; 10 | flex-direction: column; 11 | align-items: center; 12 | width: 100%; 13 | } 14 | 15 | .oneshotBottomSection { 16 | display: flex; 17 | flex: 1; 18 | flex-wrap: wrap; 19 | justify-content: center; 20 | align-content: flex-start; 21 | width: 100%; 22 | margin-top: 20px; 23 | } 24 | 25 | .oneshotTitle { 26 | font-size: 4rem; 27 | font-weight: 600; 28 | margin-top: 130px; 29 | } 30 | 31 | @media only screen and (max-width: 800px) { 32 | .oneshotTitle { 33 | font-size: 3rem; 34 | font-weight: 600; 35 | margin-top: 0; 36 | } 37 | } 38 | 39 | .oneshotQuestionInput { 40 | max-width: 800px; 41 | width: 100%; 42 | padding-left: 10px; 43 | padding-right: 10px; 44 | } 45 | 46 | .oneshotAnswerContainer { 47 | max-width: 800px; 48 | width: 100%; 49 | padding-left: 10px; 50 | padding-right: 10px; 51 | } 52 | 53 | .oneshotAnalysisPanel { 54 | width: 600px; 55 | margin-left: 20px; 56 | } 57 | 58 | .oneshotSettingsSeparator { 59 | margin-top: 15px; 60 | } 61 | 62 | .settingsButton { 63 | align-self: flex-end; 64 | margin-right: 20px; 65 | margin-top: 20px; 66 | } 67 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "search: dev", 9 | "type": "node", 10 | "request": "launch", 11 | "cwd": "${workspaceFolder}/packages/search", 12 | "runtimeExecutable": "npm", 13 | "runtimeArgs": ["run-script", "dev"], 14 | "console": "integratedTerminal" 15 | }, 16 | { 17 | "name": "indexer: dev", 18 | "type": "node", 19 | "request": "launch", 20 | "cwd": "${workspaceFolder}/packages/indexer", 21 | "runtimeExecutable": "npm", 22 | "runtimeArgs": ["run-script", "dev"], 23 | "console": "integratedTerminal" 24 | }, 25 | { 26 | "name": "webapp: dev", 27 | "type": "node", 28 | "request": "launch", 29 | "cwd": "${workspaceFolder}/packages/webapp", 30 | "runtimeExecutable": "npm", 31 | "runtimeArgs": ["run-script", "dev"], 32 | "console": "integratedTerminal" 33 | } 34 | ], 35 | "inputs": [ 36 | { 37 | "id": "dotEnvFilePath", 38 | "type": "command", 39 | "command": "azure-dev.commands.getDotEnvFilePath" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /packages/indexer/src/app.ts: -------------------------------------------------------------------------------- 1 | import path, { join } from 'node:path'; 2 | import { fileURLToPath } from 'node:url'; 3 | import { type FastifyPluginAsync } from 'fastify'; 4 | import AutoLoad, { type AutoloadPluginOptions } from '@fastify/autoload'; 5 | import cors from '@fastify/cors'; 6 | 7 | export type AppOptions = { 8 | // Place your custom options for app below here. 9 | } & Partial; 10 | 11 | // Pass --options via CLI arguments in command to enable these options. 12 | const options: AppOptions = {}; 13 | 14 | const __filename = fileURLToPath(import.meta.url); 15 | const __dirname = path.dirname(__filename); 16 | 17 | const app: FastifyPluginAsync = async (fastify, options_): Promise => { 18 | // Place here your custom code! 19 | 20 | fastify.register(cors, {}); 21 | 22 | // Do not touch the following lines 23 | 24 | // This loads all plugins defined in plugins 25 | // those should be support plugins that are reused 26 | // through your application 27 | fastify.register(AutoLoad, { 28 | dir: join(__dirname, 'plugins'), 29 | options: options_, 30 | }); 31 | 32 | // This loads all plugins defined in routes 33 | // define your routes in one of these 34 | fastify.register(AutoLoad, { 35 | dir: join(__dirname, 'routes'), 36 | options: options_, 37 | }); 38 | }; 39 | 40 | export default app; 41 | export { app, options }; 42 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - develop 7 | pull_request: 8 | branches: 9 | - main 10 | - develop 11 | 12 | jobs: 13 | test: 14 | strategy: 15 | matrix: 16 | platform: [ubuntu-latest, macos-latest, windows-latest] 17 | node-version: ['22'] 18 | 19 | name: ${{ matrix.platform }} / Node.js v${{ matrix.node-version }} 20 | runs-on: ${{ matrix.platform }} 21 | steps: 22 | # - run: git config --global core.autocrlf false # Preserve line endings 23 | - uses: actions/checkout@v4 24 | - name: Setup Node.js v${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - name: Install dependencies 29 | run: npm ci 30 | - name: Build packages 31 | run: npm run build 32 | - if: matrix.platform == 'ubuntu-latest' 33 | name: Build Docker images 34 | run: npm run docker:build 35 | - name: Lint packages 36 | run: npm run lint 37 | - name: Test packages 38 | run: npm test 39 | 40 | test_all: 41 | if: always() 42 | runs-on: ubuntu-latest 43 | needs: test 44 | steps: 45 | - name: Check build matrix status 46 | if: ${{ needs.test.result != 'success' }} 47 | run: exit 1 48 | -------------------------------------------------------------------------------- /azure.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json 2 | 3 | name: azure-search-openai-javascript 4 | metadata: 5 | template: azure-search-openai-javascript@1.0.0 6 | requiredVersions: 7 | azd: '>= 1.10.0' 8 | 9 | services: 10 | webapp: 11 | project: ./packages/webapp 12 | dist: dist 13 | language: ts 14 | host: staticwebapp 15 | hooks: 16 | predeploy: 17 | windows: 18 | shell: pwsh 19 | run: npm run build 20 | interactive: true 21 | continueOnError: false 22 | posix: 23 | shell: sh 24 | run: export SEARCH_API_URI && npm run build 25 | interactive: true 26 | continueOnError: false 27 | 28 | search: 29 | project: ./packages/search 30 | language: ts 31 | host: containerapp 32 | docker: 33 | context: ../.. 34 | remoteBuild: true 35 | 36 | indexer: 37 | project: ./packages/indexer 38 | language: ts 39 | host: containerapp 40 | docker: 41 | context: ../.. 42 | remoteBuild: true 43 | 44 | hooks: 45 | postup: 46 | windows: 47 | shell: pwsh 48 | run: ./scripts/index-data.ps1 49 | interactive: true 50 | continueOnError: false 51 | posix: 52 | shell: sh 53 | run: ./scripts/index-data.sh 54 | interactive: true 55 | continueOnError: false 56 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/debug-chat-entry.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { 3 | container, 4 | type ChatEntryActionController, 5 | ControllerType, 6 | ComposableReactiveControllerBase, 7 | } from './composable.js'; 8 | import { html } from 'lit'; 9 | import { globalConfig } from '../config/global-config.js'; 10 | import iconLightBulb from '../../public/svg/lightbulb-icon.svg?raw'; 11 | 12 | @injectable() 13 | export class DebugChatEntryActionController 14 | extends ComposableReactiveControllerBase 15 | implements ChatEntryActionController 16 | { 17 | handleClick(event: Event, entry: ChatThreadEntry) { 18 | event.preventDefault(); 19 | this.context.setState('showThoughtProcess', true); 20 | this.context.selectedChatEntry = entry; 21 | } 22 | 23 | render(entry: ChatThreadEntry, isDisabled: boolean) { 24 | const isShowingThoughtProcess = this.context.getState('showThoughtProcess'); 25 | return html` 26 | 33 | `; 34 | } 35 | } 36 | 37 | container.bind(ControllerType.ChatEntryAction).to(DebugChatEntryActionController); 38 | -------------------------------------------------------------------------------- /packages/webapp/src/pages/layout/Layout.module.css: -------------------------------------------------------------------------------- 1 | .layout { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | } 6 | 7 | .header { 8 | background-color: #222222; 9 | color: #f2f2f2; 10 | } 11 | 12 | .headerContainer { 13 | display: flex; 14 | align-items: center; 15 | justify-content: space-around; 16 | margin-right: 12px; 17 | margin-left: 12px; 18 | } 19 | 20 | .headerTitleContainer { 21 | display: flex; 22 | align-items: center; 23 | margin-right: 40px; 24 | color: #f2f2f2; 25 | text-decoration: none; 26 | } 27 | 28 | .headerLogo { 29 | height: 40px; 30 | } 31 | 32 | .headerTitle { 33 | margin-left: 12px; 34 | font-weight: 600; 35 | } 36 | 37 | .headerNavList { 38 | display: flex; 39 | list-style: none; 40 | padding-left: 0; 41 | } 42 | 43 | .headerNavPageLink { 44 | color: #f2f2f2; 45 | text-decoration: none; 46 | opacity: 0.75; 47 | 48 | transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1); 49 | transition-duration: 500ms; 50 | transition-property: opacity; 51 | } 52 | 53 | .headerNavPageLink:hover { 54 | opacity: 1; 55 | } 56 | 57 | .headerNavPageLinkActive { 58 | color: #f2f2f2; 59 | text-decoration: none; 60 | } 61 | 62 | .headerNavLeftMargin { 63 | margin-left: 20px; 64 | } 65 | 66 | .headerRightText { 67 | font-weight: normal; 68 | margin-left: 40px; 69 | } 70 | 71 | .microsoftLogo { 72 | height: 23px; 73 | font-weight: 600; 74 | } 75 | 76 | .githubLogo { 77 | height: 20px; 78 | } 79 | -------------------------------------------------------------------------------- /packages/search/test/lib/message-builder.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { MessageBuilder } from '../../src/lib/message-builder.js'; 3 | 4 | test('MessageBuilder', async (t) => { 5 | const systemContent = 'Welcome to the chat!'; 6 | const chatgptModel = 'gpt-4o-mini'; 7 | const messageBuilder = new MessageBuilder(systemContent, chatgptModel); 8 | 9 | await t.test('constructor', (t) => { 10 | t.assert.equal(messageBuilder.messages.length, 1, 'should have one message'); 11 | t.assert.equal(messageBuilder.messages[0].role, 'system', 'should have a system message'); 12 | t.assert.equal(messageBuilder.messages[0].content, systemContent, 'should have the correct system message content'); 13 | t.assert.equal(messageBuilder.model, chatgptModel, 'should have the correct ChatGPT model'); 14 | t.assert.equal(messageBuilder.tokens, 8, 'should have correct number of tokens'); 15 | }); 16 | 17 | await t.test('appendMessage', (t) => { 18 | const role = 'user'; 19 | const content = 'Hello, how are you?'; 20 | messageBuilder.appendMessage(role, content); 21 | 22 | t.assert.equal(messageBuilder.messages.length, 2, 'should have two messages'); 23 | t.assert.equal(messageBuilder.messages[1].role, role, 'should have the correct message role'); 24 | t.assert.equal(messageBuilder.messages[1].content, content, 'should have the correct message content'); 25 | t.assert.equal(messageBuilder.tokens, 17, 'should have correct number of tokens'); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/tab-component.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | .tab-component__list { 5 | list-style-type: none; 6 | display: flex; 7 | box-shadow: var(--shadow); 8 | border-radius: var(--radius-base); 9 | padding: var(--d-xsmall); 10 | width: 450px; 11 | margin: 0 auto; 12 | justify-content: space-evenly; 13 | } 14 | .tab-component__listItem { 15 | width: 33%; 16 | text-align: center; 17 | } 18 | .tab-component__link.active { 19 | background: linear-gradient(to left, var(--c-accent-light), var(--c-accent-high)); 20 | color: var(--c-white); 21 | } 22 | .tab-component__link:not(.active):hover { 23 | background: var(--c-light-gray); 24 | cursor: pointer; 25 | } 26 | .tab-component__link { 27 | border-bottom: 4px solid transparent; 28 | border-radius: var(--radius-small); 29 | text-decoration: none; 30 | color: var(--text-color); 31 | font-weight: bold; 32 | font-size: var(--font-small); 33 | cursor: pointer; 34 | display: block; 35 | padding: var(--d-small); 36 | } 37 | .tab-component__content { 38 | position: relative; 39 | } 40 | .tab-component__tab { 41 | position: absolute; 42 | top: 0; 43 | left: 30px; 44 | display: none; 45 | width: 100%; 46 | @media (max-width: 1024px) { 47 | position: relative; 48 | left: 0; 49 | } 50 | } 51 | .tab-component__tab.active { 52 | display: block; 53 | } 54 | `; 55 | -------------------------------------------------------------------------------- /packages/chat-component/src/core/http/index.ts: -------------------------------------------------------------------------------- 1 | import { ChatResponseError } from '../../utils/index.js'; 2 | 3 | export async function callHttpApi( 4 | { question, type, approach, overrides, messages }: ChatRequestOptions, 5 | { method, url, stream, signal }: ChatHttpOptions, 6 | ) { 7 | return await fetch(`${url}/${type}`, { 8 | method: method, 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | }, 12 | signal, 13 | body: JSON.stringify({ 14 | messages: [ 15 | ...(messages ?? []), 16 | { 17 | content: question, 18 | role: 'user', 19 | }, 20 | ], 21 | context: { 22 | ...overrides, 23 | approach, 24 | }, 25 | stream: type === 'chat' ? stream : false, 26 | }), 27 | }); 28 | } 29 | 30 | export async function getAPIResponse( 31 | requestOptions: ChatRequestOptions, 32 | httpOptions: ChatHttpOptions, 33 | ): Promise { 34 | const response = await callHttpApi(requestOptions, httpOptions); 35 | 36 | // TODO: we should just use the value from httpOptions.stream 37 | const streamResponse = requestOptions.type === 'ask' ? false : httpOptions.stream; 38 | if (streamResponse) { 39 | return response; 40 | } 41 | const parsedResponse: BotResponse = await response.json(); 42 | if (response.status > 299 || !response.ok) { 43 | throw new ChatResponseError(response.statusText, response.status) || 'API Response Error'; 44 | } 45 | return parsedResponse; 46 | } 47 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/chat-stage.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | .chat-stage__header { 5 | display: flex; 6 | width: var(--width-base); 7 | margin: 0 auto var(--d-large); 8 | justify-content: center; 9 | align-items: center; 10 | 11 | @media (min-width: 1024px) { 12 | width: var(--width-narrow); 13 | } 14 | } 15 | .chat-stage__link svg { 16 | width: calc(var(--width-base) - var(--d-small)); 17 | height: calc(var(--width-base) - var(--d-small)); 18 | position: relative; 19 | z-index: 1; 20 | } 21 | .chat-stage__link { 22 | flex-shrink: 0; 23 | border-radius: calc(var(--radius-large) * 3); 24 | border: var(--border-thicker) solid transparent; 25 | background-origin: border-box; 26 | background-clip: content-box, border-box; 27 | background-size: cover; 28 | background-image: linear-gradient(to right, var(--c-accent-light), var(--c-accent-high)); 29 | width: calc(var(--d-xlarge) * 2); 30 | height: calc(var(--d-xlarge) * 2); 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | margin-right: var(--d-large); 35 | overflow: hidden; 36 | padding: var(--d-small); 37 | position: relative; 38 | } 39 | .chat-stage__link::after { 40 | content: ''; 41 | border-radius: calc(var(--radius-large) * 3); 42 | width: calc(var(--width-base) - var(--d-small)); 43 | height: calc(var(--width-base) - var(--d-small)); 44 | position: absolute; 45 | background-color: var(--c-secondary); 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /packages/search/src/plugins/approaches.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { type AskApproach, AskRetrieveThenRead, type ChatApproach, ChatReadRetrieveRead } from '../lib/index.js'; 3 | 4 | export type Approaches = { chat: Record; ask: Record }; 5 | 6 | export default fp( 7 | async (fastify, _options) => { 8 | const config = fastify.config; 9 | 10 | // Various approaches to integrate GPT and external knowledge. 11 | // Most applications will use a single one of these patterns or some derivative, 12 | // here we include several for exploration purposes. 13 | fastify.decorate('approaches', { 14 | chat: { 15 | rrr: new ChatReadRetrieveRead( 16 | fastify.azure.search, 17 | fastify.openai, 18 | config.azureOpenAiChatGptModel, 19 | config.azureOpenAiEmbeddingModel, 20 | config.kbFieldsSourcePage, 21 | config.kbFieldsContent, 22 | ), 23 | }, 24 | ask: { 25 | rtr: new AskRetrieveThenRead( 26 | fastify.azure.search, 27 | fastify.openai, 28 | config.azureOpenAiChatGptModel, 29 | config.azureOpenAiEmbeddingModel, 30 | config.kbFieldsSourcePage, 31 | config.kbFieldsContent, 32 | ), 33 | }, 34 | }); 35 | }, 36 | { name: 'approaches', dependencies: ['config', 'azure', 'openai', 'langchain'] }, 37 | ); 38 | 39 | // When using .decorate you have to specify added properties for Typescript 40 | declare module 'fastify' { 41 | export interface FastifyInstance { 42 | approaches: Approaches; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /infra/core/host/container-apps-environment.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Container Apps environment.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | @description('Name of the Application Insights resource') 7 | param applicationInsightsName string = '' 8 | 9 | @description('Specifies if Dapr is enabled') 10 | param daprEnabled bool = false 11 | 12 | @description('Name of the Log Analytics workspace') 13 | param logAnalyticsWorkspaceName string 14 | 15 | resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = { 16 | name: name 17 | location: location 18 | tags: tags 19 | properties: { 20 | appLogsConfiguration: { 21 | destination: 'log-analytics' 22 | logAnalyticsConfiguration: { 23 | customerId: logAnalyticsWorkspace.properties.customerId 24 | sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey 25 | } 26 | } 27 | daprAIInstrumentationKey: daprEnabled && !empty(applicationInsightsName) ? applicationInsights.properties.InstrumentationKey : '' 28 | } 29 | } 30 | 31 | resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { 32 | name: logAnalyticsWorkspaceName 33 | } 34 | 35 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (daprEnabled && !empty(applicationInsightsName)) { 36 | name: applicationInsightsName 37 | } 38 | 39 | output defaultDomain string = containerAppsEnvironment.properties.defaultDomain 40 | output id string = containerAppsEnvironment.id 41 | output name string = containerAppsEnvironment.name 42 | -------------------------------------------------------------------------------- /packages/indexer/test.http: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # VS Code with REST Client extension is needed to use this file. 3 | # Download at: https://aka.ms/vscode/rest-client 4 | ################################################################## 5 | 6 | @api_host = http://localhost:3001 7 | 8 | # Create an index 9 | POST {{api_host}}/indexes 10 | Content-Type: application/json 11 | 12 | { 13 | "name": "test" 14 | } 15 | 16 | ### 17 | 18 | # Delete an index 19 | DELETE {{api_host}}/indexes/test 20 | 21 | ### 22 | 23 | # Index a text file 24 | POST {{api_host}}/indexes/test/files 25 | Accept: */* 26 | Content-Type: multipart/form-data; boundary=Boundary 27 | 28 | --Boundary 29 | Content-Disposition: form-data; name="file"; filename="test.txt" 30 | Content-Type: text/plain 31 | 32 | < ../../README.md 33 | --Boundary 34 | Content-Disposition: form-data; name="options" 35 | 36 | { 37 | "category": "test-category", 38 | "wait": true, 39 | "uploadToStorage": true, 40 | "useVectors": true 41 | } 42 | --Boundary-- 43 | 44 | ### 45 | 46 | # Index a pdf file 47 | POST {{api_host}}/indexes/test/files 48 | Accept: */* 49 | Content-Type: multipart/form-data; boundary=Boundary 50 | 51 | --Boundary 52 | Content-Disposition: form-data; name="file"; filename="test.pdf" 53 | Content-Type: application/pdf 54 | 55 | < ../../data/support.pdf 56 | --Boundary 57 | Content-Disposition: form-data; name="options" 58 | 59 | { 60 | "category": "test-category", 61 | "wait": true, 62 | "useVectors": true 63 | } 64 | --Boundary-- 65 | 66 | ### 67 | 68 | # Delete a file 69 | DELETE {{api_host}}/indexes/test/files/sample.txt 70 | -------------------------------------------------------------------------------- /packages/chat-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | OpenAI Chat App JavaScript 9 | 10 | 11 |
12 |

Welcome to this Azure OpenAI JavaScript Chat Sample

13 | 14 |
15 | 16 |
17 |
18 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/low-cost.md: -------------------------------------------------------------------------------- 1 | # Reduce deployment costs 2 | 3 | This AI RAG chat application is designed to be easily deployed using the Azure Developer CLI, which provisions the infrastructure according to the Bicep files in the `infra` folder. Those files describe each of the Azure resources needed, and configures their SKU (pricing tier) and other parameters. Many Azure services offer a free tier, but the infrastructure files in this project do *not* default to the free tier as there are often limitations in that tier. 4 | 5 | However, if your goal is to minimize costs while prototyping your application, follow the steps below *before* running `azd up`. Once you've gone through these steps, return to the [deployment steps](../README.md#azure-deployment). 6 | 7 | 1. Log in to your Azure account using the Azure Developer CLI: 8 | 9 | ```shell 10 | azd auth login 11 | ``` 12 | 13 | 1. Create a new azd environment for the free resource group: 14 | 15 | ```shell 16 | azd env new 17 | ``` 18 | 19 | Enter a name that will be used for the resource group. 20 | This will create a new folder in the `.azure` folder, and set it as the active environment for any calls to `azd` going forward. 21 | 22 | 1. Use the free tier of Azure AI Search: 23 | 24 | ```shell 25 | azd env set AZURE_SEARCH_SERVICE_SKU free 26 | ``` 27 | 28 | Limitations: 29 | 1. You are only allowed one free search service across all regions. 30 | 2. The free tier does not support semantic ranker. Note that will generally result in [decreased search relevance](https://techcommunity.microsoft.com/blog/azure-ai-services-blog/azure-ai-search-outperforming-vector-search-with-hybrid-retrieval-and-ranking-ca/3929167). 31 | 32 | -------------------------------------------------------------------------------- /packages/chat-component/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | OpenAI icon -------------------------------------------------------------------------------- /infra/core/host/container-apps.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Container Registry and an Azure Container Apps environment.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | 6 | param containerAppsEnvironmentName string 7 | param containerRegistryName string 8 | param containerRegistryResourceGroupName string = '' 9 | param containerRegistryAdminUserEnabled bool = false 10 | param logAnalyticsWorkspaceName string 11 | param applicationInsightsName string = '' 12 | 13 | var containerRegistryScope = !empty(containerRegistryResourceGroupName) 14 | ? resourceGroup(containerRegistryResourceGroupName) 15 | : resourceGroup() 16 | 17 | module containerAppsEnvironment 'container-apps-environment.bicep' = { 18 | name: '${name}-container-apps-environment' 19 | params: { 20 | name: containerAppsEnvironmentName 21 | location: location 22 | tags: tags 23 | logAnalyticsWorkspaceName: logAnalyticsWorkspaceName 24 | applicationInsightsName: applicationInsightsName 25 | } 26 | } 27 | 28 | module containerRegistry 'container-registry.bicep' = { 29 | name: '${name}-container-registry' 30 | scope: containerRegistryScope 31 | params: { 32 | name: containerRegistryName 33 | location: location 34 | adminUserEnabled: containerRegistryAdminUserEnabled 35 | tags: tags 36 | } 37 | } 38 | 39 | output defaultDomain string = containerAppsEnvironment.outputs.defaultDomain 40 | output environmentName string = containerAppsEnvironment.outputs.name 41 | output environmentId string = containerAppsEnvironment.outputs.id 42 | 43 | output registryLoginServer string = containerRegistry.outputs.loginServer 44 | output registryName string = containerRegistry.outputs.name 45 | -------------------------------------------------------------------------------- /packages/search/src/lib/approaches/approach.ts: -------------------------------------------------------------------------------- 1 | import { type Message } from '../message.js'; 2 | 3 | export interface ApproachResponse { 4 | choices: Array<{ 5 | index: number; 6 | message: ApproachResponseMessage; 7 | }>; 8 | object: 'chat.completion'; 9 | } 10 | 11 | export interface ApproachResponseChunk { 12 | choices: Array<{ 13 | index: number; 14 | delta: Partial; 15 | finish_reason: string | null; 16 | }>; 17 | object: 'chat.completion.chunk'; 18 | } 19 | 20 | export type ApproachResponseMessage = Message & { 21 | context?: Record & { 22 | data_points?: { 23 | text?: string[]; 24 | images?: string[]; 25 | }; 26 | thoughts?: string; 27 | }; 28 | session_state?: Record; 29 | }; 30 | 31 | export type ApproachContext = { 32 | retrieval_mode?: 'hybrid' | 'text' | 'vectors'; 33 | semantic_ranker?: boolean; 34 | semantic_captions?: boolean; 35 | top?: number; 36 | temperature?: number; 37 | prompt_template?: string; 38 | prompt_template_prefix?: string; 39 | prompt_template_suffix?: string; 40 | exclude_category?: string; 41 | }; 42 | 43 | export type ChatApproachContext = ApproachContext & { 44 | suggest_followup_questions?: boolean; 45 | }; 46 | 47 | export interface ChatApproach { 48 | run(messages: Message[], context?: ChatApproachContext): Promise; 49 | runWithStreaming(messages: Message[], context?: ChatApproachContext): AsyncGenerator; 50 | } 51 | 52 | export interface AskApproach { 53 | run(query: string, context?: ApproachContext): Promise; 54 | runWithStreaming(query: string, context?: ApproachContext): AsyncGenerator; 55 | } 56 | -------------------------------------------------------------------------------- /packages/chat-component/public/svg/history-dismiss-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/search/test.http: -------------------------------------------------------------------------------- 1 | ################################################################## 2 | # VS Code with REST Client extension is needed to use this file. 3 | # Download at: https://aka.ms/vscode/rest-client 4 | ################################################################## 5 | 6 | @api_host = http://localhost:3000 7 | 8 | # Get file content 9 | GET {{api_host}}/content/terms-of-service.md 10 | 11 | ### 12 | 13 | # Chat with the bot 14 | POST {{api_host}}/chat 15 | Content-Type: application/json 16 | 17 | { 18 | "messages": [{ 19 | "content": "How to search and book rentals?", 20 | "role": "user" 21 | }], 22 | "context": { 23 | "approach":"rrr", 24 | "retrieval_mode": "hybrid", 25 | "semantic_ranker": true, 26 | "semantic_captions": false, 27 | "top": 3, 28 | "suggest_followup_questions": false 29 | } 30 | } 31 | 32 | ### 33 | 34 | # Chat with the bot using streaming 35 | POST {{api_host}}/chat 36 | Content-Type: application/json 37 | 38 | { 39 | "messages": [{ 40 | "content": "How to search and book rentals?", 41 | "role": "user" 42 | }], 43 | "stream": true, 44 | "context": { 45 | "approach":"rrr", 46 | "retrieval_mode": "hybrid", 47 | "semantic_ranker": true, 48 | "semantic_captions": false, 49 | "top": 3, 50 | "suggest_followup_questions": true 51 | } 52 | } 53 | 54 | ### 55 | 56 | # Ask a question using the rtr approach 57 | POST {{api_host}}/ask 58 | Content-Type: application/json 59 | 60 | { 61 | "messages": [{ 62 | "content": "What is the refund policy?", 63 | "role": "user" 64 | }], 65 | "context": { 66 | "approach":"rtr", 67 | "retrieval_mode": "hybrid", 68 | "semantic_ranker": true, 69 | "semantic_captions": false, 70 | "top": 3 71 | } 72 | } 73 | 74 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/follow-up-questions.ts: -------------------------------------------------------------------------------- 1 | import { injectable } from 'inversify'; 2 | import { 3 | container, 4 | type ChatEntryInlineInputController, 5 | ControllerType, 6 | ComposableReactiveControllerBase, 7 | } from './composable.js'; 8 | import { html } from 'lit'; 9 | import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; 10 | import iconQuestion from '../../public/svg/bubblequestion-icon.svg?raw'; 11 | 12 | @injectable() 13 | export class FollowupQuestionsController 14 | extends ComposableReactiveControllerBase 15 | implements ChatEntryInlineInputController 16 | { 17 | render(entry: ChatThreadEntry, handleInput: (input: string) => void) { 18 | const followupQuestions = entry.followupQuestions; 19 | // render followup questions 20 | // need to fix first after decoupling of teaserlist 21 | if (followupQuestions && followupQuestions.length > 0) { 22 | return html` 23 |
24 | ${unsafeSVG(iconQuestion)} 25 |
    26 | ${followupQuestions.map( 27 | (followupQuestion) => html` 28 |
  • 29 | ${followupQuestion} 36 |
  • 37 | `, 38 | )} 39 |
40 |
41 | `; 42 | } 43 | 44 | return ''; 45 | } 46 | } 47 | 48 | container.bind(ControllerType.ChatEntryInlineInput).to(FollowupQuestionsController); 49 | -------------------------------------------------------------------------------- /infra/core/ai/cognitiveservices.bicep: -------------------------------------------------------------------------------- 1 | metadata description = 'Creates an Azure Cognitive Services instance.' 2 | param name string 3 | param location string = resourceGroup().location 4 | param tags object = {} 5 | @description('The custom subdomain name used to access the API. Defaults to the value of the name parameter.') 6 | param customSubDomainName string = name 7 | param deployments array = [] 8 | param kind string = 'OpenAI' 9 | 10 | @allowed([ 'Enabled', 'Disabled' ]) 11 | param publicNetworkAccess string = 'Enabled' 12 | param sku object = { 13 | name: 'S0' 14 | } 15 | param disableLocalAuth bool = false 16 | 17 | param allowedIpRules array = [] 18 | param networkAcls object = empty(allowedIpRules) ? { 19 | defaultAction: 'Allow' 20 | } : { 21 | ipRules: allowedIpRules 22 | defaultAction: 'Deny' 23 | } 24 | 25 | resource account 'Microsoft.CognitiveServices/accounts@2023-05-01' = { 26 | name: name 27 | location: location 28 | tags: tags 29 | kind: kind 30 | properties: { 31 | customSubDomainName: customSubDomainName 32 | publicNetworkAccess: publicNetworkAccess 33 | networkAcls: networkAcls 34 | disableLocalAuth: disableLocalAuth 35 | } 36 | sku: sku 37 | } 38 | 39 | @batchSize(1) 40 | resource deployment 'Microsoft.CognitiveServices/accounts/deployments@2023-05-01' = [for deployment in deployments: { 41 | parent: account 42 | name: deployment.name 43 | properties: { 44 | model: deployment.model 45 | raiPolicyName: contains(deployment, 'raiPolicyName') ? deployment.raiPolicyName : null 46 | } 47 | sku: contains(deployment, 'sku') ? deployment.sku : { 48 | name: 'Standard' 49 | capacity: 20 50 | } 51 | }] 52 | 53 | output endpoint string = account.properties.endpoint 54 | output id string = account.id 55 | output name string = account.name 56 | -------------------------------------------------------------------------------- /packages/indexer/test/helper.ts: -------------------------------------------------------------------------------- 1 | // This file contains code that we reuse between our tests. 2 | import * as helper from 'fastify-cli/helper.js'; 3 | import * as path from 'node:path'; 4 | import type * as test from 'node:test'; 5 | import process from 'node:process'; 6 | import { fileURLToPath } from 'node:url'; 7 | 8 | export type TestContext = { after: typeof test.after }; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | const AppPath = path.join(__dirname, '..', 'src', 'app.ts'); 14 | 15 | // Fill in this config with all the configurations 16 | // needed for testing the application 17 | async function config() { 18 | process.env.AZURE_OPENAI_CHATGPT_DEPLOYMENT = 'chat'; 19 | process.env.AZURE_OPENAI_CHATGPT_MODEL = 'gpt-4o-mini'; 20 | process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT = 'embeddings'; 21 | process.env.AZURE_OPENAI_EMBEDDING_MODEL = ''; 22 | process.env.AZURE_OPENAI_SERVICE = 'https://example.com'; 23 | process.env.AZURE_SEARCH_INDEX = 'testindex'; 24 | process.env.AZURE_SEARCH_SERVICE = 'https://example.com'; 25 | process.env.AZURE_STORAGE_ACCOUNT = 'dummystorage'; 26 | process.env.AZURE_STORAGE_CONTAINER = 'testfiles'; 27 | 28 | return {}; 29 | } 30 | 31 | // Automatically build and tear down our instance 32 | async function build(t: TestContext) { 33 | // you can set all the options supported by the fastify CLI command 34 | const argv = [AppPath]; 35 | 36 | // fastify-plugin ensures that all decorators 37 | // are exposed for testing purposes, this is 38 | // different from the production setup 39 | const app = await helper.build(argv, await config()); 40 | 41 | // Tear down our app after we are done 42 | t.after(() => void app.close()); 43 | 44 | return app; 45 | } 46 | 47 | export { config, build }; 48 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/teaser-list-component.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | .headline { 5 | color: var(--text-color); 6 | font-size: var(--font-r-large); 7 | padding: 0; 8 | margin: var(--d-small) 0 var(--d-large); 9 | 10 | @media (min-width: 1024px) { 11 | font-size: var(--font-r-base); 12 | text-align: center; 13 | } 14 | } 15 | [role='button'] { 16 | text-decoration: none; 17 | color: var(--text-color); 18 | display: block; 19 | font-size: var(--font-rel-base); 20 | } 21 | .teaser-list { 22 | list-style-type: none; 23 | padding: 0; 24 | text-align: center; 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: center; 28 | } 29 | .teaser-list.always-row { 30 | text-align: left; 31 | } 32 | .teaser-list:not(.always-row) { 33 | @media (min-width: 1024px) { 34 | flex-direction: row; 35 | } 36 | } 37 | .teaser-list-item { 38 | padding: var(--d-small); 39 | border-radius: var(--radius-base); 40 | background: var(--c-white); 41 | margin: var(--d-xsmall); 42 | color: var(--text-color); 43 | justify-content: space-evenly; 44 | box-shadow: var(--shadow); 45 | border: var(--border-base) solid transparent; 46 | 47 | @media (min-width: 768px) { 48 | min-height: 100px; 49 | } 50 | } 51 | .teaser-list-item:hover, 52 | .teaser-list-item:focus { 53 | color: var(--c-accent-dark); 54 | background: var(--c-secondary); 55 | transition: all 0.3s ease-in-out; 56 | border-color: var(--c-accent-high); 57 | } 58 | .teaser-list-item .teaser-click-label { 59 | color: var(--c-accent-high); 60 | font-weight: bold; 61 | display: block; 62 | margin-top: 20px; 63 | text-decoration: underline; 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /packages/indexer/src/plugins/azure.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { DefaultAzureCredential } from '@azure/identity'; 3 | import { SearchIndexClient } from '@azure/search-documents'; 4 | import { BlobServiceClient, type ContainerClient } from '@azure/storage-blob'; 5 | 6 | export type AzureClients = { 7 | credential: DefaultAzureCredential; 8 | searchIndex: SearchIndexClient; 9 | blobContainer: ContainerClient; 10 | }; 11 | 12 | export default fp( 13 | async (fastify, _options) => { 14 | const config = fastify.config; 15 | 16 | // Use the current user identity to authenticate with Azure OpenAI, AI Search and Blob Storage 17 | // (no secrets needed, just use 'az login' locally, and managed identity when deployed on Azure). 18 | // If you need to use keys, use separate AzureKeyCredential instances with the keys for each service 19 | const credential = new DefaultAzureCredential(); 20 | 21 | // Set up Azure clients 22 | const searchIndexClient = new SearchIndexClient( 23 | `https://${config.azureSearchService}.search.windows.net`, 24 | credential, 25 | ); 26 | const blobServiceClient = new BlobServiceClient( 27 | `https://${config.azureStorageAccount}.blob.core.windows.net`, 28 | credential, 29 | ); 30 | const blobContainerClient = blobServiceClient.getContainerClient(config.azureStorageContainer); 31 | 32 | fastify.decorate('azure', { 33 | credential, 34 | searchIndex: searchIndexClient, 35 | blobContainer: blobContainerClient, 36 | }); 37 | }, 38 | { 39 | name: 'azure', 40 | dependencies: ['config'], 41 | }, 42 | ); 43 | 44 | // When using .decorate you have to specify added properties for Typescript 45 | declare module 'fastify' { 46 | export interface FastifyInstance { 47 | azure: AzureClients; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/search/src/plugins/azure.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { DefaultAzureCredential } from '@azure/identity'; 3 | import { SearchClient } from '@azure/search-documents'; 4 | import { BlobServiceClient, type ContainerClient } from '@azure/storage-blob'; 5 | 6 | export type AzureClients = { 7 | credential: DefaultAzureCredential; 8 | search: SearchClient; 9 | blobContainer: ContainerClient; 10 | }; 11 | 12 | export default fp( 13 | async (fastify, _options) => { 14 | const config = fastify.config; 15 | 16 | // Use the current user identity to authenticate with Azure OpenAI, AI Search and Blob Storage 17 | // (no secrets needed, just use 'az login' locally, and managed identity when deployed on Azure). 18 | // If you need to use keys, use separate AzureKeyCredential instances with the keys for each service 19 | const credential = new DefaultAzureCredential(); 20 | 21 | // Set up Azure clients 22 | const searchClient = new SearchClient( 23 | `https://${config.azureSearchService}.search.windows.net`, 24 | config.azureSearchIndex, 25 | credential, 26 | ); 27 | const blobServiceClient = new BlobServiceClient( 28 | `https://${config.azureStorageAccount}.blob.core.windows.net`, 29 | credential, 30 | ); 31 | const blobContainerClient = blobServiceClient.getContainerClient(config.azureStorageContainer); 32 | 33 | fastify.decorate('azure', { 34 | credential, 35 | search: searchClient, 36 | blobContainer: blobContainerClient, 37 | }); 38 | }, 39 | { 40 | name: 'azure', 41 | dependencies: ['config'], 42 | }, 43 | ); 44 | 45 | // When using .decorate you have to specify added properties for Typescript 46 | declare module 'fastify' { 47 | export interface FastifyInstance { 48 | azure: AzureClients; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/search/src/plugins/langchain.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin'; 2 | import { AzureChatOpenAI, AzureOpenAIEmbeddings, type OpenAIChatInput } from '@langchain/openai'; 3 | 4 | export type LangchainService = { 5 | getChat(options?: Partial): Promise; 6 | getEmbeddings(options?: Partial): Promise; 7 | }; 8 | 9 | export default fp( 10 | async (fastify, _options) => { 11 | const config = fastify.config; 12 | const getAzureOpenAiOptions = (apiToken: string) => ({ 13 | openAIApiKey: apiToken, 14 | azureOpenAIApiVersion: fastify.openai.config.apiVersion, 15 | azureOpenAIApiKey: apiToken, 16 | azureOpenAIBasePath: `${fastify.openai.config.apiUrl}/openai/deployments`, 17 | }); 18 | 19 | fastify.decorate('langchain', { 20 | async getChat(options?: Partial) { 21 | const apiToken = await fastify.openai.getApiToken(); 22 | return new AzureChatOpenAI({ 23 | ...options, 24 | ...getAzureOpenAiOptions(apiToken), 25 | azureOpenAIApiDeploymentName: config.azureOpenAiChatGptDeployment, 26 | } as any); 27 | }, 28 | async getEmbeddings(options?: Partial) { 29 | const apiToken = await fastify.openai.getApiToken(); 30 | return new AzureOpenAIEmbeddings({ 31 | ...options, 32 | ...getAzureOpenAiOptions(apiToken), 33 | azureOpenAIApiDeploymentName: config.azureOpenAiEmbeddingDeployment, 34 | }); 35 | }, 36 | }); 37 | }, 38 | { name: 'langchain', dependencies: ['azure', 'config', 'openai'] }, 39 | ); 40 | 41 | // When using .decorate you have to specify added properties for Typescript 42 | declare module 'fastify' { 43 | export interface FastifyInstance { 44 | langchain: LangchainService; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/search/test/helper.ts: -------------------------------------------------------------------------------- 1 | // This file contains code that we reuse between our tests. 2 | import * as helper from 'fastify-cli/helper.js'; 3 | import * as path from 'node:path'; 4 | import type * as test from 'node:test'; 5 | import process from 'node:process'; 6 | import { fileURLToPath } from 'node:url'; 7 | 8 | export type TestContext = { after: typeof test.after }; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | const APP_PATH = path.join(__dirname, '../src/app.ts'); 14 | 15 | // Fill in this config with all the configurations 16 | // needed for testing the application 17 | async function config() { 18 | // Use fixed values when .env file is not present 19 | process.env.AZURE_OPENAI_CHATGPT_DEPLOYMENT = 'chat'; 20 | process.env.AZURE_OPENAI_CHATGPT_MODEL = 'gpt-4o-mini'; 21 | process.env.AZURE_OPENAI_EMBEDDING_DEPLOYMENT = 'embedding'; 22 | process.env.AZURE_OPENAI_EMBEDDING_MODEL = ''; 23 | process.env.AZURE_OPENAI_SERVICE = 'cog-x2y5k2ccncqou'; 24 | process.env.AZURE_SEARCH_INDEX = 'gptkbindex'; 25 | process.env.AZURE_SEARCH_SERVICE = 'gptkb-x2y5k2ccncqou'; 26 | process.env.AZURE_STORAGE_ACCOUNT = 'stx2y5k2ccncqou'; 27 | process.env.AZURE_STORAGE_CONTAINER = 'content'; 28 | 29 | return {}; 30 | } 31 | 32 | // Automatically build and tear down our instance 33 | async function build(t: TestContext) { 34 | // you can set all the options supported by the fastify CLI command 35 | const argv = [APP_PATH]; 36 | 37 | // fastify-plugin ensures that all decorators 38 | // are exposed for testing purposes, this is 39 | // different from the production setup 40 | const app = await helper.build(argv, await config()); 41 | 42 | // Tear down our app after we are done 43 | t.after(() => void app.close()); 44 | 45 | return app; 46 | } 47 | 48 | export { config, build }; 49 | -------------------------------------------------------------------------------- /packages/chat-component/src/styles/chat-action-button.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const styles = css` 4 | button { 5 | color: var(--text-color); 6 | text-decoration: underline; 7 | border: var(--border-thin) solid var(--c-accent-dark); 8 | text-decoration: none; 9 | border-radius: var(--radius-small); 10 | background: var(--c-white); 11 | display: flex; 12 | align-items: center; 13 | margin-left: 5px; 14 | opacity: 1; 15 | padding: var(--d-xsmall); 16 | transition: all 0.3s ease-in-out; 17 | position: relative; 18 | cursor: pointer; 19 | } 20 | button:disabled { 21 | opacity: 0.5; 22 | cursor: not-allowed; 23 | } 24 | span { 25 | font-size: smaller; 26 | transition: all 0.3s ease-out 0s; 27 | position: absolute; 28 | text-align: right; 29 | top: -80%; 30 | background: var(--c-accent-dark); 31 | color: var(--c-white); 32 | opacity: 0; 33 | right: 0; 34 | padding: var(--d-xsmall) var(--d-small); 35 | border-radius: var(--radius-small); 36 | font-weight: bold; 37 | word-wrap: nowrap; 38 | } 39 | span::after { 40 | content: ''; 41 | position: absolute; 42 | width: 0; 43 | height: 0; 44 | border-left: 5px solid transparent; 45 | border-right: 5px solid transparent; 46 | border-top: var(--border-thick) solid var(--c-accent-dark); 47 | bottom: -8px; 48 | right: 5px; 49 | } 50 | svg { 51 | fill: currentColor; 52 | padding: var(--d-xsmall); 53 | width: var(--d-base); 54 | height: var(--d-base); 55 | } 56 | button:hover > span, 57 | button:focus > span { 58 | display: inline-block; 59 | opacity: 1; 60 | } 61 | button:hover, 62 | button:focus, 63 | button:hover > svg, 64 | button:focus > svg { 65 | background-color: var(--c-light-gray); 66 | border-radius: var(--radius-small); 67 | transition: background 0.3s ease-in-out; 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/javascript-node 3 | { 4 | "name": "Azure Search OpenAI Js", 5 | 6 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 7 | "image": "mcr.microsoft.com/devcontainers/javascript-node:22-bookworm", 8 | 9 | // Features to add to the dev container. More info: https://containers.dev/features. 10 | "features": { 11 | "ghcr.io/devcontainers/features/docker-in-docker:2": { 12 | "moby": "false" 13 | }, 14 | "ghcr.io/devcontainers/features/powershell:1": {}, 15 | "ghcr.io/devcontainers/features/azure-cli:1": { 16 | "version": "latest", 17 | "installBicep": true 18 | }, 19 | "ghcr.io/azure/azure-dev/azd:latest": {}, 20 | "ghcr.io/grafana/devcontainer-features/k6:1": {} 21 | }, 22 | 23 | // Configure tool-specific properties. 24 | "customizations": { 25 | "vscode": { 26 | "extensions": [ 27 | "ms-azuretools.azure-dev", 28 | "ms-azuretools.vscode-bicep", 29 | "ms-azuretools.vscode-docker", 30 | "esbenp.prettier-vscode", 31 | "humao.rest-client", 32 | "GitHub.copilot", 33 | "runem.lit-plugin", 34 | "ms-playwright.playwright" 35 | ] 36 | } 37 | }, 38 | 39 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 40 | "forwardPorts": [3000, 3001, 5173], 41 | 42 | // Use 'postCreateCommand' to run commands after the container is created. 43 | "postCreateCommand": "./.devcontainer/postCreateCommand.sh", 44 | 45 | // Set minimal host requirements for the container. 46 | "hostRequirements": { 47 | "memory": "8gb" 48 | } 49 | 50 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 51 | // "remoteUser": "root" 52 | } 53 | -------------------------------------------------------------------------------- /packages/webapp/src/i18n/tooltips.ts: -------------------------------------------------------------------------------- 1 | // Keep values less than 20 words. 2 | // Don't add links to the tooltips. 3 | export const toolTipText = { 4 | approaches: 'Retrieve first uses Azure Search. Read first uses LangChain.', 5 | promptTemplate: "Allows user to override the chatbot's prompt.", 6 | promptTemplatePrefix: 7 | "Allows user to provide a prefix to the chatbot's prompt. For example, `Answer the following question as if I were in high school.`", 8 | promptTemplateSuffix: 9 | "Allows user to provide a suffix to the chatbot's prompt. For example, `Return the first 50 words.`", 10 | retrieveNumber: 'Number of results affecting final answer', 11 | excludeCategory: 'Example categories include ...', 12 | useSemanticRanker: 13 | 'Semantic ranker is a machine learning model to improve the relevance and accuracy of search results.', 14 | useQueryContextSummaries: 15 | 'Can improve the relevance and accuracy of search results by providing a more concise and focused summary of the most relevant information related to the query or context.', 16 | suggestFollowupQuestions: 'Provide follow-up questions to continue conversation.', 17 | retrievalMode: 18 | "The retrieval mode choices determine how the chatbot retrieves and ranks responses based on semantic similarity to the user's query. `Vectors + Text (Hybrid)` uses a combination of vector embeddings and text matching, `Vectors` uses only vector embeddings, and `Text` uses only text matching.", 19 | streamChat: 20 | 'Continuously deliver responses as they are generated or wait until all responses are generated before delivering them.', 21 | }; 22 | 23 | // beak: triangle color 24 | // beakCurtain: outer edge 25 | // calloutMain: content center 26 | // No style to control text color 27 | export const toolTipTextCalloutProps = { 28 | styles: { 29 | beak: { background: '#D3D3D3' }, 30 | beakCurtain: { background: '#D3D3D3' }, 31 | calloutMain: { background: '#D3D3D3' }, 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /packages/chat-component/src/components/document-previewer.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, type PropertyValueMap, html } from 'lit'; 2 | import { customElement, property, state } from 'lit/decorators.js'; 3 | import { globalConfig } from '../config/global-config.js'; 4 | import { marked } from 'marked'; 5 | import { unsafeHTML } from 'lit/directives/unsafe-html.js'; 6 | import './loading-indicator.js'; 7 | 8 | @customElement('document-previewer') 9 | export class DocumentPreviewerComponent extends LitElement { 10 | @property({ type: String }) 11 | url: string | undefined = undefined; 12 | 13 | @state() 14 | previewContent: string | undefined = undefined; 15 | 16 | @state() 17 | loading: boolean = false; 18 | 19 | retrieveMarkdown() { 20 | if (this.url) { 21 | fetch(this.url) 22 | .then((response) => response.text()) 23 | .then((text) => { 24 | this.previewContent = marked.parse(text); 25 | }) 26 | .finally(() => { 27 | this.loading = false; 28 | }); 29 | } 30 | } 31 | 32 | override willUpdate(_changedProperties: PropertyValueMap | Map): void { 33 | if ( 34 | this.url && 35 | _changedProperties.has('url') && 36 | _changedProperties.get('url') !== this.url && 37 | this.url.endsWith('.md') 38 | ) { 39 | this.loading = true; 40 | this.retrieveMarkdown(); 41 | } 42 | } 43 | 44 | renderContent() { 45 | if (this.url) { 46 | return html` 47 | ${this.previewContent 48 | ? html` ${unsafeHTML(this.previewContent)}` 49 | : html`