Skip to content

Commit c61f77b

Browse files
committed
feat(api): Enhance API endpoint handling and testing capabilities
- Add comprehensive API testing example with manual and OpenAPI spec loading - Improve endpoint request handling in `make_request` method * Create fresh connector for each request attempt * Prevent "Session is closed" errors during retries - Refactor endpoint parameter processing to support more complex schemas - Add new example scripts demonstrating API integration: * `agent_with_apis.py`: Showcase API usage with JSONPlaceholder and PetStore APIs * `test_api_function_names.py` and `test_api_retry.py` - Enhance connector configuration and timeout management - Support more robust parameter type and constraint handling
1 parent a7fdafd commit c61f77b

File tree

4 files changed

+277
-5
lines changed

4 files changed

+277
-5
lines changed

agentle/agents/apis/endpoint.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ async def make_request(
359359
await self._auth_handler.refresh_if_needed()
360360
await self._auth_handler.apply_auth(None, url, headers, query_params) # type: ignore
361361

362-
# Prepare connector
362+
# Prepare connector kwargs (will be used to create fresh connector for each attempt)
363363
connector_kwargs: dict[str, Any] = {
364364
"limit": 10,
365365
"limit_per_host": 5,
@@ -369,8 +369,6 @@ async def make_request(
369369
if not self.request_config.verify_ssl:
370370
connector_kwargs["ssl"] = False
371371

372-
connector = aiohttp.TCPConnector(**connector_kwargs)
373-
374372
# Prepare timeout
375373
timeout = aiohttp.ClientTimeout(
376374
total=self.request_config.timeout,
@@ -381,6 +379,8 @@ async def make_request(
381379
# Define the request function for circuit breaker
382380
async def make_single_request() -> Any:
383381
"""Make a single request attempt."""
382+
# Create a fresh connector for each request attempt to avoid "Session is closed" errors on retries
383+
connector = aiohttp.TCPConnector(**connector_kwargs)
384384
async with aiohttp.ClientSession(
385385
connector=connector, timeout=timeout
386386
) as session:
@@ -569,11 +569,11 @@ async def endpoint_callable(**kwargs: Any) -> Any:
569569

570570
if hasattr(param, "enum") and param.enum:
571571
param_info["enum"] = list(param.enum)
572-
572+
573573
# Add constraints for number/primitive types
574574
if hasattr(param, "parameter_schema") and param.parameter_schema:
575575
from agentle.agents.apis.primitive_schema import PrimitiveSchema
576-
576+
577577
schema = param.parameter_schema
578578
# Only PrimitiveSchema has minimum, maximum, format
579579
if isinstance(schema, PrimitiveSchema):

examples/agent_with_apis.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
"""
2+
Test the API feature using JSONPlaceholder - a free fake REST API for testing.
3+
4+
This example demonstrates:
5+
1. Creating an API manually with endpoints
6+
2. Loading an API from an OpenAPI spec URL
7+
3. Using the API with an agent
8+
4. Making requests through the agent
9+
"""
10+
11+
from dotenv import load_dotenv
12+
13+
from agentle.agents.agent import Agent
14+
from agentle.agents.apis.api import API
15+
from agentle.agents.apis.endpoint import Endpoint
16+
from agentle.agents.apis.endpoint_parameter import EndpointParameter
17+
from agentle.agents.apis.http_method import HTTPMethod
18+
from agentle.agents.apis.parameter_location import ParameterLocation
19+
from agentle.agents.apis.primitive_schema import PrimitiveSchema
20+
from agentle.generations.providers.google.google_generation_provider import (
21+
GoogleGenerationProvider,
22+
)
23+
24+
load_dotenv(override=True)
25+
26+
27+
async def test_manual_api():
28+
"""Test creating an API manually."""
29+
print("\n" + "=" * 70)
30+
print("TEST 1: Manual API Creation")
31+
print("=" * 70)
32+
33+
# Create API manually
34+
api = API(
35+
name="JSONPlaceholder",
36+
description="Free fake REST API for testing and prototyping",
37+
base_url="https://jsonplaceholder.typicode.com",
38+
endpoints=[],
39+
)
40+
41+
# Test various path patterns to ensure function names are valid
42+
test_endpoints = [
43+
# Normal path
44+
Endpoint(
45+
name="get_posts",
46+
description="Get all posts",
47+
path="/posts",
48+
method=HTTPMethod.GET,
49+
),
50+
# Path with parameter
51+
Endpoint(
52+
name="get_post",
53+
description="Get a specific post by ID",
54+
path="/posts/{id}",
55+
method=HTTPMethod.GET,
56+
parameters=[
57+
EndpointParameter(
58+
name="id",
59+
description="Post ID",
60+
parameter_schema=PrimitiveSchema(type="integer"),
61+
location=ParameterLocation.PATH,
62+
required=True,
63+
)
64+
],
65+
),
66+
# Path with dashes (should be converted to underscores)
67+
Endpoint(
68+
name="get_user_posts",
69+
description="Get posts by user",
70+
path="/users/{userId}/posts",
71+
method=HTTPMethod.GET,
72+
parameters=[
73+
EndpointParameter(
74+
name="userId",
75+
description="User ID",
76+
parameter_schema=PrimitiveSchema(type="integer"),
77+
location=ParameterLocation.PATH,
78+
required=True,
79+
)
80+
],
81+
),
82+
]
83+
84+
for endpoint in test_endpoints:
85+
api.add_endpoint(endpoint)
86+
87+
# Create agent with the API
88+
agent = Agent(
89+
name="Blog Assistant",
90+
generation_provider=GoogleGenerationProvider(
91+
use_vertex_ai=True, project="unicortex", location="global"
92+
),
93+
model="gemini-2.5-flash",
94+
instructions="You are a helpful assistant that can fetch blog posts. When asked about posts, use the available tools.",
95+
apis=[api],
96+
)
97+
98+
# Test the agent
99+
result = agent.run("Get me post with ID 1")
100+
print(f"\n✅ Manual API test passed!")
101+
print(f"Response preview: {result.text[:100]}...")
102+
103+
return result
104+
105+
106+
async def test_openapi_spec():
107+
"""Test loading an API from an OpenAPI spec."""
108+
print("\n" + "=" * 70)
109+
print("TEST 2: OpenAPI Spec Loading")
110+
print("=" * 70)
111+
112+
try:
113+
# Load a real OpenAPI spec from a public API
114+
# Using PetStore API as it's a standard example
115+
api = await API.from_openapi_spec(
116+
"https://petstore3.swagger.io/api/v3/openapi.json",
117+
name="PetStore",
118+
base_url_override="https://petstore3.swagger.io/api/v3", # Override the relative base URL
119+
include_operations=["getPetById"], # Only include one simple operation
120+
)
121+
122+
print(f"✅ Loaded API: {api.name}")
123+
print(f" Base URL: {api.base_url}")
124+
print(f" Endpoints: {len(api.endpoints)}")
125+
126+
# Print endpoint names to verify they're valid
127+
for endpoint in api.endpoints:
128+
print(f" - {endpoint.name}: {endpoint.method.value} {endpoint.path}")
129+
130+
# Create agent with the API
131+
agent = Agent(
132+
name="Pet Store Assistant",
133+
generation_provider=GoogleGenerationProvider(
134+
use_vertex_ai=True, project="unicortex", location="global"
135+
),
136+
model="gemini-2.5-flash",
137+
instructions="You are a pet store assistant. You can look up pets by ID.",
138+
apis=[api],
139+
)
140+
141+
# Test the agent with a specific pet ID that should exist
142+
result = agent.run("Get information about pet with ID 1")
143+
print(f"\n✅ OpenAPI spec test passed!")
144+
print(f"Response preview: {result.text[:100]}...")
145+
146+
return result
147+
148+
except Exception as e:
149+
print(f"⚠️ OpenAPI spec test skipped: {e}")
150+
import traceback
151+
152+
traceback.print_exc()
153+
return None
154+
155+
156+
async def main():
157+
"""Run all tests."""
158+
print("\n🧪 Testing API Feature")
159+
print("=" * 70)
160+
161+
# Test 1: Manual API creation
162+
await test_manual_api()
163+
164+
# Test 2: OpenAPI spec loading
165+
await test_openapi_spec()
166+
167+
print("\n" + "=" * 70)
168+
print("✅ All API tests completed successfully!")
169+
print("=" * 70)
170+
171+
172+
if __name__ == "__main__":
173+
import asyncio
174+
175+
asyncio.run(main())

examples/test_api_function_names.py

Whitespace-only changes.

examples/test_api_retry.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
Test API retry mechanism to ensure the session management fix works correctly.
3+
"""
4+
5+
from dotenv import load_dotenv
6+
7+
from agentle.agents.agent import Agent
8+
from agentle.agents.apis.api import API
9+
from agentle.agents.apis.endpoint import Endpoint
10+
from agentle.agents.apis.endpoint_parameter import EndpointParameter
11+
from agentle.agents.apis.http_method import HTTPMethod
12+
from agentle.agents.apis.parameter_location import ParameterLocation
13+
from agentle.agents.apis.primitive_schema import PrimitiveSchema
14+
from agentle.agents.apis.request_config import RequestConfig
15+
from agentle.generations.providers.google.google_generation_provider import (
16+
GoogleGenerationProvider,
17+
)
18+
19+
load_dotenv(override=True)
20+
21+
22+
async def test_retry_mechanism():
23+
"""Test that retries work correctly with the session management fix."""
24+
print("\n" + "=" * 70)
25+
print("Testing API Retry Mechanism")
26+
print("=" * 70)
27+
28+
# Create an API with retry configuration
29+
api = API(
30+
name="JSONPlaceholder",
31+
description="Free fake REST API for testing",
32+
base_url="https://jsonplaceholder.typicode.com",
33+
request_config=RequestConfig(
34+
max_retries=3,
35+
retry_delay=0.5,
36+
timeout=5.0,
37+
enable_request_logging=True,
38+
),
39+
endpoints=[],
40+
)
41+
42+
# Add an endpoint that might fail (using an invalid endpoint to trigger retries)
43+
test_endpoint = Endpoint(
44+
name="get_post",
45+
description="Get a specific post by ID",
46+
path="/posts/{id}",
47+
method=HTTPMethod.GET,
48+
parameters=[
49+
EndpointParameter(
50+
name="id",
51+
description="Post ID",
52+
parameter_schema=PrimitiveSchema(type="integer"),
53+
location=ParameterLocation.PATH,
54+
required=True,
55+
)
56+
],
57+
request_config=RequestConfig(
58+
max_retries=2,
59+
retry_delay=0.3,
60+
timeout=10.0,
61+
),
62+
)
63+
64+
api.add_endpoint(test_endpoint)
65+
66+
# Create agent
67+
agent = Agent(
68+
name="Test Assistant",
69+
generation_provider=GoogleGenerationProvider(
70+
use_vertex_ai=True, project="unicortex", location="global"
71+
),
72+
model="gemini-2.5-flash",
73+
instructions="You are a test assistant. Fetch the requested post.",
74+
apis=[api],
75+
)
76+
77+
# Test with a valid request
78+
print("\n📝 Testing valid request...")
79+
result = agent.run("Get post with ID 5")
80+
print(f"✅ Valid request succeeded!")
81+
print(f" Response preview: {result.text[:80]}...")
82+
83+
# Test with another valid request to ensure session reuse works
84+
print("\n📝 Testing second request...")
85+
result2 = agent.run("Get post with ID 10")
86+
print(f"✅ Second request succeeded!")
87+
print(f" Response preview: {result2.text[:80]}...")
88+
89+
print("\n" + "=" * 70)
90+
print("✅ Retry mechanism test completed successfully!")
91+
print("=" * 70)
92+
93+
94+
if __name__ == "__main__":
95+
import asyncio
96+
97+
asyncio.run(test_retry_mechanism())

0 commit comments

Comments
 (0)