| from typing import Literal, Annotated |
| import os |
| from pydantic import BeforeValidator, model_validator |
| from pydantic_settings import BaseSettings, SettingsConfigDict |
|
|
| def parse_cors_origins(v: any) -> list[str]: |
| if isinstance(v, str): |
| if not v.strip(): |
| return [] |
| if v.startswith("[") and v.endswith("]"): |
| try: |
| import json |
| return json.loads(v) |
| except Exception: |
| pass |
| return [item.strip() for item in v.split(",") if item.strip()] |
| elif isinstance(v, list): |
| return [str(item).strip() for item in v] |
| return v |
|
|
|
|
| class Settings(BaseSettings): |
| |
| gemini_api_key: str = "" |
| gemini_api_key_1: str = "" |
| gemini_api_key_2: str = "" |
| gemini_api_key_3: str = "" |
| openai_api_key: str = "" |
| pubmed_email: str = "" |
| pubmed_api_key: str = "" |
| pubmed_email_1: str = "" |
| pubmed_api_key_1: str = "" |
| pubmed_email_2: str = "" |
| pubmed_api_key_2: str = "" |
|
|
| |
| extraction_model: str = "gemini-2.5-flash" |
| judge_model: str = "gemini-2.5-flash" |
| |
| |
| |
| |
| llm_model: str = "" |
| llm_provider: Literal["gemini", "openai"] = "gemini" |
| nli_model: str = "cross-encoder/nli-deberta-v3-large" |
| gemini_rate_limit_interval: float = float(os.getenv("GEMINI_RATE_LIMIT_INTERVAL", "4.2")) |
|
|
| |
| max_papers: int = 25 |
| min_papers: int = 5 |
| claims_per_abstract_cap: int = 7 |
| quote_anchor_pass_threshold: float = 85.0 |
| quote_anchor_flag_threshold: float = 70.0 |
| faiss_top_k: int = 10 |
| nli_contradiction_threshold: float = 0.7 |
| max_contradictions_displayed: int = 15 |
|
|
| |
| pubmed_concurrency: int = 3 |
| llm_concurrency: int = 3 |
| section_concurrency: int = 1 |
|
|
| |
| |
| |
| |
| primary_sections_only: bool = True |
| primary_section_names: list[str] = [ |
| "abstract", |
| "results", |
| "result", |
| "discussion", |
| "discussions", |
| "conclusions", |
| "conclusion", |
| "findings", |
| "summary", |
| ] |
|
|
| |
| cost_per_paper: float = 0.0008 |
| cost_per_contradiction: float = 0.008 |
| cost_synthesis: float = 0.045 |
|
|
| |
| db_path: str = "data/claims.db" |
| faiss_index_path: str = "data/claims.faiss" |
| synonym_map_path: str = "data/synonym_map.json" |
|
|
| |
| allowed_origins: Annotated[list[str], BeforeValidator(parse_cors_origins)] = [ |
| "http://localhost:3000", |
| "http://127.0.0.1:3000", |
| ] |
|
|
| @property |
| def gemini_api_keys(self) -> list[str]: |
| keys = [] |
| |
| if self.gemini_api_key and self.gemini_api_key.strip(): |
| keys.append(self.gemini_api_key.strip()) |
| |
| for k in [self.gemini_api_key_1, self.gemini_api_key_2, self.gemini_api_key_3]: |
| if k and k.strip(): |
| keys.append(k.strip()) |
| return keys |
|
|
| @property |
| def pubmed_credentials(self) -> list[tuple[str, str]]: |
| pairs = [] |
| |
| email_1 = self.pubmed_email_1.strip() if self.pubmed_email_1 else "" |
| key_1 = self.pubmed_api_key_1.strip() if self.pubmed_api_key_1 else "" |
| if email_1 or key_1: |
| pairs.append((email_1, key_1)) |
| |
| |
| email_2 = self.pubmed_email_2.strip() if self.pubmed_email_2 else "" |
| key_2 = self.pubmed_api_key_2.strip() if self.pubmed_api_key_2 else "" |
| if email_2 or key_2: |
| pairs.append((email_2, key_2)) |
| |
| |
| if not pairs: |
| email_main = self.pubmed_email.strip() if self.pubmed_email else "" |
| key_main = self.pubmed_api_key.strip() if self.pubmed_api_key else "" |
| if email_main or key_main: |
| pairs.append((email_main, key_main)) |
| return pairs |
|
|
| @model_validator(mode="after") |
| def _apply_llm_model_override(self) -> "Settings": |
| """If LLM_MODEL is set, use it for BOTH extraction and judging (cheap trial runs).""" |
| override = self.llm_model.strip() if self.llm_model else "" |
| if override: |
| self.extraction_model = override |
| self.judge_model = override |
| return self |
|
|
| |
| model_config = SettingsConfigDict( |
| env_file=".env", |
| env_file_encoding="utf-8", |
| extra="ignore" |
| ) |
|
|
| settings = Settings() |
|
|