pyrestkit 0.0.0__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.
- pyrestkit/__init__.py +35 -0
- pyrestkit/ai/__init__.py +17 -0
- pyrestkit/ai/analyzer.py +137 -0
- pyrestkit/ai/client.py +101 -0
- pyrestkit/ai/config/__init__.py +5 -0
- pyrestkit/ai/config/ai_config.py +200 -0
- pyrestkit/ai/exceptions.py +22 -0
- pyrestkit/ai/generators/__init__.py +0 -0
- pyrestkit/ai/models.py +44 -0
- pyrestkit/ai/parsers/__init__.py +0 -0
- pyrestkit/ai/prompts/failure_analysis.md +21 -0
- pyrestkit/ai/provider.py +58 -0
- pyrestkit/ai/providers/__init__.py +21 -0
- pyrestkit/ai/providers/anthropic.py +85 -0
- pyrestkit/ai/providers/azure_openai.py +84 -0
- pyrestkit/ai/providers/base.py +39 -0
- pyrestkit/ai/providers/bedrock.py +70 -0
- pyrestkit/ai/providers/cohere.py +82 -0
- pyrestkit/ai/providers/gemini.py +113 -0
- pyrestkit/ai/providers/groq.py +81 -0
- pyrestkit/ai/providers/mistral.py +88 -0
- pyrestkit/ai/providers/ollama.py +82 -0
- pyrestkit/ai/providers/openai.py +124 -0
- pyrestkit/ai/utils/__init__.py +0 -0
- pyrestkit/ai/utils/prompt_loader.py +52 -0
- pyrestkit/assertions/__init__.py +7 -0
- pyrestkit/assertions/assertion_exception.py +4 -0
- pyrestkit/assertions/response_assertions.py +181 -0
- pyrestkit/auth/__init__.py +11 -0
- pyrestkit/auth/auth_strategy.py +14 -0
- pyrestkit/auth/authentication_manager.py +35 -0
- pyrestkit/auth/strategies/__init__.py +5 -0
- pyrestkit/auth/strategies/api_key_auth.py +18 -0
- pyrestkit/auth/strategies/basic_auth.py +24 -0
- pyrestkit/auth/strategies/bearer_auth.py +17 -0
- pyrestkit/auth/token_cache.py +44 -0
- pyrestkit/auth/token_manager.py +32 -0
- pyrestkit/auth/token_provider.py +12 -0
- pyrestkit/auth/token_response.py +13 -0
- pyrestkit/builder/__init__.py +5 -0
- pyrestkit/builder/fluent_request_builder.py +167 -0
- pyrestkit/clients/__init__.py +7 -0
- pyrestkit/clients/base_client.py +68 -0
- pyrestkit/clients/user_client.py +66 -0
- pyrestkit/config/__init__.py +5 -0
- pyrestkit/config/config.py +97 -0
- pyrestkit/constants/__init__.py +0 -0
- pyrestkit/constants/content_types.py +0 -0
- pyrestkit/constants/headers.py +0 -0
- pyrestkit/constants/status_codes.py +0 -0
- pyrestkit/core/__init__.py +13 -0
- pyrestkit/core/api_client.py +129 -0
- pyrestkit/core/logger.py +41 -0
- pyrestkit/core/request_builder.py +45 -0
- pyrestkit/core/request_executor.py +64 -0
- pyrestkit/core/request_logger.py +0 -0
- pyrestkit/core/response_logger.py +0 -0
- pyrestkit/core/session_manager.py +19 -0
- pyrestkit/database/__init__.py +0 -0
- pyrestkit/endpoints/__init__.py +5 -0
- pyrestkit/endpoints/base_endpoints.py +32 -0
- pyrestkit/endpoints/order_endpoints.py +9 -0
- pyrestkit/endpoints/payment_endpoints.py +5 -0
- pyrestkit/endpoints/user_endpoints.py +48 -0
- pyrestkit/exceptions/__init__.py +21 -0
- pyrestkit/exceptions/api_exception.py +8 -0
- pyrestkit/exceptions/authentication_exception.py +10 -0
- pyrestkit/exceptions/configuration_exception.py +10 -0
- pyrestkit/exceptions/exception_mapper.py +32 -0
- pyrestkit/exceptions/network_exception.py +10 -0
- pyrestkit/exceptions/response_exception.py +10 -0
- pyrestkit/exceptions/serialization_exception.py +10 -0
- pyrestkit/exceptions/validation_exception.py +10 -0
- pyrestkit/factories/__init__.py +5 -0
- pyrestkit/factories/base_factory.py +25 -0
- pyrestkit/factories/user_factory.py +37 -0
- pyrestkit/hooks/__init__.py +5 -0
- pyrestkit/hooks/hook.py +27 -0
- pyrestkit/hooks/hook_manager.py +39 -0
- pyrestkit/hooks/request_hook.py +18 -0
- pyrestkit/hooks/response_hook.py +17 -0
- pyrestkit/hooks/timing_hook.py +32 -0
- pyrestkit/models/__init__.py +8 -0
- pyrestkit/models/base_response.py +11 -0
- pyrestkit/models/request/__init__.py +7 -0
- pyrestkit/models/request/create_user_request.py +11 -0
- pyrestkit/models/request/update_user_request.py +10 -0
- pyrestkit/models/response/__init__.py +5 -0
- pyrestkit/models/response/create_user_response.py +26 -0
- pyrestkit/models/response/get_user_response.py +28 -0
- pyrestkit/models/response/user_response.py +12 -0
- pyrestkit/pipeline/__init__.py +5 -0
- pyrestkit/pipeline/middleware.py +18 -0
- pyrestkit/pipeline/middleware_chain.py +11 -0
- pyrestkit/pipeline/pipeline.py +27 -0
- pyrestkit/pipeline/request_context.py +26 -0
- pyrestkit/response/__init__.py +8 -0
- pyrestkit/response/framework_response.py +271 -0
- pyrestkit/response/response_body.py +124 -0
- pyrestkit/retry/__init__.py +5 -0
- pyrestkit/retry/backoff.py +32 -0
- pyrestkit/retry/retry_handler.py +52 -0
- pyrestkit/retry/retry_policy.py +33 -0
- pyrestkit/serializers/__init__.py +0 -0
- pyrestkit/serializers/response_mapper.py +25 -0
- pyrestkit/types/__init__.py +0 -0
- pyrestkit/types/model_protocol.py +17 -0
- pyrestkit/utils/__init__.py +0 -0
- pyrestkit/validators/__init__.py +7 -0
- pyrestkit/validators/response_validator.py +57 -0
- pyrestkit/validators/schema_validator.py +33 -0
- pyrestkit-0.0.0.dist-info/METADATA +741 -0
- pyrestkit-0.0.0.dist-info/RECORD +115 -0
- pyrestkit-0.0.0.dist-info/WHEEL +5 -0
- pyrestkit-0.0.0.dist-info/top_level.txt +1 -0
pyrestkit/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PyRestKit
|
|
3
|
+
|
|
4
|
+
Modern Python REST API Automation Toolkit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pyrestkit.auth.strategies.api_key_auth import ApiKeyAuth
|
|
8
|
+
from pyrestkit.auth.strategies.basic_auth import BasicAuth
|
|
9
|
+
from pyrestkit.auth.strategies.bearer_auth import BearerAuth
|
|
10
|
+
from pyrestkit.clients.user_client import UserClient
|
|
11
|
+
from pyrestkit.config.config import ConfigManager
|
|
12
|
+
from pyrestkit.core.api_client import APIClient
|
|
13
|
+
from pyrestkit.core.session_manager import SessionManager
|
|
14
|
+
from pyrestkit.factories.user_factory import UserFactory
|
|
15
|
+
from pyrestkit.models.request.create_user_request import CreateUserRequest
|
|
16
|
+
from pyrestkit.models.request.update_user_request import UpdateUserRequest
|
|
17
|
+
from pyrestkit.validators.response_validator import ResponseValidator
|
|
18
|
+
from pyrestkit.validators.schema_validator import SchemaValidator
|
|
19
|
+
|
|
20
|
+
__version__ = "1.2.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"APIClient",
|
|
24
|
+
"SessionManager",
|
|
25
|
+
"ConfigManager",
|
|
26
|
+
"BearerAuth",
|
|
27
|
+
"BasicAuth",
|
|
28
|
+
"ApiKeyAuth",
|
|
29
|
+
"UserClient",
|
|
30
|
+
"CreateUserRequest",
|
|
31
|
+
"UpdateUserRequest",
|
|
32
|
+
"ResponseValidator",
|
|
33
|
+
"SchemaValidator",
|
|
34
|
+
"UserFactory",
|
|
35
|
+
]
|
pyrestkit/ai/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pyrestkit.ai.analyzer import FailureAnalyzer, parse_failure_analysis
|
|
2
|
+
from pyrestkit.ai.client import AIClient, available_providers, register_provider
|
|
3
|
+
from pyrestkit.ai.config import AIConfig
|
|
4
|
+
from pyrestkit.ai.models import FailureAnalysis, FailureContext
|
|
5
|
+
from pyrestkit.ai.provider import AIProvider
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AIClient",
|
|
9
|
+
"AIConfig",
|
|
10
|
+
"AIProvider",
|
|
11
|
+
"FailureAnalysis",
|
|
12
|
+
"FailureAnalyzer",
|
|
13
|
+
"FailureContext",
|
|
14
|
+
"available_providers",
|
|
15
|
+
"parse_failure_analysis",
|
|
16
|
+
"register_provider",
|
|
17
|
+
]
|
pyrestkit/ai/analyzer.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from pyrestkit.ai.client import AIClient
|
|
7
|
+
from pyrestkit.ai.models import FailureAnalysis, FailureContext
|
|
8
|
+
from pyrestkit.ai.utils.prompt_loader import PromptLoader
|
|
9
|
+
|
|
10
|
+
FAILURE_ANALYZER_SYSTEM_PROMPT = (
|
|
11
|
+
"You are an expert Python API automation engineer. "
|
|
12
|
+
"Return concise, actionable failure analysis as JSON only."
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FailureAnalyzer:
|
|
17
|
+
"""
|
|
18
|
+
Generates structured analysis and fix suggestions for test failures.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
client: AIClient,
|
|
24
|
+
prompt_loader: PromptLoader | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self._client = client
|
|
27
|
+
self._prompt_loader = prompt_loader or PromptLoader.default()
|
|
28
|
+
|
|
29
|
+
def build_prompt(
|
|
30
|
+
self,
|
|
31
|
+
context: FailureContext,
|
|
32
|
+
) -> str:
|
|
33
|
+
return self._prompt_loader.render(
|
|
34
|
+
"failure_analysis.md",
|
|
35
|
+
context=json.dumps(
|
|
36
|
+
context.to_dict(),
|
|
37
|
+
indent=2,
|
|
38
|
+
sort_keys=True,
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def analyze(
|
|
43
|
+
self,
|
|
44
|
+
context: FailureContext,
|
|
45
|
+
) -> FailureAnalysis:
|
|
46
|
+
raw_response = self._client.complete(
|
|
47
|
+
self.build_prompt(context),
|
|
48
|
+
system_prompt=FAILURE_ANALYZER_SYSTEM_PROMPT,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return parse_failure_analysis(raw_response)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_failure_analysis(
|
|
55
|
+
raw_response: str,
|
|
56
|
+
) -> FailureAnalysis:
|
|
57
|
+
try:
|
|
58
|
+
data = json.loads(_extract_json(raw_response))
|
|
59
|
+
except (json.JSONDecodeError, ValueError):
|
|
60
|
+
return FailureAnalysis(
|
|
61
|
+
summary=raw_response.strip(),
|
|
62
|
+
likely_cause="",
|
|
63
|
+
recommended_fix="",
|
|
64
|
+
raw_response=raw_response,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if not isinstance(data, dict):
|
|
68
|
+
return FailureAnalysis(
|
|
69
|
+
summary=str(data).strip(),
|
|
70
|
+
likely_cause="",
|
|
71
|
+
recommended_fix="",
|
|
72
|
+
raw_response=raw_response,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return FailureAnalysis(
|
|
76
|
+
summary=str(data.get("summary", "")).strip(),
|
|
77
|
+
likely_cause=str(data.get("likely_cause", "")).strip(),
|
|
78
|
+
recommended_fix=str(data.get("recommended_fix", "")).strip(),
|
|
79
|
+
suggested_patch=_optional_text(data.get("suggested_patch")),
|
|
80
|
+
confidence=_confidence(data.get("confidence", 0.0)),
|
|
81
|
+
raw_response=raw_response,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _extract_json(
|
|
86
|
+
value: str,
|
|
87
|
+
) -> str:
|
|
88
|
+
text = value.strip()
|
|
89
|
+
|
|
90
|
+
if text.startswith("```"):
|
|
91
|
+
lines = text.splitlines()
|
|
92
|
+
|
|
93
|
+
if lines and lines[0].startswith("```"):
|
|
94
|
+
lines = lines[1:]
|
|
95
|
+
|
|
96
|
+
if lines and lines[-1].startswith("```"):
|
|
97
|
+
lines = lines[:-1]
|
|
98
|
+
|
|
99
|
+
text = "\n".join(lines).strip()
|
|
100
|
+
|
|
101
|
+
if text.startswith("{") and text.endswith("}"):
|
|
102
|
+
return text
|
|
103
|
+
|
|
104
|
+
start = text.find("{")
|
|
105
|
+
end = text.rfind("}")
|
|
106
|
+
|
|
107
|
+
if start == -1 or end == -1 or end <= start:
|
|
108
|
+
raise ValueError("No JSON object found.")
|
|
109
|
+
|
|
110
|
+
return text[start : end + 1]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _optional_text(
|
|
114
|
+
value: Any,
|
|
115
|
+
) -> str | None:
|
|
116
|
+
if value is None:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
text = str(value).strip()
|
|
120
|
+
return text or None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _confidence(
|
|
124
|
+
value: Any,
|
|
125
|
+
) -> float:
|
|
126
|
+
try:
|
|
127
|
+
confidence = float(value)
|
|
128
|
+
except (TypeError, ValueError):
|
|
129
|
+
return 0.0
|
|
130
|
+
|
|
131
|
+
if confidence < 0.0:
|
|
132
|
+
return 0.0
|
|
133
|
+
|
|
134
|
+
if confidence > 1.0:
|
|
135
|
+
return 1.0
|
|
136
|
+
|
|
137
|
+
return confidence
|
pyrestkit/ai/client.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from pyrestkit.ai.config import AIConfig
|
|
6
|
+
from pyrestkit.ai.exceptions import AIConfigurationError
|
|
7
|
+
from pyrestkit.ai.provider import AIProvider
|
|
8
|
+
from pyrestkit.ai.providers import (
|
|
9
|
+
AnthropicProvider,
|
|
10
|
+
AzureOpenAIProvider,
|
|
11
|
+
BedrockProvider,
|
|
12
|
+
CohereProvider,
|
|
13
|
+
GeminiProvider,
|
|
14
|
+
GroqProvider,
|
|
15
|
+
MistralProvider,
|
|
16
|
+
OllamaProvider,
|
|
17
|
+
OpenAIResponsesProvider,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
ProviderFactory = Callable[[], AIProvider]
|
|
21
|
+
|
|
22
|
+
_PROVIDER_FACTORIES: dict[str, ProviderFactory] = {
|
|
23
|
+
"openai": OpenAIResponsesProvider,
|
|
24
|
+
"ollama": OllamaProvider,
|
|
25
|
+
"anthropic": AnthropicProvider,
|
|
26
|
+
"gemini": GeminiProvider,
|
|
27
|
+
"azure-openai": AzureOpenAIProvider,
|
|
28
|
+
"azure_openai": AzureOpenAIProvider,
|
|
29
|
+
"cohere": CohereProvider,
|
|
30
|
+
"mistral": MistralProvider,
|
|
31
|
+
"groq": GroqProvider,
|
|
32
|
+
"bedrock": BedrockProvider,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AIClient:
|
|
37
|
+
"""
|
|
38
|
+
Small facade that keeps framework code independent from provider details.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
config: AIConfig,
|
|
44
|
+
provider: AIProvider | None = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._config = config
|
|
47
|
+
self._provider = provider or create_provider(config.provider)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def config(self) -> AIConfig:
|
|
51
|
+
return self._config
|
|
52
|
+
|
|
53
|
+
def complete(
|
|
54
|
+
self,
|
|
55
|
+
prompt: str,
|
|
56
|
+
*,
|
|
57
|
+
system_prompt: str | None = None,
|
|
58
|
+
) -> str:
|
|
59
|
+
if not prompt.strip():
|
|
60
|
+
raise ValueError("Prompt cannot be empty.")
|
|
61
|
+
|
|
62
|
+
return self._provider.complete(
|
|
63
|
+
prompt,
|
|
64
|
+
config=self._config,
|
|
65
|
+
system_prompt=system_prompt,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def create_provider(
|
|
70
|
+
provider_name: str,
|
|
71
|
+
) -> AIProvider:
|
|
72
|
+
provider = provider_name.strip().lower()
|
|
73
|
+
|
|
74
|
+
if not provider:
|
|
75
|
+
raise AIConfigurationError("AI provider name cannot be empty.")
|
|
76
|
+
|
|
77
|
+
factory = _PROVIDER_FACTORIES.get(provider)
|
|
78
|
+
|
|
79
|
+
if factory is None:
|
|
80
|
+
raise AIConfigurationError(
|
|
81
|
+
f"No AI provider registered for '{provider_name}'. "
|
|
82
|
+
"Install the provider package or register a custom provider."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return factory()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def register_provider(
|
|
89
|
+
provider_name: str,
|
|
90
|
+
factory: ProviderFactory,
|
|
91
|
+
) -> None:
|
|
92
|
+
provider = provider_name.strip().lower()
|
|
93
|
+
|
|
94
|
+
if not provider:
|
|
95
|
+
raise AIConfigurationError("AI provider name cannot be empty.")
|
|
96
|
+
|
|
97
|
+
_PROVIDER_FACTORIES[provider] = factory
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def available_providers() -> tuple[str, ...]:
|
|
101
|
+
return tuple(sorted(_PROVIDER_FACTORIES))
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class AIConfig:
|
|
11
|
+
"""
|
|
12
|
+
Provider-agnostic configuration for AI integrations.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
provider: str
|
|
16
|
+
model: str
|
|
17
|
+
|
|
18
|
+
api_key: str | None = None
|
|
19
|
+
base_url: str | None = None
|
|
20
|
+
|
|
21
|
+
temperature: float = 0.2
|
|
22
|
+
max_tokens: int | None = None
|
|
23
|
+
|
|
24
|
+
timeout: int = 60
|
|
25
|
+
|
|
26
|
+
organization: str | None = None
|
|
27
|
+
|
|
28
|
+
headers: Mapping[str, str] = field(default_factory=dict)
|
|
29
|
+
|
|
30
|
+
def __post_init__(self) -> None:
|
|
31
|
+
provider = self.provider.strip().lower()
|
|
32
|
+
model = self.model.strip()
|
|
33
|
+
|
|
34
|
+
object.__setattr__(self, "provider", provider)
|
|
35
|
+
object.__setattr__(self, "model", model)
|
|
36
|
+
object.__setattr__(self, "api_key", _blank_to_none(self.api_key))
|
|
37
|
+
object.__setattr__(self, "base_url", _blank_to_none(self.base_url))
|
|
38
|
+
object.__setattr__(self, "organization", _blank_to_none(self.organization))
|
|
39
|
+
object.__setattr__(self, "headers", dict(self.headers))
|
|
40
|
+
|
|
41
|
+
if not provider:
|
|
42
|
+
raise ValueError("Provider cannot be empty.")
|
|
43
|
+
|
|
44
|
+
if not model:
|
|
45
|
+
raise ValueError("Model cannot be empty.")
|
|
46
|
+
|
|
47
|
+
if not 0.0 <= self.temperature <= 2.0:
|
|
48
|
+
raise ValueError("Temperature must be between 0.0 and 2.0.")
|
|
49
|
+
|
|
50
|
+
if self.max_tokens is not None and self.max_tokens <= 0:
|
|
51
|
+
raise ValueError("max_tokens must be greater than zero.")
|
|
52
|
+
|
|
53
|
+
if self.timeout <= 0:
|
|
54
|
+
raise ValueError("timeout must be greater than zero.")
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_mapping(
|
|
58
|
+
cls,
|
|
59
|
+
values: Mapping[str, Any],
|
|
60
|
+
) -> AIConfig:
|
|
61
|
+
"""
|
|
62
|
+
Build AI configuration from a flat mapping or an ``ai`` section.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
if not isinstance(values, Mapping):
|
|
66
|
+
raise ValueError("AI configuration must be a mapping.")
|
|
67
|
+
|
|
68
|
+
source = values.get("ai", values)
|
|
69
|
+
|
|
70
|
+
if not isinstance(source, Mapping):
|
|
71
|
+
raise ValueError("AI configuration must be a mapping.")
|
|
72
|
+
|
|
73
|
+
provider = _required_str(source, "provider")
|
|
74
|
+
model = _required_str(source, "model")
|
|
75
|
+
timeout = _optional_int(source, "timeout", default=60)
|
|
76
|
+
|
|
77
|
+
if timeout is None:
|
|
78
|
+
raise ValueError("timeout is required.")
|
|
79
|
+
|
|
80
|
+
return cls(
|
|
81
|
+
provider=provider,
|
|
82
|
+
model=model,
|
|
83
|
+
api_key=_optional_str(source, "api_key"),
|
|
84
|
+
base_url=_optional_str(source, "base_url"),
|
|
85
|
+
temperature=_optional_float(source, "temperature", default=0.2),
|
|
86
|
+
max_tokens=_optional_int(source, "max_tokens"),
|
|
87
|
+
timeout=timeout,
|
|
88
|
+
organization=_optional_str(source, "organization"),
|
|
89
|
+
headers=_optional_headers(source),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_env(
|
|
94
|
+
cls,
|
|
95
|
+
prefix: str = "PYRESTKIT_AI_",
|
|
96
|
+
) -> AIConfig:
|
|
97
|
+
"""
|
|
98
|
+
Build AI configuration from environment variables.
|
|
99
|
+
|
|
100
|
+
Expected variables:
|
|
101
|
+
|
|
102
|
+
- PYRESTKIT_AI_PROVIDER
|
|
103
|
+
- PYRESTKIT_AI_MODEL
|
|
104
|
+
- PYRESTKIT_AI_API_KEY
|
|
105
|
+
- PYRESTKIT_AI_BASE_URL
|
|
106
|
+
- PYRESTKIT_AI_TEMPERATURE
|
|
107
|
+
- PYRESTKIT_AI_MAX_TOKENS
|
|
108
|
+
- PYRESTKIT_AI_TIMEOUT
|
|
109
|
+
- PYRESTKIT_AI_ORGANIZATION
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
model = os.getenv(f"{prefix}MODEL")
|
|
113
|
+
|
|
114
|
+
if model is None:
|
|
115
|
+
raise ValueError(f"{prefix}MODEL is required.")
|
|
116
|
+
|
|
117
|
+
mapping: dict[str, Any] = {
|
|
118
|
+
"provider": os.getenv(f"{prefix}PROVIDER", "openai"),
|
|
119
|
+
"model": model,
|
|
120
|
+
"api_key": os.getenv(f"{prefix}API_KEY"),
|
|
121
|
+
"base_url": os.getenv(f"{prefix}BASE_URL"),
|
|
122
|
+
"temperature": os.getenv(f"{prefix}TEMPERATURE", "0.2"),
|
|
123
|
+
"max_tokens": os.getenv(f"{prefix}MAX_TOKENS"),
|
|
124
|
+
"timeout": os.getenv(f"{prefix}TIMEOUT", "60"),
|
|
125
|
+
"organization": os.getenv(f"{prefix}ORGANIZATION"),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return cls.from_mapping(mapping)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _blank_to_none(value: str | None) -> str | None:
|
|
132
|
+
if value is None:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
value = value.strip()
|
|
136
|
+
return value or None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _required_str(
|
|
140
|
+
values: Mapping[str, Any],
|
|
141
|
+
key: str,
|
|
142
|
+
) -> str:
|
|
143
|
+
value = values.get(key)
|
|
144
|
+
|
|
145
|
+
if value is None:
|
|
146
|
+
raise ValueError(f"{key} is required.")
|
|
147
|
+
|
|
148
|
+
return str(value)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _optional_str(
|
|
152
|
+
values: Mapping[str, Any],
|
|
153
|
+
key: str,
|
|
154
|
+
) -> str | None:
|
|
155
|
+
value = values.get(key)
|
|
156
|
+
|
|
157
|
+
if value is None:
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
return str(value)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _optional_float(
|
|
164
|
+
values: Mapping[str, Any],
|
|
165
|
+
key: str,
|
|
166
|
+
default: float,
|
|
167
|
+
) -> float:
|
|
168
|
+
value = values.get(key, default)
|
|
169
|
+
|
|
170
|
+
if value is None or value == "":
|
|
171
|
+
return default
|
|
172
|
+
|
|
173
|
+
return float(value)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _optional_int(
|
|
177
|
+
values: Mapping[str, Any],
|
|
178
|
+
key: str,
|
|
179
|
+
default: int | None = None,
|
|
180
|
+
) -> int | None:
|
|
181
|
+
value = values.get(key, default)
|
|
182
|
+
|
|
183
|
+
if value is None or value == "":
|
|
184
|
+
return default
|
|
185
|
+
|
|
186
|
+
return int(value)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _optional_headers(
|
|
190
|
+
values: Mapping[str, Any],
|
|
191
|
+
) -> Mapping[str, str]:
|
|
192
|
+
headers = values.get("headers", {})
|
|
193
|
+
|
|
194
|
+
if headers is None:
|
|
195
|
+
return {}
|
|
196
|
+
|
|
197
|
+
if not isinstance(headers, Mapping):
|
|
198
|
+
raise ValueError("headers must be a mapping.")
|
|
199
|
+
|
|
200
|
+
return {str(key): str(value) for key, value in headers.items()}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class AIError(Exception):
|
|
2
|
+
"""
|
|
3
|
+
Base exception for AI integration failures.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AIConfigurationError(AIError):
|
|
8
|
+
"""
|
|
9
|
+
Raised when AI provider configuration is missing or invalid.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AIProviderError(AIError):
|
|
14
|
+
"""
|
|
15
|
+
Raised when an AI provider request fails.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AIResponseParseError(AIError):
|
|
20
|
+
"""
|
|
21
|
+
Raised when an AI response cannot be parsed into the expected shape.
|
|
22
|
+
"""
|
|
File without changes
|
pyrestkit/ai/models.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class FailureContext:
|
|
10
|
+
"""
|
|
11
|
+
Normalized failure details that can be sent to an AI analyzer.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
test_name: str
|
|
15
|
+
file_path: str | None = None
|
|
16
|
+
line_number: int | None = None
|
|
17
|
+
error_type: str | None = None
|
|
18
|
+
error_message: str | None = None
|
|
19
|
+
traceback: str | None = None
|
|
20
|
+
assertion: str | None = None
|
|
21
|
+
expected: str | None = None
|
|
22
|
+
actual: str | None = None
|
|
23
|
+
request_method: str | None = None
|
|
24
|
+
request_url: str | None = None
|
|
25
|
+
response_status: int | None = None
|
|
26
|
+
response_body: str | None = None
|
|
27
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
def to_dict(self) -> dict[str, Any]:
|
|
30
|
+
return asdict(self)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class FailureAnalysis:
|
|
35
|
+
"""
|
|
36
|
+
Structured AI output for a failing automation test.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
summary: str
|
|
40
|
+
likely_cause: str
|
|
41
|
+
recommended_fix: str
|
|
42
|
+
suggested_patch: str | None = None
|
|
43
|
+
confidence: float = 0.0
|
|
44
|
+
raw_response: str | None = None
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Analyze this API automation failure and identify the smallest useful fix.
|
|
2
|
+
|
|
3
|
+
Return only a JSON object with this shape:
|
|
4
|
+
|
|
5
|
+
{
|
|
6
|
+
"summary": "one sentence failure summary",
|
|
7
|
+
"likely_cause": "most likely root cause",
|
|
8
|
+
"recommended_fix": "specific next action",
|
|
9
|
+
"suggested_patch": "unified diff if a safe patch is obvious, otherwise null",
|
|
10
|
+
"confidence": 0.0
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
Rules:
|
|
14
|
+
- Do not invent endpoint behavior that is not present in the context.
|
|
15
|
+
- Prefer fixing test data, assertions, schemas, or client code only when the context supports it.
|
|
16
|
+
- Keep suggested patches minimal and in unified diff format.
|
|
17
|
+
- Do not include secrets or credentials in the response.
|
|
18
|
+
|
|
19
|
+
Failure context:
|
|
20
|
+
|
|
21
|
+
$context
|
pyrestkit/ai/provider.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from pyrestkit.ai.config import AIConfig
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AIProvider(Protocol):
|
|
10
|
+
"""
|
|
11
|
+
Contract implemented by concrete AI providers.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def complete(
|
|
15
|
+
self,
|
|
16
|
+
prompt: str,
|
|
17
|
+
*,
|
|
18
|
+
config: AIConfig,
|
|
19
|
+
system_prompt: str | None = None,
|
|
20
|
+
) -> str:
|
|
21
|
+
"""
|
|
22
|
+
Return a text completion for the given prompt.
|
|
23
|
+
"""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class AIProviderCall:
|
|
29
|
+
prompt: str
|
|
30
|
+
system_prompt: str | None
|
|
31
|
+
config: AIConfig
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True)
|
|
35
|
+
class StaticAIProvider:
|
|
36
|
+
"""
|
|
37
|
+
Deterministic provider for tests and offline demos.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
response: str
|
|
41
|
+
calls: list[AIProviderCall] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
def complete(
|
|
44
|
+
self,
|
|
45
|
+
prompt: str,
|
|
46
|
+
*,
|
|
47
|
+
config: AIConfig,
|
|
48
|
+
system_prompt: str | None = None,
|
|
49
|
+
) -> str:
|
|
50
|
+
self.calls.append(
|
|
51
|
+
AIProviderCall(
|
|
52
|
+
prompt=prompt,
|
|
53
|
+
system_prompt=system_prompt,
|
|
54
|
+
config=config,
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
return self.response
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pyrestkit.ai.providers.anthropic import AnthropicProvider
|
|
2
|
+
from pyrestkit.ai.providers.azure_openai import AzureOpenAIProvider
|
|
3
|
+
from pyrestkit.ai.providers.bedrock import BedrockProvider
|
|
4
|
+
from pyrestkit.ai.providers.cohere import CohereProvider
|
|
5
|
+
from pyrestkit.ai.providers.gemini import GeminiProvider
|
|
6
|
+
from pyrestkit.ai.providers.groq import GroqProvider
|
|
7
|
+
from pyrestkit.ai.providers.mistral import MistralProvider
|
|
8
|
+
from pyrestkit.ai.providers.ollama import OllamaProvider
|
|
9
|
+
from pyrestkit.ai.providers.openai import OpenAIResponsesProvider
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"AnthropicProvider",
|
|
13
|
+
"AzureOpenAIProvider",
|
|
14
|
+
"BedrockProvider",
|
|
15
|
+
"CohereProvider",
|
|
16
|
+
"GeminiProvider",
|
|
17
|
+
"GroqProvider",
|
|
18
|
+
"MistralProvider",
|
|
19
|
+
"OllamaProvider",
|
|
20
|
+
"OpenAIResponsesProvider",
|
|
21
|
+
]
|