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.
- gofannon/base/__init__.py +2 -0
- gofannon/base/adk_mixin.py +291 -0
- gofannon/config.py +2 -0
- gofannon/simpler_grants_gov/__init__.py +0 -0
- gofannon/simpler_grants_gov/base.py +125 -0
- gofannon/simpler_grants_gov/get_opportunity.py +66 -0
- gofannon/simpler_grants_gov/list_agencies.py +83 -0
- gofannon/simpler_grants_gov/query_by_applicant_eligibility.py +114 -0
- gofannon/simpler_grants_gov/query_by_assistance_listing.py +102 -0
- gofannon/simpler_grants_gov/query_by_award_criteria.py +117 -0
- gofannon/simpler_grants_gov/query_by_dates.py +108 -0
- gofannon/simpler_grants_gov/query_by_funding_details.py +115 -0
- gofannon/simpler_grants_gov/query_by_multiple_criteria.py +134 -0
- gofannon/simpler_grants_gov/query_opportunities.py +93 -0
- gofannon/simpler_grants_gov/query_opportunities_by_agency.py +104 -0
- gofannon/simpler_grants_gov/search_agencies.py +102 -0
- gofannon/simpler_grants_gov/search_base.py +167 -0
- gofannon/simpler_grants_gov/search_opportunities.py +360 -0
- {gofannon-0.25.19.dist-info → gofannon-0.25.21.dist-info}/METADATA +2 -2
- {gofannon-0.25.19.dist-info → gofannon-0.25.21.dist-info}/RECORD +22 -6
- {gofannon-0.25.19.dist-info → gofannon-0.25.21.dist-info}/LICENSE +0 -0
- {gofannon-0.25.19.dist-info → gofannon-0.25.21.dist-info}/WHEEL +0 -0
@@ -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.")
|