rockyaaos's picture
ChemGraph Loop: guarded real-agent API (EMT/TBLite single-point energy)
c509967 verified
Raw
History Blame Contribute Delete
2.61 kB
"""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