Skip to content

Commit e5a5f2e

Browse files
jodeklotzmsclaude
andcommitted
fix: address PR #46101 review comments
- Fix duplicate kwargs bug: remove explicit skill_directories/tools from create_session() calls — already present in sdk_config via _session_config - Bump version to 1.0.0b2 to match CHANGELOG - Remove .foundry-agent.json (contained real session/conversation IDs) - Close AsyncDefaultCredential in _load_conversation_history to prevent async transport/socket leak - Restore attachment handling: _extract_input_with_attachments() extracts input_file and input_image items from RAPI requests and appends to prompt - Observe cancellation_signal in event loop to stop early on client disconnect - Drop [tracing] extra from test Dockerfile to match pyproject.toml deps Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4380501 commit e5a5f2e

File tree

4 files changed

+103
-55
lines changed

4 files changed

+103
-55
lines changed

sdk/agentserver/azure-ai-agentserver-githubcopilot/.foundry-agent.json

Lines changed: 0 additions & 10 deletions
This file was deleted.

sdk/agentserver/azure-ai-agentserver-githubcopilot/azure/ai/agentserver/githubcopilot/_copilot_adapter.py

Lines changed: 101 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,57 @@
4141

4242
logger = 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. "

sdk/agentserver/azure-ai-agentserver-githubcopilot/azure/ai/agentserver/githubcopilot/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
# Licensed under the MIT License. See License.txt in the project root for license information.
44
# ---------------------------------------------------------
55

6-
VERSION = "1.0.0b1"
6+
VERSION = "1.0.0b2"

sdk/agentserver/azure-ai-agentserver-githubcopilot/tests/integration/test_agent/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
99
# to avoid the feed interfering with github-copilot-sdk from PyPI).
1010
RUN pip install --no-cache-dir --no-input --pre \
1111
--extra-index-url https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/ \
12-
"azure-ai-agentserver-core[tracing]>=2.0.0a1" \
12+
"azure-ai-agentserver-core>=2.0.0a1" \
1313
"azure-ai-agentserver-responses>=1.0.0a1"
1414

1515
# Copy the package source for local install (not on PyPI yet)

0 commit comments

Comments
 (0)