yjernite/space-privacy
8
1import json # Added for TLDR JSON parsing2import logging3import os4import tempfile5 6from huggingface_hub import HfApi7from huggingface_hub.inference._generated.types import \8 ChatCompletionOutput # Added for type hinting9 10# Imports from other project modules11from llm_interface import (ERROR_503_DICT, parse_qwen_response,12 query_qwen_endpoint)13from prompts import format_privacy_prompt, format_summary_highlights_prompt14from utils import (PRIVACY_FILENAME, # Import constants for filenames15 SUMMARY_FILENAME, TLDR_FILENAME, check_report_exists,16 download_cached_reports, get_space_code_files)17 18# Configure logging (can inherit from app.py if called from there, but good practice)19logging.basicConfig(20 level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"21)22 23# Load environment variables - redundant if always called by app.py which already loads them24# load_dotenv()25 26# Constants needed by helper functions (can be passed as args too)27# Consider passing these from app.py if they might change or for clarity28CACHE_INFO_MSG = "\n\n*(Report retrieved from cache)*"29TRUNCATION_WARNING = """**⚠️ Warning:** The input data (code and/or prior analysis) was too long for the AI model's context limit and had to be truncated. The analysis below may be incomplete or based on partial information.\n\n---\n\n"""30 31# --- Constants for TLDR Generation ---32TLDR_SYSTEM_PROMPT = (33 "You are an AI assistant specialized in summarizing privacy analysis reports for Hugging Face Spaces. "34 "You will receive two reports: a detailed privacy analysis and a summary/highlights report. "35 "Based **only** on the content of these two reports, generate a concise JSON object containing a structured TLDR (Too Long; Didn't Read). "36 "Do not use any information not present in the provided reports. "37 "The JSON object must have the following keys:\n"38 '- "app_description": A 1-2 sentence summary of what the application does from a user\'s perspective.\n'39 '- "privacy_tldr": A 2-3 sentence high-level overview of privacy. Mention if the analysis was conclusive based on available code, if data processing is local, or if/what data goes to external services.\n'40 '- "data_types": A list of JSON objects, where each object has two keys: \'name\' (a short, unique identifier string for the data type, e.g., "User Text") and \'description\' (a brief string explaining the data type in context, max 6-8 words, e.g., "Text prompt entered by the user").\n'41 "- \"user_input_data\": A list of strings, where each string is the 'name' of a data type defined in 'data_types' that is provided by the user to the app.\n"42 "- \"local_processing\": A list of strings describing data processed locally. Each string should start with the 'name' of a data type defined in 'data_types', followed by details (like the processing model) in parentheses if mentioned in the reports. Example: \"User Text (Local Model XYZ)\".\n"43 "- \"remote_processing\": A list of strings describing data sent to remote services. Each string should start with the 'name' of a data type defined in 'data_types', followed by the service/model name in parentheses if mentioned in the reports. Example: \"User Text (HF Inference API)\".\n"44 "- \"external_logging\": A list of strings describing data logged or saved externally. Each string should start with the 'name' of a data type defined in 'data_types', followed by the location/service in parentheses if mentioned. Example: \"User Text (External DB)\".\n"45 "Ensure the output is **only** a valid JSON object, starting with `{` and ending with `}`. Ensure all listed data types in the processing/logging lists exactly match a 'name' defined in the 'data_types' list."46)47 48# --- Analysis Pipeline Helper Functions ---49 50 51def check_cache_and_download(space_id: str, dataset_id: str, hf_token: str | None):52 """Checks cache and downloads if reports exist."""53 logging.info(f"Checking cache for '{space_id}'...")54 found_in_cache = False55 if hf_token:56 try:57 found_in_cache = check_report_exists(space_id, dataset_id, hf_token)58 except Exception as e:59 logging.warning(f"Cache check failed for {space_id}: {e}. Proceeding.")60 # Return cache_miss even if check failed, proceed to live analysis61 return {"status": "cache_miss", "error_message": f"Cache check failed: {e}"}62 63 if found_in_cache:64 logging.info(f"Cache hit for {space_id}. Downloading.")65 try:66 cached_reports = download_cached_reports(space_id, dataset_id, hf_token)67 summary_report = (68 cached_reports.get("summary", "Error: Cached summary not found.")69 + CACHE_INFO_MSG70 )71 privacy_report = (72 cached_reports.get("privacy", "Error: Cached privacy report not found.")73 + CACHE_INFO_MSG74 )75 logging.info(f"Successfully downloaded cached reports for {space_id}.")76 return {77 "status": "cache_hit",78 "summary": summary_report,79 "privacy": privacy_report,80 "tldr_json_str": cached_reports.get("tldr_json_str"),81 }82 except Exception as e:83 error_msg = f"Cache download failed for {space_id}: {e}"84 logging.warning(f"{error_msg}. Proceeding with live analysis.")85 # Return error, but let caller decide if live analysis proceeds86 return {"status": "cache_error", "ui_message": error_msg}87 else:88 logging.info(f"Cache miss for {space_id}. Performing live analysis.")89 return {"status": "cache_miss"}90 91 92def check_endpoint_status(93 endpoint_name: str, hf_token: str | None, error_503_user_message: str94):95 """Checks the status of the inference endpoint."""96 logging.info(f"Checking endpoint status for '{endpoint_name}'...")97 if not hf_token:98 # Allow proceeding if token missing, maybe endpoint is public99 logging.warning("HF_TOKEN not set, cannot check endpoint status definitively.")100 return {"status": "ready", "warning": "HF_TOKEN not set"}101 102 try:103 api = HfApi(token=hf_token)104 endpoint = api.get_inference_endpoint(name=endpoint_name)105 status = endpoint.status106 logging.info(f"Endpoint '{endpoint_name}' status: {status}")107 108 if status == "running":109 return {"status": "ready"}110 else:111 logging.warning(112 f"Endpoint '{endpoint_name}' is not ready (Status: {status})."113 )114 if status == "scaledToZero":115 logging.info(116 f"Endpoint '{endpoint_name}' is scaled to zero. Attempting to resume..."117 )118 try:119 endpoint.resume()120 # Still return an error message suggesting retry, as resume takes time121 # Keep this message concise as the action is specific (wait)122 msg = f"**Endpoint Resuming:** The analysis endpoint ('{endpoint_name}') was scaled to zero and is now restarting.\n\n{error_503_user_message}"123 return {"status": "error", "ui_message": msg}124 except Exception as resume_error:125 # Resume failed, provide detailed message126 logging.error(127 f"Failed to resume endpoint {endpoint_name}: {resume_error}"128 )129 # Construct detailed message including full explanation130 msg = f"**Endpoint Issue:** The analysis endpoint ('{endpoint_name}') is currently {status} and an attempt to resume it failed ({resume_error}).\n\n{error_503_user_message}"131 return {"status": "error", "ui_message": msg}132 else: # Paused, failed, pending etc.133 # Construct detailed message including full explanation134 msg = f"**Endpoint Issue:** The analysis endpoint ('{endpoint_name}') status is currently <span style='color:red'>**{status}**</span>.\n\n{error_503_user_message}"135 return {"status": "error", "ui_message": msg}136 137 except Exception as e:138 error_msg = f"Error checking analysis endpoint status for {endpoint_name}: {e}"139 logging.error(error_msg)140 # Let analysis stop if endpoint check fails critically141 return {"status": "error", "ui_message": f"Error checking endpoint status: {e}"}142 143 144def fetch_and_validate_code(space_id: str):145 """Fetches and validates code files for the space."""146 logging.info(f"Fetching code files for {space_id}...")147 code_files = get_space_code_files(space_id)148 if not code_files:149 error_msg = f"Could not retrieve code files for '{space_id}'. Check ID and ensure it's a public Space."150 logging.warning(error_msg)151 return {152 "status": "error",153 "ui_message": f"**Error:**\n{error_msg}\nAnalysis Canceled.",154 }155 logging.info(f"Successfully fetched {len(code_files)} files for {space_id}.")156 return {"status": "success", "code_files": code_files}157 158 159def generate_detailed_report(160 space_id: str, code_files: dict, error_503_user_message: str161):162 """Generates the detailed privacy report using the LLM."""163 logging.info("Generating detailed privacy analysis report...")164 privacy_prompt_messages, privacy_truncated = format_privacy_prompt(165 space_id, code_files166 )167 168 privacy_api_response = query_qwen_endpoint(privacy_prompt_messages, max_tokens=3072)169 170 if privacy_api_response == ERROR_503_DICT:171 logging.warning("LLM Call 1 (Privacy) failed with 503.")172 return {"status": "error", "ui_message": error_503_user_message}173 174 detailed_privacy_report = parse_qwen_response(privacy_api_response)175 176 if "Error:" in detailed_privacy_report:177 error_msg = (178 f"Failed to generate detailed privacy report: {detailed_privacy_report}"179 )180 logging.error(error_msg)181 return {182 "status": "error",183 "ui_message": f"**Error Generating Detailed Privacy Report:**\n{detailed_privacy_report}\nAnalysis Halted.",184 }185 186 if privacy_truncated:187 detailed_privacy_report = TRUNCATION_WARNING + detailed_privacy_report188 189 logging.info("Successfully generated detailed privacy report.")190 return {191 "status": "success",192 "report": detailed_privacy_report,193 "truncated": privacy_truncated,194 }195 196 197def generate_summary_report(198 space_id: str,199 code_files: dict,200 detailed_privacy_report: str,201 error_503_user_message: str,202):203 """Generates the summary & highlights report using the LLM."""204 logging.info("Generating summary and highlights report...")205 # Remove potential truncation warning from detailed report before sending to next LLM206 clean_detailed_report = detailed_privacy_report.replace(TRUNCATION_WARNING, "")207 208 summary_highlights_prompt_messages, summary_truncated = (209 format_summary_highlights_prompt(space_id, code_files, clean_detailed_report)210 )211 212 summary_highlights_api_response = query_qwen_endpoint(213 summary_highlights_prompt_messages, max_tokens=2048214 )215 216 if summary_highlights_api_response == ERROR_503_DICT:217 logging.warning("LLM Call 2 (Summary) failed with 503.")218 # Return specific status to indicate partial success219 return {"status": "error_503_summary", "ui_message": error_503_user_message}220 221 summary_highlights_report = parse_qwen_response(summary_highlights_api_response)222 223 if "Error:" in summary_highlights_report:224 error_msg = (225 f"Failed to generate summary/highlights report: {summary_highlights_report}"226 )227 logging.error(error_msg)228 # Return specific status to indicate partial success229 return {230 "status": "error_summary",231 "ui_message": f"**Error Generating Summary/Highlights:**\n{summary_highlights_report}",232 }233 234 if summary_truncated:235 summary_highlights_report = TRUNCATION_WARNING + summary_highlights_report236 237 logging.info("Successfully generated summary & highlights report.")238 return {239 "status": "success",240 "report": summary_highlights_report,241 "truncated": summary_truncated,242 }243 244 245def upload_results(246 space_id: str,247 summary_report: str,248 detailed_report: str,249 dataset_id: str,250 hf_token: str | None,251 tldr_json_data: dict | None = None,252):253 """Uploads the generated reports (Markdown and optional JSON TLDR) to the specified dataset repository."""254 if not hf_token:255 logging.warning("HF Token not provided, skipping dataset report upload.")256 return {"status": "skipped", "reason": "HF_TOKEN not set"}257 if "Error:" in detailed_report or "Error:" in summary_report:258 msg = "Skipping cache upload due to errors in generated reports."259 logging.warning(msg)260 return {"status": "skipped", "reason": msg}261 262 safe_space_id = space_id.replace("..", "")263 264 try:265 with tempfile.TemporaryDirectory() as tmpdir:266 # Define local paths267 summary_path_local = os.path.join(tmpdir, SUMMARY_FILENAME)268 privacy_path_local = os.path.join(tmpdir, PRIVACY_FILENAME)269 tldr_json_path_local = os.path.join(tmpdir, TLDR_FILENAME)270 271 # Write Markdown reports272 with open(summary_path_local, "w", encoding="utf-8") as f:273 f.write(summary_report)274 with open(privacy_path_local, "w", encoding="utf-8") as f:275 f.write(detailed_report)276 277 # Prepare commit message278 commit_message = f"Add analysis reports for Space: {safe_space_id}"279 if tldr_json_data:280 commit_message += " (including TLDR JSON)"281 print(f"Successfully wrote TLDR JSON locally for {safe_space_id}.")282 # Write JSON TLDR data if available283 try:284 with open(tldr_json_path_local, "w", encoding="utf-8") as f:285 json.dump(tldr_json_data, f, indent=2, ensure_ascii=False)286 logging.info(287 f"Successfully wrote TLDR JSON locally for {safe_space_id}."288 )289 except Exception as json_err:290 logging.error(291 f"Failed to write TLDR JSON locally for {safe_space_id}: {json_err}"292 )293 tldr_json_data = None # Prevent upload attempt if writing failed294 295 # Ensure repo exists296 api = HfApi(token=hf_token)297 repo_url = api.create_repo(298 repo_id=dataset_id,299 repo_type="dataset",300 exist_ok=True,301 )302 logging.info(f"Ensured dataset repo {repo_url} exists.")303 304 # Upload summary report305 api.upload_file(306 path_or_fileobj=summary_path_local,307 path_in_repo=f"{safe_space_id}/{SUMMARY_FILENAME}",308 repo_id=dataset_id,309 repo_type="dataset",310 commit_message=commit_message,311 )312 logging.info(f"Successfully uploaded summary report for {safe_space_id}.")313 314 # Upload privacy report315 api.upload_file(316 path_or_fileobj=privacy_path_local,317 path_in_repo=f"{safe_space_id}/{PRIVACY_FILENAME}",318 repo_id=dataset_id,319 repo_type="dataset",320 commit_message=commit_message,321 )322 logging.info(323 f"Successfully uploaded detailed privacy report for {safe_space_id}."324 )325 # print(f"Successfully uploaded detailed privacy report for {safe_space_id}.") # Keep if needed for debug326 327 # Upload JSON TLDR if it was successfully written locally328 if tldr_json_data and os.path.exists(tldr_json_path_local):329 api.upload_file(330 path_or_fileobj=tldr_json_path_local,331 path_in_repo=f"{safe_space_id}/{TLDR_FILENAME}",332 repo_id=dataset_id,333 repo_type="dataset",334 commit_message=commit_message, # Can reuse commit message or make specific335 )336 logging.info(f"Successfully uploaded TLDR JSON for {safe_space_id}.")337 print(f"Successfully uploaded TLDR JSON for {safe_space_id}.")338 339 # Return success if all uploads finished without error340 return {"status": "success"}341 342 except Exception as e:343 error_msg = f"Non-critical error during report upload for {safe_space_id}: {e}"344 logging.error(error_msg)345 print(error_msg)346 return {"status": "error", "message": error_msg}347 348 349# --- New TLDR Generation Functions ---350 351 352def format_tldr_prompt(353 detailed_report: str, summary_report: str354) -> list[dict[str, str]]:355 """Formats the prompt for the TLDR generation task."""356 # Clean potential cache/truncation markers from input reports for the LLM357 cleaned_detailed = detailed_report.replace(CACHE_INFO_MSG, "").replace(358 TRUNCATION_WARNING, ""359 )360 cleaned_summary = summary_report.replace(CACHE_INFO_MSG, "").replace(361 TRUNCATION_WARNING, ""362 )363 364 user_content = (365 "Please generate a structured JSON TLDR based on the following reports:\n\n"366 "--- DETAILED PRIVACY ANALYSIS REPORT START ---\n"367 f"{cleaned_detailed}\n"368 "--- DETAILED PRIVACY ANALYSIS REPORT END ---\n\n"369 "--- SUMMARY & HIGHLIGHTS REPORT START ---\n"370 f"{cleaned_summary}\n"371 "--- SUMMARY & HIGHLIGHTS REPORT END ---"372 )373 374 # Note: We are not handling truncation here, assuming the input reports375 # are already reasonably sized from the previous steps.376 # If reports could be extremely long, add truncation logic similar to other format_* functions.377 378 messages = [379 {"role": "system", "content": TLDR_SYSTEM_PROMPT},380 {"role": "user", "content": user_content},381 ]382 return messages383 384 385def parse_tldr_json_response(386 response: ChatCompletionOutput | dict | None,387) -> dict | None:388 """Parses the LLM response, expecting JSON content for the TLDR."""389 if response is None:390 logging.error("TLDR Generation: Failed to get response from LLM.")391 return None392 393 # Check for 503 error dict first394 if isinstance(response, dict) and response.get("error_type") == "503":395 logging.error(f"TLDR Generation: Received 503 error: {response.get('message')}")396 return None # Treat 503 as failure for this specific task397 398 # --- Direct Content Extraction (Replaces call to parse_qwen_response) ---399 raw_content = ""400 try:401 # Check if it's likely the expected ChatCompletionOutput structure402 if not hasattr(response, "choices"):403 logging.error(404 f"TLDR Generation: Unexpected response type received: {type(response)}. Content: {response}"405 )406 return None # Return None if not the expected structure407 408 # Access the generated content according to the ChatCompletionOutput structure409 if response.choices and len(response.choices) > 0:410 content = response.choices[0].message.content411 if content:412 raw_content = content.strip()413 logging.info(414 "TLDR Generation: Successfully extracted raw content from response."415 )416 else:417 logging.warning(418 "TLDR Generation: Response received, but content is empty."419 )420 return None421 else:422 logging.warning("TLDR Generation: Response received, but no choices found.")423 return None424 except AttributeError as e:425 # This might catch cases where response looks like the object but lacks expected attributes426 logging.error(427 f"TLDR Generation: Attribute error parsing response object: {e}. Response structure might be unexpected. Response: {response}"428 )429 return None430 except Exception as e:431 logging.error(432 f"TLDR Generation: Unexpected error extracting content from response object: {e}"433 )434 return None435 # --- End Direct Content Extraction ---436 437 # --- JSON Parsing Logic ---438 if not raw_content: # Should be caught by checks above, but belts and suspenders439 logging.error("TLDR Generation: Raw content is empty after extraction attempt.")440 return None441 442 try:443 # Clean potential markdown code block formatting444 if raw_content.strip().startswith("```json"):445 raw_content = raw_content.strip()[7:-3].strip()446 elif raw_content.strip().startswith("```"):447 raw_content = raw_content.strip()[3:-3].strip()448 449 tldr_data = json.loads(raw_content)450 451 # Validate structure: Check if it's a dict and has all required keys452 required_keys = [453 "app_description",454 "privacy_tldr",455 "data_types",456 "user_input_data",457 "local_processing",458 "remote_processing",459 "external_logging",460 ]461 if not isinstance(tldr_data, dict):462 logging.error(463 f"TLDR Generation: Parsed content is not a dictionary. Content: {raw_content[:500]}..."464 )465 return None466 if not all(key in tldr_data for key in required_keys):467 missing_keys = [key for key in required_keys if key not in tldr_data]468 logging.error(469 f"TLDR Generation: Parsed JSON is missing required keys: {missing_keys}. Content: {raw_content[:500]}..."470 )471 return None472 473 # --- Add validation for the new data_types structure ---474 data_types_list = tldr_data.get("data_types")475 if not isinstance(data_types_list, list):476 logging.error(477 f"TLDR Generation: 'data_types' is not a list. Content: {data_types_list}"478 )479 return None480 for item in data_types_list:481 if (482 not isinstance(item, dict)483 or "name" not in item484 or "description" not in item485 ):486 logging.error(487 f"TLDR Generation: Invalid item found in 'data_types' list: {item}. Must be dict with 'name' and 'description'."488 )489 return None490 if not isinstance(item["name"], str) or not isinstance(491 item["description"], str492 ):493 logging.error(494 f"TLDR Generation: Invalid types for name/description in 'data_types' item: {item}. Must be strings."495 )496 return None497 # --- End validation for data_types ---498 499 # Basic validation for other lists (should contain strings)500 validation_passed = True501 for key in [502 "user_input_data",503 "local_processing",504 "remote_processing",505 "external_logging",506 ]:507 data_list = tldr_data.get(key)508 # Add more detailed check and logging509 if not isinstance(data_list, list):510 logging.error(511 f"TLDR Generation Validation Error: Key '{key}' is not a list. Found type: {type(data_list)}, Value: {data_list}"512 )513 validation_passed = False514 # Allow continuing validation for other keys, but mark as failed515 elif not all(isinstance(x, str) for x in data_list):516 # This check might be too strict if LLM includes non-strings, but keep for now517 logging.warning(518 f"TLDR Generation Validation Warning: Not all items in list '{key}' are strings. Content: {data_list}"519 )520 # Decide if this should cause failure - currently it doesn't, just warns521 522 if not validation_passed:523 logging.error(524 "TLDR Generation: Validation failed due to incorrect list types."525 )526 return None # Ensure failure if any key wasn't a list527 528 logging.info("Successfully parsed and validated TLDR JSON response.")529 return tldr_data530 531 except json.JSONDecodeError as e:532 logging.error(533 f"TLDR Generation: Failed to decode JSON response: {e}. Content: {raw_content[:500]}..."534 )535 return None536 except Exception as e:537 logging.error(f"TLDR Generation: Unexpected error parsing JSON response: {e}")538 return None539 540 541def render_tldr_markdown(tldr_data: dict | None, space_id: str | None = None) -> str:542 """Renders the top-level TLDR (description, privacy) data into a Markdown string.543 544 (Does not include the data lists)545 """546 if not tldr_data:547 # Return a more specific message for this part548 return "*TLDR Summary could not be generated.*\n"549 550 output = []551 552 # Add Space link if space_id is provided553 if space_id:554 output.append(555 f"**Source Space:** [`{space_id}`](https://huggingface.co/spaces/{space_id})\n"556 )557 558 output.append(f"**App Description:** {tldr_data.get('app_description', 'N/A')}\n")559 privacy_summary = tldr_data.get("privacy_tldr", "N/A")560 output.append(f"**Privacy TLDR:** {privacy_summary}") # Removed extra newline561 562 # Removed data list rendering from this function563 564 return "\n".join(output)565 566 567def render_data_details_markdown(tldr_data: dict | None) -> str:568 """Renders the data lists (types, input, processing, logging) from TLDR data."""569 if not tldr_data:570 return "*Data details could not be generated.*\n"571 572 output = []573 # Get defined names for formatting574 defined_names = sorted(575 [576 dt.get("name", "")577 for dt in tldr_data.get("data_types", [])578 if dt.get("name")579 ],580 key=len,581 reverse=True,582 )583 584 output.append("**Data Types Defined:**") # Renamed slightly for clarity585 data_types = tldr_data.get("data_types")586 if data_types and isinstance(data_types, list):587 if not data_types:588 output.append("- None identified.")589 else:590 for item in data_types:591 name = item.get("name", "Unnamed")592 desc = item.get("description", "No description")593 output.append(f"- `{name}`: {desc}")594 else:595 output.append("- (Error loading data types)")596 output.append("") # Add newline for spacing597 598 # Reusable helper for rendering lists599 def render_list(title, key):600 output.append(f"**{title}:**")601 data_list = tldr_data.get(key)602 if isinstance(data_list, list):603 if not data_list:604 output.append("- None identified.")605 else:606 for item_str in data_list:607 formatted_item = item_str # Default608 found_match = False609 for name in defined_names:610 if item_str == name:611 formatted_item = f"`{name}`"612 found_match = True613 break614 elif item_str.startswith(name + " "):615 formatted_item = f"`{name}`{item_str[len(name):]}"616 found_match = True617 break618 if (619 not found_match620 and " " not in item_str621 and not item_str.startswith("`")622 ):623 formatted_item = f"`{item_str}`"624 output.append(f"- {formatted_item}")625 else:626 output.append("- (Error loading list)")627 output.append("")628 629 render_list("Data Sent by User to App", "user_input_data")630 render_list("Data Processed Locally within App", "local_processing")631 render_list("Data Processed Remotely", "remote_processing")632 render_list("Data Logged/Saved Externally", "external_logging")633 634 # Remove the last empty line635 if output and output[-1] == "":636 output.pop()637 638 return "\n".join(output)639 640 641# --- Combined TLDR Generation Function ---642 643 644def generate_and_parse_tldr(detailed_report: str, summary_report: str) -> dict | None:645 """Formats prompt, queries LLM, and parses JSON response for TLDR.646 647 Args:648 detailed_report: The detailed privacy report content.649 summary_report: The summary & highlights report content.650 651 Returns:652 A dictionary with the parsed TLDR data, or None if any step fails.653 """654 logging.info("Starting TLDR generation and parsing...")655 try:656 # Format657 tldr_prompt_messages = format_tldr_prompt(detailed_report, summary_report)658 if not tldr_prompt_messages:659 logging.error("TLDR Generation: Failed to format prompt.")660 return None661 662 # Query (using existing import within analysis_utils)663 # Use slightly smaller max_tokens664 llm_response = query_qwen_endpoint(tldr_prompt_messages, max_tokens=1024)665 if llm_response is None: # Check if query itself failed critically666 logging.error("TLDR Generation: LLM query returned None.")667 return None668 # 503 handled within parse function below669 670 # Parse671 parsed_data = parse_tldr_json_response(llm_response)672 if parsed_data:673 logging.info("Successfully generated and parsed TLDR.")674 return parsed_data675 else:676 logging.error("TLDR Generation: Failed to parse JSON response.")677 return None678 679 except Exception as e:680 logging.error(681 f"TLDR Generation: Unexpected error in generate_and_parse_tldr: {e}",682 exc_info=True,683 )684 return None685 