gofannon 0.25.19__py3-none-any.whl → 0.25.21__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,134 @@
1
+ import logging
2
+ import json
3
+ from typing import Optional, Dict, Any, List
4
+
5
+ from .search_base import SearchOpportunitiesBase
6
+ from ..config import FunctionRegistry
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ @FunctionRegistry.register
11
+ class QueryByMultipleCriteria(SearchOpportunitiesBase):
12
+ """
13
+ Tool to search for grant opportunities by combining multiple filter criteria.
14
+ Use this for more complex queries not covered by specialized tools.
15
+ All filter parameters are optional, but at least one filter or query_text should be provided.
16
+ """
17
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, name: str = "query_opportunities_by_multiple_criteria"):
18
+ super().__init__(api_key=api_key, base_url=base_url, name=name)
19
+ self.name = name
20
+
21
+ @property
22
+ def definition(self):
23
+ base_params = self._get_common_parameter_definitions()
24
+ # Parameters from other specific tools, all optional here
25
+ specific_params = {
26
+ "agency_codes": {
27
+ "type": "array", "items": {"type": "string"},
28
+ "description": "Optional. List of agency codes (e.g., ['USAID', 'DOC'])."
29
+ },
30
+ "funding_instruments": {
31
+ "type": "array", "items": {"type": "string"},
32
+ "description": "Optional. List of funding instruments."
33
+ },
34
+ "funding_categories": {
35
+ "type": "array", "items": {"type": "string"},
36
+ "description": "Optional. List of funding categories."
37
+ },
38
+ "applicant_types": {
39
+ "type": "array", "items": {"type": "string"},
40
+ "description": "Optional. List of applicant types."
41
+ },
42
+ "assistance_listing_numbers": {
43
+ "type": "array", "items": {"type": "string"},
44
+ "description": "Optional. List of Assistance Listing Numbers."
45
+ },
46
+ "requires_cost_sharing": {
47
+ "type": "boolean",
48
+ "description": "Optional. Filter by cost-sharing requirement."
49
+ },
50
+ # For simplicity, award criteria and dates are not individual params here.
51
+ # If needed, they could be added, or a user could use a more specific tool.
52
+ "query_text": {
53
+ "type": "string",
54
+ "description": "Optional. Text to search for within filtered results."
55
+ },
56
+ "query_operator": {
57
+ "type": "string", "enum": ["AND", "OR"],
58
+ "description": "Operator for 'query_text' if provided (default: AND).", "default": "AND"
59
+ }
60
+ }
61
+ all_properties = {**specific_params, **base_params}
62
+
63
+ return {
64
+ "type": "function",
65
+ "function": {
66
+ "name": self.name,
67
+ "description": (
68
+ "Search grant opportunities by combining various filters like agency, funding types, applicant types, etc. "
69
+ "Useful for complex queries. At least one filter criterion or query_text should be provided. "
70
+ f"{self._pagination_description}"
71
+ f"{self._status_filter_description}"
72
+ f"{self._common_search_description_suffix}"
73
+ ),
74
+ "parameters": {
75
+ "type": "object",
76
+ "properties": all_properties,
77
+ "required": [] # Logic in fn
78
+ }
79
+ }
80
+ }
81
+
82
+ def fn(self,
83
+ agency_codes: Optional[List[str]] = None,
84
+ funding_instruments: Optional[List[str]] = None,
85
+ funding_categories: Optional[List[str]] = None,
86
+ applicant_types: Optional[List[str]] = None,
87
+ assistance_listing_numbers: Optional[List[str]] = None,
88
+ requires_cost_sharing: Optional[bool] = None,
89
+ query_text: Optional[str] = None,
90
+ query_operator: str = "AND",
91
+ # Common params
92
+ items_per_page: int = 5, page_number: int = 1, order_by: str = "relevancy",
93
+ sort_direction: str = "descending", show_posted: bool = True, show_forecasted: bool = False,
94
+ show_closed: bool = False, show_archived: bool = False) -> str:
95
+
96
+ self.logger.info(f"Querying by multiple criteria, query='{query_text}'")
97
+
98
+ specific_filters: Dict[str, Any] = {}
99
+ any_filter_provided = False
100
+
101
+ if agency_codes:
102
+ specific_filters["agency"] = {"one_of": agency_codes}
103
+ any_filter_provided = True
104
+ if funding_instruments:
105
+ specific_filters["funding_instrument"] = {"one_of": funding_instruments}
106
+ any_filter_provided = True
107
+ if funding_categories:
108
+ specific_filters["funding_category"] = {"one_of": funding_categories}
109
+ any_filter_provided = True
110
+ if applicant_types:
111
+ specific_filters["applicant_type"] = {"one_of": applicant_types}
112
+ any_filter_provided = True
113
+ if assistance_listing_numbers:
114
+ specific_filters["assistance_listing_number"] = {"one_of": assistance_listing_numbers}
115
+ any_filter_provided = True
116
+ if requires_cost_sharing is not None:
117
+ specific_filters["is_cost_sharing"] = {"one_of": [requires_cost_sharing]}
118
+ any_filter_provided = True
119
+
120
+ if not any_filter_provided and not query_text:
121
+ return json.dumps({"error": "At least one filter criterion or query_text must be specified for this tool.", "success": False})
122
+
123
+ specific_query_params: Optional[Dict[str, Any]] = None
124
+ if query_text:
125
+ specific_query_params = {"query": query_text}
126
+
127
+ payload = self._build_api_payload(
128
+ specific_query_params=specific_query_params,
129
+ specific_filters=specific_filters if specific_filters else None,
130
+ items_per_page=items_per_page, page_number=page_number, order_by=order_by,
131
+ sort_direction=sort_direction, show_posted=show_posted, show_forecasted=show_forecasted,
132
+ show_closed=show_closed, show_archived=show_archived, query_operator=query_operator
133
+ )
134
+ return self._execute_search(payload)
@@ -0,0 +1,93 @@
1
+ import logging
2
+ from typing import Optional, Dict, Any
3
+
4
+ from .search_base import SearchOpportunitiesBase
5
+ from ..config import FunctionRegistry
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ @FunctionRegistry.register
10
+ class QueryOpportunities(SearchOpportunitiesBase):
11
+ """
12
+ Tool to search for grant opportunities using a general text query.
13
+ """
14
+ def __init__(self, api_key: Optional[str] = None,
15
+ base_url: Optional[str] = None,
16
+ name: str = "query_opportunities"):
17
+ super().__init__(api_key=api_key, base_url=base_url)
18
+ self.name = name
19
+
20
+ @property
21
+ def definition(self):
22
+ base_params = self._get_common_parameter_definitions()
23
+ specific_params = {
24
+ "query_text": {
25
+ "type": "string",
26
+ "description": "The text to search for in opportunity titles, descriptions, etc."
27
+ },
28
+ "query_operator": {
29
+ "type": "string",
30
+ "enum": ["AND", "OR"],
31
+ "description": "Operator for combining terms in 'query_text' (default: AND).",
32
+ "default": "AND"
33
+ }
34
+ }
35
+
36
+ # Combine and order parameters as desired for the definition
37
+ # Typically, specific parameters first, then common ones.
38
+ all_properties = {**specific_params, **base_params}
39
+
40
+ return {
41
+ "type": "function",
42
+ "function": {
43
+ "name": self.name,
44
+ "description": (
45
+ "Search for grant opportunities using a text query. "
46
+ f"{self._pagination_description}"
47
+ f"{self._status_filter_description}"
48
+ f"{self._common_search_description_suffix}"
49
+ ),
50
+ "parameters": {
51
+ "type": "object",
52
+ "properties": all_properties,
53
+ "required": ["query_text"] # Only query_text is strictly required for this tool
54
+ }
55
+ }
56
+ }
57
+
58
+ def fn(self,
59
+ query_text: str,
60
+ query_operator: str = "AND",
61
+ # Common params from base, with defaults
62
+ items_per_page: int = 5,
63
+ page_number: int = 1,
64
+ order_by: str = "relevancy",
65
+ sort_direction: str = "descending",
66
+ show_posted: bool = True,
67
+ show_forecasted: bool = False,
68
+ show_closed: bool = False,
69
+ show_archived: bool = False) -> str:
70
+ """
71
+ Executes the general opportunity search.
72
+ """
73
+ self.logger.info(f"Executing general opportunity query: '{query_text}'")
74
+
75
+ specific_query_params = {
76
+ "query": query_text # API expects "query" not "query_text"
77
+ }
78
+
79
+ payload = self._build_api_payload(
80
+ specific_query_params=specific_query_params,
81
+ specific_filters=None, # No other specific filters for this tool
82
+ items_per_page=items_per_page,
83
+ page_number=page_number,
84
+ order_by=order_by,
85
+ sort_direction=sort_direction,
86
+ show_posted=show_posted,
87
+ show_forecasted=show_forecasted,
88
+ show_closed=show_closed,
89
+ show_archived=show_archived,
90
+ query_operator=query_operator
91
+ )
92
+
93
+ return self._execute_search(payload)
@@ -0,0 +1,104 @@
1
+ import logging
2
+ from typing import Optional, Dict, Any, List
3
+
4
+ from .search_base import SearchOpportunitiesBase
5
+ from ..config import FunctionRegistry
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ @FunctionRegistry.register
10
+ class QueryOpportunitiesByAgencyCode(SearchOpportunitiesBase):
11
+ """
12
+ Tool to search for grant opportunities filtered by agency code(s).
13
+ Optionally, a text query can also be provided.
14
+ """
15
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, name: str = "query_opportunities_by_agency"):
16
+ super().__init__(api_key=api_key, base_url=base_url)
17
+ self.name = name
18
+
19
+ @property
20
+ def definition(self):
21
+ base_params = self._get_common_parameter_definitions()
22
+ specific_params = {
23
+ "agency_codes": {
24
+ "type": "array",
25
+ "items": {"type": "string"},
26
+ "description": "A list of agency codes to filter by (e.g., ['USAID', 'DOC'])."
27
+ },
28
+ "query_text": {
29
+ "type": "string",
30
+ "description": "Optional. Text to search for within the results filtered by agency."
31
+ },
32
+ "query_operator": {
33
+ "type": "string",
34
+ "enum": ["AND", "OR"],
35
+ "description": "Operator for combining terms in 'query_text' if provided (default: AND).",
36
+ "default": "AND"
37
+ }
38
+ }
39
+ all_properties = {**specific_params, **base_params}
40
+
41
+ return {
42
+ "type": "function",
43
+ "function": {
44
+ "name": self.name,
45
+ "description": (
46
+ "Search for grant opportunities filtered by one or more agency codes. "
47
+ "An optional text query can further refine results. "
48
+ f"{self._pagination_description}"
49
+ f"{self._status_filter_description}"
50
+ f"{self._common_search_description_suffix}"
51
+ ),
52
+ "parameters": {
53
+ "type": "object",
54
+ "properties": all_properties,
55
+ "required": ["agency_codes"]
56
+ }
57
+ }
58
+ }
59
+
60
+ def fn(self,
61
+ agency_codes: List[str],
62
+ query_text: Optional[str] = None,
63
+ query_operator: str = "AND",
64
+ # Common params from base
65
+ items_per_page: int = 5,
66
+ page_number: int = 1,
67
+ order_by: str = "relevancy",
68
+ sort_direction: str = "descending",
69
+ show_posted: bool = True,
70
+ show_forecasted: bool = False,
71
+ show_closed: bool = False,
72
+ show_archived: bool = False) -> str:
73
+ """
74
+ Executes the opportunity search filtered by agency codes.
75
+ """
76
+ self.logger.info(f"Executing opportunity query by agency codes: {agency_codes}, query_text: '{query_text}'")
77
+
78
+ if not agency_codes:
79
+ logger.error("agency_codes list cannot be empty.")
80
+ return json.dumps({"error": "agency_codes list cannot be empty.", "success": False})
81
+
82
+ specific_filters: Dict[str, Any] = {
83
+ "agency": {"one_of": agency_codes}
84
+ }
85
+
86
+ specific_query_params: Optional[Dict[str, Any]] = None
87
+ if query_text:
88
+ specific_query_params = {"query": query_text}
89
+
90
+ payload = self._build_api_payload(
91
+ specific_query_params=specific_query_params,
92
+ specific_filters=specific_filters,
93
+ items_per_page=items_per_page,
94
+ page_number=page_number,
95
+ order_by=order_by,
96
+ sort_direction=sort_direction,
97
+ show_posted=show_posted,
98
+ show_forecasted=show_forecasted,
99
+ show_closed=show_closed,
100
+ show_archived=show_archived,
101
+ query_operator=query_operator
102
+ )
103
+
104
+ return self._execute_search(payload)
@@ -0,0 +1,102 @@
1
+ import logging
2
+ from typing import Optional, Dict, Any
3
+ import json
4
+
5
+ from .base import SimplerGrantsGovBase
6
+ from ..config import FunctionRegistry
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ @FunctionRegistry.register
11
+ class SearchAgencies(SimplerGrantsGovBase):
12
+ """
13
+ Tool to search for agencies based on query text and filters.
14
+ Corresponds to the POST /v1/agencies/search endpoint.
15
+ """
16
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, name: str = "search_agencies"):
17
+ super().__init__(api_key=api_key, base_url=base_url)
18
+ self.name = name
19
+
20
+ @property
21
+ def definition(self):
22
+ # Based on AgencySearchRequestSchema
23
+ return {
24
+ "type": "function",
25
+ "function": {
26
+ "name": self.name,
27
+ "description": "Search for agencies using a query string and structured filters like 'has_active_opportunity' or 'is_test_agency'.",
28
+ "parameters": {
29
+ "type": "object",
30
+ "properties": {
31
+ "query": {
32
+ "type": "string",
33
+ "description": "Optional. Query string which searches against agency text fields."
34
+ },
35
+ "query_operator": {
36
+ "type": "string",
37
+ "enum": ["AND", "OR"],
38
+ "description": "Optional. Operator for combining query conditions (default: OR).",
39
+ "default": "OR"
40
+ },
41
+ "filters": {
42
+ "type": "object",
43
+ "description": "Optional. A JSON object for filtering. Keys can be 'has_active_opportunity' or 'is_test_agency'. Each key holds an object like {'one_of': [true/false]} specifying the filter.",
44
+ "properties": {
45
+ "has_active_opportunity": {
46
+ "type": "object",
47
+ "properties": {"one_of": {"type": "array", "items": {"type": "boolean"}}}
48
+ },
49
+ "is_test_agency": {
50
+ "type": "object",
51
+ "properties": {"one_of": {"type": "array", "items": {"type": "boolean"}}}
52
+ }
53
+ }
54
+ },
55
+ "pagination": {
56
+ "type": "object",
57
+ "description": "Required. A JSON object for pagination. Must include 'page_offset', 'page_size', and 'sort_order' (array of objects with 'order_by': ['agency_code', 'agency_name'] and 'sort_direction': ['ascending', 'descending']).",
58
+ "properties": {
59
+ "page_offset": {"type": "integer", "description": "Page number (starts at 1)."},
60
+ "page_size": {"type": "integer", "description": "Results per page."},
61
+ "sort_order": {
62
+ "type": "array",
63
+ "items": {
64
+ "type": "object",
65
+ "properties": {
66
+ "order_by": {"type": "string", "enum": ["agency_code", "agency_name"]},
67
+ "sort_direction": {"type": "string", "enum": ["ascending", "descending"]}
68
+ },
69
+ "required": ["order_by", "sort_direction"]
70
+ }
71
+ }
72
+ },
73
+ "required": ["page_offset", "page_size", "sort_order"]
74
+ }
75
+ },
76
+ "required": ["pagination"]
77
+ }
78
+ }
79
+ }
80
+
81
+ def fn(self, pagination: Dict[str, Any], query: Optional[str] = None, filters: Optional[Dict[str, Any]] = None, query_operator: str = "OR") -> str:
82
+ """
83
+ Executes the search agencies request.
84
+ """
85
+ self.logger.info("Executing Simpler Grants Gov search agencies tool")
86
+ payload: Dict[str, Any] = {
87
+ "pagination": pagination,
88
+ "query_operator": query_operator
89
+ }
90
+ if query:
91
+ payload["query"] = query
92
+ if filters:
93
+ payload["filters"] = filters
94
+
95
+ endpoint = "/v1/agencies/search"
96
+ try:
97
+ result = self._make_request("POST", endpoint, json_payload=payload)
98
+ self.logger.debug(f"Search agencies successful. Response length: {len(result)}")
99
+ return result
100
+ except Exception as e:
101
+ self.logger.error(f"Search agencies failed: {e}", exc_info=True)
102
+ return json.dumps({"error": f"Search agencies failed: {str(e)}", "success": False})
@@ -0,0 +1,167 @@
1
+ import logging
2
+ import json
3
+ from typing import Optional, Dict, Any, List
4
+
5
+ from .base import SimplerGrantsGovBase
6
+ from ..config import FunctionRegistry # Will be used by subclasses
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class SearchOpportunitiesBase(SimplerGrantsGovBase):
11
+ """
12
+ Base class for Simpler Grants Gov tools that search for opportunities.
13
+ Handles common pagination and opportunity status filtering logic.
14
+ """
15
+
16
+ # Common descriptions
17
+ _pagination_description = (
18
+ " Control the pagination of results. If not provided, defaults will be used by the API "
19
+ "(typically page 1, a small number of items, and sorted by relevance or ID)."
20
+ )
21
+ _status_filter_description = (
22
+ " Filter results by opportunity status. By default, only 'posted' opportunities are shown. "
23
+ "Set other flags to True to include them."
24
+ )
25
+ _common_search_description_suffix = (
26
+ " Returns a JSON string of matching opportunities."
27
+ )
28
+
29
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None, name: str = "search_opportunities_base"):
30
+ super().__init__(api_key=api_key, base_url=base_url, name=name)
31
+ # 'name' will be overridden by subclasses for their specific tool name
32
+
33
+ def _get_common_parameter_definitions(self) -> Dict[str, Any]:
34
+ """
35
+ Returns a dictionary of common parameter definitions for pagination and status.
36
+ """
37
+ return {
38
+ "items_per_page": {
39
+ "type": "integer",
40
+ "description": "Number of results per page (e.g., 10, 25, 50). Default: 5.",
41
+ "default": 5
42
+ },
43
+ "page_number": {
44
+ "type": "integer",
45
+ "description": "The page number to retrieve (starts at 1). Default: 1.",
46
+ "default": 1
47
+ },
48
+ "order_by": {
49
+ "type": "string",
50
+ "description": "Field to sort results by (e.g., 'relevancy', 'post_date', 'opportunity_id', 'agency_code'). Default: 'relevancy'.",
51
+ "default": "relevancy" # API default might be opportunity_id or post_date
52
+ },
53
+ "sort_direction": {
54
+ "type": "string",
55
+ "enum": ["ascending", "descending"],
56
+ "description": "Direction to sort (ascending or descending). Default: 'descending'.",
57
+ "default": "descending"
58
+ },
59
+ "show_posted": {
60
+ "type": "boolean",
61
+ "description": "Include 'posted' opportunities. Default: True.",
62
+ "default": True
63
+ },
64
+ "show_forecasted": {
65
+ "type": "boolean",
66
+ "description": "Include 'forecasted' opportunities. Default: False.",
67
+ "default": False
68
+ },
69
+ "show_closed": {
70
+ "type": "boolean",
71
+ "description": "Include 'closed' opportunities. Default: False.",
72
+ "default": False
73
+ },
74
+ "show_archived": {
75
+ "type": "boolean",
76
+ "description": "Include 'archived' opportunities. Default: False.",
77
+ "default": False
78
+ }
79
+ }
80
+
81
+ def _build_api_payload(
82
+ self,
83
+ specific_query_params: Optional[Dict[str, Any]] = None,
84
+ specific_filters: Optional[Dict[str, Any]] = None,
85
+ # Pagination and status args from function call
86
+ items_per_page: int = 5,
87
+ page_number: int = 1,
88
+ order_by: str = "relevancy",
89
+ sort_direction: str = "descending",
90
+ show_posted: bool = True,
91
+ show_forecasted: bool = False,
92
+ show_closed: bool = False,
93
+ show_archived: bool = False,
94
+ query_operator: str = "AND"
95
+ ) -> Dict[str, Any]:
96
+ """
97
+ Constructs the full payload for the /v1/opportunities/search API endpoint.
98
+ """
99
+ payload: Dict[str, Any] = {}
100
+
101
+ # 1. Pagination
102
+ payload["pagination"] = {
103
+ "page_offset": page_number,
104
+ "page_size": items_per_page,
105
+ "sort_order": [{"order_by": order_by, "sort_direction": sort_direction}]
106
+ }
107
+
108
+ # 2. Query and Specific Query Params
109
+ if specific_query_params and specific_query_params.get("query"): # Check if 'query' key actually has a value
110
+ payload.update(specific_query_params)
111
+ payload["query_operator"] = query_operator
112
+ elif specific_query_params: # If other specific_query_params exist without 'query'
113
+ payload.update(specific_query_params)
114
+
115
+
116
+ # 3. Filters
117
+ filters_dict: Dict[str, Any] = {}
118
+
119
+ # 3a. Opportunity Status Filter
120
+ active_statuses: List[str] = []
121
+ if show_posted:
122
+ active_statuses.append("posted")
123
+ if show_forecasted:
124
+ active_statuses.append("forecasted")
125
+ if show_closed:
126
+ active_statuses.append("closed")
127
+ if show_archived:
128
+ active_statuses.append("archived")
129
+
130
+ if active_statuses:
131
+ filters_dict["opportunity_status"] = {"one_of": active_statuses}
132
+ else:
133
+ self.logger.warning("No opportunity statuses selected for filtering.")
134
+ # Consider if an empty list should be sent or if the key should be omitted.
135
+ # For now, omitting if empty, as API might require at least one status if key is present.
136
+ # filters_dict["opportunity_status"] = {"one_of": []} # Alternative
137
+
138
+ # 3b. Specific Filters
139
+ if specific_filters:
140
+ filters_dict.update(specific_filters)
141
+
142
+ if filters_dict:
143
+ payload["filters"] = filters_dict
144
+
145
+ self.logger.debug(f"Constructed API payload: {json.dumps(payload, indent=2)}")
146
+ return payload
147
+
148
+ def _execute_search(self, payload: Dict[str, Any]) -> str:
149
+ """
150
+ Shared method to make the API call.
151
+ """
152
+ endpoint = "/v1/opportunities/search"
153
+ try:
154
+ result = self._make_request("POST", endpoint, json_payload=payload)
155
+ self.logger.debug(f"Search successful. Response length: {len(result)}")
156
+ return result
157
+ except Exception as e:
158
+ self.logger.error(f"Opportunity search failed: {e}", exc_info=True)
159
+ return json.dumps({"error": f"Opportunity search API request failed: {str(e)}", "success": False})
160
+
161
+ # Subclasses will implement their specific 'definition' and 'fn'
162
+ @property
163
+ def definition(self):
164
+ raise NotImplementedError("Subclasses must implement the 'definition' property.")
165
+
166
+ def fn(self, *args, **kwargs):
167
+ raise NotImplementedError("Subclasses must implement the 'fn' method.")