|
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.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) |
|
|
|
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' |
|
style.font.size = Pt(12) |
|
style.paragraph_format.line_spacing = 1.0 |
|
|
|
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]: |
|
|
|
|
|
with gr.Blocks(theme=gr.themes.Soft(), title="LOI writer", delete_cache=(30, 30)) as demo: |
|
gr.Markdown( |
|
""" |
|
<h1>Demo: LOI writer</h1> |
|
|
|
<p> |
|
Please read the <a |
|
href='https://info.candid.org/LOI-reference-guide' |
|
target="_blank" |
|
rel="noopener noreferrer" |
|
>guide</a> to get started. |
|
</p> |
|
<hr> |
|
""" |
|
) |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.Markdown( |
|
"""<h2>Step 1: Organization lookups</h2> |
|
Enter the name/EIN of your nonprofit and the name/EIN of the intended funder. |
|
Use <a href='https://beta.candid.org/' target="_blank" rel="noopener noreferrer">Candid</a> |
|
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("<div style='margin-bottom: 20px;'></div>") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=7): |
|
gr.Markdown( |
|
"""<h2>Step 2: Data autofill</h2> |
|
Autofill with data you have |
|
<a |
|
href='https://candid.org/share-your-impact' |
|
target="_blank" |
|
rel="noopener noreferrer" |
|
> shared with Candid</a>. |
|
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.Text(visible=False, interactive=False) |
|
funder_json = gr.State() |
|
|
|
funder_candid_id = gr.Text(visible=False, interactive=False) |
|
|
|
gr.HTML("<div style='margin-bottom: 20px;'></div>") |
|
gr.Markdown( |
|
""" |
|
<h2>Step 3: Add project details</h2> |
|
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( |
|
""" |
|
<strong>Need help with estimating amount?</strong> |
|
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( |
|
""" |
|
<strong>Need help with writing mission statement?</strong> |
|
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") |
|
|
|
|
|
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(""" |
|
<h2>Step 4: Additional questions</h2> |
|
Consider answering the following questions to make your LOI sections more detailed and |
|
compelling |
|
""") |
|
gr.Markdown("<h3>Organization Description</h3>") |
|
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("<h3>Need Statement</h3>") |
|
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( |
|
"""<strong>Need help with finding data sources?</strong> |
|
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("<h3>Project Description</h3>") |
|
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("<h3>Funding Request</h3>") |
|
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( |
|
"""<h2>Step 5: Review and submit</h2> |
|
Review your input and submit for generation |
|
""") |
|
write_btn = gr.Button("Write LOI") |
|
|
|
with gr.Column(): |
|
gr.Markdown( |
|
"""<h2>Step 6: Review the generated sections</h2> |
|
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( |
|
"""<h2>Step 7: Output to file</h2> |
|
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") |
|
|
|
|
|
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, |
|
) |
|
|
|
|
|
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, |
|
) |
|
|
|
|
|
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("<h1 align='center'>Help us improve this tool with your valuable feedback</h1>") |
|
gr.Markdown( |
|
"<h3> Please rate each of the following aspects on a rating scale of 1-5, " |
|
"where 1 is 'very poor' and 5 is 'very good'.</h3>" |
|
) |
|
|
|
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") |
|
|
|
|
|
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", |
|
) |
|
|