"""Shared utilities for LLM API interaction and message extraction. Consolidates the duplicated _extract_text / _extract_reply_text pattern (previously repeated in 5 places) and _extract_content_from_api_response (previously duplicated in 2 places). """ from typing import Any def extract_reply_text(reply: Any) -> str: """Extract plain text from a tutu-api reply object. Handles: None | str | dict with msgBody.content or content key. """ if reply is None: return "" if isinstance(reply, str): return reply if isinstance(reply, dict): body = reply.get("msgBody") or reply.get("content", "") if isinstance(body, dict): return body.get("content", "") return str(body) return str(reply) def extract_content_from_llm_response(data: dict) -> str: """Extract text content from an LLM API response dict. Handles: - OpenAI format: choices[0].message.content as a plain string - Anthropic-compatible format: choices[0].message.content as a list of content blocks {type: "text", text: "..."} (non-text blocks are skipped) """ try: content = data["choices"][0]["message"]["content"] except (KeyError, IndexError, TypeError): return "" if isinstance(content, str): return content if isinstance(content, list): parts = [] for block in content: if not isinstance(block, dict): continue if block.get("type") == "text": parts.append(block.get("text") or block.get("content") or "") return "\n".join(parts) return str(content) def parse_json_from_llm_text(content: str) -> Any: """Parse JSON from LLM output, falling back to bracket-delimited substring. Returns parsed JSON object, or raises json.JSONDecodeError if unparseable. """ import json try: return json.loads(content) except json.JSONDecodeError: pass # Fallback: find the outermost JSON object or array for open_char, close_char in [("{", "}"), ("[", "]")]: start = content.find(open_char) end = content.rfind(close_char) if start != -1 and end != -1 and end > start: return json.loads(content[start : end + 1]) raise ValueError(f"No JSON found in LLM output: {content[:200]}")