brainsqueeze commited on
Commit
7f8b26c
·
verified ·
1 Parent(s): 96a926d

Upload 9 files

Browse files
Files changed (10) hide show
  1. .gitattributes +1 -0
  2. ARIAL.TTF +3 -0
  3. LICENSE +21 -0
  4. README.md +8 -8
  5. __init__.py +0 -0
  6. app.py +4 -0
  7. constants.py +102 -0
  8. editor.py +98 -0
  9. requirements.txt +7 -0
  10. services.py +221 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ ARIAL.TTF filter=lfs diff=lfs merge=lfs -text
ARIAL.TTF ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:baa251526d6862712a58e613ef451d8a2b60482142ec6aab1d47fb8e23e21a7c
3
+ size 1045960
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Candid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,13 +1,13 @@
1
  ---
2
- title: Nonprofit Letters Of Intent
3
- emoji: 🔥
4
- colorFrom: pink
5
- colorTo: pink
 
6
  sdk: gradio
7
- sdk_version: 4.41.0
8
  app_file: app.py
9
- pinned: false
10
  license: mit
11
  ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Nonprofit Letters of Intent
3
+ short_description: Writing assistant for nonprofit letters of intent
4
+ emoji: 📝💡
5
+ colorFrom: yellow
6
+ colorTo: red
7
  sdk: gradio
8
+ sdk_version: 4.31.5
9
  app_file: app.py
10
+ pinned: true
11
  license: mit
12
  ---
13
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
__init__.py ADDED
File without changes
app.py CHANGED
@@ -583,9 +583,12 @@ def build_feedback(components: LoggedComponents) -> gr.Blocks:
583
 
584
  def build_demo():
585
  logger, drafter = build_drafter()
 
586
  feedback = build_feedback(logger)
587
 
588
  return gr.TabbedInterface(
 
 
589
  interface_list=[drafter, feedback],
590
  tab_names=["LOI writer", "Feedback"],
591
  title="Candid's letter of intent (LOI) writer",
@@ -599,6 +602,7 @@ if __name__ == '__main__':
599
  show_api=False,
600
  auth=[
601
  (os.getenv("APP_USERNAME"), os.getenv("APP_PASSWORD")),
 
602
  ],
603
  auth_message="Login to Candid's letter of intent demo"
604
  )
 
583
 
584
  def build_demo():
585
  logger, drafter = build_drafter()
586
+ # compliancy_check = build_compliancy_check()
587
  feedback = build_feedback(logger)
588
 
589
  return gr.TabbedInterface(
590
+ # interface_list=[drafter, compliancy_check, feedback],
591
+ # tab_names=["LOI writer", "Compliancy Check", "Feedback"],
592
  interface_list=[drafter, feedback],
593
  tab_names=["LOI writer", "Feedback"],
594
  title="Candid's letter of intent (LOI) writer",
 
602
  show_api=False,
603
  auth=[
604
  (os.getenv("APP_USERNAME"), os.getenv("APP_PASSWORD")),
605
+ # can add more username / password pairs
606
  ],
607
  auth_message="Login to Candid's letter of intent demo"
608
  )
