4141
4242logger = logging .getLogger ("azure.ai.agentserver.githubcopilot" )
4343
44+
45+ def _extract_input_with_attachments (request ) -> str :
46+ """Extract text from a RAPI request, including any file/image attachments.
47+
48+ ``get_input_text`` only returns the text portion of the request input.
49+ This helper also checks for ``input_file`` and ``input_image`` items and
50+ appends their content to the prompt so the Copilot SDK (which only accepts
51+ a string prompt) can still reason about attachments.
52+ """
53+ text = get_input_text (request )
54+
55+ # Check for attachment items in the request input
56+ input_items = getattr (request , "input" , None )
57+ if not isinstance (input_items , list ):
58+ return text
59+
60+ attachment_parts = []
61+ for item in input_items :
62+ item_type = None
63+ if isinstance (item , dict ):
64+ item_type = item .get ("type" )
65+ else :
66+ item_type = getattr (item , "type" , None )
67+
68+ if item_type == "input_file" :
69+ filename = (item .get ("filename" ) if isinstance (item , dict ) else getattr (item , "filename" , None )) or "file"
70+ file_data = (item .get ("file_data" ) if isinstance (item , dict ) else getattr (item , "file_data" , None )) or ""
71+ if file_data :
72+ # base64 content — decode if possible, otherwise include raw
73+ import base64
74+ try :
75+ decoded = base64 .b64decode (file_data ).decode ("utf-8" , errors = "replace" )
76+ attachment_parts .append (f"\n [Attached file: { filename } ]\n { decoded } " )
77+ except Exception :
78+ attachment_parts .append (f"\n [Attached file: { filename } (binary, { len (file_data )} chars base64)]" )
79+
80+ elif item_type == "input_image" :
81+ image_url = (item .get ("image_url" ) if isinstance (item , dict ) else getattr (item , "image_url" , None )) or ""
82+ if isinstance (image_url , dict ):
83+ image_url = image_url .get ("url" , "" )
84+ elif hasattr (image_url , "url" ):
85+ image_url = image_url .url
86+ if image_url :
87+ attachment_parts .append (f"\n [Attached image: { image_url [:200 ]} ]" )
88+
89+ if attachment_parts :
90+ logger .info ("Extracted %d attachment(s) from request input" , len (attachment_parts ))
91+ return text + "" .join (attachment_parts )
92+
93+ return text
94+
4495_COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default"
4596
4697
@@ -262,15 +313,16 @@ async def _get_or_create_session(self, conversation_id=None):
262313 client = await self ._ensure_client ()
263314 config = self ._refresh_token_if_needed ()
264315
265- # Filter out internal flags (starting with _) before passing to SDK
316+ # Filter out internal flags (starting with _) before passing to SDK.
317+ # skill_directories and tools are already in _session_config when
318+ # GitHubCopilotAdapter discovers them, so they flow through here
319+ # automatically — no need to pass them as separate kwargs.
266320 sdk_config = {k : v for k , v in config .items () if not k .startswith ("_" )}
267321
268322 session = await client .create_session (
269323 ** sdk_config ,
270324 on_permission_request = self ._make_permission_handler (),
271325 streaming = True ,
272- skill_directories = self ._session_config .get ("skill_directories" ),
273- tools = self ._session_config .get ("tools" ),
274326 )
275327
276328 if conversation_id :
@@ -309,7 +361,7 @@ async def handle_create(request, context, cancellation_signal):
309361
310362 async def _handle_create (self , request , context , cancellation_signal ):
311363 """Handle POST /responses — bridge Copilot SDK events to RAPI stream."""
312- input_text = get_input_text (request )
364+ input_text = _extract_input_with_attachments (request )
313365 conversation_id = getattr (context , "conversation_id" , None )
314366 response_id = getattr (context , "response_id" , None ) or "unknown"
315367
@@ -353,6 +405,11 @@ def on_event(event):
353405 usage = None
354406
355407 while True :
408+ # Check if the client disconnected
409+ if cancellation_signal is not None and cancellation_signal .is_set ():
410+ logger .info ("Client disconnected — ending response early" )
411+ break
412+
356413 try :
357414 event = await asyncio .wait_for (queue .get (), timeout = idle_timeout )
358415 except asyncio .TimeoutError :
@@ -610,44 +667,47 @@ async def _load_conversation_history(self, conversation_id: str) -> Optional[str
610667 from openai import AsyncOpenAI
611668
612669 cred = AsyncDefaultCredential ()
613- token_provider = get_bearer_token_provider (cred , "https://ai.azure.com/.default" )
614- token = await token_provider ()
615- openai_client = AsyncOpenAI (
616- base_url = f"{ project_endpoint } /openai" ,
617- api_key = token ,
618- default_query = {"api-version" : "2025-11-15-preview" },
619- )
620-
621- items = []
622- async for item in openai_client .conversations .items .list (conversation_id ):
623- items .append (item )
624- items .reverse () # API returns reverse chronological
625-
626- if not items :
627- return None
628-
629- lines = []
630- for item in items :
631- role = getattr (item , "role" , None )
632- content = getattr (item , "content" , None )
633- if isinstance (content , str ):
634- text = content
635- elif isinstance (content , list ):
636- text_parts = []
637- for part in content :
638- if isinstance (part , dict ):
639- text_parts .append (part .get ("text" , "" ))
640- elif hasattr (part , "text" ):
641- text_parts .append (part .text )
642- text = " " .join (p for p in text_parts if p )
643- else :
644- continue
645- if not text :
646- continue
647- label = "User" if role == "user" else "Assistant"
648- lines .append (f"{ label } : { text } " )
670+ try :
671+ token_provider = get_bearer_token_provider (cred , "https://ai.azure.com/.default" )
672+ token = await token_provider ()
673+ openai_client = AsyncOpenAI (
674+ base_url = f"{ project_endpoint } /openai" ,
675+ api_key = token ,
676+ default_query = {"api-version" : "2025-11-15-preview" },
677+ )
649678
650- return "\n " .join (lines ) if lines else None
679+ items = []
680+ async for item in openai_client .conversations .items .list (conversation_id ):
681+ items .append (item )
682+ items .reverse () # API returns reverse chronological
683+
684+ if not items :
685+ return None
686+
687+ lines = []
688+ for item in items :
689+ role = getattr (item , "role" , None )
690+ content = getattr (item , "content" , None )
691+ if isinstance (content , str ):
692+ text = content
693+ elif isinstance (content , list ):
694+ text_parts = []
695+ for part in content :
696+ if isinstance (part , dict ):
697+ text_parts .append (part .get ("text" , "" ))
698+ elif hasattr (part , "text" ):
699+ text_parts .append (part .text )
700+ text = " " .join (p for p in text_parts if p )
701+ else :
702+ continue
703+ if not text :
704+ continue
705+ label = "User" if role == "user" else "Assistant"
706+ lines .append (f"{ label } : { text } " )
707+
708+ return "\n " .join (lines ) if lines else None
709+ finally :
710+ await cred .close ()
651711 except Exception :
652712 logger .warning ("Failed to load conversation history for %s" , conversation_id , exc_info = True )
653713 return None
@@ -665,8 +725,6 @@ async def _get_or_create_session(self, conversation_id=None):
665725 ** sdk_config ,
666726 on_permission_request = self ._make_permission_handler (),
667727 streaming = True ,
668- skill_directories = self ._session_config .get ("skill_directories" ),
669- tools = self ._session_config .get ("tools" ),
670728 )
671729 preamble = (
672730 "The following is the prior conversation history. "
0 commit comments