from typing import List, Tuple, TypedDict
import os
import gradio as gr
from docx import Document
from docx.shared import Pt
from fpdf import FPDF
try:
from .editor import create_editable_section, update_highlighted_text
from .services import (
organization_pair_autofill,
cost_estimator,
draft_letter,
help_mission_statement,
send_feedback
)
from .constants import JS, CATEGORIES
except ImportError:
from editor import create_editable_section, update_highlighted_text
from services import (
organization_pair_autofill,
cost_estimator,
draft_letter,
help_mission_statement,
send_feedback
)
from constants import JS, CATEGORIES
os.environ["GRADIO_TEMP_DIR"] = "/tmp"
class LoggedComponents(TypedDict):
context: List[gr.components.Component]
letter: List[gr.components.Component]
relevance: gr.components.Component
coherence: gr.components.Component
fluency: gr.components.Component
overall_quality: gr.components.Component
comments: gr.components.Component
email: gr.components.Component
def update_links(selected_categories: List[str]) -> str:
"""Generates links of verified data sources of selected categories
Parameters
----------
selected_categories : List[str]
One or more categories of interest
Returns
-------
str
Formatted links for display
"""
all_links = ""
for category in selected_categories:
links = CATEGORIES.get(category, f"No resources available for {category}.")
all_links += f"### {category}\n" + links + "\n\n"
return all_links
def generate_pdf(*sections: str) -> str:
"""Generates a downloadable PDF of LOI
Returns
-------
str
Path to the saved PDF
"""
pdf = FPDF()
# pdf.set_auto_page_break(0)
pdf.add_page()
root = os.path.dirname(os.path.abspath(__file__))
pdf.add_font(family="Arial", fname=os.path.join(root, "ARIAL.TTF"))
pdf.set_font("Arial", size=12)
line_height = pdf.font_size * 1.2
pdf.multi_cell(w=0, h=line_height, text="[Your name]\n")
pdf.multi_cell(w=0, h=line_height, text="[Organization name]\n")
pdf.multi_cell(w=0, h=line_height, text="[Address]\n")
pdf.ln(line_height)
pdf.multi_cell(w=0, h=line_height, text="[Today's date]\n")
pdf.ln(line_height)
pdf.multi_cell(w=0, h=line_height, text="[Funder contact name]\n")
pdf.multi_cell(w=0, h=line_height, text="[Funder organization name]\n")
pdf.multi_cell(w=0, h=line_height, text="[Address]\n")
pdf.ln(line_height)
pdf.multi_cell(w=0, h=line_height, text="Re: [Project title]\n")
pdf.ln(line_height)
for section in sections:
pdf.multi_cell(w=0, h=line_height, text=section)
pdf.ln(line_height) # Single spacing
pdf_output = os.path.join(os.getenv("GRADIO_TEMP_DIR", "/tmp"), "LOI_by_Candid_AI.pdf")
pdf.output(pdf_output)
return pdf_output
def generate_docx(*sections: str) -> str:
"""Generates a downloadable Word document of LOI
Returns
-------
str
Path to the saved Word document
"""
doc = Document()
style = doc.styles['Normal']
style.font.name = 'Arial' # Arial font
style.font.size = Pt(12) # 12 point
style.paragraph_format.line_spacing = 1.0 # Single spacing
doc.add_paragraph("[Your name]")
doc.add_paragraph("[Organization name]")
doc.add_paragraph("[Address]")
doc.add_paragraph("")
doc.add_paragraph("[Today's date]")
doc.add_paragraph("")
doc.add_paragraph("[Funder contact name]")
doc.add_paragraph("[Funder organization name]")
doc.add_paragraph("[Address]")
doc.add_paragraph("")
doc.add_paragraph("Re: [Project title]")
doc.add_paragraph("")
for section in sections:
doc.add_paragraph(section)
docx_output = os.path.join(os.getenv("GRADIO_TEMP_DIR", "/tmp"), "LOI_by_Candid_AI.docx")
doc.save(docx_output)
return docx_output
def build_drafter() -> Tuple[LoggedComponents, gr.Blocks]:
# delete_cache is tuple (frequency, age) where age is in seconds
# frequency is provided as an integer num_seconds as well where the deletion occurrence is 1 / num_seconds
with gr.Blocks(theme=gr.themes.Soft(), title="LOI writer", delete_cache=(30, 30)) as demo:
gr.Markdown(
"""
Demo: LOI writer
Please read the guide to get started.
"""
)
with gr.Row():
with gr.Column():
gr.Markdown(
"""Step 1: Organization lookups
Enter the name/EIN of your nonprofit and the name/EIN of the intended funder.
Use Candid
to find this information, if not known.
"""
)
org_name_text = gr.Textbox(label="*Name of your organization", lines=1)
org_ein_text = gr.Textbox(label="EIN of your organization", lines=1)
foundation_name_text = gr.Textbox(label="*To which foundation you are writing an LOI to", lines=1)
foundation_ein_text = gr.Textbox(label="EIN of the foundation", lines=1)
gr.HTML("")
with gr.Row():
with gr.Column(scale=7):
gr.Markdown(
"""Step 2: Data autofill
Autofill with data you have
shared with Candid.
Contents can be reviewed and edited before used for LOI generation
"""
)
with gr.Column(scale=1):
autofill_btn = gr.Button("AutoFill (Optional)")
recip_json = gr.State()
# recip_candid_id = gr.State()
recip_candid_id = gr.Text(visible=False, interactive=False)
funder_json = gr.State()
# funder_candid_id = gr.State()
funder_candid_id = gr.Text(visible=False, interactive=False)
gr.HTML("")
gr.Markdown(
"""
Step 3: Add project details
Provide the basic information to make your "ask" clear
""")
projects_text = gr.Textbox(
label="*Main projects",
info="Select from the following projects or enter your own project",
interactive=True,
lines=2
)
project_name_text = gr.Textbox(
label="Project name / title",
info="Something pithy a reader remembers",
interactive=True,
lines=1
)
project_purpose_text = gr.Textbox(label="Purpose of the project", lines=1)
amount_text = gr.Textbox(label="Amount needed or requested", lines=1)
gr.Markdown(
"""
Need help with estimating amount?
Run our cost estimator to get important details to consider.
Read the recommendations, decide on a number, and fill in the field above.
"""
)
with gr.Row(variant='panel'):
amount_estimate_text = gr.Textbox(
label="Guide on How Much to Request",
value="Click on \"Estimate amount\" to get results",
lines=3,
scale=5
)
cost_calc_button = gr.Button(value="Estimate amount")
org_mission_statement_text = gr.Textbox(
label="Mission statement of your organization",
interactive=True, lines=2
)
with gr.Accordion("Need Help with writing mission statement? Expand to see details", open=False):
gr.Markdown(
"""
Need help with writing mission statement?
Enter info to include in mission statement, and click "Get help" to generate.
"""
)
textbox_info = gr.Textbox(label="Enter info")
button_help = gr.Button("Get help")
# pylint: disable=no-member
button_help.click(
help_mission_statement,
inputs=[org_name_text, textbox_info],
outputs=[org_mission_statement_text],
show_api=False,
api_name=False,
)
foundation_mission_statement_text = gr.Textbox(
label="Mission statement of the foundation",
interactive=True, lines=2
)
prior_contact_text = gr.Textbox(
label="Prior contact with or prior funding from the foundation (if applicable)",
interactive=True, lines=1
)
connection_text = gr.Textbox(label="Connection to the foundation's guidelines", lines=1)
with gr.Accordion():
gr.Markdown("""
Step 4: Additional questions
Consider answering the following questions to make your LOI sections more detailed and
compelling
""")
gr.Markdown("Organization Description
")
capacity_text = gr.Textbox(
label="What is capacity of the organization to meet the stated need?",
interactive=True, lines=3
)
history_text = gr.Textbox(
label=(
"Share a very brief history/description of current programs that showcase credibility "
"and commitment."
),
interactive=True, lines=3
)
path_text = gr.Textbox(
label=(
"Demonstrate direct connection between what is currently being done and what you wish to "
"accomplish with the requested funding."
),
interactive=True, lines=3
)
accomplishment_text = gr.Textbox(
label="What recent organizational accomplishment is most aligned with this project?",
interactive=True, lines=3
)
gr.Markdown("Need Statement
")
target_text = gr.Textbox(
label=(
"A description of the target population and geographical area. Who will benefit from this "
"project?"
),
interactive=True, lines=2
)
data_text = gr.Textbox(
label=(
"Appropriate statistical data in abbreviated form."
),
info="Please cite reliable sources: current media, research studies / reports",
interactive=True, lines=3
)
gr.Markdown(
"""Need help with finding data sources?
See below for selected statistics sources recommended by Candid Learning.
"""
)
with gr.Row():
category_dropdown = gr.Dropdown(
label="Select One or More Categories",
choices=list(CATEGORIES.keys()),
multiselect=True
)
data_source_button = gr.Button(value="Get Resource Links by Category")
link_markdown = gr.Markdown()
gr.Markdown("Project Description
")
desired_objectives_text = gr.Textbox(
label="Describe the desired objectives for this project.",
interactive=True, lines=2
)
major_activities_text = gr.Textbox(
label="Describe the major activities of this project.",
interactive=True, lines=2
)
key_staff_text = gr.Textbox(
label="Names and titles of key project staff.",
interactive=True, lines=2
)
stand_out_text = gr.Textbox(
label="What is going to make your project/organization stand out?",
interactive=True, lines=2
)
success_text = gr.Textbox(label="How will you know if the project is successful?", lines=2)
gr.Markdown("Funding Request
")
funding_history_text = gr.Textbox(label="What is your funding history?", interactive=True, lines=2)
other_funding_text = gr.Textbox(
label=(
"Other funding sources being approached for support of this project should be listed in a "
"brief sentence/paragraph."
),
interactive=True, lines=3
)
follow_up_text = gr.Textbox(
label=(
"Affirm your readiness to answer further questions and suggest how the funder can learn "
"more. Be clear about how to follow up."
),
interactive=True, lines=2
)
gr.Markdown(
"""Step 5: Review and submit
Review your input and submit for generation
""")
write_btn = gr.Button("Write LOI")
with gr.Column():
gr.Markdown(
"""Step 6: Review the generated sections
Click on "Edit Section" to run the editorial AI for rewrite suggestions, or make your own edits.
""")
opening = gr.Textbox(label="Opening", interactive=True, lines=3)
create_editable_section("Opening", JS, update_highlighted_text, opening)
org_desc = gr.Textbox(label="Organization Description", interactive=True, lines=10)
create_editable_section("Organization Description", JS, update_highlighted_text, org_desc)
need = gr.Textbox(label="Need", interactive=True, lines=10)
create_editable_section("Need", JS, update_highlighted_text, need)
project_desc = gr.Textbox(label="Project Description", interactive=True, lines=10)
create_editable_section("Project Description", JS, update_highlighted_text, project_desc)
fund_request = gr.Textbox(label="Funding Request", interactive=True, lines=5)
create_editable_section("Funding Request", JS, update_highlighted_text, fund_request)
conclusion = gr.Textbox(label="Conclusion", interactive=True, lines=5)
create_editable_section("Conclusion", JS, update_highlighted_text, conclusion)
gr.Markdown(
"""Step 7: Output to file
Select your preferred document type and scroll to the bottom of the page to download the file.
""")
with gr.Row():
download_pdf_button = gr.Button("Download as PDF")
download_word_button = gr.Button("Download as docx")
# pylint: disable=no-member
download_pdf_button.click(
generate_pdf,
inputs=[opening, org_desc, need, project_desc, fund_request, conclusion],
outputs=gr.File(label="Download as PDF"),
scroll_to_output=True,
show_api=False,
api_name=False,
)
# pylint: disable=no-member
download_word_button.click(
generate_docx,
inputs=[opening, org_desc, need, project_desc, fund_request, conclusion],
outputs=gr.File(label="Download as docx"),
scroll_to_output=True,
show_api=False,
api_name=False,
)
# pylint: disable=no-member
autofill_btn.click(
fn=organization_pair_autofill,
inputs=[org_name_text, org_ein_text, foundation_name_text, foundation_ein_text],
outputs=[
projects_text,
capacity_text,
follow_up_text,
accomplishment_text,
org_mission_statement_text,
foundation_mission_statement_text,
funding_history_text,
recip_json,
recip_candid_id,
funder_json,
funder_candid_id
],
scroll_to_output=True,
show_api=False,
api_name=False,
queue=True,
)
cost_calc_button.click(
fn=cost_estimator,
inputs=[
recip_candid_id,
recip_json,
funder_json,
projects_text
],
outputs=[amount_estimate_text],
show_api=False,
api_name=False,
queue=True
)
data_source_button.click(fn=update_links, inputs=category_dropdown, outputs=link_markdown, show_api=False)
write_btn.click(
fn=draft_letter,
inputs=[
org_name_text,
foundation_name_text,
projects_text,
amount_text,
org_mission_statement_text,
foundation_mission_statement_text,
project_name_text,
project_purpose_text,
prior_contact_text,
connection_text,
capacity_text,
path_text,
accomplishment_text,
history_text,
funding_history_text,
target_text,
data_text,
desired_objectives_text,
major_activities_text,
key_staff_text,
stand_out_text,
success_text,
other_funding_text,
follow_up_text
],
outputs=[opening, org_desc, need, project_desc, fund_request, conclusion],
scroll_to_output=True,
show_api=False,
api_name=False,
queue=True,
)
logged = LoggedComponents(
context=[
recip_candid_id,
funder_candid_id,
org_mission_statement_text,
foundation_mission_statement_text,
project_name_text,
project_purpose_text,
amount_text,
prior_contact_text,
connection_text,
capacity_text,
history_text,
accomplishment_text,
target_text,
data_text,
desired_objectives_text,
funding_history_text,
other_funding_text
],
letter=[opening, org_desc, need, project_desc, fund_request, conclusion]
)
return logged, demo
def build_feedback(components: LoggedComponents) -> gr.Blocks:
def handle_feedback(*args):
try:
context = {
"recipient_candid_entity_id": args[0],
"funder_candid_entity_id": args[1],
"recipient_mission": args[2],
"funder_mission": args[3],
"project_name": args[4],
"project_purpose": args[5],
"amount": args[6],
"prior_contact": args[7],
"connection": args[8],
"capacity": args[9],
"history": args[10],
"accomplishment": args[11],
"target": args[12],
"data": args[13],
"desired_objectives": args[14],
"funding_history": args[15],
"other_funding": args[16],
}
letter_fields = (
"opening", "org_description", "need",
"project_description", "funding_request", "conclusion"
)
letter = dict(zip(letter_fields, args[17: (17 + 6)]))
send_feedback(
context=context,
letter=letter,
relevance=args[-6],
coherence=args[-5],
fluency=args[-4],
overall_quality=args[-3],
comments=args[-2],
email=args[-1]
)
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: {ex} :: {error_msg}")
raise gr.Error(f"Failed to submit feedback: {ex}")
with gr.Blocks(theme=gr.themes.Soft(), title="LOI writer") as demo:
gr.Markdown("Help us improve this tool with your valuable feedback
")
gr.Markdown(
" Please rate each of the following aspects on a rating scale of 1-5, "
"where 1 is 'very poor' and 5 is 'very good'.
"
)
with gr.Row():
with gr.Column():
relevance = gr.Radio(
[1, 2, 3, 4, 5],
label="Relevance",
info=(
"Relevance measures the extent to which the model's generated responses are pertinent "
"and directly related to the given questions."
)
)
coherence = gr.Radio(
[1, 2, 3, 4, 5],
label="Coherence",
info=(
"Coherence evaluates how well the language model can produce output that flows smoothly, "
"reads naturally, and resembles human-like language."
)
)
fluency = gr.Radio(
[1, 2, 3, 4, 5],
label="Fluency",
info=(
"Fluency evaluates the language proficiency of a generative AI's predicted answer. It assesses "
"how well the generated text adheres to grammatical rules, syntactic structures, and "
"appropriate usage of vocabulary, resulting in linguistically correct and "
"natural-sounding responses."
)
)
overall_quality = gr.Radio([1, 2, 3, 4, 5], label="Overall Quality")
comment = gr.Textbox(label="Additional comments (optional)", lines=4)
email = gr.Textbox(label="Your email (optional)", lines=1)
components["relevance"] = relevance
components["coherence"] = coherence
components["fluency"] = fluency
components["overall_quality"] = overall_quality
components["comments"] = comment
components["email"] = email
submit = gr.Button("Submit Feedback")
# 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, drafter = build_drafter()
feedback = build_feedback(logger)
return gr.TabbedInterface(
interface_list=[drafter, feedback],
tab_names=["LOI writer", "Feedback"],
title="Candid's letter of intent (LOI) writer",
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",
)