agentle 0.9.4__py3-none-any.whl → 0.9.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentle/agents/agent.py +175 -10
- agentle/agents/agent_run_output.py +8 -1
- agentle/agents/apis/__init__.py +79 -6
- agentle/agents/apis/api.py +342 -73
- agentle/agents/apis/api_key_authentication.py +43 -0
- agentle/agents/apis/api_key_location.py +11 -0
- agentle/agents/apis/api_metrics.py +16 -0
- agentle/agents/apis/auth_type.py +17 -0
- agentle/agents/apis/authentication.py +32 -0
- agentle/agents/apis/authentication_base.py +42 -0
- agentle/agents/apis/authentication_config.py +117 -0
- agentle/agents/apis/basic_authentication.py +34 -0
- agentle/agents/apis/bearer_authentication.py +52 -0
- agentle/agents/apis/cache_strategy.py +12 -0
- agentle/agents/apis/circuit_breaker.py +69 -0
- agentle/agents/apis/circuit_breaker_error.py +7 -0
- agentle/agents/apis/circuit_breaker_state.py +11 -0
- agentle/agents/apis/endpoint.py +413 -254
- agentle/agents/apis/file_upload.py +23 -0
- agentle/agents/apis/hmac_authentication.py +56 -0
- agentle/agents/apis/no_authentication.py +27 -0
- agentle/agents/apis/oauth2_authentication.py +111 -0
- agentle/agents/apis/oauth2_grant_type.py +12 -0
- agentle/agents/apis/object_schema.py +86 -1
- agentle/agents/apis/params/__init__.py +10 -1
- agentle/agents/apis/params/boolean_param.py +44 -0
- agentle/agents/apis/params/number_param.py +56 -0
- agentle/agents/apis/rate_limit_error.py +7 -0
- agentle/agents/apis/rate_limiter.py +57 -0
- agentle/agents/apis/request_config.py +126 -4
- agentle/agents/apis/request_hook.py +16 -0
- agentle/agents/apis/response_cache.py +49 -0
- agentle/agents/apis/retry_strategy.py +12 -0
- agentle/agents/whatsapp/human_delay_calculator.py +462 -0
- agentle/agents/whatsapp/models/audio_message.py +6 -4
- agentle/agents/whatsapp/models/key.py +2 -2
- agentle/agents/whatsapp/models/whatsapp_bot_config.py +375 -21
- agentle/agents/whatsapp/models/whatsapp_response_base.py +31 -0
- agentle/agents/whatsapp/models/whatsapp_webhook_payload.py +5 -1
- agentle/agents/whatsapp/providers/base/whatsapp_provider.py +51 -0
- agentle/agents/whatsapp/providers/evolution/evolution_api_provider.py +237 -10
- agentle/agents/whatsapp/providers/meta/meta_whatsapp_provider.py +126 -0
- agentle/agents/whatsapp/v2/batch_processor_manager.py +4 -0
- agentle/agents/whatsapp/v2/bot_config.py +188 -0
- agentle/agents/whatsapp/v2/message_limit.py +9 -0
- agentle/agents/whatsapp/v2/payload.py +0 -0
- agentle/agents/whatsapp/v2/whatsapp_bot.py +13 -0
- agentle/agents/whatsapp/v2/whatsapp_cloud_api_provider.py +0 -0
- agentle/agents/whatsapp/v2/whatsapp_provider.py +0 -0
- agentle/agents/whatsapp/whatsapp_bot.py +827 -45
- agentle/generations/providers/google/adapters/generate_generate_content_response_to_generation_adapter.py +13 -10
- agentle/generations/providers/google/google_generation_provider.py +35 -5
- agentle/generations/providers/openrouter/_adapters/openrouter_message_to_generated_assistant_message_adapter.py +35 -1
- agentle/mcp/servers/stdio_mcp_server.py +23 -4
- agentle/parsing/parsers/docx.py +8 -0
- agentle/parsing/parsers/file_parser.py +4 -0
- agentle/parsing/parsers/pdf.py +7 -1
- agentle/storage/__init__.py +11 -0
- agentle/storage/file_storage_manager.py +44 -0
- agentle/storage/local_file_storage_manager.py +122 -0
- agentle/storage/s3_file_storage_manager.py +124 -0
- agentle/tts/audio_format.py +6 -0
- agentle/tts/elevenlabs_tts_provider.py +108 -0
- agentle/tts/output_format_type.py +26 -0
- agentle/tts/speech_config.py +14 -0
- agentle/tts/speech_result.py +15 -0
- agentle/tts/tts_provider.py +16 -0
- agentle/tts/voice_settings.py +30 -0
- agentle/utils/parse_streaming_json.py +39 -13
- agentle/voice_cloning/__init__.py +0 -0
- agentle/voice_cloning/voice_cloner.py +0 -0
- agentle/web/extractor.py +282 -148
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/METADATA +1 -1
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/RECORD +78 -39
- agentle/tts/real_time/definitions/audio_data.py +0 -20
- agentle/tts/real_time/definitions/speech_config.py +0 -27
- agentle/tts/real_time/definitions/speech_result.py +0 -14
- agentle/tts/real_time/definitions/tts_stream_chunk.py +0 -15
- agentle/tts/real_time/definitions/voice_gender.py +0 -9
- agentle/tts/real_time/definitions/voice_info.py +0 -18
- agentle/tts/real_time/real_time_speech_to_text_provider.py +0 -66
- /agentle/{tts/real_time → agents/whatsapp/v2}/__init__.py +0 -0
- /agentle/{tts/real_time/definitions/__init__.py → agents/whatsapp/v2/in_memory_batch_processor_manager.py} +0 -0
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/WHEEL +0 -0
- {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""File upload support for endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import mimetypes
|
|
6
|
+
from rsb.models.base_model import BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FileUpload(BaseModel):
|
|
10
|
+
"""Represents a file to be uploaded."""
|
|
11
|
+
|
|
12
|
+
filename: str
|
|
13
|
+
content: bytes
|
|
14
|
+
mime_type: str | None = None
|
|
15
|
+
|
|
16
|
+
def to_form_part(self) -> tuple[str, bytes, str]:
|
|
17
|
+
"""Convert to multipart form part."""
|
|
18
|
+
mime = (
|
|
19
|
+
self.mime_type
|
|
20
|
+
or mimetypes.guess_type(self.filename)[0]
|
|
21
|
+
or "application/octet-stream"
|
|
22
|
+
)
|
|
23
|
+
return (self.filename, self.content, mime)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""HMAC signature authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import MutableMapping
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
from agentle.agents.apis.authentication_base import AuthenticationBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HMACAuthentication(AuthenticationBase):
|
|
16
|
+
"""HMAC signature authentication."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
secret_key: str,
|
|
21
|
+
algorithm: str = "sha256",
|
|
22
|
+
header_name: str = "X-Signature",
|
|
23
|
+
include_timestamp: bool = True,
|
|
24
|
+
):
|
|
25
|
+
self.secret_key = secret_key
|
|
26
|
+
self.algorithm = algorithm
|
|
27
|
+
self.header_name = header_name
|
|
28
|
+
self.include_timestamp = include_timestamp
|
|
29
|
+
|
|
30
|
+
async def apply_auth(
|
|
31
|
+
self,
|
|
32
|
+
session: aiohttp.ClientSession,
|
|
33
|
+
url: str,
|
|
34
|
+
headers: MutableMapping[str, str],
|
|
35
|
+
params: MutableMapping[str, Any],
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Add HMAC signature to headers."""
|
|
38
|
+
# Build signature string
|
|
39
|
+
timestamp = str(int(time.time()))
|
|
40
|
+
signature_string = url
|
|
41
|
+
|
|
42
|
+
if self.include_timestamp:
|
|
43
|
+
signature_string = f"{timestamp}:{signature_string}"
|
|
44
|
+
headers["X-Timestamp"] = timestamp
|
|
45
|
+
|
|
46
|
+
# Calculate HMAC
|
|
47
|
+
hash_func = getattr(hashlib, self.algorithm)
|
|
48
|
+
signature = hmac.new(
|
|
49
|
+
self.secret_key.encode(), signature_string.encode(), hash_func
|
|
50
|
+
).hexdigest()
|
|
51
|
+
|
|
52
|
+
headers[self.header_name] = signature
|
|
53
|
+
|
|
54
|
+
async def refresh_if_needed(self) -> bool:
|
|
55
|
+
"""No refresh needed for HMAC."""
|
|
56
|
+
return False
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""No authentication handler."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import MutableMapping
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
from agentle.agents.apis.authentication_base import AuthenticationBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NoAuthentication(AuthenticationBase):
|
|
13
|
+
"""No authentication."""
|
|
14
|
+
|
|
15
|
+
async def apply_auth(
|
|
16
|
+
self,
|
|
17
|
+
session: aiohttp.ClientSession,
|
|
18
|
+
url: str,
|
|
19
|
+
headers: MutableMapping[str, str],
|
|
20
|
+
params: MutableMapping[str, Any],
|
|
21
|
+
) -> None:
|
|
22
|
+
"""No authentication to apply."""
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
async def refresh_if_needed(self) -> bool:
|
|
26
|
+
"""No refresh needed."""
|
|
27
|
+
return False
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""OAuth2 authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import MutableMapping
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
from agentle.agents.apis.authentication_base import AuthenticationBase
|
|
12
|
+
from agentle.agents.apis.oauth2_grant_type import OAuth2GrantType
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OAuth2Authentication(AuthenticationBase):
|
|
16
|
+
"""OAuth2 authentication with token refresh."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
token_url: str,
|
|
21
|
+
client_id: str,
|
|
22
|
+
client_secret: str,
|
|
23
|
+
grant_type: OAuth2GrantType = OAuth2GrantType.CLIENT_CREDENTIALS,
|
|
24
|
+
scope: str | None = None,
|
|
25
|
+
refresh_token: str | None = None,
|
|
26
|
+
scopes: list[str] | None = None,
|
|
27
|
+
):
|
|
28
|
+
self.token_url = token_url
|
|
29
|
+
self.client_id = client_id
|
|
30
|
+
self.client_secret = client_secret
|
|
31
|
+
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
|
|
35
|
+
self.scope = scope
|
|
36
|
+
self.refresh_token_value = refresh_token
|
|
37
|
+
|
|
38
|
+
self.access_token: str | None = None
|
|
39
|
+
self.token_expiry: datetime | None = None
|
|
40
|
+
self._refresh_lock = asyncio.Lock()
|
|
41
|
+
|
|
42
|
+
async def apply_auth(
|
|
43
|
+
self,
|
|
44
|
+
session: aiohttp.ClientSession,
|
|
45
|
+
url: str,
|
|
46
|
+
headers: MutableMapping[str, str],
|
|
47
|
+
params: MutableMapping[str, Any],
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Add OAuth2 token to Authorization header."""
|
|
50
|
+
# Ensure we have a valid token
|
|
51
|
+
await self.refresh_if_needed()
|
|
52
|
+
|
|
53
|
+
if self.access_token:
|
|
54
|
+
headers["Authorization"] = f"Bearer {self.access_token}"
|
|
55
|
+
|
|
56
|
+
async def refresh_if_needed(self) -> bool:
|
|
57
|
+
"""Refresh token if expired or missing."""
|
|
58
|
+
# Check if token needs refresh
|
|
59
|
+
if self.access_token and self.token_expiry:
|
|
60
|
+
# Add 60 second buffer before expiry
|
|
61
|
+
if datetime.now() < self.token_expiry - timedelta(seconds=60):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# Use lock to prevent concurrent refreshes
|
|
65
|
+
async with self._refresh_lock:
|
|
66
|
+
# Double-check after acquiring lock
|
|
67
|
+
if self.access_token and self.token_expiry:
|
|
68
|
+
if datetime.now() < self.token_expiry - timedelta(seconds=60):
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
# Refresh the token
|
|
72
|
+
await self._fetch_token()
|
|
73
|
+
return True
|
|
74
|
+
|
|
75
|
+
async def _fetch_token(self) -> None:
|
|
76
|
+
"""Fetch a new access token."""
|
|
77
|
+
data: dict[str, str] = {
|
|
78
|
+
"client_id": self.client_id,
|
|
79
|
+
"client_secret": self.client_secret,
|
|
80
|
+
"grant_type": self.grant_type.value,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# Handle scopes - prefer scopes list over single scope
|
|
84
|
+
if self.scopes:
|
|
85
|
+
data["scope"] = " ".join(self.scopes)
|
|
86
|
+
elif self.scope:
|
|
87
|
+
data["scope"] = self.scope
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
self.grant_type == OAuth2GrantType.REFRESH_TOKEN
|
|
91
|
+
and self.refresh_token_value
|
|
92
|
+
):
|
|
93
|
+
data["refresh_token"] = self.refresh_token_value
|
|
94
|
+
|
|
95
|
+
async with aiohttp.ClientSession() as session:
|
|
96
|
+
async with session.post(self.token_url, data=data) as response:
|
|
97
|
+
if response.status == 200:
|
|
98
|
+
token_data = await response.json()
|
|
99
|
+
self.access_token = token_data["access_token"]
|
|
100
|
+
|
|
101
|
+
# Calculate expiry
|
|
102
|
+
expires_in = token_data.get("expires_in", 3600)
|
|
103
|
+
self.token_expiry = datetime.now() + timedelta(seconds=expires_in)
|
|
104
|
+
|
|
105
|
+
# Update refresh token if provided
|
|
106
|
+
if "refresh_token" in token_data:
|
|
107
|
+
self.refresh_token_value = token_data["refresh_token"]
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Failed to fetch OAuth2 token: HTTP {response.status}"
|
|
111
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""OAuth2 grant types."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class OAuth2GrantType(StrEnum):
|
|
7
|
+
"""OAuth2 grant types."""
|
|
8
|
+
|
|
9
|
+
CLIENT_CREDENTIALS = "client_credentials"
|
|
10
|
+
AUTHORIZATION_CODE = "authorization_code"
|
|
11
|
+
REFRESH_TOKEN = "refresh_token"
|
|
12
|
+
PASSWORD = "password"
|
|
@@ -8,7 +8,7 @@ Simply replace the existing EndpointParameter and related classes with these imp
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
from collections.abc import Mapping, Sequence
|
|
11
|
-
from typing import TYPE_CHECKING, Any, Literal
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Literal, cast
|
|
12
12
|
|
|
13
13
|
from rsb.models.base_model import BaseModel
|
|
14
14
|
from rsb.models.field import Field
|
|
@@ -39,3 +39,88 @@ class ObjectSchema(BaseModel):
|
|
|
39
39
|
example: Mapping[str, Any] | None = Field(
|
|
40
40
|
default=None, description="Example value for the object"
|
|
41
41
|
)
|
|
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
|
+
)
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
from .array_param import array_param
|
|
2
|
+
from .boolean_param import boolean_param
|
|
2
3
|
from .integer_param import integer_param
|
|
4
|
+
from .number_param import number_param
|
|
3
5
|
from .object_param import object_param
|
|
4
6
|
from .string_param import string_param
|
|
5
7
|
|
|
6
|
-
__all__: list[str] = [
|
|
8
|
+
__all__: list[str] = [
|
|
9
|
+
"array_param",
|
|
10
|
+
"boolean_param",
|
|
11
|
+
"integer_param",
|
|
12
|
+
"number_param",
|
|
13
|
+
"object_param",
|
|
14
|
+
"string_param",
|
|
15
|
+
]
|
|
@@ -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
|
+
)
|
|
@@ -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,0 +1,57 @@
|
|
|
1
|
+
"""Rate limiter for API calls.
|
|
2
|
+
|
|
3
|
+
This module adapts the resilience module's rate limiter implementations
|
|
4
|
+
for use in the APIs module, maintaining backward compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
|
|
11
|
+
from agentle.agents.apis.request_config import RequestConfig
|
|
12
|
+
from agentle.resilience.rate_limiting.rate_limit_config import RateLimitConfig
|
|
13
|
+
from agentle.resilience.rate_limiting.in_memory_rate_limiter import (
|
|
14
|
+
InMemoryRateLimiter as ResilienceRateLimiter,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class RateLimiter:
|
|
19
|
+
"""
|
|
20
|
+
Rate limiter for API calls.
|
|
21
|
+
|
|
22
|
+
This wraps the resilience module's InMemoryRateLimiter to provide
|
|
23
|
+
a simpler acquire-based API for endpoint usage.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: RequestConfig):
|
|
27
|
+
self.config = config
|
|
28
|
+
self._identifier = "default" # Single rate limit per endpoint
|
|
29
|
+
|
|
30
|
+
# Convert rate limit config to resilience module format
|
|
31
|
+
rate_limit_config: RateLimitConfig = {
|
|
32
|
+
"max_requests_per_minute": int(
|
|
33
|
+
config.rate_limit_calls * (60 / config.rate_limit_period)
|
|
34
|
+
)
|
|
35
|
+
if config.rate_limit_period <= 60
|
|
36
|
+
else config.rate_limit_calls,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Initialize the underlying rate limiter from resilience module
|
|
40
|
+
self._impl = ResilienceRateLimiter(
|
|
41
|
+
default_config=rate_limit_config,
|
|
42
|
+
enable_metrics=config.enable_metrics,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
async def acquire(self) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Acquire rate limit slot, waiting if necessary.
|
|
48
|
+
|
|
49
|
+
This will block until a slot is available.
|
|
50
|
+
"""
|
|
51
|
+
# Wait until we can proceed
|
|
52
|
+
while not await self._impl.can_proceed(self._identifier):
|
|
53
|
+
# Wait a short time before checking again
|
|
54
|
+
await asyncio.sleep(0.1)
|
|
55
|
+
|
|
56
|
+
# Record the request
|
|
57
|
+
await self._impl.record_request(self._identifier)
|
|
@@ -1,20 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced request configuration with advanced features.
|
|
3
|
+
|
|
4
|
+
Includes timeouts, retries, circuit breakers, rate limiting, caching, and more.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
|
|
1
11
|
from rsb.models.base_model import BaseModel
|
|
2
12
|
from rsb.models.field import Field
|
|
3
13
|
|
|
14
|
+
from agentle.agents.apis.cache_strategy import CacheStrategy
|
|
15
|
+
from agentle.agents.apis.retry_strategy import RetryStrategy
|
|
16
|
+
|
|
4
17
|
|
|
5
18
|
class RequestConfig(BaseModel):
|
|
6
|
-
"""
|
|
19
|
+
"""
|
|
20
|
+
Enhanced configuration for HTTP requests.
|
|
7
21
|
|
|
8
|
-
|
|
22
|
+
This configuration can be set at both API-level and per-endpoint level.
|
|
23
|
+
Endpoint-level configs override API-level configs.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
```python
|
|
27
|
+
# API-level config (applies to all endpoints)
|
|
28
|
+
api_config = RequestConfig(
|
|
29
|
+
timeout=30.0,
|
|
30
|
+
max_retries=3,
|
|
31
|
+
enable_caching=True
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Per-endpoint override
|
|
35
|
+
endpoint_config = RequestConfig(
|
|
36
|
+
timeout=60.0, # Override for this specific endpoint
|
|
37
|
+
max_retries=5,
|
|
38
|
+
cache_ttl=600.0
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Timeouts (in seconds)
|
|
44
|
+
timeout: float = Field(description="Total request timeout in seconds", default=30.0)
|
|
45
|
+
connect_timeout: float | None = Field(
|
|
46
|
+
description="Connection timeout in seconds", default=None
|
|
47
|
+
)
|
|
48
|
+
read_timeout: float | None = Field(
|
|
49
|
+
description="Read timeout in seconds", default=None
|
|
50
|
+
)
|
|
9
51
|
|
|
52
|
+
# Retry configuration
|
|
10
53
|
max_retries: int = Field(
|
|
11
54
|
description="Maximum number of retries for failed requests", default=3
|
|
12
55
|
)
|
|
13
|
-
|
|
14
56
|
retry_delay: float = Field(
|
|
15
|
-
description="
|
|
57
|
+
description="Base delay between retries in seconds", default=1.0
|
|
58
|
+
)
|
|
59
|
+
retry_strategy: RetryStrategy = Field(
|
|
60
|
+
description="Strategy for calculating retry delays",
|
|
61
|
+
default=RetryStrategy.EXPONENTIAL,
|
|
62
|
+
)
|
|
63
|
+
retry_on_status_codes: Sequence[int] = Field(
|
|
64
|
+
description="HTTP status codes that should trigger retries",
|
|
65
|
+
default_factory=lambda: [408, 429, 500, 502, 503, 504],
|
|
66
|
+
)
|
|
67
|
+
retry_on_exceptions: bool = Field(
|
|
68
|
+
description="Whether to retry on network exceptions", default=True
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Circuit breaker configuration
|
|
72
|
+
enable_circuit_breaker: bool = Field(
|
|
73
|
+
description="Enable circuit breaker pattern", default=False
|
|
74
|
+
)
|
|
75
|
+
circuit_breaker_failure_threshold: int = Field(
|
|
76
|
+
description="Number of failures before opening circuit", default=5
|
|
77
|
+
)
|
|
78
|
+
circuit_breaker_recovery_timeout: float = Field(
|
|
79
|
+
description="Seconds to wait before attempting recovery", default=60.0
|
|
80
|
+
)
|
|
81
|
+
circuit_breaker_success_threshold: int = Field(
|
|
82
|
+
description="Number of successes in half-open state to close circuit", default=2
|
|
16
83
|
)
|
|
17
84
|
|
|
85
|
+
# Rate limiting
|
|
86
|
+
enable_rate_limiting: bool = Field(
|
|
87
|
+
description="Enable rate limiting", default=False
|
|
88
|
+
)
|
|
89
|
+
rate_limit_calls: int = Field(
|
|
90
|
+
description="Maximum number of calls per period", default=100
|
|
91
|
+
)
|
|
92
|
+
rate_limit_period: float = Field(
|
|
93
|
+
description="Rate limit period in seconds", default=60.0
|
|
94
|
+
)
|
|
95
|
+
respect_retry_after: bool = Field(
|
|
96
|
+
description="Respect Retry-After header from server", default=True
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Caching
|
|
100
|
+
enable_caching: bool = Field(description="Enable response caching", default=False)
|
|
101
|
+
cache_strategy: CacheStrategy = Field(
|
|
102
|
+
description="Caching strategy to use", default=CacheStrategy.MEMORY
|
|
103
|
+
)
|
|
104
|
+
cache_ttl: float = Field(description="Cache TTL in seconds", default=300.0)
|
|
105
|
+
cache_only_get: bool = Field(description="Only cache GET requests", default=True)
|
|
106
|
+
|
|
107
|
+
# Request/Response hooks
|
|
108
|
+
enable_request_logging: bool = Field(
|
|
109
|
+
description="Enable request logging", default=False
|
|
110
|
+
)
|
|
111
|
+
enable_response_logging: bool = Field(
|
|
112
|
+
description="Enable response logging", default=False
|
|
113
|
+
)
|
|
114
|
+
enable_metrics: bool = Field(description="Enable metrics collection", default=False)
|
|
115
|
+
|
|
116
|
+
# Connection settings
|
|
18
117
|
follow_redirects: bool = Field(
|
|
19
118
|
description="Whether to follow HTTP redirects", default=True
|
|
20
119
|
)
|
|
120
|
+
max_redirects: int = Field(
|
|
121
|
+
description="Maximum number of redirects to follow", default=10
|
|
122
|
+
)
|
|
123
|
+
verify_ssl: bool = Field(description="Verify SSL certificates", default=True)
|
|
124
|
+
ssl_cert_path: str | None = Field(
|
|
125
|
+
description="Path to custom SSL certificate", default=None
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Proxy configuration
|
|
129
|
+
proxy_url: str | None = Field(
|
|
130
|
+
description="Proxy URL (http://host:port)", default=None
|
|
131
|
+
)
|
|
132
|
+
proxy_auth: tuple[str, str] | None = Field(
|
|
133
|
+
description="Proxy authentication (username, password)", default=None
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Content handling
|
|
137
|
+
compress_request: bool = Field(
|
|
138
|
+
description="Enable request compression", default=False
|
|
139
|
+
)
|
|
140
|
+
decompress_response: bool = Field(
|
|
141
|
+
description="Enable response decompression", default=True
|
|
142
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Request hooks for endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from rsb.models.base_model import BaseModel
|
|
9
|
+
from rsb.models.field import Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RequestHook(BaseModel):
|
|
13
|
+
"""Hook for request/response interception."""
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
callback: Callable[[dict[str, Any]], Any] | None = None
|