| 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) |
|
|
| |
| if hasattr(response, "content"): |
| return str(response.content) |
|
|
| |
| if isinstance(response, str): |
| return response |
|
|
| |
| 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) |
|
|
|
|
|
|
|
|