citation-validator / components /basic_formatter.py
iluksic's picture
Added initial app version
f473a91
Raw
History Blame Contribute Delete
7.05 kB
"""
Basic rule-based formatting of API validation results without AI.
"""
import logging
from typing import Dict, List, Any, Optional
logger = logging.getLogger(__name__)
class BasicFormatter:
"""
Formats API validation results using simple rules without AI.
Provides clean, display-friendly output based on API scores and matches.
"""
def __init__(self):
"""Initialize the basic formatter."""
pass
def format_results(self, api_results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Formats API results using rule-based logic without AI.
Args:
api_results: The results from the API validators.
Returns:
A dictionary containing formatted results.
"""
formatted_results = []
for result in api_results:
formatted = self._format_single_result(result)
formatted_results.append(formatted)
logger.info(f"Formatted {len(formatted_results)} results using basic rules")
return {"results": formatted_results, "candidates_grounding_metadata": []}
def _format_single_result(self, api_result: Dict[str, Any]) -> Dict[str, Any]:
"""
Formats a single API result.
Args:
api_result: Single API validation result.
Returns:
Formatted result dictionary.
"""
query = api_result.get("query", {})
exists = api_result.get("exists", False)
best_match = api_result.get("best_match", {})
# Score is stored in best_match as overall_score
score = best_match.get("overall_score", 0) if best_match else 0
source = api_result.get("source", "unknown")
# Check if there was an API error (critical difference from legitimate "not found")
error_type = api_result.get("error") # e.g., "rate_limit", "max_retries_exceeded", etc.
error_message = api_result.get("message", "")
# Extract data from query or best match
title = best_match.get("title") or query.get("title", "Unknown")
# Handle authors list - get first author
authors = best_match.get("authors", [])
first_author = authors[0] if authors else query.get("first_author", "Unknown")
year = best_match.get("year") or query.get("year", "Unknown")
journal = best_match.get("journal") or query.get("journal", "Unknown")
link = best_match.get("link") or best_match.get("url") or best_match.get("doi", "")
# Build explanation and identify issues (now includes error detection)
explanation, issues = self._build_explanation_and_issues(
exists, score, source, query, best_match, error_type, error_message
)
return {
"title": title,
"exists": exists,
"link": link,
"explanation": explanation,
"first_author": first_author,
"year": str(year),
"journal": journal,
"issues": issues if issues else None
}
def _build_explanation_and_issues(
self,
exists: bool,
score: float,
source: str,
query: Dict[str, Any],
best_match: Dict[str, Any],
error_type: Optional[str] = None,
error_message: str = ""
) -> tuple[str, List[str]]:
"""
Builds explanation and identifies issues based on API results.
Args:
exists: Whether reference exists in API
score: Match score
source: API source name
query: Original query data
best_match: Best match from API
error_type: Type of error if API failed (e.g., "rate_limit", "max_retries_exceeded")
error_message: Human-readable error message
Returns:
Tuple of (explanation string, list of issues)
"""
issues = []
# Handle API errors differently from legitimate "not found"
if not exists:
if error_type:
# API error occurred - don't claim "not found", explain the error
if error_type == "rate_limit":
explanation = "API rate limit exceeded - validation failed"
issues.append("API rate limit hit - try again later")
elif error_type == "max_retries_exceeded":
explanation = "API unavailable after retries - validation failed"
issues.append("API temporarily unavailable")
elif error_type == "API timeout":
explanation = "API request timed out - validation failed"
issues.append("Network timeout")
else:
# Generic error
explanation = f"Validation error: {error_message}"
issues.append(f"API error: {error_type}")
return explanation, issues
else:
# No error - legitimate "not found"
return "Not found in any API database", []
# Build explanation
if score >= 90:
explanation = f"Confirmed via {source.title()}"
elif score >= 70:
explanation = f"Likely match via {source.title()} (score: {score:.0f}%)"
issues.append("Medium confidence match")
else:
explanation = f"Weak match via {source.title()} (score: {score:.0f}%)"
issues.append("Low confidence match")
# Check for discrepancies
query_year = str(query.get("year", ""))
match_year = str(best_match.get("year", ""))
if query_year and match_year and query_year != match_year:
issues.append(f"Year mismatch: {query_year} vs {match_year}")
query_title = query.get("title", "").lower()
match_title = best_match.get("title", "").lower()
if query_title and match_title:
# Simple title similarity check
if query_title not in match_title and match_title not in query_title:
# Check if at least some words match
query_words = set(query_title.split())
match_words = set(match_title.split())
common_words = query_words & match_words
if len(common_words) < min(3, len(query_words) // 2):
issues.append("Title variation detected")
query_author = query.get("first_author", "").lower()
# Get first author from authors list
match_authors = best_match.get("authors", [])
match_author = match_authors[0].lower() if match_authors else ""
if query_author and match_author and query_author != match_author:
# Check if last names match (simple check)
query_last = query_author.split()[-1] if query_author else ""
match_last = match_author.split()[-1] if match_author else ""
if query_last != match_last:
issues.append(f"Author mismatch: {query_author} vs {match_author}")
return explanation, issues