177 lines
5.6 KiB
Python
177 lines
5.6 KiB
Python
"""
|
|
SBIR/STTR Award Search
|
|
|
|
Uses the SBIR.gov API to search for Small Business Innovation Research
|
|
and Small Business Technology Transfer awards.
|
|
|
|
API Documentation: https://www.sbir.gov/api
|
|
Free, no API key required.
|
|
"""
|
|
|
|
import logging
|
|
import requests
|
|
from typing import List, Optional, Dict, Any
|
|
from datetime import datetime
|
|
|
|
from ..search.base import BaseSearcher, SearchResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SBIRSearcher(BaseSearcher):
|
|
"""
|
|
Search SBIR/STTR awards database.
|
|
|
|
The SBIR program funds R&D at small businesses, making it
|
|
excellent for finding emerging defense technologies.
|
|
"""
|
|
|
|
BASE_URL = "https://www.sbir.gov/api/awards.json"
|
|
|
|
def __init__(self, timeout: int = 30):
|
|
self.timeout = timeout
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
return "SBIR.gov"
|
|
|
|
@property
|
|
def source_type(self) -> str:
|
|
return "sbir"
|
|
|
|
def search(
|
|
self,
|
|
query: str,
|
|
max_results: int = 20,
|
|
agency: Optional[str] = None,
|
|
year_start: Optional[int] = None,
|
|
year_end: Optional[int] = None,
|
|
phase: Optional[str] = None # "Phase I", "Phase II", "Phase III"
|
|
) -> List[SearchResult]:
|
|
"""
|
|
Search SBIR/STTR awards.
|
|
|
|
Args:
|
|
query: Keyword search
|
|
max_results: Maximum results to return
|
|
agency: Filter by agency (e.g., "DOD", "NASA", "DOE")
|
|
year_start: Start year filter
|
|
year_end: End year filter
|
|
phase: Filter by phase
|
|
|
|
Returns:
|
|
List of SearchResult objects
|
|
"""
|
|
params = {
|
|
"keyword": query,
|
|
"rows": min(max_results, 100), # API limit
|
|
}
|
|
|
|
if agency:
|
|
params["agency"] = agency
|
|
if year_start:
|
|
params["year_start"] = year_start
|
|
if year_end:
|
|
params["year_end"] = year_end
|
|
if phase:
|
|
params["phase"] = phase
|
|
|
|
results = []
|
|
|
|
try:
|
|
response = requests.get(
|
|
self.BASE_URL,
|
|
params=params,
|
|
timeout=self.timeout
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
|
|
for rank, award in enumerate(data, 1):
|
|
if rank > max_results:
|
|
break
|
|
|
|
# Estimate TRL based on phase
|
|
trl_estimate = self._estimate_trl(award.get("phase", ""))
|
|
|
|
# Build URL to award page
|
|
award_id = award.get("award_id", "")
|
|
url = f"https://www.sbir.gov/sbirsearch/detail/{award_id}" if award_id else ""
|
|
|
|
results.append(SearchResult(
|
|
title=award.get("award_title", ""),
|
|
url=url,
|
|
snippet=award.get("abstract", "")[:500] if award.get("abstract") else "",
|
|
source=self.name,
|
|
source_type=self.source_type,
|
|
rank=rank,
|
|
published_date=award.get("award_year"),
|
|
organization=award.get("firm", ""),
|
|
award_amount=self._parse_amount(award.get("award_amount")),
|
|
trl_estimate=trl_estimate,
|
|
award_id=award_id,
|
|
raw_data=award
|
|
))
|
|
|
|
logger.info(f"SBIR search for '{query}' returned {len(results)} results")
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
logger.error(f"SBIR API request failed: {e}")
|
|
except Exception as e:
|
|
logger.error(f"SBIR search error: {e}")
|
|
|
|
return results
|
|
|
|
def _estimate_trl(self, phase: str) -> int:
|
|
"""Estimate TRL based on SBIR phase."""
|
|
phase = phase.lower()
|
|
if "phase i" in phase and "ii" not in phase:
|
|
return 3 # Proof of concept
|
|
elif "phase ii" in phase and "iii" not in phase:
|
|
return 5 # Prototype development
|
|
elif "phase iii" in phase:
|
|
return 7 # Production ready
|
|
return 4 # Default mid-range
|
|
|
|
def _parse_amount(self, amount) -> Optional[float]:
|
|
"""Parse award amount to float."""
|
|
if amount is None:
|
|
return None
|
|
try:
|
|
if isinstance(amount, str):
|
|
amount = amount.replace("$", "").replace(",", "")
|
|
return float(amount)
|
|
except (ValueError, TypeError):
|
|
return None
|
|
|
|
def search_defense(self, query: str, max_results: int = 20) -> List[SearchResult]:
|
|
"""Search specifically DOD SBIR awards."""
|
|
return self.search(query, max_results=max_results, agency="DOD")
|
|
|
|
def search_space(self, query: str, max_results: int = 20) -> List[SearchResult]:
|
|
"""Search NASA and Space Force SBIR awards."""
|
|
# Search NASA
|
|
nasa_results = self.search(query, max_results=max_results // 2, agency="NASA")
|
|
# Search DOD (includes Space Force)
|
|
dod_results = self.search(
|
|
f"{query} space OR satellite OR orbital",
|
|
max_results=max_results // 2,
|
|
agency="DOD"
|
|
)
|
|
return nasa_results + dod_results
|
|
|
|
def get_award_details(self, award_id: str) -> Optional[Dict[str, Any]]:
|
|
"""Get detailed information about a specific award."""
|
|
try:
|
|
response = requests.get(
|
|
self.BASE_URL,
|
|
params={"award_id": award_id},
|
|
timeout=self.timeout
|
|
)
|
|
response.raise_for_status()
|
|
data = response.json()
|
|
return data[0] if data else None
|
|
except Exception as e:
|
|
logger.error(f"Failed to get award details: {e}")
|
|
return None
|