Spaces:
Running
Running
| """Shared parsing utilities for extracting structured JSON from LLM output. | |
| Both ``single_agent.py`` and ``multi_agent.py`` need to parse free-form | |
| LLM text into Pydantic models (``ResponseFormatter``, ``PlannerResponse``). | |
| This module centralises the common helpers so they stay in sync. | |
| """ | |
| import re | |
| from chemgraph.schemas.agent_response import ResponseFormatter | |
| from chemgraph.utils.logging_config import setup_logger | |
| logger = setup_logger(__name__) | |
| def extract_json_block(text: str) -> str | None: | |
| """Try to extract a JSON object from *text*. | |
| Handles markdown-fenced blocks (```json ... ```) and bare JSON objects. | |
| Returns the extracted string or *None* if nothing looks like JSON. | |
| Parameters | |
| ---------- | |
| text : str | |
| Text that may contain a JSON object. | |
| Returns | |
| ------- | |
| str or None | |
| Extracted JSON text, or ``None`` when no object is found. | |
| """ | |
| # Try markdown-fenced JSON first | |
| m = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.DOTALL) | |
| if m: | |
| return m.group(1) | |
| # Try bare top-level JSON object | |
| m = re.search(r"(\{.*\})", text, re.DOTALL) | |
| if m: | |
| return m.group(1) | |
| return None | |
| def parse_response_formatter( | |
| raw_text: str, | |
| ) -> tuple[ResponseFormatter, str | None]: | |
| """Parse LLM output into a :class:`ResponseFormatter`. | |
| Attempts direct validation first, then tries to extract a JSON block | |
| from the text. Falls back to an empty ``ResponseFormatter`` (all | |
| fields ``None``) so the pipeline never breaks -- the raw text is | |
| still available in the agent's message history. | |
| Parameters | |
| ---------- | |
| raw_text : str | |
| Raw LLM output to parse. | |
| Returns | |
| ------- | |
| tuple[ResponseFormatter, str | None] | |
| A tuple of ``(parsed_formatter, parse_error)``. ``parse_error`` | |
| is ``None`` on success, or a descriptive string when parsing | |
| failed and the empty fallback was used. | |
| """ | |
| # 1. Direct validation | |
| try: | |
| return ResponseFormatter.model_validate_json(raw_text.strip()), None | |
| except Exception: | |
| pass | |
| # 2. Extract JSON block and retry | |
| extracted = extract_json_block(raw_text) | |
| if extracted: | |
| try: | |
| return ResponseFormatter.model_validate_json(extracted), None | |
| except Exception: | |
| pass | |
| # 3. Fallback: return empty ResponseFormatter (all fields None). | |
| error_msg = ( | |
| "ResponseAgent: could not parse structured output; " | |
| "returning empty ResponseFormatter." | |
| ) | |
| logger.warning(error_msg) | |
| return ResponseFormatter(), error_msg | |