Skip to content

Commit 0abbfb2

Browse files
jgbradley1americanthinkertimothymeyersChristine Caggiano
authored
Add frontend application (#68)
Co-authored-by: americanthinker <[email protected]> Co-authored-by: Tim <[email protected]> Co-authored-by: Christine Caggiano <[email protected]>
1 parent 5dd5060 commit 0abbfb2

18 files changed

+1629
-1
lines changed

Diff for: .github/workflows/dev.yaml

+13
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,16 @@ jobs:
5252
context: .
5353
file: docker/Dockerfile-backend
5454
push: false
55+
build-frontend:
56+
needs: [lint-check]
57+
runs-on: ubuntu-latest
58+
if: ${{ !github.event.pull_request.draft }}
59+
steps:
60+
- name: Checkout repository
61+
uses: actions/checkout@v4
62+
- name: Build docker image
63+
uses: docker/build-push-action@v2
64+
with:
65+
context: .
66+
file: docker/Dockerfile-frontend
67+
push: false

Diff for: .gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,4 @@ main.parameters.json
166166
**/charts/*.tgz
167167
**/Chart.lock
168168

169-
.history
169+
.history

Diff for: docker/Dockerfile-frontend

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
FROM python:3.10
5+
6+
ENV PIP_ROOT_USER_ACTION=ignore
7+
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
8+
ENV SETUPTOOLS_USE_DISTUTILS=stdlib
9+
10+
COPY poetry.lock pyproject.toml /
11+
COPY frontend /frontend
12+
RUN pip install poetry \
13+
&& poetry config virtualenvs.create false \
14+
&& poetry install --without backend
15+
16+
WORKDIR /frontend
17+
EXPOSE 8080
18+
CMD ["streamlit", "run", "app.py", "--server.port", "8080"]

Diff for: frontend/.streamlit/config.toml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
[server]
5+
enableXsrfProtection = false

Diff for: frontend/README.md

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Frontend Application Launch Instructions
2+
A small frontend application, a streamlit app, is provided to demonstrate how to build a UI on top of the solution accelerator API.
3+
4+
### 1. Deploy the GraphRAG solution accelerator
5+
Follow instructions from the [deployment guide](../docs/DEPLOYMENT-GUIDE.md) to deploy a full instance of the solution accelerator.
6+
7+
### 2. (optional) Create a `.env` file:
8+
9+
| Variable Name | Required | Example | Description |
10+
| :--- | --- | :--- | ---: |
11+
DEPLOYMENT_URL | No | https://<my_apim>.azure-api.net | Base url of the deployed graphrag API. Also referred to as the APIM Gateway URL.
12+
APIM_SUBSCRIPTION_KEY | No | <subscription_key> | A [subscription key](https://learn.microsoft.com/en-us/azure/api-management/api-management-subscriptions) generated by APIM.
13+
DEPLOYER_EMAIL | No | [email protected] | Email address of the person/organization that deployed the solution accelerator.
14+
15+
### 3. Start UI
16+
17+
The frontend application can be run locally as a docker container. If a `.env` file is not provided, the UI will prompt the user for additional information.
18+
19+
```
20+
# cd to the root directory of the repo
21+
> docker build -t graphrag:frontend -f docker/Dockerfile-frontend .
22+
> docker run --env-file <env_file> -p 8080:8080 graphrag:frontend
23+
```
24+
To access the app , visit `localhost:8080` in your browser.
25+
26+
This UI application can also be hosted in Azure as a [Web App](https://azure.microsoft.com/en-us/products/app-service/web).

Diff for: frontend/app.py

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
import os
5+
6+
import streamlit as st
7+
from src.components import tabs
8+
from src.components.index_pipeline import IndexPipeline
9+
from src.enums import EnvVars
10+
from src.functions import initialize_app
11+
from src.graphrag_api import GraphragAPI
12+
13+
# Load environment variables
14+
initialized = initialize_app()
15+
st.session_state["initialized"] = True if initialized else False
16+
17+
18+
def graphrag_app(initialized: bool):
19+
# main entry point for app interface
20+
st.title("Microsoft GraphRAG Copilot")
21+
main_tab, prompt_gen_tab, prompt_edit_tab, index_tab, query_tab = st.tabs(
22+
[
23+
"**Intro**",
24+
"**1. Prompt Generation**",
25+
"**2. Prompt Configuration**",
26+
"**3. Index**",
27+
"**4. Query**",
28+
]
29+
)
30+
with main_tab:
31+
tabs.get_main_tab(initialized)
32+
33+
# if not initialized, only main tab is displayed
34+
if initialized:
35+
# assign API request information
36+
COLUMN_WIDTHS = [0.275, 0.45, 0.275]
37+
api_url = st.session_state[EnvVars.DEPLOYMENT_URL.value]
38+
apim_key = st.session_state[EnvVars.APIM_SUBSCRIPTION_KEY.value]
39+
client = GraphragAPI(api_url, apim_key)
40+
indexPipe = IndexPipeline(client, COLUMN_WIDTHS)
41+
42+
# display tabs
43+
with prompt_gen_tab:
44+
tabs.get_prompt_generation_tab(client, COLUMN_WIDTHS)
45+
with prompt_edit_tab:
46+
tabs.get_prompt_configuration_tab()
47+
with index_tab:
48+
tabs.get_index_tab(indexPipe)
49+
with query_tab:
50+
tabs.get_query_tab(client)
51+
52+
deployer_email = os.getenv("DEPLOYER_EMAIL", "[email protected]")
53+
54+
footer = f"""
55+
<div class="footer">
56+
<p> Responses may be inaccurate; please review all responses for accuracy. Learn more about Azure OpenAI code of conduct <a href="https://learn.microsoft.com/en-us/legal/cognitive-services/openai/code-of-conduct"> here</a>. </br> For feedback, email us at <a href="mailto:{deployer_email}">{deployer_email}</a>.</p>
57+
</div>
58+
"""
59+
st.markdown(footer, unsafe_allow_html=True)
60+
61+
62+
if __name__ == "__main__":
63+
graphrag_app(st.session_state["initialized"])

Diff for: frontend/src/__init__.py

Whitespace-only changes.

Diff for: frontend/src/components/__init__.py

Whitespace-only changes.

Diff for: frontend/src/components/index_pipeline.py

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
from io import StringIO
5+
6+
import streamlit as st
7+
8+
from src.components.upload_files_component import upload_files
9+
from src.enums import PromptKeys
10+
from src.functions import GraphragAPI
11+
12+
13+
class IndexPipeline:
14+
def __init__(self, client: GraphragAPI, column_widths: list[float]) -> None:
15+
self.client = client
16+
self.containers = client.get_storage_container_names()
17+
self.column_widths = column_widths
18+
19+
def storage_data_step(self):
20+
"""
21+
Builds the Storage Data Step for the Indexing Pipeline.
22+
"""
23+
24+
disable_other_input = False
25+
_, col2, _ = st.columns(self.column_widths)
26+
27+
with col2:
28+
st.header(
29+
"1. Data Storage",
30+
divider=True,
31+
help="Select a Data Storage Container to upload data to or select an existing container to use for indexing. The data will be processed by the LLM to create a Knowledge Graph.",
32+
)
33+
select_storage_name = st.selectbox(
34+
label="Select an existing Storage Container.",
35+
options=[""] + self.containers
36+
if isinstance(self.containers, list)
37+
else [],
38+
key="index-storage",
39+
index=0,
40+
)
41+
42+
if select_storage_name != "":
43+
disable_other_input = True
44+
st.write("Or...")
45+
with st.expander("Upload data to a storage container."):
46+
# TODO: validate storage container name before uploading
47+
# TODO: add user message that option not available while existing storage container is selected
48+
upload_files(
49+
self.client,
50+
key_prefix="index",
51+
disable_other_input=disable_other_input,
52+
)
53+
54+
if select_storage_name != "":
55+
disable_other_input = True
56+
57+
def build_index_step(self):
58+
"""
59+
Creates the Build Index Step for the Indexing Pipeline.
60+
"""
61+
_, col2, _ = st.columns(self.column_widths)
62+
with col2:
63+
st.header(
64+
"2. Build Index",
65+
divider=True,
66+
help="Building an index will process the data from step 1 and create a Knowledge Graph suitable for querying. The LLM will use either the default prompt configuration or the prompts that you generated previously. To track the status of an indexing job, use the check index status below.",
67+
)
68+
# use data from either the selected storage container or the uploaded data
69+
select_storage_name = st.session_state["index-storage"]
70+
input_storage_name = (
71+
st.session_state["index-storage-name-input"]
72+
if st.session_state["index-upload-button"]
73+
else ""
74+
)
75+
storage_selection = select_storage_name or input_storage_name
76+
77+
# Allow user to choose either default or custom prompts
78+
custom_prompts = any([st.session_state[k.value] for k in PromptKeys])
79+
prompt_options = ["Default", "Custom"] if custom_prompts else ["Default"]
80+
prompt_choice = st.radio(
81+
"Choose LLM Prompt Configuration",
82+
options=prompt_options,
83+
index=1 if custom_prompts else 0,
84+
key="prompt-config-choice",
85+
horizontal=True,
86+
)
87+
88+
# Create new index name
89+
index_name = st.text_input("Enter Index Name", key="index-name-input")
90+
91+
st.write(f"Selected Storage Container: **:blue[{storage_selection}]**")
92+
if st.button(
93+
"Build Index",
94+
help="You must enter both an Index Name and Select a Storage Container to enable this button",
95+
disabled=not index_name or not storage_selection,
96+
):
97+
entity_prompt = (
98+
StringIO(st.session_state[PromptKeys.ENTITY.value])
99+
if prompt_choice == "Custom"
100+
else None
101+
)
102+
summarize_prompt = (
103+
StringIO(st.session_state[PromptKeys.SUMMARY.value])
104+
if prompt_choice == "Custom"
105+
else None
106+
)
107+
community_prompt = (
108+
StringIO(st.session_state[PromptKeys.COMMUNITY.value])
109+
if prompt_choice == "Custom"
110+
else None
111+
)
112+
113+
response = self.client.build_index(
114+
storage_name=storage_selection,
115+
index_name=index_name,
116+
entity_extraction_prompt_filepath=entity_prompt,
117+
summarize_description_prompt_filepath=summarize_prompt,
118+
community_prompt_filepath=community_prompt,
119+
)
120+
121+
if response.status_code == 200:
122+
st.success(
123+
f"Job submitted successfully, using {prompt_choice} prompts!"
124+
)
125+
else:
126+
st.error(
127+
f"Failed to submit job.\nStatus: {response.json()['detail']}"
128+
)
129+
130+
def check_status_step(self):
131+
"""
132+
Checks the progress of a running indexing job.
133+
"""
134+
_, col2, _ = st.columns(self.column_widths)
135+
with col2:
136+
st.header(
137+
"3. Check Index Status",
138+
divider=True,
139+
help="Select an index to check the status of what stage indexing is in. Indexing must be complete in order to be able to execute queries.",
140+
)
141+
142+
options_indexes = self.client.get_index_names()
143+
# create logic for defaulting to running job index if one exists
144+
new_index_name = st.session_state["index-name-input"]
145+
default_index = (
146+
options_indexes.index(new_index_name)
147+
if new_index_name in options_indexes
148+
else 0
149+
)
150+
index_name_select = st.selectbox(
151+
label="Select an index to check its status.",
152+
options=options_indexes if any(options_indexes) else [],
153+
index=default_index,
154+
)
155+
progress_bar = st.progress(0, text="Index Job Progress")
156+
if st.button("Check Status"):
157+
status_response = self.client.check_index_status(index_name_select)
158+
if status_response.status_code == 200:
159+
status_response_text = status_response.json()
160+
if status_response_text["status"] != "":
161+
try:
162+
# build status message
163+
job_status = status_response_text["status"]
164+
status_message = f"Status: {status_response_text['status']}"
165+
st.success(status_message) if job_status in [
166+
"running",
167+
"complete",
168+
] else st.warning(status_message)
169+
except Exception as e:
170+
print(e)
171+
try:
172+
# build percent complete message
173+
percent_complete = status_response_text["percent_complete"]
174+
progress_bar.progress(float(percent_complete) / 100)
175+
completion_message = (
176+
f"Percent Complete: {percent_complete}% "
177+
)
178+
st.warning(
179+
completion_message
180+
) if percent_complete < 100 else st.success(
181+
completion_message
182+
)
183+
except Exception as e:
184+
print(e)
185+
try:
186+
# build progress message
187+
progress_status = status_response_text["progress"]
188+
progress_status = (
189+
progress_status if progress_status else "N/A"
190+
)
191+
progress_message = f"Progress: {progress_status}"
192+
st.success(
193+
progress_message
194+
) if progress_status != "N/A" else st.warning(
195+
progress_message
196+
)
197+
except Exception as e:
198+
print(e)
199+
else:
200+
st.warning(
201+
f"No status information available for this index: {index_name_select}"
202+
)
203+
else:
204+
st.warning(
205+
f"No workflow information available for this index: {index_name_select}"
206+
)

Diff for: frontend/src/components/login_sidebar.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
import streamlit as st
5+
6+
from src.enums import EnvVars
7+
from src.graphrag_api import GraphragAPI
8+
9+
10+
def login():
11+
"""
12+
Login component that displays in the sidebar. Requires the user to enter
13+
the APIM Gateway URL and Subscription Key to login. After entering user
14+
credentials, a simple health check call is made to the GraphRAG API.
15+
"""
16+
with st.sidebar:
17+
st.title(
18+
"Login",
19+
help="Enter your APIM credentials to get started. Refreshing the browser will require you to login again.",
20+
)
21+
with st.form(key="login-form", clear_on_submit=True):
22+
apim_url = st.text_input("APIM Gateway URL", key="apim-url")
23+
apim_sub_key = st.text_input(
24+
"APIM Subscription Key", key="subscription-key"
25+
)
26+
form_submit = st.form_submit_button("Login")
27+
if form_submit:
28+
client = GraphragAPI(apim_url, apim_sub_key)
29+
status_code = client.health_check()
30+
if status_code == 200:
31+
st.success("Login Successful")
32+
st.session_state[EnvVars.DEPLOYMENT_URL.value] = apim_url
33+
st.session_state[EnvVars.APIM_SUBSCRIPTION_KEY.value] = apim_sub_key
34+
st.session_state["initialized"] = True
35+
st.rerun()
36+
else:
37+
st.error("Login Failed")
38+
st.error("Please check the APIM Gateway URL and Subscription Key")
39+
return status_code

0 commit comments

Comments
 (0)