Skip to content

Commit 7b10157

Browse files
committed
feat(apis): new boolen type, recursive object schema serialization, request config and more
1 parent 3145d34 commit 7b10157

File tree

9 files changed

+277
-14
lines changed

9 files changed

+277
-14
lines changed

agentle/agents/apis/api.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,12 +271,15 @@ def _extract_auth_from_spec(
271271
elif scheme_type == "oauth2":
272272
flows = scheme.get("flows", {})
273273
if "clientCredentials" in flows:
274-
token_url = flows["clientCredentials"].get("tokenUrl")
274+
flow = flows["clientCredentials"]
275+
token_url = flow.get("tokenUrl")
276+
scopes = flow.get("scopes", {})
275277
if token_url:
276278
return AuthenticationConfig(
277279
type=AuthType.OAUTH2,
278280
oauth2_token_url=token_url,
279281
oauth2_grant_type=OAuth2GrantType.CLIENT_CREDENTIALS,
282+
oauth2_scopes=list(scopes.keys()) if scopes else None,
280283
)
281284

282285
return None
@@ -535,14 +538,19 @@ def _parse_openapi_paths(
535538
components,
536539
)
537540

538-
# Determine response format
541+
# Determine response format and extract response schema
539542
response_format = "json" # Default
543+
response_schema = None
540544
responses = operation.get("responses", {})
541545
if "200" in responses:
542546
response_200 = responses["200"]
543547
content = response_200.get("content", {})
544548
if "application/json" in content:
545549
response_format = "json"
550+
# Extract response schema if available
551+
json_content = content["application/json"]
552+
if "schema" in json_content:
553+
response_schema = json_content["schema"]
546554
elif "text/plain" in content:
547555
response_format = "text"
548556
elif "application/xml" in content:
@@ -555,6 +563,8 @@ def _parse_openapi_paths(
555563
method=HTTPMethod(method.upper()),
556564
parameters=endpoint_parameters,
557565
response_format=response_format, # type: ignore
566+
response_schema=response_schema,
567+
validate_response_schema=bool(response_schema),
558568
)
559569

560570
endpoints.append(endpoint)

agentle/agents/apis/authentication_config.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,14 @@ class AuthenticationConfig(BaseModel):
4343
oauth2_grant_type: OAuth2GrantType = Field(
4444
default=OAuth2GrantType.CLIENT_CREDENTIALS
4545
)
46-
oauth2_scope: str | None = Field(default=None)
46+
oauth2_scope: str | None = Field(
47+
default=None,
48+
description="Single scope string (deprecated, use oauth2_scopes for multiple)",
49+
)
50+
oauth2_scopes: list[str] | None = Field(
51+
default=None,
52+
description="List of OAuth2 scopes to request (e.g., ['read', 'write', 'admin'])",
53+
)
4754
oauth2_refresh_token: str | None = Field(default=None)
4855

4956
# HMAC
@@ -96,6 +103,7 @@ def create_handler(self) -> AuthenticationBase:
96103
self.oauth2_grant_type,
97104
self.oauth2_scope,
98105
self.oauth2_refresh_token,
106+
self.oauth2_scopes,
99107
)
100108

101109
elif self.type == AuthType.HMAC:

agentle/agents/apis/endpoint.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,8 @@
1818

1919
import asyncio
2020
import logging
21-
import mimetypes
2221
import random
23-
from collections.abc import AsyncIterator, Callable, MutableMapping, Sequence
22+
from collections.abc import AsyncIterator, MutableMapping, Sequence
2423
from typing import Any, Literal
2524

2625
import aiohttp
@@ -42,7 +41,6 @@
4241
from agentle.agents.apis.request_config import RequestConfig
4342
from agentle.agents.apis.response_cache import ResponseCache
4443
from agentle.agents.apis.retry_strategy import RetryStrategy
45-
from agentle.agents.apis.request_hook import RequestHook
4644
from agentle.generations.tools.tool import Tool
4745

