Spaces:
Running
Running
Re-add /tools
Browse files- tools/__init__.py +0 -0
- tools/config.py +5 -0
- tools/org_seach.py +188 -0
- tools/question_reformulation.py +44 -0
tools/__init__.py
ADDED
File without changes
|
tools/config.py
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
CDS_API = {
|
3 |
+
'CDS_API_URL': os.getenv('CDS_API_URL'),
|
4 |
+
'CDS_API_KEY': os.getenv('CDS_API_KEY')
|
5 |
+
}
|
tools/org_seach.py
ADDED
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import List
|
2 |
+
import re
|
3 |
+
|
4 |
+
from fuzzywuzzy import fuzz
|
5 |
+
|
6 |
+
from langchain.output_parsers.openai_tools import JsonOutputToolsParser
|
7 |
+
from langchain_openai.chat_models import ChatOpenAI
|
8 |
+
from langchain_core.runnables import RunnableSequence
|
9 |
+
from langchain_core.prompts import ChatPromptTemplate
|
10 |
+
from pydantic import BaseModel
|
11 |
+
|
12 |
+
try:
|
13 |
+
from common.org_search_component import OrgSearch
|
14 |
+
except ImportError:
|
15 |
+
from ...common.org_search_component import OrgSearch
|
16 |
+
|
17 |
+
search = OrgSearch()
|
18 |
+
|
19 |
+
|
20 |
+
class OrganizationNames(BaseModel):
|
21 |
+
orgnames: List[str]
|
22 |
+
|
23 |
+
|
24 |
+
def extract_org_links_from_chatbot(chatbot_output: str):
|
25 |
+
"""
|
26 |
+
Extracts a list of organization names from the provided text.
|
27 |
+
|
28 |
+
Args:
|
29 |
+
chatbot_output (str):The chatbot output containing organization names and other content.
|
30 |
+
|
31 |
+
Returns:
|
32 |
+
list: A list of organization names extracted from the text.
|
33 |
+
|
34 |
+
Raises:
|
35 |
+
ValueError: If parsing fails or if an unexpected output format is received.
|
36 |
+
"""
|
37 |
+
prompt = """Extract only the names of officially recognized organizations, foundations, and government entities from the text below. Do not include any entries that contain descriptions, regional identifiers, or explanations within parentheses or following the name. Strictly exclude databases, resources, crowdfunding platforms, and general terms. Provide the output only in the specified JSON format.
|
38 |
+
|
39 |
+
input text below:
|
40 |
+
|
41 |
+
```{chatbot_output}``
|
42 |
+
|
43 |
+
output format:
|
44 |
+
{{
|
45 |
+
'orgnames' : [list of organization names without any additional descriptions or identifiers]
|
46 |
+
}}
|
47 |
+
|
48 |
+
"""
|
49 |
+
|
50 |
+
try:
|
51 |
+
parser = JsonOutputToolsParser()
|
52 |
+
llm = ChatOpenAI(model="gpt-4o").bind_tools([OrganizationNames])
|
53 |
+
prompt = ChatPromptTemplate.from_template(prompt)
|
54 |
+
chain = RunnableSequence(prompt, llm, parser)
|
55 |
+
|
56 |
+
# Run the chain with the input data
|
57 |
+
result = chain.invoke({"chatbot_output": chatbot_output})
|
58 |
+
|
59 |
+
# Extract the organization names from the output
|
60 |
+
output_list = result[0]["args"].get("orgnames", [])
|
61 |
+
|
62 |
+
# Validate output format
|
63 |
+
if not isinstance(output_list, list):
|
64 |
+
raise ValueError("Unexpected output format: 'orgnames' should be a list")
|
65 |
+
|
66 |
+
return output_list
|
67 |
+
|
68 |
+
except Exception as e:
|
69 |
+
# Log or print the error as needed for debugging
|
70 |
+
print(f"text does not have any organization: {e}")
|
71 |
+
return []
|
72 |
+
|
73 |
+
|
74 |
+
def is_similar(name: str, list_of_dict: list, threshold: int = 80):
|
75 |
+
"""
|
76 |
+
Returns True if `name` is similar to any names in `list_of_dict` based on a similarity threshold.
|
77 |
+
"""
|
78 |
+
try:
|
79 |
+
for item in list_of_dict:
|
80 |
+
try:
|
81 |
+
# Attempt to calculate similarity score
|
82 |
+
similarity = fuzz.ratio(name.lower(), item["name"].lower())
|
83 |
+
if similarity >= threshold:
|
84 |
+
return True
|
85 |
+
except KeyError:
|
86 |
+
# Handle cases where 'name' key might be missing in dictionary
|
87 |
+
print(f"KeyError: Missing 'name' key in dictionary item {item}")
|
88 |
+
continue
|
89 |
+
except AttributeError:
|
90 |
+
# Handle non-string name values in dictionary items
|
91 |
+
print(f"AttributeError: Non-string 'name' in dictionary item {item}")
|
92 |
+
continue
|
93 |
+
except TypeError as e:
|
94 |
+
# Handle cases where input types are incorrect
|
95 |
+
print(f"TypeError: {e}")
|
96 |
+
return False
|
97 |
+
|
98 |
+
return False
|
99 |
+
|
100 |
+
|
101 |
+
def generate_org_link_dict(org_names_list: list):
|
102 |
+
"""
|
103 |
+
Maps organization names to their Candid profile URLs if available.
|
104 |
+
|
105 |
+
For each organization in `output_list`, this function attempts to retrieve a matching profile
|
106 |
+
using `search_org`. If a similar name is found and a Candid entity ID is available, it constructs
|
107 |
+
a profile URL. If no ID or similar match is found, or if an error occurs, it assigns an empty string.
|
108 |
+
|
109 |
+
Args:
|
110 |
+
output_list (list): List of organization names (str) to retrieve Candid profile links for.
|
111 |
+
|
112 |
+
Returns:
|
113 |
+
dict: Dictionary with organization names as keys and Candid profile URLs or empty strings as values.
|
114 |
+
|
115 |
+
Example:
|
116 |
+
get_org_link(['New York-Presbyterian Hospital'])
|
117 |
+
# {'New York-Presbyterian Hospital': 'https://app.candid.org/profile/6915255'}
|
118 |
+
"""
|
119 |
+
link_dict = {}
|
120 |
+
|
121 |
+
for org in org_names_list:
|
122 |
+
try:
|
123 |
+
# Attempt to retrieve organization data
|
124 |
+
response = search(org)
|
125 |
+
|
126 |
+
# Check if there is a valid response and if names are similar
|
127 |
+
if response and is_similar(org, response[0].get("names", "")):
|
128 |
+
# Try to get the Candid entity ID and construct the URL
|
129 |
+
candid_entity_id = response[0].get("candid_entity_id")
|
130 |
+
if candid_entity_id:
|
131 |
+
link_dict[org] = (
|
132 |
+
f"https://app.candid.org/profile/{candid_entity_id}"
|
133 |
+
)
|
134 |
+
else:
|
135 |
+
link_dict[org] = "" # No ID found, set empty string
|
136 |
+
else:
|
137 |
+
link_dict[org] = "" # No similar match found
|
138 |
+
|
139 |
+
except KeyError as e:
|
140 |
+
# Handle missing keys in the response dictionary
|
141 |
+
print(f"KeyError encountered for organization '{org}': {e}")
|
142 |
+
link_dict[org] = ""
|
143 |
+
|
144 |
+
except Exception as e:
|
145 |
+
# Catch any other unexpected errors
|
146 |
+
|
147 |
+
print(f"An error occurred for organization '{org}': {e}")
|
148 |
+
link_dict[org] = ""
|
149 |
+
|
150 |
+
return link_dict
|
151 |
+
|
152 |
+
|
153 |
+
def embed_org_links_in_text(input_text: str, org_link_dict: dict):
|
154 |
+
"""
|
155 |
+
Replaces organization names in `text` with links from `link_dict` and appends a Candid info message.
|
156 |
+
|
157 |
+
Args:
|
158 |
+
text (str): The text containing organization names.
|
159 |
+
link_dict (dict): Mapping of organization names to URLs.
|
160 |
+
|
161 |
+
Returns:
|
162 |
+
str: Updated text with linked organization names and an appended Candid message.
|
163 |
+
"""
|
164 |
+
try:
|
165 |
+
for org_name, url in org_link_dict.items():
|
166 |
+
if url: # Only proceed if the URL is not empty
|
167 |
+
regex_pattern = re.compile(re.escape(org_name))
|
168 |
+
input_text = regex_pattern.sub(
|
169 |
+
repl=f"<a href={url} target='_blank' rel='noreferrer' class='candid-org-link'>{org_name}</a>",
|
170 |
+
string=input_text
|
171 |
+
)
|
172 |
+
|
173 |
+
# Append Candid information message at the end
|
174 |
+
input_text += "<p class='candid-app-link'> Visit <a href=https://app.candid.org/ target='_blank' rel='noreferrer' class='candid-org-link'>Candid</a> to get nonprofit information you need.</p>"
|
175 |
+
|
176 |
+
except TypeError as e:
|
177 |
+
print(f"TypeError encountered: {e}")
|
178 |
+
return input_text
|
179 |
+
|
180 |
+
except re.error as e:
|
181 |
+
print(f"Regex error encountered for '{org_name}': {e}")
|
182 |
+
return input_text
|
183 |
+
|
184 |
+
except Exception as e:
|
185 |
+
print(f"Unexpected error: {e}")
|
186 |
+
return input_text
|
187 |
+
|
188 |
+
return input_text
|
tools/question_reformulation.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from langchain_core.prompts import ChatPromptTemplate
|
2 |
+
from langchain_core.output_parsers import StrOutputParser
|
3 |
+
|
4 |
+
|
5 |
+
def reformulate_question_using_history(state, llm):
|
6 |
+
"""
|
7 |
+
Transform the query to produce a better query with details from previous messages.
|
8 |
+
|
9 |
+
Args:
|
10 |
+
state (messages): The current state
|
11 |
+
llm: LLM to use
|
12 |
+
Returns:
|
13 |
+
dict: The updated state with re-phrased question and original user_input for UI
|
14 |
+
"""
|
15 |
+
print("---REFORMULATE THE USER INPUT---")
|
16 |
+
messages = state["messages"]
|
17 |
+
question = messages[-1].content
|
18 |
+
|
19 |
+
if len(messages) > 1:
|
20 |
+
contextualize_q_system_prompt = """Given a chat history and the latest user input \
|
21 |
+
which might reference context in the chat history, formulate a standalone input \
|
22 |
+
which can be understood without the chat history.
|
23 |
+
Chat history:
|
24 |
+
\n ------- \n
|
25 |
+
{chat_history}
|
26 |
+
\n ------- \n
|
27 |
+
User input:
|
28 |
+
\n ------- \n
|
29 |
+
{question}
|
30 |
+
\n ------- \n
|
31 |
+
Do NOT answer the question, \
|
32 |
+
just reformulate it if needed and otherwise return it as is.
|
33 |
+
"""
|
34 |
+
|
35 |
+
contextualize_q_prompt = ChatPromptTemplate([
|
36 |
+
("system", contextualize_q_system_prompt),
|
37 |
+
("human", question),
|
38 |
+
])
|
39 |
+
|
40 |
+
rag_chain = contextualize_q_prompt | llm | StrOutputParser()
|
41 |
+
new_question = rag_chain.invoke({"chat_history": messages, "question": question})
|
42 |
+
print(f"user asked: '{question}', agent reformulated the question basing on the chat history: {new_question}")
|
43 |
+
return {"messages": [new_question], "user_input" : question}
|
44 |
+
return {"messages": [question], "user_input" : question}
|