Coverage for graphqler / compiler / resolvers / llm / llm_query_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 query resolver.
3Resolves hardDependsOn + softDependsOn for every query by asking an LLM for its
4interpretation of the schema, then merging that result with the classic ID-based
5resolver 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.query_object_resolver import QueryObjectResolver
17from .llm_resolver import LLMResolver
18from .prompt_templates import QUERY_SYSTEM_PROMPT, QUERY_USER_PROMPT_TEMPLATE
20logger = logging.getLogger(__name__)
23class LLMQueryObjectResolver(LLMResolver):
24 """Resolves queries 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 query name
30 def resolve(self, objects: dict, queries: dict, input_objects: dict) -> dict:
31 """Resolve queries via LLM, falling back to classic resolver on failure.
33 Args:
34 objects (dict): Compiled objects.
35 queries (dict): Raw parsed queries.
36 input_objects (dict): Raw parsed input objects.
38 Returns:
39 dict: Queries enriched with hardDependsOn, softDependsOn.
40 """
41 # Always run classic resolver — used as fallback and for comparison
42 classic_queries = QueryObjectResolver().resolve(objects, copy.deepcopy(queries), input_objects)
44 try:
45 llm_raw = self._call_llm_for_queries(objects, queries)
46 llm_validated = self.validate_llm_query_result(llm_raw, list(queries.keys()), objects)
47 merged = self.merge_with_classic(llm_validated, classic_queries, list(queries.keys()))
48 self.comparison = self._build_comparison(classic_queries, merged, queries.keys())
49 logger.info(f"LLM query resolver: resolved {len(llm_validated)}/{len(queries)} queries")
50 return merged
51 except Exception as exc:
52 if config.LLM_RESOLVER_FALLBACK_TO_ID:
53 logger.warning(f"LLM query resolver failed ({exc}), falling back to classic resolver")
54 self.comparison = {}
55 return classic_queries
56 raise
58 def _call_llm_for_queries(self, objects: dict, queries: dict) -> dict:
59 """Build the prompt and call the LLM.
61 Args:
62 objects (dict): Compiled objects (for schema context).
63 queries (dict): Raw parsed queries.
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(queries)
70 queries_json = json.dumps(simplified, indent=2)
72 user_prompt = QUERY_USER_PROMPT_TEMPLATE.format(
73 schema_context=schema_context,
74 queries_json=queries_json,
75 )
76 return self.call_llm(QUERY_SYSTEM_PROMPT, user_prompt)
78 def _build_comparison(self, classic: dict, llm_merged: dict, query_names) -> dict:
79 """Build a per-query comparison dict.
81 Args:
82 classic (dict): Classic resolver output.
83 llm_merged (dict): LLM resolver output (after merge).
84 query_names: Iterable of query names.
86 Returns:
87 dict: Keyed by query name; each entry has 'classic', 'llm', 'differs', 'diff'.
88 """
89 comparison = {}
90 for name in query_names:
91 c = classic.get(name, {})
92 llm_entry = llm_merged.get(name, {})
94 c_summary = {
95 "hardDependsOn": c.get("hardDependsOn", {}),
96 "softDependsOn": c.get("softDependsOn", {}),
97 }
98 l_summary = {
99 "hardDependsOn": llm_entry.get("hardDependsOn", {}),
100 "softDependsOn": llm_entry.get("softDependsOn", {}),
101 }
103 diff = {}
104 for key in ("hardDependsOn", "softDependsOn"):
105 if c_summary[key] != l_summary[key]:
106 diff[key] = {"classic": c_summary[key], "llm": l_summary[key]}
108 comparison[name] = {
109 "classic": c_summary,
110 "llm": l_summary,
111 "differs": bool(diff),
112 "diff": diff,
113 }
114 return comparison