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.
Files changed (85) hide show
  1. agentle/agents/agent.py +175 -10
  2. agentle/agents/agent_run_output.py +8 -1
  3. agentle/agents/apis/__init__.py +79 -6
  4. agentle/agents/apis/api.py +342 -73
  5. agentle/agents/apis/api_key_authentication.py +43 -0
  6. agentle/agents/apis/api_key_location.py +11 -0
  7. agentle/agents/apis/api_metrics.py +16 -0
  8. agentle/agents/apis/auth_type.py +17 -0
  9. agentle/agents/apis/authentication.py +32 -0
  10. agentle/agents/apis/authentication_base.py +42 -0
  11. agentle/agents/apis/authentication_config.py +117 -0
  12. agentle/agents/apis/basic_authentication.py +34 -0
  13. agentle/agents/apis/bearer_authentication.py +52 -0
  14. agentle/agents/apis/cache_strategy.py +12 -0
  15. agentle/agents/apis/circuit_breaker.py +69 -0
  16. agentle/agents/apis/circuit_breaker_error.py +7 -0
  17. agentle/agents/apis/circuit_breaker_state.py +11 -0
  18. agentle/agents/apis/endpoint.py +413 -254
  19. agentle/agents/apis/file_upload.py +23 -0
  20. agentle/agents/apis/hmac_authentication.py +56 -0
  21. agentle/agents/apis/no_authentication.py +27 -0
  22. agentle/agents/apis/oauth2_authentication.py +111 -0
  23. agentle/agents/apis/oauth2_grant_type.py +12 -0
  24. agentle/agents/apis/object_schema.py +86 -1
  25. agentle/agents/apis/params/__init__.py +10 -1
  26. agentle/agents/apis/params/boolean_param.py +44 -0
  27. agentle/agents/apis/params/number_param.py +56 -0
  28. agentle/agents/apis/rate_limit_error.py +7 -0
  29. agentle/agents/apis/rate_limiter.py +57 -0
  30. agentle/agents/apis/request_config.py +126 -4
  31. agentle/agents/apis/request_hook.py +16 -0
  32. agentle/agents/apis/response_cache.py +49 -0
  33. agentle/agents/apis/retry_strategy.py +12 -0
  34. agentle/agents/whatsapp/human_delay_calculator.py +462 -0
  35. agentle/agents/whatsapp/models/audio_message.py +6 -4
  36. agentle/agents/whatsapp/models/key.py +2 -2
  37. agentle/agents/whatsapp/models/whatsapp_bot_config.py +375 -21
  38. agentle/agents/whatsapp/models/whatsapp_response_base.py +31 -0
  39. agentle/agents/whatsapp/models/whatsapp_webhook_payload.py +5 -1
  40. agentle/agents/whatsapp/providers/base/whatsapp_provider.py +51 -0
  41. agentle/agents/whatsapp/providers/evolution/evolution_api_provider.py +237 -10
  42. agentle/agents/whatsapp/providers/meta/meta_whatsapp_provider.py +126 -0
  43. agentle/agents/whatsapp/v2/batch_processor_manager.py +4 -0
  44. agentle/agents/whatsapp/v2/bot_config.py +188 -0
  45. agentle/agents/whatsapp/v2/message_limit.py +9 -0
  46. agentle/agents/whatsapp/v2/payload.py +0 -0
  47. agentle/agents/whatsapp/v2/whatsapp_bot.py +13 -0
  48. agentle/agents/whatsapp/v2/whatsapp_cloud_api_provider.py +0 -0
  49. agentle/agents/whatsapp/v2/whatsapp_provider.py +0 -0
  50. agentle/agents/whatsapp/whatsapp_bot.py +827 -45
  51. agentle/generations/providers/google/adapters/generate_generate_content_response_to_generation_adapter.py +13 -10
  52. agentle/generations/providers/google/google_generation_provider.py +35 -5
  53. agentle/generations/providers/openrouter/_adapters/openrouter_message_to_generated_assistant_message_adapter.py +35 -1
  54. agentle/mcp/servers/stdio_mcp_server.py +23 -4
  55. agentle/parsing/parsers/docx.py +8 -0
  56. agentle/parsing/parsers/file_parser.py +4 -0
  57. agentle/parsing/parsers/pdf.py +7 -1
  58. agentle/storage/__init__.py +11 -0
  59. agentle/storage/file_storage_manager.py +44 -0
  60. agentle/storage/local_file_storage_manager.py +122 -0
  61. agentle/storage/s3_file_storage_manager.py +124 -0
  62. agentle/tts/audio_format.py +6 -0
  63. agentle/tts/elevenlabs_tts_provider.py +108 -0
  64. agentle/tts/output_format_type.py +26 -0
  65. agentle/tts/speech_config.py +14 -0
  66. agentle/tts/speech_result.py +15 -0
  67. agentle/tts/tts_provider.py +16 -0
  68. agentle/tts/voice_settings.py +30 -0
  69. agentle/utils/parse_streaming_json.py +39 -13
  70. agentle/voice_cloning/__init__.py +0 -0
  71. agentle/voice_cloning/voice_cloner.py +0 -0
  72. agentle/web/extractor.py +282 -148
  73. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/METADATA +1 -1
  74. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/RECORD +78 -39
  75. agentle/tts/real_time/definitions/audio_data.py +0 -20
  76. agentle/tts/real_time/definitions/speech_config.py +0 -27
  77. agentle/tts/real_time/definitions/speech_result.py +0 -14
  78. agentle/tts/real_time/definitions/tts_stream_chunk.py +0 -15
  79. agentle/tts/real_time/definitions/voice_gender.py +0 -9
  80. agentle/tts/real_time/definitions/voice_info.py +0 -18
  81. agentle/tts/real_time/real_time_speech_to_text_provider.py +0 -66
  82. /agentle/{tts/real_time → agents/whatsapp/v2}/__init__.py +0 -0
  83. /agentle/{tts/real_time/definitions/__init__.py → agents/whatsapp/v2/in_memory_batch_processor_manager.py} +0 -0
  84. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/WHEEL +0 -0
  85. {agentle-0.9.4.dist-info → agentle-0.9.28.dist-info}/licenses/LICENSE +0 -0
