import logging import os from openai import OpenAI import requests from ask_candid.agents.schema import AgentState, Context from ask_candid.base.config.rest import OPENAI logging.basicConfig(format="[%(levelname)s] (%(asctime)s) :: %(message)s") logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) def detect_intent_with_llm(state: AgentState) -> AgentState: """Detect query intent (which type of recommendation) and update the state.""" logger.info("---DETECT INTENT---") client = OpenAI(api_key=OPENAI['key']) # query = state["messages"][-1]["content"] query = state["messages"][-1].content prompt = f"""Classify the following query as one of the following categories: - 'none': The query is not asking for funding recommendations. - 'funder': The query is asking for recommendations about funders, such as foundations or donors, who might provide longer-term or general funding. - 'rfp': The query is asking for recommendations about specific, active Requests for Proposals (RFPs) that typically focus on short-term, recent opportunities with a deadline. Your classification should consider: 1. RFPs often focus on active and time-bound opportunities for specific projects or programs. 2. Funders refer to broader, long-term funding sources like organizations or individuals who offer grants. Query: "{query}" """ response = client.chat.completions.create( # model="gpt-4-turbo", model="gpt-4o", messages=[{"role": "system", "content": prompt}], max_tokens=10, stop=["\n"] ) intent = response.choices[0].message.content.strip().lower() state["intent"] = intent.strip("'").strip('"') # Remove extra quotes return state def determine_context(state: AgentState) -> AgentState: """Extract subject/geography/population codes and update the state.""" logger.info("---GETTING RECOMMENDATION CONTEXT---") query = state["messages"][-1].content subject_codes, population_codes, geo_ids = [], [], [] # subject and population autocoding_headers = { 'x-api-key': os.getenv("AUTOCODING_API_KEY"), 'Content-Type': 'application/json' } autocoding_params = { 'text': query, 'taxonomy': 'pcs-v3' } autocoding_response = requests.get( os.getenv("AUTOCODING_API_URL"), headers=autocoding_headers, params=autocoding_params, timeout=30 ) if autocoding_response.status_code == 200: returned_pcs = autocoding_response.json()["data"] population_codes = [item['full_code'] for item in returned_pcs.get("population", [])] subject_codes = [item['full_code'] for item in returned_pcs.get("subject", [])] # geography geo_headers = { 'x-api-key': os.getenv("GEO_API_KEY"), 'Content-Type': 'application/json' } geo_data = { 'text': query } geo_response = requests.post(os.getenv("GEO_API_URL"), headers=geo_headers, json=geo_data, timeout=30) if geo_response.status_code == 200: entities = geo_response.json()['data']['entities'] geo_ids = [entity['geo']['id'] for entity in entities if 'id' in entity['geo']] state["context"] = Context( subject=subject_codes, population=population_codes, geography=geo_ids ) return state def make_recommendation(state: AgentState) -> AgentState: """Make an API call based on the extracted context and update the state.""" # query = state["messages"][-1]["content"] logger.info("---RECOMMENDING---") org_id = "6908122" funder_or_rfp = state["intent"] # Extract context contexts = state["context"] subject_codes = contexts.get("subject", []) population_codes = contexts.get("population", []) geo_ids = contexts.get("geography", []) # Prepare parameters params = { "subjects": ",".join(subject_codes), "geos": ",".join([str(geo) for geo in geo_ids]), "populations": ",".join(population_codes) } headers = {"x-api-key": os.getenv("FUNDER_REC_API_KEY")} base_url = os.getenv("FUNDER_REC_API_URL") # Initialize response response = None recommendation_display_text = "" try: # Make the API call based on intent if funder_or_rfp == "funder": response = requests.get(base_url, headers=headers, params=params, timeout=30) elif funder_or_rfp == "rfp": params["candid_entity_id"] = org_id #placeholder response = requests.get(f"{base_url}/rfp", headers=headers, params=params, timeout=30) else: # Handle unknown intent state["recommendation"] = "Unknown intent. Intent 'funder' or 'rfp' expected." return state # Validate response if response and response.status_code == 200: recommendations = response.json().get("recommendations", []) if recommendations: if funder_or_rfp == "funder": # Format recommendations recommendation_display_text = "Here are the top 10 recommendations. Click their profiles to learn more:\n" + "\n".join([ f"{recommendation['funder_data']['main_sort_name']} - Profile: https://app.candid.org/profile/{recommendation['funder_id']}" for recommendation in recommendations ]) elif funder_or_rfp == "rfp": recommendation_display_text = "Here are the top recommendations:\n" + "\n".join([ f"Title: {rec['title']}\n" f"Funder: {rec['funder_name']}\n" f"Amount: {rec.get('amount', 'Not specified')}\n" f"Description: {rec.get('description', 'No description available')}\n" f"Deadline: {rec.get('deadline', 'No deadline provided')}\n" f"Application URL: {rec.get('application_url', 'No URL available')}\n" for rec in recommendations ]) else: # No recommendations found recommendation_display_text = "No recommendations were found for your query. Please try refining your search criteria." elif response and response.status_code == 400: # Handle bad request error_details = response.json() recommendation_display_text = ( "An error occurred while processing your request. " f"Details: {error_details.get('message', 'Unknown error.')}" ) elif response: # Handle other unexpected status codes recommendation_display_text = ( f"An unexpected error occurred (Status Code: {response.status_code}). " "Please try again later or contact support if the problem persists." ) else: # Handle case where response is None recommendation_display_text = "No response from the server. Please check your connection or try again later." except requests.exceptions.RequestException as e: # Handle network-related errors recommendation_display_text = ( "A network error occurred while trying to connect to the recommendation service. " f"Details: {str(e)}" ) except Exception as e: # Handle other unexpected errors print(params) recommendation_display_text = ( "An unexpected error occurred while processing your request. " f"Details: {str(e)}" ) # Update state with recommendations or error messages state["recommendation"] = recommendation_display_text return state