4846
logger = logging.getLogger(__name__)
@@ -326,13 +324,22 @@ async def make_request(
326324
files[param_name] = value
327325
continue
328326

329-
# Place parameter in appropriate location
327+
# Place parameter in appropriate location with proper type handling
330328
if param.location == ParameterLocation.QUERY:
331-
query_params[param_name] = value
329+
# Handle boolean conversion for query params
330+
if isinstance(value, bool):
331+
# Convert Python bool to lowercase string for URL compatibility
332+
query_params[param_name] = str(value).lower()
333+
else:
334+
query_params[param_name] = value
332335
elif param.location == ParameterLocation.BODY:
333336
body_params[param_name] = value
334337
elif param.location == ParameterLocation.HEADER:
335-
header_params[param_name] = str(value)
338+
# Convert to string for headers
339+
if isinstance(value, bool):
340+
header_params[param_name] = str(value).lower()
341+
else:
342+
header_params[param_name] = str(value)
336343
elif param.location == ParameterLocation.PATH:
337344
path_params[param_name] = value
338345

@@ -547,8 +554,10 @@ async def endpoint_callable(**kwargs: Any) -> Any:
547554

548555
for param in self.parameters:
549556
if hasattr(param, "to_tool_parameter_schema"):
557+
# Use the parameter's own schema conversion method
550558
tool_parameters[param.name] = param.to_tool_parameter_schema()
551559
else:
560+
# Fallback for parameters without schema method
552561
param_info: dict[str, object] = {
553562
"type": getattr(param, "param_type", "string") or "string",
554563
"description": param.description,
@@ -560,6 +569,20 @@ async def endpoint_callable(**kwargs: Any) -> Any:
560569

561570
if hasattr(param, "enum") and param.enum:
562571
param_info["enum"] = list(param.enum)
572+
573+
# Add constraints for number/primitive types
574+
if hasattr(param, "parameter_schema") and param.parameter_schema:
575+
from agentle.agents.apis.primitive_schema import PrimitiveSchema
576+
577+
schema = param.parameter_schema
578+
# Only PrimitiveSchema has minimum, maximum, format
579+
if isinstance(schema, PrimitiveSchema):
580+
if schema.minimum is not None:
581+
param_info["minimum"] = schema.minimum
582+
if schema.maximum is not None:
583+
param_info["maximum"] = schema.maximum
584+
if schema.format:
585+
param_info["format"] = schema.format
563586

564587
tool_parameters[param.name] = param_info
565588

agentle/agents/apis/oauth2_authentication.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,15 @@ def __init__(
2323
grant_type: OAuth2GrantType = OAuth2GrantType.CLIENT_CREDENTIALS,
2424
scope: str | None = None,
2525
refresh_token: str | None = None,
26+
scopes: list[str] | None = None,
2627
):
2728
self.token_url = token_url
2829
self.client_id = client_id
2930
self.client_secret = client_secret
3031
self.grant_type = grant_type
32+
# Support both single scope and multiple scopes
33+
# If scopes list is provided, use it; otherwise fall back to single scope
34+
self.scopes = scopes
3135
self.scope = scope
3236
self.refresh_token_value = refresh_token
3337

@@ -76,7 +80,10 @@ async def _fetch_token(self) -> None:
7680
"grant_type": self.grant_type.value,
7781
}
7882

79-
if self.scope:
83+
# Handle scopes - prefer scopes list over single scope
84+
if self.scopes:
85+
data["scope"] = " ".join(self.scopes)
86+
elif self.scope:
8087
data["scope"] = self.scope
8188

8289
if (

agentle/agents/apis/object_schema.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from __future__ import annotations
99

1010
from collections.abc import Mapping, Sequence
11-
from typing import TYPE_CHECKING, Any, Literal
11+
from typing import TYPE_CHECKING, Any, Literal, cast
1212

1313
from rsb.models.base_model import BaseModel
1414
from rsb.models.field import Field
@@ -39,3 +39,88 @@ class ObjectSchema(BaseModel):
3939
example: Mapping[str, Any] | None = Field(
4040
default=None, description="Example value for the object"
4141
)
42+
43+
@classmethod
44+
def from_json_schema(
45+
cls, schema: Mapping[str, Any]
46+
) -> ObjectSchema | ArraySchema | PrimitiveSchema:
47+
"""
48+
Recursively convert a JSON Schema definition to Agentle schema types.
49+
50+
This method handles deeply nested objects, arrays, and primitives,
51+
making it easy to convert complex JSON Schema definitions.
52+
53+
Args:
54+
schema: JSON Schema definition (dict with 'type', 'properties', etc.)
55+
56+
Returns:
57+
Appropriate schema type (ObjectSchema, ArraySchema, or PrimitiveSchema)
58+
59+
Example:
60+
```python
61+
from agentle.agents.apis.object_schema import ObjectSchema
62+
63+
json_schema = {
64+
"type": "object",
65+
"properties": {
66+
"user": {
67+
"type": "object",
68+
"properties": {
69+
"name": {"type": "string"},
70+
"age": {"type": "integer"},
71+
"settings": {
72+
"type": "object",
73+
"properties": {
74+
"theme": {"type": "string"},
75+
"notifications": {"type": "boolean"}
76+
}
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
schema = ObjectSchema.from_json_schema(json_schema)
84+
```
85+
"""
86+
from agentle.agents.apis.array_schema import ArraySchema
87+
from agentle.agents.apis.primitive_schema import PrimitiveSchema
88+
89+
schema_type = schema.get("type", "string")
90+
91+
if schema_type == "object":
92+
properties: dict[str, ObjectSchema | ArraySchema | PrimitiveSchema] = {}
93+
for prop_name, prop_schema in schema.get("properties", {}).items():
94+
properties[prop_name] = cls.from_json_schema(prop_schema)
95+
96+
return cls(
97+
properties=properties,
98+
required=list(schema.get("required", [])),
99+
additional_properties=schema.get("additionalProperties", True),
100+
example=schema.get("example"),
101+
)
102+
103+
elif schema_type == "array":
104+
items_schema = schema.get("items", {"type": "string"})
105+
return ArraySchema(
106+
items=cls.from_json_schema(items_schema),
107+
min_items=schema.get("minItems"),
108+
max_items=schema.get("maxItems"),
109+
example=schema.get("example"),
110+
)
111+
112+
else:
113+
# Primitive type
114+
return PrimitiveSchema(
115+
type=cast(
116+
Literal["string", "integer", "boolean", "number"], schema_type
117+
)
118+
if schema_type in ["string", "integer", "number", "boolean"]
119+
else "string",
120+
format=schema.get("format"),
121+
enum=schema.get("enum"),
122+
minimum=schema.get("minimum"),
123+
maximum=schema.get("maximum"),
124+
pattern=schema.get("pattern"),
125+
example=schema.get("example"),
126+
)
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
from .array_param import array_param
2+
from .boolean_param import boolean_param
23
from .integer_param import integer_param
4+
from .number_param import number_param
35
from .object_param import object_param
46
from .string_param import string_param
57

6-
__all__: list[str] = ["array_param", "integer_param", "object_param", "string_param"]
8+
__all__: list[str] = [
9+
"array_param",
10+
"boolean_param",
11+
"integer_param",
12+
"number_param",
13+
"object_param",
14+
"string_param",
15+
]
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from agentle.agents.apis.endpoint_parameter import EndpointParameter
2+
from agentle.agents.apis.parameter_location import ParameterLocation
3+
from agentle.agents.apis.primitive_schema import PrimitiveSchema
4+
5+
6+
def boolean_param(
7+
name: str,
8+
description: str,
9+
required: bool = False,
10+
default: bool | None = None,
11+
location: ParameterLocation = ParameterLocation.QUERY,
12+
) -> EndpointParameter:
13+
"""Create a boolean parameter.
14+
15+
Args:
16+
name: Parameter name
17+
description: Parameter description
18+
required: Whether the parameter is required
19+
default: Default value for the parameter
20+
location: Where the parameter should be placed in the request
21+
22+
Returns:
23+
EndpointParameter configured for boolean values
24+
25+
Example:
26+
```python
27+
from agentle.agents.apis.params.boolean_param import boolean_param
28+
29+
boolean_param(
30+
name="enabled",
31+
description="Enable feature",
32+
required=False,
33+
default=True
34+
)
35+
```
36+
"""
37+
return EndpointParameter(
38+
name=name,
39+
description=description,
40+
parameter_schema=PrimitiveSchema(type="boolean"),
41+
location=location,
42+
required=required,
43+
default=default,
44+
)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from agentle.agents.apis.endpoint_parameter import EndpointParameter
2+
from agentle.agents.apis.parameter_location import ParameterLocation
3+
from agentle.agents.apis.primitive_schema import PrimitiveSchema
4+
5+
6+
def number_param(
7+
name: str,
8+
description: str,
9+
required: bool = False,
10+
minimum: float | None = None,
11+
maximum: float | None = None,
12+
default: float | None = None,
13+
location: ParameterLocation = ParameterLocation.QUERY,
14+
format: str | None = None,
15+
) -> EndpointParameter:
16+
"""Create a number (float/decimal) parameter.
17+
18+
Args:
19+
name: Parameter name
20+
description: Parameter description
21+
required: Whether the parameter is required
22+
minimum: Minimum allowed value
23+
maximum: Maximum allowed value
24+
default: Default value for the parameter
25+
location: Where the parameter should be placed in the request
26+
format: Format hint (e.g., 'float', 'double', 'decimal')
27+
28+
Returns:
29+
EndpointParameter configured for number values
30+
31+
Example:
32+
```python
33+
from agentle.agents.apis.params.number_param import number_param
34+
35+
number_param(
36+
name="price",
37+
description="Product price",
38+
required=True,
39+
minimum=0.0,
40+
default=99.99
41+
)
42+
```
43+
"""
44+
return EndpointParameter(
45+
name=name,
46+
description=description,
47+
parameter_schema=PrimitiveSchema(
48+
type="number",
49+
minimum=minimum,
50+
maximum=maximum,
51+
format=format,
52+
),
53+
location=location,
54+
required=required,
55+
default=default,
56+
)

0 commit comments

Comments
 (0)