Skip to content

Commit d48235e

Browse files
authored
Merge pull request #285 from jinhao2019/feat/Support_customer_http_client
Feat: Support custom http client for RemoteVeAgent
2 parents 1d20a2d + 7404d2f commit d48235e

File tree

1 file changed

+111
-58
lines changed

1 file changed

+111
-58
lines changed

veadk/a2a/remote_ve_agent.py

Lines changed: 111 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# limitations under the License.
1414

1515
import json
16-
from typing import Literal
16+
from typing import Literal, Optional
1717

1818
import httpx
1919
import requests
@@ -36,38 +36,52 @@ def _convert_agent_card_dict_to_obj(agent_card_dict: dict) -> AgentCard:
3636
class RemoteVeAgent(RemoteA2aAgent):
3737
"""Connect to a remote agent on the VeFaaS platform.
3838
39-
This class provides an interface to remotely connect with an agent deployed on the VeFaaS platform. It automatically fetches the agent card (metadata) and configures an HTTP client for secure communication. Authentication can be handled either via a bearer token in the HTTP header or via a query string parameter.
39+
This class provides an interface to remotely connect with an agent deployed on the
40+
VeFaaS platform. It automatically fetches the agent card (metadata) and configures
41+
an HTTP client for secure communication.
4042
41-
The class extends `RemoteA2aAgent` to provide compatibility with the A2A (Agent-to-Agent) communication layer.
43+
The class extends `RemoteA2aAgent` to provide compatibility with the A2A
44+
(Agent-to-Agent) communication layer.
4245
43-
This constructor connects to a remote VeFaaS agent endpoint, retrieves its metadata (`agent_card`), and sets up an asynchronous HTTP client (`httpx.AsyncClient`) for subsequent communication. Depending on the provided authentication parameters, it supports three connection modes:
44-
- **No authentication:** Directly fetches the agent card.
45-
- **Header authentication:** Sends a bearer token in the `Authorization` header.
46-
- **Query string authentication:** Appends the token to the URL query.
46+
This constructor handles agent discovery and HTTP client setup. It determines the
47+
agent's URL, fetches its metadata (`agent_card`), and prepares an
48+
`httpx.AsyncClient` for subsequent communication. You can either provide a URL
49+
directly, or pass a pre-configured `httpx.AsyncClient` with a `base_url`.
50+
51+
Authentication can be handled via a bearer token in the HTTP header or via a
52+
query string parameter. If a custom `httpx_client` is provided, authentication
53+
details will be added to it.
4754
4855
Attributes:
4956
name (str):
5057
A unique name identifying this remote agent instance.
51-
url (str):
52-
The base URL of the remote agent on the VeFaaS platform.
53-
auth_token (str | None):
54-
Optional authentication token used for secure access.
55-
If not provided, the agent will be accessed without authentication.
58+
url (Optional[str]):
59+
The base URL of the remote agent. This is optional if an `httpx_client`
60+
with a configured `base_url` is provided. If both are given, they must
61+
not conflict.
62+
auth_token (Optional[str]):
63+
Optional authentication token used for secure access. If not provided,
64+
the agent will be accessed without authentication.
5665
auth_method (Literal["header", "querystring"] | None):
5766
The method of attaching the authentication token.
5867
- `"header"`: Token is passed via HTTP `Authorization` header.
5968
- `"querystring"`: Token is passed as a query parameter.
60-
- `None`: No authentication used.
69+
httpx_client (Optional[httpx.AsyncClient]):
70+
An optional, pre-configured `httpx.AsyncClient` to use for communication.
71+
This allows for client sharing and advanced configurations (e.g., proxies).
72+
If its `base_url` is set, it will be used as the agent's location.
6173
6274
Raises:
6375
ValueError:
64-
If an unsupported `auth_method` is provided when `auth_token` is set.
76+
- If `url` and `httpx_client.base_url` are both provided and conflict.
77+
- If neither `url` nor an `httpx_client` with a `base_url` is provided.
78+
- If an unsupported `auth_method` is provided when `auth_token` is set.
6579
requests.RequestException:
6680
If fetching the agent card from the remote URL fails.
6781
6882
Examples:
6983
```python
70-
# Example 1: No authentication
84+
# Example 1: Connect using a URL
7185
agent = RemoteVeAgent(
7286
name="public_agent",
7387
url="https://vefaas.example.com/agents/public"
@@ -81,65 +95,104 @@ class RemoteVeAgent(RemoteA2aAgent):
8195
auth_method="header"
8296
)
8397
84-
# Example 3: Using token in query string
98+
# Example 3: Using a pre-configured httpx_client
99+
import httpx
100+
client = httpx.AsyncClient(
101+
base_url="https://vefaas.example.com/agents/query",
102+
timeout=600
103+
)
85104
agent = RemoteVeAgent(
86105
name="query_agent",
87-
url="https://vefaas.example.com/agents/query",
88106
auth_token="my_secret_token",
89-
auth_method="querystring"
107+
auth_method="querystring",
108+
httpx_client=client
90109
)
91110
```
92111
"""
93112

