Skip to content

Commit d22043c

Browse files
authored
Merge branch 'main' into dependabot/pip/pip-9ac9876c91
2 parents 9bd8da6 + 00aa376 commit d22043c

File tree

30 files changed

+866
-223
lines changed

30 files changed

+866
-223
lines changed

.github/workflows/claude-code-review.yml

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,6 @@ on:
77
- opened
88
- synchronize
99
- ready_for_review
10-
# Optional: Only run on specific file changes
11-
# paths:
12-
# - "src/**/*.ts"
13-
# - "src/**/*.tsx"
14-
# - "src/**/*.js"
15-
# - "src/**/*.jsx"
1610
# For PRs from forked repositories (secure path with secrets)
1711
pull_request_target:
1812
types:
@@ -21,14 +15,12 @@ on:
2115
- ready_for_review
2216

2317
jobs:
24-
claude-review:
25-
# Skip draft PRs and prevent duplicate runs
18+
# Job for same-repo PRs (can use OIDC if needed)
19+
claude-review-same-repo:
2620
if: |
27-
github.event.pull_request.draft == false &&
28-
(
29-
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
30-
(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository)
31-
)
21+
github.event_name == 'pull_request' &&
22+
github.event.pull_request.head.repo.full_name == github.repository &&
23+
github.event.pull_request.draft == false
3224
3325
runs-on: ubuntu-latest
3426
permissions:
@@ -47,12 +39,65 @@ jobs:
4739
env:
4840
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4941
run: |
50-
if [ "${{ github.event_name }}" = "pull_request_target" ]; then
51-
echo "⚠️ Forked PR detected - running in secure mode"
52-
echo "PR from: ${{ github.event.pull_request.head.repo.full_name }}"
53-
echo "Base repo: ${{ github.repository }}"
54-
fi
42+
echo "Checking out PR #${{ github.event.pull_request.number }}"
43+
gh pr checkout ${{ github.event.pull_request.number }}
44+
echo "✅ PR branch checked out successfully"
45+
46+
- name: Run Claude Code Review
47+
id: claude-review
48+
uses: anthropics/claude-code-action@v1
49+
with:
50+
github_token: ${{ secrets.GITHUB_TOKEN }}
51+
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
52+
prompt: |
53+
REPO: ${{ github.repository }}
54+
PR NUMBER: ${{ github.event.pull_request.number }}
55+
56+
Please review this pull request and provide feedback on:
57+
- Code quality and best practices
58+
- Potential bugs or issues
59+
- Performance considerations
60+
- Security concerns
61+
- Test coverage
62+
63+
# Steps to run a Review:
64+
1) Check if previous review is already done by Claude. If so, perform a re-reivew with the latest changes referring previous review.
65+
2) If no previous review is found, perform a new review with the latest changes.
66+
67+
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
68+
69+
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
5570
71+
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
72+
73+
# Job for forked PRs (no OIDC, token-based only)
74+
claude-review-forked:
75+
if: |
76+
github.event_name == 'pull_request_target' &&
77+
github.event.pull_request.head.repo.full_name != github.repository &&
78+
github.event.pull_request.draft == false
79+
80+
runs-on: ubuntu-latest
81+
permissions:
82+
contents: read
83+
pull-requests: write
84+
issues: read
85+
# Explicitly disable id-token to avoid OIDC flow
86+
87+
steps:
88+
- name: Checkout repository (no credentials persisted)
89+
uses: actions/checkout@v4
90+
with:
91+
fetch-depth: 1
92+
persist-credentials: false
93+
94+
- name: Checkout PR branch (forked PR)
95+
env:
96+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
97+
run: |
98+
echo "⚠️ Forked PR detected - running in secure mode"
99+
echo "PR from: ${{ github.event.pull_request.head.repo.full_name }}"
100+
echo "Base repo: ${{ github.repository }}"
56101
echo "Checking out PR #${{ github.event.pull_request.number }}"
57102
gh pr checkout ${{ github.event.pull_request.number }}
58103
echo "✅ PR branch checked out successfully"
@@ -61,6 +106,7 @@ jobs:
61106
id: claude-review
62107
uses: anthropics/claude-code-action@v1
63108
with:
109+
github_token: ${{ secrets.GITHUB_TOKEN }}
64110
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
65111
prompt: |
66112
REPO: ${{ github.repository }}
@@ -73,10 +119,12 @@ jobs:
73119
- Security concerns
74120
- Test coverage
75121
122+
# Steps to run a Review:
123+
1) Check if previous review is already done by Claude. If so, perform a re-reivew with the latest changes referring previous review.
124+
2) If no previous review is found, perform a new review with the latest changes.
125+
76126
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
77127
78128
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
79129
80-
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
81-
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
82130
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'

autogen/a2a/agent_executor.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
from datetime import datetime, timezone
6+
from uuid import uuid4
67

