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", )