Coverage for graphqler / compiler / resolvers / llm / llm_mutation_object_resolver.py: 28%
46 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
« prev ^ index » next coverage.py v7.13.4, created at 2026-03-20 10:09 -0400
1"""LLM-backed mutation resolver.
3Resolves mutationType + hardDependsOn + softDependsOn for every mutation by
4asking an LLM for its interpretation of the schema, then merging that result
5with the classic ID-based resolver output.
7The `.comparison` attribute (set after `resolve()`) holds the side-by-side diff
8between the LLM result and the classic result for systematic analysis.
9"""
11import json
12import copy
13import logging
15from graphqler import config
16from graphqler.compiler.resolvers.mutation_object_resolver import MutationObjectResolver
17from .llm_resolver import LLMResolver
18from .prompt_templates import MUTATION_SYSTEM_PROMPT, MUTATION_USER_PROMPT_TEMPLATE
20logger = logging.getLogger(__name__)
23class LLMMutationObjectResolver(LLMResolver):
24 """Resolves mutations using an LLM, with optional fallback to the classic resolver."""
26 def __init__(self):
27 super().__init__()
28 self.comparison: dict = {} # populated by resolve(); keyed by mutation name
30 def resolve(self, objects: dict, mutations: dict, input_objects: dict) -> dict:
31 """Resolve mutations via LLM, falling back to classic resolver on failure.
33 Args:
34 objects (dict): Compiled objects.
35 mutations (dict): Raw parsed mutations.
36 input_objects (dict): Raw parsed input objects.
38 Returns:
39 dict: Mutations enriched with mutationType, hardDependsOn, softDependsOn.
40 """
41 # Always run classic resolver — used as fallback and for comparison
42 classic_mutations = MutationObjectResolver().resolve(objects, copy.deepcopy(mutations), input_objects)
44 try:
45 llm_raw = self._call_llm_for_mutations(objects, mutations)
46 llm_validated = self.validate_llm_mutation_result(llm_raw, list(mutations.keys()), objects)
47 merged = self.merge_with_classic(llm_validated, classic_mutations, list(mutations.keys()))
48 self.comparison = self._build_comparison(classic_mutations, merged, mutations.keys())
49 logger.info(f"LLM mutation resolver: resolved {len(llm_validated)}/{len(mutations)} mutations")
50 return merged
51 except Exception as exc:
52 if config.LLM_RESOLVER_FALLBACK_TO_ID:
53 logger.warning(f"LLM mutation resolver failed ({exc}), falling back to classic resolver")
54 self.comparison = {}
55 return classic_mutations
56 raise
58 def _call_llm_for_mutations(self, objects: dict, mutations: dict) -> dict:
59 """Build the prompt and call the LLM.
61 Args:
62 objects (dict): Compiled objects (for schema context).
63 mutations (dict): Raw parsed mutations.
65 Returns:
66 dict: Raw parsed JSON response from the LLM.
67 """
68 schema_context = self.build_schema_context(objects)
69 simplified = self.simplify_endpoints(mutations)
70 mutations_json = json.dumps(simplified, indent=2)
72 user_prompt = MUTATION_USER_PROMPT_TEMPLATE.format(
73 schema_context=schema_context,
74 mutations_json=mutations_json,
75 )
76 return self.call_llm(MUTATION_SYSTEM_PROMPT, user_prompt)
78 def _build_comparison(self, classic: dict, llm_merged: dict, mutation_names) -> dict:
79 """Build a per-mutation comparison dict.
81 Args:
82 classic (dict): Classic resolver output.
83 llm_merged (dict): LLM resolver output (after merge).
84 mutation_names: Iterable of mutation names.
86 Returns:
87 dict: Keyed by mutation name; each entry has 'classic', 'llm', 'differs', 'diff'.
88 """
89 comparison = {}
90 for name in mutation_names:
91 c = classic.get(name, {})
92 llm_entry = llm_merged.get(name, {})
94 c_summary = {
95 "mutationType": c.get("mutationType", "UNKNOWN"),
96 "hardDependsOn": c.get("hardDependsOn", {}),
97 "softDependsOn": c.get("softDependsOn", {}),
98 }
99 l_summary = {
100 "mutationType": llm_entry.get("mutationType", "UNKNOWN"),
101 "hardDependsOn": llm_entry.get("hardDependsOn", {}),
102 "softDependsOn": llm_entry.get("softDependsOn", {}),
103 }
105 diff = {}
106 for key in ("mutationType", "hardDependsOn", "softDependsOn"):
107 if c_summary[key] != l_summary[key]:
108 diff[key] = {"classic": c_summary[key], "llm": l_summary[key]}
110 comparison[name] = {
111 "classic": c_summary,
112 "llm": l_summary,
113 "differs": bool(diff),
114 "diff": diff,
115 }
116 return comparison