@@ -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
- from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence
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
- Represents a collection of related API endpoints with shared configuration.
56
+ Enhanced API collection with comprehensive features.
25
57
 
26
- This class groups multiple endpoints that share common settings like base URL,
27
- authentication headers, and request configuration. It provides a convenient
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 (uses info.title from spec if not provided)
72
- description: Override the API description (uses info.description from spec if not provided)
73
- base_url_override: Override the base URL (uses first server from spec if not provided)
74
- headers: Additional headers to include with all requests
75
- request_config: Request configuration for all endpoints
76
- include_operations: List of operationIds to include (if None, includes all)
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
- def add_endpoint(self, endpoint: Endpoint) -> None:
146
- """
147
- Add an endpoint to this API.
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
- Args:
150
- endpoint: Endpoint to add
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
- Get an endpoint by name.
321
+ Execute multiple requests in batch.
159
322
 
160
323
  Args:
161
- name: Name of the endpoint to find
324
+ requests: List of (endpoint_name, kwargs) tuples
162
325
 
163
326
  Returns:
164
- Endpoint if found, None otherwise
327
+ List of results in the same order as requests
165
328
  """
166
- for endpoint in self.endpoints:
167
- if endpoint.name == name:
168
- return endpoint
169
- return None
170
-
171
- def to_tools(self) -> Sequence[Tool[Any]]:
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
- Convert all endpoints in this API to Tool instances.
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
- List of Tool instances for all endpoints
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 include/exclude filters
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
- endpoint_name: str = cast(
279
- str,
280
- (
281
- operation_id
282
- or f"{method}_{path.replace('/', '_').replace('{', '').replace('}', '')}"
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 (simplified - doesn't handle complex nested refs)
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, # Treat cookies as headers
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, then any other content type
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
@@ -0,0 +1,11 @@
1
+ """API key location types."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class ApiKeyLocation(StrEnum):
7
+ """Where to place API key."""
8
+
9
+ HEADER = "header"
10
+ QUERY = "query"
11
+ COOKIE = "cookie"