feat: add invoke module with discover-first flow (v0.12.0)#45
Conversation
New core/invoke.py consolidates agent invocation logic into a single source of truth. CLI and MCP server now delegate here instead of duplicating protocol payloads and HTTP handling. Discover-first pattern: callers provide domain+name instead of raw endpoint URLs. DNS discovery resolves the agent, agent card prefetch validates the endpoint and retrieves metadata, then the call proceeds through SDK (with telemetry) or raw httpx fallback. Key fixes: hardcoded 30s future.result() timeout in _run_async, empty error strings from SDK exceptions, missing type guards on A2A response parsing, unhandled SDK exceptions in invoke path. Signed-off-by: Igor Racic <iracic82@gmail.com>
| from dns_aid.sdk import AgentClient, SDKConfig | ||
|
|
||
| _sdk_available = True | ||
| except ImportError: |
Check notice
Code scanning / CodeQL
Empty except Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 7 days ago
In general, to fix empty except blocks you should either remove them (letting exceptions propagate) or add explicit handling: logging, fallback behavior, or at least a clear comment explaining why ignoring the exception is safe. For optional imports, the usual fix is to leave the fallback behavior intact but document it and optionally emit a low-level log.
For this specific code, the best fix without changing existing functionality is:
- Keep the current behavior where a failed import simply means
_sdk_availableremainsFalse. - Add a short explanatory comment in the
except ImportError:block to document that the SDK is optional. - Optionally add a
logger.debugcall to record that the SDK could not be imported, without being noisy at higher log levels.
This requires only editing the except ImportError: block around lines 39–44 in src/dns_aid/core/invoke.py. No new imports or other definitions are needed because logger is already defined earlier in the file.
| @@ -41,7 +41,8 @@ | ||
|
|
||
| _sdk_available = True | ||
| except ImportError: | ||
| pass | ||
| # SDK is optional; fall back to raw HTTP client without telemetry. | ||
| logger.debug("dns_aid.sdk not available; telemetry will be disabled.") | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- |
| the timeout passed to the underlying network call so the | ||
| thread pool does not abandon a still-in-flight request. | ||
| """ | ||
| import concurrent.futures |
Check notice
Code scanning / CodeQL
Module is imported with 'import' and 'import from' Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 7 days ago
In general, to fix “Module is imported with both import and from ... import ...,” consolidate imports so that either the module or its members are imported consistently. Here, we already use from concurrent.futures import ThreadPoolExecutor at the top of the file, and inside _run_async we only need concurrent.futures.TimeoutError. The best fix is to import TimeoutError at the top along with ThreadPoolExecutor, then remove the inner import concurrent.futures and update the exception handler to use TimeoutError directly.
Concretely:
- Edit
src/dns_aid/mcp/server.pynear line 32 to changefrom concurrent.futures import ThreadPoolExecutorintofrom concurrent.futures import ThreadPoolExecutor, TimeoutError. - Inside
_run_async, remove the localimport concurrent.futures. - Change
except concurrent.futures.TimeoutError:toexcept TimeoutError:so it uses the newly imported name.
This does not alter functionality: the same exception class is caught, just referenced via a different imported name.
| @@ -29,7 +29,7 @@ | ||
| import logging | ||
| import sys | ||
| import time | ||
| from concurrent.futures import ThreadPoolExecutor | ||
| from concurrent.futures import ThreadPoolExecutor, TimeoutError | ||
| from typing import Literal | ||
|
|
||
| # Configure logging BEFORE importing any dns_aid modules to ensure | ||
| @@ -135,7 +135,6 @@ | ||
| the timeout passed to the underlying network call so the | ||
| thread pool does not abandon a still-in-flight request. | ||
| """ | ||
| import concurrent.futures | ||
|
|
||
| try: | ||
| loop = asyncio.get_running_loop() | ||
| @@ -148,7 +147,7 @@ | ||
| future = executor.submit(asyncio.run, coro) | ||
| try: | ||
| return future.result(timeout=timeout) | ||
| except concurrent.futures.TimeoutError: | ||
| except TimeoutError: | ||
| raise TimeoutError(f"Operation did not complete within {timeout:.0f}s") from None | ||
| else: | ||
| return asyncio.run(coro) |
Summary
core/invoke.pymodule — single source of truth for agent invocation (A2A + MCP). Eliminates 3-way protocol logic duplication across CLI, MCP server, and SDK.send_a2a_message()acceptsdomain+nameinstead of raw endpoint URLs. Resolution: DNS discovery → agent card prefetch → invoke./.well-known/agent-card.jsonfor canonical endpoint URL and metadata. Host mismatch protection prevents internal URL leakage (e.g., Bedrock AgentCore runtime URLs)._run_async()thread pool wrapper killed requests after 30s regardless of configured timeout. Now passes actual timeout with headroom.Changes
Code
src/dns_aid/core/invoke.py— NEW:send_a2a_message(),call_mcp_tool(),list_mcp_tools(),resolve_a2a_endpoint(),InvokeResult, with structlog observabilitysrc/dns_aid/mcp/server.py— Simplified: tools delegate to invoke.py; fixed_run_asynctimeout; added executor cleanup via atexit;send_a2a_messageMCP tool acceptsdomain/namesrc/dns_aid/cli/main.py— Simplified: commands delegate to invoke.py;messagecommand accepts--domain/--namesrc/dns_aid/__init__.py— Version bump 0.11.0 → 0.12.0pyproject.toml— Version bumpCITATION.cff— Version bumpTests
tests/unit/core/test_invoke.py— NEW: 28 tests covering all public functions, utility helpers, raw invocation paths, and edge casestests/unit/test_mcp_server.py— Updated imports for moved functionsDocumentation
CHANGELOG.md— v0.12.0 entry (Added, Changed, Fixed)README.md— Added Python SDK section with discover-first examplesdocs/api-reference.md— CLI agent communication commands, invoke module API referencedocs/architecture.md— Invocation layer architecture sectiondocs/getting-started.md— High-level invoke API tutorial, updated MCP Claude Desktop examplesdocs/diagrams/invocation-flow.excalidraw.json— Invocation flow + before/after architecture diagramTest plan
uv run pytest tests/unit/ -x -q)uv run ruff check src/)uv run ruff format --check src/)uv run mypy src/dns_aid/)dns-aid message --domain ai.infoblox.com --name security-analyzer "hello"— success via discover-firstsend_a2a_message(domain="ai.infoblox.com", name="security-analyzer")— success with 36s latency, full security analysis returnedcall_agent_tool(endpoint="...", tool_name="search_flights")— success in 1.6s