11"""Artifact relationships tool implementation."""
22
3- from typing import Any , Dict , List , Literal
3+ from typing import Any , Dict , List , Literal , Optional
44from urllib .parse import urljoin
55
66import httpx
@@ -37,6 +37,7 @@ async def get_artifact_relationships(
3737 identifier : str ,
3838 profile : Literal ["callsOnly" , "inheritanceOnly" , "allRelevant" , "referencesOnly" ] = "callsOnly" ,
3939 max_count_per_type : int = 50 ,
40+ data_source : Optional [str ] = None ,
4041) -> Dict [str , Any ]:
4142 """
4243 Retrieve relationship groups for a single artifact by profile.
@@ -84,6 +85,11 @@ async def get_artifact_relationships(
8485 - "allRelevant": calls + inheritance only; references are excluded
8586 - "referencesOnly": where-used LSP references for non-call usage
8687 max_count_per_type: Maximum related artifacts per relationship type (1–1000, default 50).
88+ data_source: Optional data-source Name or Id used to disambiguate an identifier that
89+ exists in more than one data source. Copy the `dataSource.name` or
90+ `dataSource.id` from a search result. Omit it for normal lookups; if the
91+ source identifier is ambiguous and you omit it, the backend returns a 409
92+ listing the candidate data sources.
8793
8894 Returns:
8995 A dict with grouped relationships:
@@ -103,6 +109,7 @@ async def get_artifact_relationships(
103109 "identifier" : identifier ,
104110 "profile" : profile ,
105111 "max_count_per_type" : max_count_per_type ,
112+ "data_source" : data_source ,
106113 }
107114
108115 if not identifier :
@@ -143,6 +150,8 @@ async def get_artifact_relationships(
143150 "profile" : api_profile ,
144151 "maxCountPerType" : max_count_per_type ,
145152 }
153+ if data_source :
154+ body ["dataSource" ] = data_source
146155
147156 await ctx .debug (f"Fetching { profile } relationships for artifact" )
148157
@@ -156,7 +165,7 @@ async def get_artifact_relationships(
156165 log_api_response (response , request_id )
157166 response .raise_for_status ()
158167
159- return _build_relationships_dict (response .json ())
168+ return _build_relationships_dict (response .json (), data_source = data_source )
160169
161170 except (httpx .HTTPStatusError , Exception ) as e :
162171 logger .bind (
@@ -173,15 +182,24 @@ async def get_artifact_relationships(
173182 "(2) call semantic_search or grep_search again to get a fresh identifier — the index may have changed, "
174183 "(3) check that the artifact is a function/class (relationships are not available for non-symbol artifacts)"
175184 ),
185+ 409 : (
186+ "(1) the identifier exists in more than one data source — see the candidate data sources in the Detail above; each one will resolve, "
187+ "(2) retry get_artifact_relationships with data_source set to one candidate's Name or Id; if that data source isn't the one you want, retry with the next candidate, "
188+ "(3) do NOT invent relation results — pick from the listed data sources"
189+ ),
176190 },
177191 )
178192
179193
180- def _build_relationships_dict (data : dict ) -> Dict [str , Any ]:
194+ def _build_relationships_dict (data : dict , data_source : Optional [ str ] = None ) -> Dict [str , Any ]:
181195 """Build a dict representation of an artifact relationships response.
182196
183197 FastMCP serializes the dict via pydantic_core.to_json, which preserves UTF-8 —
184198 don't reintroduce json.dumps here, it would re-escape non-ASCII identifiers.
199+
200+ ``data_source`` is the selector the caller passed (if any); when the source is not
201+ found it shapes the recovery hint so the agent can retry with another data source
202+ or drop the selector.
185203 """
186204 raw_source_id = data .get ("sourceIdentifier" ) or ""
187205 raw_profile = data .get ("profile" ) or ""
@@ -208,9 +226,9 @@ def _build_relationships_dict(data: dict) -> Dict[str, Any]:
208226 counts = _build_counts (data .get ("availableRelationshipCounts" ))
209227 if counts is not None :
210228 payload ["availableRelationshipCounts" ] = counts
211- payload ["hint" ] = _build_relationship_hint (found , mcp_profile , groups , counts )
229+ payload ["hint" ] = _build_relationship_hint (found , mcp_profile , groups , counts , data_source )
212230 else :
213- payload ["hint" ] = _build_relationship_hint (found , mcp_profile , [], None )
231+ payload ["hint" ] = _build_relationship_hint (found , mcp_profile , [], None , data_source )
214232
215233 return payload
216234
@@ -266,9 +284,19 @@ def _build_relationship_hint(
266284 profile : str ,
267285 groups : List [Dict [str , Any ]],
268286 counts : Dict [str , int ] | None ,
287+ data_source : Optional [str ] = None ,
269288) -> str :
270289 """Give model-facing next-step guidance for graph traversal results."""
271290 if not found :
291+ if data_source :
292+ return (
293+ f'No relationship data was found for this identifier in data source "{ data_source } ". '
294+ "The identifier may belong to a different data source, or the data_source value may be "
295+ "wrong. Try: re-run with data_source set to a different candidate (use the `dataSource` "
296+ "name or id from your search results, or call get_data_sources), or omit data_source "
297+ "entirely — if the identifier is ambiguous you then get a 409 listing the candidate data "
298+ "sources. Otherwise re-run semantic_search or grep_search to get a fresh identifier."
299+ )
272300 return (
273301 "No relationship data was found for this identifier. Verify that the identifier came from "
274302 "a recent search/fetch result and points to a symbol-level artifact; otherwise re-run "
0 commit comments