constants.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dictionary of categories and their corresponding links
2
+ CATEGORIES = {
3
+ "AFFORDABLE HOUSING AND HOMELESSNESS": """
4
+ - National Low Income Housing Coalition: [Visit Site](https://nlihc.org)
5
+ - National Alliance to End Homelessness: [Visit Site](https://endhomelessness.org)
6
+ - National Coalition for the Homeless: [Visit Site](https://nationalhomeless.org)
7
+ - Department of Housing and Urban Development: [Visit Site](https://portal.hud.gov/hudportal/HUD)
8
+ """,
9
+ "AGING": """
10
+ - Administration on Aging/Data Resources: [Visit Site](https://aoa.acl.gov/Aging_Statistics/index.asp)
11
+ """,
12
+ "ARTS": """
13
+ - The Research Center for Arts and Culture: [Visit Site](https://artsandcultureresearch.org)
14
+ - Americans for the Arts: [Visit Site](https://americansforthearts.org)
15
+ """,
16
+ "CHILDREN AND POVERTY": """
17
+ - National Center for Children in Poverty: [Visit Site](https://nccp.org)
18
+ - Children’s Defense Fund: [Visit Site](https://childrensdefense.org)
19
+ - Save the Children: [Visit Site](https://savethechildren.org)
20
+ """,
21
+ "EDUCATION": """
22
+ - National Center for Education Statistics: [Visit Site](https://nces.ed.gov)
23
+ """,
24
+ "ENERGY ASSISTANCE": """
25
+ - Low Income Energy Assistance Program: [Visit Site](https://acf.hhs.gov/programs/ocs/liheap)
26
+ """,
27
+ "ENVIRONMENT": """
28
+ - Stanford Geospacial Center (GIS data online): [Visit Site](https://library.stanford.edu/research/stanford-geospatial-center/data)
29
+ - Environmental Protection Agency: [Visit Site](https://epa.gov)
30
+ """,
31
+ "HEALTH": """
32
+ - Center for Disease Control: [Visit Site](https://cdc.gov)
33
+ - State Public Health Data/Trust for America’s Health: [Visit Site](https://healthyamericans.org/states)
34
+ """,
35
+ "HUNGER": """
36
+ - Crop Walk: [Visit Site](https://crophungerwalk.org)
37
+ - Bread for the World: [Visit Site](https://bread.org)
38
+ """,
39
+ "INTERNATIONAL": """
40
+ - United Nations Statistics Division: [Visit Site](https://unstats.un.org/unsd/databases.htm)
41
+ """,
42
+ "LIVING WAGE": """
43
+ - The Living Wage Resource Center: [Visit Site](https://livingwagecampaign.org)
44
+ - Economic Policy Institute: [Visit Site](https://epi.org)
45
+ - Bureau of Labor Statistics: [Visit Site](https://bls.gov)
46
+ """,
47
+ "POVERTY": """
48
+ - U.S. Department of Health and Human Services Poverty Guidelines: [Visit Site](https://aspe.hhs.gov/poverty)
49
+ - Center of Urban Poverty: [Visit Site](https://povertycenter.case.edu)
50
+ - National Poverty Center: [Visit Site](https://npc.umich.edu)
51
+ """,
52
+ "UNITED STATES": """
53
+ - IssueLab: [Visit Site](https://issuelab.org)
54
+ - US Census: [Visit Site](https://census.gov)
55
+ - FedStats: [Visit Site](https://fedstats.sites.usa.gov)
56
+ - The Urban Institute: [Visit Site](https://urban.org)
57
+ """,
58
+ "YOUTH": """
59
+ - Youth Risk Behavioral Survey: [Visit Site](https://cdc.gov/Features/YRBS)
60
+ """
61
+ }
62
+
63
+ # Editorial ai css
64
+ CSS = """
65
+ .custom-textarea {
66
+ width: 100%;
67
+ height: 300px;
68
+ border: 1px solid #ccc;
69
+ padding: 10px;
70
+ font-size: 16px;
71
+ line-height: 1.5;
72
+ }
73
+
74
+ mark {
75
+ background-color: #ffcccc; /* Faded red */
76
+ }
77
+ """
78
+
79
+
80
+ # Editorial ai js
81
+ JS = """
82
+ document.addEventListener('DOMContentLoaded', function() {
83
+ // Listen for changes in the textarea
84
+ let textarea = document.getElementById('highlighted_textarea');
85
+ textarea.addEventListener('input', function() {
86
+ // Reapply the highlighting (simple implementation)
87
+ let content = textarea.innerText;
88
+ let highlightedContent = content;
89
+ for (let highlightText of window.highlightTexts) {
90
+ highlightedContent = highlightedContent.replace(
91
+ new RegExp(escapeRegExp(highlightText), 'g'),
92
+ `<mark>${highlightText}</mark>`
93
+ );
94
+ }
95
+ textarea.innerHTML = highlightedContent;
96
+ });
97
+ });
98
+
99
+ function escapeRegExp(string) {
100
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
101
+ }
102
+ """
editor.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Tuple
2
+ import logging
3
+
4
+ import gradio as gr
5
+
6
+ from .services import identify_vague_statements
7
+
8
+
9
+ def create_editable_section(label, js_script, submit_fn, textbox):
10
+ with gr.Accordion(f"Edit {label}", open=False) as acc:
11
+ output_text = gr.HTML(label="Highlighted Text", elem_id=f"highlighted_textarea_{label}", value=textbox)
12
+ output_text_suggestion = gr.Textbox(label="Suggestions", lines=5, interactive=True)
13
+ with gr.Row():
14
+ submit_button = gr.Button("Run Editorial AI", size="small")
15
+ accept_button = gr.Button("Apply Changes", size="small")
16
+
17
+ edit_pairs = gr.State([])
18
+
19
+ def submit_callback(default_text):
20
+ result = submit_fn(default_text)
21
+ pairs = result["pairs"]
22
+ edit_pairs.value = pairs
23
+ return result["html"], result["suggestions"]
24
+
25
+ def update_change(changes, original):
26
+ for old_text, new_text in changes:
27
+ modified = original.replace(old_text, new_text)
28
+ return modified
29
+
30
+ # pylint: disable=no-member
31
+ submit_button.click(
32
+ # fn=submit_fn,
33
+ fn=submit_callback,
34
+ inputs=textbox,
35
+ outputs=[output_text, output_text_suggestion]
36
+ )
37
+
38
+ # pylint: disable=no-member
39
+ accept_button.click(
40
+ fn=update_change,
41
+ inputs=[edit_pairs, textbox],
42
+ outputs=textbox
43
+ )
44
+
45
+ # Add custom JavaScript to make the output area editable
46
+ gr.HTML(f"<script>{js_script}</script>")
47
+ return acc
48
+
49
+
50
+ def highlight_text(input_section: str) -> Tuple[str, List[str], List[str], List[str]]:
51
+ try:
52
+ # Escape special characters for HTML
53
+ input_section = input_section.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
54
+ vague_statements, alternative_texts, reasons = identify_vague_statements(input_section)
55
+
56
+ highlighted_text = input_section
57
+ for vague_statement in vague_statements:
58
+ vague_statement = vague_statement.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
59
+ highlighted_text = highlighted_text.replace(vague_statement, f'<mark>{vague_statement}</mark>')
60
+
61
+ logging.info("Success: Highlight text")
62
+ return highlighted_text, vague_statements, alternative_texts, reasons
63
+
64
+ except Exception as e:
65
+ logging.error("Error: %s", e)
66
+ # return input_section, [], [], []
67
+ raise gr.Error("Error in Extracting vague statements..")
68
+
69
+
70
+ def update_highlighted_text(default_text: str):
71
+ gr.Info("Running Editorial AI..")
72
+ # progress = gr.Progress()
73
+ # progress(0, desc="Starting...")
74
+ highlighted_text, vague_statements, alternative_texts, reasons = highlight_text(default_text)
75
+ suggestions = ""
76
+ pairs = []
77
+ # for vague, alternative, reason in progress.tqdm(zip(vague_statements, alternative_texts, reasons)):
78
+ for vague, alternative, reason in zip(vague_statements, alternative_texts, reasons):
79
+ pairs.append((vague, alternative))
80
+ suggestions += f"Vague Statement: {vague}\nReason: {reason}\nAlternative Text: {alternative}\n\n"
81
+
82
+ html = f"""
83
+ <div id="highlighted_textarea" class="custom-textarea" contenteditable="true">
84
+ {highlighted_text}
85
+ </div>
86
+ <script>
87
+ window.highlightTexts = [{', '.join([f'`{sentence.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")}`' for sentence in vague_statements])}];
88
+ </script>
89
+ """
90
+
91
+ result = {
92
+ "html": html,
93
+ "suggestions": suggestions,
94
+ "pairs": pairs
95
+ }
96
+
97
+ logging.info("Highlighted the text successfully..")
98
+ return result
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ backoff
2
+ fpdf2
3
+ gradio
4
+ langchain
5
+ pydantic
6
+ python-docx
7
+ requests
services.py ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Tuple, Union, Optional, Any
2
+ import os
3
+
4
+ import gradio as gr
5
+
6
+ import requests
7
+ import backoff
8
+
9
+
10
+ class OrganizationNotFound(Exception):
11
+ "No organization(s) found"
12
+
13
+
14
+ @backoff.on_exception(
15
+ backoff.expo,
16
+ (requests.exceptions.Timeout, requests.exceptions.ConnectionError),
17
+ max_tries=5
18
+ )
19
+ def _get_json(route: str, **request_kwargs) -> Dict[str, Any] | str:
20
+ r = requests.get(
21
+ url=f"{os.getenv('LOI_API_URL')}/{route}",
22
+ params=request_kwargs,
23
+ headers={"x-api-key": os.getenv("LOI_API_KEY")},
24
+ timeout=30
25
+ )
26
+
27
+ r.raise_for_status()
28
+ return r.json().get("response")
29
+
30
+
31
+ @backoff.on_exception(
32
+ backoff.expo,
33
+ (requests.exceptions.Timeout, requests.exceptions.ConnectionError),
34
+ max_tries=5
35
+ )
36
+ def _post_json(route: str, *, payload: Dict[str, Any]) -> Dict[str, Any] | str | int:
37
+ r = requests.post(
38
+ url=f"{os.getenv('LOI_API_URL')}/{route}",
39
+ json=payload,
40
+ headers={"x-api-key": os.getenv("LOI_API_KEY")},
41
+ timeout=30
42
+ )
43
+
44
+ r.raise_for_status()
45
+ return r.json().get("response")
46
+
47
+
48
+ def organization_pair_autofill(
49
+ recipient_name: str,
50
+ recipient_ein: str,
51
+ funder_name: str,
52
+ funder_ein: str
53
+ ):
54
+ recip_match = _get_json("/organization/search", name=recipient_name, ein=recipient_ein)
55
+ if len(recip_match or []) == 0:
56
+ # raise OrganizationNotFound()
57
+ raise gr.Error("No matching recipient could be found")
58
+ gr.Info(f"{recipient_name} found, auto-filling fields...")
59
+
60
+ funder_match = _get_json("/organization/search", name=funder_name, ein=funder_ein)
61
+ if len(funder_match or []) == 0:
62
+ # raise OrganizationNotFound()
63
+ raise gr.Error("No matching funder could be found")
64
+ gr.Info(f"{funder_name} found, auto-filling fields...")
65
+
66
+ data = _get_json(
67
+ "/organization/autofill",
68
+ recipient_candid_entity_id=recip_match[0]["candid_entity_id"],
69
+ funder_candid_entity_id=funder_match[0]["candid_entity_id"],
70
+ )
71
+
72
+ return (
73
+ data.get("recipient_data", {}).get("projects_text"),
74
+ data.get("recipient_data", {}).get("capacity_text"),
75
+ data.get("recipient_data", {}).get("contact_text"),
76
+ data.get("recipient_data", {}).get("data_text"), # accomplishments
77
+ data.get("recipient_data", {}).get("mission_statement_text"),
78
+ data.get("funder_data", {}).get("mission_statement_text"),
79
+ data.get("funding_history_text"),
80
+ data.get("recipient_data", {}).get("org_data"),
81
+ recip_match[0]["candid_entity_id"],
82
+ data.get("funder_data", {}).get("org_data"),
83
+ funder_match[0]["candid_entity_id"],
84
+ )
85
+
86
+
87
+ def cost_estimator(
88
+ recipient_candid_entity_id: Union[int, str],
89
+ recipient_data: Dict[str, Any],
90
+ funder_data: Dict[str, Any],
91
+ program_desc: Optional[str] = None
92
+ ) -> str:
93
+ estimate: str = _post_json(
94
+ "/budget",
95
+ payload={
96
+ "recipient_candid_entity_id": recipient_candid_entity_id,
97
+ "program_description": program_desc,
98
+ "recipient_data": recipient_data,
99
+ "funder_data": funder_data,
100
+ }
101
+ )
102
+ return estimate
103
+
104
+
105
+ def identify_vague_statements(text: str) -> Tuple[List[str], List[str], List[str]]:
106
+ data = _get_json("/editorialai/vaguestatement", input_section=text)
107
+ return (
108
+ data.get("vague_statements") or [],
109
+ data.get("alternative_texts") or [],
110
+ data.get("reasons") or [],
111
+ # data.get("alternative_texts") or [],
112
+ )
113
+
114
+
115
+ def help_mission_statement(recipient_name: str, recipient_mission_info: str):
116
+ return _get_json("/writerhelper/missionstatement", info=recipient_mission_info, org_name=recipient_name)
117
+
118
+
119
+ def draft_letter(
120
+ recipient_name: str, funder_name: str, projects: str, amount: str,
121
+ recipient_mission_statement: str = "", funder_mission_statement: str = "",
122
+ project_name: str = "", project_purpose: str = "",
123
+ prior_contact: str = "", connection: str = "",
124
+ capacity: str = "", path_to_solution: str = "", recent_accomplishments: str = "",
125
+ recipient_history: str = "", mutual_history: str = "",
126
+ geo_pop_targets: str = "", project_data_stats: str = "",
127
+ desired_objectives: str = "", major_activities: str = "", key_staff: str = "", standout_features: str = "",
128
+ success_metric: str = "",
129
+ other_funding_sources: str = "",
130
+ contact_information: str = ""
131
+ ):
132
+ gr.Info("Writing the letter, please scroll to the top of the page.")
133
+ opening: str = _get_json(
134
+ "/writer/opening",
135
+ funder_name=funder_name,
136
+ recipient_name=recipient_name,
137
+ project_name=project_name,
138
+ project_purpose=project_purpose,
139
+ amount=amount,
140
+ prior_contact=prior_contact,
141
+ connection=connection
142
+ )
143
+
144
+ recipient_description: str = _get_json(
145
+ "/writer/org",
146
+ opening=opening,
147
+ recipient_mission_statement=recipient_mission_statement,
148
+ capacity=capacity,
149
+ history=recipient_history,
150
+ path=path_to_solution,
151
+ accomplishment=recent_accomplishments
152
+ )
153
+
154
+ need_statement: str = _get_json(
155
+ "/writer/need",
156
+ recipient_desc=recipient_description,
157
+ target=geo_pop_targets,
158
+ data=project_data_stats,
159
+ funder_mission_statement=funder_mission_statement
160
+ )
161
+
162
+ project_description: str = _get_json(
163
+ "/writer/project",
164
+ need=need_statement,
165
+ projects=projects,
166
+ desired_objectives=desired_objectives,
167
+ major_activities=major_activities,
168
+ key_staff=key_staff,
169
+ stand_out=standout_features,
170
+ success=success_metric
171
+ )
172
+
173
+ funding_request: str = _get_json(
174
+ "/writer/fund",
175
+ project_desc=project_description,
176
+ amount=amount,
177
+ funding_history=mutual_history,
178
+ other_funding=other_funding_sources
179
+ )
180
+
181
+ conclusion: str = _get_json(
182
+ "/writer/conclusion",
183
+ funding_request=funding_request,
184
+ project_desc=project_description,
185
+ follow_up=contact_information
186
+ )
187
+
188
+ return (
189
+ opening,
190
+ recipient_description,
191
+ need_statement,
192
+ project_description,
193
+ funding_request,
194
+ conclusion
195
+ )
196
+
197
+
198
+ def send_feedback(
199
+ context: Dict[str, Any],
200
+ letter: Dict[str, Any],
201
+ relevance: int,
202
+ coherence: int,
203
+ fluency: int,
204
+ overall_quality: int,
205
+ comments: Optional[str] = None,
206
+ email: Optional[str] = None
207
+ ) -> int:
208
+ count = _post_json(
209
+ "/feedback",
210
+ payload={
211
+ "context": context,
212
+ "letter": letter,
213
+ "relevance": relevance,
214
+ "coherence": coherence,
215
+ "fluency": fluency,
216
+ "overall_quality": overall_quality,
217
+ "comments": comments,
218
+ "email": email
219
+ }
220
+ )
221
+ return count