TechScout/techscout/sources/sbir.py

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