94113
def __init__(
95114
self,
96115
name: str,
97-
url: str,
98-
auth_token: str | None = None,
116+
url: Optional[str] = None,
117+
auth_token: Optional[str] = None,
99118
auth_method: Literal["header", "querystring"] | None = None,
119+
httpx_client: Optional[httpx.AsyncClient] = None,
100120
):
101-
if not auth_token:
102-
agent_card_dict = requests.get(url + AGENT_CARD_WELL_KNOWN_PATH).json()
103-
# replace agent_card_url with actual host
104-
agent_card_dict["url"] = url
121+
# Determine the effective URL for the agent and handle conflicts.
122+
effective_url = url
123+
if httpx_client and httpx_client.base_url:
124+
client_url_str = str(httpx_client.base_url).rstrip("/")
125+
if url and url.rstrip("/") != client_url_str:
126+
raise ValueError(
127+
f"The `url` parameter ('{url}') conflicts with the `base_url` of the provided "
128+
f"httpx_client ('{client_url_str}'). Please provide only one or ensure they match."
129+
)
130+
effective_url = client_url_str
105131

106-
agent_card_object = _convert_agent_card_dict_to_obj(agent_card_dict)
132+
if not effective_url:
133+
raise ValueError(
134+
"Could not determine agent URL. Please provide the `url` parameter or an `httpx_client` with a configured `base_url`."
135+
)
107136

108-
logger.debug(f"Agent card of {name}: {agent_card_object}")
109-
super().__init__(name=name, agent_card=agent_card_object)
110-
else:
111-
if auth_method == "header":
112-
headers = {"Authorization": f"Bearer {auth_token}"}
113-
agent_card_dict = requests.get(
114-
url + AGENT_CARD_WELL_KNOWN_PATH, headers=headers
115-
).json()
116-
agent_card_dict["url"] = url
117-
118-
agent_card_object = _convert_agent_card_dict_to_obj(agent_card_dict)
119-
httpx_client = httpx.AsyncClient(
120-
base_url=url, headers=headers, timeout=600
121-
)
137+
req_headers = {}
138+
req_params = {}
122139

123-
logger.debug(f"Agent card of {name}: {agent_card_object}")
124-
super().__init__(
125-
name=name, agent_card=agent_card_object, httpx_client=httpx_client
126-
)
140+
if auth_token:
141+
if auth_method == "header":
142+
req_headers = {"Authorization": f"Bearer {auth_token}"}
127143
elif auth_method == "querystring":
128-
agent_card_dict = requests.get(
129-
url + AGENT_CARD_WELL_KNOWN_PATH + f"?token={auth_token}"
130-
).json()
131-
agent_card_dict["url"] = url
132-
133-
agent_card_object = _convert_agent_card_dict_to_obj(agent_card_dict)
134-
httpx_client = httpx.AsyncClient(
135-
base_url=url, params={"token": auth_token}, timeout=600
136-
)
137-
138-
logger.debug(f"Agent card of {name}: {agent_card_object}")
139-
super().__init__(
140-
name=name, agent_card=agent_card_object, httpx_client=httpx_client
141-
)
142-
else:
144+
req_params = {"token": auth_token}
145+
elif auth_method:
143146
raise ValueError(
144147
f"Unsupported auth method {auth_method}, use `header` or `querystring` instead."
145148
)
149+
150+
agent_card_dict = requests.get(
151+
effective_url + AGENT_CARD_WELL_KNOWN_PATH,
152+
headers=req_headers,
153+
params=req_params,
154+
).json()
155+
# replace agent_card_url with actual host
156+
agent_card_dict["url"] = effective_url
157+
158+
agent_card_object = _convert_agent_card_dict_to_obj(agent_card_dict)
159+
160+
logger.debug(f"Agent card of {name}: {agent_card_object}")
161+
162+
client_was_provided = httpx_client is not None
163+
client_to_use = httpx_client
164+
165+
if client_was_provided:
166+
# If a client was provided, update it with auth info
167+
if auth_token:
168+
if auth_method == "header":
169+
client_to_use.headers.update(req_headers)
170+
elif auth_method == "querystring":
171+
new_params = dict(client_to_use.params)
172+
new_params.update(req_params)
173+
client_to_use.params = new_params
174+
else:
175+
# If no client was provided, create a new one with auth info
176+
if auth_token:
177+
if auth_method == "header":
178+
client_to_use = httpx.AsyncClient(
179+
base_url=effective_url, headers=req_headers, timeout=600
180+
)
181+
elif auth_method == "querystring":
182+
client_to_use = httpx.AsyncClient(
183+
base_url=effective_url, params=req_params, timeout=600
184+
)
185+
else: # No auth, no client provided
186+
client_to_use = httpx.AsyncClient(base_url=effective_url, timeout=600)
187+
188+
super().__init__(
189+
name=name, agent_card=agent_card_object, httpx_client=client_to_use
190+
)
191+
192+
# The parent class sets _httpx_client_needs_cleanup based on whether
193+
# the httpx_client it received was None. Since we always pass a
194+
# client (either the user's or one we create), it will always set
195+
# it to False. We must override this to ensure clients we create
196+
# are properly cleaned up.
197+
if not client_was_provided:
198+
self._httpx_client_needs_cleanup = True

0 commit comments

Comments
 (0)