Spaces:
Sleeping
Sleeping
| import os | |
| import re | |
| import sys | |
| import threading | |
| import time | |
| import unicodedata | |
| from typing import Any | |
| import torch | |
| from fastapi import FastAPI, HTTPException | |
| from pydantic import BaseModel | |
| from transformers import AutoModelForCausalLM, AutoTokenizer | |
| sys.path.append("/app/iDEM_writing_assistant") | |
| sys.path.append("/app") | |
| try: | |
| from core.correctifier import Correctifier | |
| except Exception: | |
| Correctifier = None | |
| app = FastAPI() | |
| _model_lock = threading.Lock() | |
| _generation_lock = threading.Lock() | |
| _model = None | |
| _model_error = None | |
| MODEL_ALIASES = { | |
| "QWEN05B": "Qwen/Qwen2.5-0.5B-Instruct", | |
| "LLAMA1B": "meta-llama/Llama-3.2-1B-Instruct", | |
| "GEMMA2B": "google/gemma-2-2b-it", | |
| "SALAMANDRA2B": "BSC-LT/salamandra-2b-instruct", | |
| "LLAMA3B": "meta-llama/Llama-3.2-3B-Instruct", | |
| "GEMMA4B": "google/gemma-3-4b-it", | |
| "OLMO7B": "allenai/OLMo-2-1124-7B-Instruct", | |
| "SALAMANDRA7B": "BSC-LT/salamandra-7b-instruct", | |
| } | |
| OFFICIAL_IDEM_MODELS = { | |
| "LLAMA1B", | |
| "GEMMA2B", | |
| "SALAMANDRA2B", | |
| "LLAMA3B", | |
| "GEMMA4B", | |
| "OLMO7B", | |
| "SALAMANDRA7B", | |
| } | |
| CPU_THREADS = max(1, min(int(os.getenv("IDEM_CPU_THREADS", "2")), os.cpu_count() or 1)) | |
| torch.set_num_threads(CPU_THREADS) | |
| try: | |
| torch.set_num_interop_threads(1) | |
| except RuntimeError: | |
| pass | |
| LANGUAGE_NAMES = { | |
| "en": "English", | |
| "es": "Spanish", | |
| "ca": "Catalan", | |
| } | |
| FALLBACKS = { | |
| "en": "I do not have enough information in the context to answer.", | |
| "es": "No tengo informacion suficiente en el contexto para responder.", | |
| "ca": "No tinc prou informacio en el context per respondre.", | |
| } | |
| IDEM_SIMPLIFICATION_PROMPTS = { | |
| "en": ( | |
| "Please rewrite the following complex sentence in order to make it easier to understand by non-native " | |
| "speakers of English. You can do so by replacing complex words with simpler synonyms (i.e. paraphrasing), " | |
| "deleting unimportant information (i.e. compression), and/or splitting a long complex sentence into " | |
| "several simpler ones. The final simplified sentence needs to be grammatical, fluent, and retain the " | |
| "main ideas of its original counterpart without altering its meaning.\n" | |
| "Return one reformulation. Do not generate any text except the reformulation." | |
| ), | |
| "es": ( | |
| "Por favor, reescriba la siguiente oración compleja para que sea más fácil de entender para quienes no " | |
| "hablan español como lengua materna. Puede hacerlo reemplazando palabras complejas con sinónimos más " | |
| "simples, eliminando información irrelevante o dividiendo una oración compleja larga en varias más " | |
| "simples. La oración simplificada final debe ser gramaticalmente correcta, fluida y conservar las ideas " | |
| "principales de su contraparte original sin alterar su significado.\n" | |
| "Da una reformulación. No genere ningún texto excepto la reformulación." | |
| ), | |
| "ca": ( | |
| "Reescriviu la següent frase complexa per tal que sigui més fàcil d'entendre per a parlants " | |
| "no nadius del català. Podeu fer-ho substituint paraules complexes per sinònims més simples, eliminant " | |
| "informació no important o dividint una frase llarga i complexa en diverses de més simples. La frase " | |
| "simplificada final ha de ser gramatical, fluida i conservar les idees principals de la seva contrapart " | |
| "original sense alterar-ne el significat.\n" | |
| "Dona una reformulació. No genereu cap text excepte la reformulació." | |
| ), | |
| } | |
| class AnswerRequest(BaseModel): | |
| question: str = "" | |
| prompt: str | None = None | |
| text: str | None = None | |
| context: list[dict[str, Any]] | None = None | |
| rows: list[dict[str, Any]] | None = None | |
| graph_context: dict[str, Any] | None = None | |
| language: str = "es" | |
| room: str | None = None | |
| artwork: str | None = None | |
| visitor_profile: str | None = None | |
| personality: str | None = None | |
| instructions: str | None = None | |
| max_new_tokens: int | None = None | |
| class IdemGenerator: | |
| def __init__(self, model_name: str, token: str | None, device: torch.device): | |
| self.device = device | |
| self.model_name = model_name.upper() | |
| self.use_official_correctifier = ( | |
| os.getenv("IDEM_USE_OFFICIAL_CORRECTIFIER", "true").strip().lower() != "false" | |
| and Correctifier is not None | |
| and self.model_name in OFFICIAL_IDEM_MODELS | |
| ) | |
| if self.use_official_correctifier: | |
| print("Loading official iDEM Correctifier", self.model_name, "device", device, flush=True) | |
| self.correctifier = Correctifier(self.model_name, token, device, lang="en-multi") | |
| self.model = self.correctifier.model | |
| self.tokenizer = self.correctifier.tokenizer | |
| self.model_id = self.correctifier.pretrained_model | |
| print("Official iDEM Correctifier loaded.", flush=True) | |
| return | |
| self.correctifier = None | |
| self.model_id = MODEL_ALIASES.get(self.model_name, model_name) | |
| self.tokenizer = AutoTokenizer.from_pretrained(self.model_id, token=token, use_fast=True) | |
| if self.tokenizer.pad_token_id is None: | |
| self.tokenizer.pad_token = self.tokenizer.eos_token | |
| dtype = torch.bfloat16 if device.type == "cuda" else torch.float32 | |
| kwargs: dict[str, Any] = {"token": token, "torch_dtype": dtype} | |
| if device.type == "cuda": | |
| kwargs["device_map"] = "auto" | |
| print( | |
| "Loading model", | |
| self.model_id, | |
| "device", | |
| device, | |
| "cuda", | |
| torch.cuda.is_available(), | |
| "gpus", | |
| torch.cuda.device_count(), | |
| "dtype", | |
| dtype, | |
| "cpu_threads", | |
| CPU_THREADS, | |
| flush=True, | |
| ) | |
| self.model = AutoModelForCausalLM.from_pretrained(self.model_id, **kwargs).eval() | |
| if device.type != "cuda": | |
| self.model.to(device) | |
| self.model.generation_config.pad_token_id = self.tokenizer.pad_token_id | |
| self.model.generation_config.do_sample = False | |
| self.model.generation_config.temperature = None | |
| self.model.generation_config.top_p = None | |
| self.model.generation_config.top_k = None | |
| print("Model loaded.", flush=True) | |
| print( | |
| "Model on GPU: " | |
| + str(round(100 * sum(param.is_cuda for param in self.model.parameters()) / len(list(self.model.parameters())))) | |
| + "%", | |
| flush=True, | |
| ) | |
| def render_prompt(self, prompt: str) -> str: | |
| messages = [{"role": "user", "content": prompt}] | |
| try: | |
| return self.tokenizer.apply_chat_template( | |
| messages, | |
| tokenize=False, | |
| add_generation_prompt=True, | |
| ) | |
| except Exception: | |
| return prompt | |
| def generate(self, prompt: str, max_new_tokens: int) -> str: | |
| max_input_tokens = int(os.getenv("IDEM_MAX_INPUT_TOKENS", "1024")) | |
| rendered_prompt = self.render_prompt(prompt) | |
| inputs = self.tokenizer( | |
| rendered_prompt, | |
| return_tensors="pt", | |
| return_token_type_ids=False, | |
| truncation=True, | |
| max_length=max_input_tokens, | |
| ) | |
| inputs = {key: value.to(self.device) for key, value in inputs.items()} | |
| input_len = inputs["input_ids"].shape[-1] | |
| print("Input tokens:", input_len, "Max new tokens:", max_new_tokens, flush=True) | |
| start = time.perf_counter() | |
| with torch.inference_mode(): | |
| generation = self.model.generate( | |
| **inputs, | |
| max_new_tokens=max_new_tokens, | |
| do_sample=False, | |
| num_beams=1, | |
| use_cache=True, | |
| eos_token_id=self.tokenizer.eos_token_id, | |
| pad_token_id=self.tokenizer.pad_token_id, | |
| ) | |
| elapsed = time.perf_counter() - start | |
| print("Response time:", round(elapsed, 2), flush=True) | |
| output_ids = generation[0][input_len:] | |
| return self.tokenizer.decode(output_ids, skip_special_tokens=True) | |
| def simplify_with_official_idem(self, text: str, lang: str) -> str: | |
| if not self.use_official_correctifier: | |
| return "" | |
| self.correctifier.lang = lang | |
| prompt = IDEM_SIMPLIFICATION_PROMPTS.get(lang, IDEM_SIMPLIFICATION_PROMPTS["en"]) | |
| try: | |
| return self.correctifier.correct(text, force_prompt=prompt) | |
| finally: | |
| self.correctifier.lang = "en-multi" | |
| def language_code(value: str | None) -> str: | |
| key = (value or "es").strip().lower() | |
| if key in {"english", "eng", "en"}: | |
| return "en" | |
| if key in {"spanish", "espanol", "es"}: | |
| return "es" | |
| if key in {"catalan", "catala", "ca"}: | |
| return "ca" | |
| return "es" | |
| def load_model(): | |
| global _model, _model_error | |
| with _model_lock: | |
| if _model is not None: | |
| return _model | |
| if _model_error is not None: | |
| raise _model_error | |
| try: | |
| default_model = "SALAMANDRA2B" if torch.cuda.is_available() else "QWEN05B" | |
| model_name = os.getenv("IDEM_MODEL", default_model) | |
| token = os.getenv("HF_TOKEN") or os.getenv("IDEM_HF_TOKEN") | |
| device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu") | |
| _model = IdemGenerator(model_name, token, device) | |
| return _model | |
| except Exception as exc: | |
| _model_error = exc | |
| raise | |
| def warmup_model(): | |
| thread = threading.Thread(target=load_model, daemon=True) | |
| thread.start() | |
| def rows_to_context(rows: list[dict[str, Any]] | None) -> str: | |
| if not rows: | |
| return "No context is available." | |
| parts = [] | |
| max_rows = int(os.getenv("IDEM_MAX_CONTEXT_ROWS", "3")) | |
| max_value_chars = int(os.getenv("IDEM_MAX_CONTEXT_VALUE_CHARS", "700")) | |
| max_total_chars = int(os.getenv("IDEM_MAX_CONTEXT_CHARS", "2500")) | |
| for index, row in enumerate(rows[:max_rows], start=1): | |
| values = [] | |
| for key, value in row.items(): | |
| if value is None: | |
| continue | |
| if isinstance(value, str) and not value.strip(): | |
| continue | |
| text = str(value).strip() | |
| if len(text) > max_value_chars: | |
| text = text[:max_value_chars].rsplit(" ", 1)[0].strip() + "..." | |
| values.append(f"{key}: {text}") | |
| if values: | |
| parts.append(f"{index}. " + "\n".join(values)) | |
| context = "\n\n".join(parts) if parts else "No context is available." | |
| if len(context) > max_total_chars: | |
| context = context[:max_total_chars].rsplit(" ", 1)[0].strip() + "..." | |
| return context | |
| def normalize_key(value: str) -> str: | |
| normalized = unicodedata.normalize("NFKD", str(value)) | |
| ascii_text = normalized.encode("ascii", "ignore").decode("ascii") | |
| return re.sub(r"[^a-z0-9]+", "_", ascii_text.lower()).strip("_") | |
| def truncate_text(text: str, max_chars: int) -> str: | |
| if len(text) <= max_chars: | |
| return text | |
| cut = text[:max_chars].strip() | |
| sentence_end = max(cut.rfind("."), cut.rfind("!"), cut.rfind("?")) | |
| if sentence_end >= max_chars * 0.55: | |
| return cut[:sentence_end + 1].strip() | |
| word_end = cut.rfind(" ") | |
| if word_end >= max_chars * 0.55: | |
| return cut[:word_end].strip() + "..." | |
| return cut + "..." | |
| def clean_value(value: Any, max_chars: int = 220) -> str: | |
| if value is None: | |
| return "" | |
| if isinstance(value, (list, tuple, set)): | |
| text = ", ".join(clean_value(item, max_chars=max_chars) for item in value) | |
| else: | |
| text = str(value) | |
| text = text.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\t", " ") | |
| text = re.sub(r"[ \t]+", " ", text) | |
| text = re.sub(r"\n{3,}", "\n\n", text).strip() | |
| if not text or text.lower() in {"none", "null", "nan", "not provided"}: | |
| return "" | |
| if text.startswith(("http://", "https://")): | |
| return "" | |
| return truncate_text(text, max_chars) | |
| FIELD_ALIASES = { | |
| "title": ("title", "titol", "titulo", "obra", "artwork", "nom_obra", "name"), | |
| "artist": ("artist", "artista", "autor", "autoria", "creator", "created_by"), | |
| "date": ("date", "data", "any", "year", "cronologia", "chronology", "datacio"), | |
| "room": ("room", "sala", "ubicacio", "location", "palau"), | |
| "material": ("material", "materials", "technique", "tecnica", "medium"), | |
| "description": ( | |
| "description", | |
| "descripcio", | |
| "descripcion", | |
| "text", | |
| "resum", | |
| "summary", | |
| "explicacio", | |
| "comentari", | |
| "context", | |
| ), | |
| } | |
| def row_value(row: dict[str, Any], field: str) -> str: | |
| aliases = FIELD_ALIASES[field] | |
| normalized_items = [(normalize_key(key), value) for key, value in row.items()] | |
| for key, value in normalized_items: | |
| if key in aliases or key.split("_")[-1] in aliases: | |
| cleaned = clean_value(value) | |
| if cleaned: | |
| return cleaned | |
| for key, value in normalized_items: | |
| if any(alias in key for alias in aliases): | |
| cleaned = clean_value(value) | |
| if cleaned: | |
| return cleaned | |
| return "" | |
| def best_context_row(rows: list[dict[str, Any]], req: AnswerRequest) -> dict[str, Any]: | |
| if not rows: | |
| return {} | |
| wanted = " ".join( | |
| clean_value(value, max_chars=120).lower() | |
| for value in (req.artwork, req.room, req.question) | |
| if clean_value(value, max_chars=120) | |
| ) | |
| if not wanted: | |
| return rows[0] | |
| def score(row: dict[str, Any]) -> int: | |
| text = " ".join(clean_value(value, max_chars=500).lower() for value in row.values()) | |
| return sum(1 for word in re.findall(r"\w+", wanted) if len(word) > 3 and word in text) | |
| return max(rows, key=score) | |
| def text_looks_english(text: str) -> bool: | |
| words = set(re.findall(r"[a-zA-Z]+", text.lower())) | |
| english_markers = { | |
| "the", "with", "and", "of", "from", "that", "this", "it", "is", "are", | |
| "was", "were", "had", "has", "still", "also", "very", "likely", "because", | |
| } | |
| return len(words & english_markers) >= 3 | |
| def text_matches_language(text: str, lang: str) -> bool: | |
| if not text: | |
| return True | |
| if lang in {"ca", "es"} and text_looks_english(text): | |
| return False | |
| return True | |
| def is_meta_reformulation(text: str) -> bool: | |
| lower = text.lower().strip() | |
| meta_prefixes = ( | |
| "here are", | |
| "here is", | |
| "rephrasing", | |
| "rewrite", | |
| "si us plau", | |
| "aquí tenen", | |
| "aqui tenen", | |
| "aquí tens", | |
| "aqui tens", | |
| "te proporciono", | |
| "a continuación", | |
| "a continuacion", | |
| ) | |
| return ( | |
| lower.startswith(meta_prefixes) | |
| or lower == "si us plau" | |
| or "reformulació" in lower | |
| or "reformulación" in lower | |
| or "reformulation" in lower | |
| ) | |
| def extractive_answer(req: AnswerRequest, lang: str, description_override: str | None = None) -> str: | |
| rows = req.context or req.rows or [] | |
| if not rows: | |
| return "" | |
| row = best_context_row(rows, req) | |
| title = clean_value(req.artwork) or row_value(row, "title") | |
| artist = row_value(row, "artist") | |
| date = row_value(row, "date") | |
| room = clean_value(req.room) or row_value(row, "room") | |
| material = row_value(row, "material") | |
| description = row_value(row, "description") if description_override is None else description_override | |
| if lang == "ca": | |
| lines = [] | |
| if title: | |
| lines.append(f"L'obra es diu {title}.") | |
| if artist: | |
| lines.append(f"La va fer {artist}.") | |
| if date: | |
| lines.append(f"És de {date}.") | |
| if material: | |
| lines.append(f"Està feta amb {material}.") | |
| if description: | |
| lines.append(description) | |
| if room: | |
| lines.append(f"La pots veure a {room}.") | |
| elif lang == "es": | |
| lines = [] | |
| if title: | |
| lines.append(f"La obra se llama {title}.") | |
| if artist: | |
| lines.append(f"La hizo {artist}.") | |
| if date: | |
| lines.append(f"Es de {date}.") | |
| if material: | |
| lines.append(f"Está hecha con {material}.") | |
| if description: | |
| lines.append(description) | |
| if room: | |
| lines.append(f"La puedes ver en {room}.") | |
| else: | |
| lines = [] | |
| if title: | |
| lines.append(f"The artwork is called {title}.") | |
| if artist: | |
| lines.append(f"It was made by {artist}.") | |
| if date: | |
| lines.append(f"It is from {date}.") | |
| if material: | |
| lines.append(f"It is made with {material}.") | |
| if description: | |
| lines.append(description) | |
| if room: | |
| lines.append(f"You can see it in {room}.") | |
| cleaned_lines = [] | |
| for line in lines: | |
| line = clean_value(line, max_chars=260) | |
| if line and line not in cleaned_lines: | |
| cleaned_lines.append(line) | |
| return "\n".join(cleaned_lines[:5]) | |
| def build_factual_draft(req: AnswerRequest, lang: str) -> str: | |
| rows = req.context or req.rows or [] | |
| if not rows: | |
| return "" | |
| row = best_context_row(rows, req) | |
| title = clean_value(req.artwork) or row_value(row, "title") | |
| artist = row_value(row, "artist") | |
| date = row_value(row, "date") | |
| room = clean_value(req.room) or row_value(row, "room") | |
| material = row_value(row, "material") | |
| description = row_value(row, "description") | |
| facts = [] | |
| if title: | |
| facts.append(f"Title: {title}") | |
| if artist: | |
| facts.append(f"Artist: {artist}") | |
| if date: | |
| facts.append(f"Date: {date}") | |
| if material: | |
| facts.append(f"Material: {material}") | |
| if room: | |
| facts.append(f"Room: {room}") | |
| if description: | |
| facts.append(f"Description: {clean_value(description, max_chars=1400)}") | |
| return "\n".join(facts) | |
| def question_asks_artist(question: str) -> bool: | |
| normalized = normalize_key(question) | |
| terms = {"artista", "autor", "autoria", "artist", "author", "creator", "creador"} | |
| return any(term in normalized for term in terms) | |
| def sentence_join(lines: list[str]) -> str: | |
| return "\n".join(line for line in lines if line) | |
| def normalize_answer_text(text: str) -> str: | |
| text = str(text or "").replace("\\r\\n", "\n").replace("\\n", "\n").replace("\\t", " ") | |
| text = re.sub(r"[ \t]+", " ", text) | |
| text = re.sub(r" *\n *", "\n", text) | |
| text = re.sub(r"\n{3,}", "\n\n", text) | |
| return text.strip() | |
| def build_grounded_answer(req: AnswerRequest, lang: str) -> str: | |
| rows = req.context or req.rows or [] | |
| if not rows: | |
| return "" | |
| row = best_context_row(rows, req) | |
| title = clean_value(req.artwork) or row_value(row, "title") | |
| artist = row_value(row, "artist") | |
| date = row_value(row, "date") | |
| room = clean_value(req.room) or row_value(row, "room") | |
| material = row_value(row, "material") | |
| description = row_value(row, "description") | |
| if question_asks_artist(req.question): | |
| if not artist: | |
| return "" | |
| if lang == "ca": | |
| return f"L'artista de {title or 'aquesta obra'} és {artist}." | |
| if lang == "es": | |
| return f"El artista de {title or 'esta obra'} es {artist}." | |
| return f"The artist of {title or 'this artwork'} is {artist}." | |
| if lang == "ca": | |
| return sentence_join([ | |
| f"L'obra es diu {title}." if title else "", | |
| f"La va fer {artist}." if artist else "", | |
| f"És de {date}." if date else "", | |
| f"Està feta amb {material}." if material else "", | |
| clean_value(description, max_chars=1400) if description else "", | |
| f"La pots veure a {room}." if room else "", | |
| ]) | |
| if lang == "es": | |
| return sentence_join([ | |
| f"La obra se llama {title}." if title else "", | |
| f"La hizo {artist}." if artist else "", | |
| f"Es de {date}." if date else "", | |
| f"Está hecha con {material}." if material else "", | |
| clean_value(description, max_chars=1400) if description else "", | |
| f"La puedes ver en {room}." if room else "", | |
| ]) | |
| return sentence_join([ | |
| f"The artwork is called {title}." if title else "", | |
| f"It was made by {artist}." if artist else "", | |
| f"It is from {date}." if date else "", | |
| f"It is made with {material}." if material else "", | |
| clean_value(description, max_chars=1400) if description else "", | |
| f"You can see it in {room}." if room else "", | |
| ]) | |
| def introduces_unsupported_ownership(original: str, simplified: str) -> bool: | |
| original_key = normalize_key(original) | |
| simplified_key = normalize_key(simplified) | |
| ownership_terms = ("propietari", "propietaria", "dueno", "duena", "owner", "owns", "owned") | |
| return not any(term in original_key for term in ownership_terms) and any( | |
| term in simplified_key for term in ownership_terms | |
| ) | |
| def has_enough_context(req: AnswerRequest) -> bool: | |
| rows = req.context or req.rows or [] | |
| if not rows: | |
| return False | |
| row = best_context_row(rows, req) | |
| return any(row_value(row, field) for field in ("description", "artist", "date", "material")) | |
| def simplify_grounded_answer(generator: IdemGenerator, req: AnswerRequest, lang: str, max_new_tokens: int) -> str: | |
| grounded_answer = build_grounded_answer(req, lang) | |
| if not grounded_answer or not has_enough_context(req): | |
| return "" | |
| simplification_prompt = ( | |
| f"{IDEM_SIMPLIFICATION_PROMPTS.get(lang, IDEM_SIMPLIFICATION_PROMPTS['en'])}\n" | |
| f"INPUT:\n{grounded_answer}" | |
| ) | |
| simplified = generator.simplify_with_official_idem(grounded_answer, lang) | |
| if not simplified: | |
| raw_simplified = generator.generate(simplification_prompt, max_new_tokens=max_new_tokens) | |
| simplified = first_reformulation(raw_simplified, grounded_answer, lang) | |
| if not simplified: | |
| simplified = grounded_answer | |
| simplified = normalize_answer_text(clean_value(simplified, max_chars=3000)) | |
| if introduces_unsupported_ownership(grounded_answer, simplified): | |
| simplified = grounded_answer | |
| if not simplified or not text_matches_language(simplified, lang): | |
| return "" | |
| return simplified | |
| def clean_idem_output(raw: str, original_input: str) -> str: | |
| if not raw: | |
| return "" | |
| lines = [] | |
| for line in raw.splitlines(): | |
| cleaned = line.strip() | |
| if not cleaned: | |
| continue | |
| if cleaned in {"INPUT:", "OUTPUT:"}: | |
| continue | |
| if cleaned == original_input.strip(): | |
| continue | |
| if is_meta_reformulation(cleaned): | |
| continue | |
| cleaned = cleaned.lstrip("1234567890-").lstrip(".").strip() | |
| if cleaned: | |
| lines.append(cleaned) | |
| return normalize_answer_text("\n".join(lines).strip() or raw.strip()) | |
| def first_reformulation(output_text: str, original_text: str, lang: str) -> str: | |
| responses = [] | |
| for line in output_text.splitlines(): | |
| cleaned = clean_value(line, max_chars=3000) | |
| cleaned = re.sub(r"^(OUTPUT|INPUT|ANSWER|RESPOSTA|RESPUESTA)\s*:?\s*", "", cleaned, flags=re.I) | |
| cleaned = re.sub(r"^si us plau,?\s*", "", cleaned, flags=re.I) | |
| cleaned = cleaned.lstrip("1234567890-").lstrip(".").strip() | |
| if not cleaned or cleaned == original_text.strip(): | |
| continue | |
| if is_meta_reformulation(cleaned): | |
| continue | |
| if text_matches_language(cleaned, lang): | |
| responses.append(cleaned) | |
| return responses[0] if responses else "" | |
| def health(): | |
| return {"status": "ok", "service": "guia-idem-api"} | |
| def answer(req: AnswerRequest): | |
| generator = load_model() | |
| lang = language_code(req.language) | |
| language_name = LANGUAGE_NAMES[lang] | |
| direct_prompt = (req.prompt or req.text or "").strip() | |
| if direct_prompt and not req.question.strip(): | |
| input_text = direct_prompt[:2000] | |
| else: | |
| rows = req.context or req.rows | |
| if not rows: | |
| return {"answer": FALLBACKS[lang], "source": "no-context"} | |
| context_text = rows_to_context(rows) | |
| force_prompt = ( | |
| f"Answer in {language_name} only.\n" | |
| "Use only the museum context.\n" | |
| "Use Easy Read language: short sentences and common words.\n" | |
| "Answer with the detail needed by the question and available context.\n" | |
| "Use complete sentences. Do not cut the answer in the middle of a sentence.\n" | |
| "Do not invent facts. Return only the answer.\n" | |
| ) | |
| if req.instructions: | |
| force_prompt += f"\nExtra instructions:\n{str(req.instructions)[:700]}\n" | |
| input_text = ( | |
| f"{force_prompt}\n" | |
| f"Context:\n{context_text}\n\n" | |
| f"Room: {req.room or 'Not provided'}\n" | |
| f"Artwork: {req.artwork or 'Not provided'}\n" | |
| f"Question: {req.question[:320]}" | |
| ) | |
| default_tokens = 180 if generator.device.type == "cpu" else 220 | |
| requested_tokens = req.max_new_tokens or int(os.getenv("IDEM_MAX_NEW_TOKENS", str(default_tokens))) | |
| max_new_tokens = max(64, min(requested_tokens, 240 if generator.device.type == "cpu" else 320)) | |
| if not _generation_lock.acquire(blocking=False): | |
| raise HTTPException( | |
| status_code=503, | |
| detail="iDEM is already generating an answer. Please use the fallback or try again later.", | |
| ) | |
| try: | |
| use_cpu_simplifier = os.getenv("IDEM_CPU_SIMPLIFIER", "true").strip().lower() != "false" | |
| if ( | |
| generator.device.type == "cpu" | |
| and use_cpu_simplifier | |
| and not (direct_prompt and not req.question.strip()) | |
| and (req.context or req.rows) | |
| ): | |
| result = simplify_grounded_answer(generator, req, lang, max_new_tokens) | |
| source = "idem-easy-read" | |
| else: | |
| raw_result = generator.generate(input_text, max_new_tokens=max_new_tokens) | |
| result = clean_idem_output(raw_result, input_text) | |
| source = "idem-generate" | |
| finally: | |
| _generation_lock.release() | |
| if not result: | |
| result = FALLBACKS[lang] | |
| return {"answer": normalize_answer_text(result), "source": source} | |