CoolFace
Apppublic

yjernite/space-privacy

sourceHugging Facemitupdated 1y agoView on Hugging Face
8likes
analysis_utils.py685 linesDownload Raw Back to root
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