from __future__ import annotations import uuid from datetime import datetime, timedelta, timezone from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from api.models.suggestion import ( OPEN_SUGGESTION_STAGES, Suggestion, SuggestionRelevanceBump, SuggestionStage, ) DEBOUNCE_SECONDS = 3600 async def bump_relevance( session: AsyncSession, suggestion: Suggestion, *, source: str, source_key: str, reason: str | None = None, ) -> bool: """Increment relevance when a suggestion is needed-but-unmet. Returns True when the counter was incremented, False when debounced. """ if suggestion.stage not in OPEN_SUGGESTION_STAGES: return False cutoff = datetime.now(tz=timezone.utc) - timedelta(seconds=DEBOUNCE_SECONDS) existing = ( await session.execute( select(SuggestionRelevanceBump.id) .where(SuggestionRelevanceBump.suggestion_id == suggestion.id) .where(SuggestionRelevanceBump.source == source) .where(SuggestionRelevanceBump.source_key == source_key) .where(SuggestionRelevanceBump.created_at >= cutoff) .limit(1) ) ).scalar_one_or_none() if existing is not None: return False now = datetime.now(tz=timezone.utc) suggestion.relevance += 1 suggestion.relevance_events += 1 suggestion.last_requested_at = now session.add( SuggestionRelevanceBump( suggestion_id=suggestion.id, source=source, source_key=source_key, reason=reason, ) ) return True async def bump_matching_for_capability_request( session: AsyncSession, *, request_id: uuid.UUID, title: str, description: str, capability_type: str, catalog_entry_id: uuid.UUID | None = None, ) -> int: """Bump open suggestions that match an unfulfilled capability need.""" rows = ( await session.execute( select(Suggestion).where(Suggestion.stage.in_(OPEN_SUGGESTION_STAGES)) ) ).scalars().all() haystack = f"{title} {description} {capability_type}".lower() bumped = 0 for suggestion in rows: matched = False if suggestion.origin_ref: ref = suggestion.origin_ref.lower() if ref in haystack or any(token in haystack for token in ref.split("-") if len(token) > 4): matched = True if not matched and suggestion.title.lower() in haystack: matched = True if not matched: continue if await bump_relevance( session, suggestion, source="capability_request", source_key=str(request_id), reason=f"Capability request matched: {title}", ): bumped += 1 return bumped async def bump_suggestions_for_next_steps( session: AsyncSession, suggestions: list[Suggestion], ) -> int: """Bump only suggestions surfaced in a next-steps response.""" bucket = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H") bumped = 0 for suggestion in suggestions: if await bump_relevance( session, suggestion, source="next_steps", source_key=bucket, reason="Surfaced during get_next_steps lookup", ): bumped += 1 return bumped