chatTurns = new ArrayList<>(); chatTurns.add(chatTurn);
25 | *
26 | * ChatRequest chatRequest = new ChatRequest(); chatRequest.setChatHistory(chatTurns);
27 | * chatRequest.setApproach("rrr"); HttpEntity request = new
28 | * HttpEntity<>(chatRequest);
29 | *
30 | * ResponseEntity result =
31 | * this.restTemplate.postForEntity(uri("/api/chat"), chatRequest, ChatResponse.class);
32 | *
33 | * assertEquals(HttpStatus.OK, result.getStatusCode());
34 | */
35 | }
36 |
37 | private URI uri(String path) {
38 | return restTemplate.getRestTemplate().getUriTemplateHandler().expand(path);
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/backend/src/test/java/com/microsoft/openai/samples/rag/test/config/ProxyMockConfiguration.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 | package com.microsoft.openai.samples.rag.test.config;
3 |
4 | import com.azure.ai.openai.OpenAIAsyncClient;
5 | import com.microsoft.openai.samples.rag.proxy.AzureAISearchProxy;
6 | import com.microsoft.openai.samples.rag.proxy.BlobStorageProxy;
7 | import com.microsoft.openai.samples.rag.proxy.OpenAIProxy;
8 | import org.mockito.Mockito;
9 | import org.springframework.context.annotation.Bean;
10 | import org.springframework.context.annotation.Configuration;
11 | import org.springframework.context.annotation.Primary;
12 | import org.springframework.context.annotation.Profile;
13 |
14 | @Profile("test")
15 | @Configuration
16 | public class ProxyMockConfiguration {
17 |
18 | @Bean
19 | @Primary
20 | public AzureAISearchProxy mockedCognitiveSearchProxy() {
21 | return Mockito.mock(AzureAISearchProxy.class);
22 | }
23 |
24 | @Bean
25 | @Primary
26 | public OpenAIProxy mockedOpenAISearchProxy() {
27 | return Mockito.mock(OpenAIProxy.class);
28 | }
29 |
30 | @Bean
31 | @Primary
32 | public BlobStorageProxy mockedBlobStorageProxy() {
33 | return Mockito.mock(BlobStorageProxy.class);
34 | }
35 |
36 | @Bean
37 | @Primary
38 | public OpenAIAsyncClient mockedOpenAIAsynchClient() {
39 | return Mockito.mock(OpenAIAsyncClient.class);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | manifests
2 | node_modules
--------------------------------------------------------------------------------
/app/frontend/.env.dev:
--------------------------------------------------------------------------------
1 | VITE_BACKEND_URI=http://localhost:8081/api
2 |
--------------------------------------------------------------------------------
/app/frontend/.env.local:
--------------------------------------------------------------------------------
1 | VITE_BACKEND_URI=http://localhost:8081/api
2 |
--------------------------------------------------------------------------------
/app/frontend/.env.production:
--------------------------------------------------------------------------------
1 | VITE_BACKEND_URI=/api
2 |
3 |
--------------------------------------------------------------------------------
/app/frontend/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/app/frontend/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore JSON
2 | **/*.json
3 |
--------------------------------------------------------------------------------
/app/frontend/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4,
3 | "printWidth": 160,
4 | "arrowParens": "avoid",
5 | "trailingComma": "none"
6 | }
7 |
--------------------------------------------------------------------------------
/app/frontend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine AS build
2 |
3 | # make the 'app' folder the current working directory
4 | WORKDIR /app
5 |
6 | COPY . .
7 |
8 |
9 | # install project dependencies
10 | RUN npm install
11 | RUN npm run build
12 |
13 | FROM nginx:alpine
14 |
15 | WORKDIR /usr/share/nginx/html
16 | COPY --from=build /app/build .
17 | COPY --from=build /app/nginx/nginx.conf.template /etc/nginx/conf.d
18 |
19 | EXPOSE 80
20 |
21 | CMD ["/bin/sh", "-c", "envsubst < /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf && nginx -g \"daemon off;\""]
22 |
--------------------------------------------------------------------------------
/app/frontend/Dockerfile-aks:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine AS build
2 |
3 | # make the 'app' folder the current working directory
4 | WORKDIR /app
5 |
6 | COPY . .
7 |
8 |
9 | # install project dependencies
10 | RUN npm install
11 | RUN npm run build
12 |
13 | FROM nginx:alpine
14 |
15 | WORKDIR /usr/share/nginx/html
16 | COPY --from=build /app/build .
17 |
18 | EXPOSE 80
19 |
20 | CMD ["/bin/sh", "-c", "nginx -g \"daemon off;\""]
--------------------------------------------------------------------------------
/app/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | GPT + Enterprise data | Java Sample
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/frontend/manifests/frontend-deployment.tmpl.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: frontend-deployment
5 | namespace: azure-open-ai
6 | labels:
7 | app: frontend
8 | spec:
9 | replicas: 1
10 | selector:
11 | matchLabels:
12 | app: frontend
13 | template:
14 | metadata:
15 | labels:
16 | app: frontend
17 | spec:
18 | containers:
19 | - name: frontend
20 | image: {{.Env.SERVICE_FRONTEND_IMAGE_NAME}}
21 | imagePullPolicy: IfNotPresent
22 | ports:
23 | - containerPort: 80
24 | envFrom:
25 | - configMapRef:
26 | name: azd-env-configmap
27 |
--------------------------------------------------------------------------------
/app/frontend/manifests/frontend-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: frontend-service
5 | namespace: azure-open-ai
6 | spec:
7 | type: ClusterIP
8 | ports:
9 | - protocol: TCP
10 | port: 80
11 | targetPort: 80
12 | selector:
13 | app: frontend
14 |
--------------------------------------------------------------------------------
/app/frontend/nginx/nginx.conf.template:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | location / {
4 | root /usr/share/nginx/html;
5 | index index.html index.htm;
6 | }
7 |
8 | location /api {
9 | proxy_ssl_server_name on;
10 | proxy_http_version 1.1;
11 | proxy_pass $REACT_APP_API_BASE_URL;
12 | }
13 | }
--------------------------------------------------------------------------------
/app/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "1.4.0-alpha",
5 | "type": "module",
6 | "engines": {
7 | "node": ">=14.0.0"
8 | },
9 | "scripts": {
10 | "dev": "vite --port=8081",
11 | "build": "tsc && vite build",
12 | "preview": "vite preview"
13 | },
14 | "dependencies": {
15 | "@azure/msal-browser": "^3.1.0",
16 | "@azure/msal-react": "^2.0.4",
17 | "@fluentui/react": "^8.112.5",
18 | "@fluentui/react-components": "^9.37.3",
19 | "@fluentui/react-icons": "^2.0.221",
20 | "@react-spring/web": "^9.7.3",
21 | "dompurify": "^3.1.3",
22 | "frontend": "file:",
23 | "ndjson-readablestream": "^1.0.7",
24 | "react": "^18.2.0",
25 | "react-dom": "^18.2.0",
26 | "react-router-dom": "^6.18.0",
27 | "scheduler": "^0.20.2"
28 | },
29 | "devDependencies": {
30 | "@types/dompurify": "^3.0.3",
31 | "@types/react": "^18.2.34",
32 | "@types/react-dom": "^18.2.14",
33 | "@vitejs/plugin-react": "^4.1.1",
34 | "prettier": "^3.0.3",
35 | "typescript": "^5.2.2",
36 | "vite": "^4.5.5"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/app/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/app/frontend/src/api/api.ts:
--------------------------------------------------------------------------------
1 | import { ChatAppResponse, ChatAppResponseOrError, ChatAppRequest } from "./models";
2 | import { useLogin } from "../authConfig";
3 |
4 | const BACKEND_URI = import.meta.env.VITE_BACKEND_URI ? import.meta.env.VITE_BACKEND_URI : "";
5 |
6 | function getHeaders(idToken: string | undefined, stream:boolean): Record {
7 | var headers: Record = {
8 | "Content-Type": "application/json"
9 | };
10 | // If using login, add the id token of the logged in account as the authorization
11 | if (useLogin) {
12 | if (idToken) {
13 | headers["Authorization"] = `Bearer ${idToken}`
14 | }
15 | }
16 |
17 | if (stream) {
18 | headers["Accept"] = "application/x-ndjson";
19 | } else {
20 | headers["Accept"] = "application/json";
21 | }
22 |
23 | return headers;
24 | }
25 |
26 | export async function askApi(request: ChatAppRequest, idToken: string | undefined): Promise {
27 | const response = await fetch(`${BACKEND_URI}/ask`, {
28 | method: "POST",
29 | headers: getHeaders(idToken, request.stream || false),
30 | body: JSON.stringify(request)
31 | });
32 |
33 | const parsedResponse: ChatAppResponseOrError = await response.json();
34 | if (response.status > 299 || !response.ok) {
35 | throw Error(parsedResponse.error || "Unknown error");
36 | }
37 |
38 | return parsedResponse as ChatAppResponse;
39 | }
40 |
41 | export async function chatApi(request: ChatAppRequest, idToken: string | undefined): Promise {
42 | return await fetch(`${BACKEND_URI}/chat`, {
43 | method: "POST",
44 | headers: getHeaders(idToken, request.stream || false),
45 | body: JSON.stringify(request)
46 | });
47 | }
48 |
49 | export function getCitationFilePath(citation: string): string {
50 | return `${BACKEND_URI}/content/${citation}`;
51 | }
52 |
--------------------------------------------------------------------------------
/app/frontend/src/api/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./api";
2 | export * from "./models";
3 |
--------------------------------------------------------------------------------
/app/frontend/src/api/models.ts:
--------------------------------------------------------------------------------
1 | export const enum Approaches {
2 | JAVA_OPENAI_SDK = "jos",
3 | JAVA_SEMANTIC_KERNEL = "jsk",
4 | JAVA_SEMANTIC_KERNEL_PLANNER = "jskp"
5 | }
6 |
7 | export const enum RetrievalMode {
8 | Hybrid = "hybrid",
9 | Vectors = "vectors",
10 | Text = "text"
11 | }
12 |
13 | export const enum SKMode {
14 | Chains = "chains",
15 | Planner = "planner"
16 | }
17 |
18 | export type ChatAppRequestOverrides = {
19 | retrieval_mode?: RetrievalMode;
20 | semantic_ranker?: boolean;
21 | semantic_captions?: boolean;
22 | exclude_category?: string;
23 | top?: number;
24 | temperature?: number;
25 | prompt_template?: string;
26 | prompt_template_prefix?: string;
27 | prompt_template_suffix?: string;
28 | suggest_followup_questions?: boolean;
29 | use_oid_security_filter?: boolean;
30 | use_groups_security_filter?: boolean;
31 | semantic_kernel_mode?: SKMode;
32 | };
33 |
34 | export type ResponseMessage = {
35 | content: string;
36 | role: string;
37 | };
38 |
39 | export type ResponseContext = {
40 | thoughts: string | null;
41 | data_points: string[];
42 | };
43 |
44 | export type ResponseChoice = {
45 | index: number;
46 | message: ResponseMessage;
47 | context: ResponseContext;
48 | session_state: any;
49 | };
50 |
51 | export type ChatAppResponseOrError = {
52 | choices?: ResponseChoice[];
53 | error?: string;
54 | };
55 |
56 | export type ChatAppResponse = {
57 | choices: ResponseChoice[];
58 | };
59 |
60 | export type ChatAppRequestContext = {
61 | overrides?: ChatAppRequestOverrides;
62 | };
63 |
64 | export type ChatAppRequest = {
65 | messages: ResponseMessage[];
66 | approach: Approaches;
67 | context?: ChatAppRequestContext;
68 | stream?: boolean;
69 | session_state: any;
70 | };
71 |
--------------------------------------------------------------------------------
/app/frontend/src/assets/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/frontend/src/assets/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/frontend/src/components/AnalysisPanel/AnalysisPanel.module.css:
--------------------------------------------------------------------------------
1 | .thoughtProcess {
2 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
3 | word-wrap: break-word;
4 | padding-top: 12px;
5 | padding-bottom: 12px;
6 | }
7 |
--------------------------------------------------------------------------------
/app/frontend/src/components/AnalysisPanel/AnalysisPanelTabs.tsx:
--------------------------------------------------------------------------------
1 | export enum AnalysisPanelTabs {
2 | ThoughtProcessTab = "thoughtProcess",
3 | SupportingContentTab = "supportingContent",
4 | CitationTab = "citation"
5 | }
6 |
--------------------------------------------------------------------------------
/app/frontend/src/components/AnalysisPanel/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./AnalysisPanel";
2 | export * from "./AnalysisPanelTabs";
3 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Answer/AnswerError.tsx:
--------------------------------------------------------------------------------
1 | import { Stack, PrimaryButton } from "@fluentui/react";
2 | import { ErrorCircle24Regular } from "@fluentui/react-icons";
3 |
4 | import styles from "./Answer.module.css";
5 |
6 | interface Props {
7 | error: string;
8 | onRetry: () => void;
9 | }
10 |
11 | export const AnswerError = ({ error, onRetry }: Props) => {
12 | return (
13 |
14 |
15 |
16 |
17 | {error}
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Answer/AnswerIcon.tsx:
--------------------------------------------------------------------------------
1 | import { Sparkle28Filled } from "@fluentui/react-icons";
2 |
3 | export const AnswerIcon = () => {
4 | return ;
5 | };
6 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Answer/AnswerLoading.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from "@fluentui/react";
2 | import { animated, useSpring } from "@react-spring/web";
3 |
4 | import styles from "./Answer.module.css";
5 | import { AnswerIcon } from "./AnswerIcon";
6 |
7 | export const AnswerLoading = () => {
8 | const animatedStyles = useSpring({
9 | from: { opacity: 0 },
10 | to: { opacity: 1 }
11 | });
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | Generating answer
20 |
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Answer/AnswerParser.tsx:
--------------------------------------------------------------------------------
1 | import { renderToStaticMarkup } from "react-dom/server";
2 | import { getCitationFilePath } from "../../api";
3 |
4 | type HtmlParsedAnswer = {
5 | answerHtml: string;
6 | citations: string[];
7 | followupQuestions: string[];
8 | };
9 |
10 | export function parseAnswerToHtml(answer: string, isStreaming: boolean, onCitationClicked: (citationFilePath: string) => void): HtmlParsedAnswer {
11 | const citations: string[] = [];
12 | const followupQuestions: string[] = [];
13 |
14 | // Extract any follow-up questions that might be in the answer
15 | let parsedAnswer = answer.replace(/<<([^>>]+)>>/g, (match, content) => {
16 | followupQuestions.push(content);
17 | return "";
18 | });
19 |
20 | // trim any whitespace from the end of the answer after removing follow-up questions
21 | parsedAnswer = parsedAnswer.trim();
22 |
23 | // Omit a citation that is still being typed during streaming
24 | if (isStreaming) {
25 | let lastIndex = parsedAnswer.length;
26 | for (let i = parsedAnswer.length - 1; i >= 0; i--) {
27 | if (parsedAnswer[i] === "]") {
28 | break;
29 | } else if (parsedAnswer[i] === "[") {
30 | lastIndex = i;
31 | break;
32 | }
33 | }
34 | const truncatedAnswer = parsedAnswer.substring(0, lastIndex);
35 | parsedAnswer = truncatedAnswer;
36 | }
37 |
38 | const parts = parsedAnswer.split(/\[([^\]]+)\]/g);
39 |
40 | const fragments: string[] = parts.map((part, index) => {
41 | if (index % 2 === 0) {
42 | return part;
43 | } else {
44 | let citationIndex: number;
45 | if (citations.indexOf(part) !== -1) {
46 | citationIndex = citations.indexOf(part) + 1;
47 | } else {
48 | citations.push(part);
49 | citationIndex = citations.length;
50 | }
51 |
52 | const path = getCitationFilePath(part);
53 |
54 | return renderToStaticMarkup(
55 | onCitationClicked(path)}>
56 | {citationIndex}
57 |
58 | );
59 | }
60 | });
61 |
62 | return {
63 | answerHtml: fragments.join(""),
64 | citations,
65 | followupQuestions
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Answer/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Answer";
2 | export * from "./AnswerLoading";
3 | export * from "./AnswerError";
4 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ClearChatButton/ClearChatButton.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | gap: 6px;
5 | cursor: pointer;
6 | }
7 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ClearChatButton/ClearChatButton.tsx:
--------------------------------------------------------------------------------
1 | import { Delete24Regular } from "@fluentui/react-icons";
2 | import { Button } from "@fluentui/react-components";
3 |
4 | import styles from "./ClearChatButton.module.css";
5 |
6 | interface Props {
7 | className?: string;
8 | onClick: () => void;
9 | disabled?: boolean;
10 | }
11 |
12 | export const ClearChatButton = ({ className, disabled, onClick }: Props) => {
13 | return (
14 |
15 | } disabled={disabled} onClick={onClick}>
16 | {"Clear chat"}
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/app/frontend/src/components/ClearChatButton/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./ClearChatButton";
2 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Example/Example.module.css:
--------------------------------------------------------------------------------
1 | .examplesNavList {
2 | list-style: none;
3 | padding-left: 0;
4 | display: flex;
5 | flex-wrap: wrap;
6 | gap: 10px;
7 | flex: 1;
8 | justify-content: center;
9 | }
10 |
11 | .example {
12 | word-break: break-word;
13 | background: #dbdbdb;
14 | border-radius: 8px;
15 | display: flex;
16 | flex-direction: column;
17 | padding: 20px;
18 | margin-bottom: 5px;
19 | cursor: pointer;
20 | }
21 |
22 | .example:hover {
23 | box-shadow:
24 | 0px 8px 16px rgba(0, 0, 0, 0.14),
25 | 0px 0px 2px rgba(0, 0, 0, 0.12);
26 | outline: 2px solid rgba(115, 118, 225, 1);
27 | }
28 |
29 | .exampleText {
30 | margin: 0;
31 | font-size: 22px;
32 | width: 280px;
33 | height: 100px;
34 | }
35 |
36 | @media only screen and (max-height: 780px) {
37 | .exampleText {
38 | font-size: 20px;
39 | height: 80px;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Example/Example.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./Example.module.css";
2 |
3 | interface Props {
4 | text: string;
5 | value: string;
6 | onClick: (value: string) => void;
7 | }
8 |
9 | export const Example = ({ text, value, onClick }: Props) => {
10 | return (
11 | onClick(value)}>
12 |
{text}
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Example/ExampleList.tsx:
--------------------------------------------------------------------------------
1 | import { Example } from "./Example";
2 |
3 | import styles from "./Example.module.css";
4 |
5 | export type ExampleModel = {
6 | text: string;
7 | value: string;
8 | };
9 |
10 | const EXAMPLES: ExampleModel[] = [
11 | {
12 | text: "What is included in my Northwind Health Plus plan that is not in standard?",
13 | value: "What is included in my Northwind Health Plus plan that is not in standard?"
14 | },
15 | { text: "What happens in a performance review?", value: "What happens in a performance review?" },
16 | { text: "What does a Product Manager do?", value: "What does a Product Manager do?" }
17 | ];
18 |
19 | interface Props {
20 | onExampleClicked: (value: string) => void;
21 | }
22 |
23 | export const ExampleList = ({ onExampleClicked }: Props) => {
24 | return (
25 |
26 | {EXAMPLES.map((x, i) => (
27 | -
28 |
29 |
30 | ))}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/app/frontend/src/components/Example/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./Example";
2 | export * from "./ExampleList";
3 |
--------------------------------------------------------------------------------
/app/frontend/src/components/LoginButton/LoginButton.module.css:
--------------------------------------------------------------------------------
1 | .loginButton {
2 | border-radius: 5px;
3 | padding: 30px 30px;
4 | font-weight: 100;
5 | }
6 |
--------------------------------------------------------------------------------
/app/frontend/src/components/LoginButton/LoginButton.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultButton } from "@fluentui/react";
2 | import { useMsal } from "@azure/msal-react";
3 |
4 | import styles from "./LoginButton.module.css";
5 | import { getRedirectUri, loginRequest } from "../../authConfig";
6 |
7 | export const LoginButton = () => {
8 | const { instance } = useMsal();
9 | const activeAccount = instance.getActiveAccount();
10 | const handleLoginPopup = () => {
11 | /**
12 | * When using popup and silent APIs, we recommend setting the redirectUri to a blank page or a page
13 | * that does not implement MSAL. Keep in mind that all redirect routes must be registered with the application
14 | * For more information, please follow this link: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/login-user.md#redirecturi-considerations
15 | */
16 | instance
17 | .loginPopup({
18 | ...loginRequest,
19 | redirectUri: getRedirectUri()
20 | })
21 | .catch(error => console.log(error));
22 | };
23 | const handleLogoutPopup = () => {
24 | instance
25 | .logoutPopup({
26 | mainWindowRedirectUri: "/", // redirects the top level app after logout
27 | account: instance.getActiveAccount()
28 | })
29 | .catch(error => console.log(error));
30 | };
31 | const logoutText = `Logout\n${activeAccount?.username}`;
32 | return (
33 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/app/frontend/src/components/LoginButton/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./LoginButton";
2 |
--------------------------------------------------------------------------------
/app/frontend/src/components/QuestionInput/QuestionInput.module.css:
--------------------------------------------------------------------------------
1 | .questionInputContainer {
2 | border-radius: 8px;
3 | box-shadow:
4 | 0px 8px 16px rgba(0, 0, 0, 0.14),
5 | 0px 0px 2px rgba(0, 0, 0, 0.12);
6 | height: 90px;
7 | width: 100%;
8 | padding: 15px;
9 | background: white;
10 | }
11 |
12 | .questionInputTextArea {
13 | width: 100%;
14 | line-height: 40px;
15 | }
16 |
17 | .questionInputButtonsContainer {
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: flex-end;
21 | }
22 |
--------------------------------------------------------------------------------
/app/frontend/src/components/QuestionInput/QuestionInput.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Stack, TextField } from "@fluentui/react";
3 | import { Button, Tooltip, Field, Textarea } from "@fluentui/react-components";
4 | import { Send28Filled } from "@fluentui/react-icons";
5 |
6 | import styles from "./QuestionInput.module.css";
7 |
8 | interface Props {
9 | onSend: (question: string) => void;
10 | disabled: boolean;
11 | placeholder?: string;
12 | clearOnSend?: boolean;
13 | }
14 |
15 | export const QuestionInput = ({ onSend, disabled, placeholder, clearOnSend }: Props) => {
16 | const [question, setQuestion] = useState("");
17 |
18 | const sendQuestion = () => {
19 | if (disabled || !question.trim()) {
20 | return;
21 | }
22 |
23 | onSend(question);
24 |
25 | if (clearOnSend) {
26 | setQuestion("");
27 | }
28 | };
29 |
30 | const onEnterPress = (ev: React.KeyboardEvent) => {
31 | if (ev.key === "Enter" && !ev.shiftKey) {
32 | ev.preventDefault();
33 | sendQuestion();
34 | }
35 | };
36 |
37 | const onQuestionChange = (_ev: React.FormEvent, newValue?: string) => {
38 | if (!newValue) {
39 | setQuestion("");
40 | } else if (newValue.length <= 1000) {
41 | setQuestion(newValue);
42 | }
43 | };
44 |
45 | const sendQuestionDisabled = disabled || !question.trim();
46 |
47 | return (
48 |
49 |
59 |
60 |
61 | } disabled={sendQuestionDisabled} onClick={sendQuestion} />
62 |
63 |
64 |
65 | );
66 | };
67 |
--------------------------------------------------------------------------------
/app/frontend/src/components/QuestionInput/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./QuestionInput";
2 |
--------------------------------------------------------------------------------
/app/frontend/src/components/SettingsButton/SettingsButton.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | gap: 6px;
5 | cursor: pointer;
6 | }
7 |
--------------------------------------------------------------------------------
/app/frontend/src/components/SettingsButton/SettingsButton.tsx:
--------------------------------------------------------------------------------
1 | import { Settings24Regular } from "@fluentui/react-icons";
2 | import { Button } from "@fluentui/react-components";
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 |
14 | } onClick={onClick}>
15 | {"Developer settings"}
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/app/frontend/src/components/SettingsButton/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./SettingsButton";
2 |
--------------------------------------------------------------------------------
/app/frontend/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 |
--------------------------------------------------------------------------------
/app/frontend/src/components/SupportingContent/SupportingContent.tsx:
--------------------------------------------------------------------------------
1 | import { parseSupportingContentItem } from "./SupportingContentParser";
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 |
--------------------------------------------------------------------------------
/app/frontend/src/components/SupportingContent/SupportingContentParser.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 |
--------------------------------------------------------------------------------
/app/frontend/src/components/SupportingContent/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./SupportingContent";
2 |
--------------------------------------------------------------------------------
/app/frontend/src/components/TokenClaimsDisplay/index.tsx:
--------------------------------------------------------------------------------
1 | export * from "./TokenClaimsDisplay";
2 |
--------------------------------------------------------------------------------
/app/frontend/src/components/UserChatMessage/UserChatMessage.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | justify-content: flex-end;
4 | margin-bottom: 20px;
5 | max-width: 80%;
6 | margin-left: auto;
7 | }
8 |
9 | .message {
10 | padding: 20px;
11 | background: #e8ebfa;
12 | border-radius: 8px;
13 | box-shadow:
14 | 0px 2px 4px rgba(0, 0, 0, 0.14),
15 | 0px 0px 2px rgba(0, 0, 0, 0.12);
16 | outline: transparent solid 1px;
17 | }
18 |
--------------------------------------------------------------------------------
/app/frontend/src/components/UserChatMessage/UserChatMessage.tsx:
--------------------------------------------------------------------------------
1 | import styles from "./UserChatMessage.module.css";
2 |
3 | interface Props {
4 | message: string;
5 | }
6 |
7 | export const UserChatMessage = ({ message }: Props) => {
8 | return (
9 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/app/frontend/src/components/UserChatMessage/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./UserChatMessage";
2 |
--------------------------------------------------------------------------------
/app/frontend/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 | html {
13 | background: #f2f2f2;
14 |
15 | font-family:
16 | "Segoe UI",
17 | -apple-system,
18 | BlinkMacSystemFont,
19 | "Roboto",
20 | "Oxygen",
21 | "Ubuntu",
22 | "Cantarell",
23 | "Fira Sans",
24 | "Droid Sans",
25 | "Helvetica Neue",
26 | sans-serif;
27 | -webkit-font-smoothing: antialiased;
28 | -moz-osx-font-smoothing: grayscale;
29 | }
30 |
31 | #root {
32 | height: 100%;
33 | }
34 |
--------------------------------------------------------------------------------
/app/frontend/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 | import { MsalProvider } from "@azure/msal-react";
6 | import { PublicClientApplication, EventType, AccountInfo } from "@azure/msal-browser";
7 | import { msalConfig, useLogin } from "./authConfig";
8 |
9 | import "./index.css";
10 |
11 | import Layout from "./pages/layout/Layout";
12 | import Chat from "./pages/chat/Chat";
13 |
14 | var layout;
15 | if (useLogin) {
16 | var msalInstance = new PublicClientApplication(msalConfig);
17 |
18 | // Default to using the first account if no account is active on page load
19 | if (!msalInstance.getActiveAccount() && msalInstance.getAllAccounts().length > 0) {
20 | // Account selection logic is app dependent. Adjust as needed for different use cases.
21 | msalInstance.setActiveAccount(msalInstance.getActiveAccount());
22 | }
23 |
24 | // Listen for sign-in event and set active account
25 | msalInstance.addEventCallback(event => {
26 | if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
27 | const account = event.payload as AccountInfo;
28 | msalInstance.setActiveAccount(account);
29 | }
30 | });
31 |
32 | layout = (
33 |
34 |
35 |
36 | );
37 | } else {
38 | layout = ;
39 | }
40 |
41 | initializeIcons();
42 |
43 | const router = createHashRouter([
44 | {
45 | path: "/",
46 | element: layout,
47 | children: [
48 | {
49 | index: true,
50 | element:
51 | },
52 | {
53 | path: "qa",
54 | lazy: () => import("./pages/oneshot/OneShot")
55 | },
56 | {
57 | path: "*",
58 | lazy: () => import("./pages/NoPage")
59 | }
60 | ]
61 | }
62 | ]);
63 |
64 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
65 |
66 |
67 |
68 | );
69 |
--------------------------------------------------------------------------------
/app/frontend/src/pages/NoPage.tsx:
--------------------------------------------------------------------------------
1 | export function Component(): JSX.Element {
2 | return 404
;
3 | }
4 |
5 | Component.displayName = "NoPage";
6 |
--------------------------------------------------------------------------------
/app/frontend/src/pages/chat/Chat.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | flex: 1;
3 | display: flex;
4 | flex-direction: column;
5 | margin-top: 20px;
6 | }
7 |
8 | .chatRoot {
9 | flex: 1;
10 | display: flex;
11 | }
12 |
13 | .chatContainer {
14 | flex: 1;
15 | display: flex;
16 | flex-direction: column;
17 | align-items: center;
18 | width: 100%;
19 | }
20 |
21 | .chatEmptyState {
22 | flex-grow: 1;
23 | display: flex;
24 | flex-direction: column;
25 | justify-content: center;
26 | align-items: center;
27 | max-height: 1024px;
28 | padding-top: 60px;
29 | }
30 |
31 | .chatEmptyStateTitle {
32 | font-size: 4rem;
33 | font-weight: 600;
34 | margin-top: 0;
35 | margin-bottom: 30px;
36 | }
37 |
38 | .chatEmptyStateSubtitle {
39 | font-weight: 600;
40 | margin-bottom: 10px;
41 | }
42 |
43 | @media only screen and (max-height: 780px) {
44 | .chatEmptyState {
45 | padding-top: 0;
46 | }
47 |
48 | .chatEmptyStateTitle {
49 | font-size: 3rem;
50 | margin-bottom: 0px;
51 | }
52 | }
53 |
54 | .chatMessageStream {
55 | flex-grow: 1;
56 | max-height: 1024px;
57 | max-width: 1028px;
58 | width: 100%;
59 | overflow-y: auto;
60 | padding-left: 24px;
61 | padding-right: 24px;
62 | display: flex;
63 | flex-direction: column;
64 | }
65 |
66 | .chatMessageGpt {
67 | margin-bottom: 20px;
68 | max-width: 80%;
69 | display: flex;
70 | min-width: 500px;
71 | }
72 |
73 | .chatMessageGptMinWidth {
74 | max-width: 500px;
75 | margin-bottom: 20px;
76 | }
77 |
78 | .chatInput {
79 | position: sticky;
80 | bottom: 0;
81 | flex: 0 0 100px;
82 | padding-top: 12px;
83 | padding-bottom: 24px;
84 | padding-left: 24px;
85 | padding-right: 24px;
86 | width: 100%;
87 | max-width: 1028px;
88 | background: #f2f2f2;
89 | }
90 |
91 | .chatAnalysisPanel {
92 | flex: 1;
93 | overflow-y: auto;
94 | max-height: 89vh;
95 | margin-left: 20px;
96 | margin-right: 20px;
97 | }
98 |
99 | .chatSettingsSeparator {
100 | margin-top: 15px;
101 | }
102 |
103 | .loadingLogo {
104 | font-size: 28px;
105 | }
106 |
107 | .commandsContainer {
108 | display: flex;
109 | align-self: flex-end;
110 | }
111 |
112 | .commandButton {
113 | margin-right: 20px;
114 | margin-bottom: 20px;
115 | }
116 |
--------------------------------------------------------------------------------
/app/frontend/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 |
--------------------------------------------------------------------------------
/app/frontend/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 |
--------------------------------------------------------------------------------
/app/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/app/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": true,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "types": ["vite/client"]
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------
/app/frontend/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | build: {
8 | outDir: "./build",
9 | emptyOutDir: true,
10 | sourcemap: true,
11 | rollupOptions: {
12 | output: {
13 | manualChunks: id => {
14 | if (id.includes("@fluentui/react-icons")) {
15 | return "fluentui-icons";
16 | } else if (id.includes("@fluentui/react")) {
17 | return "fluentui-react";
18 | } else if (id.includes("node_modules")) {
19 | return "vendor";
20 | }
21 | }
22 | }
23 | },
24 | target: "esnext"
25 | },
26 | server: {
27 | proxy: {
28 | "/api/ask": {
29 | target: 'http://localhost:8080',
30 | changeOrigin: true
31 | },
32 | "/api/chat": {
33 | target: 'http://localhost:8080',
34 | changeOrigin: true
35 | },
36 | "/api/content": {
37 | target: 'http://localhost:8080',
38 | changeOrigin: true
39 | },
40 | "/api/auth_setup": {
41 | target: 'http://localhost:8080',
42 | changeOrigin: true
43 | }
44 | }
45 | }
46 | });
47 |
--------------------------------------------------------------------------------
/app/indexer/.mvn/jvm.config:
--------------------------------------------------------------------------------
1 | --add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
2 | --add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
3 | --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
4 | --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
5 |
--------------------------------------------------------------------------------
/app/indexer/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/app/indexer/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/app/indexer/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.2/maven-wrapper-0.5.2.tar.gz
3 |
--------------------------------------------------------------------------------
/app/indexer/cli/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | com.microsoft.openai.samples
9 | indexer-parent
10 | 1.4.0-SNAPSHOT
11 | ../pom.xml
12 |
13 |
14 | indexer-cli
15 |
16 |
17 |
18 | info.picocli
19 | picocli
20 | ${picocli.version}
21 |
22 |
23 | com.microsoft.openai.samples
24 | indexer-core
25 | 1.4.0-SNAPSHOT
26 |
27 |
28 |
29 |
30 |
31 |
32 | org.apache.maven.plugins
33 | maven-shade-plugin
34 | 3.5.1
35 |
36 |
37 | package
38 |
39 | shade
40 |
41 |
42 |
43 |
44 | com.microsoft.openai.samples.indexer.CLI
45 |
46 |
47 |
48 |
49 | *:*
50 |
51 | META-INF/*.SF
52 | META-INF/*.DSA
53 | META-INF/*.RSA
54 |
55 |
56 |
57 | cli
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/app/indexer/cli/src/main/java/com/microsoft/openai/samples/indexer/AddCommand.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer;
2 |
3 | import com.microsoft.openai.samples.indexer.index.SearchIndexManager;
4 | import com.microsoft.openai.samples.indexer.storage.BlobManager;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import java.nio.file.Files;
9 | import java.nio.file.Path;
10 |
11 | public class AddCommand {
12 |
13 | private static final Logger logger = LoggerFactory.getLogger(AddCommand.class);
14 | private final SearchIndexManager searchIndexManager;
15 | private final BlobManager blobManager;
16 | private final DocumentProcessor documentProcessor;
17 |
18 | public AddCommand(SearchIndexManager searchIndexManager, BlobManager blobManager, DocumentProcessor documentProcessor) {
19 | this.searchIndexManager = searchIndexManager;
20 | this.blobManager = blobManager;
21 | this.documentProcessor = documentProcessor;
22 | }
23 |
24 | public void run(Path path,String category){
25 |
26 | searchIndexManager.createIndex();
27 |
28 | if(Files.isDirectory(path))
29 | processDirectory(path,category);
30 | else
31 | processFile(path,category);
32 | }
33 |
34 | private void processDirectory(Path directory, String category) {
35 | logger.debug("Processing directory {}", directory);
36 | try {
37 | Files.newDirectoryStream(directory).forEach(path -> {
38 | processFile(path,category);
39 | });
40 | logger.debug("All files in directory {} processed", directory.toRealPath().toString());
41 | } catch (Exception e) {
42 | throw new RuntimeException("Error processing folder ",e);
43 | }
44 | }
45 |
46 | private void processFile(Path path,String category) {
47 | try {
48 | String absoluteFilePath = path.toRealPath().toString();
49 | documentProcessor.indexDocumentfromFile(absoluteFilePath,category);
50 | logger.debug("file {} indexed", absoluteFilePath);
51 | blobManager.uploadBlob(path.toFile());
52 | logger.debug("file {} uploaded", absoluteFilePath);
53 | } catch (Exception e) {
54 | throw new RuntimeException("Error processing file ",e);
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/indexer/cli/src/main/java/com/microsoft/openai/samples/indexer/UploadCommand.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer;
2 |
3 | import com.microsoft.openai.samples.indexer.index.SearchIndexManager;
4 | import com.microsoft.openai.samples.indexer.storage.BlobManager;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 |
8 | import java.nio.file.Files;
9 | import java.nio.file.Path;
10 |
11 | public class UploadCommand {
12 |
13 | private static final Logger logger = LoggerFactory.getLogger(UploadCommand.class);
14 | private final SearchIndexManager searchIndexManager;
15 | private final BlobManager blobManager;
16 |
17 | public UploadCommand(SearchIndexManager searchIndexManager, BlobManager blobManager) {
18 | this.searchIndexManager = searchIndexManager;
19 | this.blobManager = blobManager;
20 | }
21 |
22 | public void run(Path path,String category){
23 |
24 | searchIndexManager.createIndex();
25 |
26 | if(Files.isDirectory(path))
27 | uploadDirectory(path);
28 | else
29 | uploadFile(path);
30 | }
31 |
32 | private void uploadDirectory( Path directory) {
33 | logger.debug("Uploading directory {}", directory);
34 | try {
35 | Files.newDirectoryStream(directory).forEach(path -> {
36 | uploadFile(path);
37 | });
38 | logger.debug("All files in directory {} have been uploaded", directory.toRealPath().toString());
39 | } catch (Exception e) {
40 | throw new RuntimeException("Error processing folder ",e);
41 | }
42 | }
43 |
44 | private void uploadFile(Path path) {
45 | try {
46 | String absoluteFilePath = path.toRealPath().toString();
47 | blobManager.uploadBlob(path.toFile());
48 | logger.debug("file {} uploaded", absoluteFilePath);
49 | } catch (Exception e) {
50 | throw new RuntimeException("Error processing file ",e);
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/indexer/cli/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/indexer/core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 |
8 | com.microsoft.openai.samples
9 | indexer-parent
10 | 1.4.0-SNAPSHOT
11 | ../pom.xml
12 |
13 |
14 | indexer-core
15 |
16 |
17 |
18 |
19 |
20 | com.azure
21 | azure-core
22 |
23 |
24 |
25 | com.azure
26 | azure-identity
27 |
28 |
29 |
30 |
31 | com.azure
32 | azure-storage-blob
33 |
34 |
35 |
36 | com.azure
37 | azure-search-documents
38 |
39 |
40 | com.azure
41 | azure-core-serializer-json-jackson
42 |
43 |
44 |
45 |
46 |
47 | com.azure
48 | azure-ai-openai
49 | ${azure-openai.version}
50 |
51 |
52 | com.azure
53 | azure-ai-formrecognizer
54 |
55 |
56 |
57 | com.knuddels
58 | jtokkit
59 | 0.6.1
60 |
61 |
62 | com.itextpdf
63 | itextpdf
64 | ${itextpdf.version}
65 |
66 |
67 | org.apache.commons
68 | commons-text
69 | ${apache.common.text}
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/Section.java:
--------------------------------------------------------------------------------
1 |
2 | package com.microsoft.openai.samples.indexer;
3 |
4 | import java.nio.charset.StandardCharsets;
5 | import java.util.regex.Pattern;
6 | import java.util.Base64;
7 |
8 | public class Section {
9 | private SplitPage splitPage;
10 | private String filename;
11 | private String category;
12 |
13 | public Section(SplitPage splitPage, String filename, String category) {
14 | this.splitPage = splitPage;
15 | this.filename = filename;
16 | this.category = category;
17 | }
18 |
19 | public SplitPage getSplitPage() {
20 | return splitPage;
21 | }
22 |
23 | public String getFilename() {
24 | return filename;
25 | }
26 |
27 | public String getCategory() {
28 | return category;
29 | }
30 |
31 | public String getFilenameToId() {
32 | String filenameAscii = Pattern.compile("[^0-9a-zA-Z_-]").matcher(filename).replaceAll("_");
33 | String filenameHash = Base64.getEncoder().encodeToString(filename.getBytes(StandardCharsets.UTF_8));
34 | return "file-" + filenameAscii + "-" + filenameHash;
35 |
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/SplitPage.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer;
2 |
3 | public class SplitPage {
4 | private int pageNum;
5 | private String text;
6 |
7 | public SplitPage(int pageNum, String text) {
8 | this.pageNum = pageNum;
9 | this.text = text;
10 | }
11 |
12 | public int getPageNum() {
13 | return pageNum;
14 | }
15 |
16 | public String getText() {
17 | return text;
18 | }
19 | }
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/embeddings/AzureOpenAIEmbeddingService.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.embeddings;
2 |
3 | import com.azure.ai.openai.OpenAIAsyncClient;
4 | import com.azure.ai.openai.OpenAIClientBuilder;
5 | import com.azure.core.credential.TokenCredential;
6 | import com.azure.core.http.policy.HttpLogDetailLevel;
7 | import com.azure.core.http.policy.HttpLogOptions;
8 |
9 | public class AzureOpenAIEmbeddingService extends AbstractTextEmbeddingsService {
10 | private String openAIServiceName;
11 | private TokenCredential tokenCredential;
12 |
13 | public AzureOpenAIEmbeddingService(String openAIServiceName, String openAiDeploymentName, TokenCredential tokenCredential, boolean verbose) {
14 | //current Azure OpenAI Embeddings service limit are 16 batch items per request 8192 tokens
15 | super(openAiDeploymentName, verbose, 16, 8192);
16 | this.openAIServiceName = openAIServiceName;
17 | this.tokenCredential = tokenCredential;
18 | }
19 |
20 | @Override
21 | protected OpenAIAsyncClient createClient() {
22 | String endpoint = "https://%s.openai.azure.com".formatted(openAIServiceName);
23 |
24 |
25 | var httpLogOptions = new HttpLogOptions();
26 | if(verbose){
27 | // still not sure to include the http log if verbose is true.
28 | // httpLogOptions.setPrettyPrintBody(true);
29 | // httpLogOptions.setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS);
30 | }
31 |
32 | return new OpenAIClientBuilder()
33 | .endpoint(endpoint)
34 | .credential(tokenCredential)
35 | .httpLogOptions(httpLogOptions)
36 | .buildAsyncClient();
37 | }
38 |
39 |
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/embeddings/EmbeddingBatch.java:
--------------------------------------------------------------------------------
1 |
2 | package com.microsoft.openai.samples.indexer.embeddings;
3 |
4 | import java.util.List;
5 |
6 | public class EmbeddingBatch {
7 | private List texts;
8 | private int tokenLength;
9 |
10 | public EmbeddingBatch(List texts, int tokenLength) {
11 | this.texts = texts;
12 | this.tokenLength = tokenLength;
13 | }
14 |
15 | public List getTexts() {
16 | return texts;
17 | }
18 |
19 | public int getTokenLength() {
20 | return tokenLength;
21 | }
22 | }
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/embeddings/EmbeddingData.java:
--------------------------------------------------------------------------------
1 |
2 | package com.microsoft.openai.samples.indexer.embeddings;
3 |
4 | import java.util.List; // Add the missing import statement
5 |
6 | public class EmbeddingData {
7 | private List embedding;
8 |
9 | public List getEmbedding() {
10 | return embedding;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/embeddings/EmbeddingResponse.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.embeddings;
2 |
3 | import java.util.List;
4 |
5 | public class EmbeddingResponse {
6 | private List data;
7 |
8 | public List getData() {
9 | return data;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/embeddings/TextEmbeddingsService.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.embeddings;
2 |
3 | import java.util.List;
4 |
5 | public interface TextEmbeddingsService {
6 |
7 | public List> createEmbeddingBatch(List texts);
8 | }
9 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/index/AzureSearchClientFactory.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.index;
2 |
3 | import com.azure.core.credential.TokenCredential;
4 | import com.azure.search.documents.SearchClient;
5 | import com.azure.search.documents.indexes.SearchIndexClient;
6 | import com.azure.search.documents.indexes.SearchIndexerClient;
7 | import com.azure.search.documents.SearchClientBuilder;
8 | import com.azure.search.documents.indexes.SearchIndexClientBuilder;
9 | import com.azure.search.documents.indexes.SearchIndexerClientBuilder;
10 |
11 | public class AzureSearchClientFactory {
12 | /**
13 | * Class representing a connection to a search service.
14 | * To learn more, please visit https://learn.microsoft.com/azure/search/search-what-is-azure-search
15 | */
16 | private final String endpoint;
17 | private final TokenCredential credential;
18 | private final String indexName;
19 | private final boolean verbose;
20 |
21 | public AzureSearchClientFactory(String serviceName, TokenCredential credential, String indexName, boolean verbose) {
22 | this.endpoint = "https://%s.search.windows.net".formatted(serviceName);
23 | this.credential = credential;
24 | this.indexName = indexName;
25 | this.verbose = verbose;
26 | }
27 |
28 | public SearchClient createSearchClient() {
29 | return new SearchClientBuilder()
30 | .endpoint(endpoint)
31 | .credential(credential)
32 | .indexName(indexName)
33 | .buildClient();
34 | }
35 |
36 | public SearchIndexClient createSearchIndexClient() {
37 | return new SearchIndexClientBuilder()
38 | .endpoint(endpoint)
39 | .credential(credential)
40 | .buildClient();
41 | }
42 |
43 | public SearchIndexerClient createSearchIndexerClient() {
44 | return new SearchIndexerClientBuilder()
45 | .endpoint(endpoint)
46 | .credential(credential)
47 | .buildClient();
48 | }
49 |
50 | public String getEndpoint() {
51 | return endpoint;
52 | }
53 |
54 | public TokenCredential getCredential() {
55 | return credential;
56 | }
57 |
58 | public String getIndexName() {
59 | return indexName;
60 | }
61 |
62 | public boolean isVerbose() {
63 | return verbose;
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/parser/ItextPDFParser.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.parser;
2 |
3 | import com.itextpdf.text.pdf.PdfReader;
4 | import com.itextpdf.text.pdf.parser.PdfTextExtractor;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.util.List;
9 | import java.util.ArrayList;
10 |
11 | /**
12 | * This is an implementation of a PDF parser using open source iText library.
13 | * It can only handle text within pdf.
14 | * Can't extract data from tables within images. See @DocumentIntelligencePDFParser for that.
15 | */
16 | public class ItextPDFParser implements PDFParser {
17 | @Override
18 | public List parse(File file) {
19 | List pages = new ArrayList<>();
20 | PdfReader reader = null;
21 |
22 | try {
23 | reader = new PdfReader(file.getAbsolutePath());
24 | Integer offset = 0;
25 | for (int i = 1; i <= reader.getNumberOfPages(); i++) {
26 | String pageText = PdfTextExtractor.getTextFromPage(reader, i);
27 | Page page = new Page(i, offset, pageText);
28 | offset += pageText.length();
29 | pages.add(page);
30 | }
31 | } catch (IOException e) {
32 | throw new RuntimeException(e);
33 | } finally {
34 | if (reader != null) {
35 | reader.close();
36 | }
37 | }
38 | return pages;
39 | }
40 |
41 | @Override
42 | public List parse(byte[] content) {
43 | List pages = new ArrayList<>();
44 | PdfReader reader = null;
45 |
46 | try {
47 | reader = new PdfReader(content);
48 | Integer offset = 0;
49 | for (int i = 1; i <= reader.getNumberOfPages(); i++) {
50 | String pageText = PdfTextExtractor.getTextFromPage(reader, i);
51 | Page page = new Page(i, offset, pageText);
52 | offset += pageText.length();
53 | pages.add(page);
54 | }
55 | } catch (IOException e) {
56 | throw new RuntimeException(e);
57 | } finally {
58 | if (reader != null) {
59 | reader.close();
60 | }
61 | }
62 | return pages;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/parser/PDFParser.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.parser;
2 |
3 | import java.io.File;
4 | import java.util.List;
5 |
6 |
7 | public interface PDFParser {
8 |
9 | public List parse(File file);
10 | public List parse(byte[] content);
11 | }
--------------------------------------------------------------------------------
/app/indexer/core/src/main/java/com/microsoft/openai/samples/indexer/parser/Page.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.parser;
2 |
3 | public class Page {
4 | /**
5 | * A single page from a pdf
6 | *
7 | * Attributes:
8 | * page_num (int): Page number
9 | * offset (int): If the text of the entire PDF was concatenated into a single string, the index of the first character on the page. For example, if page 1 had the text "hello" and page 2 had the text "world", the offset of page 2 is 5 ("hellow")
10 | * text (str): The text of the page
11 | */
12 |
13 | private int page_num;
14 | private int offset;
15 | private String text;
16 |
17 | public Page(int page_num, int offset, String text) {
18 | this.page_num = page_num;
19 | this.offset = offset;
20 | this.text = text;
21 | }
22 |
23 | public int getPageNum() {
24 | return page_num;
25 | }
26 |
27 | public int getOffset() {
28 | return offset;
29 | }
30 |
31 | public String getText() {
32 | return text;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/indexer/core/src/test/java/com/microsoft/openai/samples/indexer/parser/TextSplitterTest.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.parser;
2 |
3 | import com.microsoft.openai.samples.indexer.SplitPage;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.util.List;
7 |
8 | import static org.junit.jupiter.api.Assertions.*;
9 |
10 | class TextSplitterTest {
11 |
12 | @Test
13 | void testSplitTinyPages() {
14 | List testPages = List.of(new Page[]{
15 | new Page(1, 0, "hello, world")
16 | });
17 | TextSplitter splitter = new TextSplitter(false);
18 | List result = splitter.splitPages(testPages);
19 | assertEquals(1, result.size());
20 | assertEquals("hello, world", result.get(0).getText());
21 | }
22 | }
--------------------------------------------------------------------------------
/app/indexer/functions/.gitignore:
--------------------------------------------------------------------------------
1 | # Build output
2 | target/
3 | *.class
4 |
5 | # Log file
6 | *.log
7 |
8 | # BlueJ files
9 | *.ctxt
10 |
11 | # Mobile Tools for Java (J2ME)
12 | .mtj.tmp/
13 |
14 | # Package Files #
15 | *.jar
16 | *.war
17 | *.ear
18 | *.zip
19 | *.tar.gz
20 | *.rar
21 |
22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
23 | hs_err_pid*
24 |
25 | # IDE
26 | .idea/
27 | *.iml
28 | .settings/
29 | .project
30 | .classpath
31 | .vscode/
32 |
33 | # macOS
34 | .DS_Store
35 |
36 | # Azure Functions
37 | local.settings.json
38 | bin/
39 | obj/
40 |
--------------------------------------------------------------------------------
/app/indexer/functions/host.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0",
3 | "extensionBundle": {
4 | "id": "Microsoft.Azure.Functions.ExtensionBundle",
5 | "version": "[4.0.0, 5.0.0)"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/indexer/functions/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/indexer/manifests/indexer-deployment.tmpl.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: indexer-deployment
5 | namespace: azure-open-ai
6 | labels:
7 | app: indexer
8 | spec:
9 | replicas: 1
10 | selector:
11 | matchLabels:
12 | app: indexer
13 | template:
14 | metadata:
15 | labels:
16 | app: indexer
17 | spec:
18 | containers:
19 | - name: indexer
20 | image: {{.Env.SERVICE_INDEXER_IMAGE_NAME}}
21 | imagePullPolicy: IfNotPresent
22 | ports:
23 | - containerPort: 8080
24 | envFrom:
25 | - configMapRef:
26 | name: azd-env-configmap
27 | resources:
28 | requests:
29 | memory: "2Gi"
30 |
--------------------------------------------------------------------------------
/app/indexer/manifests/indexer-service.yml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Service
3 | metadata:
4 | name: indexer-service
5 | namespace: azure-open-ai
6 | spec:
7 | type: ClusterIP
8 | ports:
9 | - protocol: TCP
10 | port: 80
11 | targetPort: 8080
12 | selector:
13 | app: indexer
14 |
--------------------------------------------------------------------------------
/app/indexer/microservice/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/openjdk/jdk:17-mariner AS build
2 |
3 | WORKDIR /workspace/app
4 | EXPOSE 3100
5 |
6 | COPY mvnw .
7 | COPY .mvn .mvn
8 | COPY pom.xml .
9 | COPY cli cli
10 | COPY core core
11 | COPY microservice microservice
12 | COPY functions functions
13 |
14 | RUN chmod +x ./mvnw
15 | # Convert CRLF to LF
16 | RUN sed -i 's/\r$//' ./mvnw
17 | RUN ./mvnw package -DskipTests
18 | RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../../microservice/target/*.jar)
19 |
20 | FROM mcr.microsoft.com/openjdk/jdk:17-mariner
21 |
22 | ARG DEPENDENCY=/workspace/app/target/dependency
23 | COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
24 | COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
25 | COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
26 |
27 | RUN curl -s -LJ -o /app/applicationinsights-agent-3.4.19.jar https://github.com/microsoft/ApplicationInsights-Java/releases/download/3.4.19/applicationinsights-agent-3.4.19.jar
28 | COPY microservice/applicationinsights.json /app
29 |
30 | ENTRYPOINT ["java","-javaagent:/app/applicationinsights-agent-3.4.19.jar","-noverify", "-XX:MaxRAMPercentage=70", "-XX:+UseParallelGC", "-XX:ActiveProcessorCount=2", "-cp","app:app/lib/*","com.microsoft.openai.samples.indexer.service.Application"]
31 |
--------------------------------------------------------------------------------
/app/indexer/microservice/applicationinsights.json:
--------------------------------------------------------------------------------
1 | {
2 | "role": {
3 | "name": "indexer"
4 | }
5 | }
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft.openai.samples.indexer.service/Application.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 | package com.microsoft.openai.samples.indexer.service;
3 |
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.boot.SpringApplication;
7 | import org.springframework.boot.autoconfigure.SpringBootApplication;
8 | import org.springframework.jms.annotation.EnableJms;
9 |
10 | @SpringBootApplication
11 | @EnableJms
12 | public class Application {
13 |
14 | private static final Logger LOG = LoggerFactory.getLogger(Application.class);
15 | // private final ServiceBusProcessorClient processorClient;
16 |
17 | // public Application(ServiceBusProcessorClient processorClient) {
18 | // this.processorClient = processorClient;
19 | // }
20 |
21 | public static void main(String[] args) {
22 | LOG.info(
23 | "Application profile from system property is [{}]",
24 | System.getProperty("spring.profiles.active"));
25 | SpringApplication.run(Application.class,args);
26 | }
27 |
28 |
29 |
30 | public void run(String... args) throws Exception {
31 |
32 | // System.out.printf("Starting the processor");
33 | // processorClient.start();
34 | // TimeUnit.SECONDS.sleep(10);
35 | // System.out.printf("Stopping and closing the processor");
36 | // processorClient.close();
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft.openai.samples.indexer.service/BlobMessageConsumer.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.service;
2 |
3 | import org.slf4j.Logger;
4 | import org.slf4j.LoggerFactory;
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | public class BlobMessageConsumer {
10 | private static final Logger logger = LoggerFactory.getLogger(BlobMessageConsumer.class);
11 |
12 |
13 |
14 | /**
15 | public void processMessage(ServiceBusReceivedMessageContext context) {
16 | ServiceBusReceivedMessage message = context.getMessage();
17 | logger.info("Processing message. Id: {}, Sequence #: {}. Contents: {}",
18 | message.getMessageId(), message.getSequenceNumber(), message.getBody());
19 | }
20 |
21 | public void processError(ServiceBusErrorContext context) {
22 | logger.error("Error when receiving messages from namespace: '%s'. Entity: '%s'%n",
23 | context.getFullyQualifiedNamespace(), context.getEntityPath());
24 | } */
25 | }
26 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft.openai.samples.indexer.service/IndexerService.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.service;
2 |
3 | import com.microsoft.openai.samples.indexer.DocumentProcessor;
4 | import com.microsoft.openai.samples.indexer.index.SearchIndexManager;
5 | import com.microsoft.openai.samples.indexer.parser.DocumentIntelligencePDFParser;
6 | import com.microsoft.openai.samples.indexer.parser.TextSplitter;
7 | import com.microsoft.openai.samples.indexer.storage.BlobManager;
8 | import org.slf4j.Logger;
9 | import org.slf4j.LoggerFactory;
10 | import org.springframework.stereotype.Component;
11 |
12 | import java.io.IOException;
13 |
14 | @Component
15 | public class IndexerService {
16 |
17 | private static final Logger logger = LoggerFactory.getLogger(IndexerService.class);
18 |
19 | private final BlobManager blobManager;
20 | private final DocumentProcessor documentProcessor;
21 |
22 | public IndexerService(SearchIndexManager searchIndexManager, DocumentIntelligencePDFParser documentIntelligencePDFParser, BlobManager blobManager){
23 | this.blobManager = blobManager;
24 | this.documentProcessor = new DocumentProcessor(searchIndexManager, documentIntelligencePDFParser, new TextSplitter(false));
25 | }
26 | public void indexBlobDocument(String bloburl) throws IOException {
27 | logger.debug("Indexer: Processing blob document {}", bloburl);
28 | String filename = bloburl.substring(bloburl.lastIndexOf("/") + 1);
29 | documentProcessor.indexDocumentFromBytes(filename, "", blobManager.getFileAsBytes(filename));
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/config/AzureAuthenticationConfiguration.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 | package com.microsoft.openai.samples.indexer.service.config;
3 |
4 | import com.azure.core.credential.TokenCredential;
5 | import com.azure.identity.AzureCliCredentialBuilder;
6 | import com.azure.identity.EnvironmentCredentialBuilder;
7 | import com.azure.identity.ManagedIdentityCredentialBuilder;
8 | import com.microsoft.openai.samples.indexer.service.BlobMessageConsumer;
9 |
10 | import org.slf4j.Logger;
11 | import org.slf4j.LoggerFactory;
12 | import org.springframework.beans.factory.annotation.Value;
13 | import org.springframework.context.annotation.Bean;
14 | import org.springframework.context.annotation.Configuration;
15 | import org.springframework.context.annotation.Primary;
16 | import org.springframework.context.annotation.Profile;
17 |
18 | @Configuration
19 | public class AzureAuthenticationConfiguration {
20 | private static final Logger logger = LoggerFactory.getLogger(AzureAuthenticationConfiguration.class);
21 |
22 |
23 | @Value("${azure.identity.client-id}")
24 | String clientId;
25 |
26 | @Profile("dev")
27 | @Bean
28 | @Primary
29 | public TokenCredential localTokenCredential() {
30 | logger.info("Dev Profile activated using AzureCliCredentialBuilder");
31 | return new AzureCliCredentialBuilder().build();
32 | }
33 |
34 | @Profile("docker")
35 | @Bean
36 | @Primary
37 | public TokenCredential servicePrincipalTokenCredential() {
38 | return new EnvironmentCredentialBuilder().build();
39 | }
40 | @Bean
41 | @Profile("default")
42 | @Primary
43 | public TokenCredential managedIdentityTokenCredential() {
44 | logger.info("Using identity with client id: {}", this.clientId);
45 | if (this.clientId.equals("system-managed-identity"))
46 | return new ManagedIdentityCredentialBuilder().build();
47 | else
48 | return new ManagedIdentityCredentialBuilder().clientId(this.clientId).build();
49 |
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/config/BlobManagerConfiguration.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 | package com.microsoft.openai.samples.indexer.service.config;
3 |
4 | import com.azure.core.credential.TokenCredential;
5 | import com.azure.identity.AzureCliCredentialBuilder;
6 | import com.azure.identity.ManagedIdentityCredentialBuilder;
7 | import com.microsoft.openai.samples.indexer.storage.BlobManager;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.context.annotation.Bean;
10 | import org.springframework.context.annotation.Configuration;
11 | import org.springframework.context.annotation.Primary;
12 | import org.springframework.context.annotation.Profile;
13 | @Configuration
14 | public class BlobManagerConfiguration {
15 |
16 | @Bean
17 | public BlobManager blobManager ( @Value("${storage-account.service}") String storageAccountServiceName,
18 | @Value("${blob.container.name}") String containerName,
19 | TokenCredential tokenCredential){
20 | return new BlobManager(storageAccountServiceName, containerName, tokenCredential, false);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/config/DocumentIntelligencePDFParserConfiguration.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 | package com.microsoft.openai.samples.indexer.service.config;
3 |
4 | import com.azure.core.credential.TokenCredential;
5 | import com.microsoft.openai.samples.indexer.parser.DocumentIntelligencePDFParser;
6 | import com.microsoft.openai.samples.indexer.storage.BlobManager;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.context.annotation.Bean;
9 | import org.springframework.context.annotation.Configuration;
10 |
11 | @Configuration
12 | public class DocumentIntelligencePDFParserConfiguration {
13 |
14 | @Bean
15 | public DocumentIntelligencePDFParser documentIntelligencePDFParser ( @Value("${formrecognizer.name}") String formRecognizerName,
16 | TokenCredential tokenCredential){
17 | return new DocumentIntelligencePDFParser(formRecognizerName,tokenCredential,true);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/config/SearchIndexManagerConfiguration.java:
--------------------------------------------------------------------------------
1 | // Copyright (c) Microsoft. All rights reserved.
2 | package com.microsoft.openai.samples.indexer.service.config;
3 |
4 | import com.azure.core.credential.TokenCredential;
5 | import com.microsoft.openai.samples.indexer.embeddings.AzureOpenAIEmbeddingService;
6 | import com.microsoft.openai.samples.indexer.index.AzureSearchClientFactory;
7 | import com.microsoft.openai.samples.indexer.index.SearchIndexManager;
8 | import com.microsoft.openai.samples.indexer.storage.BlobManager;
9 | import org.springframework.beans.factory.annotation.Value;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.context.annotation.Configuration;
12 |
13 | @Configuration
14 | public class SearchIndexManagerConfiguration {
15 |
16 | @Bean
17 | public AzureOpenAIEmbeddingService azureOpenAIEmbeddingService(@Value("${openai.embedding.deployment}") String openaiEmbdeployment,
18 | @Value("${openai.service}") String openaiServiceName,
19 | TokenCredential tokenCredential) {
20 | return new AzureOpenAIEmbeddingService(openaiServiceName, openaiEmbdeployment, tokenCredential, false);
21 | }
22 | @Bean
23 | public AzureSearchClientFactory azureSearchClientFactory(@Value("${cognitive.search.service}") String searchservice,
24 | @Value("${cognitive.search.index}") String index,
25 | TokenCredential tokenCredential) {
26 | return new AzureSearchClientFactory(searchservice, tokenCredential, index, false);
27 | }
28 |
29 | @Bean
30 | public SearchIndexManager searchIndexManager(AzureSearchClientFactory azureSearchClientFactory,
31 | AzureOpenAIEmbeddingService azureOpenAIEmbeddingService,
32 | @Value("${cognitive.search.analizername}") String searchAnalyzerName) {
33 |
34 | return new SearchIndexManager(azureSearchClientFactory,
35 | searchAnalyzerName,
36 | azureOpenAIEmbeddingService);
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/config/ServiceBusConfig.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.service.config;
2 |
3 | import org.springframework.beans.factory.annotation.Value;
4 | import org.springframework.context.annotation.Configuration;
5 | //@Configuration(proxyBeanMethods = false)
6 | public class ServiceBusConfig {
7 | @Value("${servicebus.namespace}")
8 | String SERVICE_BUS_FQDN;
9 |
10 | @Value("${servicebus.queue-name}")
11 | String QUEUE_NAME;
12 | /**
13 |
14 | @Bean
15 | ServiceBusClientBuilder serviceBusClientBuilder(TokenCredential tokenCredential){
16 | String fullyQualifiedNamespace = SERVICE_BUS_FQDN+".servicebus.windows.net";
17 | return new ServiceBusClientBuilder()
18 | .fullyQualifiedNamespace(fullyQualifiedNamespace)
19 | .credential(tokenCredential);
20 | }
21 | @Bean
22 | ServiceBusProcessorClient serviceBusProcessorClient(ServiceBusClientBuilder builder, BlobMessageConsumer consumer) {
23 | ServiceBusProcessorClient serviceBusProcessorClient = builder.processor()
24 | .queueName(QUEUE_NAME)
25 | .processMessage(consumer::processMessage)
26 | .processError(consumer::processError)
27 | .buildProcessorClient();
28 | serviceBusProcessorClient.start();
29 | System.out.println("Started the processor");
30 | return serviceBusProcessorClient;
31 | }
32 |
33 | **/
34 | }
35 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/config/ServiceBusProcessorClientConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.service.config;
2 |
3 | //@Configuration(proxyBeanMethods = false)
4 | public class ServiceBusProcessorClientConfiguration {
5 |
6 | /**
7 | @Bean
8 | ServiceBusRecordMessageListener processMessage() {
9 | return context -> {
10 | ServiceBusReceivedMessage message = context.getMessage();
11 | System.out.printf("Processing message. Id: %s, Sequence #: %s. Contents: %s%n", message.getMessageId(),
12 | message.getSequenceNumber(), message.getBody());
13 | };
14 | }
15 |
16 | @Bean
17 | ServiceBusErrorHandler processError() {
18 | return context -> {
19 | System.out.printf("Error when receiving messages from namespace: '%s'. Entity: '%s'%n",
20 | context.getFullyQualifiedNamespace(), context.getEntityPath());
21 | };
22 | }
23 |
24 | **/
25 | }
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/events/BlobEventGridData.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.service.events;
2 |
3 | public record BlobEventGridData (
4 | String contentType,
5 | String url,
6 | Integer contentLength
7 | ){}
8 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/events/BlobMessageListener.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.service.events;
2 |
3 | import com.fasterxml.jackson.databind.DeserializationFeature;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import com.microsoft.openai.samples.indexer.service.IndexerService;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.jms.annotation.JmsListener;
9 | import org.springframework.stereotype.Component;
10 |
11 | @Component
12 | public class BlobMessageListener {
13 | private static final Logger logger = LoggerFactory.getLogger(BlobMessageListener.class);
14 | final private ObjectMapper mapper;
15 | final private IndexerService indexerService;
16 |
17 | public BlobMessageListener(IndexerService indexerService){
18 | mapper = new ObjectMapper()
19 | .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
20 | this.indexerService = indexerService;
21 | }
22 |
23 | @JmsListener(destination = "${servicebus.queue-name}")
24 | public void receiveMessage(String message) {
25 | logger.debug("Message received from EventGrid: {}", message);
26 | String blobUrl = "";
27 |
28 | try {
29 | BlobUpsertEventGridEvent event = mapper.readValue(message, BlobUpsertEventGridEvent.class);
30 | blobUrl = event.data().url();
31 | logger.info("New request to ingest document received: {}", blobUrl);
32 | } catch (Exception e) {
33 | throw new RuntimeException("Error when trying to unmarshall event grid message %s ".formatted(message),e);
34 | }
35 |
36 | try{
37 | indexerService.indexBlobDocument(blobUrl);
38 | } catch (Exception e) {
39 | throw new RuntimeException("Error when trying to index document %s ".formatted(blobUrl),e);
40 | }
41 |
42 |
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/java/com/microsoft/openai/samples/indexer/service/events/BlobUpsertEventGridEvent.java:
--------------------------------------------------------------------------------
1 | package com.microsoft.openai.samples.indexer.service.events;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4 |
5 | import java.util.List;
6 |
7 | @JsonIgnoreProperties(ignoreUnknown = true)
8 | public record BlobUpsertEventGridEvent (
9 | String id,
10 | String eventType,
11 | String subject,
12 | String eventTime,
13 | String dataVersion,
14 | BlobEventGridData data){}
15 |
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | #Configuraing the Azure Service Bus using JMS
2 | spring.jms.servicebus.namespace=${AZURE_SERVICEBUS_NAMESPACE}
3 | spring.jms.servicebus.pricing-tier=${AZURE_SERVICEBUS_SKU_NAME:Standard}
4 | spring.jms.servicebus.passwordless-enabled=true
5 | spring.jms.listener.receive-timeout=60000
6 | spring.jms.listener.max-concurrency=10
7 | spring.jms.servicebus.credential.managedIdentityEnabled=true
8 | spring.jms.servicebus.credential.clientId=${AZURE_CLIENT_ID}
9 |
10 |
11 | servicebus.queue-name=${AZURE_SERVICEBUS_QUEUE_NAME:documents-queue}
12 |
13 | openai.service=${AZURE_OPENAI_SERVICE}
14 | openai.embedding.deployment=${AZURE_OPENAI_EMB_DEPLOYMENT:embedding}
15 |
16 |
17 | cognitive.search.service=${AZURE_SEARCH_SERVICE:example}
18 | cognitive.search.index=${AZURE_SEARCH_INDEX:gptkbindex}
19 | cognitive.search.analizername=${AZURE_SEARCH_ANALYZERNAME:en.microsoft}
20 |
21 |
22 | storage-account.service=${AZURE_STORAGE_ACCOUNT}
23 | blob.container.name=${AZURE_STORAGE_CONTAINER:content}
24 |
25 | formrecognizer.name=${AZURE_FORMRECOGNIZER_SERVICE}
26 |
27 | # Support for User Assigned Managed identity
28 | azure.identity.client-id=${AZURE_CLIENT_ID:system-managed-identity}
--------------------------------------------------------------------------------
/app/indexer/microservice/src/main/resources/local-dev.properties:
--------------------------------------------------------------------------------
1 |
2 | spring.jms.servicebus.namespace=${AZURE_SERVICEBUS_NAMESPACE:sb-6th64os63ccie}
3 | spring.jms.servicebus.pricing-tier=${AZURE_SERVICEBUS_SKU_NAME:Standard}
4 | spring.jms.servicebus.passwordless-enabled=true
5 | spring.jms.listener.receive-timeout=60000
6 | spring.jms.listener.max-concurrency=10
7 |
8 | servicebus.queue-name=${AZURE_SERVICEBUS_QUEUE_NAME:documents-queue}
9 |
10 | #logging.level.org.springframework=DEBUG
11 | #logging.level.org.springframework.jms=DEBUG
12 |
13 | openai.service=${AZURE_OPENAI_SERVICE}
14 | openai.embedding.deployment=${AZURE_OPENAI_EMB_DEPLOYMENT:embedding}
15 |
16 |
17 | cognitive.search.service=${AZURE_SEARCH_SERVICE:example}
18 | cognitive.search.index=${AZURE_SEARCH_INDEX:gptkbindex}
19 | cognitive.search.analizername=${AZURE_SEARCH_ANALYZERNAME:en.microsoft}
20 |
21 | storage-account.service=${AZURE_STORAGE_ACCOUNT}
22 | blob.container.name=${AZURE_STORAGE_CONTAINER:content}
23 |
24 | formrecognizer.name=${AZURE_FORMRECOGNIZER_SERVICE}
25 |
26 | # Support for User Assigned Managed identity
27 | azure.identity.client-id=${AZURE_CLIENT_ID:system-managed-identity}
--------------------------------------------------------------------------------
/app/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "app",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------
/data/Benefit_Options.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/data/Benefit_Options.pdf
--------------------------------------------------------------------------------
/data/Northwind_Health_Plus_Benefits_Details.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/data/Northwind_Health_Plus_Benefits_Details.pdf
--------------------------------------------------------------------------------
/data/Northwind_Standard_Benefits_Details.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/data/Northwind_Standard_Benefits_Details.pdf
--------------------------------------------------------------------------------
/data/PerksPlus.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/data/PerksPlus.pdf
--------------------------------------------------------------------------------
/data/employee_handbook.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/data/employee_handbook.pdf
--------------------------------------------------------------------------------
/data/role_library.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Azure-Samples/azure-search-openai-demo-java/e9b5f67d6d98c22aaa3a778fb9a10c46d304e7b8/data/role_library.pdf
--------------------------------------------------------------------------------
/deploy/aca/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-demo-java-aca
4 | metadata:
5 | template: azure-search-openai-demo-java-aca@1.4.0-alpha
6 | services:
7 | api:
8 | project: ../../app/backend
9 | language: java
10 | host: containerapp
11 | indexer:
12 | project: ../../app/indexer
13 | language: java
14 | host: containerapp
15 | docker:
16 | path: ./microservice/Dockerfile
17 | web:
18 | project: ../../app/frontend
19 | language: js
20 | host: containerapp
21 |
22 |
23 | hooks:
24 | postprovision:
25 | windows:
26 | shell: pwsh
27 | run: ./scripts/prepdocs.ps1
28 | interactive: true
29 | continueOnError: false
30 | posix:
31 | shell: sh
32 | run: ./scripts/prepdocs.sh
33 | interactive: true
34 | continueOnError: false
35 |
--------------------------------------------------------------------------------
/deploy/aca/compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | frontend:
3 | image: ai-chat-reference-java/frontend
4 | build: ./frontend
5 | environment:
6 | REACT_APP_API_BASE_URL: "http://backend:8080"
7 | ports:
8 | - "80:80"
9 | backend:
10 | image: ai-chat-reference-java/backend
11 | build: ./backend
12 | environment:
13 | - AZURE_STORAGE_ACCOUNT=${AZURE_STORAGE_ACCOUNT}
14 | - AZURE_STORAGE_CONTAINER=${AZURE_STORAGE_CONTAINER}
15 | - AZURE_SEARCH_INDEX=${AZURE_SEARCH_INDEX}
16 | - AZURE_SEARCH_SERVICE=${AZURE_SEARCH_SERVICE}
17 | - AZURE_SEARCH_QUERY_LANGUAGE=${AZURE_SEARCH_QUERY_LANGUAGE}
18 | - AZURE_SEARCH_QUERY_SPELLER=${AZURE_SEARCH_QUERY_SPELLER}
19 | - AZURE_OPENAI_EMB_MODEL_NAME=${AZURE_OPENAI_EMB_MODEL_NAME}
20 | - AZURE_OPENAI_EMB_DEPLOYMENT=${AZURE_OPENAI_EMB_DEPLOYMENT}
21 | - AZURE_OPENAI_CHATGPT_MODEL=${AZURE_OPENAI_CHATGPT_MODEL}
22 | - AZURE_OPENAI_SERVICE=${AZURE_OPENAI_SERVICE}
23 | - AZURE_OPENAI_CHATGPT_DEPLOYMENT=${AZURE_OPENAI_CHATGPT_DEPLOYMENT}
24 | - spring_profiles_active=docker
25 | - AZURE_CLIENT_ID=${servicePrincipal}
26 | - AZURE_CLIENT_SECRET=${servicePrincipalPassword}
27 | - AZURE_TENANT_ID=${servicePrincipalTenant}
28 | ports:
29 | - "8080:8080"
30 | indexer:
31 | image: ai-chat-reference-java/indexer
32 | build:
33 | context: ./indexer
34 | dockerfile: microservice/Dockerfile
35 | environment:
36 | - AZURE_STORAGE_ACCOUNT=${AZURE_STORAGE_ACCOUNT}
37 | - AZURE_STORAGE_CONTAINER=${AZURE_STORAGE_CONTAINER}
38 | - AZURE_SEARCH_INDEX=${AZURE_SEARCH_INDEX}
39 | - AZURE_SEARCH_SERVICE=${AZURE_SEARCH_SERVICE}
40 | - AZURE_FORMRECOGNIZER_SERVICE=${AZURE_FORMRECOGNIZER_SERVICE}
41 | - AZURE_SEARCH_QUERY_SPELLER=${AZURE_SEARCH_QUERY_SPELLER}
42 | - AZURE_OPENAI_EMB_MODEL_NAME=${AZURE_OPENAI_EMB_MODEL_NAME}
43 | - AZURE_OPENAI_EMB_DEPLOYMENT=${AZURE_OPENAI_EMB_DEPLOYMENT}
44 | - AZURE_OPENAI_SERVICE=${AZURE_OPENAI_SERVICE}
45 | - AZURE_SERVICEBUS_NAMESPACE=${AZURE_SERVICEBUS_NAMESPACE}
46 | - spring_profiles_active=docker
47 | - SPRING_CONFIG_LOCATION=classpath:/local-dev.properties
48 | - AZURE_CLIENT_ID=${servicePrincipal}
49 | - AZURE_CLIENT_SECRET=${servicePrincipalPassword}
50 | - AZURE_TENANT_ID=${servicePrincipalTenant}
51 |
52 |
--------------------------------------------------------------------------------
/deploy/aca/infra/app/api.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param identityName string
6 | param applicationInsightsName string
7 | param containerAppsEnvironmentName string
8 | param containerRegistryName string
9 | param serviceName string = 'api'
10 | param corsAcaUrl string
11 | param exists bool
12 |
13 | @description('The environment variables for the container')
14 | param env array = []
15 |
16 | resource apiIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
17 | name: identityName
18 | location: location
19 | }
20 |
21 |
22 | module app '../../../shared/host/container-app-upsert.bicep' = {
23 | name: '${serviceName}-container-app'
24 | params: {
25 | name: name
26 | location: location
27 | tags: union(tags, { 'azd-service-name': serviceName })
28 | identityType: 'UserAssigned'
29 | identityName: apiIdentity.name
30 | exists: exists
31 | containerAppsEnvironmentName: containerAppsEnvironmentName
32 | containerRegistryName: containerRegistryName
33 | containerCpuCoreCount: '1.0'
34 | containerMemory: '2.0Gi'
35 | targetPort: 8080
36 | external:false
37 | env: union(env, [
38 | {
39 | name: 'AZURE_CLIENT_ID'
40 | value: apiIdentity.properties.clientId
41 | }
42 |
43 | {
44 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
45 | value: applicationInsights.properties.ConnectionString
46 | }
47 | {
48 | name: 'API_ALLOW_ORIGINS'
49 | value: corsAcaUrl
50 | }
51 | ])
52 |
53 | }
54 | }
55 |
56 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = {
57 | name: applicationInsightsName
58 | }
59 |
60 |
61 | output SERVICE_API_IDENTITY_PRINCIPAL_ID string = apiIdentity.properties.principalId
62 | output SERVICE_API_NAME string = app.outputs.name
63 | output SERVICE_API_URI string = app.outputs.uri
64 | output SERVICE_API_IMAGE_NAME string = app.outputs.imageName
65 |
--------------------------------------------------------------------------------
/deploy/aca/infra/app/indexer.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param identityName string
6 | param applicationInsightsName string
7 | param containerAppsEnvironmentName string
8 | param containerRegistryName string
9 | param serviceName string = 'indexer'
10 | param exists bool
11 |
12 | @description('The environment variables for the container')
13 | param env array = []
14 |
15 | resource indexerIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
16 | name: identityName
17 | location: location
18 | }
19 |
20 | module app '../../../shared/host/container-app-upsert.bicep' = {
21 | name: '${serviceName}-container-app'
22 | params: {
23 | name: name
24 | location: location
25 | tags: union(tags, { 'azd-service-name': serviceName })
26 | identityType: 'UserAssigned'
27 | identityName: indexerIdentity.name
28 | exists: exists
29 | containerAppsEnvironmentName: containerAppsEnvironmentName
30 | containerRegistryName: containerRegistryName
31 | containerCpuCoreCount: '1.0'
32 | containerMemory: '2.0Gi'
33 | targetPort: 8080
34 | external:false
35 | env: union(env, [
36 | {
37 | name: 'AZURE_CLIENT_ID'
38 | value: indexerIdentity.properties.clientId
39 | }
40 |
41 | {
42 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
43 | value: applicationInsights.properties.ConnectionString
44 | }
45 |
46 | ])
47 |
48 | }
49 | }
50 |
51 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = {
52 | name: applicationInsightsName
53 | }
54 |
55 | output SERVICE_INDEXER_IDENTITY_PRINCIPAL_ID string = indexerIdentity.properties.principalId
56 | output SERVICE_INDEXER_NAME string = app.outputs.name
57 | output SERVICE_INDEXER_URI string = app.outputs.uri
58 | output SERVICE_INDEXER_IMAGE_NAME string = app.outputs.imageName
59 |
--------------------------------------------------------------------------------
/deploy/aca/infra/app/web.bicep:
--------------------------------------------------------------------------------
1 | param name string
2 | param location string = resourceGroup().location
3 | param tags object = {}
4 |
5 | param identityName string
6 | param apiBaseUrl string
7 | param applicationInsightsName string
8 | param containerAppsEnvironmentName string
9 | param containerRegistryName string
10 | param serviceName string = 'web'
11 | param exists bool
12 |
13 | resource webIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
14 | name: identityName
15 | location: location
16 | }
17 |
18 | module app '../../../shared/host/container-app-upsert.bicep' = {
19 | name: '${serviceName}-container-app'
20 | params: {
21 | name: name
22 | location: location
23 | tags: union(tags, { 'azd-service-name': serviceName })
24 | identityType: 'UserAssigned'
25 | identityName: identityName
26 | exists: exists
27 | containerAppsEnvironmentName: containerAppsEnvironmentName
28 | containerRegistryName: containerRegistryName
29 | env: [
30 | {
31 | name: 'REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING'
32 | value: applicationInsights.properties.ConnectionString
33 | }
34 | {
35 | name: 'REACT_APP_API_BASE_URL'
36 | value: apiBaseUrl
37 | }
38 | {
39 | name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
40 | value: applicationInsights.properties.ConnectionString
41 | }
42 | ]
43 | targetPort: 80
44 | }
45 | }
46 |
47 | resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = {
48 | name: applicationInsightsName
49 | }
50 |
51 | output SERVICE_WEB_IDENTITY_PRINCIPAL_ID string = webIdentity.properties.principalId
52 | output SERVICE_WEB_NAME string = app.outputs.name
53 | output SERVICE_WEB_URI string = app.outputs.uri
54 | output SERVICE_WEB_IMAGE_NAME string = app.outputs.imageName
55 |
--------------------------------------------------------------------------------
/deploy/aca/scripts/prepdocs.ps1:
--------------------------------------------------------------------------------
1 | Write-Host ""
2 | Write-Host "Loading azd .env file from current environment"
3 | Write-Host ""
4 |
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 "Environment variables set."
18 |
19 |
20 | Write-Host 'Building java indexer..'
21 | Start-Process -FilePath "mvn" -ArgumentList "package -f ../../app/indexer/pom.xml" -Wait -NoNewWindow
22 |
23 | Write-Host 'Running the java indexer cli.jar...'
24 | Start-Process -FilePath "java" -ArgumentList "-jar ../../app/indexer/cli/target/cli.jar `"../../data`" --storageaccount $env:AZURE_STORAGE_ACCOUNT --container $env:AZURE_STORAGE_CONTAINER --searchservice $env:AZURE_SEARCH_SERVICE --openai-service-name $env:AZURE_OPENAI_SERVICE --openai-emb-deployment $env:AZURE_OPENAI_EMB_DEPLOYMENT --index $env:AZURE_SEARCH_INDEX --formrecognizerservice $env:AZURE_FORMRECOGNIZER_SERVICE --verbose upload" -Wait -NoNewWindow
--------------------------------------------------------------------------------
/deploy/aca/scripts/prepdocs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo ""
4 | echo "Loading azd .env file from current environment"
5 | echo ""
6 |
7 | while IFS='=' read -r key value; do
8 | value=$(echo "$value" | sed 's/^"//' | sed 's/"$//')
9 | export "$key=$value"
10 | done < "$tempEnvFile"
12 |
13 | # Get the current date in the required format
14 | date=$(date +"%Y-%m-%d %H:%M:%S")
15 |
16 | # Start building the ConfigMap YAML content
17 | configMapContent="# Updated $date
18 | apiVersion: v1
19 | kind: ConfigMap
20 | metadata:
21 | name: azd-env-configmap
22 | data:
23 | "
24 |
25 | # Read each line from the temp file and process it
26 | while IFS= read -r line; do
27 | if [[ $line =~ ^([^=]+)=(.*)$ ]]; then
28 | key="${BASH_REMATCH[1]}"
29 | value="${BASH_REMATCH[2]}"
30 | # Trim quotes from value
31 | trimmedValue="${value%\"}"
32 | trimmedValue="${trimmedValue#\"}"
33 | # Append to the ConfigMap content
34 | configMapContent+=" ${key}: \"${trimmedValue}\""$'\n'
35 | fi
36 | done < "$tempEnvFile"
37 |
38 | # Specify the output file path for the ConfigMap
39 | outputFilePath="../../app/backend/manifests/azd-env-configmap.yml"
40 |
41 | # Check if the output file exists and remove it before creating a new one
42 | if [ -f "$outputFilePath" ]; then
43 | rm -f "$outputFilePath"
44 | fi
45 |
46 | # Write the ConfigMap content to the file
47 | echo -e "$configMapContent" > "$outputFilePath"
48 |
49 | # Cleaning up temporary files
50 | rm -f "$tempEnvFile"
51 |
52 | echo "ConfigMap generated at $outputFilePath"
--------------------------------------------------------------------------------
/deploy/aks/scripts/prepdocs.ps1:
--------------------------------------------------------------------------------
1 | Write-Host ""
2 | Write-Host "Loading azd .env file from current environment"
3 | Write-Host ""
4 |
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 "Environment variables set."
18 |
19 |
20 | Write-Host 'Building java indexer..'
21 | Start-Process -FilePath "mvn" -ArgumentList "package -f ../../app/indexer/pom.xml" -Wait -NoNewWindow
22 |
23 | Write-Host 'Running the java indexer cli.jar...'
24 | Start-Process -FilePath "java" -ArgumentList "-jar ../../app/indexer/cli/target/cli.jar `"../../data`" --storageaccount $env:AZURE_STORAGE_ACCOUNT --container $env:AZURE_STORAGE_CONTAINER --searchservice $env:AZURE_SEARCH_SERVICE --openai-service-name $env:AZURE_OPENAI_SERVICE --openai-emb-deployment $env:AZURE_OPENAI_EMB_DEPLOYMENT --index $env:AZURE_SEARCH_INDEX --formrecognizerservice $env:AZURE_FORMRECOGNIZER_SERVICE --verbose upload" -Wait -NoNewWindow
--------------------------------------------------------------------------------
/deploy/aks/scripts/prepdocs.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | echo ""
4 | echo "Loading azd .env file from current environment"
5 | echo ""
6 |
7 | while IFS='=' read -r key value; do
8 | value=$(echo "$value" | sed 's/^"//' | sed 's/"$//')
9 | export "$key=$value"
10 | done <