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
agentle/agents/apis/api.py
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Complete enhanced API module with comprehensive OpenAPI support.
|
|
3
|
+
|
|
4
|
+
Provides advanced features for managing collections of related endpoints with:
|
|
5
|
+
- Full OpenAPI 3.0/3.1 and Swagger 2.0 support
|
|
6
|
+
- Shared authentication across endpoints
|
|
7
|
+
- Request/response interceptors
|
|
8
|
+
- API-level rate limiting and circuit breaking
|
|
9
|
+
- Batch request support
|
|
10
|
+
- GraphQL support
|
|
11
|
+
- And more...
|
|
12
|
+
"""
|
|
13
|
+
|
|
1
14
|
from __future__ import annotations
|
|
2
15
|
|
|
3
|
-
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
from collections.abc import (
|
|
19
|
+
Coroutine,
|
|
20
|
+
Mapping,
|
|
21
|
+
MutableMapping,
|
|
22
|
+
MutableSequence,
|
|
23
|
+
Sequence,
|
|
24
|
+
)
|
|
4
25
|
from pathlib import Path
|
|
5
26
|
from typing import Any, Literal, cast
|
|
6
27
|
|
|
@@ -10,22 +31,32 @@ import yaml
|
|
|
10
31
|
from rsb.models.base_model import BaseModel
|
|
11
32
|
from rsb.models.field import Field
|
|
12
33
|
|
|
34
|
+
from agentle.agents.apis.api_metrics import APIMetrics
|
|
13
35
|
from agentle.agents.apis.array_schema import ArraySchema
|
|
36
|
+
from agentle.agents.apis.authentication import (
|
|
37
|
+
ApiKeyLocation,
|
|
38
|
+
AuthType,
|
|
39
|
+
AuthenticationConfig,
|
|
40
|
+
OAuth2GrantType,
|
|
41
|
+
)
|
|
14
42
|
from agentle.agents.apis.endpoint import Endpoint
|
|
15
43
|
from agentle.agents.apis.endpoint_parameter import EndpointParameter
|
|
44
|
+
from agentle.agents.apis.http_method import HTTPMethod
|
|
16
45
|
from agentle.agents.apis.object_schema import ObjectSchema
|
|
46
|
+
from agentle.agents.apis.parameter_location import ParameterLocation
|
|
17
47
|
from agentle.agents.apis.primitive_schema import PrimitiveSchema
|
|
18
48
|
from agentle.agents.apis.request_config import RequestConfig
|
|
19
49
|
from agentle.generations.tools.tool import Tool
|
|
20
50
|
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
21
53
|
|
|
22
54
|
class API(BaseModel):
|
|
23
55
|
"""
|
|
24
|
-
|
|
56
|
+
Enhanced API collection with comprehensive features.
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
authentication
|
|
28
|
-
way to define complete APIs that can be used by agents.
|
|
58
|
+
Represents a collection of related API endpoints with shared configuration,
|
|
59
|
+
authentication, rate limiting, and monitoring capabilities.
|
|
29
60
|
"""
|
|
30
61
|
|
|
31
62
|
name: str = Field(description="Name of the API")
|
|
@@ -46,10 +77,59 @@ class API(BaseModel):
|
|
|
46
77
|
default_factory=RequestConfig,
|
|
47
78
|
)
|
|
48
79
|
|
|
80
|
+
auth_config: AuthenticationConfig | None = Field(
|
|
81
|
+
description="Authentication configuration for the API",
|
|
82
|
+
default=None,
|
|
83
|
+
)
|
|
84
|
+
|
|
49
85
|
endpoints: Sequence[Endpoint] = Field(
|
|
50
86
|
description="List of endpoints in this API", default_factory=list
|
|
51
87
|
)
|
|
52
88
|
|
|
89
|
+
# API-level features
|
|
90
|
+
enable_batch_requests: bool = Field(
|
|
91
|
+
description="Enable batch request support", default=False
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
enable_graphql: bool = Field(description="Enable GraphQL support", default=False)
|
|
95
|
+
|
|
96
|
+
graphql_endpoint: str | None = Field(
|
|
97
|
+
description="GraphQL endpoint path", default=None
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Monitoring
|
|
101
|
+
enable_metrics: bool = Field(
|
|
102
|
+
description="Enable API metrics collection", default=False
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# API version
|
|
106
|
+
version: str = Field(description="API version", default="1.0.0")
|
|
107
|
+
|
|
108
|
+
# OpenAPI spec reference
|
|
109
|
+
openapi_spec_url: str | None = Field(
|
|
110
|
+
description="URL to OpenAPI specification", default=None
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Internal state
|
|
114
|
+
_metrics: APIMetrics | None = None
|
|
115
|
+
|
|
116
|
+
def model_post_init(self, __context: Any) -> None:
|
|
117
|
+
"""Initialize API components."""
|
|
118
|
+
super().model_post_init(__context)
|
|
119
|
+
|
|
120
|
+
if self.enable_metrics:
|
|
121
|
+
self._metrics = APIMetrics()
|
|
122
|
+
|
|
123
|
+
# Apply API-level config to endpoints that don't have their own
|
|
124
|
+
for endpoint in self.endpoints:
|
|
125
|
+
# Inherit auth config if endpoint doesn't have one
|
|
126
|
+
if not endpoint.auth_config and self.auth_config:
|
|
127
|
+
endpoint.auth_config = self.auth_config
|
|
128
|
+
|
|
129
|
+
# Inherit request config settings
|
|
130
|
+
if endpoint.request_config == RequestConfig():
|
|
131
|
+
endpoint.request_config = self.request_config
|
|
132
|
+
|
|
53
133
|
@classmethod
|
|
54
134
|
async def from_openapi_spec(
|
|
55
135
|
cls,
|
|
@@ -60,42 +140,30 @@ class API(BaseModel):
|
|
|
60
140
|
base_url_override: str | None = None,
|
|
61
141
|
headers: MutableMapping[str, str] | None = None,
|
|
62
142
|
request_config: RequestConfig | None = None,
|
|
143
|
+
auth_config: AuthenticationConfig | None = None,
|
|
63
144
|
include_operations: Sequence[str] | None = None,
|
|
64
145
|
exclude_operations: Sequence[str] | None = None,
|
|
146
|
+
include_tags: Sequence[str] | None = None,
|
|
147
|
+
exclude_tags: Sequence[str] | None = None,
|
|
65
148
|
) -> API:
|
|
66
149
|
"""
|
|
67
150
|
Create an API instance from an OpenAPI specification.
|
|
68
151
|
|
|
69
152
|
Args:
|
|
70
153
|
spec: OpenAPI specification as dict, file path, or URL
|
|
71
|
-
name: Override the API name
|
|
72
|
-
description: Override the API description
|
|
73
|
-
base_url_override: Override the base URL
|
|
74
|
-
headers: Additional headers
|
|
75
|
-
request_config: Request configuration
|
|
76
|
-
|
|
154
|
+
name: Override the API name
|
|
155
|
+
description: Override the API description
|
|
156
|
+
base_url_override: Override the base URL
|
|
157
|
+
headers: Additional headers
|
|
158
|
+
request_config: Request configuration
|
|
159
|
+
auth_config: Authentication configuration
|
|
160
|
+
include_operations: List of operationIds to include
|
|
77
161
|
exclude_operations: List of operationIds to exclude
|
|
162
|
+
include_tags: List of tags to include
|
|
163
|
+
exclude_tags: List of tags to exclude
|
|
78
164
|
|
|
79
165
|
Returns:
|
|
80
166
|
API instance configured from the OpenAPI spec
|
|
81
|
-
|
|
82
|
-
Example:
|
|
83
|
-
```python
|
|
84
|
-
# From URL
|
|
85
|
-
api = await API.from_openapi_spec("https://petstore.swagger.io/v2/swagger.json")
|
|
86
|
-
|
|
87
|
-
# From local file
|
|
88
|
-
api = await API.from_openapi_spec(Path("./api-spec.yaml"))
|
|
89
|
-
|
|
90
|
-
# From dict with custom settings
|
|
91
|
-
api = await API.from_openapi_spec(
|
|
92
|
-
spec_dict,
|
|
93
|
-
name="Custom Pet Store",
|
|
94
|
-
base_url_override="https://api.example.com",
|
|
95
|
-
headers={"Authorization": "Bearer token"},
|
|
96
|
-
include_operations=["getPetById", "updatePet"]
|
|
97
|
-
)
|
|
98
|
-
```
|
|
99
167
|
"""
|
|
100
168
|
# Load the OpenAPI spec
|
|
101
169
|
spec_dict = await cls._load_openapi_spec(spec)
|
|
@@ -107,10 +175,13 @@ class API(BaseModel):
|
|
|
107
175
|
"Invalid OpenAPI specification: missing 'openapi' or 'swagger' field"
|
|
108
176
|
)
|
|
109
177
|
|
|
178
|
+
logger.info(f"Loading OpenAPI spec version: {openapi_version}")
|
|
179
|
+
|
|
110
180
|
# Extract API info
|
|
111
181
|
info = spec_dict.get("info", {})
|
|
112
182
|
api_name = name or info.get("title", "Generated API")
|
|
113
183
|
api_description = description or info.get("description")
|
|
184
|
+
api_version = info.get("version", "1.0.0")
|
|
114
185
|
|
|
115
186
|
# Extract base URL
|
|
116
187
|
if base_url_override:
|
|
@@ -126,55 +197,216 @@ class API(BaseModel):
|
|
|
126
197
|
base_path = spec_dict.get("basePath", "")
|
|
127
198
|
api_base_url = f"{schemes[0]}://{host}{base_path}"
|
|
128
199
|
|
|
200
|
+
# Extract authentication from OpenAPI spec if not provided
|
|
201
|
+
if not auth_config:
|
|
202
|
+
auth_config = cls._extract_auth_from_spec(spec_dict)
|
|
203
|
+
|
|
129
204
|
# Parse endpoints from paths
|
|
130
205
|
endpoints = cls._parse_openapi_paths(
|
|
131
206
|
spec_dict,
|
|
132
207
|
include_operations=include_operations,
|
|
133
208
|
exclude_operations=exclude_operations,
|
|
209
|
+
include_tags=include_tags,
|
|
210
|
+
exclude_tags=exclude_tags,
|
|
134
211
|
)
|
|
135
212
|
|
|
213
|
+
logger.info(f"Loaded {len(endpoints)} endpoints from OpenAPI spec")
|
|
214
|
+
|
|
136
215
|
return cls(
|
|
137
216
|
name=api_name,
|
|
138
217
|
description=api_description,
|
|
139
218
|
base_url=api_base_url,
|
|
140
219
|
headers=headers or {},
|
|
141
220
|
request_config=request_config or RequestConfig(),
|
|
221
|
+
auth_config=auth_config,
|
|
142
222
|
endpoints=endpoints,
|
|
223
|
+
version=api_version,
|
|
143
224
|
)
|
|
144
225
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
226
|
+
@classmethod
|
|
227
|
+
def _extract_auth_from_spec(
|
|
228
|
+
cls, spec_dict: Mapping[str, Any]
|
|
229
|
+
) -> AuthenticationConfig | None:
|
|
230
|
+
"""Extract authentication configuration from OpenAPI spec."""
|
|
231
|
+
# OpenAPI 3.x security schemes
|
|
232
|
+
components = spec_dict.get("components", {})
|
|
233
|
+
security_schemes = components.get("securitySchemes", {})
|
|
148
234
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
235
|
+
# OpenAPI 2.x security definitions
|
|
236
|
+
if not security_schemes:
|
|
237
|
+
security_schemes = spec_dict.get("securityDefinitions", {})
|
|
238
|
+
|
|
239
|
+
if not security_schemes:
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
# Get the first security scheme (simplified - real implementation would handle multiple)
|
|
243
|
+
scheme_name, scheme = next(iter(security_schemes.items()))
|
|
244
|
+
scheme_type = scheme.get("type", "").lower()
|
|
245
|
+
|
|
246
|
+
logger.debug(f"Detected security scheme: {scheme_name} ({scheme_type})")
|
|
247
|
+
|
|
248
|
+
if scheme_type == "http":
|
|
249
|
+
http_scheme = scheme.get("scheme", "").lower()
|
|
250
|
+
if http_scheme == "bearer":
|
|
251
|
+
return AuthenticationConfig(type=AuthType.BEARER)
|
|
252
|
+
elif http_scheme == "basic":
|
|
253
|
+
return AuthenticationConfig(type=AuthType.BASIC)
|
|
254
|
+
|
|
255
|
+
elif scheme_type == "apikey":
|
|
256
|
+
location = scheme.get("in", "header")
|
|
257
|
+
name = scheme.get("name", "X-API-Key")
|
|
258
|
+
|
|
259
|
+
if location == "header":
|
|
260
|
+
return AuthenticationConfig(
|
|
261
|
+
type=AuthType.API_KEY,
|
|
262
|
+
api_key_location=ApiKeyLocation.HEADER,
|
|
263
|
+
api_key_name=name,
|
|
264
|
+
)
|
|
265
|
+
elif location == "query":
|
|
266
|
+
return AuthenticationConfig(
|
|
267
|
+
type=AuthType.API_KEY,
|
|
268
|
+
api_key_location=ApiKeyLocation.QUERY,
|
|
269
|
+
api_key_name=name,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
elif scheme_type == "oauth2":
|
|
273
|
+
flows = scheme.get("flows", {})
|
|
274
|
+
if "clientCredentials" in flows:
|
|
275
|
+
flow = flows["clientCredentials"]
|
|
276
|
+
token_url = flow.get("tokenUrl")
|
|
277
|
+
scopes = flow.get("scopes", {})
|
|
278
|
+
if token_url:
|
|
279
|
+
return AuthenticationConfig(
|
|
280
|
+
type=AuthType.OAUTH2,
|
|
281
|
+
oauth2_token_url=token_url,
|
|
282
|
+
oauth2_grant_type=OAuth2GrantType.CLIENT_CREDENTIALS,
|
|
283
|
+
oauth2_scopes=list(scopes.keys()) if scopes else None,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
return None
|
|
287
|
+
|
|
288
|
+
def add_endpoint(self, endpoint: Endpoint) -> None:
|
|
289
|
+
"""Add an endpoint to this API."""
|
|
152
290
|
if not isinstance(self.endpoints, list):
|
|
153
291
|
self.endpoints = list(self.endpoints)
|
|
292
|
+
|
|
293
|
+
# Apply API-level configs
|
|
294
|
+
if not endpoint.auth_config and self.auth_config:
|
|
295
|
+
endpoint.auth_config = self.auth_config
|
|
296
|
+
|
|
297
|
+
if endpoint.request_config == RequestConfig():
|
|
298
|
+
endpoint.request_config = self.request_config
|
|
299
|
+
|
|
154
300
|
self.endpoints.append(endpoint)
|
|
301
|
+
logger.debug(f"Added endpoint '{endpoint.name}' to API '{self.name}'")
|
|
155
302
|
|
|
156
303
|
def get_endpoint(self, name: str) -> Endpoint | None:
|
|
304
|
+
"""Get an endpoint by name."""
|
|
305
|
+
for endpoint in self.endpoints:
|
|
306
|
+
if endpoint.name == name:
|
|
307
|
+
return endpoint
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
def get_endpoints_by_tag(self, tag: str) -> Sequence[Endpoint]:
|
|
311
|
+
"""Get all endpoints with a specific tag."""
|
|
312
|
+
# This would require adding tags to Endpoint model
|
|
313
|
+
# For now, return empty list
|
|
314
|
+
return []
|
|
315
|
+
|
|
316
|
+
async def batch_request(
|
|
317
|
+
self,
|
|
318
|
+
requests: Sequence[tuple[str, dict[str, Any]]],
|
|
319
|
+
) -> Sequence[Any]:
|
|
157
320
|
"""
|
|
158
|
-
|
|
321
|
+
Execute multiple requests in batch.
|
|
159
322
|
|
|
160
323
|
Args:
|
|
161
|
-
|
|
324
|
+
requests: List of (endpoint_name, kwargs) tuples
|
|
162
325
|
|
|
163
326
|
Returns:
|
|
164
|
-
|
|
327
|
+
List of results in the same order as requests
|
|
165
328
|
"""
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
329
|
+
if not self.enable_batch_requests:
|
|
330
|
+
raise ValueError("Batch requests not enabled for this API")
|
|
331
|
+
|
|
332
|
+
import asyncio
|
|
333
|
+
|
|
334
|
+
tasks: list[Coroutine[None, None, Any]] = []
|
|
335
|
+
for endpoint_name, kwargs in requests:
|
|
336
|
+
endpoint = self.get_endpoint(endpoint_name)
|
|
337
|
+
if not endpoint:
|
|
338
|
+
raise ValueError(f"Endpoint '{endpoint_name}' not found")
|
|
339
|
+
|
|
340
|
+
# Create task for this request
|
|
341
|
+
task = endpoint.make_request(
|
|
342
|
+
base_url=self.base_url,
|
|
343
|
+
global_headers=self.headers,
|
|
344
|
+
**kwargs,
|
|
345
|
+
)
|
|
346
|
+
tasks.append(task)
|
|
347
|
+
|
|
348
|
+
# Execute all requests concurrently
|
|
349
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
350
|
+
return results
|
|
351
|
+
|
|
352
|
+
async def graphql_query(
|
|
353
|
+
self,
|
|
354
|
+
query: str,
|
|
355
|
+
variables: dict[str, Any] | None = None,
|
|
356
|
+
operation_name: str | None = None,
|
|
357
|
+
) -> Any:
|
|
172
358
|
"""
|
|
173
|
-
|
|
359
|
+
Execute a GraphQL query.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
query: GraphQL query string
|
|
363
|
+
variables: Query variables
|
|
364
|
+
operation_name: Operation name
|
|
174
365
|
|
|
175
366
|
Returns:
|
|
176
|
-
|
|
367
|
+
Query result
|
|
177
368
|
"""
|
|
369
|
+
if not self.enable_graphql:
|
|
370
|
+
raise ValueError("GraphQL not enabled for this API")
|
|
371
|
+
|
|
372
|
+
if not self.graphql_endpoint:
|
|
373
|
+
raise ValueError("GraphQL endpoint not configured")
|
|
374
|
+
|
|
375
|
+
# Build GraphQL request
|
|
376
|
+
payload: dict[str, Any] = {"query": query}
|
|
377
|
+
if variables:
|
|
378
|
+
payload["variables"] = variables
|
|
379
|
+
if operation_name:
|
|
380
|
+
payload["operationName"] = operation_name
|
|
381
|
+
|
|
382
|
+
# Make request
|
|
383
|
+
url = f"{self.base_url.rstrip('/')}/{self.graphql_endpoint.lstrip('/')}"
|
|
384
|
+
|
|
385
|
+
async with aiohttp.ClientSession() as session:
|
|
386
|
+
async with session.post(
|
|
387
|
+
url,
|
|
388
|
+
json=payload,
|
|
389
|
+
headers=self.headers,
|
|
390
|
+
) as response:
|
|
391
|
+
if response.status == 200:
|
|
392
|
+
result = await response.json()
|
|
393
|
+
if "errors" in result:
|
|
394
|
+
raise ValueError(f"GraphQL errors: {result['errors']}")
|
|
395
|
+
return result.get("data")
|
|
396
|
+
else:
|
|
397
|
+
raise ValueError(f"GraphQL request failed: HTTP {response.status}")
|
|
398
|
+
|
|
399
|
+
def get_metrics(self) -> APIMetrics | None:
|
|
400
|
+
"""Get API usage metrics."""
|
|
401
|
+
return self._metrics
|
|
402
|
+
|
|
403
|
+
def reset_metrics(self) -> None:
|
|
404
|
+
"""Reset API metrics."""
|
|
405
|
+
if self._metrics:
|
|
406
|
+
self._metrics = APIMetrics()
|
|
407
|
+
|
|
408
|
+
def to_tools(self) -> Sequence[Tool[Any]]:
|
|
409
|
+
"""Convert all endpoints to Tool instances."""
|
|
178
410
|
tools: list[Tool[Any]] = []
|
|
179
411
|
|
|
180
412
|
for endpoint in self.endpoints:
|
|
@@ -182,15 +414,12 @@ class API(BaseModel):
|
|
|
182
414
|
merged_headers = dict(self.headers)
|
|
183
415
|
merged_headers.update(endpoint.headers)
|
|
184
416
|
|
|
185
|
-
# Use endpoint's request config or fall back to API's
|
|
186
|
-
if endpoint.request_config == RequestConfig():
|
|
187
|
-
endpoint.request_config = self.request_config
|
|
188
|
-
|
|
189
417
|
tool = endpoint.to_tool(
|
|
190
418
|
base_url=self.base_url, global_headers=merged_headers
|
|
191
419
|
)
|
|
192
420
|
tools.append(tool)
|
|
193
421
|
|
|
422
|
+
logger.info(f"Created {len(tools)} tools from API '{self.name}'")
|
|
194
423
|
return tools
|
|
195
424
|
|
|
196
425
|
@classmethod
|
|
@@ -208,6 +437,7 @@ class API(BaseModel):
|
|
|
208
437
|
if isinstance(spec, str) and (
|
|
209
438
|
spec.startswith("http://") or spec.startswith("https://")
|
|
210
439
|
):
|
|
440
|
+
logger.info(f"Fetching OpenAPI spec from URL: {spec}")
|
|
211
441
|
async with aiohttp.ClientSession() as session:
|
|
212
442
|
async with session.get(spec) as response:
|
|
213
443
|
if response.status != 200:
|
|
@@ -226,6 +456,7 @@ class API(BaseModel):
|
|
|
226
456
|
if not spec_path.exists():
|
|
227
457
|
raise FileNotFoundError(f"OpenAPI spec file not found: {spec_path}")
|
|
228
458
|
|
|
459
|
+
logger.info(f"Loading OpenAPI spec from file: {spec_path}")
|
|
229
460
|
content = spec_path.read_text()
|
|
230
461
|
if spec_path.suffix.lower() in [".yaml", ".yml"]:
|
|
231
462
|
return yaml.safe_load(content)
|
|
@@ -242,11 +473,10 @@ class API(BaseModel):
|
|
|
242
473
|
spec_dict: Mapping[str, Any],
|
|
243
474
|
include_operations: Sequence[str] | None = None,
|
|
244
475
|
exclude_operations: Sequence[str] | None = None,
|
|
476
|
+
include_tags: Sequence[str] | None = None,
|
|
477
|
+
exclude_tags: Sequence[str] | None = None,
|
|
245
478
|
) -> Sequence[Endpoint]:
|
|
246
479
|
"""Parse OpenAPI paths into Endpoint instances."""
|
|
247
|
-
from agentle.agents.apis.endpoint import Endpoint
|
|
248
|
-
from agentle.agents.apis.http_method import HTTPMethod
|
|
249
|
-
|
|
250
480
|
endpoints: MutableSequence[Endpoint] = []
|
|
251
481
|
paths: Mapping[str, Any] = spec_dict.get("paths", {})
|
|
252
482
|
components = spec_dict.get("components", {})
|
|
@@ -267,21 +497,47 @@ class API(BaseModel):
|
|
|
267
497
|
continue
|
|
268
498
|
|
|
269
499
|
operation_id = operation.get("operationId")
|
|
500
|
+
operation_tags = operation.get("tags", [])
|
|
270
501
|
|
|
271
|
-
# Apply
|
|
502
|
+
# Apply operation filters
|
|
272
503
|
if include_operations and operation_id not in include_operations:
|
|
273
504
|
continue
|
|
274
505
|
if exclude_operations and operation_id in exclude_operations:
|
|
275
506
|
continue
|
|
276
507
|
|
|
508
|
+
# Apply tag filters
|
|
509
|
+
if include_tags and not any(
|
|
510
|
+
tag in include_tags for tag in operation_tags
|
|
511
|
+
):
|
|
512
|
+
continue
|
|
513
|
+
if exclude_tags and any(tag in exclude_tags for tag in operation_tags):
|
|
514
|
+
continue
|
|
515
|
+
|
|
277
516
|
# Create endpoint
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
517
|
+
# Generate a valid function name from the path
|
|
518
|
+
if operation_id:
|
|
519
|
+
endpoint_name = operation_id
|
|
520
|
+
else:
|
|
521
|
+
# Clean the path to create a valid function name
|
|
522
|
+
# Remove leading/trailing slashes and replace special chars
|
|
523
|
+
clean_path = (
|
|
524
|
+
path.strip("/")
|
|
525
|
+
.replace("/", "_")
|
|
526
|
+
.replace("{", "")
|
|
527
|
+
.replace("}", "")
|
|
528
|
+
.replace("-", "_")
|
|
529
|
+
)
|
|
530
|
+
# Remove any consecutive underscores
|
|
531
|
+
clean_path = re.sub(r"_+", "_", clean_path)
|
|
532
|
+
# Ensure it doesn't start with a number
|
|
533
|
+
if clean_path and clean_path[0].isdigit():
|
|
534
|
+
clean_path = f"n{clean_path}"
|
|
535
|
+
# If empty after cleaning, use a generic name
|
|
536
|
+
if not clean_path:
|
|
537
|
+
clean_path = "root"
|
|
538
|
+
endpoint_name = f"{method.lower()}_{clean_path}"
|
|
539
|
+
|
|
540
|
+
endpoint_name = cast(str, endpoint_name)
|
|
285
541
|
|
|
286
542
|
endpoint_description: str = cast(
|
|
287
543
|
str,
|
|
@@ -300,12 +556,33 @@ class API(BaseModel):
|
|
|
300
556
|
components,
|
|
301
557
|
)
|
|
302
558
|
|
|
559
|
+
# Determine response format and extract response schema
|
|
560
|
+
response_format = "json" # Default
|
|
561
|
+
response_schema = None
|
|
562
|
+
responses = operation.get("responses", {})
|
|
563
|
+
if "200" in responses:
|
|
564
|
+
response_200 = responses["200"]
|
|
565
|
+
content = response_200.get("content", {})
|
|
566
|
+
if "application/json" in content:
|
|
567
|
+
response_format = "json"
|
|
568
|
+
# Extract response schema if available
|
|
569
|
+
json_content = content["application/json"]
|
|
570
|
+
if "schema" in json_content:
|
|
571
|
+
response_schema = json_content["schema"]
|
|
572
|
+
elif "text/plain" in content:
|
|
573
|
+
response_format = "text"
|
|
574
|
+
elif "application/xml" in content:
|
|
575
|
+
response_format = "xml"
|
|
576
|
+
|
|
303
577
|
endpoint = Endpoint(
|
|
304
578
|
name=endpoint_name,
|
|
305
579
|
description=endpoint_description,
|
|
306
580
|
path=path,
|
|
307
581
|
method=HTTPMethod(method.upper()),
|
|
308
582
|
parameters=endpoint_parameters,
|
|
583
|
+
response_format=response_format, # type: ignore
|
|
584
|
+
response_schema=response_schema,
|
|
585
|
+
validate_response_schema=bool(response_schema),
|
|
309
586
|
)
|
|
310
587
|
|
|
311
588
|
endpoints.append(endpoint)
|
|
@@ -320,15 +597,12 @@ class API(BaseModel):
|
|
|
320
597
|
components: Mapping[str, Any],
|
|
321
598
|
) -> Sequence[EndpointParameter]:
|
|
322
599
|
"""Parse OpenAPI parameters into EndpointParameter instances."""
|
|
323
|
-
from agentle.agents.apis.endpoint_parameter import EndpointParameter
|
|
324
|
-
from agentle.agents.apis.parameter_location import ParameterLocation
|
|
325
|
-
|
|
326
600
|
endpoint_params: MutableSequence[EndpointParameter] = []
|
|
327
601
|
|
|
328
602
|
# Process standard parameters
|
|
329
603
|
for param in parameters:
|
|
330
604
|
if "$ref" in param:
|
|
331
|
-
# Resolve reference
|
|
605
|
+
# Resolve reference
|
|
332
606
|
ref_path = param["$ref"].split("/")
|
|
333
607
|
if len(ref_path) >= 4 and ref_path[1] == "components":
|
|
334
608
|
param = components.get(ref_path[2], {}).get(ref_path[3], {})
|
|
@@ -343,7 +617,7 @@ class API(BaseModel):
|
|
|
343
617
|
"query": ParameterLocation.QUERY,
|
|
344
618
|
"header": ParameterLocation.HEADER,
|
|
345
619
|
"path": ParameterLocation.PATH,
|
|
346
|
-
"cookie": ParameterLocation.HEADER,
|
|
620
|
+
"cookie": ParameterLocation.HEADER,
|
|
347
621
|
}
|
|
348
622
|
param_location = location_map.get(param_in, ParameterLocation.QUERY)
|
|
349
623
|
|
|
@@ -366,23 +640,22 @@ class API(BaseModel):
|
|
|
366
640
|
if request_body:
|
|
367
641
|
content = request_body.get("content", {})
|
|
368
642
|
|
|
369
|
-
# Look for JSON content first
|
|
643
|
+
# Look for JSON content first
|
|
370
644
|
schema = None
|
|
371
645
|
for content_type in [
|
|
372
646
|
"application/json",
|
|
373
647
|
"application/x-www-form-urlencoded",
|
|
648
|
+
"multipart/form-data",
|
|
374
649
|
]:
|
|
375
650
|
if content_type in content:
|
|
376
651
|
schema = content[content_type].get("schema", {})
|
|
377
652
|
break
|
|
378
653
|
|
|
379
654
|
if not schema and content:
|
|
380
|
-
# Take the first available content type
|
|
381
655
|
first_content = next(iter(content.values()))
|
|
382
656
|
schema = first_content.get("schema", {})
|
|
383
657
|
|
|
384
658
|
if schema:
|
|
385
|
-
# For request body, create a single parameter representing the body
|
|
386
659
|
body_param = EndpointParameter(
|
|
387
660
|
name="requestBody",
|
|
388
661
|
description=request_body.get("description", "Request body"),
|
|
@@ -401,10 +674,6 @@ class API(BaseModel):
|
|
|
401
674
|
components: Mapping[str, Any],
|
|
402
675
|
) -> PrimitiveSchema | ObjectSchema | ArraySchema:
|
|
403
676
|
"""Parse OpenAPI schema into our schema types."""
|
|
404
|
-
from agentle.agents.apis.array_schema import ArraySchema
|
|
405
|
-
from agentle.agents.apis.object_schema import ObjectSchema
|
|
406
|
-
from agentle.agents.apis.primitive_schema import PrimitiveSchema
|
|
407
|
-
|
|
408
677
|
# Handle references
|
|
409
678
|
if "$ref" in schema:
|
|
410
679
|
ref_path = schema["$ref"].split("/")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""API key authentication."""
|
|
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.api_key_location import ApiKeyLocation
|
|
10
|
+
from agentle.agents.apis.authentication_base import AuthenticationBase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiKeyAuthentication(AuthenticationBase):
|
|
14
|
+
"""API Key authentication."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
api_key: str,
|
|
19
|
+
location: ApiKeyLocation = ApiKeyLocation.HEADER,
|
|
20
|
+
key_name: str = "X-API-Key",
|
|
21
|
+
):
|
|
22
|
+
self.api_key = api_key
|
|
23
|
+
self.location = location
|
|
24
|
+
self.key_name = key_name
|
|
25
|
+
|
|
26
|
+
async def apply_auth(
|
|
27
|
+
self,
|
|
28
|
+
session: aiohttp.ClientSession,
|
|
29
|
+
url: str,
|
|
30
|
+
headers: MutableMapping[str, str],
|
|
31
|
+
params: MutableMapping[str, Any],
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Add API key to the appropriate location."""
|
|
34
|
+
if self.location == ApiKeyLocation.HEADER:
|
|
35
|
+
headers[self.key_name] = self.api_key
|
|
36
|
+
elif self.location == ApiKeyLocation.QUERY:
|
|
37
|
+
params[self.key_name] = self.api_key
|
|
38
|
+
elif self.location == ApiKeyLocation.COOKIE:
|
|
39
|
+
headers["Cookie"] = f"{self.key_name}={self.api_key}"
|
|
40
|
+
|
|
41
|
+
async def refresh_if_needed(self) -> bool:
|
|
42
|
+
"""No refresh needed for API key."""
|
|
43
|
+
return False
|