import json import re from typing import Any def safe_invoke(llm, prompt: str) -> str: """ Safely invoke any LangChain LLM and always return a string. Handles: - AIMessage (ChatOpenAI, etc.) - raw string outputs - dict / list responses """ if not prompt or not isinstance(prompt, str): raise ValueError("Prompt must be a non-empty string") try: response = llm.invoke(prompt) # Case 1: Chat models (AIMessage) if hasattr(response, "content"): return str(response.content) # Case 2: Already string if isinstance(response, str): return response # Case 3: dict / list → stringify safely return json.dumps(response, indent=2) except Exception as e: return f"[LLM_ERROR] {str(e)}" # --------------------------------------------------- def safe_json_parse(text: str) -> dict: """ Try to parse LLM output into JSON safely. If fails, return fallback structure. """ if not isinstance(text, str): return { "error": "Invalid JSON from model", "raw_output": text, } candidates = [] stripped = text.strip() candidates.append(stripped) fence_match = re.search(r"```(?:json)?\s*(.*?)\s*```", stripped, re.DOTALL | re.IGNORECASE) if fence_match: candidates.append(fence_match.group(1).strip()) first_object = stripped.find("{") last_object = stripped.rfind("}") if first_object != -1 and last_object != -1 and last_object > first_object: candidates.append(stripped[first_object:last_object + 1].strip()) first_array = stripped.find("[") last_array = stripped.rfind("]") if first_array != -1 and last_array != -1 and last_array > first_array: candidates.append(stripped[first_array:last_array + 1].strip()) seen = set() for candidate in candidates: if not candidate or candidate in seen: continue seen.add(candidate) try: return json.loads(candidate) except Exception: continue return { "error": "Invalid JSON from model", "raw_output": text } # --------------------------------------------------- def safe_merge(*args: Any) -> str: """ Safely merge multiple inputs into one string. Handles: - None - dict - list - string """ merged_parts = [] for arg in args: if arg is None: continue if isinstance(arg, (dict, list)): merged_parts.append(json.dumps(arg, indent=2)) else: merged_parts.append(str(arg)) return "\n".join(merged_parts) # --------------------------------------------------- def safe_input(text: Any, fallback: str) -> str: """ Normalize optional user inputs (chat / feelings). """ if text is None: return fallback if isinstance(text, str) and text.strip() == "": return fallback return str(text)