brainsqueeze's picture
Link to guide PDF
f86ac12 verified
from typing import List, Literal, Tuple, TypedDict
import os
import gradio as gr
try:
from common import org_search_component as oss
from formatting import process_reasons, parse_pcs_descriptions, parse_geo_descriptions
from services import RfpRecommend, RfpFeedback
except ImportError:
from ..common import org_search_component as oss
from .formatting import process_reasons, parse_pcs_descriptions, parse_geo_descriptions
from .services import RfpRecommend, RfpFeedback
api = RfpRecommend()
reporting = RfpFeedback()
class LoggedComponents(TypedDict):
recommendations: gr.components.Component
ratings: List[gr.components.Component]
correctness: gr.components.Component
sufficiency: gr.components.Component
comments: gr.components.Component
email: gr.components.Component
def single_recommendation_response(
item_number: int,
rec_type: Literal["RFP"] = "RFP"
) -> gr.Radio:
"""Generates a radio button group to provide feedback for single recommendation indexed by `item_number`.
Since the index values start from `0` we add `1` to indicate the ordinal value in the info text.
Parameters
----------
item_number : int
Recommendation index starting from 0
Returns
-------
gr.Radio
"""
ordinal = str(item_number + 1)
suffix = "th"
if ordinal.endswith('1') and not ordinal.endswith('11'):
suffix = "st"
elif ordinal.endswith('2') and not ordinal.endswith('12'):
suffix = "nd"
elif ordinal.endswith('3') and not ordinal.endswith('13'):
suffix = "rd"
elem = gr.Radio(
choices=[
"Not relevant and not useful",
"Relevant but not useful",
"Relevant and useful"
],
label=f"Recommendation #{ordinal}",
info=f"Evaluate the {ordinal}{suffix} {rec_type} (if applicable)"
)
return elem
def recommend_invoke(recipient: gr.State):
response = api(candid_entity_id=recipient[0])
output = []
for rfp in (response.get("recommendations", []) or []):
output.append([
rfp["funder_id"],
rfp["funder_name"],
rfp["funder_address"],
rfp["amount"],
(
f"<a href='{rfp['application_url']}' target='_blank' rel='noopener noreferrer'>"
f"{rfp['application_url']}</a>"
),
rfp["deadline"],
rfp["description"],
parse_pcs_descriptions(rfp["taxonomy"]),
parse_geo_descriptions(rfp["area_served"])
])
if len(output) == 0:
raise gr.Error("No relevant RFPs were found, please try again in the future as new RFPs become available.")
return output, process_reasons(response.get("meta", {}) or {}), response
def build_recommender() -> Tuple[LoggedComponents, gr.Blocks]:
with gr.Blocks(theme=gr.themes.Soft(), title="RFP recommendations") as demo:
gr.Markdown(
"""
<h1>RFP recommendations</h1>
<p>Receive recommendations for funding opportunities relevant to your work.</p>
<p>
Please read the <a
href='https://info.candid.org/rfp-recommendation-guide'
target="_blank"
rel="noopener noreferrer"
>guide</a> to get started.
</p>
<hr>
"""
)
with gr.Row():
with gr.Column():
_, selected_org_state = oss.render()
with gr.Row():
recommend = gr.Button("Get recommendations", scale=5, variant="primary")
with gr.Row():
with gr.Accordion(label="Parameters used for recommendations", open=False):
reasons_output = gr.DataFrame(
col_count=3,
headers=["Reason category", "Reason value", "Reason description"],
interactive=False
)
rec_outputs = gr.DataFrame(
label="Recommended RFPs",
type="array",
headers=[
"Funder ID", "Name", "Address",
"Amount", "Application URL", "Deadline",
"Description", "About", "Where"
],
col_count=(9, "fixed"),
datatype=[
"number", "str", "str",
"str", "markdown", "date",
"str", "markdown", "markdown"
],
wrap=True,
max_height=1000,
column_widths=[
"5%", "10%", "20%",
"5", "15%", "5%",
"10%", "10%", "20%"
],
interactive=False
)
recommendations_json = gr.JSON(label="Recommended RFPs JSON", visible=False)
# pylint: disable=no-member
recommend.click(
fn=recommend_invoke,
inputs=[selected_org_state],
outputs=[rec_outputs, reasons_output, recommendations_json]
)
logged = LoggedComponents(
recommendations=recommendations_json
)
return logged, demo
def build_feedback(
components: LoggedComponents,
N: int = 5,
rec_type: Literal["RFP"] = "RFP",
) -> gr.Blocks:
def handle_feedback(*args):
try:
reporting(
recommendation_data=args[0],
ratings=list(args[1: (N + 1)]),
info_is_correct=args[N + 1],
info_is_sufficient=args[N + 2],
comments=args[N + 3],
email=args[N + 4]
)
gr.Info("Thank you for providing feedback!")
except Exception as ex:
if hasattr(ex, "response"):
error_msg = ex.response.json().get("response", {}).get("error")
raise gr.Error(f"Failed to submit feedback: {error_msg}")
raise gr.Error("Failed to submit feedback")
feedback_components = []
with gr.Blocks(theme=gr.themes.Soft(), title="Candid AI demo") as demo:
gr.Markdown("""
<h1>Help us improve this tool with your valuable feedback</h1>
Please provide feedback for the recommendations on the previous tab.
It is not required to provide feedback on all recommendations before submitting.
"""
)
with gr.Row():
with gr.Column():
with gr.Group():
for i in range(N):
f = single_recommendation_response(i, rec_type=rec_type)
feedback_components.append(f)
if "ratings" not in components:
components["ratings"] = [f]
else:
components["ratings"].append(f)
correctness = gr.Radio(
choices=["True", "False"],
label="Information is correct?",
info="Are the displayed RFP details correct?"
)
sufficiency = gr.Radio(
choices=["True", "False"],
label="Sufficient data?",
info="Is enough RFP data available to provide meaningful recommendations?"
)
comment = gr.Textbox(label="Additional comments (optional)", lines=4)
email = gr.Textbox(label="Your email (optional)", lines=1)
components["correctness"] = correctness
components["sufficiency"] = sufficiency
components["comments"] = comment
components["email"] = email
with gr.Row():
submit = gr.Button("Submit Feedback", variant='primary', scale=5)
gr.ClearButton(components=feedback_components, variant="stop")
# pylint: disable=no-member
submit.click(
fn=handle_feedback,
inputs=[comp for k, cl in components.items() for comp in (cl if isinstance(cl, list) else [cl])],
outputs=None,
show_api=False,
api_name=False,
preprocess=False,
)
return demo
def build_demo():
logger, recommender = build_recommender()
feedback = build_feedback(logger)
return gr.TabbedInterface(
interface_list=[recommender, feedback],
tab_names=["RFP recommendations", "Feedback"],
title="Candid's RFP recommendation engine",
theme=gr.themes.Soft()
)
if __name__ == '__main__':
app = build_demo()
app.queue(max_size=5).launch(
show_api=False,
auth=[
(os.getenv("APP_USERNAME"), os.getenv("APP_PASSWORD")),
(os.getenv("APP_PUBLIC_USERNAME"), os.getenv("APP_PUBLIC_PASSWORD")),
],
auth_message="Login to Candid's letter of intent demo",
ssr_mode=False
)