78
from a2a.server.agent_execution import AgentExecutor, RequestContext
89
from a2a.server.events import EventQueue
9-
from a2a.types import TaskArtifactUpdateEvent, TaskState, TaskStatus, TaskStatusUpdateEvent
10-
from a2a.utils import new_task
10+
from a2a.types import Task, TaskArtifactUpdateEvent, TaskState, TaskStatus, TaskStatusUpdateEvent
1111
from a2a.utils.message import new_agent_text_message
1212

1313
from autogen import ConversableAgent
@@ -33,8 +33,17 @@ async def execute(self, context: RequestContext, event_queue: EventQueue) -> Non
3333

3434
task = context.current_task
3535
if not task:
36-
task = new_task(context.message)
37-
task.status.timestamp = datetime.now(timezone.utc).isoformat()
36+
request = context.message
37+
# build task object manually to allow empty messages
38+
task = Task(
39+
status=TaskStatus(
40+
state=TaskState.submitted,
41+
timestamp=datetime.now(timezone.utc).isoformat(),
42+
),
43+
id=request.task_id or str(uuid4()),
44+
context_id=request.context_id or str(uuid4()),
45+
history=[request],
46+
)
3847
# publish the task status submitted event
3948
await event_queue.enqueue_event(task)
4049

autogen/a2a/client.py

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from a2a.client import ClientFactory as A2AClientFactory
1414
from a2a.types import AgentCard, Message, Task, TaskIdParams, TaskQueryParams, TaskState
1515
from a2a.utils.constants import AGENT_CARD_WELL_KNOWN_PATH, EXTENDED_AGENT_CARD_PATH, PREV_AGENT_CARD_WELL_KNOWN_PATH
16+
from typing_extensions import Self
1617

1718
from autogen import ConversableAgent
1819
from autogen.agentchat.group import ContextVariables
@@ -55,9 +56,14 @@ def __init__(
5556
max_reconnects: int = 3,
5657
polling_interval: float = 0.5,
5758
) -> None:
58-
self.url = url
59+
self.url = url # make it public for backward compatibility
5960

6061
self._httpx_client_factory = client or EmptyClientFactory()
62+
self._card_resolver = A2ACardResolver(
63+
httpx_client=self._httpx_client_factory(),
64+
base_url=url,
65+
)
66+
6167
self._max_reconnects = max_reconnects
6268
self._polling_interval = polling_interval
6369

