1+ from typing import Any
2+
13from sqlalchemy import select
24
3- from fastapi import APIRouter , Depends , HTTPException , Response
5+ from fastapi import APIRouter , Depends , HTTPException , Query , Response
6+ from langchain_core .runnables .utils import create_model
47
58from agentseek_api .core .auth_deps import get_current_user
69from agentseek_api .core .database import db_manager
1316 AssistantVersionInfo ,
1417 ErrorDetailResponse ,
1518)
19+ from agentseek_api .services .default_assistants import resolve_assistant_id
1620from agentseek_api .services .langgraph_service import get_langgraph_service
1721
1822router = APIRouter (dependencies = [Depends (get_current_user )])
19- ASSISTANT_SUBGRAPHS_UNSUPPORTED = "Assistant subgraph inspection is not supported"
2023ASSISTANT_VERSION_PROMOTION_UNSUPPORTED = "Assistant version promotion is not supported"
2124DELETE_THREADS_UNSUPPORTED = "delete_threads=true is not supported"
25+ SUBGRAPHS_UNSUPPORTED = "The graph does not support subgraphs"
2226
2327
2428def _detail_response (* , description : str , detail : str ) -> dict [str , object ]:
@@ -111,19 +115,21 @@ async def count_assistants(payload: AssistantSearchRequest) -> int:
111115
112116@router .get ("/{assistant_id}" , response_model = AssistantRead )
113117async def get_assistant (assistant_id : str ) -> AssistantRead :
118+ resolved_id = resolve_assistant_id (assistant_id )
114119 session_factory = db_manager .get_session_factory ()
115120 async with session_factory () as session :
116- row = await session .scalar (select (Assistant ).where (Assistant .assistant_id == assistant_id ))
121+ row = await session .scalar (select (Assistant ).where (Assistant .assistant_id == resolved_id ))
117122 if row is None :
118123 raise HTTPException (status_code = 404 , detail = "Assistant not found" )
119124 return _to_read_model (row )
120125
121126
122127@router .patch ("/{assistant_id}" , response_model = AssistantRead )
123128async def patch_assistant (assistant_id : str , payload : AssistantPatch ) -> AssistantRead :
129+ resolved_id = resolve_assistant_id (assistant_id )
124130 session_factory = db_manager .get_session_factory ()
125131 async with session_factory () as session :
126- row = await session .scalar (select (Assistant ).where (Assistant .assistant_id == assistant_id ))
132+ row = await session .scalar (select (Assistant ).where (Assistant .assistant_id == resolved_id ))
127133 if row is None :
128134 raise HTTPException (status_code = 404 , detail = "Assistant not found" )
129135 if payload .graph_id is not None :
@@ -154,9 +160,10 @@ async def patch_assistant(assistant_id: str, payload: AssistantPatch) -> Assista
154160async def delete_assistant (assistant_id : str , delete_threads : bool = False ) -> Response :
155161 if delete_threads :
156162 raise HTTPException (status_code = 400 , detail = DELETE_THREADS_UNSUPPORTED )
163+ resolved_id = resolve_assistant_id (assistant_id )
157164 session_factory = db_manager .get_session_factory ()
158165 async with session_factory () as session :
159- row = await session .scalar (select (Assistant ).where (Assistant .assistant_id == assistant_id ))
166+ row = await session .scalar (select (Assistant ).where (Assistant .assistant_id == resolved_id ))
160167 if row is None :
161168 raise HTTPException (status_code = 404 , detail = "Assistant not found" )
162169 await session .delete (row )
@@ -165,55 +172,119 @@ async def delete_assistant(assistant_id: str, delete_threads: bool = False) -> R
165172
166173
167174@router .get ("/{assistant_id}/graph" )
168- async def get_assistant_graph (assistant_id : str ) -> dict [str , object ]:
175+ async def get_assistant_graph (
176+ assistant_id : str ,
177+ xray : bool | int | None = Query (
178+ None ,
179+ description = "Expand subgraph nodes. Pass true or a positive integer depth." ,
180+ ),
181+ ) -> dict [str , object ]:
169182 assistant = await get_assistant (assistant_id )
170- return {
171- "assistant_id" : assistant .assistant_id ,
172- "graph_id" : assistant .graph_id ,
173- "registered_graph_ids" : get_langgraph_service ().registered_graph_ids (),
174- }
183+ entry = get_langgraph_service ().get_entry (assistant .graph_id )
184+ graph = entry .build_graph ()
185+ if isinstance (xray , int ) and not isinstance (xray , bool ) and xray <= 0 :
186+ raise HTTPException (status_code = 422 , detail = "Invalid xray value" )
187+ xray_value : bool | int = xray if xray is not None else False
188+ try :
189+ drawable_graph = await graph .aget_graph (xray = xray_value )
190+ except NotImplementedError as exc :
191+ raise HTTPException (status_code = 422 , detail = "The graph does not support visualization" ) from exc
192+ json_graph = drawable_graph .to_json ()
193+ for node in json_graph .get ("nodes" , []):
194+ data = node .get ("data" ) if isinstance (node , dict ) else None
195+ if isinstance (data , dict ):
196+ data .pop ("id" , None )
197+ return json_graph
175198
176199
177200@router .get ("/{assistant_id}/schemas" )
178201async def get_assistant_schemas (assistant_id : str ) -> dict [str , object ]:
179202 assistant = await get_assistant (assistant_id )
180203 entry = get_langgraph_service ().get_entry (assistant .graph_id )
204+ graph = entry .build_graph ()
205+ return {"graph_id" : assistant .graph_id , ** _extract_graph_schemas (graph )}
206+
207+
208+ def _safe_schema (getter , * args , ** kwargs ) -> dict [str , object ] | None :
209+ try :
210+ return getter (* args , ** kwargs )
211+ except Exception : # noqa: BLE001 - graph helpers raise broad errors
212+ return None
213+
214+
215+ def _state_jsonschema (graph ) -> dict [str , object ] | None :
216+ channel_list = getattr (graph , "stream_channels_list" , None )
217+ channels = getattr (graph , "channels" , None )
218+ if not channel_list or channels is None :
219+ return None
220+ fields : dict [str , tuple [object , object ]] = {}
221+ for key in channel_list :
222+ channel = channels .get (key ) if isinstance (channels , dict ) else getattr (channels , key , None )
223+ update_type = getattr (channel , "UpdateType" , Any ) if channel is not None else Any
224+ fields [key ] = (update_type , None )
225+ try :
226+ name = graph .get_name ("State" ) if hasattr (graph , "get_name" ) else "State"
227+ return create_model (name , ** fields ).model_json_schema ()
228+ except Exception : # noqa: BLE001
229+ return None
230+
231+
232+ def _extract_graph_schemas (graph ) -> dict [str , object | None ]:
181233 return {
182- "assistant_id" : assistant .assistant_id ,
183- "graph_id" : assistant .graph_id ,
184- "name" : assistant .name ,
185- "description" : assistant .description ,
186- "input_schema" : entry .input_schema ,
187- "output_schema" : entry .output_schema ,
234+ "input_schema" : _safe_schema (graph .get_input_jsonschema ),
235+ "output_schema" : _safe_schema (graph .get_output_jsonschema ),
236+ "state_schema" : _state_jsonschema (graph ),
237+ "config_schema" : _safe_schema (graph .get_config_jsonschema ) if hasattr (graph , "get_config_jsonschema" ) else None ,
238+ "context_schema" : _safe_schema (graph .get_context_jsonschema ) if hasattr (graph , "get_context_jsonschema" ) else None ,
188239 }
189240
190241
242+ async def _collect_subgraphs (assistant_id : str , * , namespace : str | None , recurse : bool ) -> dict [str , dict [str , object | None ]]:
243+ assistant = await get_assistant (assistant_id )
244+ entry = get_langgraph_service ().get_entry (assistant .graph_id )
245+ graph = entry .build_graph ()
246+ aget_subgraphs = getattr (graph , "aget_subgraphs" , None )
247+ if not callable (aget_subgraphs ):
248+ raise HTTPException (status_code = 422 , detail = SUBGRAPHS_UNSUPPORTED )
249+ try :
250+ return {
251+ ns : _extract_graph_schemas (subgraph )
252+ async for ns , subgraph in aget_subgraphs (namespace = namespace , recurse = recurse )
253+ }
254+ except NotImplementedError as exc :
255+ raise HTTPException (status_code = 422 , detail = SUBGRAPHS_UNSUPPORTED ) from exc
256+
257+
191258@router .get (
192259 "/{assistant_id}/subgraphs" ,
193- status_code = 501 ,
194- response_model = None ,
260+ response_model = dict [str , dict [str , object | None ]],
195261 responses = {
196262 404 : _detail_response (description = "Assistant not found" , detail = "Assistant not found" ),
197- 501 : _detail_response (description = "Unsupported helper endpoint " , detail = ASSISTANT_SUBGRAPHS_UNSUPPORTED ),
263+ 422 : _detail_response (description = "Graph does not support subgraphs " , detail = SUBGRAPHS_UNSUPPORTED ),
198264 },
199265)
200- async def get_assistant_subgraphs (assistant_id : str ) -> None :
201- _ = await get_assistant (assistant_id )
202- raise HTTPException (status_code = 501 , detail = ASSISTANT_SUBGRAPHS_UNSUPPORTED )
266+ async def get_assistant_subgraphs (
267+ assistant_id : str ,
268+ recurse : bool = Query (False , description = "Recursively include nested subgraphs." ),
269+ namespace : str | None = Query (None , description = "Filter to a specific subgraph namespace." ),
270+ ) -> dict [str , dict [str , object | None ]]:
271+ return await _collect_subgraphs (assistant_id , namespace = namespace , recurse = recurse )
203272
204273
205274@router .get (
206275 "/{assistant_id}/subgraphs/{namespace}" ,
207- status_code = 501 ,
208- response_model = None ,
276+ response_model = dict [str , dict [str , object | None ]],
209277 responses = {
210278 404 : _detail_response (description = "Assistant not found" , detail = "Assistant not found" ),
211- 501 : _detail_response (description = "Unsupported helper endpoint " , detail = ASSISTANT_SUBGRAPHS_UNSUPPORTED ),
279+ 422 : _detail_response (description = "Graph does not support subgraphs " , detail = SUBGRAPHS_UNSUPPORTED ),
212280 },
213281)
214- async def get_assistant_subgraphs_by_namespace (assistant_id : str , namespace : str ) -> None :
215- _ = (await get_assistant (assistant_id ), namespace )
216- raise HTTPException (status_code = 501 , detail = ASSISTANT_SUBGRAPHS_UNSUPPORTED )
282+ async def get_assistant_subgraphs_by_namespace (
283+ assistant_id : str ,
284+ namespace : str ,
285+ recurse : bool = Query (False , description = "Recursively include nested subgraphs." ),
286+ ) -> dict [str , dict [str , object | None ]]:
287+ return await _collect_subgraphs (assistant_id , namespace = namespace , recurse = recurse )
217288
218289
219290@router .post (
0 commit comments