OutputFixer (fixer.py)
On this page
Overview
OutputFixer repairs malformed LLM output through a hierarchy of four strategies, attempted in order, stopping at the first one that produces parseable text. Depends on schema.py (FixStrategy, OutputFormat) and the standard library (json, re).
Module-level repair helpers
These are stateless functions used internally by OutputFixer (not classes, no self):
OutputFixer
Constructor
OutputFixer(
required_fields: Optional[List[str]] = None,
field_defaults: Optional[Dict[str, Any]] = None,
llm_fn: Optional[Callable[[str], str]] = None,
)
_JSON_REPAIR_PIPELINE (class attribute) — an ordered list applied in sequence during REGEX_REPAIR: [_strip_markdown_fences, _fix_trailing_commas, _fix_json_quotes, _fix_unquoted_keys].
Public Methods
fix
def fix(self, text: str, expected_format: OutputFormat = OutputFormat.JSON) -> Tuple[str, FixStrategy]
Attempts to repair text. Returns (repaired_text, strategy_used) — the caller is expected to re-parse repaired_text; fix() itself never parses into structured data, only returns cleaned-up text.
Internal workflow (stops at the first strategy that reports success, i.e. returns something other than FixStrategy.NONE):
- Empty/whitespace input → immediately returns
(text, FixStrategy.NONE). _try_regex_repair(text, expected_format)._try_field_injection(text)._try_fallback_parse(text)._try_llm_reformat(text, expected_format)— only attempted ifself.llm_fnis set.- If nothing worked, logs a warning and returns
(text, FixStrategy.NONE)— the original, unmodified text.
Raises: Does not raise on its own — all internal strategy attempts are wrapped so a failed strategy just falls through to the next one rather than propagating an exception. (Note: _try_llm_reformat catches exceptions from self.llm_fn itself via a broad except Exception.)
Side effects: logger.info at the start; logger.info on each strategy success; logger.warning if all strategies exhaust; if LLM_REFORMAT is attempted, makes a network/API call via self.llm_fn.
fixer = OutputFixer(required_fields=["answer", "sources"])
fixed_text, strategy = fixer.fix("{answer: 'Paris', sources: ['wiki'],}", expected_format=OutputFormat.JSON)
# fixed_text == '{"answer": "Paris", "sources": ["wiki"]}'
# strategy == FixStrategy.REGEX_REPAIR
fix_dict
def fix_dict(self, data: Dict[str, Any]) -> Tuple[Dict[str, Any], FixStrategy]
Operates on an already-parsed dict rather than raw text. If self.required_fields is empty, returns (data, FixStrategy.NONE) unchanged. Otherwise finds fields that are missing or explicitly None, injects self.field_defaults.get(f, None) for each, and returns (fixed_dict, FixStrategy.FIELD_INJECTION) — or (data, FixStrategy.NONE) if nothing was missing.
fixer = OutputFixer(required_fields=["answer", "confidence"], field_defaults={"confidence": 0.5})
fixed, strategy = fixer.fix_dict({"answer": "Paris"})
# fixed == {"answer": "Paris", "confidence": 0.5}
Private strategy methods
_try_regex_repair
For expected_format == OutputFormat.JSON: runs the full _JSON_REPAIR_PIPELINE in sequence, then tries json.loads(). If that fails, tries _extract_json_block() on the pipeline output. If that fails too, tries _truncate_to_valid_json(). Returns the first result that's actually valid JSON, tagged REGEX_REPAIR.
For any other expected_format: only strips markdown fences (no JSON-specific repairs applied) — returns REGEX_REPAIR only if stripping actually changed something and the result is non-empty.
_try_field_injection
Only meaningful if required_fields is non-empty. Tries json.loads(_extract_json_block(text) or text); if that yields a dict, delegates to fix_dict() and, if it made changes, re-serializes with json.dumps(fixed, ensure_ascii=False).
_try_fallback_parse
Calls _parse_key_value_fallback(text). If it found anything, injects defaults for any required_fields still missing, then json.dumps()s the result, tagged FALLBACK_PARSE.
_try_llm_reformat
Builds a reformat prompt embedding: the required-fields hint (if any), a human-readable format name ("valid JSON object" / "valid YAML" / "CSV with header row" / "structured text" fallback via a hardcoded dict keyed by OutputFormat), and the full malformed text wrapped in --- BEGIN MALFORMED OUTPUT --- ... --- END --- markers. Calls self.llm_fn(prompt); if the response is non-empty after stripping, returns it tagged LLM_REFORMAT. Catches any exception from self.llm_fn and falls through to (text, FixStrategy.NONE).
build_answer_fixer
def build_answer_fixer(llm_fn: Optional[LLMCallable] = None) -> OutputFixer
Pre-configured for AnswerSchema: required_fields=["answer"], field_defaults={"answer": "", "sources": [], "confidence": 0.5}.
Complexity
_try_regex_repair's _truncate_to_valid_json fallback is the worst-case cost center: up to O(n) json.loads() calls on shrinking prefixes, each of which is itself O(k) in the prefix length — O(n²) worst case for pathological near-valid JSON. All other strategies are O(n) single-pass regex operations. Space is O(n) for text copies made at each pipeline stage.
Thread safety
Safe to share a single OutputFixer instance across threads for concurrent fix()/fix_dict() calls — no mutable state beyond the read-only required_fields/field_defaults/llm_fn set at construction. If llm_fn itself is not thread-safe (e.g. a non-thread-safe HTTP client), that constraint is inherited by concurrent LLM_REFORMAT calls.
Side effects
- Regex strategies: none beyond logging.
LLM_REFORMAT: makes an external call viallm_fn— network/API cost, latency, and any side effects yourllm_fnimplementation has.
Best practices
- Order matters: only pass an
llm_fnif you're comfortable with the cost/latency of a reformat call as a last resort — it's tried only after all free, local strategies fail. - If your expected format is consistently JSON, set
required_fieldseven if you're not usingOutputFixerstandalone — the orchestrator's_build_fixer()derives this automatically fromOutputParser(fields=...).
Common mistakes
- Assuming
_fix_json_quotesis safe on all text — it globally replaces every unescaped single quote, including ones inside legitimate string content (contractions like "it's", possessives, etc.), which can introduce new JSON errors rather than fixing them. This is a known sharp edge of the regex-repair strategy, not a bug to "fix" in your own code — just be aware it can behave unexpectedly on prose-heavy JSON string values. - Expecting
fix()to return parsed data — it returns text. You must re-parse the returned string yourself (this is exactly whatOutputParser._attempt_fix_and_parsedoes internally).
Related APIs
FixStrategy,OutputFormatOutputParser— primary caller, via_attempt_fix_and_parse.
Version History
Not determinable from source.