@@ -66,7 +72,7 @@ def __init__(
6672
self.__llm_config: dict[str, Any] = {}
6773

6874
self._client_config = client_config or ClientConfig()
69-
self.__agent_card: AgentCard | None = None
75+
self._agent_card: AgentCard | None = None
7076

7177
self.replace_reply_func(
7278
ConversableAgent.generate_oai_reply,
@@ -77,6 +83,48 @@ def __init__(
7783
A2aRemoteAgent.a_generate_remote_reply,
7884
)
7985

86+
@classmethod
87+
def from_card(
88+
cls,
89+
card: AgentCard,
90+
*,
91+
silent: bool | None = None,
92+
client: ClientFactory | None = None,
93+
client_config: ClientConfig | None = None,
94+
max_reconnects: int = 3,
95+
polling_interval: float = 0.5,
96+
) -> Self:
97+
"""Creates an A2aRemoteAgent instance from an existing AgentCard.
98+
99+
This method allows you to instantiate an A2aRemoteAgent directly using a pre-existing
100+
AgentCard, such as one retrieved from a discovery service or constructed manually.
101+
The resulting agent will use the data from the given card and avoid redundant card
102+
fetching. The agent's registryURL is set to "UNKNOWN" since it is assumed to be derived
103+
from the card.
104+
105+
Args:
106+
card: The agent card containing metadata and configuration for the remote agent.
107+
silent: whether to print the message sent. If None, will use the value of silent in each function.
108+
client: An optional HTTPX client instance factory.
109+
client_config: A2A Client configuration options.
110+
max_reconnects: Maximum number of reconnection attempts before giving up.
111+
polling_interval: Time in seconds between polling operations. Works for A2A Servers doesn't support streaming.
112+
113+
Returns:
114+
Self: An instance of the A2aRemoteAgent configured with the provided card.
115+
"""
116+
instance = cls(
117+
url="UNKNOWN",
118+
name=card.name,
119+
silent=silent,
120+
client=client,
121+
client_config=client_config,
122+
max_reconnects=max_reconnects,
123+
polling_interval=polling_interval,
124+
)
125+
instance._agent_card = card
126+
return instance
127+
80128
def generate_remote_reply(
81129
self,
82130
messages: list[dict[str, Any]] | None = None,
@@ -94,8 +142,8 @@ async def a_generate_remote_reply(
94142
if messages is None:
95143
messages = self._oai_messages[sender]
96144

97-
if not self.__agent_card:
98-
self.__agent_card = await self._get_agent_card()
145+
if not self._agent_card:
146+
self._agent_card = await self._get_agent_card()
99147

100148
initial_message = request_message_to_a2a(
101149
request_message=RequestMessage(
@@ -108,9 +156,9 @@ async def a_generate_remote_reply(
108156

109157
self._client_config.httpx_client = self._httpx_client_factory()
110158
async with self._client_config.httpx_client:
111-
agent_client = A2AClientFactory(self._client_config).create(self.__agent_card)
159+
agent_client = A2AClientFactory(self._client_config).create(self._agent_card)
112160

113-
if self.__agent_card.capabilities.streaming:
161+
if self._agent_card.capabilities.streaming:
114162
reply = await self._ask_streaming(agent_client, initial_message)
115163
return self._apply_reply(reply, sender)
116164

@@ -142,9 +190,9 @@ async def _ask_streaming(self, client: Client, message: Message) -> ResponseMess
142190
if task and connection_attemps < self._max_reconnects:
143191
pass
144192

145-
if not self.__agent_card:
193+
if not self._agent_card:
146194
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
147-
raise A2aClientError(f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}") from e
195+
raise A2aClientError(f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}") from e
148196

149197
task = cast(Task, task)
150198
while connection_attemps < self._max_reconnects:
@@ -159,11 +207,9 @@ async def _ask_streaming(self, client: Client, message: Message) -> ResponseMess
159207
if connection_attemps < self._max_reconnects:
160208
pass
161209

162-
if not self.__agent_card:
210+
if not self._agent_card:
163211
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
164-
raise A2aClientError(
165-
f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}"
166-
) from e
212+
raise A2aClientError(f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}") from e
167213

168214
return None
169215

@@ -175,9 +221,9 @@ async def _ask_polling(self, client: Client, message: Message) -> ResponseMessag
175221
return result
176222
break
177223
except httpx.ConnectError as e:
178-
if not self.__agent_card:
224+
if not self._agent_card:
179225
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
180-
raise A2aClientError(f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}") from e
226+
raise A2aClientError(f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}") from e
181227

182228
started_task, connection_attemps = cast(Task, started_task), 0
183229
while connection_attemps < self._max_reconnects:
@@ -190,10 +236,10 @@ async def _ask_polling(self, client: Client, message: Message) -> ResponseMessag
190236
if connection_attemps < self._max_reconnects:
191237
pass
192238

193-
if not self.__agent_card:
239+
if not self._agent_card:
194240
raise A2aClientError("Failed to connect to the agent: agent card not found") from e
195241
raise A2aClientError(
196-
f"Failed to connect to the agent: {pformat(self.__agent_card.model_dump())}"
242+
f"Failed to connect to the agent: {pformat(self._agent_card.model_dump())}"
197243
) from e
198244

199245
else:
@@ -231,27 +277,27 @@ async def _get_agent_card(
231277
self,
232278
auth_http_kwargs: dict[str, Any] | None = None,
233279
) -> AgentCard:
234-
resolver = A2ACardResolver(httpx_client=self._httpx_client_factory(), base_url=self.url)
235-
236280
card: AgentCard | None = None
237281

238282
try:
239-
logger.info(f"Attempting to fetch public agent card from: {self.url}{AGENT_CARD_WELL_KNOWN_PATH}")
283+
logger.info(
284+
f"Attempting to fetch public agent card from: {self._card_resolver.base_url}{AGENT_CARD_WELL_KNOWN_PATH}"
285+
)
240286

241287
try:
242-
card = await resolver.get_agent_card(relative_card_path=AGENT_CARD_WELL_KNOWN_PATH)
288+
card = await self._card_resolver.get_agent_card(relative_card_path=AGENT_CARD_WELL_KNOWN_PATH)
243289
except A2AClientHTTPError as e_public:
244290
if e_public.status_code == 404:
245291
logger.info(
246-
f"Attempting to fetch public agent card from: {self.url}{PREV_AGENT_CARD_WELL_KNOWN_PATH}"
292+
f"Attempting to fetch public agent card from: {self._card_resolver.base_url}{PREV_AGENT_CARD_WELL_KNOWN_PATH}"
247293
)
248-
card = await resolver.get_agent_card(relative_card_path=PREV_AGENT_CARD_WELL_KNOWN_PATH)
294+
card = await self._card_resolver.get_agent_card(relative_card_path=PREV_AGENT_CARD_WELL_KNOWN_PATH)
249295
else:
250296
raise e_public
251297

252298
if card.supports_authenticated_extended_card:
253299
try:
254-
card = await resolver.get_agent_card(
300+
card = await self._card_resolver.get_agent_card(
255301
relative_card_path=EXTENDED_AGENT_CARD_PATH,
256302
http_kwargs=auth_http_kwargs,
257303
)

autogen/a2a/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010

1111
from autogen.remote.protocol import RequestMessage, ResponseMessage
1212

13-
CLIENT_TOOLS_KEY = "ag2_client_tools"
14-
CONTEXT_KEY = "ag2_context_update"
13+
AG2_METADATA_KEY_PREFIX = "ag2_"
14+
CLIENT_TOOLS_KEY = f"{AG2_METADATA_KEY_PREFIX}client_tools"
15+
CONTEXT_KEY = f"{AG2_METADATA_KEY_PREFIX}context_update"
1516

1617

1718
def request_message_to_a2a(

0 commit comments

Comments
 (0)