From 82947969f4a9432850e1047da6eb1f44a5c0fc78 Mon Sep 17 00:00:00 2001 From: Raheem Date: Sat, 30 May 2026 10:48:28 +0100 Subject: [PATCH] feat: Refactor agent classes to use gemini_client and firecrawl_service, update async methods to sync state without await --- backend/agents/base.py | 6 +++--- backend/agents/contract_agent.py | 4 ++-- backend/agents/job_agent.py | 4 ++-- backend/agents/linkedin_agent.py | 2 ++ backend/agents/orchestrator.py | 6 +++--- backend/agents/resume_agent.py | 4 ++-- backend/core/config.py | 2 ++ backend/services/firecrawl.py | 2 +- backend/services/linkedin.py | 2 ++ backend/tests/test_contract_agent.py | 3 +-- backend/tests/test_integration.py | 3 ++- backend/tests/test_job_agent.py | 2 +- backend/tests/test_linkedin_agent.py | 3 +-- backend/tests/test_resume_agent.py | 3 +-- backend/tests/test_utils.py | 5 +++-- backend/utils/currency.py | 11 ++++++++--- 16 files changed, 36 insertions(+), 26 deletions(-) diff --git a/backend/agents/base.py b/backend/agents/base.py index c67acb81..2f351a9d 100644 --- a/backend/agents/base.py +++ b/backend/agents/base.py @@ -36,10 +36,10 @@ async def run(self, context: Dict[str, Any]) -> Dict[str, Any]: """ pass - async def update_progress(self, progress: float, status: str = "running"): + def update_progress(self, progress: float, status: str = "running"): self.state.progress = progress self.state.status = status - await self._sync_state() + self._sync_state() logger.info( f"Agent {self.agent_type} progress updated", extra={ @@ -49,7 +49,7 @@ async def update_progress(self, progress: float, status: str = "running"): } ) - async def _sync_state(self): + def _sync_state(self): """ Syncs current agent state to Firestore. Rule 4.3 compliant: Always save intermediate state to Firestore. diff --git a/backend/agents/contract_agent.py b/backend/agents/contract_agent.py index c6a38c6a..64f2bb8d 100644 --- a/backend/agents/contract_agent.py +++ b/backend/agents/contract_agent.py @@ -1,14 +1,14 @@ # backend/agents/contract_agent.py from typing import Dict, Any from backend.agents.base import BaseAgent -from backend.services.gemini import GeminiService +from backend.services.gemini import gemini_client from backend.core.config import settings from backend.core.logging import logger class ContractAgent(BaseAgent): def __init__(self, session_id: str): super().__init__("contract", session_id) - self.gemini = GeminiService() + self.gemini = gemini_client async def run(self, context: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/backend/agents/job_agent.py b/backend/agents/job_agent.py index 4bad9fe9..6384c656 100644 --- a/backend/agents/job_agent.py +++ b/backend/agents/job_agent.py @@ -5,7 +5,7 @@ from backend.utils.currency import format_salary_display, FALLBACK_RATE from backend.services.currency_converter import currency_converter_service from backend.utils.timezones import calculate_wat_overlap -from backend.services.firecrawl import FirecrawlService +from backend.services.firecrawl import firecrawl_service from backend.core.config import settings from backend.core.logging import logger from backend.models.job import Job @@ -13,7 +13,7 @@ class JobAgent(BaseAgent): def __init__(self, session_id: str): super().__init__("job", session_id) - self.firecrawl = FirecrawlService() + self.firecrawl = firecrawl_service self.currency_converter_service = currency_converter_service async def run(self, context: Dict[str, Any]) -> Dict[str, Any]: diff --git a/backend/agents/linkedin_agent.py b/backend/agents/linkedin_agent.py index 710dcc63..eff5378a 100644 --- a/backend/agents/linkedin_agent.py +++ b/backend/agents/linkedin_agent.py @@ -2,6 +2,7 @@ from typing import Dict, Any from backend.agents.base import BaseAgent from backend.services.linkedin import linkedin_service +from backend.services.gemini import gemini_client from backend.core.config import settings from backend.core.logging import logger @@ -9,6 +10,7 @@ class LinkedinAgent(BaseAgent): def __init__(self, session_id: str): super().__init__("linkedin", session_id) self.linkedin_service = linkedin_service + self.gemini = gemini_client async def run(self, context: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/backend/agents/orchestrator.py b/backend/agents/orchestrator.py index c7006e63..50613d6d 100644 --- a/backend/agents/orchestrator.py +++ b/backend/agents/orchestrator.py @@ -40,7 +40,7 @@ async def execute_agent_with_retry(self, agent_type: str, context: Dict[str, Any agent.state.status = "running" agent.state.started_at = datetime.now(timezone.utc) - await agent._sync_state() + agent._sync_state() while retries <= max_retries: try: @@ -49,7 +49,7 @@ async def execute_agent_with_retry(self, agent_type: str, context: Dict[str, Any agent.state.progress = 100.0 agent.state.result = result agent.state.completed_at = datetime.now(timezone.utc) - await agent._sync_state() + agent._sync_state() logger.info( f"Agent {agent_type} completed successfully", @@ -78,7 +78,7 @@ async def execute_agent_with_retry(self, agent_type: str, context: Dict[str, Any agent.state.status = "failed" agent.state.error = str(e) agent.state.completed_at = datetime.now(timezone.utc) - await agent._sync_state() + agent._sync_state() logger.error( f"Agent {agent_type} exhausted all retries", extra={ diff --git a/backend/agents/resume_agent.py b/backend/agents/resume_agent.py index 9aa00f6f..8fcef50f 100644 --- a/backend/agents/resume_agent.py +++ b/backend/agents/resume_agent.py @@ -1,14 +1,14 @@ # backend/agents/resume_agent.py from typing import Dict, Any from backend.agents.base import BaseAgent -from backend.services.gemini import GeminiService +from backend.services.gemini import gemini_client from backend.core.config import settings from backend.core.logging import logger class ResumeAgent(BaseAgent): def __init__(self, session_id: str): super().__init__("resume", session_id) - self.gemini = GeminiService() + self.gemini = gemini_client async def run(self, context: Dict[str, Any]) -> Dict[str, Any]: """ diff --git a/backend/core/config.py b/backend/core/config.py index d1c01b37..89285e8f 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -12,6 +12,8 @@ class Settings(BaseSettings): FIRESTORE_PROJECT_ID: str = os.getenv("FIRESTORE_PROJECT_ID", "jobjockey-default") # API Keys + FIRECRAWL_API_KEY: Optional[str] = os.getenv("FIRECRAWL_API_KEY", None) + GEMINI_API_KEY: Optional[str] = os.getenv("GEMINI_API_KEY", None) EXCHANGERATE_API_KEY: Optional[str] = os.getenv("EXCHANGERATE_API_KEY", None) GITHUB_TOKEN: Optional[str] = os.getenv("GITHUB_TOKEN", None) HACKERRANK_API_KEY: Optional[str] = os.getenv("HACKERRANK_API_KEY", None) diff --git a/backend/services/firecrawl.py b/backend/services/firecrawl.py index 67f5b5ef..6578d3b9 100644 --- a/backend/services/firecrawl.py +++ b/backend/services/firecrawl.py @@ -3,7 +3,7 @@ from firecrawl import FirecrawlApp from backend.core.logging import logger -from typing import List, Dict, Any +from backend.core.config import settings class FirecrawlService: def __init__(self, api_key: str): diff --git a/backend/services/linkedin.py b/backend/services/linkedin.py index 00382d73..b5067abd 100644 --- a/backend/services/linkedin.py +++ b/backend/services/linkedin.py @@ -36,3 +36,5 @@ async def send_direct_message(self, chat_id: str, text: str) -> bool: # Simulate API call delay await asyncio.sleep(0.5) return True + +linkedin_service = LinkedInService() diff --git a/backend/tests/test_contract_agent.py b/backend/tests/test_contract_agent.py index c63c78ab..3ed87159 100644 --- a/backend/tests/test_contract_agent.py +++ b/backend/tests/test_contract_agent.py @@ -5,9 +5,8 @@ @pytest.mark.asyncio async def test_contract_agent_run(mock_openai): - with patch("backend.agents.contract_agent.OpenAIService", return_value=mock_openai), \ + with patch("backend.agents.contract_agent.gemini_client", mock_openai), \ patch("backend.agents.base.firebase_service"): - agent = ContractAgent(session_id="test-session") context = { "contract_text": "Sample US employment contract with relocation lock-in.", diff --git a/backend/tests/test_integration.py b/backend/tests/test_integration.py index abef8d52..342e39e9 100644 --- a/backend/tests/test_integration.py +++ b/backend/tests/test_integration.py @@ -202,7 +202,7 @@ async def test_orchestrator_executes_agent_and_updates_state(mock_job_agent): result = await orchestrator.execute_agent_with_retry("job", context) mock_job_agent.run.assert_awaited_once_with(context) - mock_job_agent._sync_state.assert_awaited() # Should be called multiple times for state updates + mock_job_agent._sync_state.assert_called() # Should be called multiple times for state updates assert mock_job_agent.state.status == "completed" assert mock_job_agent.state.progress == 100.0 @@ -241,3 +241,4 @@ async def test_orchestrator_handles_persistent_agent_failure(mock_job_agent): assert "Persistent simulated failure" in mock_job_agent.state.error assert result == {"status": "failed", "error": "Persistent simulated failure"} + diff --git a/backend/tests/test_job_agent.py b/backend/tests/test_job_agent.py index fd0f88d1..79655c7e 100644 --- a/backend/tests/test_job_agent.py +++ b/backend/tests/test_job_agent.py @@ -6,7 +6,7 @@ @pytest.mark.asyncio async def test_job_agent_run(mock_firecrawl): # Patch FirecrawlService and firebase_service to avoid real network/db calls - with patch("backend.agents.job_agent.FirecrawlService", return_value=mock_firecrawl), \ + with patch("backend.agents.job_agent.firecrawl_service", mock_firecrawl), \ patch("backend.agents.base.firebase_service") as mock_fb: agent = JobAgent(session_id="test-session") diff --git a/backend/tests/test_linkedin_agent.py b/backend/tests/test_linkedin_agent.py index 5ab85259..3a5b1494 100644 --- a/backend/tests/test_linkedin_agent.py +++ b/backend/tests/test_linkedin_agent.py @@ -5,9 +5,8 @@ @pytest.mark.asyncio async def test_linkedin_agent_run(mock_openai): - with patch("backend.agents.linkedin_agent.OpenAIService", return_value=mock_openai), \ + with patch("backend.agents.linkedin_agent.gemini_client", mock_openai), \ patch("backend.agents.base.firebase_service"): - agent = LinkedinAgent(session_id="test-session") context = { "recruiter_name": "Jane Smith", diff --git a/backend/tests/test_resume_agent.py b/backend/tests/test_resume_agent.py index 14580f18..0c30af1e 100644 --- a/backend/tests/test_resume_agent.py +++ b/backend/tests/test_resume_agent.py @@ -5,9 +5,8 @@ @pytest.mark.asyncio async def test_resume_agent_run(mock_openai): - with patch("backend.agents.resume_agent.OpenAIService", return_value=mock_openai), \ + with patch("backend.agents.resume_agent.gemini_client", mock_openai), \ patch("backend.agents.base.firebase_service"): - agent = ResumeAgent(session_id="test-session") context = { "resume_text": "NYSC Software Engineer", diff --git a/backend/tests/test_utils.py b/backend/tests/test_utils.py index c7cfe2a7..5afbdf9f 100644 --- a/backend/tests/test_utils.py +++ b/backend/tests/test_utils.py @@ -4,10 +4,11 @@ from backend.utils.currency import get_usd_to_ngn_rate, format_salary_display from backend.utils.timezones import calculate_wat_overlap -def test_currency_formatting(): +@pytest.mark.asyncio +async def test_currency_formatting(): # Mock rate to 1600 for predictable output with patch("backend.utils.currency.get_usd_to_ngn_rate", return_value=1600.0): - display = format_salary_display(100000.0) + display = await format_salary_display(100000.0) assert "$100,000" in display assert "₦160,000,000" in display diff --git a/backend/utils/currency.py b/backend/utils/currency.py index 90d77e3b..66c343bf 100644 --- a/backend/utils/currency.py +++ b/backend/utils/currency.py @@ -5,14 +5,19 @@ # Base exchange rate utility. Defaults to 1550 NGN to 1 USD as a fallback. FALLBACK_RATE = 1550.0 + +async def get_usd_to_ngn_rate() -> float: + rate = await currency_converter_service.get_exchange_rate("USD", "NGN") + if rate is None: + rate = FALLBACK_RATE + return rate + async def format_salary_display(salary_usd: float) -> str: """ Formats a USD salary display with NGN equivalent. Example: $80,000 / yr (~₦124,000,000 NGN) """ - rate = await currency_converter_service.get_exchange_rate("USD", "NGN") - if rate is None: - rate = FALLBACK_RATE + rate = await get_usd_to_ngn_rate() salary_ngn = salary_usd * rate formatted_usd = f"${salary_usd:,.0f}" formatted_ngn = f"₦{salary_ngn:,.0f}"