@@ -93,6 +93,146 @@ def _extract_base_type_from_annotated(annotation: Any) -> Any:
9393 return annotation
9494
9595
96+ def _resolve_pydantic_refs (schema : Dict [str , Any ]) -> Dict [str , Any ]:
97+ """Resolve $ref pointers in Pydantic JSON schema and inline nested objects.
98+
99+ Pydantic generates JSON schemas with $ref pointers to $defs for nested
100+ BaseModel classes. This function resolves these references and inlines
101+ nested properties so that Field descriptions from nested models are
102+ directly accessible in the schema sent to the LLM.
103+
104+ This is similar to the reference resolution in openapi_spec_parser.py but
105+ optimized for Pydantic v2 schema structure (handles allOf wrappers).
106+
107+ Args:
108+ schema: Pydantic model_json_schema() output with $defs.
109+
110+ Returns:
111+ Schema with all $ref resolved and nested properties inlined. The $defs
112+ section is removed as all definitions are now inlined.
113+
114+ Example:
115+ Input:
116+ {
117+ "properties": {
118+ "user": {"allOf": [{"$ref": "#/$defs/Person"}], "description": "User"}
119+ },
120+ "$defs": {
121+ "Person": {"properties": {"name": {"description": "Name"}}}
122+ }
123+ }
124+
125+ Output:
126+ {
127+ "properties": {
128+ "user": {
129+ "type": "object",
130+ "description": "User",
131+ "properties": {"name": {"description": "Name"}}
132+ }
133+ }
134+ }
135+ """
136+ import copy
137+
138+ schema = copy .deepcopy (schema )
139+ defs = schema .get ("$defs" , {})
140+
141+ def resolve_ref (ref_string : str ) -> Optional [Dict ]:
142+ """Resolve a $ref string like '#/$defs/Person'."""
143+ if not ref_string .startswith ("#/$defs/" ):
144+ return None
145+ def_name = ref_string .split ("/" )[- 1 ]
146+ return defs .get (def_name )
147+
148+ def resolve_property (
149+ prop_schema : Dict , seen_refs : Optional [set ] = None
150+ ) -> Dict :
151+ """Recursively resolve $ref in a property schema.
152+
153+ Args:
154+ prop_schema: A property schema that may contain $ref or allOf with $ref.
155+ seen_refs: Set of already-visited $ref strings to prevent circular refs.
156+
157+ Returns:
158+ Property schema with all $ref resolved and nested properties inlined.
159+ """
160+ if seen_refs is None :
161+ seen_refs = set ()
162+
163+ prop_schema = copy .deepcopy (prop_schema )
164+
165+ # Handle allOf wrapper (Pydantic v2 pattern: {"allOf": [{"$ref": "..."}]})
166+ if "allOf" in prop_schema and len (prop_schema ["allOf" ]) == 1 :
167+ ref_item = prop_schema ["allOf" ][0 ]
168+ if "$ref" in ref_item :
169+ ref_string = ref_item ["$ref" ]
170+
171+ # Prevent circular references
172+ if ref_string in seen_refs :
173+ # Return schema without allOf to break the cycle
174+ return {k : v for k , v in prop_schema .items () if k != "allOf" }
175+
176+ seen_refs_copy = seen_refs .copy ()
177+ seen_refs_copy .add (ref_string )
178+
179+ resolved = resolve_ref (ref_string )
180+ if resolved :
181+ resolved = copy .deepcopy (resolved )
182+
183+ # Preserve parameter-level description (takes precedence over model docstring)
184+ param_description = prop_schema .get ("description" )
185+
186+ # Recursively resolve nested properties within the resolved definition
187+ if "properties" in resolved :
188+ for nested_name , nested_schema in resolved ["properties" ].items ():
189+ resolved ["properties" ][nested_name ] = resolve_property (
190+ nested_schema , seen_refs_copy
191+ )
192+
193+ # If there was a parameter-level description, keep it
194+ # (e.g., "User info" instead of model's docstring "Person model")
195+ if param_description :
196+ resolved ["description" ] = param_description
197+
198+ return resolved
199+
200+ # Handle direct $ref (less common in Pydantic v2, but supported for completeness)
201+ elif "$ref" in prop_schema :
202+ ref_string = prop_schema ["$ref" ]
203+ if ref_string not in seen_refs :
204+ seen_refs_copy = seen_refs .copy ()
205+ seen_refs_copy .add (ref_string )
206+ resolved = resolve_ref (ref_string )
207+ if resolved :
208+ return resolve_property (copy .deepcopy (resolved ), seen_refs_copy )
209+
210+ # Recursively resolve nested properties (for already-inlined objects)
211+ if "properties" in prop_schema :
212+ for nested_name in list (prop_schema ["properties" ].keys ()):
213+ prop_schema ["properties" ][nested_name ] = resolve_property (
214+ prop_schema ["properties" ][nested_name ], seen_refs
215+ )
216+
217+ # Handle arrays with items that might have refs
218+ if "items" in prop_schema :
219+ prop_schema ["items" ] = resolve_property (prop_schema ["items" ], seen_refs )
220+
221+ return prop_schema
222+
223+ # Resolve all top-level properties
224+ if "properties" in schema :
225+ for prop_name in list (schema ["properties" ].keys ()):
226+ schema ["properties" ][prop_name ] = resolve_property (
227+ schema ["properties" ][prop_name ]
228+ )
229+
230+ # Clean up $defs since all definitions are now inlined
231+ schema .pop ("$defs" , None )
232+
233+ return schema
234+
235+
96236def _get_fields_dict (func : Callable ) -> Dict :
97237 """Build a dictionary of field definitions for Pydantic model creation.
98238
@@ -214,7 +354,15 @@ def _get_pydantic_schema(func: Callable) -> Dict:
214354 context_param = find_context_parameter (func ) or "tool_context"
215355 if context_param in fields_dict .keys ():
216356 fields_dict .pop (context_param )
217- return pydantic .create_model (func .__name__ , ** fields_dict ).model_json_schema ()
357+
358+ schema = pydantic .create_model (
359+ func .__name__ , ** fields_dict
360+ ).model_json_schema ()
361+
362+ # Resolve $ref for nested Pydantic models to inline Field descriptions
363+ schema = _resolve_pydantic_refs (schema )
364+
365+ return schema
218366
219367
220368def _process_pydantic_schema (vertexai : bool , schema : Dict ) -> Dict :
0 commit comments