Spaces:
Sleeping
Sleeping
First commit
Browse files- LICENSE +21 -0
- README.md +8 -9
- __init__.py +0 -0
- app.py +108 -0
- common/__init__.py +0 -0
- common/base/__init__.py +0 -0
- common/base/api_base.py +42 -0
- common/base/api_base_async.py +48 -0
- common/base/utils.py +14 -0
- common/org_search_component.py +223 -0
- formatting.py +45 -0
- requirements.txt +4 -0
- services.py +52 -0
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,14 +1,13 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
|
4 |
-
|
5 |
-
|
|
|
6 |
sdk: gradio
|
7 |
-
sdk_version: 5.
|
8 |
app_file: app.py
|
9 |
-
pinned:
|
10 |
license: mit
|
11 |
-
short_description: Get recommendations for funding opportunities
|
12 |
---
|
13 |
-
|
14 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
1 |
---
|
2 |
+
title: RFP recommendations
|
3 |
+
short_description: Get recommendations for funding opportunities
|
4 |
+
emoji: 🤑
|
5 |
+
colorFrom: green
|
6 |
+
colorTo: blue
|
7 |
sdk: gradio
|
8 |
+
sdk_version: 5.5.0
|
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
ADDED
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
import gradio as gr
|
4 |
+
|
5 |
+
from common import org_search_component as oss
|
6 |
+
from formatting import process_reasons, parse_pcs_descriptions, parse_geo_descriptions
|
7 |
+
from services import RfpRecommend
|
8 |
+
|
9 |
+
api = RfpRecommend()
|
10 |
+
|
11 |
+
|
12 |
+
def recommend_invoke(recipient: gr.State):
|
13 |
+
response = api(candid_entity_id=recipient[0])
|
14 |
+
output = []
|
15 |
+
for rfp in (response.get("recommendations", []) or []):
|
16 |
+
output.append([
|
17 |
+
rfp["funder_id"],
|
18 |
+
rfp["funder_name"],
|
19 |
+
rfp["funder_address"],
|
20 |
+
rfp["amount"],
|
21 |
+
(
|
22 |
+
f"<a href='{rfp['application_url']}' target='_blank' rel='noopener noreferrer'>"
|
23 |
+
f"{rfp['application_url']}</a>"
|
24 |
+
),
|
25 |
+
rfp["deadline"],
|
26 |
+
rfp["description"],
|
27 |
+
parse_pcs_descriptions(rfp["taxonomy"]),
|
28 |
+
parse_geo_descriptions(rfp["area_served"])
|
29 |
+
])
|
30 |
+
return (
|
31 |
+
output,
|
32 |
+
process_reasons(response.get("meta", {}) or {}),
|
33 |
+
response.get("recommendations", [])
|
34 |
+
)
|
35 |
+
|
36 |
+
|
37 |
+
def build_demo():
|
38 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="RFP recommendations") as demo:
|
39 |
+
gr.Markdown(
|
40 |
+
"""
|
41 |
+
<h1>RFP recommendations</h1>
|
42 |
+
|
43 |
+
<p>Receive recommendations for funding opportunities relevant to your work.</p>
|
44 |
+
<p>To get started lookup your nonprofit and then click **Get recommendations**.</p>
|
45 |
+
"""
|
46 |
+
)
|
47 |
+
|
48 |
+
with gr.Row():
|
49 |
+
with gr.Column():
|
50 |
+
_, selected_org_state = oss.render()
|
51 |
+
with gr.Row():
|
52 |
+
recommend = gr.Button("Get recommendations", scale=5, variant="primary")
|
53 |
+
with gr.Row():
|
54 |
+
with gr.Accordion(label="Parameters used for recommendations", open=False):
|
55 |
+
reasons_output = gr.DataFrame(
|
56 |
+
col_count=3,
|
57 |
+
headers=["Reason category", "Reason value", "Reason description"],
|
58 |
+
interactive=False
|
59 |
+
)
|
60 |
+
|
61 |
+
rec_outputs = gr.DataFrame(
|
62 |
+
label="Recommended RFPs",
|
63 |
+
type="array",
|
64 |
+
headers=[
|
65 |
+
"Funder ID", "Name", "Address",
|
66 |
+
"Amount", "Application URL", "Deadline",
|
67 |
+
"Description", "About", "Where"
|
68 |
+
],
|
69 |
+
col_count=(9, "fixed"),
|
70 |
+
datatype=[
|
71 |
+
"number", "str", "str",
|
72 |
+
"str", "markdown", "date",
|
73 |
+
"str", "markdown", "markdown"
|
74 |
+
],
|
75 |
+
wrap=True,
|
76 |
+
max_height=1000,
|
77 |
+
column_widths=[
|
78 |
+
"5%", "10%", "20%",
|
79 |
+
"5", "15%", "5%",
|
80 |
+
"10%", "10%", "20%"
|
81 |
+
],
|
82 |
+
interactive=False
|
83 |
+
)
|
84 |
+
|
85 |
+
with gr.Accordion("JSON output", open=False):
|
86 |
+
recommendations_json = gr.JSON(label="Recommended RFPs JSON")
|
87 |
+
|
88 |
+
# pylint: disable=no-member
|
89 |
+
recommend.click(
|
90 |
+
fn=recommend_invoke,
|
91 |
+
inputs=[selected_org_state],
|
92 |
+
outputs=[rec_outputs, reasons_output, recommendations_json]
|
93 |
+
)
|
94 |
+
|
95 |
+
return demo
|
96 |
+
|
97 |
+
|
98 |
+
if __name__ == '__main__':
|
99 |
+
app = build_demo()
|
100 |
+
app.queue(max_size=5).launch(
|
101 |
+
show_api=False,
|
102 |
+
auth=[
|
103 |
+
(os.getenv("APP_USERNAME"), os.getenv("APP_PASSWORD")),
|
104 |
+
(os.getenv("APP_PUBLIC_USERNAME"), os.getenv("APP_PUBLIC_PASSWORD")),
|
105 |
+
],
|
106 |
+
auth_message="Login to Candid's letter of intent demo",
|
107 |
+
ssr_mode=False
|
108 |
+
)
|
common/__init__.py
ADDED
File without changes
|
common/base/__init__.py
ADDED
File without changes
|
common/base/api_base.py
ADDED
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Optional, Any
|
2 |
+
|
3 |
+
from urllib3.util.retry import Retry
|
4 |
+
from requests.adapters import HTTPAdapter
|
5 |
+
import requests
|
6 |
+
|
7 |
+
|
8 |
+
class BaseAPI:
|
9 |
+
|
10 |
+
def __init__(
|
11 |
+
self,
|
12 |
+
url: str,
|
13 |
+
headers: Optional[Dict[str, Any]] = None,
|
14 |
+
total_retries: int = 3,
|
15 |
+
backoff_factor: int = 2
|
16 |
+
) -> None:
|
17 |
+
total_retries = max(total_retries, 10)
|
18 |
+
|
19 |
+
adapter = HTTPAdapter(
|
20 |
+
max_retries=Retry(
|
21 |
+
total=total_retries,
|
22 |
+
status_forcelist=[429, 500, 502, 503, 504],
|
23 |
+
allowed_methods=frozenset({"HEAD", "GET", "POST", "OPTIONS"}),
|
24 |
+
backoff_factor=backoff_factor,
|
25 |
+
)
|
26 |
+
)
|
27 |
+
self.session = requests.Session()
|
28 |
+
self.session.mount("https://", adapter)
|
29 |
+
self.session.mount("http://", adapter)
|
30 |
+
|
31 |
+
self.__url = url
|
32 |
+
self.__headers = headers
|
33 |
+
|
34 |
+
def get(self, **request_kwargs):
|
35 |
+
r = self.session.get(url=self.__url, headers=self.__headers, params=request_kwargs, timeout=30)
|
36 |
+
r.raise_for_status()
|
37 |
+
return r.json()
|
38 |
+
|
39 |
+
def post(self, payload: Dict[str, Any]):
|
40 |
+
r = self.session.post(url=self.__url, headers=self.__headers, json=payload, timeout=30)
|
41 |
+
r.raise_for_status()
|
42 |
+
return r.json()
|
common/base/api_base_async.py
ADDED
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Dict, Optional, Any
|
2 |
+
import json
|
3 |
+
|
4 |
+
import aiohttp
|
5 |
+
|
6 |
+
|
7 |
+
class BaseAsyncAPI:
|
8 |
+
|
9 |
+
def __init__(self, url: str, headers: Optional[Dict[str, Any]] = None, retries: int = 3) -> None:
|
10 |
+
self.__url = url
|
11 |
+
self.__headers = headers
|
12 |
+
self.__retries = max(retries, 5)
|
13 |
+
|
14 |
+
async def get(self, **request_kwargs):
|
15 |
+
session_timeout = aiohttp.ClientTimeout(total=30)
|
16 |
+
async with aiohttp.ClientSession(headers=self.__headers, timeout=session_timeout) as session:
|
17 |
+
output = {}
|
18 |
+
count = 1
|
19 |
+
while True:
|
20 |
+
if count >= self.__retries:
|
21 |
+
break
|
22 |
+
async with session.get(url=self.__url, params=request_kwargs) as r:
|
23 |
+
if r.status in {429, 500, 502, 503, 504}:
|
24 |
+
count += 1
|
25 |
+
elif r.status == 200:
|
26 |
+
output = await r.json()
|
27 |
+
break
|
28 |
+
else:
|
29 |
+
break
|
30 |
+
return output
|
31 |
+
|
32 |
+
async def post(self, payload: Dict[str, Any]):
|
33 |
+
session_timeout = aiohttp.ClientTimeout(total=30)
|
34 |
+
async with aiohttp.ClientSession(headers=self.__headers, timeout=session_timeout) as session:
|
35 |
+
output = {}
|
36 |
+
count = 1
|
37 |
+
while True:
|
38 |
+
if count >= self.__retries:
|
39 |
+
break
|
40 |
+
async with session.post(url=self.__url, data=json.dumps(payload).encode('utf8')) as r:
|
41 |
+
if r.status in {429, 500, 502, 503, 504}:
|
42 |
+
count += 1
|
43 |
+
elif r.status == 200:
|
44 |
+
output = await r.json()
|
45 |
+
break
|
46 |
+
else:
|
47 |
+
break
|
48 |
+
return output
|
common/base/utils.py
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import asyncio
|
2 |
+
|
3 |
+
|
4 |
+
def async_tasks(*tasks):
|
5 |
+
|
6 |
+
async def gather(*t):
|
7 |
+
t = [await _ for _ in t]
|
8 |
+
return await asyncio.gather(*t)
|
9 |
+
|
10 |
+
loop = asyncio.new_event_loop()
|
11 |
+
results = loop.run_until_complete(gather(*tasks))
|
12 |
+
loop.stop()
|
13 |
+
loop.close()
|
14 |
+
return results
|
common/org_search_component.py
ADDED
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Tuple, Optional, Any
|
2 |
+
import os
|
3 |
+
|
4 |
+
import gradio as gr
|
5 |
+
|
6 |
+
try:
|
7 |
+
from base.api_base import BaseAPI
|
8 |
+
except ImportError:
|
9 |
+
from .base.api_base import BaseAPI
|
10 |
+
|
11 |
+
|
12 |
+
class OrgSearch(BaseAPI):
|
13 |
+
|
14 |
+
def __init__(self):
|
15 |
+
super().__init__(
|
16 |
+
url=f"{os.getenv('CDS_API_URL')}/v1/organization/search",
|
17 |
+
headers={"x-api-key": os.getenv('CDS_API_KEY')}
|
18 |
+
)
|
19 |
+
|
20 |
+
def __call__(self, name: str, **kwargs):
|
21 |
+
is_valid = False
|
22 |
+
|
23 |
+
payload = {
|
24 |
+
"names": [{
|
25 |
+
"value": name,
|
26 |
+
"type": "main"
|
27 |
+
}],
|
28 |
+
"status": "authorized"
|
29 |
+
}
|
30 |
+
|
31 |
+
if kwargs.get("ein"):
|
32 |
+
ein = kwargs.get("ein")
|
33 |
+
if "-" not in ein:
|
34 |
+
ein = f"{ein[:2]}-{ein[2:]}"
|
35 |
+
payload["ids"] = [{
|
36 |
+
"value": ein,
|
37 |
+
"type": "ein"
|
38 |
+
}]
|
39 |
+
is_valid = True
|
40 |
+
|
41 |
+
if kwargs.get("street") or kwargs.get("city") or kwargs.get("state") or kwargs.get("postal_code"):
|
42 |
+
payload["addresses"] = [{
|
43 |
+
"street1": kwargs.get("street") or "",
|
44 |
+
"city": kwargs.get("city") or "",
|
45 |
+
"state": kwargs.get("state") or "",
|
46 |
+
"postal_code": kwargs.get("postal_code") or ""
|
47 |
+
}]
|
48 |
+
is_valid = True
|
49 |
+
|
50 |
+
if not is_valid:
|
51 |
+
return None
|
52 |
+
|
53 |
+
result = self.post(payload=payload)
|
54 |
+
return result.get("payload", [])
|
55 |
+
|
56 |
+
|
57 |
+
search_org = OrgSearch()
|
58 |
+
|
59 |
+
|
60 |
+
def callback_to_state(event: gr.SelectData, state: gr.State) -> Tuple[List[Any], int]:
|
61 |
+
"""Handles DataFrame `select` events. Updates the internal state for either the recipient or funder based on
|
62 |
+
selection. Also sends along the selected Candid entity ID to the proposal generation tab.
|
63 |
+
|
64 |
+
Parameters
|
65 |
+
----------
|
66 |
+
event : gr.SelectData
|
67 |
+
state : gr.State
|
68 |
+
|
69 |
+
Returns
|
70 |
+
-------
|
71 |
+
Tuple[List[Any], int]
|
72 |
+
(Updated state, Candid entity ID)
|
73 |
+
"""
|
74 |
+
|
75 |
+
row, _ = event.index
|
76 |
+
|
77 |
+
if len(state) == 0:
|
78 |
+
return [], None
|
79 |
+
|
80 |
+
# the state should be a nested list of lists
|
81 |
+
# if the state is a single list with non-list elements then we just want a pass-through
|
82 |
+
if all(isinstance(s, list) for s in state):
|
83 |
+
return state[row], state[row][0]
|
84 |
+
return state, state[0]
|
85 |
+
|
86 |
+
|
87 |
+
def lookup_organization(
|
88 |
+
name: str,
|
89 |
+
ein: Optional[str] = None,
|
90 |
+
# street: Optional[str] = None,
|
91 |
+
city: Optional[str] = None,
|
92 |
+
state: Optional[str] = None,
|
93 |
+
postal: Optional[str] = None,
|
94 |
+
) -> Tuple[List[List[str]], List[List[str]]]:
|
95 |
+
"""Performs a simple search using the CDS organization search API. Results are sent to the DataFrame table and also
|
96 |
+
populate the state for the recipient information.
|
97 |
+
|
98 |
+
Parameters
|
99 |
+
----------
|
100 |
+
name : str
|
101 |
+
Org name
|
102 |
+
ein : Optional[str], optional
|
103 |
+
Org EIN, by default None
|
104 |
+
street : Optional[str], optional
|
105 |
+
Street address, by default None
|
106 |
+
city : Optional[str], optional
|
107 |
+
Address city, by default None
|
108 |
+
state : Optional[str], optional
|
109 |
+
Address state, by default None
|
110 |
+
postal : Optional[str], optional
|
111 |
+
Address postal code, by default None
|
112 |
+
|
113 |
+
Returns
|
114 |
+
-------
|
115 |
+
Tuple[List[List[str]], List[List[str]]]
|
116 |
+
(recip data, recip data)
|
117 |
+
|
118 |
+
Raises
|
119 |
+
------
|
120 |
+
gr.Error
|
121 |
+
Raised if not enough information was entered to run a search
|
122 |
+
gr.Error
|
123 |
+
Raised if no search results were returned
|
124 |
+
"""
|
125 |
+
|
126 |
+
results = search_org(name=name, ein=ein, city=city, state=state, postal=postal)
|
127 |
+
if results is None:
|
128 |
+
raise gr.Error("You must provide a name, and either an EIN or an address.")
|
129 |
+
if not results:
|
130 |
+
raise gr.Error("No organizations could be found. Please refine the search criteria.")
|
131 |
+
|
132 |
+
data = []
|
133 |
+
for applicant_data in results:
|
134 |
+
address = applicant_data.get("addresses", [{}])[0].get("normalized")
|
135 |
+
seal = (applicant_data.get("current_seal", {}) or {}).get("image")
|
136 |
+
|
137 |
+
record = [
|
138 |
+
applicant_data.get('candid_entity_id'),
|
139 |
+
applicant_data.get('main_sort_name'),
|
140 |
+
address
|
141 |
+
]
|
142 |
+
|
143 |
+
if seal:
|
144 |
+
record.append(f"")
|
145 |
+
else:
|
146 |
+
record.append("")
|
147 |
+
|
148 |
+
data.append(record)
|
149 |
+
return data, data
|
150 |
+
|
151 |
+
|
152 |
+
def render(org_id_element: Optional[gr.Blocks] = None) -> Tuple[gr.Blocks, gr.State]:
|
153 |
+
"""Main blocks build and render function.
|
154 |
+
|
155 |
+
Parameters
|
156 |
+
----------
|
157 |
+
org_id_element : Optional[gr.Blocks], optional
|
158 |
+
Callback Gradio element, by default None
|
159 |
+
|
160 |
+
Returns
|
161 |
+
-------
|
162 |
+
Tuple[gr.Blocks, gr.State]
|
163 |
+
(component, selected org state)
|
164 |
+
"""
|
165 |
+
|
166 |
+
with gr.Blocks() as component:
|
167 |
+
org_data = gr.State([])
|
168 |
+
selected_org_data = gr.State([])
|
169 |
+
|
170 |
+
with gr.Row():
|
171 |
+
with gr.Column(scale=2):
|
172 |
+
name = gr.Textbox(label="Name of organization", lines=1)
|
173 |
+
ein = gr.Textbox(label="EIN of organization", lines=1)
|
174 |
+
with gr.Column(scale=3):
|
175 |
+
with gr.Group():
|
176 |
+
with gr.Row():
|
177 |
+
with gr.Column():
|
178 |
+
# street = gr.Textbox(label="Street address", lines=1)
|
179 |
+
city = gr.Textbox(label="City", lines=1)
|
180 |
+
with gr.Column():
|
181 |
+
state = gr.Textbox(label="State/province", lines=1)
|
182 |
+
postal = gr.Textbox(label="Postal code", lines=1)
|
183 |
+
|
184 |
+
search_button = gr.Button("Find organization", variant="primary")
|
185 |
+
org_info = gr.DataFrame(
|
186 |
+
label="Organizations",
|
187 |
+
type="array",
|
188 |
+
headers=["Candid ID", "Name", "Address", "Seal"],
|
189 |
+
col_count=(4, "fixed"),
|
190 |
+
datatype=["number", "str", "str", "markdown"],
|
191 |
+
wrap=True,
|
192 |
+
column_widths=["20%", "30%", "30%", "20%"]
|
193 |
+
)
|
194 |
+
|
195 |
+
if org_id_element is None:
|
196 |
+
org_id_element = gr.Textbox(label="Selected Candid entity ID", lines=1)
|
197 |
+
|
198 |
+
# pylint: disable=no-member
|
199 |
+
search_button.click(
|
200 |
+
fn=lambda name, ein, city, state, postal: lookup_organization(
|
201 |
+
name=name,
|
202 |
+
ein=ein,
|
203 |
+
# street=street,
|
204 |
+
city=city,
|
205 |
+
state=state,
|
206 |
+
postal=postal
|
207 |
+
),
|
208 |
+
# inputs=[name, ein, street, city, state, postal],
|
209 |
+
inputs=[name, ein, city, state, postal],
|
210 |
+
outputs=[org_info, org_data],
|
211 |
+
api_name=False,
|
212 |
+
show_api=False
|
213 |
+
)
|
214 |
+
|
215 |
+
# pylint: disable=no-member
|
216 |
+
org_info.select(
|
217 |
+
fn=callback_to_state,
|
218 |
+
inputs=org_data,
|
219 |
+
outputs=[selected_org_data, org_id_element],
|
220 |
+
api_name=False,
|
221 |
+
show_api=False
|
222 |
+
)
|
223 |
+
return component, selected_org_data
|
formatting.py
ADDED
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List, Tuple, Dict, Any
|
2 |
+
|
3 |
+
|
4 |
+
def process_reasons(reasons: dict) -> List[Tuple[str, str, str]]:
|
5 |
+
"""Proprocessing for recommendation reasons data to display in a dataframe.
|
6 |
+
|
7 |
+
Parameters
|
8 |
+
----------
|
9 |
+
reasons : dict
|
10 |
+
|
11 |
+
Returns
|
12 |
+
-------
|
13 |
+
List[Tuple[str, str, str]]
|
14 |
+
(attribute type, attribute code, attribute description)
|
15 |
+
"""
|
16 |
+
|
17 |
+
output = []
|
18 |
+
for code, desc in (reasons.get("subject_codes", {}) or {}).items():
|
19 |
+
output.append(["PCS subject", code, desc])
|
20 |
+
|
21 |
+
for code, desc in (reasons.get("pop_group_codes", {}) or {}).items():
|
22 |
+
output.append(["PCS population", code, desc])
|
23 |
+
|
24 |
+
for code, desc in (reasons.get("geo_ids", {}) or {}).items():
|
25 |
+
output.append(["Geography served", code, desc])
|
26 |
+
return output
|
27 |
+
|
28 |
+
|
29 |
+
def parse_pcs_descriptions(taxonomy: List[Dict[str, Any]]) -> str:
|
30 |
+
reasons = []
|
31 |
+
|
32 |
+
for term in taxonomy:
|
33 |
+
if term.get("facet") == "subject":
|
34 |
+
reasons.append(f"- For <b>{term.get('description')}</b>")
|
35 |
+
elif term.get("facet") == "population":
|
36 |
+
reasons.append(f"- Serving <b>{term.get('description')}</b>")
|
37 |
+
return '\n'.join(reasons)
|
38 |
+
|
39 |
+
|
40 |
+
def parse_geo_descriptions(geos: List[Dict[str, Any]]) -> str:
|
41 |
+
reasons = []
|
42 |
+
|
43 |
+
for geo in geos:
|
44 |
+
reasons.append(f"- Serving <b>{geo['name']}</b>")
|
45 |
+
return '\n'.join(reasons)
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
aiohttp
|
2 |
+
gradio
|
3 |
+
requests
|
4 |
+
urllib3
|
services.py
ADDED
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
|
3 |
+
from common.base.api_base_async import BaseAsyncAPI
|
4 |
+
from common.base.api_base import BaseAPI
|
5 |
+
|
6 |
+
|
7 |
+
class RfpRecommend(BaseAPI):
|
8 |
+
|
9 |
+
def __init__(self):
|
10 |
+
super().__init__(
|
11 |
+
url=os.getenv("RFP_RECOMMENDATION_URL"),
|
12 |
+
headers={"x-api-key": os.getenv("RFP_RECOMMENDATION_API_KEY")},
|
13 |
+
total_retries=5,
|
14 |
+
backoff_factor=10
|
15 |
+
)
|
16 |
+
|
17 |
+
def __call__(self, candid_entity_id: int):
|
18 |
+
results = self.get(candid_entity_id=candid_entity_id)
|
19 |
+
return results
|
20 |
+
|
21 |
+
|
22 |
+
class Inferencing(BaseAsyncAPI):
|
23 |
+
|
24 |
+
def __init__(self):
|
25 |
+
super().__init__(
|
26 |
+
url=os.getenv("AUTOCODING_API_URL"),
|
27 |
+
headers={"x-api-key": os.getenv("AUTOCODING_API_KEY")}
|
28 |
+
)
|
29 |
+
|
30 |
+
async def __call__(self, text: str, taxonomy: str = "pcs",
|
31 |
+
chunk: bool = False, high_precision: bool = False
|
32 |
+
):
|
33 |
+
result = self.post(payload={
|
34 |
+
"text": text,
|
35 |
+
"taxonomy": taxonomy,
|
36 |
+
"chunk": chunk,
|
37 |
+
"high_precision": high_precision
|
38 |
+
})
|
39 |
+
return result
|
40 |
+
|
41 |
+
|
42 |
+
class Entities(BaseAsyncAPI):
|
43 |
+
|
44 |
+
def __init__(self):
|
45 |
+
super().__init__(
|
46 |
+
url=os.getenv("DOCUMENT_API_URL").replace("/analyze", "/entities"),
|
47 |
+
headers={"x-api-key": os.getenv("DOCUMENT_API_KEY")}
|
48 |
+
)
|
49 |
+
|
50 |
+
async def __call__(self, text: str):
|
51 |
+
result = self.post(payload={"text": text})
|
52 |
+
return result
|